#!/bin/bash if [[ -z "${BASH_VERSINFO[0]:-}" ]]; then echo "SHELL ENV ERROR: You must use a BASH Shell to load this library. Exiting..." exit 255 fi export RTD_VERSION="2.05" : <<'Library_Documentation' :: R T D F u n c t i o n L i b r a r y :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::// Linux //::::::: :: Author(s): SLS, KLS, NB. Buffalo Center, IA & Avarua, Cook Islands :: Version: 2.01 :: :: :: Purpose: To collect and enable the use of code snippets in other scripts. :: To document these thoroughly so that they may be useful for learning BASH. :: Usage: call this file using the "source" statement in bash. :: :: This script is shared in the hopes that :: someone will find it useful. :: :: :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: .--. .---. .-. .---|--| .-. | A | .---. |~| .--. .--|===|Ch|---|_|--.__| S |--|:::| |~|-==-|==|---. |%%|NT2|oc|===| |~~|%%| C |--| |_|~|CATS| |___|-. | | |ah|===| |==| | I | |:::|=| | |GB|---|=| | | |ol| |_|__| | I |__| | | | | |___| | |~~|===|--|===|~|~~|%%|~~~|--|:::|=|~|----|==|---|=| ^--^---'--^---^-^--^--^---'--^---^-^-^-==-^--^---^-' :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: --- To see options to use this library type "bash _rtd_library --help" --- To see useful documentation on each function in this library in a Terminal or remote ssh: "bash _rtd_library --devhelp" --- To see useful documentation on each function in this library in GTK (local desktop): "bash _rtd_library --devhelp-gtk" Functions defined in this library: 1 add_gnome3_favorite_app ; Function to add a new favorite app to the gnome favorites bar. 2 create_physical_media_from_iso ; Interactively write an ISO to a USB drive, elevating privileges if needed. 3 dependency::check_hardware_virtualization_support ; Checks if the hardware supports virtualization (VT-x or AMD-V). 4 dependency::check_if_running_in_vm ; Checks if the system is running inside a virtual machine. 5 dependency::command_exists ; Checks if specified commands exist on the system and installs missing ones. 6 dependency::desktop ; Identifies the currently running desktop environment. 7 dependency::file ; Checks for a Bash library or script file and sources it. 8 dependency::os_linux ; Checks if the current operating system is Linux. 9 dependency::search_local ; Search common RTD paths for a requested file and source it when found. 10 dependency::virtualization ; Checks for required components for making virtual machines and verifies hardware support for virtualization. 11 dialog::check_menu_availability ; First discover what menu system is installed. Some systems use "dialog" and 12 dialog::copy_file_progress ; Displays a file copy progress dialog. 13 dialog::copy_with_progress ; Function to display a progress bar while copying a large file to a destination. 14 dialog::display_cmd_output ; Displays the output of a command in a dialog box. 15 dialog::display_error ; Displays an error notice using a dialog box. 16 dialog::display_notice ; Displays an information notice using a dialog box. 17 dialog::display_result ; Displays the contents of the variable "result" in a message box. 18 dialog::display_summary_message ; Function to read a variable "result" and display its contents. The purpose of this function 19 dialog::prompt_yes_no ; Displays a Yes/No prompt using a dialog box. 20 disk::reencrypt_device ; Back up a disk, LUKS-format it, and restore the data with new encryption. 21 display_gui ; Displays a GUI dialog box with the given message. 22 display_software_installation_choices_gtk ; Display a legacy GTK checklist of software recipes that can be installed. 23 fedora::get_fedora_netinst_iso_url ; Resolve the download URL for the Fedora Everything netinstall ISO version. 24 fedora::get_latest_release_version ; Fetch the latest Fedora release number from the official mirror index. 25 github::clone_repo_user ; Clone every repository for a given GitHub user into the current directory. 26 github::list_all_user_repositories ; List git clone URLs for all repositories owned by a GitHub user. 27 gnome::configure_dash_to_dock ; Configure GNOME Dash to Dock with the OEM default layout and behavior. 28 gnome::configure_dash_to_panel ; Configure GNOME Dash to Panel with OEM defaults. 29 gnome::configure_nautilus ; Apply OEM defaults to the Nautilus file manager. 30 gnome::organize_overlay_menu ; Organize GNOME application overlays into OEM-defined categories. 31 gnome::set_basic_extensions_enabled ; Enable the OEM set of GNOME Shell extensions for the current user. 32 gnome::set_better_font_smoothing_for_user ; Apply OEM font smoothing, antialiasing, and subpixel defaults in GNOME. 33 gnome::set_better_usability_for_user ; Apply GNOME desktop usability improvements for the OEM user. 34 gnome::set_ui_tweaks_for_user ; Apply the core OEM GNOME tweaks (Tilix, usability, Nautilus, extensions). 35 gnome::set_power_configuraton_for_user ; Set OEM power/Tracker defaults plus common GNOME UI status toggles. 36 gnome::set_startup_sound ; Configure the GNOME startup sound for the current user. 37 gnome::set_tilix_ui_tweaks_for_user ; Configure Tilix and GNOME Terminal profiles with imported OEM settings. 38 InstallSoftwareFromRepo ; Function to simplify the installation of software by including all display and 39 kvm::backup_all_running_vm ; Snapshot running KVM VMs and rsync their disks to the backup target. 40 kvm::cicd::_create_nocloud_iso ; Create a NoCloud seed ISO for cloud-init. 41 kvm::cicd::_render_extra_vars ; Render ansible-pull extra vars from selections. 42 kvm::cicd::_render_meta_data ; Build NoCloud meta-data for cloud-init. 43 kvm::cicd::_render_user_data ; Build cloud-init user-data to run ansible-pull. 44 kvm::cicd::configure_vm_with_ansible_pull ; Seed/attach CI/CD ISO and start a VM. 45 kvm::clone_server_template_vm ; Clone a server template VM and optionally run virt-sysprep or virt-customize. 46 kvm::clone_vdi_template_vm ; Clone a VDI template VM and optionally run virt-sysprep or virt-customize. 47 kvm::create_expanding_vm_hardisk ; Create a dynamic qcow2 VM disk with optional initial preallocation. 48 kvm::get_almalinux_boot_iso ; Download the AlmaLinux boot/minimal ISO for a given version if missing. 49 kvm::get_fedora_workstation_iso ; Download the specified/latest Fedora Workstation ISO into libvirt boot. 50 kvm::get_kali_iso ; Fetch the newest Kali installer ISO into /var/lib/libvirt/boot when absent. 51 kvm::get_vm_config_preferences ; Retrieves VM configuration values from a configuration file or defaults. 52 kvm::get_vm_size_preferences ; Loads VM sizing defaults, optionally prompting and persisting user changes. 53 kvm::get_zorin_iso ; Downloads the latest Zorin OS ISO file for KVM if not already present. 54 kvm::make_vm_now_from_debian_org ; Function to create a Debian KVM virtual michine disk and define a VM. This function should be 55 kvm::make_vm_now_from_fedora_org ; Function to create a Fedora KVM virtual michine disk and define a VM. This function should be 56 kvm::make_vm_now_from_kali_org ; Function to create a Debian KVM virtual michine disk and define a VM. This function should be 57 kvm::make_vm_now_from_microsoft ; Build a Windows KVM VM using supplied install media and guided prompts. 58 kvm::make_vm_now_from_opensuse_org ; Creates a SUSE KVM virtual machine disk and defines a VM. This function is intended to be 59 kvm::make_vm_now_from_redhat_com ; Function to create a Alma Linux KVM virtual michine disk and define a VM. This function should be 60 kvm::make_vm_now_from_ubuntu_com ; Automate Ubuntu VM creation with virt-install, ISO retrieval, and cloud-init. 61 kvm::make_vm_now_from_zorin ; Creates a KVM virtual machine template using a Zorin OS ISO. 62 kvm::mount_local_folder_to_guest ; Attach a host directory to a KVM guest via virtiofs using virsh attach. 63 kvm::util::read_common_options ; Function to read common options for KVM virtual machine creation. This function processes 64 kvm::util::read_distro_options ; Function to read distribution specific options for KVM virtual machine creation. This function processes 65 kvm::util::set_vm_name ; Set the VM name consistently across functions. 66 kvm::util::validate_vm_name ; Validate a VM name according to hostname rules. 67 library::apply_rtd_defaults ; Normalize core RTD paths/URLs with fallback defaults. 68 library::list_loaded_internal_functions ; List loaded RTD library functions in menu, doc, or dialog-friendly formats. 69 library::list_loaded_software_functions ; Function to list all software (_rtd_recipes) functions loaded... for debugging/support purposes. 70 main ; Entry point for executing library utility modes when run as a script. 71 mint::get_mint_lmde_iso ; Download the latest LMDE Cinnamon 64-bit ISO to libvirt boot and return it. 72 network::check_inet_access ; Ping known IPs (optionally waiting) to verify internet connectivity. 73 network::get_oem_test_ips ; Return configured test IPs honoring _OEM_TEST_IPS overrides. 74 network::rsync_download ; Rsync from a remote host with optional dialog-based progress feedback. 75 network::rsync_upload ; Rsync to a remote host with optional dialog-based progress feedback. 76 network::whats_my_external_ip ; Retrieve the external IP using multiple fallback HTTP services. 77 network::whats_my_ipinfo ; Query public IP plus geolocation data with retries and optional timeout. 78 oem::check_boot_splash_screen_enable ; OEM function to enable splash screen on boot if desktop is indicated in 79 oem::create_single_launcher ; Function to create a single .desktop file for an application. 80 oem::deploy_themes ; Downloads and deploys themes from GitHub. 81 oem::distro_release_upgrade ; Attempt a release upgrade on supported distros with optional interaction. 82 oem::register_all_tools ; Function to register all OEM scripts on the current device. 83 oem::register_wallpapers_for_gnome ; Register OEM wallpaper assets with GNOME so they appear in background selectors. 84 oem::remove_non_western_latin_fonts ; a simple function to remove known 85 oem::rtd_reset_default_environment_config ; Revert OEM setup tweaks (auto login/sudo/scripts) back to defaults. 86 oem::rtd_tools_make_launchers ; Function that creates a default set of launchers for the Linux GUI menu system. 87 oem::rtd_tools_make_launchers_from_locations ; Function that creates launchers using definitions from _locations.info. 88 oem::setup_brand_splash_screen ; Overlay custom branding text onto an installer splash image. 89 oem::setup_choices_server ; Function to display legacy installation options. This will install software that is usefull 90 pause::a_given_time ; Pause with a countdown for a given time and optional start/end messages. 91 pause::for_input ; Pause execution for user acknowledgement when a step fails or on demand. 92 rtd_oem_turn_on_gui_network_management ; Function to set NetworkManager by default to manage networking. 93 rtd_oem_ubuntu_auto_install_iso_builder ; Function to generate an edited ISO file from a folder. 94 rtd_server_setup_choices_productivity ; Function to display terminal software installation options. This will install 95 rtd_server_setup_choices_services ; Function to display server installation options. This will install software that is usefull 96 security::change_disk_pass ; Function to change the passphrase fo an encrypted storage device. 97 security::check_if_password_pOwned ; Check a proposed password against the Have I Been Pwned database. 98 security::configure_auditd ; Function to install and configure auditd for system auditing. 99 security::configure_clamav ; Install and harden ClamAV services, update signatures, and start them. 100 security::configure_fail2ban ; Install/configure Fail2Ban with a secure SSH jail and enable the service. 101 security::configure_rkhunter ; Function to install and configure RKHunter (Rootkit Hunter) for system security. 102 security::enable_firewall ; Enable UFW/firewalld, keep SSH open, and optionally add Minecraft/custom ports. 103 security::encrypt_disk_with_luks ; Wipe and LUKS2-encrypt a device, format ext4, and mount it securely. 104 security::ensure_admin ; Ensures the script is executed with administrative (root) privileges, allowing 105 security::harden_ubuntu ; Run an interactive checklist to apply selected Ubuntu hardening tasks. 106 security::install_AIDE ; Function to install and configure AIDE (Advanced Intrusion Detection Environment). 107 security::scan_for_malware ; Scan directories with ClamAV and quarantine infected files. 108 security::secure_password_policy ; Function to enforce a secure password policy on the system. 109 security::sysctl_hardening ; Write and apply a hardened sysctl config for network and memory settings. 110 gnome::set_ui_common_tweaks_for_user ; Convenience alias that calls gnome::set_ui_tweaks_for_user. 111 gnome::set_ui_corporate_crisp_tweaks_for_user ; Function to set UI look to appeal to teh corporate or business user. 112 gnome::set_ui_mac_tweaks_for_user ; Function to set UI look to resemble MAC for users accustomed to that. 113 gnome::set_ui_moca_tweaks_for_user ; Function to set UI look to be friendly on the eyes, limiting eye strain. 114 gnome::set_ui_tweak_no_media_error ; Function to handle missing media when attempting to set UI look to 115 gnome::set_ui_win10_tweaks_for_user ; Function to set UI look to resemble Windows 10 for users accustomed to that. 116 software::add_gnome_extensions ; Install specified GNOME Shell extensions via the RTD installer helper. 117 software::add_native_package ; Install one or more packages using the detected native package manager. 118 software::add_software_task ; Display executed task and echo ON/NOK based on sucess 119 software::check_native_package_dependency ; Ensure required packages are installed, installing missing ones automatically. 120 software::determine_package_name_from_cmd ; Function to determine the package name from the command line. 121 software::display_bundle_install_choices_gtk ; Show a GTK checklist of OEM software bundles that are not yet installed. 122 software::display_bundle_removal_choices_gtk ; Show a GTK checklist of previously installed OEM bundles for removal. 123 software::ensure_flatpak_package_managment ; Function to ensure that flatpak is installed and flathup enabled. 124 software::ensure_gnome_software_store_available ; Function to ensure that flatpak is installed and flathup enabled. 125 software::ensure_restricted_codecs ; simple function to ensure that restricted codecs are available on the system. 126 software::ensure_snap_package_managment ; Function to ensure that snap is installed. 127 software::is_native_package_available ; Function to chek if a package is available in whetever repository, and 128 software::is_native_package_installed ; Function to check if a piece of software is installed. This function will first check 129 software::list_bundles ; Function to list all software bundles defined in _rtd_recipes that are either installable or removable. 130 software::native_management_availability_check ; Wait until the native package manager locks clear before proceeding. 131 software::package_kit::add_package ; Function to simplify the installation of software by including all display and 132 software::package_kit::remove_package ; Function to simplify the installation of software by including all display and 133 software::remove_native_software_package ; Remove a named package using the system’s native package manager. 134 software::set_install_command ; Set the global _INSTCMD installer string based on the available package manager. 135 software::update_all_ui ; Refresh installed OEM UI customizations, icons, and cursors. 136 software::update_system ; Update native packages plus snaps/flatpaks with concise status output. 137 software::update_system_txt ; Text-mode update of native packages plus snaps/flatpaks with minimal adornment. 138 software::vendor_download_and_install ; Function by Nate Beaken to ease and make consistent the downloading of the non repository 139 ssh::ensure_key_installed_on_remote ; Verify/install the local SSH key on a remote host via ssh-copy-id. 140 ssh::ensure_user_ssh_key ; Ensure the current user has an SSH keypair, prompting to generate one. 141 system::_disable_service ; Stop and disable a service across systemd, SysVinit, or OpenRC. 142 system::_enable_service ; Function to enable and start a systemd service. 143 system::_turn_on_gui_network_management ; Configures the system to use NetworkManager for network configuration, 144 system::add_or_remove_login_script ; Add, remove, or toggle a .desktop autostart to run a script at login. 145 system::check_file_limits ; Report open file usage versus limits with a progress spinner. 146 system::check_required_variables ; Function to check for the presence and non-emptiness of a list of variables. 147 system::cleanup_and_finish ; Function to remove all temporary file locations left over from building 148 system::create_iso_image_debian ; Function to generate the new ISO file from the extracted and 149 system::create_physical_media_from_ubuntu_iso ; Write an Ubuntu ISO to USB with autoinstall support, prompting for media. 150 system::create_swapfile ; Function to create a swapfile and enable it automatically. 151 system::detect_session ; Detects the current session type (TTY, SSH, GUI, Wayland). 152 system::determine_logfile ; Resolve and create the appropriate log file path for the running script. 153 system::display_spinner ; Simple function to display a spinner in the terminal. 154 system::distribution_type ; Detect the host Linux distribution family (debian/redhat/suse/etc.). 155 system::download_and_manipulate_iso_debian ; Download Debian netinst ISO and rebuild it with preseed/customizations. 156 system::ensure_directory_exists ; Function to ensure a directory exists, creating it if necessary. 157 system::find_download_ubuntu_iso ; Locate a requested Ubuntu ISO and download it when not already cached. 158 system::find_vm_bridge ; Return the primary bridge interface name or default when none exists. 159 system::optimize_startup ; Apply basic boot/runtime tunables for responsiveness. 160 system::generate_autoyast_file ; Build an AutoYaST XML profile from provided options for SUSE installs. 161 system::generate_cloudconfig ; Create cloud-init user-data/meta-data and CIDATA ISO for automated VMs. 162 system::generate_ks_cfg_file ; Generates an installation configuration file (kickstart) for Fedora/Red Hat. 163 system::generate_report_disk_space_used_by_directory ; Function to generate a report for folders' disk space use. 164 system::get_Windows_Product_Key ; Function to retrieve a Windows product key from the BIOS. This assumes that the OEM 165 system::install_missing_commands ; Installs missing commands based on the distribution type. 166 system::io_on_notify_wait ; Function that, when called, will wait for a disk change 167 system::keyboard_set_layout ; Set keyboard layout persistently for GUI and TTY, optionally via dialog. 168 system::laptop_detect ; Determine whether the current host appears to be a laptop without relying on laptop-detect. 169 system::log_item ; Log timestamped messages to the resolved log file with caller context. 170 system::make_preseed_cfg ; Function to write out a debina preeseed file to a location requested by the first parameter. 171 system::make_system_recovery_partition ; Incomplete function to build an OEM rescue partition... 172 system::oem_autounlock_disk ; Configure the system to unlock an encrypted root volume automatically. 173 system::prepare_environment_for_iso_creation ; Function to check that all dependencies are available for manipulating the 174 system::preseed_to_ubuntu_ks_cfg ; Generates a preseed section [Ubuntu only] (ks.cfg). 175 system::process_vm_opt_args ; Parse VM option flags to override CPU, memory, disk, role, and task defaults. 176 system::read_config ; This function reads a configuration file with key-value pairs. 177 system::remove_old_kernel ; Function to remove all old linux kernels. 178 system::restart_sound ; Function to check for PipeWire or Pulse Audio and attempt to restart sound. 179 system::rtd_oem_find_live_release ; This function will return the URL for the version of Debian, Ubuntu server or Destktop requested. 180 system::rtd_oem_reseal ; Run distro-specific reseal steps then shut down for OEM handoff. 181 system::run_command_in_gnome_user_session ; Function to do a reverse sudo back to teh original Gnome user who called upon sudo. 182 system::set_oem_elevated_privilege_gui ; Configure sudo/root environments so GUI apps can launch using the OEM user's X session. 183 system::toggle_oem_auto_elevated_privilege ; Toggles passwordless sudo privileges for a specified user. 184 system::toggle_oem_auto_login ; Enable, disable, or toggle auto-login for common display managers. 185 system::tune_system_power_profile ; Tuned is a Linux feature that monitors a system and optimizes its performance 186 system::update_config ; This function updates a configuration file with key-value pairs. 187 system::validate_parameters ; Function to validate that all parameters match the pattern --option "value". 188 system::wait_for_internet_availability ; Waits for an active internet connection before proceeding. 189 template::Agama_json ; Generate an Agama installer JSON profile for automated SUSE installs. 190 template::autounattend_xml ; Generate a Windows autounattend.xml for the specified Windows version. 191 template::AutoYast_xml ; Generate an AutoYaST XML profile using OEM defaults for SUSE installs. 192 template::cloud_config ; Append role-specific cloud-config YAML to a user-data file for autoinstall. 193 template::config_menu_cmd ; Generate the Windows configuration menu batch script. 194 template::kickstart_cfg ; Generate a Kickstart configuration for Red Hat–style automated installs. 195 template::minecraft_server_launcher ; Write the template launcher script used by the Minecraft server manager. 196 template::preseed_cfg::auto_disk_layout ; Write the preseed disk layout section, honoring encryption and recipe choices. 197 template::preseed_cfg::early_command ; Prevent installs to the boot USB by selecting a suitable target disk early. 198 template::preseed_cfg::expert_recipe ; Add an expert partition recipe (EFI, LVM, encrypted root) to a preseed file. 199 template::preseed_cfg::late_command ; Append role-specific late commands (e.g., Minecraft/KVM setup) to preseed. 200 template::preseed_cfg::main ; Write the core preseed settings for unattended Debian/Ubuntu installs. 201 template::rtd_me_sh_cmd ; Generate the Windows rtd_me.sh.cmd helper for OEM post-setup tasks. 202 term::animate_while_command ; Displays an animation during the execution of a silent command. 203 term::err_no_menu_system_found ; Function to handle the error condition if a manu system is not found on the system. 204 term::set_colors ; Set colors for prompting on screen in human readable variables. These will be set globally 205 term::start_animation ; Start a simple terminal spinner while a background command runs. 206 term::stop_animation ; Stop the spinner animation and print success or failure status. 207 term::task_exec_list_output ; Execute a command while displaying a formatted status line in the terminal. 208 tool::compress_all_items_here ; Function to compress the contents of the present working directory. 209 tool::compress_provided_items ; Function to compress the contents of the present working directory. 210 tool::recompress_all_items_in_folder ; Function to recompress all archive files in the current directory to a specified format. 211 tool::recompress_provided_items ; Function to recompress specified files to a target compression format. 212 tool::test_iso_boot_media ; Function to test a created ISO file by booting it in a temporary VM using QEMU. 213 tool::up_2_date ; Function to simplify updating system completely. At present this function sets the 214 ubuntu::download_iso ; Downloads the specified Ubuntu ISO (Desktop or Server) based on the provided flavor, version, and type. 215 ubuntu::enable_plymouth_theme ; Detects the Ubuntu spin (flavor) and ensures the appropriate 216 ubuntu::get_latest_release_version ; Return the newest Ubuntu release version from releases.ubuntu.com. 217 ubuntu::get_target_version ; Determine the target Ubuntu version interactively or via defaults. 218 util::add_iso_to_ventoy ; Function to add an ISO file to a Ventoy USB drive for multiple boot options. 219 util::validate_heredoc_tabs_only ; Check <<-EOF heredocs to ensure indentation uses tabs only. 220 write_error ; Print an error message in red with caller context and log it. 221 write_host ; Print text with optional color flags as the base output helper. 222 write_information ; Print an informational message in blue and record it in the log. 223 write_status ; Print a green status message and log it for traceability. 224 write_warning ; Print a yellow warning message and log the warning. 225 yad::display_cmd_output ; Displays the output of a command in a YAD box. 226 yad::display_file ; Displays the content of a text file in a Zenity window. 227 yad::display_info ; Displays an information notice using a Zenity info box. 228 yad::display_progressbar_pulsating ; Displays the output of a command in a pulsating progress box. 229 yad::display_url ; Displays the content of a URL in a GUI using Zenity. 230 zenity::display_file ; Displays the content of a text file in a Zenity window. 231 zenity::display_info ; Displays an information notice using a Zenity info box. 232 zenity::display_url ; Displays the content of a URL in a GUI using Zenity. 233 whonix::version_ge ; Compare two dotted version strings, returns success when first is >= second. 234 whonix::current_vm_version ; Extract the Whonix version from an existing VM disk path. 235 whonix::discover_bundle ; Discover the latest Whonix libvirt bundle (prefers GUI/LXQt) and checksum URLs. 236 whonix::prepare_workdir ; Ensure the working directory is ready for a Whonix bundle. 237 whonix::download_with_verify ; Download a file with retries and optional SHA-512 verification. 238 whonix::download_bundle ; Fetch the Whonix bundle if not already present locally. 239 whonix::extract_archive ; Extract the Whonix libvirt archive when images/XMLs are missing. 240 whonix::detect_image_owner ; Determine the appropriate owner:group for libvirt image placement. 241 whonix::stage_images ; Move/reuse Whonix qcow2/raw images into the libvirt images directory. 242 whonix::ensure_networks_defined ; Define Whonix virtual networks idempotently. 243 whonix::ensure_networks_running ; Start Whonix virtual networks if they are not active. 244 whonix::define_vms ; Define or refresh Whonix Gateway/Workstation VMs against target images. 245 whonix::add_extra_workstation ; Create an additional workstation VM overlaying an existing base disk. 246 whonix::maybe_skip_install ; Skip all work when existing VMs are already at/above the target version (unless refresh/add requested). 247 desktop::enable_firefox_pip_global ; Keep Firefox Picture-in-Picture windows on top across workspaces. 248 gnome::_apply_gtk4_theme ; Write GTK4 settings.ini matching the chosen theme, icons, and dark preference. 249 gnome::_apply_terminal_preferences ; Apply consistent GNOME Terminal profile settings across all profiles. 250 gnome::_reload_shell_theme ; Request a GNOME Shell reload after theme changes. 251 gnome::_reset_ui_theme_settings ; Reset GNOME theme settings and clear GTK4 overrides. 252 gnome::ensure_window_buttons_visible ; Force-enable window control buttons with an optional layout string. 253 kde::set_wallpaper ; Set KDE Plasma wallpaper from an image or video path. 254 kvm::cleanup_unused_qcow_images ; Remove unattached qcow/qcow2 images from libvirt directories. 255 kvm::cleanup_virtual_floppy_images ; Remove generated virtual floppy images from libvirt boot directories. 256 kvm::clone_template_vm::_build_customize_args ; Parse virt-customize options or scripts for template cloning. 257 kvm::clone_template_vm::_clone_from_category ; Drive the interactive clone flow for a template VM category. 258 kvm::clone_template_vm::_display_template_label ; Build human-friendly labels for template selection menus. 259 kvm::clone_template_vm::_format_name_component ; Normalize strings into Title_Case components with separators. 260 kvm::util::config_label ; Build a formatted configuration label from raw tokens and role hints. 261 kvm::wait_for_domain_definition ; Wait until a libvirt domain is defined, with timeout and delay controls. 262 kvm::wait_for_domain_start ; Wait for a libvirt domain start event or running state with a timeout. 263 library::check_status ; Report whether the library was sourced or executed directly. 264 library::envcheck_report ; Summarize library load context for debugging/support. 265 software::rtd_ppa_checker ; Inspect PPAs on Ubuntu-like systems and optionally delete unused ones. 266 software::zypper_add_repo ; Add a zypper repository safely with logging. 267 system::optimize_harddrive ; Apply storage performance tunables for detected block devices. 268 system::preserve_graphical_session_environment ; Export display/session vars when running GUI commands as root. 269 system::rtd_oem_reseal::_finalize_shutdown ; Log context and power off after reseal tasks. 270 system::rtd_oem_reseal::_install_guest_additions ; Install guest integration tools during reseal workflows. 271 system::rtd_oem_reseal::_prepare_debian ; Prep Debian systems for reseal, cleaning IDs and keys. 272 system::rtd_oem_reseal::_prepare_fedora ; Prep Fedora/Red Hat systems for reseal and enable initial-setup. 273 system::rtd_oem_reseal::_prepare_suse ; Prep SUSE systems for reseal and enable YaST firstboot. 274 system::rtd_oem_reseal::_prepare_ubuntu_like ; Prep Ubuntu-like systems for reseal; add guest tools when virtual. 275 system::rtd_oem_reseal::_virtualization_context ; Detect whether the system is running as a virtual guest. 276 whonix::init_context ; Initialize Whonix helper globals safely (set -u friendly). ### Namespace Map (grouped for quick navigation) System: library::apply_rtd_defaults system::ensure_directory_exists system::_enable_service system::_disable_service system::_turn_on_gui_network_management system::process_vm_opt_args system::find_download_ubuntu_iso system::create_physical_media_from_ubuntu_iso Network: network::check_inet_access network::get_oem_test_ips network::whats_my_ipinfo network::whats_my_external_ip network::rsync_upload network::rsync_download Security: security::ensure_admin security::encrypt_disk_with_luks security::enable_firewall security::scan_for_malware security::harden_ubuntu security::configure_clamav security::configure_fail2ban security::configure_rkhunter security::configure_auditd security::sysctl_hardening security::secure_password_policy security::install_AIDE Software: software::check_native_package_dependency software::add_native_package software::is_native_package_available software::ensure_gnome_software_store_available software::ensure_flatpak_package_managment software::update_system_txt software::update_all_ui software::from_flathub.org software::from_snapcraft.io software::vendor_download_and_install software::determine_package_name_from_cmd KVM/Template (selection): kvm::util::set_vm_name kvm::util::read_common_options kvm::get_vm_config_preferences kvm::get_vm_size_preferences kvm::clone_server_template_vm kvm::clone_vdi_template_vm kvm::cicd::configure_vm_with_ansible_pull template::cloud_config template::preseed_cfg::main / ::late_command / ::auto_disk_layout Whonix: whonix::discover_bundle whonix::prepare_workdir whonix::download_bundle whonix::extract_archive whonix::stage_images whonix::ensure_networks_defined whonix::ensure_networks_running whonix::define_vms whonix::add_extra_workstation whonix::maybe_skip_install Use Google Coding standards: In general any function that is not both obvious and short must be commented. Any function in a library must be commented regardless of length or complexity. It should be possible for someone else to learn how to use your program or to use a function in your library by reading the comments (and self-help, if provided) without reading the code. All function comments should describe the intended function behavior using: 1 Description of the function. 2 Globals: List of global variables used and modified. 3 Arguments: Arguments taken. 4 Outputs: Output to STDOUT or STDERR. 5 Returns: Returned values other than the default exit status of the last command run. Advice: As a "best practice" working with this file, given the size and complexities, it is better to write a new function in an external script and use the "source" statement to include this library in said script, so that all the other functions here are available to use in the new function. . ├── 📜 Standard Interactions & UI │ ├── dialog::* (TUI dialogs: notice, error, progress, yes/no) │ ├── yad::* (GUI dialogs using YAD) │ ├── zenity::* (GUI dialogs using Zenity) │ └── term::* (Terminal animations and formatted output) │ ├── ⚙️ System & OEM Management │ ├── system::* (Service management, logging, ISO creation, sys-info) │ ├── oem::* (Branding, theming, creating launchers, resealing) │ ├── gnome::* (GNOME-specific tweaks: Dash-to-Panel, Nautilus, fonts) │ └── security::* (Firewall, encryption, hardening, malware scanning) │ ├── 📦 Software Management │ ├── software::* (Install/remove packages, manage Flatpak/Snap, update system) │ └── dependency::* (Check for commands, OS, desktop environment) │ ├── 🌐 Network Operations │ ├── network::* (Check internet, get public IP, rsync wrappers) │ └── ssh::* (Manage SSH keys) │ ├── 🖥️ Virtualization (KVM) │ ├── kvm::* (Create, clone, and manage KVM virtual machines) │ ├── kvm::cicd::* (Automate VM config with cloud-init and Ansible) │ ├── kvm::util::* (VM naming and configuration helpers) │ └── whonix::* (Specific functions to deploy Whonix Gateway/Workstation VMs) │ ├── 📝 Templates & Configuration │ ├── template::* (Generate config files: AutoYast, Kickstart, preseed, etc.) │ └── library::* (Core library functions, path normalization) │ └── 🛠️ Miscellaneous Tools ├── tool::* (Compress/recompress files, test ISO boot media) ├── disk::* (Device re-encryption) └── fedora::*, ubuntu::*, mint::* (Distro-specific ISO downloaders) Library_Documentation ################################################################################################ # |>>> |>>> # | |>>> |>>> | # * | | * # / \ * * / \ # /___\ _/ \ / \_ /___\ # [ ] |/ \_________/ \| [ ] # [ I ] / \ / \ [ I ] # [ ]_ _ _ / \ / \ _ _ _[ ] # [ ] U U | {#########} {#########} | U U [ ] # [ ]====/ \=======/ \=======/ \====[ ] # [ ] | | I |_ _ _ _| I | | [ ] # [___] |_ _ _ _ _ _| | U U U | |_ _ _ _ _ _| [___] # \===/ I | U U U U U | |=======| | U U U U U | I \===/ # \=/ |===========| I | + W + | I |===========| \=/ # | I | | |_______| | | I | # | | | ||||||||| | | | # | | | I ||vvvvv|| I | | | # _-_-|______|-----------|_____|| ||_____|-----------|______|-_-_ # /________\ /______|| ||______\ /________\ # |__________|-------|________\_____/________|-------|__________| # ################################################################################################ # # 888 d8b 888 # 888 Y8P 888 # 888 888 # 888 888 88888b. 888d888 8888b. 888d888 888 888 # 888 888 888 "88b 888P" "88b 888P" 888 888 # 888 888 888 888 888 .d888888 888 888 888 # 888 888 888 d88P 888 888 888 888 Y88b 888 # 88888888 888 88888P" 888 "Y888888 888 "Y88888 # 888 # Y8b d88P # "Y88P" # .d8888b. 888 888 d8b # d88P Y88b 888 888 Y8P # Y88b. 888 888 # "Y888b. .d88b. 888888 888888 888 88888b. .d88b. .d8888b # "Y88b. d8P Y8b 888 888 888 888 "88b d88P"88b 88K # "888 88888888 888 888 888 888 888 888 888 "Y8888b. # Y88b d88P Y8b. Y88b. Y88b. 888 888 888 Y88b 888 X88 # "Y8888P" "Y8888 "Y888 "Y888 888 888 888 "Y88888 88888P' # 888 # Y8b d88P # "Y88P" ################################################################################################ # Disable unset variable error checking because we are using default values :-"default_value" set +u # Define the name of the library files required for this script # For flexibility we use variables with default values that can be overridden by the script that # sources this library by simply setting them in the parent environment. : ${_branding_info:="_branding.info"} : ${_locations_info:="_locations.info"} : ${_rtd_recipies_info:="_rtd_recipies.info"} # Online source for scripts and configuration : ${_src_url="https://github.com/${_GIT_PROFILE:-vonschutter}/RTD-Setup/raw/main/core/${1}"} # load global information: declare -a _OEM_dependencies=("$_branding_info" "$_locations_info" "$_rtd_recipies_info") # All library functions are listed below in a few sections: # - Standard Interactions # - System Management # - Software Management # - Network # - Security # - Virtual Machine Management # - Internal Configuration Repository # - Script Internal Functions ################################################################################################ # # . -------------------------------------------------------------------. # | [Esc] [F1][F2][F3][F4][F5][F6][F7][F8][F9][F0][F10][F11][F12] o o o| # | | # | [`][1][2][3][4][5][6][7][8][9][0][-][=][_<_] [I][H][U] [N][/][*][-]| # | [|-][Q][W][E][R][T][Y][U][I][O][P][{][}] | | [D][E][D] [7][8][9]|+|| # | [CAP][A][S][D][F][G][H][J][K][L][;]['][#]|_| [4][5][6]|_|| # | [^][\][Z][X][C][V][B][N][M][,][.][/] [__^__] [^] [1][2][3]| || # | [c] [a][________________________][a] [c] [<][V][>] [ 0 ][.]|_|| # `--------------------------------------------------------------------' # # # .d8888b. 888 888 888 # d88P Y88b 888 888 888 # Y88b. 888 888 888 # "Y888b. 888888 8888b. 88888b. .d88888 8888b. 888d888 .d88888 # "Y88b. 888 "88b 888 "88b d88" 888 "88b 888P" d88" 888 # "888 888 .d888888 888 888 888 888 .d888888 888 888 888 # Y88b d88P Y88b. 888 888 888 888 Y88b 888 888 888 888 Y88b 888 # "Y8888P" "Y888 "Y888888 888 888 "Y88888 "Y888888 888 "Y88888 # # # # 8888888 888 888 d8b # 888 888 888 Y8P # 888 888 888 # 888 88888b. 888888 .d88b. 888d888 8888b. .d8888b 888888 888 .d88b. 88888b. .d8888b # 888 888 "88b 888 d8P Y8b 888P" "88b d88P" 888 888 d88""88b 888 "88b 88K # 888 888 888 888 88888888 888 .d888888 888 888 888 888 888 888 888 "Y8888b. # 888 888 888 Y88b. Y8b. 888 888 888 Y88b. Y88b. 888 Y88..88P 888 888 X88 # 8888888 888 888 "Y888 "Y8888 888 "Y888888 "Y8888P "Y888 888 "Y88P" 888 888 88888P' # ################################################################################################ display_gui() { # Description: Displays a GUI dialog box with the given message. # # This function uses the 'dialog' command to display a GUI dialog box with the provided message. # The dialog box is displayed in a terminal window, and the user can interact with it. # # Globals: # - RTD_GUI # Arguments: # - "message": The message to be displayed in the dialog box. # Outputs: # - Displays a GUI dialog box with the provided message. # Returns: # - 0 on success, ESC on user cancellation. # # Usage: # display_gui "Hello, World!" # # End of Documentation dialog::check_menu_availability dialog::display_notice "$1" } dialog::check_menu_availability() { # Description: First discover what menu system is installed. Some systems use "dialog" and # other systems use whiptail for the terminal to show menus and dialogs. # If nothing is found, then make sure it is available before continuing. # # Globals: # RTD_GUI respected as default value. If unset it will be set to "dialog" or "whiptail" # depending on which is present. IF both are present, the dialog is prefferred. # # Arguments: None accepted # Outputs: Interactive # Returns: Default exit status of the last command run. # Usage: dialog::check_menu_availability # # End of documentation if hash dialog 2>/dev/null; then : "${RTD_GUI:=dialog --clear}" export RTD_GUI elif hash whiptail 2>/dev/null; then : "${RTD_GUI:="whiptail --fb"}" export RTD_GUI if ($RTD_GUI --backtitle "$BRANDING" \ --title "System Information Menu" \ --no-button "NO: Use whiptail" \ --yes-button "YES: Install dialog" \ --yesno "Please NOTE that we are using WHIPTAIL as a terminal menu system, which is missing several features compared to the default: DIALOG menu system. There are a few reasons we may be using WHIPTAIL instead: I was forced to use it or it is the only terminal menu system available... If you continue with the current WHIPTAIL some things might not work. \n \n May I please try to install the dialog system in stead? . \n " \ 20 90); then RTD_GUI=dialog term::err_no_menu_system_found else echo "User selected No, exit status was $?." fi else term::err_no_menu_system_found dialog && return 1 || exit 1 fi } dialog::display_cmd_output() { # Description: Displays the output of a command in a dialog box. # # This function takes a command as an argument and executes it. # The output of the command is then displayed in a dialog box. # This is a convenient way to display the output of a command in a terminal in a nice way. # The dialog box is displayed using the 'dialog' command, which must be installed on the system. # If the user cancels the operation, the function returns ESC. # # Globals: # - dialog # Arguments: # - ["String to execute"]: The command to be executed and its arguments. # Outputs: # - Displays the command output in a dialog box. # Returns: # - 0 on success, ESC on user cancellation. # # Usage: # dialog::display_cmd_output "apt -y upgrade" # # End of Documentation _cmd="$*" ${_cmd} | ${RTD_GUI:-dialog} --backtitle "\Zb$PUBLICATION $VERSION\ZB" --progressbox "RUNNING: ${_cmd}" 25 120 } dialog::copy_file_progress() { # Description: Displays a file copy progress dialog. # This function shows a progress bar while copying files from a source directory to a destination directory. # # Globals: # - dialog # Arguments: # - "source": The directory containing the files to be copied. # - "destination": The directory where the files will be copied to. # Outputs: # - Displays a progress dialog box showing the file copy progress. # Returns: # - 0 on success, ESC on user cancellation. # # Usage: # dialog::copy_file_progress "source" "destination" # # Example: # dialog::copy_file_progress "/path/to/source" "/path/to/destination" # # Details: # - This function takes two arguments: the source directory containing the files to be copied, # and the destination directory where the files will be copied to. # - It creates a progress bar dialog using the 'dialog' command and updates it as each file is copied. # - The progress bar shows the percentage of files copied and the name of the current file being copied. # - The function returns 0 if the copying process completes successfully, or ESC if the user cancels the operation. # # Note: The 'dialog' command must be installed for this function to work. # # End of Documentation _dirs=(${1}/*) # Destination directory _dest="${2}" [ ! -d "${_dest}" ] && mkdir -p "${_dest}" dialog --title "Copying files" --gauge "Copying file..." 10 75 < <( # Get total number of files in array n=${#_dirs[*]} i=0 for f in "${_dirs[@]}"; do # calculate progress _pct=$((100 * (++i) / n)) # update dialog box cat </dev/null done ) unset _dirs unset _dest unset _pct } dialog::copy_with_progress() { # Description: Function to display a progress bar while copying a large file to a destination. # This function uses 'dialog' to create a progress bar that shows the percentage of an ISO file being copied. # # Globals: # - None # Arguments: # - "$1": The path to the large file (ISO) file to be copied. # - "$2": The destination path where the large file (ISO) file will be copied. # - "$3": The mount point directory (used for display purposes). # Outputs: # - Displays a dialog box with the copy progress. # Returns: # - 0 on success. # # Usage: # dialog::copy_with_progress "/path/to/iso/file.iso" "/path/to/destination" "/mount/point" # # Example: # dialog::copy_with_progress "/home/user/file.iso" "/mnt/usb/file.iso" "/mnt/usb" # # Dependencies: # - dialog: To display the progress bar. # - pv: To monitor the progress of the file copy. # - mktemp: To create a temporary named pipe. # # Notes: # - Ensure 'dialog' and 'pv' are installed on your system for this function to work correctly. # # End of Documentation local iso_file="$1" local destination="$2" local mount_point="$3" # Get the size of the ISO file local iso_size=$(stat -c%s "$iso_file") # Create a named pipe for progress updates local pipe=$(mktemp -u) mkfifo "$pipe" # Start dialog progress bar dialog --title "Copying ISO" --gauge "Copying $iso_file to $mount_point/$rtd_ventoy_dir" 10 70 0 <"$pipe" & # Copy the file and update progress (pv -n "$iso_file" >"$destination" 2>&1 | while read -r progress; do echo $((progress * 100 / iso_size)) done >"$pipe") # Clean up rm "$pipe" } dialog::display_notice() { # Description: Displays an information notice using a dialog box. # This function provides a convenient way to display a message to an end user in a terminal. # # Globals: # - "${RTD_GUI}" (optional): Specifies the command to use for displaying the dialog (defaults to 'dialog'). # - "${PUBLICATION}" (optional): Sets the backtitle of the dialog box. # Arguments: # - "$1": The message string to be displayed in the dialog box. # Outputs: # - Displays the specified message in a dialog box. # Returns: # - 0 on success, ESC on user cancellation. # # Usage: # dialog::display_notice "The message that you want to display" # # Dependencies: # - dialog: To display the message in a dialog box. # # Notes: # - Ensure 'dialog' is installed on your system for this function to work correctly. # # End of Documentation system::log_item "checking for dialog..." if ! hash "${RTD_GUI:-dialog}" &>/dev/null; then system::log_item "${RTD_GUI:-dialog} not found..." software::check_native_package_dependency "${RTD_GUI:-dialog}" &>/dev/null || return 1 else system::log_item "${RTD_GUI:-dialog} found..." fi system::log_item "Notice: ${*}" dialog --backtitle "${PUBLICATION:-"Notice"}" --colors --no-collapse --title "Notice" --msgbox "\n ${*} \n" 15 78 clear return 0 } dialog::display_error() { # Description: Displays an error notice using a dialog box. # This function provides a convenient way to display an error message to an end user in a terminal. # # Globals: # - "${RTD_GUI}" (optional): Specifies the command to use for displaying the dialog (defaults to 'dialog'). # - "${PUBLICATION}" (optional): Sets the backtitle of the dialog box. # Arguments: # - "$1": The error message string to be displayed in the dialog box. # Outputs: # - Displays the specified error message in a dialog box. # Returns: # - 0 on success, ESC on user cancellation. # # Usage: # dialog::display_error "The message that you want to display" # # Dependencies: # - dialog: To display the error message in a dialog box. # # Notes: # - Ensure 'dialog' is installed on your system for this function to work correctly. # # End of Documentation system::log_item "checking for dialog..." if ! hash "${RTD_GUI:-dialog}" &>/dev/null; then system::log_item "${RTD_GUI:-dialog} not found..." software::check_native_package_dependency "${RTD_GUI:-dialog}" &>/dev/null || return 1 else system::log_item "${RTD_GUI:-dialog} found..." fi system::log_item "Notice: ${*}" dialog --backtitle "${PUBLICATION:-"ERROR"}" --colors --no-collapse --title "ERROR" --msgbox "\n ${*} \n" 15 78 clear return 0 } dialog::display_summary_message() { # Description: Function to read a variable "result" and display its contents. The purpose of this function # is simply to make it easy and convenient to display a message to an end user in a terminal # in a nice way. To use this function, simply call it after populating the variable result. # # Globals: ${result}, ${BRANDING} # # Arguments: ["Title to display"] # # Outputs: Message in dialog box # # Returns: 0 or ESC # # Usage: # # result=$( command with output) # dialog::display_result "title of the message" # # End of Documentation if ! hash dialog &>/dev/null; then system::log_item "dialog not found..." software::check_native_package_dependency dialog &>/dev/null || return 1 else system::log_item "dialog found..." fi system::log_item "Displaying summary message TITLE: $1 and the message: ${_summary_message}" if echo "${FUNCNAME[1]}" | grep "make_kvm_virtual_machine"; then dialog --backtitle "$BRANDING" --colors --no-collapse --title "$1" --msgbox "\n ${_summary_message} \n" 0 0 else dialog --backtitle "$BRANDING" --colors --no-collapse --title "$1" --msgbox "${_summary_message}" "${HEIGHT:-"20"}" "${WIDTH:-"80"}" fi clear return 0 } dialog::display_result() { # Description: Displays the contents of the variable "result" in a message box. # This function makes it easy and convenient to display a message to an end user in a terminal. # It checks for the availability of the dialog utility and uses it to display a message box. # # Globals: # - ${result}: The variable whose contents will be displayed. # - ${BRANDING} (optional): Sets the backtitle of the dialog box. # Arguments: # - "$1": The title of the message box. # Outputs: # - Displays the content of the "result" variable in a dialog box. # Returns: # - 0 on success, ESC on user cancellation. # # Usage: # result=$(command_with_output) # dialog::display_result "Title of the message" # # Example: # result=$(ls -l) # dialog::display_result "Directory Listing" # # Dependencies: # - dialog: To display the message box. # # Notes: # - If the dialog utility is not found, the function checks for dependencies and returns 1 if they are not met. # - The backtitle, colors, collapse, height, and width of the message box are customizable using environment variables. # # End of Documentation system::log_item "checking for dialog..." if ! hash "${RTD_GUI:-dialog}" &>/dev/null; then system::log_item "${RTD_GUI:-dialog} not found..." software::check_native_package_dependency "${RTD_GUI:-dialog}" &>/dev/null || return 1 else system::log_item "${RTD_GUI:-dialog} found..." fi system::log_item "Displaying result: $1 " ${RTD_GUI:-dialog} --backtitle "$BRANDING" --colors --no-collapse --title "$1" --msgbox "$result" "${HEIGHT:-"20"}" "${WIDTH:-"80"}" clear } dialog::prompt_yes_no() { # Description: Displays a Yes/No prompt using a dialog box. # This function provides a convenient way to ask the user a Yes/No question in a terminal, # with customizable backtitle, title, and message. If only one argument is passed, it will # treat it as the message. Otherwise, it will intelligently parse --back-title, --title, and # --message flags to determine what to display. # # Globals: # - "${RTD_GUI}" (optional): Specifies the command to use for displaying the dialog (defaults to 'dialog'). # - "${PUBLICATION}" (optional): Sets the default backtitle of the dialog box. # Arguments: # - "--back-title": (Optional) The backtitle of the dialog box. # - "--title": (Optional) The title of the dialog box. # - "--message": (Optional) The message string (query) to be displayed in the dialog box. # - If a single argument is provided without flags, it is treated as the message. # Outputs: # - Displays a Yes/No prompt to the user. # Returns: # - 0 if the user selects Yes. # - 1 if the user selects No. # - ESC on user cancellation. # # Usage: # dialog::prompt_yes_no --back-title "System Check" --title "Restart Confirmation" --message "Do you want to restart?" # dialog::prompt_yes_no "Do you want to proceed?" # # Dependencies: # - dialog: To display the prompt in a dialog box. # # Notes: # - 'dialog' will be installed on your system if required for this function to work correctly. # # End of Documentation # Initialize default values local backtitle="${PUBLICATION:-"Query"}" local title="Confirmation" local message="Are you sure you want to proceed?" # Parse arguments while [[ $# -gt 0 ]]; do case "$1" in --back-title) backtitle="$2" shift 2 ;; --title) title="$2" shift 2 ;; --message) message="$2" shift 2 ;; *) # If only a single unflagged argument is passed, treat it as the message message="$1" shift ;; esac done system::log_item "Checking for dialog..." if ! hash "${RTD_GUI:-dialog}" &>/dev/null; then system::log_item "${RTD_GUI:-dialog} not found..." software::check_native_package_dependency "${RTD_GUI:-dialog}" &>/dev/null || return 1 else system::log_item "${RTD_GUI:-dialog} found..." fi system::log_item "Displaying Yes/No prompt: $message" # Display the Yes/No dialog "${RTD_GUI:-dialog}" --backtitle "$backtitle" --title "$title" --yesno "\n$message\n" 10 50 # Capture the exit status local exit_status=$? system::log_item "Exit status: $exit_status" clear return $exit_status } yad::display_cmd_output() { # Description: Displays the output of a command in a YAD box. # This function takes a command as an argument, executes it, and displays the output in a YAD box. # # Globals: # - yad # Arguments: # - "$@": The command to be executed and its arguments. # Outputs: # - Displays the command output in a YAD box. # Returns: # - 0 on success or if the YAD box is closed with the "Close" button. # - "ESC" if the YAD box is closed with the "ESC" key. # # Usage: # yad::display_cmd_output "command_to_execute" # # Example: # yad::display_cmd_output "apt -y upgrade" # # Dependencies: # - yad: To display the command output in a YAD box. # # Notes: # - If the YAD utility is not found, the function checks for dependencies and returns 1 if they are not met. # # End of Documentation # Command to execute local cmd=$@ system::log_item "checking for diayadog..." if ! hash "yad" &>/dev/null; then system::log_item "yad not found..." software::check_native_package_dependency "yad" &>/dev/null || return 1 else system::log_item "yad found..." fi # Execute the command and pipe its output to yad ( # Ensure any error messages are sent to standard output so yad can display them exec 2>&1 # Execute the command echo "Executing: $cmd" $cmd echo "Command completed. You can close this window." ) | yad --text-info --width=700 --height=400 --title "Command Output" } yad::display_progressbar_pulsating() { # Description: Displays the output of a command in a pulsating progress box. # This function takes a command as an argument, executes it, and displays the output in a progress box using the 'yad' command. # # Globals: # - yad # Arguments: # - "$*": The command to be executed and its arguments. # Outputs: # - Displays the command output in a YAD progress box. # Returns: # - 0 on success or if the YAD box is closed with the "Close" button. # - "ESC" if the YAD box is closed with the "ESC" key. # # Usage: # yad::display_progressbar_pulsating "command_to_execute" # # Example: # yad::display_progressbar_pulsating "apt -y upgrade" # # Dependencies: # - yad: To display the command output in a YAD progress box. # # Notes: # - The progress box shows a pulsating animation and automatically closes when the command execution is complete. # - If the YAD utility is not found, the function checks for dependencies and returns 1 if they are not met. # # End of Documentation _cmd="$*" system::log_item "checking for yad..." if ! hash "yad" &>/dev/null; then system::log_item "yad not found..." software::check_native_package_dependency "yad" &>/dev/null || return 1 else system::log_item "yad found..." fi ${_cmd} | yad --title="$PUBLICATION $VERSION" --progress --pulsate --auto-close --auto-kill --text="RUNNING: ${_cmd}" --width=300 --height=100 } zenity::display_info() { # Description: Displays an information notice using a Zenity info box. # This function provides a convenient way to display a message to an end user in a GUI. # # Globals: # - $_TITLE (optional): Sets the title of the Zenity info box. # - $selection (optional): Additional text to include in the title. # Arguments: # - "$*": The message string to be displayed in the Zenity info box. # Outputs: # - Displays the specified message in a Zenity info box. # Returns: # - None # # Usage: # zenity::display_info "The message that you want to display" # # Example: # zenity::display_info "This is an important notice for the user." # # Dependencies: # - zenity: To display the message in a Zenity info box. # # Notes: # - If the Zenity utility is not found, the function checks for dependencies and returns 1 if they are not met. # - The info box is customizable using the global variables $_TITLE and $selection. # # End of Documentation _text="${*}" hash zenity &>/dev/null || software::check_native_package_dependency zenity &>/dev/null || return 1 zenity --info --text --title="${_TITLE:="Notice"}: $selection" --text="$_text" --ellipsize --height=600 --width=800 2>/dev/null } yad::display_info() { # Description: Displays an information notice using a Zenity info box. # This function provides a convenient way to display a message to an end user in a GUI. # # Globals: # - $_TITLE (optional): Sets the title of the Zenity info box. # - $selection (optional): Additional text to include in the title. # Arguments: # - "$*": The message string to be displayed in the Zenity info box. # Outputs: # - Displays the specified message in a Zenity info box. # Returns: # - None # # Usage: # zenity::display_info "The message that you want to display" # # Example: # zenity::display_info "This is an important notice for the user." # # Dependencies: # - zenity: To display the message in a Zenity info box. # # Notes: # - If the Zenity utility is not found, the function checks for dependencies and returns 1 if they are not met. # - The info box is customizable using the global variables $_TITLE and $selection. # # End of Documentation _text="${*}" hash yad &>/dev/null || software::check_native_package_dependency yad &>/dev/null || return 1 yad --info --text --title="${_TITLE:="Notice"}: $selection" --text="$_text" --ellipsize --height=600 --width=800 2>/dev/null } zenity::display_url() { # Description: Displays the content of a URL in a GUI using Zenity. # This function makes it easy and convenient to display web content to an end user in a nice way. # To use this function, simply call it passing the URL as an argument. # # Globals: # $_TITLE (optional): Sets the title of the Zenity window. # Arguments: # ["URL"]: The URL whose content will be displayed. # Outputs: # - Formatted HTML message in a Zenity window. # Returns: # - None # # Usage: # zenity::display_url "https://www.someplace.com/index.html" # # Example: # zenity::display_url "https://www.example.com" # # Dependencies: # - zenity: To display the URL content in a Zenity window. # # Notes: # - If the Zenity utility is not found, the function checks for dependencies and returns 1 if they are not met. # # End of Documentation local _url="${*}" hash zenity &>/dev/null || software::check_native_package_dependency zenity &>/dev/null || return 1 hash wget &>/dev/null || software::check_native_package_dependency wget &>/dev/null || return 1 local tmp_folder=$(mktemp -d) wget -q "${_url}" -O "${tmp_folder}/file.md" &>/dev/null yad::display_file "${tmp_folder}/file.md" #&>/dev/null } yad::display_url() { # Description: Displays the content of a URL in a GUI using Zenity. # This function makes it easy and convenient to display web content to an end user in a nice way. # To use this function, simply call it passing the URL as an argument. # # Globals: # $_TITLE (optional): Sets the title of the Zenity window. # Arguments: # ["URL"]: The URL whose content will be displayed. # Outputs: # - Formatted HTML message in a Zenity window. # Returns: # - None # # Usage: # zenity::display_url "https://www.someplace.com/index.html" # # Example: # zenity::display_url "https://www.example.com" # # Dependencies: # - zenity: To display the URL content in a Zenity window. # # Notes: # - If the Zenity utility is not found, the function checks for dependencies and returns 1 if they are not met. # # End of Documentation local _url="${*}" hash yad &>/dev/null || software::check_native_package_dependency yad &>/dev/null || return 1 hash wget &>/dev/null || software::check_native_package_dependency wget &>/dev/null || return 1 local tmp_folder=$(mktemp -d) wget -q "${_url}" -O "${tmp_folder}/yad.md" &>/dev/null yad::display_file "${tmp_folder}/yad.md" #&>/dev/null unset _url } zenity::display_file() { # Description: Displays the content of a text file in a Zenity window. # This function makes it easy and convenient to display text content to an end user in a GUI. # To use this function, simply call it passing the file path as an argument. # # Globals: # $_TITLE (optional): Sets the title of the Zenity window. # Arguments: # ["/path/to/file.txt"]: The path to the text file to be displayed. # Outputs: # - Displays the content of the specified text file in a Zenity window. # Returns: # - None # # Usage: # zenity::display_file "/path/to/file.txt" # # Example: # zenity::display_file "/path/to/file.txt" # # Dependencies: # - zenity: To display the text file content in a Zenity window. # # Notes: # - If the Zenity utility is not found, the function checks for dependencies and returns 1 if they are not met. # # End of Documentation _filename="${*}" hash zenity &>/dev/null || software::check_native_package_dependency zenity &>/dev/null || return 1 hash pandoc &>/dev/null || software::check_native_package_dependency pandoc &>/dev/null || return 1 zenity --text-info --title="${_TITLE:="$_filename"}:" --filename=<(pandoc -s $_filename -t plain) --width=800 --height=600 2>/dev/null #&>/dev/null } yad::display_file() { # Description: Displays the content of a text file in a Zenity window. # This function makes it easy and convenient to display text content to an end user in a GUI. # To use this function, simply call it passing the file path as an argument. # # Globals: # $_TITLE (optional): Sets the title of the Zenity window. # Arguments: # ["/path/to/file.txt"]: The path to the text file to be displayed. # Outputs: # - Displays the content of the specified text file in a Zenity window. # Returns: # - None # # Usage: # zenity::display_file "/path/to/file.txt" # # Example: # zenity::display_file "/path/to/file.txt" # # Dependencies: # - zenity: To display the text file content in a Zenity window. # # Notes: # - If the Zenity utility is not found, the function checks for dependencies and returns 1 if they are not met. # # End of Documentation _filename="${*}" hash yad &>/dev/null || software::check_native_package_dependency yad &>/dev/null || return 1 hash pandoc &>/dev/null || software::check_native_package_dependency pandoc &>/dev/null || return 1 yad --text-info --title="${_TITLE:="$_filename"}:" --filename=<(pandoc -s $_filename -t plain) --wrap --width=800 --height=600 &>/dev/null } term::animate_while_command() { # Description: Displays an animation during the execution of a silent command. # This function is useful for running another function or command that may take some time to complete. # Note that this works best if the function or command does not print to the terminal. # # Globals: # - None # Arguments: # - "$@": The command or function to be executed. # Outputs: # - Standard output (suppressed) # Returns: # - 0 on success, 1 if no command is specified. # # Usage: # term::animate_while_command "command_or_function" # # Example: # term::animate_while_command "sleep 10" # # Dependencies: # - term::start_animation: To start the animation. # - term::stop_animation: To stop the animation. # # Notes: # - Ensure that the command or function passed as an argument does not produce output to the terminal for best results. # # End of Documentation if [[ -z ${1} ]]; then write_error "Specifying a command to wait for is required!" return 1 fi local logfile="${TERM_TASK_LOGFILE:-/dev/null}" local monitor_was_enabled=0 if [[ -o monitor ]]; then monitor_was_enabled=1 set +o monitor fi term::start_animation "$@" # Run the command and log output while the animation updates in the background. "${@}" >>"$logfile" 2>&1 RET_CODE=$? term::stop_animation "$RET_CODE" if (( monitor_was_enabled )); then set -o monitor fi return "$RET_CODE" } term::start_animation() { # Description: # A simple function to display and animation during a silent acton # # Globals: # Arguments: # Outputs: Standard out # Returns: # Usage: # term::start_animation "Display text" # term::stop_animation # # End of documentation local cmd_while if [[ -n ${TERM_TASK_CMD_DISPLAY:-} ]]; then cmd_while="${TERM_TASK_CMD_DISPLAY}" else printf -v cmd_while '%q ' "$@" cmd_while=${cmd_while% } cmd_while="Executing ${cmd_while}" fi if [[ -z ${cmd_while} ]]; then write_error "Specifying a command to wait for is required!" return 1 fi # make some decent display terminal graphics... setterm -cursor off export ANIM_PID="0" local PAD="${green}---------------------------------------------------------" LINE="$(printf "%s %s" "${magenta}${cmd_while}" " $PAD" | cut -c 1-${#PAD}) : " anim=( "${LINE} ${blue}•${green}•${red}•${magenta}• ${ENDCOLOR}" "${LINE} ${green}•${red}•${magenta}•${blue}• ${ENDCOLOR}" "${LINE} ${red}•${magenta}•${blue}•${green}• ${ENDCOLOR}" "${LINE} ${magenta}•${blue}•${green}•${red}• ${ENDCOLOR}" "${LINE} ${blue}•${green}•${red}•${magenta}•${ENDCOLOR}" ) ( while true; do for i in {0..4}; do printf "\r\033[2K ${anim[i]}" sleep 0.1 done for i in {4..0}; do printf "\r\033[2K ${anim[i]}" sleep 0.1 done done ) & export ANIM_PID="${!}" } term::stop_animation() { # Description: # A simple function to STOP display and animation during a silent acton # # Globals: # Arguments: # Outputs: Standard out # Returns: # Usage: # term::stop_animation # # End of documentation if [[ -n "$ANIM_PID" ]] && kill -0 "$ANIM_PID" 2>/dev/null; then kill "$ANIM_PID" wait "$ANIM_PID" 2>/dev/null fi if [[ $RET_CODE == 0 ]]; then printf "\r\033[2K ${LINE} [ \xE2\x9C\x94 ] ${GREEN} OK! ${ENDCOLOR} \n" else printf "\r\033[2K ${LINE} [ ! ] ${RED} FAILED! ${ENDCOLOR} \n" fi unset LINE setterm -cursor on 2>/dev/null } term::task_exec_list_output() { # DESCRIPTION: # Executes a given command while displaying a formatted status line # in the terminal and logging output to the current log file ($_LOGFILE). # Provides visual feedback with terminal graphics to indicate success or failure. # # Intended to be used for clear visual execution tracking in scripts. # # USAGE: # term::task_exec_list_output [args...] # # PARAMETERS: # $@ - The full command to execute (can include arguments). # # OUTPUTS: # - Prints a truncated command summary with an "Executing ..." message. # - Shows a success checkmark (✔ OK) in green or a failure mark (FAILED) in red. # - Appends full command output (stdout and stderr) to $_LOGFILE. # # LOGGING: # - Logs the function call and parameters via system::log_item. # # EXAMPLE: # term::task_exec_list_output ls -lh /tmp # # DEPENDENCIES: # - system::log_item # - Global vars: $_LOGFILE, $GREEN, $RED, $ENDCOLOR # # RETURNS: # - Returns the exit status of the executed command. # # NOTE: # End of documentation local logfile cmd_display status if ! logfile=$(system::determine_logfile); then return 1 fi system::log_item "Running ${FUNCNAME[0]} requested by ${FUNCNAME[1]} with parameters $*" printf -v cmd_display '%q ' "$@" cmd_display=${cmd_display% } TERM_TASK_LOGFILE="$logfile" TERM_TASK_CMD_DISPLAY="Executing ${cmd_display}" term::animate_while_command "$@" status=$? unset TERM_TASK_LOGFILE TERM_TASK_CMD_DISPLAY return "$status" } term::err_no_menu_system_found() { # Description: Function to handle the error condition if a manu system is not found on the system. # On occasion it may be neede to display dialog boxes and selection menus even in a terminal. # for these cases; dialog, whiptail (newt), or zenity may be needed. # This function will attempt to install the desired menu system and if it cannot or the user # opts out it will error and exit the script. # # Globals: RTD_GUI # Arguments: None # Outputs: Interactive # Returns: default exit status of the last command run. # Usage: # term::err_no_menu_system_found # End of documentation write_error " ______________________________________________________________________ \n There is eiter no way to display menus on this system or you have told me \n to install the default menu system! \n This is required to display the administrative menus... \n \n ¯\_( ͡👁️ ͜ʖ ͡👁️)_/¯ \n ______________________________________________________________________ \n" write_warning "May I attepmpt to install this ability to your system? \a " read -p "Add software: (y/n)?" choice case "$choice" in y | Y) : "${RTD_GUI:=$1}" for i in dnf yum zypper apt-get; do if hash $i 2>/dev/null; then $i install $RTD_GUI -y if $? eq 0; then echo -e $YELLOW"$RTD_GUI installed... exporting..." RTD_GUI="dialog --clear" export RTD_GUI return 0 fi fi done ;; n | N) err_no_menu_system_available ;; *) read -p "Invalid Selection" && term::err_no_menu_system_found || exit 1 ;; esac } ################################################################################################ # # # +-----+ +-----+ +-----+ # |Edge1| |Edge2| |Edge3| # +--+--+ +--+--+ +--+--+ # | | | # | +--+--+ | # +-------+BGP1 +-------+ # +--+--+ # | # +--+--+ # |VMH1 | # +-----+ # ################################################################################################ # # ██    ██ ████████ ████████ ██      ██  ███████  ████████  ██    ██  # ███   ██ ██          ██    ██  ██  ██ ██     ██ ██     ██ ██   ██   # ████  ██ ██          ██    ██  ██  ██ ██     ██ ██     ██ ██  ██    # ██ ██ ██ ██████      ██    ██  ██  ██ ██     ██ ████████  █████     # ██  ████ ██          ██    ██  ██  ██ ██     ██ ██   ██   ██  ██    # ██   ███ ██          ██    ██  ██  ██ ██     ██ ██    ██  ██   ██   # ██    ██ ████████    ██     ███  ███   ███████  ██     ██ ██    ██  # ################################################################################################ network::check_inet_access() { # Description: # Verifies internet connectivity by pinging known public IPs. # Supports an optional wait prompt in interactive mode if connectivity is unavailable. # # Globals: # _OEM_TEST_IPS - Optional space-separated list of IPs to ping. Defaults to 8.8.8.8, 1.1.1.1, 9.9.9.9. # # Arguments: # [--interactive] [WAIT_SECONDS] # --interactive Enables interactive prompt if internet is not available. # WAIT_SECONDS Optional: Time to wait before continuing. Default is 10 seconds. # # Outputs: # STDOUT: Status messages about internet connectivity. # # Returns: # 0 if internet access is available # 1 if no internet access is detected # # Usage: # network::check_inet_access # network::check_inet_access --interactive # network::check_inet_access --interactive 60 # # Notes: # If --interactive is used without a second argument, it waits indefinitely for user input. # If a timeout is provided, it waits the specified number of seconds then continues. # The function will return 0 if internet access is available, otherwise it returns 1. # # End of Documentation local wait_time local -a test_ips mapfile -t test_ips < <(network::get_oem_test_ips) case "$1" in --interactive) wait_time="$2" for ip in "${test_ips[@]}"; do if ping -c 1 "$ip" &>/dev/null; then write_status "✅ Internet access is available." return 0 else write_error "❌ No internet access detected." if [[ -n "$wait_time" ]]; then read -t "$wait_time" -p "NOTICE! No internet access. Press [ENTER] to continue or [CTRL+C] to abort. Continuing in $wait_time seconds... " else read -p "NOTICE! No internet access. Press [ENTER] to continue or [CTRL+C] to abort. " fi return 1 fi done ;; *) local default_ip="${test_ips[0]:-8.8.8.8}" if ping -c 1 "$default_ip" &>/dev/null; then system::log_item "✅ Internet access is available." return 0 else system::log_item "❌ No internet access detected." return 1 fi ;; esac } network::get_oem_test_ips() { # Description: Return test IPs as newline-separated list honoring _OEM_TEST_IPS override. # Arguments: None. # Outputs: STDOUT newline-delimited IPs. # Returns: 0 # End of documentation if [[ -n "${_OEM_TEST_IPS}" ]]; then # shellcheck disable=SC2206 local ips=(${_OEM_TEST_IPS}) printf "%s\n" "${ips[@]}" else printf "%s\n" "8.8.8.8" "1.1.1.1" "9.9.9.9" fi } network::whats_my_ipinfo() { # Description: # Attempts to retrieve public IP and geolocation data using multiple external services. # Automatically installs curl if missing. Supports optional timeout for error prompt. # # Globals: # None # # Arguments: # --timeout Optional: timeout in seconds to wait on failure prompt. # # Outputs: # STDOUT: IP and geolocation info from the first responsive service. # # Returns: # 0 if geolocation data was retrieved successfully # 1 if all services failed or curl is missing/unavailable # # Usage: # network::whats_my_ipinfo # network::whats_my_ipinfo --timeout 10 # # Example: # - Get IP info and print to console immediatley: # network::whats_my_ipinfo # # - Get IP info and display in a dialog box: # dialog::display_result "IP Info" "$(network::whats_my_ipinfo)" # # - Get IP info and store in a variable and print to console later: # result=$(network::whats_my_ipinfo) # printf "%s\n" "$result" # # - Get IP info with a timeout of 5 seconds # network::whats_my_ipinfo --timeout 5 # # Notes: # - The function will try multiple services in order until one succeeds. # - If curl is not installed, it will attempt to install it using software::add_native_package. # - If all services fail, it will prompt the user to continue or abort. # - The timeout option allows the user to specify a wait time before continuing. # - The function will return 0 if successful, otherwise it returns 1. # # End of Documentation local timeout service url response local -a ipinfo_services=( "https://ipinfo.io" "https://ifconfig.co/json" "https://api.myip.com" "https://ipapi.co/json" "https://ipwho.is" ) if [[ "$1" == "--timeout" && -n "$2" ]]; then timeout="$2" fi if ! command -v curl >/dev/null 2>&1; then system::log_item "🚫 curl not found, attempting to install..." if ! software::add_native_package curl; then system::log_item "❌ Failed to install curl. Please install it manually." return 1 fi clear fi for url in "${ipinfo_services[@]}"; do system::log_item "🌐 Trying geolocation service: $url" if response=$(curl -s --connect-timeout 5 "$url") && [[ -n "$response" ]]; then system::log_item "✅ Retrieved IP and geolocation data from $url" printf '%s\n' "$response" return 0 else system::log_item "⚠️ Failed to retrieve data from $url" fi done system::log_item "❌ All geolocation services failed." if [[ -n "$timeout" ]]; then read -t "$timeout" -p $"NOTICE! No internet access.\nThis script may require internet to function properly.\nPress [ENTER] to continue or [CTRL+C] to abort. Continuing in $timeout seconds...\n" else read -p $'\nNOTICE! No internet access.\nThis script may require internet to function properly.\nPress [ENTER] to continue or [CTRL+C] to abort.\n' fi return 1 } network::whats_my_external_ip() { # Description: # Retrieves the system's external IP address using multiple fallback services. # Automatically installs curl if missing (via software::add_native_package). # # Globals: # None # # Arguments: # None # # Outputs: # STDOUT: External IP address # STDERR: Error messages and status logs via system::log_item # # Returns: # 0 if external IP was retrieved successfully # 1 if curl could not be installed or all lookups failed # # Usage: # network::whats_my_external_ip # # Example: # ip=$(network::whats_my_external_ip) local _my_ip="unknown" local url local -a fallback_urls=( "https://myip.dnsomatic.com" "https://ipinfo.io/ip" "https://api.ipify.org" "https://checkip.amazonaws.com" "https://icanhazip.com" "https://ifconfig.co" "https://api64.ipify.org" "https://myexternalip.com/raw" ) if ! command -v curl >/dev/null 2>&1; then system::log_item "🚫 curl not found, attempting to install..." if ! software::add_native_package curl; then system::log_item "❌ Failed to install curl. Please install it manually." return 1 fi system::log_item "✅ curl installed successfully." fi system::log_item "🌐 Attempting to retrieve external IP address..." for url in "${fallback_urls[@]}"; do if _my_ip=$(curl -s --connect-timeout 5 "$url"); then if [[ "$_my_ip" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then system::log_item "✅ External IP address retrieved from $url: $_my_ip" printf '%s\n' "$_my_ip" return 0 fi fi done system::log_item "❌ All IP services failed to provide a valid external IP address." return 1 } network::rsync_upload() { # Description: # Transfer files or directories to a remote host using rsync over SSH, # showing a live progress bar with dialog unless --gui false is given. # # Usage: # dialog::rsync_upload --source /path --destination /remote/path --user USER --host HOST [--exclude '*.git'] [--gui false] # # Example: # # # Returns: # 0 on success, 1 on failure # # Globals: # - None # # End of Documentation local src= dest= ssh_user= ssh_host= gui="true" exclude_opt= while [[ $# -gt 0 ]]; do case "$1" in --source) src="$2"; shift 2 ;; --destination) dest="$2"; shift 2 ;; --user) ssh_user="$2"; shift 2 ;; --host) ssh_host="$2"; shift 2 ;; --exclude) exclude_opt="--exclude=$2"; shift 2 ;; --gui) gui="$2"; shift 2 ;; *) printf "⛔ Unknown option: %s\n" "$1" >&2 return 1 ;; esac done if [[ -z "$src" || -z "$dest" || -z "$ssh_user" || -z "$ssh_host" ]]; then [[ "$gui" == "true" && -x "$(command -v dialog)" ]] && dialog::display_error "⛔ ERROR: Missing required arguments." \ || write_error "⛔ ERROR: Missing required arguments.\n" return 1 fi if [[ "$gui" != "false" ]] && ! dependency::command_exists dialog; then gui="false" fi if ! dependency::command_exists rsync find; then [[ "$gui" == "true" ]] && dialog::display_error "⛔ ERROR: Required dependencies missing: rsync or find, and could not be installed" \ || write_error "⛔ ERROR: Required dependencies missing, and could not be installed" return 1 fi if ! ssh::ensure_key_installed_on_remote "$ssh_user" "$ssh_host" ; then [[ "$gui" == "true" ]] && dialog::display_error "⛔ ERROR: The ssh keys are not installed on the remote host. This is required for security purposes" \ || write_error "⛔ ERROR: The ssh keys are not installed on the remote host. This is required for security purposes" return 1 fi if [[ ! -e "$src" ]]; then [[ "$gui" == "true" ]] && dialog::display_error "⛔ ERROR: Source path does not exist: $src" \ || write_error "⛔ ERROR: Source path does not exist: $src\n" return 1 fi if [[ "$gui" == "false" ]]; then rsync -a --info=progress2 --ignore-times $exclude_opt "$src" "${ssh_user}@${ssh_host}:${dest}" if [[ $? -ne 0 ]]; then write_error "⛔ ERROR: rsync command failed." return 1 fi return 0 fi local progress_file; progress_file=$(mktemp) local error_file; error_file=$(mktemp) local count=0 total_files=0 percent=0 if ! total_files=$(find "$src" -type f | wc -l); then dialog::display_error "⛔ ERROR: Could not count files in: $src" return 1 fi if [[ "$total_files" -eq 0 ]]; then dialog::display_error "⛔ ERROR: No files to transfer from: $src" return 1 fi { echo 0 rsync -a --info=NAME --no-inc-recursive --ignore-times $exclude_opt "$src" "${ssh_user}@${ssh_host}:${dest}" 2>>"$error_file" | \ while IFS= read -r line; do [[ "$line" =~ ^skipping ]] && continue ((count++)) percent=$((count * 100 / total_files)) printf "XXX\n%d\nProcessing: %s\nXXX\n" "$percent" "$line" done echo 100 } | dialog --title "📤 Uploading Files to ${ssh_user}@${ssh_host}..." --gauge "Transferring to ${ssh_user}@${ssh_host}..." 10 80 0 if [[ -s "$error_file" ]]; then dialog::display_error "⛔ rsync failed:\n$(tail -n 10 "$error_file")" rm -f "$progress_file" "$error_file" return 1 fi rm -f "$progress_file" "$error_file" dialog::display_notice "✅ Transfer completed successfully." return 0 } network::rsync_download() { # Description: # Download files or directories from a remote host using rsync over SSH, # showing a live progress bar with dialog unless --gui false is given. # # Usage: # dialog::rsync_download --source /remote/path --destination /local/path --user USER --host HOST [--exclude '*.git'] [--gui false] # # Returns: # 0 on success, 1 on failure # # Globals: # - None # # Example: # dialog::rsync_download --source /opt/tla --destination /home/user/tla --user bob --host server.mycompany.com --exclude '*.git' # # End of Documentation local src= dest= ssh_user= ssh_host= gui="true" exclude_opt= while [[ $# -gt 0 ]]; do case "$1" in --source) src="$2"; shift 2 ;; --destination) dest="$2"; shift 2 ;; --user) ssh_user="$2"; shift 2 ;; --host) ssh_host="$2"; shift 2 ;; --exclude) exclude_opt="--exclude=$2"; shift 2 ;; --gui) gui="$2"; shift 2 ;; *) printf "⛔ Unknown option: %s\n" "$1" >&2 return 1 ;; esac done if [[ -z "$src" || -z "$dest" || -z "$ssh_user" || -z "$ssh_host" ]]; then [[ "$gui" == "true" && -x "$(command -v dialog)" ]] && dialog::display_error "⛔ ERROR: Missing required arguments." \ || write_error "⛔ ERROR: Missing required arguments.\n" return 1 fi if [[ "$gui" != "false" ]] && ! dependency::command_exists dialog; then gui="false" fi if ! dependency::command_exists rsync find; then [[ "$gui" == "true" ]] && dialog::display_error "⛔ ERROR: Required dependencies missing: rsync or find, and could not be installed" \ || write_error "⛔ ERROR: Required dependencies missing, and could not be installed" return 1 fi if [[ "$gui" == "false" ]]; then rsync -a --info=progress2 --ignore-times $exclude_opt "${ssh_user}@${ssh_host}:${src}" "$dest" if [[ $? -ne 0 ]]; then write_error "⛔ ERROR: rsync command failed." return 1 fi return 0 fi local progress_file; progress_file=$(mktemp) local error_file; error_file=$(mktemp) local count=0 total_files=0 percent=0 if ! total_files=$(ssh "${ssh_user}@${ssh_host}" "find '$src' -type f 2>/dev/null" | wc -l); then dialog::display_error "⛔ ERROR: Could not count files in remote path: $src" return 1 fi if [[ "$total_files" -eq 0 ]]; then dialog::display_error "⛔ ERROR: No files to transfer from remote: $src" return 1 fi { echo 0 rsync -a --info=NAME --no-inc-recursive --ignore-times $exclude_opt "${ssh_user}@${ssh_host}:${src}" "$dest" 2>>"$error_file" | \ while IFS= read -r line; do [[ "$line" =~ ^skipping ]] && continue ((count++)) percent=$((count * 100 / total_files)) printf "XXX\n%d\nProcessing: %s\nXXX\n" "$percent" "$line" done echo 100 } | dialog --title "📥 Downloading Files from ${ssh_user}@${ssh_host}..." --gauge "Transferring from ${ssh_user}@${ssh_host}..." 10 80 0 if [[ -s "$error_file" ]]; then dialog::display_error "⛔ rsync failed:\n$(tail -n 10 "$error_file")" rm -f "$progress_file" "$error_file" return 1 fi rm -f "$progress_file" "$error_file" dialog::display_notice "✅ Download completed successfully." return 0 } ################################################################################################ # ________________________________________________ # / \ # | _________________________________________ | # | | | | # | | user@host:\> _ | | # | | | | # | | | | # | | | | # | | | | # | | | | # | | | | # | | | | # | | | | # | | | | # | | | | # | | | | # | |_________________________________________| | # | | # \_________________________________________________/ # \___________________________________/ # ___________________________________________ # _-' .-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-. --- `-_ # _-'.-.-. .---.-.-.-.-.-.-.-.-.-.-.-.-.-.-.--. .-.-.`-_ # _-'.-.-.-. .---.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-`__`. .-.-.-.`-_ # _-'.-.-.-.-. .-----.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-----. .-.-.-.-.`-_ # _-'.-.-.-.-.-. .---.-. .-------------------------. .-.---. .---.-.-.-.`-_ # :-------------------------------------------------------------------------: # `---._.-------------------------------------------------------------._.---' # ---------------------------------------------------------------- ################################################################################################ # # .d8888b. 888 # d88P Y88b 888 # Y88b. 888 # "Y888b. 888 888 .d8888b 888888 .d88b. 88888b.d88b. # "Y88b. 888 888 88K 888 d8P Y8b 888 "888 "88b # "888 888 888 "Y8888b. 888 88888888 888 888 888 # Y88b d88P Y88b 888 X88 Y88b. Y8b. 888 888 888 # "Y8888P" "Y88888 88888P' "Y888 "Y8888 888 888 888 # 888 # Y8b d88P # "Y88P" # 888b d888 888 # 8888b d8888 888 # 88888b.d88888 888 # 888Y88888P888 8888b. 88888b. 8888b. .d88b. .d88b. 88888b.d88b. .d88b. 88888b. 888888 # 888 Y888P 888 "88b 888 "88b "88b d88P"88b d8P Y8b 888 "888 "88b d8P Y8b 888 "88b 888 # 888 Y8P 888 .d888888 888 888 .d888888 888 888 88888888 888 888 888 88888888 888 888 888 # 888 " 888 888 888 888 888 888 888 Y88b 888 Y8b. 888 888 888 Y8b. 888 888 Y88b. # 888 888 "Y888888 888 888 "Y888888 "Y88888 "Y8888 888 888 888 "Y8888 888 888 "Y888 # 888 # Y8b d88P # "Y88P" ################################################################################################ dependency::command_exists() { # Description: Checks if specified commands exist on the system and installs missing ones. # # This function checks if specified commands are available on the system. If any commands are missing, # it prompts the user to install them. The function supports both terminal and GUI prompts based on # the session type. Additionally, it can automatically install missing commands without prompting # the user when the `--auto` flag is used. # # Globals: # - None # Arguments: # - Command names to check (e.g., "command1 command2"). # - "--auto" (optional): Automatically installs missing commands without prompting. # Outputs: # - Prompts the user to install missing commands if any (unless `--auto` is specified). # Returns: # - 0 if all commands exist. # - 1 if any commands are missing and not installed. # # Usage: # dependency::command_exists [--auto] "command1 command2" # # Example: # dependency::command_exists "curl git" # dependency::command_exists --auto "curl git" # # Notes: # - This function detects the current session type (TTY, SSH, GUI, WAYLAND) to determine # the appropriate prompt method. # - Uses `zenity` for GUI prompts if available. # - Prompts the user to install missing commands and attempts to install them # using `system::install_missing_commands`. # - The `--auto` flag suppresses prompts and automatically installs missing commands. # # End of Documentation local _auto_mode=0 # Check for the --auto flag if [[ "$1" == "--auto" ]]; then _auto_mode=1 shift fi # List of required commands from input local required_commands=("$@") # Collect any missing commands local missing_commands=() for cmd in "${required_commands[@]}"; do if ! command -v "$cmd" &>/dev/null ; then missing_commands+=("$cmd") fi done # If none are missing, we’re good if [[ ${#missing_commands[@]} -eq 0 ]]; then return 0 fi # If in auto mode, just install them (no prompt) if [[ $_auto_mode -eq 1 ]]; then write_status "Installing missing commands: ${missing_commands[*]}" system::install_missing_commands "${missing_commands[@]}" return $? fi # Otherwise, prompt the user once local session_type session_type=$(system::detect_session) local prompt_text="${#missing_commands[@]} command(s) missing: ${missing_commands[*]}.\nInstall now? [y/N]: " local user_response="" # Use zenity if in GUI/Wayland (and if `zenity` is available) if [[ "$session_type" =~ ^(GUI|WAYLAND)$ ]] && command -v zenity &>/dev/null; then if ! zenity --question \ --title="Missing Commands" \ --text="The following commands are missing: ${missing_commands[*]}. \nInstall now?" \ 2>/dev/null; then write_error "Required command(s) not found: ${missing_commands[*]}" return 1 fi else # TTY / SSH or fallback read -r -p "$(echo -e "$prompt_text")" user_response # Convert to lowercase user_response="${user_response,,}" if [[ "$user_response" != "y" && "$user_response" != "yes" ]]; then write_error "Required command(s) not found: ${missing_commands[*]}" return 1 fi fi # Attempt install, return based on success/failure system::install_missing_commands "${missing_commands[@]}" return $? } dependency::desktop() { # Description: Identifies the currently running desktop environment. # # This function simplifies the process of determining the desktop environment currently in use. # It addresses the inconsistencies in using environment variables such as DESKTOP_SESSION and GDMSESSION. # The function checks multiple sources to reliably identify the running desktop environment. # # Globals: # - None # Arguments: # - "$1": The desktop environment to check for (e.g., gnome, kde, xfce, mate). # Outputs: # - Logs the process of identifying the desktop environment. # Returns: # - 0 if the specified desktop environment is found. # - 1 if the specified desktop environment is not found. # # Usage: # dependency::desktop # # Example: # dependency::desktop gnome # Check if there is a running GNOME desktop session # dependency::desktop kde # Check if there is a running KDE desktop session # dependency::desktop xfce # Check if there is a running Xfce desktop session # dependency::desktop mate # Check if there is a running MATE desktop session # # Dependencies: # - system::log_item: For logging the process of identifying the desktop environment. # # Notes: # This function checks various environment variables and processes to identify the desktop # environment.It first attempts to use XDG_CURRENT_DESKTOP. If that is unset, it searches # for known desktop processes. # The function checks for the following desktop environments: # - KDE # - GNOME # - MATE # - Xfce # - Cinnamon # - LXDE # - LXQt # - Unity # - Pantheon # - Sway # - Enlightenment # - Openbox # - IceWM # - Fluxbox # - JWM # # Reference: # The main problem with checking the DESKTOP_SESSION is that it is set by the display # manager rather than the desktop session and is subject to inconsistencies. # For lightdm on Debian, the values come from the names of files under /usr/share/xsessions/. # DESKTOP_SESSION reflects the desktop environment if a specific selection is made at log in, # however the lightdm-xsession is always used the default session. # # GDMSESSION is another option, but seems to have a similar predicament # (it is often the same value as DESKTOP_SESSION). # # XDG_CURRENT_DESKTOP looks like a good choice, however it is currently not in the XDG # standard and thus not always implemented. See here for a discussion of this. This # answer shows its values for different distros/desktops. # # The reasonable fallback for XDG_CURRENT_DESKTOP not existing would be to try # XDG_DATA_DIRS. Provided the data files for the desktop environment are installed # in a directory bearing its name, this approach should work. This will hopefully # be the case for all distros/desktops! # # End of documentation local _sought="${1,,}" [[ -z "${_sought}" ]] && { system::log_item "No desktop environment specified to check for."; return 1; } system::log_item "checking if there is a running $_sought desktop session..." local detected="" # Prefer XDG_CURRENT_DESKTOP when set; normalize tokens and allow colon/semicolon separators. if [[ -n "$XDG_CURRENT_DESKTOP" ]]; then for token in ${XDG_CURRENT_DESKTOP//[:;]/ }; do token="${token,,}" [[ -z "$detected" ]] && detected="$token" if [[ "$token" == *"$_sought"* ]]; then system::log_item "Found desktop via XDG_CURRENT_DESKTOP: $token" return 0 fi done else system::log_item "XDG_CURRENT_DESKTOP appears to be unset, attempting to search processes for desktop match..." fi # Fallback: look for well-known session processes with exact matches to avoid false positives. local proc_map=( "plasmashell:kde" "gnome-shell:gnome" "mate-session:mate" "xfce4-session:xfce" "cinnamon:cinnamon" "lxsession:lxde" "lxqt-session:lxqt" "unity-panel-service:unity" "pantheon-session:pantheon" "sway:sway" "enlightenment:enlightenment" "openbox:openbox" "icewm:icewm" "fluxbox:fluxbox" "jwm:jwm" ) for entry in "${proc_map[@]}"; do local proc="${entry%%:*}" local name="${entry##*:}" if pgrep -x "$proc" &>/dev/null; then detected="$name" break fi done system::log_item "Found: detected=${detected:-unknown}" local detected_lc="${detected,,}" if [[ -n "$detected_lc" && "$detected_lc" == *"$_sought"* ]]; then return 0 fi return 1 } dependency::os_linux() { # Description: Checks if the current operating system is Linux. # # This function simplifies the process of determining if the operating system is Linux. # It provides clear error messages for unsupported operating systems. # # Globals: # - None # Arguments: # - None # Outputs: # - Standard error messages if the OS is not Linux. # Returns: # - 0 if the operating system is Linux. # - 1 if the operating system is not Linux. # # Usage: # dependency::os_linux # # Example: # dependency::os_linux # # Dependencies: # - write_error: Function to log error messages. # # Notes: # - This function checks the OSTYPE environment variable to determine the operating system. # - Provides specific messages for unsupported operating systems like macOS, CYGWIN, MSYS, and FreeBSD. # # End of Documentation local _ostype="${OSTYPE:-}" case "$_ostype" in linux*) # Detect WSL for callers that may need to branch on it if grep -qi "microsoft" /proc/version 2>/dev/null || [[ -n "${WSL_DISTRO_NAME:-}" ]]; then system::log_item "${FUNCNAME[0]}: Detected Linux (WSL)." fi return 0 ;; darwin*) write_error "${FUNCNAME[0]}: Mac OSX is currently not supported." return 1 ;; cygwin*) write_error "${FUNCNAME[0]}: CYGWIN is currently unsupported." return 1 ;; msys*) write_error "${FUNCNAME[0]}: Lightweight shell is currently unsupported." return 1 ;; freebsd*) write_error "${FUNCNAME[0]}: FreeBSD is currently unsupported." return 1 ;; "") # Fallback to uname when OSTYPE is unset or unusual if [[ "$(uname -s 2>/dev/null)" == "Linux" ]]; then return 0 fi write_error "${FUNCNAME[0]}: Unable to determine OS (OSTYPE unset)." return 1 ;; *) write_error "${FUNCNAME[0]}: Unsupported operating system: ${_ostype}." return 1 ;; esac } dependency::search_local() { # Description: # This function searches a predetermined set of paths for a given file and sources it if found. # It is not typically useful to call this directly; it is part of the dependency::file function. # # Globals: # - ${SUDO_USER} (optional): Used to determine user-specific paths. # Arguments: # - "$1": The name of the file to search for. # Outputs: # - Logs the search process and results. # - Sources the found file. # Returns: # - 0 if the file is found and sourced successfully. # - 1 if the file is not found. # # Usage: # Called internally by dependency::file; not intended for direct use. # # Example: # dependency::file "somefile.sh" (calls dependency::search_local internally) # # Dependencies: # - system::log_item: For logging the search process. # # Notes: # - Ensure that the paths to be searched are correct and accessible. # - This function is designed to be used internally and may not be useful when called directly. # - The predetermined paths are: /opt/${_TLA:-"rtd"} (calue of _TLA if set, otherwise "rtd"), # /home/${SUDO_USER}/GIT, /home/${SUDO_USER}/bin, and the script directory (location of calling script ). # - I all esle fails, the function will search the user's entire home directory. # # End of Documentation system::log_item "Requested dependency file: ${1} ..." local script_path="$(readlink -f "$0")" local script_dir="$(dirname "$script_path")" local _tla="${_TLA,,}" local _tla="${_tla:-"rtd"}" local paths=( "$(dirname "$(find /opt/${_tla} -name ${1} 2>/dev/null | head -n 1)")/${1}" "$(dirname "$(find /home/${SUDO_USER}/GIT -name ${1} 2>/dev/null | head -n 1)")/${1}" "$(dirname "$(find /home/${SUDO_USER}/bin -name ${1} 2>/dev/null | head -n 1)")/${1}" "${script_dir}/${1}" "./${1}" ) for path in "${paths[@]}"; do system::log_item "Searching for ${path} ..." if [[ -e "${path}" ]]; then system::log_item "Found ${path}" source "${path}" return 0 fi done return 1 } dependency::file() { # Description: Checks for a Bash library or script file and sources it. # # This function searches a predetermined set of paths for a given file and sources it if found. # It first searches for the file locally in a set of predefined paths. If the file is found, it is sourced. # If the file is not found locally, the function attempts to download it from a specified URL and source it. # If the download is successful, the file is sourced. If the download fails, the function logs an error and exits. # # Globals: # - _GIT_PROFILE: The GitHub profile name to use for downloading the file. # Arguments: # - "$1": The name of the file to search for and source. # Outputs: # - None # Returns: # - 0 if the file is sourced successfully, 1 if the file is not found or download fails. # # Usage: # dependency::file # # Example: # dependency::file _config.ini # # Dependencies: # This function depends on the following functions: # - system::log_item: Logs messages. # - dependency::search_local: Searches for a file locally in predefined paths. # # Notes: # - Ensure the GitHub profile and repository structure are correct for downloading the file. # # End of Documentation local _src_url="https://github.com/${_GIT_PROFILE:-vonschutter}/RTD-Setup/raw/main/core/${1}" local _tgt="${1}" if dependency::search_local "${1}"; then return 0 else system::log_item "$(date) failure to find $1 on the local computer, now searching online..." local tmpdir=$(mktemp -d) if curl -sL "$_src_url" -o "${tmpdir}/${1}"; then system::log_item "Using: ${_src_url} directly from URL..." source "${tmpdir}/${1}" elif wget "${_src_url}" -O "${tmpdir}/${1}" &>/dev/null; then source "${tmpdir}/${1}" system::log_item "Using: ${_src_url} downloaded..." else system::log_item "Failed to find ${1} " return 1 fi rm -rf "${tmpdir}" fi } dependency::check_hardware_virtualization_support() { # Description: Checks if the hardware supports virtualization (VT-x or AMD-V). # # This function checks if the hardware on the system supports virtualization technologies # such as Intel VT-x or AMD-V. It provides status messages indicating the presence or # absence of virtualization support. # # Globals: # - None # Arguments: # - "--quiet" (optional): If specified, suppresses status messages. # Outputs: # - Status messages indicating hardware support for virtualization. # Returns: # - 0 if hardware supports virtualization. # - 1 if hardware does not support virtualization. # # Usage: # dependency::check_hardware_virtualization_support [--quiet] # # Example: # dependency::check_hardware_virtualization_support # dependency::check_hardware_virtualization_support --quiet # # Notes: # - This function uses the `lscpu` command to check for virtualization support. # - It looks for the presence of 'vmx' (Intel VT-x) or 'svm' (AMD-V) flags in the CPU info. # - Status messages can be suppressed using the `--quiet` flag. # # End of Documentation local _quiet_mode=0 if [[ "$1" == "--quiet" ]]; then _quiet_mode=1 fi check_hardware_virtualization_support::_log_status() { if [[ $_quiet_mode -eq 0 ]]; then write_status "$1" else system::log_item "$1" fi } check_hardware_virtualization_support::_log_error() { if [[ $_quiet_mode -eq 0 ]]; then write_error "$1" else system::log_item "$1" fi } if lscpu | grep -E 'vmx|svm' >/dev/null 2>&1; then check_hardware_virtualization_support::_log_status "⛼ Hardware supports virtualization (VT-x or AMD-V)." return 0 else check_hardware_virtualization_support::_log_error "🚫 Hardware virtualization support (VT-x or AMD-V) not found. Virtual machines may not run efficiently." return 1 fi } dependency::check_if_running_in_vm() { # Description: Checks if the system is running inside a virtual machine. # # This function checks if the system is running inside a virtual machine by using the `virt-what` command. # It provides status messages indicating whether the system is virtualized and attempts to install `virt-what` # if it is not already installed. # # Globals: # - None # Arguments: # - "--quiet" (optional): If specified, suppresses status messages. # Outputs: # - Status messages indicating if the system is running inside a virtual machine. # - Attempts to install `virt-what` if it is not found. # Returns: # - 0 if the system is not running inside a virtual machine. # - 1 if `virt-what` could not be installed or if virtualization status cannot be determined. # - 2 if the system is running inside a virtual machine. # # Usage: # dependency::check_if_running_in_vm [--quiet] # # Example: # dependency::check_if_running_in_vm # dependency::check_if_running_in_vm --quiet # # Notes: # - This function uses `virt-what` to detect virtualization. # - If `virt-what` is not installed, it attempts to install it using `software::add_native_package`. # - Status messages can be suppressed using the `--quiet` flag. # # End of Documentation local _quiet_mode=0 if [[ "$1" == "--quiet" ]]; then _quiet_mode=1 fi check_if_running_in_vm::_log_status() { if [[ $_quiet_mode -eq 0 ]]; then write_status "$1" else system::log_item "$1" fi } check_if_running_in_vm::_log_error() { if [[ $_quiet_mode -eq 0 ]]; then write_error "$1" else system::log_item "$1" fi } if command -v virt-what >/dev/null 2>&1; then local _vm_type=$(virt-what) if [ -n "$_vm_type" ]; then check_if_running_in_vm::_log_status "⛼ Running inside a virtual machine: $_vm_type" return 2 else check_if_running_in_vm::_log_status "⛼ Not running inside a virtual machine." return 0 fi else check_if_running_in_vm::_log_error "⛔ virt-what is not installed. Unable to determine if running inside a VM." # Attempt to install virt-what if not found software::add_native_package "virt-what" # Re-run virt-what after installation if command -v virt-what >/dev/null 2>&1; then local _vm_type=$(virt-what) if [ -n "$_vm_type" ]; then check_if_running_in_vm::_log_status "⛼ Running inside a virtual machine: $_vm_type" return 2 else check_if_running_in_vm::_log_status "⛼ Not running inside a virtual machine." return 0 fi else check_if_running_in_vm::_log_error "💥 Failed to install virt-what. Unable to determine if running inside a VM." return 1 fi fi } dependency::virtualization() { # Description: Checks for required components for making virtual machines and verifies hardware support for virtualization. # # This function checks if the necessary software (KVM, QEMU, etc.) for creating virtual machines is installed and # installs it if missing. It also verifies if the hardware supports virtualization (VT-x or AMD-V) and checks if # the script is running inside a virtual machine using `virt-what`. # # Globals: # - None # Arguments: # - "--quiet" (optional): If specified, suppresses status messages. # Outputs: # - Status messages indicating the presence or installation of virtualization software (unless --quiet is specified). # - Warnings if hardware virtualization is not supported or if running inside a VM. # Returns: # - 0 if virtualization is supported on hardware. # - 1 if virtualization is not supported. # - 2 if virtualization is supported but the system is running in a VM. # # Usage: # dependency::virtualization [--quiet] # # Example: # dependency::virtualization # dependency::virtualization --quiet # # Notes: # - This function is designed to work on multiple Linux distributions including openSUSE, Fedora, RHEL, CentOS, Debian, and Ubuntu. # - Ensures that KVM and QEMU are available, and installs them if missing. # - Verifies hardware support for virtualization using `lscpu` command. # - Checks if running inside a virtual machine using `virt-what`. # # End of Documentation local _quiet_mode=0 if [[ "$1" == "--quiet" ]]; then _quiet_mode=1 fi dependency::virtualization_log_status() { if [[ $_quiet_mode -eq 0 ]]; then write_status "$1" else system::log_item "$1" fi } dependency::virtualization_log_error() { if [[ $_quiet_mode -eq 0 ]]; then write_error "$1" else system::log_item "$1" fi } # Perform checks dependency::check_hardware_virtualization_support local _hv_support=$? dependency::check_if_running_in_vm local _vm_check=$? if [[ $_hv_support -eq 0 && $_vm_check -eq 0 ]]; then write_status "✅ Virtualization support and environment check passed." elif [[ $_hv_support -eq 1 ]]; then return 1 elif [[ $_vm_check -eq 2 ]]; then return 2 fi local _OS_NAME=$(hostnamectl | awk '/Operating System/ {print $3}') dependency::virtualization_log_status "${FUNCNAME[0]}: Checking for virtualization software..." case $_OS_NAME in openSUSE | Tumbleweed) if ! command -v qemu-system-x86_64 >/dev/null 2>&1; then dependency::virtualization_log_status "KVM or QEMU not found. Installing virtualization software for $_OS_NAME..." software::add_native_package "kvm_server" software::add_native_package "kvm_tools" sudo systemctl enable --now libvirtd else dependency::virtualization_log_status "KVM and QEMU are available..." fi if ! command -v virt-install >/dev/null 2>&1; then dependency::virtualization_log_status "virt-install not found. Installing virt-install for $_OS_NAME..." software::add_native_package "virt-install" :${BIN_VIRT_INSTALL:=$(type -P virt-install)} else dependency::virtualization_log_status "virt-install is available..." fi ;; Fedora | RHEL | CentOS) if ! command -v qemu-system-x86_64 >/dev/null 2>&1; then dependency::virtualization_log_status "KVM or QEMU not found. Installing virtualization software for $_OS_NAME..." software::add_native_package "@virtualization" sudo systemctl enable --now libvirtd else dependency::virtualization_log_status "KVM and QEMU are available..." fi if ! command -v virt-install >/dev/null 2>&1; then dependency::virtualization_log_status "virt-install not found. Installing virt-install for $_OS_NAME..." software::add_native_package "virt-install" :${BIN_VIRT_INSTALL:=$(type -P virt-install)} else dependency::virtualization_log_status "virt-install is available..." fi ;; Debian | Ubuntu | TUXEDO | *) if ! command -v qemu-system-x86_64 >/dev/null 2>&1; then dependency::virtualization_log_status "KVM or QEMU not found. Installing virtualization software for $_OS_NAME..." local _count=1 for _dependency in qemu-kvm libvirt-daemon bridge-utils virtinst libvirt-daemon-system libosinfo-bin; do dependency::virtualization_log_status "${_count} - Verifying dependency (${_dependency})..." software::add_native_package "$_dependency" _count=$((_count + 1)) done sudo systemctl enable --now libvirtd else dependency::virtualization_log_status "KVM and QEMU are available..." fi if ! command -v virt-install >/dev/null 2>&1; then dependency::virtualization_log_status "virt-install not found. Installing virt-install for $_OS_NAME..." software::add_native_package "virt-install" :${BIN_VIRT_INSTALL:=$(type -P virt-install)} else dependency::virtualization_log_status "virt-install is available..." fi ;; esac wait # Wait for all background processes to finish } system::prepare_environment_for_iso_creation() { # Description: Function to check that all dependencies are available for manipulating the # net install ISO from Ubuntu. Subsequently, the temporary file locations # are setup and templates are downloaded. # # Several key software components are required to create ISO files (virtual DVD/CD/BlueRay) # as well as a few others needed to download files from the internet, etc. # # Globals: # Arguments: None # Outputs: # Returns: # Usage: # DEPENDENCIES: dos2unix, cpio, gzip, genisoimage, whois, pwgen, wget, fakeroot, xorriso. # # This function will find full paths for the binaries needed since if they are recently # added, they may not be found in the current $PATH. # # Software PATH insurance: # : "${bin_7z:=$(type -P 7z)}" # : "${bin_xorriso:=$(type -P xorriso)}" # : "${bin_cpio:=$(type -P gnucpio || type -P cpio)}" # : "${bin_qemu_img:=$(type -P qemu-img)}" # : "${bin_kvm:=$(type -P kvm)}" # : "${bin_qemu_system_x86_64:=$(type -P qemu-system-x86_64 )}" # : "${put_iso_file_here_when_done:="/home/$SUDO_USER/Virtual-DVDs"}" # : "${put_qcow_file_here_when_done:="/home/$SUDO_USER/Virtual-HDs"}" # : "${VOLUME_TITLE:="RTD Auto Installer"}" # : "${ssh_public_key_file:="/home/$SUDO_USER/.ssh/id_rsa.pub"}" # : "${permanent_download_dir:="/home/$SUDO_USER/Virtual-DVDs/Downloaded"}" # # The main tool to create ISO files system is "xorriso". # [xorriso] copies file objects from POSIX compliant filesystems into Rock Ridge # enhanced ISO 9660 filesystems and allows session-wise manipulation of such filesystems. # It can load the management information of existing ISO images and it writes the session results # to optical media or to filesystem objects. # # At this time this function expects no arguments. # # End of Documentation if [[ "$1" == "--cleanup" ]]; then for dir in "$tmp_download_dir" "$tmp_disc_dir" "$tmp_initrd_dir"; do if [ -z "$dir" ]; then system::log_item "Warning: Directory variable is not set for the directory cleanup." fi if [ "$dir" = "/" ] || [[ "$dir" =~ ^/[^/]+/?$ ]] || [ "$dir" = "$HOME" ]; then system::log_item "Error: Attempt to delete critical directory $dir" else if [ -d "$dir" ]; then system::log_item "Cleaning up temporary directory: $dir" rm -rf "$dir" else system::log_item "Warning: Directory $dir does not exist." fi fi done unset bin_7z unset bin_xorriso unset bin_cpio unset bin_qemu_img unset bin_kvm unset bin_qemu_system_x86_64 unset put_iso_file_here_when_done unset put_qcow_file_here_when_done unset VOLUME_TITLE unset ssh_public_key_file unset permanent_download_dir unset isohdpfx_bin unset current_dir unset script_dir unset tmp_download_dir unset tmp_disc_dir unset tmp_initrd_dir unset _dependencies return 0 else system::log_item "Preparing environment for ISO creation..." : "${target_iso="$put_iso_file_here_when_done/$target_iso_file_name"}" # List of package dependencies for the script depending on the distro (naming may vary) if cat "/etc/os-release" | grep "debian"; then _dependencies="dos2unix cpio gzip genisoimage wget fakeroot xorriso isolinux qemu-system libvirt-daemon-system" elif cat "/etc/os-release" | grep "fedora"; then _dependencies="dos2unix cpio gzip genisoimage wget fakeroot xorriso isolinux qemu-system libvirt-daemon-system" elif cat "/etc/os-release" | grep "suse"; then _dependencies="dos2unix cpio gzip genisoimage wget fakeroot xorriso isolinux qemu-x86 libvirt-daemon-qemu" else write_error "Neither debian, fedora, or suse base distros or derivatives could be found; NOT installing dependencies." return 1 fi # set index to be augmented and count the total number of dependencies local index=0 local total_dependencies=0 for dep in $_dependencies; do ((total_dependencies++)) done # Check for dependencies for i in $_dependencies; do software::check_native_package_dependency $i && ((index++)) done # Compare the count of available dependencies to the total number of dependencies if [[ $index -eq $total_dependencies ]]; then system::log_item "All dependencies are available." else write_error "⛔ Some dependencies are missing. Available: $index, Required: $total_dependencies" return 1 fi : "${bin_7z:=$(type -P 7z)}" : "${bin_xorriso:=$(type -P xorriso)}" : "${bin_cpio:=$(type -P gnucpio || type -P cpio)}" : "${bin_qemu_img:=$(type -P qemu-img)}" : "${bin_kvm:=$(type -P kvm)}" : "${bin_qemu_system_x86_64:=$(type -P qemu-system-x86_64)}" : "${put_iso_file_here_when_done:="/home/$SUDO_USER/Virtual-DVDs"}" : "${put_qcow_file_here_when_done:="/home/$SUDO_USER/Virtual-HDs"}" : "${VOLUME_TITLE:="🤖 RTD_AUTO_INSTALLER 🤖"}" : "${ssh_public_key_file:="/home/$SUDO_USER/.ssh/id_rsa.pub"}" : "${permanent_download_dir:="/home/$SUDO_USER/Virtual-DVDs/Downloaded"}" system::log_item "Checking for presence of (isohdpfx.bin)..." if [[ -e /usr/lib/ISOLINUX/isohdpfx.bin ]]; then : "${isohdpfx_bin:="/usr/lib/ISOLINUX/isohdpfx.bin"}" write_information "Found: $isohdpfx_bin" else write_warning "An important file (isohdpfx.bin) was not found in the expected location. Attepting to workaround the issue..." if hash locate 2>/dev/null; then locate isohdpfx.bin || ( write_error "The file isohdpfx.bin could not be found or fetched. Please correct manually!" pause::for_input 1 return 1 ) : "${isohdpfx_bin:=$(locate isohdpfx.bin | head -n 1)}" write_information "Found: $isohdpfx_bin" else software::check_native_package_dependency plocate && ( write_warning "Updating file location DB, this may take a long time if there are a lot of files on the system..." updatedb ) locate isohdpfx.bin || ( write_error "The file isohdpfx.bin could not be found or fetched. Please correct manually!" pause::for_input 1 return 1 ) : "${isohdpfx_bin:=$(locate isohdpfx.bin | head -n 1)}" write_information "Found: $isohdpfx_bin" fi fi if [[ ! -f "$ssh_public_key_file" ]]; then write_warning "Error: public SSH key $ssh_public_key_file not found! You will need to setup automatic login using ssh manually each time you build a server with this media." fi current_dir="$(pwd)" script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" tmp_download_dir=$(mktemp -d) tmp_disc_dir=$(mktemp -d) for i in $put_qcow_file_here_when_done $put_iso_file_here_when_done $permanent_download_dir; do mkdir -p "$i" && chown "$SUDO_USER":"$SUDO_USER" "$i" done fi } system::download_and_manipulate_iso_debian() { # Description: # system::download_and_manipulate_iso_debian "debian" "openssh-server" "MinecraftTasks" # This is a simple command sequence to read the preference of distribution to install # and what release to get... then download the network install file from debian # and manipulate the iso file to complete the edited instruction in the preseed.cfg... # # Globals: # Arguments: None # Outputs: # Returns: # Usage: # # system::download_and_manipulate_iso_debian [ISO Name] [Environment] [Addon Task] # # where [ISO Name] may be: any string to starte the file name with # where [Environment] may be: any debian supported desktop environment # where [Addon Task] may be: additional pre determined configuration # # EXAMPLE: # system::download_and_manipulate_iso_debian openssh-server MinecraftTasks # system::download_and_manipulate_iso_debian gnome-desktop # system::download_and_manipulate_iso_debian xfce-desktop # # End of Documentation kvm::util::read_common_options "$@" kvm::util::read_distro_options --distribution debian "$@" system::prepare_environment_for_iso_creation # Determine the name of the ISO file to create, and wich iso to download using default values if none are provided. # Capitlal variable names are generally global and set in the _loctations file. # Variables will be populated either from the default _locations file or detected. : "${_SOURCE_ISO_URL:="https://cdimage.debian.org/debian-cd/current/amd64/iso-cd/$(curl --silent https://cdimage.debian.org/debian-cd/current/amd64/iso-cd/SHA256SUMS | \grep -o "debian-.*netinst*.iso" | grep -v mac | grep -v edu)"}" : "${_SOURCE_FIRMWARE:="http://cdimage.debian.org/cdimage/unofficial/non-free/firmware/buster/current/firmware.zip"}" : "${_CURRENT_DEB_ISO:="$(curl --silent https://cdimage.debian.org/debian-cd/current/amd64/iso-cd/SHA256SUMS | \grep -o "debian-.*netinst*.iso" | grep -v mac | grep -v edu)"}" : "${target_iso_file_name="Auto_Install_${_CURRENT_DEB_ISO%.iso}-${_role:-"ssh-server"}-${_UserDesktopEnvironmentSelection:-cli}.iso"}" target_iso="$put_iso_file_here_when_done/$target_iso_file_name" iso_store="$put_iso_file_here_when_done/Downloaded/$_CURRENT_DEB_ISO" tmp_initrd_dir=$(mktemp -d) if [[ ! -f "$iso_store" ]]; then write_status "Geting install image..." write_information "Download: $_SOURCE_ISO_URL" if ! wget -4 -q --show-progress "$_SOURCE_ISO_URL" -O "$iso_store"; then write_error "FAILED to download $_SOURCE_ISO_URL" system::prepare_environment_for_iso_creation --cleanup return 1 fi write_information "Retreived: $(ls "$iso_store")" fi if [[ ! -s "$iso_store" ]]; then write_error "Downloaded ISO is missing or empty: $iso_store" system::prepare_environment_for_iso_creation --cleanup return 1 fi if ! "$bin_7z" x "$iso_store" "-o$tmp_disc_dir"; then write_error "FAILED to extract $_SOURCE_ISO_URL" system::prepare_environment_for_iso_creation --cleanup return 1 fi mkdir -p "$tmp_initrd_dir/custom" && system::log_item "Created custom directory in $tmp_disc_dir" || write_error "FAILED to create custom directory in $tmp_disc_dir" # write_status "Geting non-free firmware..." # write_information "Download: $_SOURCE_FIRMWARE" # wget -4 -q --show-progress "$_SOURCE_FIRMWARE" -O "$tmp_download_dir/firmware.zip" && ( mkdir "$tmp_disc_dir/firmware" & "$bin_7z" x "$tmp_download_dir/firmware.zip" "-o$tmp_disc_dir/firmware" ) # write_information "Retreived: $(ls "$tmp_disc_dir"/firmware )" # Load the installation answers template. write_status "Getting intall instructions template... " system::make_preseed_cfg --saveto ${tmp_initrd_dir} --ask YES || ( write_error "Failed to create preseed.cfg template" dialog::display_error "Something went wrong, please check the logfile $_LOGFILE" ) # ----------------------- Modify Boot Media --------------------------- # # Edit the grub.cfg file... #sed -i '/timeout/s/.*/set timeout=10/' "$tmp_disc_dir/boot/grub/grub.cfg" sed -i s/"menuentry --hotkey=g 'Graphical install' {"/"menuentry --hotkey=g 'Automatic Graphical install' {"/g "$tmp_disc_dir/boot/grub/grub.cfg" sed -i s/"linux /install.amd/vmlinuz vga=788 --- quiet {"/"linux /install.amd/vmlinuz vga=788 theme=dark --- quiet {"/g "$tmp_disc_dir/boot/grub/grub.cfg" sed -i s/"menuentry --hotkey=i 'install' {"/"menuentry --hotkey=g 'Automatic install' {"/g "$tmp_disc_dir/boot/grub/grub.cfg" #sed -i '/insmod play/d' "$tmp_disc_dir/boot/grub/grub.cfg" sed -i 's/^[[:space:]]*set gfxmode=800x600$/ set gfxmode=1024x768/' "$tmp_disc_dir/boot/grub/grub.cfg" # Edit the gtk.cfg file... sed -i s/"menu label ^Graphical install"/"menu label ^Automatic Graphical install"/g "$tmp_disc_dir/isolinux/gtk.cfg" # Edit the txt.cfg file... sed -i s/"menu label ^Install"/"menu label ^Automatic Install"/g "$tmp_disc_dir/isolinux/txt.cfg" # Edit the spkgtk.cfg file... sed -i 's|ontimeout /install.amd/vmlinuz vga=788 initrd=/install.amd/gtk/initrd.gz speakup.synth=soft --- quiet|ontimeout /install.amd/vmlinuz vga=788 initrd=/install.amd/gtk/initrd.gz theme=dark --- quiet|' "$tmp_disc_dir/isolinux/spkgtk.cfg" dos2unix "$tmp_disc_dir/isolinux/isolinux.cfg" sed -i 's/^timeout 0$/timeout 100/' "$tmp_disc_dir/isolinux/isolinux.cfg" # --------------------------------------------------------------------- # system::download_and_manipulate_iso_debian::modify_installer() { local initrd_path="$1" write_status "🔧 Modifying the installer: $initrd_path" gzip -d -c "$initrd_path" >"$tmp_initrd_dir/initrd" || { write_error "Failed to extract $initrd_path" return 1 } for file in "preseed.cfg" "task.sh" "custom"; do if [ -e "./$file" ]; then if [ "$file" == "custom" ]; then find "./custom" | fakeroot "$bin_cpio" -o -H newc -A -F "./initrd" else echo "./$file" | fakeroot "$bin_cpio" -o -H newc -A -F "./initrd" fi else write_error "No $file found in $(pwd)" fi done write_status "🔩📀 Injecting created content into the new INITRD in the folder $tmp_disc_dir ..." if cat "$tmp_initrd_dir/initrd" | gzip -9c >"$initrd_path"; then write_status "🔧 Modified the installer: $initrd_path" else write_error "❌ Failed to modify the installer: $initrd_path" fi rm "$tmp_initrd_dir/initrd" || write_error "Failed to remove initrd file" } pushd "$tmp_initrd_dir" || { write_error "Failed to enter init directory: $tmp_initrd_dir" exit 1 } system::download_and_manipulate_iso_debian::modify_installer "$tmp_disc_dir/install.amd/initrd.gz" || pause::for_input 1 "Failed to modify installer: $tmp_disc_dir/install.amd/initrd.gz" system::download_and_manipulate_iso_debian::modify_installer "$tmp_disc_dir/install.amd/gtk/initrd.gz" || pause::for_input 1 "Failed to modify installer: $tmp_disc_dir/install.amd/gtk/initrd.gz" popd || { write_error "Failed to popd" pause::for_input 1 } oem::setup_brand_splash_screen "$tmp_disc_dir/splash.png" "${_FULLNAME:-"RunTime Data"} OEM Install: $PREFERENCE" system::create_iso_image_debian --target-iso "$target_iso" --iso-source-dir "$tmp_disc_dir" ret_val=$? if [[ $ret_val -eq 255 ]]; then system::log_item "User declined to create a bootable thumbdrive from the new ISO media." return 255 elif [[ $ret_val -eq 0 ]]; then system::log_item "Successfully created a bootable thumbdrive from the new ISO media." return 0 else pause::for_input 1 "An error may have occurred (review above outout) then: Press [ENTER] to continue:" return 1 fi } system::create_iso_image_debian() { # Description: Function to generate the new ISO file from the extracted and # altered original ISO. # Delete old iso file if there... # # Globals: # Arguments: None # Outputs: # Returns: # Usage: # End of Documentation # Check for other input and create them if not defined... # : "${target_iso}" # : "${put_iso_file_here_when_done}" system::validate_parameters ${*} while [[ $# -gt 0 ]]; do case $1 in --target-iso) target_iso="$2" shift 2 ;; --iso-source-dir) iso_source_dir="$2" shift 2 ;; --isohdpfx-bin) isohdpfx_bin="$2" shift 2 ;; --prompt) prompt="$2" shift 2 ;; *) write_error "Unknown option: $1" shift ;; esac done local rc=0 local missing_files=0 system::log_item "Check for required information (a target and source directory)..." system::check_required_variables target_iso iso_source_dir isohdpfx_bin write_status "------ CREATING ISO: ${target_iso} --------" write_information "Volume Title: ${VOLUME_TITLE:-"RTD"}" write_information "ISO Source: ${iso_source_dir}" write_information "ISO Target: ${target_iso}" write_information "ISO Boot Media: ${isohdpfx_bin}" if [ -f "${target_iso}" ]; then rm -f "${target_iso}" fi if ! pushd "${iso_source_dir}"; then system::log_item "Failed to enter ${iso_source_dir}" system::prepare_environment_for_iso_creation --cleanup return 1 fi [[ -e '[BOOT]' ]] && (rm -r '[BOOT]' || system::log_item "Failed to delete [BOOT] directory") for req in "isolinux/isolinux.bin" "isolinux/boot.cat" "isolinux/isolinux.cfg"; do if [[ ! -f "$req" ]]; then write_error "Missing required boot file: ${iso_source_dir}/$req" missing_files=1 fi done if [[ $missing_files -ne 0 ]]; then popd system::prepare_environment_for_iso_creation --cleanup return 1 fi if ! "$bin_xorriso" -as mkisofs -r \ -V "${VOLUME_TITLE:-"RTD"}" \ -J -b "isolinux/isolinux.bin" \ -c boot.cat \ -no-emul-boot \ -boot-load-size 4 \ -boot-info-table \ -input-charset utf-8 \ -isohybrid-mbr "${isohdpfx_bin}" \ -eltorito-alt-boot \ -e boot/grub/efi.img \ -no-emul-boot \ -isohybrid-gpt-basdat -o "${target_iso}" ./ &>>"${_LOGFILE}"; then pause::for_input 1 "An error may have occurred (review above outout) then: Press [ENTER] to continue:" rc=1 fi if [[ $rc -eq 0 ]] && ! chmod 777 "${target_iso}" &>>"${_LOGFILE}"; then pause::for_input 1 "${FUNCNAME[0]}: An error may have occurred (review above outout) then: Press [ENTER] to continue:" rc=1 fi popd || { write_error "Failed to popd" pause::for_input 1 rc=1 } if [[ $rc -eq 0 ]]; then tool::test_iso_boot_media "${target_iso}" rc=$? fi if [[ $rc -ne 0 ]]; then system::prepare_environment_for_iso_creation --cleanup return $rc fi while true; do exec 3>&1 selection=$($RTD_GUI \ --backtitle "$BRANDING" --title "Create Automatic OS Install Media" --menu "Please select which media to create:" "$HEIGHT" "$WIDTH" "$LIST_HEIGHT" \ "1" "Create bootable USB Media" \ "2" "Add ISO to my Ventoy media (for multiple boot)" \ "3" "Just show me the location of the new ISO file" \ 2>&1 1>&3) exit_status=$? exec 3>&- clear case $exit_status in "$DIALOG_CANCEL") rc=255 break ;; "$DIALOG_ESC") rc=255 break ;; esac case $selection in 1) if $RTD_GUI --yesno "Proceed to make a bootable thumbdrive from this ISO media (${target_iso})?" "$HEIGHT" "$WIDTH"; then create_physical_media_from_iso "$target_iso" rc=$? else rc=255 fi break ;; 2) if $RTD_GUI --yesno "Proceed to add this ISO (${target_iso}) to your Ventoy media for multiple boot options?" "$HEIGHT" "$WIDTH"; then util::add_iso_to_ventoy "$target_iso" rc=$? else rc=255 fi break ;; 3) $RTD_GUI --msgbox "The new ISO file is located at: $target_iso" "$HEIGHT" "$WIDTH" rc=0 break ;; *) dialog::display_error "Invalid selection: $selection" ;; esac done system::prepare_environment_for_iso_creation --cleanup return $rc } system::detect_session() { # Description: Detects the current session type (TTY, SSH, GUI, Wayland). # Globals: None # Arguments: None # Outputs: Echoes and stores the detected session type in the _SESSION_TYPE variable. # Possible values are: # - SSH # - GUI # - WAYLAND # - TTY # # Returns: 0 # Examples: # - system::detect_session; echo $_SESSION_TYPE # - MySession=$(system::detect_session); echo $MySession # # Dependencies: A standard Linux system with Bash and common utilities. # # End of Documentation if [ -n "${SSH_CLIENT}" ] || [ -n "${SSH_TTY}" ]; then _SESSION_TYPE="SSH" system::log_item "👌 SSH session detected." elif [ -n "${DISPLAY}" ]; then _SESSION_TYPE="GUI" system::log_item "👌 An X session is running." elif [ -n "$WAYLAND_DISPLAY" ]; then _SESSION_TYPE="WAYLAND" system::log_item "👌 A Wayland session is running." else _SESSION_TYPE="TTY" system::log_item "👌 TTY session detected." fi echo "${_SESSION_TYPE}" return 0 } system::laptop_detect() { # Description: Determine if the current system appears to be a laptop without relying on the laptop-detect utility. # # This function uses multiple heuristics to classify the host form factor: # - systemd's hostnamectl chassis information (if available) # - SMBIOS/DMI chassis type via /sys/class/dmi/id/chassis_type or dmidecode # - Presence of battery power supplies and lid switches exposed in sysfs/procfs # - Common laptop keywords in the hardware product name # # Globals: # - _SYSTEM_FORM_FACTOR: Updated to LAPTOP or DESKTOP. # - RTD_IS_LAPTOP: Set to 1 when a laptop is detected, 0 otherwise. # Arguments: # - "--quiet": Suppress user-facing status output (logs only). # - "--explain": Print the detection reason alongside the result. # - "--help": Display usage information. # Outputs: # - Echoes "laptop" or "desktop". # - With --explain, echoes ":". # - Status/log messages describing the detection path. # Returns: # - 0 when laptop indicators are found. # - 1 when indicators are absent and the host is assumed to be a desktop/unknown. # - 2 on invalid usage. # # Usage: # system::laptop_detect # system::laptop_detect --quiet --explain # # Dependencies: # - Optional: hostnamectl, dmidecode (only used when present). # # End of Documentation local _quiet_mode=0 local _print_reason=0 while [[ $# -gt 0 ]]; do case "$1" in --quiet) _quiet_mode=1 shift ;; --explain) _print_reason=1 shift ;; --help) cat <<-'EOF' Usage: system::laptop_detect [--quiet] [--explain] Detect whether the current host appears to be a laptop. The function prints "laptop" (exit 0) or "desktop" (exit 1) and can optionally include the reason for the decision with --explain. EOF return 0 ;; *) write_error "system::laptop_detect: Unknown option '$1'" return 2 ;; esac done local _result="desktop" local _exit_code=1 local _reason_summary="" local -a _positive_matches=() local -a _negative_matches=() system::laptop_detect::_log() { if [[ $_quiet_mode -eq 0 ]]; then write_status "$1" else system::log_item "$1" fi } # Heuristic 1: hostnamectl chassis classification. if command -v hostnamectl >/dev/null 2>&1; then local _hostnamectl_chassis _hostnamectl_chassis=$(hostnamectl 2>/dev/null | awk -F: '/Chassis/ {gsub(/^[ \t]+/, "", $2); print tolower($2); exit}') if [[ -n "$_hostnamectl_chassis" ]]; then case "$_hostnamectl_chassis" in laptop | portable | notebook | convertible | tablet | handheld) _positive_matches+=("hostnamectl chassis '${_hostnamectl_chassis}'") ;; desktop | server | vm | container) _negative_matches+=("hostnamectl chassis '${_hostnamectl_chassis}'") ;; esac fi fi # Heuristic 2: SMBIOS/DMI chassis type codes. local _dmi_file="/sys/class/dmi/id/chassis_type" if [[ -r "$_dmi_file" ]]; then local _dmi_code _dmi_code=$(<"$_dmi_file") _dmi_code=${_dmi_code//[!0-9]/} if [[ -n "$_dmi_code" ]]; then case "$_dmi_code" in 8) _positive_matches+=("SMBIOS chassis type 8 (Portable)") ;; 9) _positive_matches+=("SMBIOS chassis type 9 (Laptop)") ;; 10) _positive_matches+=("SMBIOS chassis type 10 (Notebook)") ;; 11) _positive_matches+=("SMBIOS chassis type 11 (Hand Held)") ;; 14) _positive_matches+=("SMBIOS chassis type 14 (Sub Notebook)") ;; 30) _positive_matches+=("SMBIOS chassis type 30 (Tablet)") ;; 31) _positive_matches+=("SMBIOS chassis type 31 (Convertible)") ;; 32) _positive_matches+=("SMBIOS chassis type 32 (Detachable)") ;; 3) _negative_matches+=("SMBIOS chassis type 3 (Desktop)") ;; 4) _negative_matches+=("SMBIOS chassis type 4 (Low-profile Desktop)") ;; 5) _negative_matches+=("SMBIOS chassis type 5 (Pizza Box)") ;; 6) _negative_matches+=("SMBIOS chassis type 6 (Mini Tower)") ;; 7) _negative_matches+=("SMBIOS chassis type 7 (Tower)") ;; 13) _negative_matches+=("SMBIOS chassis type 13 (All-in-One)") ;; *) _negative_matches+=("SMBIOS chassis type ${_dmi_code}") ;; esac fi elif command -v dmidecode >/dev/null 2>&1 && [[ $EUID -eq 0 ]]; then local _dmidecode_output _dmidecode_output=$(dmidecode -s chassis-type 2>/dev/null | head -n 1) if [[ -n "$_dmidecode_output" ]]; then local _dmidecode_lower=${_dmidecode_output,,} case "$_dmidecode_lower" in laptop | portable | notebook | sub\ notebook | hand\ held | handheld | tablet | convertible) _positive_matches+=("dmidecode chassis '${_dmidecode_output}'") ;; desktop | tower | server | mini-tower | space-saving | all\ in\ one | rack\ mount) _negative_matches+=("dmidecode chassis '${_dmidecode_output}'") ;; esac fi fi # Preserve nullglob state while checking for battery and lid devices. local _nullglob_restore if ! _nullglob_restore=$(shopt -p nullglob 2>/dev/null); then _nullglob_restore='shopt -u nullglob' fi shopt -s nullglob # Heuristic 3: Detect battery power supplies. local battery_path="" for battery_path in /sys/class/power_supply/BAT*; do if [[ -r "$battery_path/type" ]]; then local battery_type="" read -r battery_type <"$battery_path/type" if [[ "${battery_type,,}" == "battery" ]]; then _positive_matches+=("Power supply $(basename "$battery_path") reports a Battery device") break fi fi done # Heuristic 4: Detect lid switches. local lid_path="" for lid_path in /proc/acpi/button/lid/*; do if [[ -r "$lid_path/state" ]]; then _positive_matches+=("ACPI lid device $(basename "$lid_path") is present") break fi done eval "$_nullglob_restore" # Heuristic 5: Look for laptop keywords in the product name. if [[ -r /sys/class/dmi/id/product_name ]]; then local _product_name="" read -r _product_name $1/ks.cfg } system::make_preseed_cfg() { # Description: Function to write out a debina preeseed file to a location requested by the first parameter. # Other parameters accepted are CONFIG (multiselect de), SRVFUNC (task "Minecraft" "Tasks") # some parameters have defaults. # Globals: # Arguments: [-p ] [-r server|desktop|minecraft ] [-t gnome-desktop|kde-desktop|xfce-desktop|mate-desktop|cinnamon-desktop ] # Outputs: /path/to/preseed.cfg # Returns: # Usage: system::make_preseed_cfg -r "${_role}" -p "$( mktemp -d)" -t ${_task} # Example: # # system::make_preseed_cfg -r "${_role}" -p "$( mktemp -d)" -t ${_task} # # To install a basic server: # system::make_preseed_cfg -r "default" -p "$( mktemp -d)" -t ssh-server # # To install a basic ansible server: # system::make_preseed_cfg -r "default" -p "$( mktemp -d)" -t ansible-server # # To autoinstall a basic minecraft server: # system::make_preseed_cfg -r "minecraft-server" -p "$( mktemp -d)" -t ssh-server # # To autoinstall a basic desktop with gnome: # system::make_preseed_cfg -r "default" -p "$( mktemp -d)" -t gnome-desktop # # End of documentation # Read requested configuration options kvm::util::read_common_options "$@" kvm::util::read_distro_options "$@" system::log_item "Distribution: Role: ${_role} | Desktop: ${_UserDesktopEnvironmentSelection:-"Not selected"} | Server: ${_UserServerEnvironemtSelection:-"Not selected"} | Ask: ${_ask} | Save to: ${_saveto}" system::log_item "Transferring standard selections _role to the preseed file multiselect option..." case "${_role}" in workstation | desktop | VDI) _task="${_UserDesktopEnvironmentSelection:="gnome-desktop"}" system::log_item "Setting multiselect _task to: ${_task} because the primary purpose is to be a VDI or Desktop" ;; server) _task="${_UserServerEnvironemtSelection:="ssh-server"}" system::log_item "Setting multiselect _task to: ${_task} because the primary purpose is to be a server" ;; *) write_error "Unable to set _task for PREESEED file: (${_task})" return 1 ;; esac export PRESEED_FILE="${_saveto}/preseed.cfg" # The name of the preseed file to be created export TASK_FILE="${_saveto}/task.sh" # The name of the task file to be created (if needed) touch "${PRESEED_FILE}" 2>&1 | tee -a "${_LOGFILE}" touch "${TASK_FILE}" 2>&1 | tee -a "${_LOGFILE}" # kvm::get_vm_config_preferences will load the preferences from the config file if it exists # otherwise it will use the defaults set in the function. # it will return predefined variables that can be used in the preseed file. if [[ "$_ask" == "YES" || "$_ask" == "Yes" || "$_ask" == "yes" ]]; then system::log_item "Prompting for configuration options..." if kvm::get_vm_config_preferences --ask || dialog::yesno "Cancelled or something went wrong... continue with defaults?"; then exit_status=$? case $exit_status in 0) system::log_item "Save choices and continue..." ;; 1) system::log_item "[Cancel] pressed; not updating or changing any configuration items..." ;; 255) system::log_item "[ESC] pressed, exiting the dialog immediately..." return 1 ;; *) system::log_item "[ERROR] Dialog closed or unexpected error." pause::for_input 1 "Dialog closed or unexpected error." ;; esac fi else system::log_item "Using default configuration options..." if ! kvm::get_vm_config_preferences; then system::log_item "Failed to get configuration options from saved preferences..." fi fi # If the user has requested to copy their SSH key to the host, then do so: if [[ "${Preference_CopySSHKey}" == "yes" || "${Preference_CopySSHKey}" == "YES" ]]; then Preference_ssh_pub_key="$(cat /home/$SUDO_USER/.ssh/*.pub | head -1)" fi system::log_item "Adding late commands to the preseed file..." # Add late commands to the preseed file based on either the distribution requested via (--dist) # or the function that called this function if echo $_distribution | grep -i ubuntu; then system::log_item "Ubuntu requested, using ubiquity/success_command" _d_i_post_statment_ubunto_or_deb="ubiquity ubiquity/success_command string" elif echo $_distribution | grep -i debian; then system::log_item "Debian requested, using d-i preseed/late_command" _d_i_post_statment_ubunto_or_deb="d-i preseed/late_command string" elif echo "${FUNCNAME[1]}" | grep -i ubuntu; then system::log_item "Ubuntu assumed from calling function name, using ubiquity/success_command" _d_i_post_statment_ubunto_or_deb="ubiquity ubiquity/success_command string" elif echo "${FUNCNAME[1]}" | grep -i debian; then system::log_item "Debian assumed from calling function name, using d-i preseed/late_command" _d_i_post_statment_ubunto_or_deb="d-i preseed/late_command string" else system::log_item "Unknown distribution (not requested or detectable), using d-i preseed/late_command" _d_i_post_statment_ubunto_or_deb="ubiquity ubiquity/success_command string" fi touch "${PRESEED_FILE}" template::preseed_cfg::early_command ${PRESEED_FILE} template::preseed_cfg::main ${PRESEED_FILE} template::preseed_cfg::auto_disk_layout ${PRESEED_FILE} template::preseed_cfg::late_command ${PRESEED_FILE} system::preseed_to_ubuntu_ks_cfg "${_saveto}" system::log_item "Preseed file created: ${PRESEED_FILE}" system::log_item "Containging DATA: $(cat ${PRESEED_FILE})" } system::generate_cloudconfig() { # Description: # Generates a cloud-config and meta-data ISO (CIDATA.iso) for automated Ubuntu deployments # within KVM or other cloud-init-enabled environments. The function manages: # - Creating a YAML user-data file (via template::cloud_config). # - Embedding meta-data and user-data into an ISO image (using genisoimage). # - Handling optional SSH key injection, user-defined directories, and role-specific configs. # # Key Features: # - Supports cloud-init processes by packaging meta-data and user-data into a single ISO. # - Integrates with broader VM configuration scripts and environment variables. # - Logs key steps and handles basic error reporting (e.g., missing genisoimage). # # Prerequisites: # - Must be run in an environment where supporting functions are loaded: # (e.g., kvm::util::read_distro_options, template::cloud_config, system::log_item, etc.). # - The genisoimage package must be installed. # - Adequate permissions to write to the specified cloud config directory and ISO file. # # Globals: # GIT_RTD_SRC_URL : Git repository URL for RTD source (if relevant). # GIT_Profile : Git profile name (if relevant). # _TLA : Three-letter acronym for the project. # Preference_CopySSHKey : Flag to decide whether to embed local SSH keys. # _ask, _role, _LOGFILE : Other environment variables used in the process. # # Arguments: # --CloudConfigDir : Destination directory for generated cloud-config files # and the resulting ISO (default: temporary folder). # --flavor : Role of the VM (e.g., ubuntu-server or ubuntu-desktop). # --vmname : Hostname for the target VM. # # Outputs: # - CIDATA.iso : ISO image containing user-data and meta-data for cloud-init. # - meta-data : Basic instance information for cloud-init. # - user-data : YAML configuration for automated Ubuntu installations. # # Return Codes: # - 0 : Successful execution. # - 1 : Failure to write user-data file. # - 200 : Failure to generate the CIDATA.iso. # # Usage: # system::generate_cloudconfig [--CloudConfigDir ] \ # [--flavor ] [--vmname ] # # Example: # system::generate_cloudconfig --CloudConfigDir /tmp/cloudconfig \ # --flavor ubuntu-server --vmname my-server # # Notes: # - This function is part of a larger VM provisioning workflow. It typically runs before # calling the VM creation step (e.g., virt-install) so the generated ISO can be attached # as a cloud-init CD-ROM. # - Cloud-init uses user-data and meta-data for initial setup tasks: user accounts, # SSH keys, package installs, etc. # - Ownership and permissions on the cloud-config directory and files are adjusted to # match the libvirt/qemu user for secure read access during VM startup. # # End of Documentation # Initialize variables with defaults. system::log_item "Called by: ${FUNCNAME[1]}" system::log_item "Processing parameters: $*" local all_params=${*} local cloud_config_dir="$CloudConfigDir" local server_app="${_UserServerEnvironemtSelection:-"ubuntu-server"}" local ask="${_ask}" local role="${_role}" # ----------------------------------------------------------------------------------------- # Collect required information from the parameters passed to the function. # ----------------------------------------------------------------------------------------- while [[ $# -gt 0 ]]; do case "$1" in --CloudConfigDir) cloud_config_dir="$2" system::log_item "cloud_config_dir set to: $cloud_config_dir" shift 2 ;; *) system::log_item "Skippinng option: $1" shift 2 ;; esac done # ----------------------------------------------------------------------------------------- # get default VM configurations options from the kvm::get_vm_config_preferences # ----------------------------------------------------------------------------------------- # Ask the user for VM config options if required... kvm::util::read_distro_options --distribution ubuntu ${all_params} # ----------------------------------------------------------------------------------------- # Directory for cloud config files, if not set, create a temporary directory and use that. # ----------------------------------------------------------------------------------------- local cloud_config_dir="${cloud_config_dir:=$(mktemp -d /tmp/CloudConfig.XXXXXX)}" touch ${cloud_config_dir}/meta-data touch ${cloud_config_dir}/user-data # Display parameter interpretation (or proceed with processing). system::log_item "Cloud Configuration Directory: $cloud_config_dir" system::log_item "Role: $role" system::log_item "Server App: $server_app" system::log_item "Ask: $ask" # ----------------------------------------------------------------------------------------- # If the user has requested to copy their SSH key to the host, capture it and store in a # var for use in the config file. Preferences are pulled from the config file via # kvm::get_vm_config_preferences stored in environment variables. # ----------------------------------------------------------------------------------------- if [[ "${Preference_CopySSHKey}" == "yes" || "${Preference_CopySSHKey}" == "YES" ]]; then local Preference_ssh_pub_key="$(cat /home/$SUDO_USER/.ssh/*.pub | head -1)" fi # ----------------------------------------------------------------------------------------- # Write out the user-data file # ----------------------------------------------------------------------------------------- system::log_item "Writing out the user-data file..." template::cloud_config --write "${cloud_config_dir}/user-data" ${all_params} || { system::log_item "Failed to write out the user-data file." return 1 } # Log configuration data for troupleshooting puropses system::log_item "Configuration data supplied: $(cat ${cloud_config_dir}/user-data)" write_information "Generating Ubuntu autoinstall instruction media: ${cloud_config_dir}/CIDATA.iso" software::check_native_package_dependency genisoimage # ----------------------------------------------------------------------------------------- # Generate the CIDATA.iso file # ----------------------------------------------------------------------------------------- write_information "Generating: ${cloud_config_dir}/CIDATA.iso" if genisoimage -output ${cloud_config_dir}/CIDATA.iso -volid cidata -joliet -rock -full-iso9660-filenames ${cloud_config_dir}/* &>>${_LOGFILE}; then { system::log_item "Generated: ${cloud_config_dir}/CIDATA.iso" success; } else { system::log_item "Failed to generate: ${cloud_config_dir}/CIDATA.iso" return 200 } fi # ----------------------------------------------------------------------------------------- # Secure the cloud config directory # ----------------------------------------------------------------------------------------- write_information "Securing the cloud config directory..." if chown $(stat -c "%U:%G" /var/lib/libvirt/qemu/) "${cloud_config_dir}"; then if chmod $(stat -c "%a" /var/lib/libvirt/qemu/) "${cloud_config_dir}"; then write_information "Changed ownership and permissions of ${cloud_config_dir} successfully" else write_information "Failed to change permissions of ${cloud_config_dir} to $(stat -c "%a" /var/lib/libvirt/qemu/) " fi else write_error "Failed to change ownership of ${cloud_config_dir} to $(stat -c "%U:%G" /var/lib/libvirt/qemu/)" fi return 0 } system::generate_ks_cfg_file() { # Description: Generates an installation configuration file (kickstart) for Fedora/Red Hat. # # This function generates a kickstart configuration file for automating the installation of # Fedora or Red Hat systems. It supports various options to customize the installation, # including specifying the filename, repository URL, desktop environment, server role, timezone, # disk encryption, and initial user settings. # # Globals: # - None # Arguments: # - --filename : Name of the kickstart file to create. # - --desktop_environment : Desktop environment to install (e.g., deepin-desktop-environment, sway-desktop-environment). # - --server_role : Server role to install (e.g., minimal-environment, web-server-environment). # - --role : General role of the system (workstation or server). # - --source : Repository URL for installation source. # - --timezone : Timezone to set for the system. # - --disk_encryption : Whether to enable disk encryption. # - --disk_password : Password for disk encryption. # - --initial_user : Initial user to create. # - --initial_user_password : Password for the initial user. # Outputs: # - One kickstart configuration file. # Returns: # - 0 on success. # - 1 on error. # # Usage: # system::generate_ks_cfg_file --filename --source --role [options] # # Example: # system::generate_ks_cfg_file --filename ks.cfg --source http://repo.url --role workstation --desktop_environment kde-desktop-environment # # Notes: # - This function uses various options to customize the kickstart configuration file. # - Ensure that mandatory parameters (filename, source URL, and role) are provided. # - Supports a wide range of desktop environments and server roles for customization. # # End of Documentation system::log_item "Called by: ${FUNCNAME[1]}" # Get vm config standard options kvm::get_vm_config_preferences # Determine if special handling (like EPEL) is required based on the caller. if [[ "${FUNCNAME[1]}" == "kvm::make_vm_now_from_redhat_com" ]]; then _kickstart_enable_epel="true" else unset _kickstart_enable_epel fi while [[ $# -gt 0 ]]; do case "$1" in --filename | --file) local _ks_file="$2" write_host --cyan "📜 Directed to create kickstart instructions file: ${_ks_file}" shift ;; --desktop_environment | --DE) if [[ $2 == "@workstation-product" || $2 == "@deepin-desktop-environment" || $2 == "@sway-desktop-environment" || $2 == "@basic-desktop-environment" || $2 == "@(i3-desktop-environment" || $2 == "@developer-workstation-environment" || $2 == "@budgie-desktop-environment" || $2 == "@sugar-desktop-environment" || $2 == "@mate-desktop-environment" || $2 == "@kde-desktop-environment" || $2 == "@xfce-desktop-environment" || $2 == "@lxde-desktop-environment" || $2 == "@lxqt-desktop-environment" || $2 == "@cinnamon-desktop-environment" || $2 == "@mate-desktop-environment" || $2 == "@workstation-product-environment" ]]; then local _UserDesktopEnvironmentSelection="${2}" write_host --cyan "🖥️ Selecting desktop environment: ${_UserDesktopEnvironmentSelection}" else write_error "⛔ Unknown Desktop Environment: $2 ==> Use deepin-desktop-environment, sway-desktop-environment, basic-desktop-environment, i3-desktop-environment, developer-workstation-environment, budgie-desktop-environment, sugar-desktop-environment, mate-desktop-environment, kde-desktop-environment, xfce-desktop-environment, lxde-desktop-environment, lxqt-desktop-environment, cinnamon-desktop-environment, mate-desktop-environment, workstation-product-environment" return 1 fi shift ;; --server_role | --ServerRole | --SR) if [[ $2 == "@minimal-environment" || $2 == "@web-server-environment" || $2 == "@freeipa-server" || $2 == "@network-server" || $2 == "@cloud-server-environment" || $2 == "@infrastructure-server-environment" ]]; then local _UserServerEnvironemtSelection="${2}" write_status "🎭 Selecting server role: ${2}" else write_error "⛔ Unknown server role selection: ${2} ==> Use web-server-environment, freeipa-server, network-server, cloud-server-environment, infrastructure-server-environment" return 1 fi shift ;; --role | --Role) if [[ $2 == "workstation" || $2 == "server" ]]; then local _role="${2}" write_status "🎭 Selecting role: ${2}" else write_error "⛔ Unknown role selection: ${2} ==> Use workstation or server" return 1 fi shift ;; --source | --URL | --url) local _repo_url="${2}" write_status "🌐 Using URL: ${_repo_url}" shift ;; --timezone | --TZ) local Preference_InitialTimeZone="${2}" write_status "🕰️ Using Timezone: ${Preference_InitialTimeZone}" shift ;; --disk_encryption | --DC) Preference_DiskEncryption="$2" write_status "🔐 Disk encryption is requested and enabled." shift ;; --disk_password | --DP) Preference_Disk_Password="$2" write_status "🔐 Disk encryption password is set." shift ;; --initial_user | --IU) Preference_InitialUser="$2" write_status "👤 Initial user is set to: ${Preference_InitialUser}" shift ;; --initial_user_password | --IUP) Preference_InitialUserPassword="$2" write_status "🔐 Initial user password is set." shift ;; --help | -h) write_information "Usage: system::generate_ks_cfg_file [-p ] [-u URL ] [-c workstation|ssh-server|ansible-server ]" return 0 ;; *) write_error "Unknown parameter: $1" ;; esac shift done if [[ ${Preference_DiskEncryption} == "YES" || ${Preference_DiskEncryption} == "yes" || ${Preference_DiskEncryption} == "Yes" ]]; then # set disk encryption password string, if password not set and Preference_DiskEncryption not set use default system::log_item "🔐 Disk encryption is requested and enabled." Preference_DiskEncryption_RH="--encrypted --passphrase ${Preference_Disk_Password:-"letmein1234"}" else system::log_item "🔐 Disk encryption is not requested and not enabled." fi [[ -v _repo_url && -v _role && -v _ks_file ]] || ( echo missing mandatory parameters! return 1 ) if [[ -n "${_mirrorlist_url:-}" ]]; then printf -v _kickstart_install_source 'url --mirrorlist "%s"' "${_mirrorlist_url}" elif [[ -n "${_repo_url:-}" ]]; then printf -v _kickstart_install_source 'url --url "%s"' "${_repo_url}" else unset _kickstart_install_source fi # Generate the kickstart file if template::kickstart_cfg ${_ks_file} ; then system::log_item "Kickstart file generation succeeded." else system::log_item "Kickstart file generation failed." return 1 fi unset _kickstart_install_source unset _kickstart_enable_epel system::log_item "Kickstart file created: ${_ks_file}" system::log_item "Containing DATA: $(cat ${_ks_file})" return 0 } system::generate_autoyast_file() { # Description: # This function dynamically generates an AutoYaST XML file for automated SUSE Linux installations. # It allows customization of the installation process, including desktop environment, server role, # disk encryption, initial user setup, and more based on provided arguments. # # Globals: # None explicitly declared within the function, but it expects certain environment variables # to be set (e.g., _autoyast_filename, _UserDesktopEnvironmentSelection, etc.) based on the arguments passed. # # Arguments: # --filename : Specifies the path and filename where the AutoYaST XML file will be saved. # --desktop_environment : Selects the desktop environment to install (kde, gnome, xfce, lxde, mate, cinnamon, enlightenment, lxqt). # --server_role : Specifies the server role (lamp, dns, dhcp_dns_server, file_server, print_server, mail_server, monitoring, desktop). # --disk_encryption : Enables or disables disk encryption. # --disk_password : Sets the disk encryption password (ignored if disk encryption is NO). # --initial_user : Sets the username for the initial user account. # --initial_user_password : Sets the password for the initial user account. # --help | -h : Displays usage information for the function. # # Outputs: # The function generates an AutoYaST XML file at the specified location. It also outputs status messages, # error messages, and usage information to the console based on the operations performed and arguments provided. # # Returns: # 0 : Successfully generated the AutoYaST XML file. # 1 : Encountered an error due to incorrect or missing arguments. # # Usage: # To generate an AutoYaST file with specific configurations, call the function with the desired options. For example: # system::generate_autoyast_file --filename "/path/to/autoyast.xml" --desktop_environment "kde" --server_role "lamp" \ # --disk_encryption "YES" --disk_password "securepassword" --initial_user "admin" --initial_user_password "adminpassword" # # Providing the --help or -h argument displays usage information: # system::generate_autoyast_file --help # # Supported Desktop Environments: # | basic_desktop | A very basic desktop (previously part of x11 pattern) | pattern # | budgie | Budgie Desktop Environment | pattern # | budgie_applets | Applets for Budgie Desktop Environment | pattern # | cinnamon | Cinnamon Desktop Environment | pattern # | deepin | Deepin Desktop Environment | pattern # | gnome | GNOME Desktop Environment (Wayland) | pattern # | gnome_basic | GNOME Desktop Environment (Basic) | pattern # | gnome_x11 | GNOME Desktop Environment (X11) | pattern # | kde | KDE Applications and Plasma 5 Desktop | pattern # | kde_plasma | KDE Plasma 5 Desktop Base | pattern # | lxde | LXDE Desktop Environment | pattern # | lxqt | LXQt Desktop Environment | pattern # | mate | MATE Desktop Environment | pattern # | xfce | XFCE Desktop Environment | pattern # | yast2_desktop | YaST Desktop Utilities | pattern # # Supported Server Roles: # | dhcp_dns_server | DHCP and DNS Server | pattern # | directory_server | Directory Server (LDAP) | pattern # | file_server | File Server | pattern # | gateway_server | Internet Gateway | pattern # | kvm_server | KVM Host Server | pattern # | lamp_server | Web and LAMP Server | pattern # | mail_server | Mail and News Server | pattern # | oracle_server | Oracle Server Base | pattern # | print_server | Print Server | pattern # | sap-bone | SAP BusinessOne Server Base | pattern # | sap-hana | SAP HANA Server Base | pattern # | sap-nw | SAP NetWeaver Server Base | pattern # | sap_server | SAP Application Server Base | pattern # | xen_server | Xen Virtual Machine Host Server | pattern # | yast2_server | YaST Server Utilities | pattern # # End of Documentation # Get vm config standard options system::log_item "Called by: ${FUNCNAME[1]} with options $*" kvm::get_vm_config_preferences || system::log_item "Failed to get configuration options from saved preferences..." # Read input and set specific options for SUSE... (roles, desktops, etc.) kvm::util::read_distro_options --distribution suse ${*} # Localized variables based on User preferences, but with suse speciffic defaults: # We cannot have empty tags in XML or validation will fail: so echo stored values with relevant tags. if [[ ${Preference_DiskEncryption} == "YES" || ${Preference_DiskEncryption} == "yes" || ${Preference_DiskEncryption} == "Yes" ]]; then Preference_DiskEncryption_SUSE="${Preference_Disk_Password}" else #Preference_DiskEncryption_SUSE="" system::log_item "Disk encryption is not enabled." fi # Ensure that a filename is provided for the AutoYaST XML file, otherwise create a temporary file # and store the path in the _autoyast_filename variable. This can be read by other functions if needed. if [[ -z ${_autoyast_filename} ]]; then _autoyast_filename="$(mktemp /tmp/autoyast.XXXXXX)" fi write_host --cyan "Creating AUTOYAST Instructions (XML): ${_autoyast_filename}" # Populate AutoYaST Template and write the autoyast file template::AutoYast_xml ${_autoyast_filename} system::log_item "AutoYaST file created: ${_autoyast_filename} With the contents: $(cat ${_autoyast_filename}) " return 0 } system::install_missing_commands() { # Description: Installs missing commands based on the distribution type. # # This function checks if specified commands are available on the system and installs them if they are missing. # It supports Debian, Ubuntu, Fedora, RHEL, openSUSE, and SUSE distributions. # The function relies on the `command-not-found` package for Debian-based systems to suggest missing packages. # # Globals: # - None # Arguments: # - List of commands to check and install. # Outputs: # - Status messages indicating the presence or installation of commands. # Returns: # - 0 if all commands are found or installed successfully. # - 1 if no command is specified or if an error occurs. # # Usage: # system::install_missing_commands ... # # Example: # system::install_missing_commands curl git # # Notes: # - This function relies on the `system::distribution_type` function to detect the distribution type. # - It attempts to install missing commands using the native package manager for each supported distribution. # - For Debian-based systems, it also attempts to install commands via snap if not available in apt. # # End of Documentation system::log_item "Called by: ${FUNCNAME[1]}" system::log_item "Processing parameters: $*" if [ "$#" -eq 0 ]; then write_error "No command specified to find and install." return 1 fi local distro local cmd distro=$(system::distribution_type) for cmd in ${*}; do case "$distro" in ubuntu | debian ) local pkg install_info snap_info if command -v "$cmd" >/dev/null 2>&1; then write_status "Command '$cmd' is already available." return 0 fi if [[ -n "$cmd" ]]; then pkg="$cmd" write_status "Trying first APT package '$pkg' for command '$cmd'" if software::add_native_package "$pkg"; then write_status "Installed '$cmd' using package '$pkg'." return 0 fi else write_warning "No deb package found matching the name of the command '$cmd'. Trying alternatives..." fi write_warning "Command '$cmd' not found. Attempting to determine the providing package..." wrrite_status "Trying to extract APT suggestion for the missing command: $cmd..." install_info="$(software::determine_package_name_from_cmd "$cmd")" if [[ -n "$install_info" ]]; then pkg="$install_info" write_status "APT suggests installing '$pkg' for command '$cmd'" if software::add_native_package "$pkg"; then write_status "Installed '$cmd' using package '$pkg'." return 0 else write_warning "APT package '$pkg' failed to install." fi else write_warning "No APT package suggestion found for '$cmd'." fi # Try finding a Snap package as fallback if command -v snap >/dev/null 2>&1; then write_status "Searching Snap Store for '$cmd'..." if snap_info=$(snap find "$cmd" 2>/dev/null | awk 'NR==2 {print $1}'); then if [[ -n "$snap_info" ]]; then write_status "Snap suggests package '$snap_info' for '$cmd'" if software::from_snapcraft.io "$snap_info"; then write_status "Installed '$cmd' using Snap package '$snap_info'." return 0 else write_error "Snap package '$snap_info' failed to install." fi else write_warning "No relevant Snap packages found for '$cmd'." fi fi else write_warning "Snap is not installed or not available. Skipping Snap search." fi write_error "Unable to resolve or install the required command: '$cmd'" return 1 ;; redhat ) # Fedora and RHEL local pkg pkg=$(dnf provides */$cmd | grep ' : ' | head -n1 | cut -d' ' -f1) if [[ -n "$pkg" ]]; then software::add_native_package "$pkg" write_status "Installed '$cmd' using package $pkg" fi ;; suse ) # openSUSE / SUSE for cmd in "${missing_commands[@]}"; do cnf_output=$(cnf "$cmd" 2>&1) if [[ $cnf_output == *"can be found in following package"* ]]; then # Extracting package name pkg=$(echo "$cnf_output" | grep -F 'sudo zypper install' | awk '{print $4}') if [[ -n "$pkg" ]]; then software::add_native_package "$pkg" write_status "Installed '$cmd' using package '$pkg'." return 0 else write_error "Package for command '$cmd' not found." fi else write_error "Command '$cmd' not found in cnf database." fi done ;; * ) write_error "Unsupported distribution: $distro" return 1 ;; esac done } system::keyboard_set_layout() { # Description: # Configure and persist keyboard layout for both GUI and TTY across major Linux distributions. # Supports Debian, RedHat, Fedora, SUSE, Mandriva, Gentoo, Slackware. # Allows interactive layout selection or non-interactive application. # # Globals: # None # # Arguments: # --keyboard-layout Non-interactive mode, set specific layout (e.g., us, de, fr) # --interactive Interactive mode using dialog to select layout # # Returns: # 0 if successful, 1 on error # # Dependencies: # dialog, sed, localectl, udevadm, dpkg-reconfigure/setupcon (on Debian-based) # # Example: # system::keyboard_set_layout --keyboard-layout us # system::keyboard_set_layout --interactive # # End of Documentation local layout distro interactive valid_layouts selected layout_list local -a args args=("$@") system::log_item "Called by: ${FUNCNAME[1]}" system::log_item "Processing parameters: $*" if [ "$#" -eq 0 ]; then write_error "No keyboard layout specified. Use --keyboard-layout or --interactive." return 1 fi if ! dependency::os_linux; then write_error "This function is only supported on Linux systems." return 1 fi if ! command -v localectl >/dev/null || ! command -v udevadm >/dev/null; then write_error "Required tools (localectl, udevadm) are missing." return 1 fi if ! distro=$(system::distribution_type); then write_error "Unable to determine system distribution type." return 1 fi if [[ "${args[*]}" =~ --interactive ]]; then if ! command -v dialog >/dev/null; then software::add_native_package dialog if ! command -v dialog >/dev/null; then write_error "Dialog is not installed. Please install it to use interactive mode." return 1 fi fi if ! layout_list=$(localectl list-x11-keymap-layouts 2>/dev/null); then dialog::display_error "Unable to fetch available keyboard layouts." return 1 fi mapfile -t valid_layouts < <(printf '%s\n' "$layout_list" | grep -E '^[a-z]{2,4}$' | sort -u) local dialog_opts=() for layout in "${valid_layouts[@]}"; do dialog_opts+=("$layout" "" off) done if ! selected=$(dialog --stdout --radiolist "Choose a keyboard layout" 20 60 15 "${dialog_opts[@]}"); then dialog::display_notice "No layout selected. Aborting." clear return 1 fi layout="$selected" clear else for ((i = 0; i < ${#args[@]}; i++)); do if [[ "${args[$i]}" == "--keyboard-layout" && -n "${args[$((i + 1))]}" ]]; then layout="${args[$((i + 1))]}" break fi done if [[ -z "$layout" ]]; then write_error "Usage: keyboard::set_layout --keyboard-layout or --interactive" return 1 fi if ! layout_list=$(localectl list-x11-keymap-layouts 2>/dev/null); then write_error "Unable to fetch available keyboard layouts." return 1 fi if ! printf '%s\n' "$layout_list" | grep -q -x "$layout"; then write_error "Invalid keyboard layout: '$layout'" return 1 fi fi write_status "Applying keyboard layout '$layout' for distribution: $distro" case "$distro" in debian) if ! command -v dpkg-reconfigure >/dev/null || ! command -v setupcon >/dev/null; then write_error "Missing required Debian tools (dpkg-reconfigure, setupcon)." return 1 fi if ! sudo sed -i "s/^XKBLAYOUT=.*/XKBLAYOUT=\"$layout\"/" /etc/default/keyboard; then write_error "Failed to update /etc/default/keyboard" return 1 fi if ! sudo dpkg-reconfigure -f noninteractive keyboard-configuration; then write_error "Failed to reconfigure keyboard-configuration" return 1 fi if ! sudo setupcon; then write_error "setupcon failed" return 1 fi ;; redhat|fedora|suse|mandriva|gentoo|slackware) if ! sudo localectl set-keymap "$layout" || ! sudo localectl set-x11-keymap "$layout"; then write_error "Failed to apply keyboard layout using localectl on $distro." return 1 fi ;; *) write_error "Unsupported or unknown distribution: '$distro'" return 1 ;; esac if ! sudo mkdir -p /etc/X11/xorg.conf.d; then write_error "Failed to create /etc/X11/xorg.conf.d" return 1 fi if ! sudo tee /etc/X11/xorg.conf.d/00-keyboard.conf >/dev/null <<-EOF Section "InputClass" Identifier "system-keyboard" MatchIsKeyboard "on" Option "XkbLayout" "$layout" EndSection EOF then write_error "Failed to write X11 keyboard config" return 1 fi if ! sudo udevadm trigger --subsystem-match=input --action=change; then write_error "udevadm trigger failed" return 1 fi write_information "✅ Keyboard layout successfully set to '$layout'" dialog::display_result "Keyboard Layout Updated" "Keyboard layout has been set to: $layout" return 0 } system::check_file_limits() { # Description: # # This function is designed to check and display the current usage of file descriptors against # the set limits in a Linux system. It provides a clear overview of open files and system limits, # aiding in resource monitoring and troubleshooting. # # The function performs the following actions: # - Retrieves the count of currently open files using `lsof`. # - Fetches the user-specific soft and hard limits for open files (`nofile`). # - Obtains the system-wide file handle limit. # - Displays the collected information in a user-friendly format. # # Additionally, it includes a spinner animation to indicate progress while gathering data, # enhancing the user experience during potentially longer operations. # # Usage: # Call `system::check_file_limits` from the terminal or within a script to execute the function. # # Notes: # - Uses a temporary file (`/tmp/open_files_count`) to store the count of open files. # - Implements a spinner animation to indicate ongoing processing. # - Uses `printf` with formatting to align and present data clearly, including thousand separators. # - Checks if current usage exceeds any of the defined limits and displays appropriate warnings or # critical alerts. # # Dependencies: # - Requires `lsof`, `sysctl`, `awk`, and standard bash utilities. # # Limitations: # - Spinner animation relies on the terminal's ability to interpret control characters for cursor # movement. May not work in all terminal environments. # - Number formatting is locale-dependent and assumes `en_US.UTF-8` is available and correctly # configured on the system. # # End of Documentation # Get the number of currently open files in the background lsof | wc -l >/tmp/open_files_count & local lsof_pid=$! system::check_file_limits::spinner() { local pid=$1 local delay=0.1 local spinstr='|/-\\' echo -n " Working... " while kill -0 "$pid" 2>/dev/null; do local temp=${spinstr#?} printf "[%c]" "$spinstr" local spinstr=$temp${spinstr%"$temp"} sleep $delay printf "\b\b\b" done echo -ne "\r \r" } # Start the spinner system::check_file_limits::spinner "$lsof_pid" # Read the open files count local open_files=$(cat /tmp/open_files_count) # Rest of your function... local user_soft_limit=$(ulimit -Sn) local user_hard_limit=$(ulimit -Hn) local system_limit=$(sysctl -n fs.file-max) local allocated_file_handles=$(awk '{print $1}' /proc/sys/fs/file-nr) # Print the results write_information " 💻 File Descriptors Usage and Limits: $( tput bold tput setaf 1 )-------------------------------------------------------- $(tput sgr0) $(printf "%-43s \e[36m%'10d\e[0m\n" "Open Files:" $open_files) $(printf "%-43s \e[36m%'10d\e[0m\n" "User Soft Limit:" $user_soft_limit) $(printf "%-43s \e[36m%'10d\e[0m\n" "User Hard Limit:" $user_hard_limit) $(printf "%-43s \e[36m%'10d\e[0m\n" "System-Wide File Handles Limit:" $system_limit) $(printf "%-43s \e[36m%'10d\e[0m\n" "Allocated File Handles (System-Wide):" $allocated_file_handles) $( tput bold tput setaf 1 )-------------------------------------------------------- $(tput sgr0) " # Additional information based on limits... if [ "$open_files" -gt "$user_soft_limit" ]; then echo "Warning: You are exceeding your soft limit of open files." fi if [ "$open_files" -gt "$user_hard_limit" ]; then echo "Critical: You are exceeding your hard limit of open files." fi if [ "$allocated_file_handles" -gt "$system_limit" ]; then echo "Critical: The system is exceeding its overall file handles limit." fi } system::distribution_type() { # Description: # # This function determines the Linux distribution type of the system on which it is run. It identifies the # distribution by checking for the presence of specific files and functions unique to each distribution's # initialization system. The function aims to support a variety of Linux distributions, including Fedora, # RHEL, CentOS, SUSE, Debian, Ubuntu, Gentoo, Mandriva, and Slackware. # # Parameters: none # # Returns: # # Prints the type of the Linux distribution to standard output. The possible return values are: # # "fedora": For Fedora Linux. # "redhat": For Fedora, RHEL, CentOS, and other Red Hat derivatives. # "suse": For SUSE Linux distributions. # "debian": For Debian, Ubuntu, and derivatives. # "gentoo": For Gentoo Linux. # "mandriva": For Mandriva Linux. # "slackware": For Slackware Linux. # "unknown": If the distribution cannot be determined. # # Usage: # # To use this function, simply call it within a bash script or from the command line in a script # environment where the function has been defined. Example usage: # # distribution=$(system::distribution_type) # echo "Detected distribution: $distribution" # # # Notes: # # The order of checks matters, as some distributions may share certain files or functions. The function # is structured to minimize false positives. For Mandriva and Slackware, the function currently # checks for the presence of /etc/mandriva-release and /etc/slackware-version, # respectively, but acknowledges that a better method of identification might be needed. # This function does not have external dependencies beyond the bash shell and standard Linux filesystem # and command-line tools. # # Example: # # Here's how you might call this function within a script to execute different commands based on the detected distribution: # # bash # # case $(system::distribution_type) in # redhat) # echo "This is a Red Hat derivative." # ;; # debian) # echo "This is a Debian derivative." # ;; # *) # echo "Distribution not specifically handled." # ;; # esac # # End of documentation local dtype marker os_release_file os_id os_id_like dtype="unknown" # Prefer ID/ID_LIKE from os-release (covers rolling releases like Tumbleweed). for os_release_file in /etc/os-release /usr/lib/os-release; do if [ -r "$os_release_file" ]; then local ID="" ID_LIKE="" # shellcheck disable=SC1091 . "$os_release_file" os_id="$ID" os_id_like="$ID_LIKE" break fi done if [ -n "$os_id" ] || [ -n "$os_id_like" ]; then for marker in "$os_id" $os_id_like; do [ -n "$marker" ] || continue marker=$(printf '%s' "$marker" | tr '[:upper:]' '[:lower:]') case "$marker" in fedora) dtype="fedora" break ;; redhat|redhatenterpriseserver|rhel|centos|rocky|almalinux|alma|scientific|oraclelinux|ol|cloudlinux) dtype="redhat" break ;; suse|sles|sled|opensuse*|sle_hpc) dtype="suse" break ;; ubuntu) dtype="ubuntu" break ;; debian|linuxmint|elementary|pop|zorin|raspbian|kali) dtype="debian" break ;; gentoo) dtype="gentoo" break ;; mandriva|mageia|mandrake) dtype="mandriva" break ;; slackware) dtype="slackware" break ;; esac done fi if [ "$dtype" == "unknown" ]; then { # First test specifically for Fedora if [ -r /etc/fedora-release ]; then dtype="fedora" # Then test against Fedora / RHEL / CentOS / generic Redhat derivative elif [ -r /etc/rc.d/init.d/functions ]; then source /etc/rc.d/init.d/functions [ "zz$(type -t passed 2>/dev/null)" == "zzfunction" ] && dtype="redhat" # Then test against SUSE (must be after Redhat, elif [ -r /etc/rc.status ]; then source /etc/rc.status [ "zz$(type -t rc_reset 2>/dev/null)" == "zzfunction" ] && dtype="suse" # Then test against Debian, Ubuntu and friends elif [ -r /lib/lsb/init-functions ]; then source /lib/lsb/init-functions [ "zz$(type -t log_begin_msg 2>/dev/null)" == "zzfunction" ] && dtype="debian" # Double-check for Ubuntu since though debian based, ubuntu does many things very differently... [ -r /etc/lsb-release ] && cat /etc/lsb-release | grep Ubuntu && dtype="ubuntu" # Then test against Gentoo elif [ -r /etc/init.d/functions.sh ]; then source /etc/init.d/functions.sh [ "zz$(type -t ebegin 2>/dev/null)" == "zzfunction" ] && dtype="gentoo" # For Mandriva we currently just test if /etc/mandriva-release exists elif [ -s /etc/mandriva-release ]; then dtype="mandriva" # For Slackware we currently just test if /etc/slackware-version exists elif [ -s /etc/slackware-version ]; then dtype="slackware" fi } &>/dev/null fi echo "$dtype" } system::check_required_variables() { # Description: Function to check for the presence and non-emptiness of a list of variables. # This function checks for the presence and non-emptiness of a list of variables. # It is designed to validate that certain required variables are both defined (set) # and have a non-empty value. This is particularly useful in scripts where specific # variables are essential for correct execution. # # Arguments: # $@: A list of variable names (as strings) to be checked. Each argument should # be the name of a variable to check. # # Outputs: # If any variables are found to be unset, it prints a message listing these variables. # If any variables are found to be set but empty, it prints a message listing these variables. # # Return Values: # Returns 0 (success) if all variables are set and non-empty. # Returns 1 (failure) if any variable is either unset or set but empty. # # Usage: # To use this function, pass the names of the variables you want to check as arguments. For example: # # system::check_required_variables "VAR1" "VAR2" "VAR3" # # Example: # VAR1="data" # VAR2="" # VAR3= # # if system::check_required_variables "VAR1" "VAR2" "VAR3"; then # echo "All required variables are set and non-empty." # else # echo "Some required variables are either unset or empty." # fi # # Notes: # This function is useful in scenarios where the correct configuration of the environment or # script depends on certain variables. It helps in early detection of misconfigurations # or missing data, allowing for more robust and error-resistant scripts. The function # does not modify any of the variables it checks; it only reports on their status. # End of Documentation local unset_vars=() local empty_vars=() for var in "$@"; do if [ -z "${!var+x}" ]; then unset_vars+=("$var") elif [ -z "${!var}" ]; then empty_vars+=("$var") fi done if [ ${#unset_vars[@]} -ne 0 ]; then write_error "The following requred variables are unset: ${unset_vars[*]}" fi if [ ${#empty_vars[@]} -ne 0 ]; then write_error "The following required variables are set but empty: ${empty_vars[*]}" fi if [ ${#unset_vars[@]} -ne 0 ] || [ ${#empty_vars[@]} -ne 0 ]; then return 1 fi } system::find_vm_bridge() { # Description: Locate a usable network for VM networking. Prefer a host bridge with a physical member; otherwise fall back to a libvirt network (preferring "default"). Logs diagnostics, returning 1 only when nothing usable is found. # # Globals: None # # Arguments: None # # Outputs: STDOUT; prints "bridge=" when a br* interface is found, "network=" when a libvirt network is usable, or "NONE" when missing. Diagnostics are logged. # # Returns: 0 when a bridge or libvirt network is found; 1 when nothing usable exists. # # Usage: # bridge=$(system::find_vm_bridge) # e.g., "bridge=br0", "network=default", or "NONE" # echo "Using ${bridge}" # # Notes: # - Scans /sys/class/net for interfaces starting with "br" that have a physical member (not just veth/vnet/tap). # - Intended for libvirt/KVM style bridges; adjust if your environment uses a different naming scheme. # - If no bridge is found and virsh is available, the first active libvirt network is returned (preferring "default"). If none are active, a defined network is returned (preferring "default"). # # End of documentation # Alternative short hand: # if ls /sys/class/net | grep '^br' | head -n 1 | xargs -I {} echo "bridge={}" |grep br ; else echo default; fi # First, look for a host bridge that appears to have a physical member (avoid docker/CNI-only bridges). local bridge_path bridge opstate ipv4_addr has_physical member_path member for bridge_path in /sys/class/net/br*; do [[ -e "$bridge_path" ]] || continue bridge="${bridge_path##*/}" # Skip docker-style bridges (br-<12hex>) which are usually isolated if [[ "$bridge" =~ ^br-[0-9a-fA-F]{12}$ ]]; then system::log_item "Skipping docker-style bridge ${bridge}" continue fi # Prefer bridges that are up; skip clearly down interfaces opstate=$(cat "/sys/class/net/${bridge}/operstate" 2>/dev/null || echo "") if [[ "$opstate" == "down" || "$opstate" == "dormant" || "$opstate" == "lowerlayerdown" ]]; then system::log_item "Skipping bridge ${bridge} (operstate=${opstate:-unknown})" continue fi # Require at least one non-virtual member (e.g., en*, eth*, bond*, team*). Skip veth/vnet/tap-only bridges. has_physical=0 if [[ -d "/sys/class/net/${bridge}/brif" ]]; then for member_path in /sys/class/net/${bridge}/brif/*; do [[ -e "$member_path" ]] || continue member="${member_path##*/}" if [[ "$member" =~ ^(veth|vnet|tap|docker|br-).*$ ]]; then continue fi if [[ "$member" =~ ^(en|eth|ens|eno|enp|em|bond|team).* ]]; then has_physical=1 break fi done fi if (( ! has_physical )); then system::log_item "Skipping bridge ${bridge} (no physical member detected)" continue fi ipv4_addr=$(ip -o -4 addr show dev "$bridge" 2>/dev/null | awk 'NR==1{print $4}') echo "bridge=${bridge}" system::log_item "found bridge ${bridge}${ipv4_addr:+ with IPv4 ${ipv4_addr}}" return 0 done # If no suitable bridge, prefer active libvirt networks (NAT) before giving up local active_nets default_active first_net defined_nets first_defined if command -v virsh >/dev/null 2>&1; then active_nets=$(virsh net-list --state-active --name 2>/dev/null | awk 'NF') default_active=$(printf '%s\n' "$active_nets" | grep -Fx "default" || true) if [[ -n "$default_active" ]]; then system::log_item "Using libvirt NAT network 'default'." echo "network=default" return 0 fi first_net=$(printf '%s\n' "$active_nets" | head -n 1) if [[ -n "$first_net" ]]; then system::log_item "Using first active libvirt network: ${first_net}." echo "network=${first_net}" return 0 fi # If no active networks, fall back to any defined libvirt network (prefer default) defined_nets=$(virsh net-list --all --name 2>/dev/null | awk 'NF') if printf '%s\n' "$defined_nets" | grep -Fxq "default"; then system::log_item "Using defined libvirt network 'default' (will auto-start if needed)." echo "network=default" return 0 fi first_defined=$(printf '%s\n' "$defined_nets" | head -n 1) if [[ -n "$first_defined" ]]; then system::log_item "Using first defined libvirt network: ${first_defined}." echo "network=${first_defined}" return 0 fi fi system::log_item "No suitable bridge or libvirt network found; returning NONE." echo "NONE" return 1 } system::restart_sound() { # Description: Function to check for PipeWire or Pulse Audio and attempt to restart sound. # # This function checks if the system has PipeWire or Pulse Audio installed. If PipeWire is # installed, it restarts the pipewire.service. If Pulse Audio is installed, it kills the pulseaudio # process and reloads the ALSA sound driver. If neither PipeWire nor Pulse Audio is found, # it displays an error message. # # Globals: None # Arguments: None # Outputs: None # Returns: Standard error level. # Usage: Simply call this function in a script. # End of documentation if hash pw-cli; then systemctl --user restart pipewire.service elif hash pactl; then pulseaudio -k && sudo alsa force-reload else write_error "Pulse Audio or PipeWire sound software was not found..." fi } system::remove_old_kernel() { # Description: Purge older Linux kernel packages, keeping the currently running kernel. # Globals: OSTYPE (r) # Arguments: None # Outputs: Status messages via dialog::display_notice; package manager output on STDOUT/STDERR. # Returns: 0 on success; 1 on unsupported OS/PM or user cancellation. # Usage: # system::remove_old_kernel # Notes: # - Supports apt, zypper, and dnf-based distros; requires admin privileges. # - No action on non-Linux systems (returns 1 with an error). # End of documentation if echo "$OSTYPE" | grep -q "linux" &>/dev/null; then # these tests focus on the latest versions of the distros evaluated... security::ensure_admin if hash apt-get &>/dev/null; then dialog::display_notice "🧩 ${FUNCNAME[0]}: An apt based system has been found... proceeding... " if [[ $? -eq 0 ]]; then dpkg -l linux-* | awk '/^ii/{print $2}' | grep -E "[0-9]" | sort -t- -k3,4 --version-sort -r | sed -e "1,/$(uname -r | cut -f1,2 -d"-")/d" | grep -v -e "$(uname -r | cut -f1,2 -d"-")" | xargs apt-get -y purge return $? else return 1 fi elif hash zypper &>/dev/null; then dialog::display_notice "🧩 ${FUNCNAME[0]}: An Zypper based package management system has been found... proceeding... " if [[ $? -eq 0 ]]; then zypper purge-kernels return $? else return 1 fi elif hash dnf &>/dev/null; then dialog::display_notice "🧩 ${FUNCNAME[0]}: An DNF based package management system has been found... proceeding... " if [[ $? -eq 0 ]]; then dnf remove "$(dnf repoquery --installonly --latest-limit 2 -q)" return $? else return 1 fi else dialog::display_notice "${FUNCNAME[0]}: a supported package manager was not found for this action..." return 1 fi elif [[ "$OSTYPE" == "darwin"* ]]; then write_error "${FUNCNAME[0]}: Mac OSX is currently not supported!" return 1 elif [[ "$OSTYPE" == "cygwin" ]]; then write_error "${FUNCNAME[0]}: CYGWIN is currently unsupported!" return 1 elif [[ "$OSTYPE" == "msys" ]]; then write_error "${FUNCNAME[0]}: Lightweight shell is currently unsupported! " return 1 elif [[ "$OSTYPE" == "freebsd"* ]]; then write_error "${FUNCNAME[0]}: Free BSD is currently unsupported! " return 1 else echo "I have no Idea what this system is" return 1 fi } system::oem_autounlock_disk() { # Setup automatic unlocking of the encrypted system disk. # NOTE: This will render the encryption useless since the key to unlock the encrypted # volume will be located on an unencrypted location on the same system as the encrypted volume. # This is the same as locking your door and leaving the key by the door outside. # # The intention behind this is to be able to complete all build activites without manual intervention # of any kind. The intention is to remove the key file after all administrative tasks are complete. # # 1. Back up your initramfs disk # # Globals: # Arguments: None accepted # Outputs: # Returns: default exit status of the last command run. # Usage: oem_autounlock_disk # # End of Documentation local _pass="$1" local kernel_version=$(uname -r) local encrypted_dev local uuid # 1. Backup initramfs disk cp /boot/initrd.img-${kernel_version} /boot/initrd.img-${kernel_version}.bak # 2. Update GRUB configuration cat >/boot/grub/grub.cfg <<-EOF ### BEGIN /etc/grub.d/10_linux ### menuentry 'Debian GNU/Linux, with Linux ${kernel_version} (crypto safe)' { load_video insmod gzio insmod part_msdos insmod ext2 set root='hd0,msdos1' search --no-floppy --fs-uuid --set=root 2a5e9b7f-2128-4a50-83b6-d1c285410145 echo 'Loading Linux ${kernel_version} ...' linux /vmlinuz-${kernel_version} root=/dev/mapper/dradispro-root ro quiet echo 'Loading initial ramdisk ...' initrd /initrd.img-${kernel_version}.safe } ### END /etc/grub.d/10_linux ### EOF # 3. Create the key file in the unencrypted /boot partition dd if=/dev/urandom of=/boot/keyfile bs=1024 count=4 chmod 0400 /boot/keyfile # 4. Add the new file as unlock key to the encrypted volume encrypted_dev=$(blkid | grep crypto_LUKS | cut -d : -f 1) echo $_pass | cryptsetup -v luksAddKey "$encrypted_dev" /boot/keyfile - # 5. Edit /etc/crypttab cp /etc/crypttab /etc/crypttab.bak chmod 0600 /etc/crypttab uuid=$(udevadm info "$encrypted_dev" | grep by-uuid | cut -d : -f 2 | head -1) sed -i /"$(cat /etc/crypttab | cut -d " " -f 1)"/d /etc/crypttab echo "$(cat /etc/crypttab | cut -d " " -f 1-2)" /"${uuid}":/keyfile luks,keyscript=/lib/cryptsetup/scripts/pa$ >>/etc/crypttab chmod 0440 /etc/crypttab # 6. Generate new initramfs mkinitramfs -o /boot/initrd.img-${kernel_version} ${kernel_version} } system::get_Windows_Product_Key() { # Description: Function to retrieve a Windows product key from the BIOS. This assumes that the OEM # has indeed stored the windows key in the BIOS... Doing so is a common practice. It is useful to # retrieve the key if it is desired to run Windows in a Virtual Machine and Presumably would prefer to # not run the evaluation version of Windows. This function could also be used to take advantage of the # ${WinKey} variable and echo it in to the autoinattend.xml file for unattended builds. # # Globals: RTD_GUI # Arguments: None # Outputs: A variable named: "${WinKey}" and echoing its contents to standard out. # Returns: default exit status of the last command run. # Usage: get_Windows_Product_Key # End of documentation WinKey=$(sudo strings /sys/firmware/acpi/tables/MSDM | tail -1) echo "${WinKey}" } system::wait_for_internet_availability() { # Description: Waits for an active internet connection before proceeding. # Pings test IPs and verifies HTTP connectivity by fetching the external IP. # Globals: # _OEM_TEST_IPS: Optional, space-separated list of IPs for connectivity testing. # RTD_INTERNET_IP: Exported variable containing the external IP on success. # Arguments: None # Outputs: Status messages via write_status/write_error. # Returns: 0 if internet is available and external IP is fetched, 1 otherwise. # Dependencies: ping, curl, jq # Example: system::wait_for_internet_availability # End of documentation # --- Configuration --- local -r max_attempts=5 local -r ping_timeout_s=1 # Timeout for each ping packet local -r curl_connect_timeout_s=5 # Timeout for establishing curl connection local -r curl_max_time_s=10 # Max total time for curl operation local -r sleep_duration_s=5 # Sleep time between failed attempts # --- Dependency Check --- dependency::command_exists ping curl jq || { write_error "Missing dependencies: ping, curl, or jq." return 1 } local -a test_ips mapfile -t test_ips < <(network::get_oem_test_ips) # Ensure array is not empty if [[ ${#test_ips[@]} -eq 0 ]]; then write_error "No test IPs provided or configured." return 1 fi # --- Main Loop --- write_status "🌐 Checking for internet connectivity (max ${max_attempts} attempts)..." local -i attempt=1 while (( attempt <= max_attempts )); do write_status "--> Attempt ${attempt}/${max_attempts}:" # Select IP for this attempt (cycle through the list) local ip_index=$(( (attempt - 1) % ${#test_ips[@]} )) local current_ip="${test_ips[$ip_index]}" # 1. Ping Check write_status " Pinging ${current_ip}..." if ping -c 1 -W "$ping_timeout_s" "$current_ip" &>/dev/null; then write_status " Ping to ${current_ip} successful." # 2. HTTP/S Check (only if ping worked) write_status " Fetching external IP via HTTPS..." local response local curl_exit_code response=$(curl -s --connect-timeout "$curl_connect_timeout_s" --max-time "$curl_max_time_s" https://httpbin.org/ip) curl_exit_code=$? if [[ $curl_exit_code -eq 0 ]]; then local external_ip external_ip=$(echo "$response" | jq -r '.origin' 2>/dev/null) if [[ -n "$external_ip" ]]; then export RTD_INTERNET_IP="$external_ip" write_status "✅ Internet access confirmed. External IP: ${RTD_INTERNET_IP}" return 0 else write_status " ❌ Failed to parse external IP from response (jq or format issue)." fi else write_status " ❌ Failed to fetch external IP via curl (Exit code: ${curl_exit_code})." fi # Ping was successful, but HTTP/S check failed write_status " ❌ Internet access confirmed but unable to determine gateway address..." return 0 else write_status " ❌ Ping to ${current_ip} failed." fi write_status "<-- Attempt ${attempt} failed." # Sleep before next attempt, unless it was the last one if (( attempt < max_attempts )); then write_status " Waiting ${sleep_duration_s}s..." sleep "$sleep_duration_s" fi ((attempt++)) done # All attempts failed write_error "❌ Failed to confirm internet connectivity after ${max_attempts} attempts." unset RTD_INTERNET_IP return 1 } system::io_on_notify_wait() { # Description: Function that, when called, will wait for a disk change # and then execute the command passed as an argument. This is useful when # wanting to keep two location in sync with minimal delay, or to scan # for malware as soon as a change has occurred. # # Globals: # Arguments: None # Outputs: # Returns: # Usage: system::io_on_notify_wait rsync -avt . host:/remote/dir ... # End of documentation software::check_native_package_dependency inotify-tools EVENTS="CREATE,CLOSE_WRITE,DELETE,MODIFY,MOVED_FROM,MOVED_TO" if [ -z "$1" ]; then write_error "Usage: $0 rsync -avt . host:/remote/dir ..." exit 1 fi inotifywait -e "$EVENTS" -m -r --format '%:e %f' . | ( WAITING="" while true; do LINE="" read -t 1 LINE if test -z "$LINE"; then if test ! -z "$WAITING"; then echo "CHANGE" WAITING="" fi else WAITING=1 fi done ) | ( while true; do read TMP echo "$@" "$@" done ) } system::display_spinner() { # Description: Simple function to display a spinner in the terminal. # Globals: # Arguments: start/stop # Outputs: # Returns: # Usage: # # # # End of documentation function _spinner() { # $1 start/stop # # on start: $2 display message # on stop : $2 process exit status # $3 spinner function pid (supplied from stop_spinner) local on_success="DONE" local on_fail="FAIL" local nc="\e[0m" case $1 in start) # calculate the column where spinner and status msg will be displayed let column=$(tput cols)-${#2}-46 # display message and position the cursor in $column column # echo -ne ${2} printf "%${column}s" # start spinner i=1 sp='\|/-' delay=${SPINNER_DELAY:-0.15} while :; do printf "${RED}\b[${sp:i++%${#sp}:1}]\b\b${ENDCOLOR}" sleep $delay done ;; stop) if [[ -z ${3} ]]; then echo "spinner is not running.." exit 1 fi kill $3 >/dev/null 2>&1 # inform the user uppon success or failure echo -en "\b[" if [[ $2 -eq 0 ]]; then echo -en "${green}${on_success}${nc}" else echo -en "${red}${on_fail}${nc}" fi echo -e "]" ;; *) write_error "invalid argument, try {start/stop}" exit 1 ;; esac } function _start_spinner () { # $1 : msg to display _spinner "start" "${1}" & # set global spinner pid export _sp_pid=$! disown } function _stop_spinner () { # $1 : command exit status _spinner "stop" $1 $_sp_pid unset _sp_pid } if [[ "$1" == "start" ]]; then _start_spinner $2 elif [[ "$1" == "stop" ]]; then _stop_spinner $2 else write_error "invalid argument, try {start/stop}" fi } system::add_or_remove_login_script() { # Description: # Adds, removes, or toggles a .desktop launcher to execute a script at login. # # Arguments: # $1 - Action: --add, --remove, or blank (toggle) # $2 - Absolute path to script to run # $3 - Optional: Path to .desktop launcher (default: /etc/xdg/autostart/oem-run.desktop) # # Usage: # system::add_or_remove_login_script --add /path/to/script.sh # system::add_or_remove_login_script --remove /etc/xdg/autostart/oem-run.desktop # system::add_or_remove_login_script --remove /path/to/script.sh /etc/xdg/autostart/custom.desktop # system::add_or_remove_login_script /path/to/script.sh # # Returns: # 0 on success, 1 on failure local action="$1" local script_path="$2" local file_path="${3:-"/etc/xdg/autostart/oem-run.desktop"}" local log_file=${_LOGFILE:-"/tmp/${FUNCNAME[0]}.log"} security::ensure_admin if [[ -z "$script_path" || ! -f "$script_path" ]]; then write_error "🚫 Script not found: ${script_path}" return 1 fi system::log_item "📜 Action [$action] on login script (${script_path}), target: ${file_path}" _create_autostart_file() { local exec_line if command -v xterm >/dev/null 2>&1; then exec_line="/usr/bin/xterm -fa Monospace -iconic -fs 10 -e \"sudo -E bash -c \"$script_path\"\"" write_information "📜 Creating autostart entry using xterm" else software::add_native_package xterm exec_line="/usr/bin/xterm -fa Monospace -iconic -fs 10 -e \"sudo -E bash -c \"$script_path\"\"" write_information "📜 Creating autostart entry using default terminal" fi if ! tee "$file_path" <<-EOF [Desktop Entry] Type=Application Exec=$exec_line Terminal=false Hidden=false X-GNOME-Autostart-enabled=true Name=OEM Run Comment=OEM Setup EOF then write_error "🚫 Failed to create autostart entry: ${file_path}" return 1 fi if [[ -e "$file_path" ]]; then write_information "✅ Autostart entry created: ${file_path}" else write_error "🚫 Failed to create autostart entry: ${file_path}" return 1 fi if command -v desktop-file-validate >/dev/null 2>&1; then write_information "📜 Validating autostart entry: ${file_path}" if ! desktop-file-validate ${file_path} &>> "${log_file}" ; then write_error "🚫 Failed to validate autostart entry: ${file_path}" return 1 fi else write_warning "🚫 desktop-file-validate not found, skipping validation." fi } _remove_autostart_file() { if [[ -e "$file_path" ]]; then write_information "🗑️ Removing autostart entry: ${file_path}" if ! rm -f "$file_path"; then write_error "🚫 Failed to remove autostart file: ${file_path}" return 1 fi else write_status "ℹ️ Autostart entry already removed: ${file_path}" fi } case "$action" in --remove) _remove_autostart_file || return 1 ;; --add) _create_autostart_file || return 1 ;; *) if [[ -e "$file_path" ]]; then write_status "📜 Toggling OFF login script" _remove_autostart_file || return 1 else write_status "📜 Toggling ON login script" _create_autostart_file || return 1 fi ;; esac return 0 } util::validate_heredoc_tabs_only() { # Description: # Scans a script file for here-documents declared with <<-EOF # and verifies that the lines inside are indented only with TAB characters. # # Globals: # None # # Arguments: # $1 - Path to the script file to validate # # Outputs: # STDERR: Warning messages for lines with invalid indentation # # Returns: # 0 if all here-docs are correctly tab-indented, 1 otherwise local file="$1" local inside_heredoc=false local heredoc_delimiter local line_number=0 local valid=true if [[ ! -f "$file" ]]; then write_error "🚫 File not found: $file" return 1 fi local line key value while IFS= read -r line || [[ -n "$line" ]]; do ((line_number++)) if [[ "$inside_heredoc" == false ]]; then local pattern='^[[:space:]]*<<-[[:space:]]*([A-Za-z0-9_]+)' if [[ "$line" =~ $pattern ]]; then heredoc_delimiter="${BASH_REMATCH[1]}" inside_heredoc=true continue fi else # Check if this line is the closing heredoc delimiter (must match exactly with no trailing spaces) if [[ "$line" =~ ^[[:space:]]*${heredoc_delimiter}$ ]]; then inside_heredoc=false continue fi # If line is non-empty and does not start with a literal TAB, warn if [[ -n "$line" && "${line:0:1}" != $'\t' ]]; then printf "⚠️ Line %d: Non-TAB indentation inside <<-%s heredoc\n" "$line_number" "$heredoc_delimiter" >&2 valid=false fi fi done < "$file" [[ "$valid" == true ]] && return 0 || return 1 } system::update_config() { # Description: This function updates a configuration file with key-value pairs. # The `system::update_config` function is designed to update a configuration file # with key-value pairs. It takes as parameters the path to the configuration # file and the key-value pairs to be updated or added to the configuration file. # If the key already exists in the configuration file, the value is updated. # If the key does not exist in the configuration file, the key-value pair is added. # Globals: # Arguments: config_file key-value pairs # Outputs: The configuration file with the updated key-value pairs. # Returns: None # Usage: system::update_config /home/user/.config/myapp/config.conf key1 value1 key2 value2 # # Parameters: # - config_file: The path to the configuration file. # - key-value pairs: The key-value pairs to be updated or added to the configuration file. # Returns: None # # Example: # # system::update_config /home/user/.config/myapp/config.conf key1 value1 key2 value2 # # End of documentation local config_file=$1 shift # Shift past the first argument (the config file path) system::log_item "📔 Updating configuration file with key-value pairs: $*" mkdir -p ${config_file%/*} touch $config_file || system::log_item "Failed to create config file: $config_file" while (("$#")); do local key=$1 local value=$2 shift 2 # Shift past the processed key-value pair if grep -q "^$key=" "$config_file"; then system::log_item "📔 Update existing entry $key=$value in $config_file" sed -i "s|^$key=.*|$key=$value|" "$config_file" else system::log_item "📔 Add new entry $key=$value to $config_file" echo "$key=$value" >>"$config_file" fi done } system::read_config() { # Description: This function reads a configuration file with key-value pairs. # The configuration file should contain one key-value pair per line, with the # key and value separated by an equals sign. # Globals: # Arguments: config_file (path to configuration file) # Outputs: # Returns: standard exit code # Usage: system::read_config /home/user/.config/myapp/config.conf # # Parameters: # - config_file: The path to the configuration file. # # Returns: None # # Example: # system::read_config "/home/user/.config/myapp/config.conf" # # End of documentation # Specify the path to your configuration file local config_file=$1 system::log_item "📖 Reading configuration file: $config_file" # Read each line in the configuration file while IFS='=' read -r key value; do if [[ $key && $value ]]; then # Use declare to safely assign value to the variable named by 'key' declare -g "$key=$value" system::log_item "📖 Read key-value pair: $key=$value" fi done <"$config_file" } system::generate_report_disk_space_used_by_directory() { # Description: Function to generate a report for folders' disk space use. # This function requres one argument; what root folder to analyse the directories in. # This functio will return a report contained in the variable $return that may then be used. # Globals: # Arguments: None # Outputs: # Returns: # Usage: # Example: # system::generate_report_disk_space_used_by_directory /home # # This will generate a report on the disk space per directory in the /home folder. # # End of documentation DIRS=$(ls $1) store="/$(mktemp -d)/out.ct" $RTD_GUI --backtitle "$BRANDING" --title "📋 Generating report for: $1" --gauge "Calculating disk space: this may take some time" $HEIGHT $WIDTH < <( echo "Storage Utilization Report:" >>${store} echo "______________________________" >>${store} n=$(ls $1 | wc -l) i=0 for f in $DIRS; do du -cksh $1/$f 2>/dev/null | grep -v total >>${store} PCT=$((100 * (++i) / n)) cat <<-EOF XXX $PCT Completed calculation for: "$f"... XXX EOF sleep 1 done ) total=$(echo ----- >>${store} && du -cksh $1 2>/dev/null | grep total >>${store}) | $RTD_GUI --backtitle "$BRANDING" --title "Completing report..." --progressbox "Working..." $HEIGHT $WIDTH result="$(cat ${store})" rm ${store} } system::toggle_oem_auto_login() { # Description: # Toggles auto-login for major display managers and TTY on Debian, Ubuntu, Fedora, SUSE-based distros. # # Globals: # _OEM_USER - Username to auto-login (default: tangarora) # _TLA - Optional namespace for RTD backups # # Usage: # system::toggle_oem_auto_login --enable|--disable # system::toggle_oem_auto_login # # Dependencies: # Requires root privileges to modify DM and systemd configs. local _flip case "$1" in --help|-h) write_information "Usage: system::toggle_oem_auto_login [--enable|--disable]" return 0 ;; --enable|-e) _flip="on" write_status "🔒 Enabling auto-login..." ;; --disable|-d) _flip="off" write_status "🔒 Disabling auto-login..." ;; *) _flip="toggle" write_status "🔁 Toggling auto-login..." ;; esac local user="${_OEM_USER:-tangarora}" local _backup_dir="/etc/${_TLA,,}/backup" mkdir -p "$_backup_dir" local toggle= __toggle_dm_autologin() { local name="$1" local file="$2" local content="$3" local fallback="$4" if [[ ! -f "$file" ]]; then return 1; fi local backup_file="${_backup_dir}/${file##*/}.rtd-bak" if [[ "$_flip" == "off" ]]; then write_status "⛔ $name: disabling auto-login..." if [[ -f "$backup_file" ]]; then mv -f "$backup_file" "$file" || write_error "❌ Failed to restore backup for $name" else rm -f "$file" || write_warning "⚠️ Could not remove $file" fi else write_status "✅ $name: enabling auto-login..." cp -f "$file" "$backup_file" 2>/dev/null || true printf "%s\n" "$content" > "$file" fi toggle=1 return 0 } __toggle_dm_autologin "LightDM" "/etc/lightdm/lightdm.conf" \ "[Seat:*] autologin-user=$user autologin-user-timeout=0" || true __toggle_dm_autologin "SDDM" "/etc/sddm.conf" \ "[Autologin] User=$user Session=plasma.desktop" || true __toggle_dm_autologin "GDM3 (Ubuntu)" "/etc/gdm3/custom.conf" \ "[daemon] AutomaticLoginEnable=True AutomaticLogin=$user" || true __toggle_dm_autologin "GDM3 (Debian)" "/etc/gdm3/daemon.conf" \ "[daemon] AutomaticLoginEnable=True AutomaticLogin=$user" || true __toggle_dm_autologin "GDM (Fedora)" "/etc/gdm/custom.conf" \ "[daemon] WaylandEnable=true AutomaticLoginEnable=True AutomaticLogin=$user [security] [xdmcp] [chooser] [debug]" || true # SUSE special case if [[ -f /etc/sysconfig/displaymanager ]]; then local file="/etc/sysconfig/displaymanager" cp -f "$file" "$_backup_dir/$(basename "$file").rtd-bak" 2>/dev/null || true if [[ "$_flip" == "off" ]]; then write_status "⛔ SUSE: disabling auto-login..." sed -i 's/^DISPLAYMANAGER_AUTOLOGIN=.*/DISPLAYMANAGER_AUTOLOGIN=""/' "$file" sed -i 's/^DISPLAYMANAGER_PASSWORD_LESS_LOGIN=.*/DISPLAYMANAGER_PASSWORD_LESS_LOGIN="no"/' "$file" else write_status "✅ SUSE: enabling auto-login..." sed -i "s/^DISPLAYMANAGER_AUTOLOGIN=.*/DISPLAYMANAGER_AUTOLOGIN=\"$user\"/" "$file" sed -i 's/^DISPLAYMANAGER_PASSWORD_LESS_LOGIN=.*/DISPLAYMANAGER_PASSWORD_LESS_LOGIN="yes"/' "$file" fi toggle=1 fi # TTY auto login local tty_conf="/etc/systemd/system/getty@tty1.service.d/override.conf" local tty_dir="${tty_conf%/*}" mkdir -p "$tty_dir" if [[ "$_flip" == "off" ]]; then write_status "⛔ TTY: disabling auto-login..." rm -f "$tty_conf" else write_status "✅ TTY: enabling auto-login..." cat > "$tty_conf" <<-EOF [Service] ExecStart= ExecStart=-/sbin/agetty --noissue --autologin $user %I \$TERM Type=idle EOF fi toggle=1 # Fallback: if no DM matched, drop in a basic SDDM config if [[ -z "$toggle" ]]; then write_warning "⚠️ No supported display manager config found. Creating fallback SDDM config..." cat > /etc/sddm.conf <<-EOF [Autologin] User=$user Session=plasma.desktop EOF fi } system::set_oem_elevated_privilege_gui() { # Description # This function configures the local X environment to allow root to display GUI applications. # It is particularly useful for Debian and other Linux distributions where GUI apps cannot # be run under sudo or in a root environment by default. This function modifies the .bashrc # files for the specified user and root to enable GUI applications to be launched with elevated # privileges. It is compatible with distributions like Slackware and Debian. # # Globals: # # $_OEM_USER: The username for whom the X environment will be configured. # # Arguments: # # None. The function does not take any arguments. # # Outputs: # # The function modifies the .bashrc file in the home directory of $_OEM_USER and the root user # to configure the X environment. It outputs information about the changes being made. # # Returns: # # None. The function does not return a value. # # Usage: # # To use this function, ensure that the $_OEM_USER global variable is set to the appropriate username, # then call the function: # # _OEM_USER="username" # set_oem_elevated_privilege_gui # # This will append the necessary configurations to the .bashrc files of the specified user and the root user. # Function Behavior: # # The function appends xhost local:root to the .bashrc file in the home directory of $_OEM_USER. # This command allows the root user to display GUI applications on the local X server. # It also appends export XAUTHORITY=/home/${_OEM_USER}/.Xauthority to /root/.bashrc. This export # sets the XAUTHORITY environment variable for the root user, pointing to the .Xauthority file in # the home directory of $_OEM_USER. # # End of documentation local user_bashrc="/home/${_OEM_USER:-"tangarora"}/.bashrc" local root_bashrc="/root/.bashrc" case $1 in --help | -h) write_information "usage : system::set_oem_elevated_privilege_gui [--enable|--disable]" return ;; --enable | -e) write_status "🚧 Enabling GUI elevated privileges..." # Ensure the commands are idempotent: don't add lines if they already exist if [[ ! -f "${user_bashrc}" ]]; then touch "${user_bashrc}" fi grep -qxF 'xhost local:root' "${user_bashrc}" || echo "xhost local:root" >>"${user_bashrc}" grep -qxF "export XAUTHORITY=/home/${_OEM_USER:-"tangarora"}/.Xauthority" "${root_bashrc}" || echo "export XAUTHORITY=/home/${_OEM_USER:-"tangarora"}/.Xauthority" >>"${root_bashrc}" write_information "🚧 Editing bashrc: xhost local:root in ${user_bashrc}" write_information "🚧 Editing bashrc: export XAUTHORITY=/home/${_OEM_USER:-"tangarora"}/.Xauthority in ${root_bashrc}" ;; --disable | -d) write_status "🚧 Disabling GUI elevated privileges..." if [[ ! -f "${user_bashrc}" ]]; then write_error "⛔ Error: ${user_bashrc} does not exist." return 1 fi # Use sed to remove the specific lines added during the enable step sed -i '/xhost local:root/d' "${user_bashrc}" sed -i "/export XAUTHORITY=\/home\/${_OEM_USER:-"tangarora"}\/.Xauthority/d" "${root_bashrc}" write_information "🚧 Removed xhost command from ${user_bashrc}" write_information "🚧 Removed XAUTHORITY export from ${root_bashrc}" ;; *) write_status "👽 Un-known option. Use --enable to enable or --disable to disable GUI elevated privileges." ;; esac } system::preserve_graphical_session_environment() { # Description: Ensure graphical environment variables are set when running as root after escalation. # # Globals: # - SUDO_USER # - PKEXEC_UID # - DISPLAY # - WAYLAND_DISPLAY # - XDG_RUNTIME_DIR # - DBUS_SESSION_BUS_ADDRESS # - XAUTHORITY # Arguments: # - None # Outputs: # - Exports DISPLAY, WAYLAND_DISPLAY, XDG_RUNTIME_DIR, DBUS_SESSION_BUS_ADDRESS, and XAUTHORITY when missing. # Returns: # - 0 on success or when no action is required. # Usage: # system::preserve_graphical_session_environment # End of Documentation if [[ $EUID -ne 0 ]]; then return 0 fi local invoking_user="" local invoking_uid="" if [[ -n "${PKEXEC_UID:-}" ]]; then invoking_uid="$PKEXEC_UID" invoking_user=$(id -nu "$PKEXEC_UID" 2>/dev/null || true) elif [[ -n "${SUDO_USER:-}" ]]; then invoking_user="$SUDO_USER" invoking_uid=$(id -u "$SUDO_USER" 2>/dev/null || true) fi if [[ -z "$invoking_user" || -z "$invoking_uid" ]]; then system::log_item "system::preserve_graphical_session_environment: unable to determine invoking user." return 1 fi local runtime_dir="/run/user/${invoking_uid}" local display_hint="" if command -v loginctl >/dev/null 2>&1; then display_hint=$(loginctl show-user "$invoking_user" -p Display --value 2>/dev/null || true) fi if [[ -z "${DISPLAY:-}" ]]; then if [[ -n "$display_hint" ]]; then DISPLAY="$display_hint" else DISPLAY=":0" fi export DISPLAY fi if [[ -z "${WAYLAND_DISPLAY:-}" && -n "$display_hint" ]]; then [[ "$display_hint" == wayland* ]] && export WAYLAND_DISPLAY="$display_hint" fi if [[ -z "${XDG_RUNTIME_DIR:-}" ]]; then export XDG_RUNTIME_DIR="$runtime_dir" fi if [[ -z "${DBUS_SESSION_BUS_ADDRESS:-}" && -d "$runtime_dir" ]]; then export DBUS_SESSION_BUS_ADDRESS="unix:path=${runtime_dir}/bus" fi if [[ -z "${XAUTHORITY:-}" ]]; then local auth_file auth_file=$(eval echo "~${invoking_user}/.Xauthority") [[ -f "$auth_file" ]] && export XAUTHORITY="$auth_file" fi if command -v xhost >/dev/null 2>&1; then if command -v runuser >/dev/null 2>&1; then runuser -u "$invoking_user" -- xhost +SI:localuser:root >/dev/null 2>&1 || true else su - "$invoking_user" -c "xhost +SI:localuser:root" >/dev/null 2>&1 || true fi fi return 0 } system::toggle_oem_auto_elevated_privilege() { # Description: Toggles passwordless sudo privileges for a specified user. # WARNING: This function modifies sudoers and can pose a security risk. # Ensure to disable this with 'rtd_oem_reset_default_environment_config' after use. # Globals: # _OEM_USER - The user for whom to toggle passwordless sudo. Defaults to SUDO_USER. # Arguments: None # Usage: # system::toggle_oem_auto_elevated_privilege [--enable|--disable] security::ensure_admin : ${_OEM_USER:="${SUDO_USER}"} local sudoers_file="/etc/sudoers.d/99_sudo_include_file" if [[ -z "${_OEM_USER}" ]]; then write_error "⛔ Error: User not specified for elevated privileges." return 1 fi case ${1} in --help | -h) write_information "usage : system::toggle_oem_auto_elevated_privilege [--enable|--disable]" return ;; --enable | -e) write_status "🚧 Enabling passwordless sudo for user: ${_OEM_USER}" # Ensure passwordless sudo is not already set for the user if ! grep -q "^${_OEM_USER} ALL=(ALL) NOPASSWD:ALL" "${sudoers_file}" 2>/dev/null; then # Add passwordless sudo entry if [[ ! -f "${sudoers_file}" ]]; then write_information "🚧 Adding passwordless sudo for user: ${_OEM_USER}" cat >"${sudoers_file}" <<-EOF # Sudoers file for RTD OEM user elevated privileges # Created on $(date) ${_OEM_USER} ALL=(ALL) NOPASSWD:ALL EOF if ! grep -q "^${_OEM_USER} ALL=(ALL) NOPASSWD:ALL" "${sudoers_file}" 2>/dev/null; then write_error "⛔ Error: Failed to add passwordless sudo for user: ${_OEM_USER}" else write_information "🚧 Created sudoers file and added passwordless sudo for user: ${_OEM_USER}" fi else echo "${_OEM_USER} ALL=(ALL) NOPASSWD:ALL" | sudo EDITOR='tee -a' visudo -f "${sudoers_file}" fi else write_information "✅ Passwordless sudo already enabled for user: ${_OEM_USER}" fi ;; --disable | -d) write_status "🚧 Disabling passwordless sudo for user: ${_OEM_USER}" # Check if passwordless sudo is set for the user and remove it if grep -q "^${_OEM_USER} ALL=(ALL) NOPASSWD:ALL" "${sudoers_file}" 2>/dev/null; then # Remove passwordless sudo entry write_information "🚧 Removing passwordless sudo for user: ${_OEM_USER}" sed -i "/^${_OEM_USER} ALL=(ALL) NOPASSWD:ALL/d" "${sudoers_file}" if grep -q "^${_OEM_USER} ALL=(ALL) NOPASSWD:ALL" "${sudoers_file}" 2>/dev/null; then write_error "⛔ Error: Failed to remove passwordless sudo for user: ${_OEM_USER}" else write_information "🚧 Removed passwordless sudo for user: ${_OEM_USER}" fi else write_information "✅ Passwordless sudo already disabled for user: ${_OEM_USER}" fi ;; *) write_status "👽 Unknown option. Use --enable to enable or --disable to disable passwordless sudo." return 1 ;; esac # Validate sudoers file syntax if [[ -f "${sudoers_file}" ]]; then write_information "🚧 Validating sudoers file syntax..." if ! visudo -cf "${sudoers_file}" &>/dev/null; then write_error "⛔ Error: Syntax error in sudoers file." else write_information "🏆 Sudoers file syntax is valid." fi else write_status "🔍 ${sudoers_file} not present." fi write_information "🚧 Passwordless sudo configuration updated for ${_OEM_USER}." } system::make_system_recovery_partition() { # Description: Incomplete function to build an OEM rescue partition... # Function to enable system OEM Recovery # Globals: # Arguments: None # Outputs: # Returns: # Usage: NOTE: I have yet to figura out how to do this! # End of documentation # Find removable devices that are not loops or CD/DVD drives readarray -t devlist < <(lsblk -dnlo name,rm | awk '$2 == "1" {print $1}') if [[ ${#devlist[@]} -eq 0 ]]; then # No removable media found DIALOGRC="/root/.dialogrc" [[ -e $DIALOGRC ]] && mv "$DIALOGRC" "${DIALOGRC}.bak" echo 'screen_color = (CYAN,RED,ON)' >"$DIALOGRC" dialog --title "Error!" --backtitle "OS Media Manager" --msgbox "No removable media found!" 14 90 rm -f "$DIALOGRC" [[ -e ${DIALOGRC}.bak ]] && mv "${DIALOGRC}.bak" "$DIALOGRC" return 1 fi # If only one removable device is found, use it directly if [[ ${#devlist[@]} -eq 1 ]]; then local targetDevice="/dev/${devlist[0]}" else # If multiple devices are found, let the user choose local menuOptions=() for dev in "${devlist[@]}"; do menuOptions+=("$dev" "$(fdisk -l /dev/$dev | head -1)") done targetDevice=$(dialog --stdout --title "Choose a device" --menu "Select target device:" 0 0 0 "${menuOptions[@]}") targetDevice="/dev/$targetDevice" fi if [[ -z $targetDevice ]]; then echo "No device selected or dialog cancelled." return 1 fi echo "Using device: $targetDevice for recovery partition." mkdir -p /boot/iso if dd if="$targetDevice" of=/boot/iso/recovery.iso status=progress; then echo "Recovery image created successfully." else echo "Failed to create recovery image." return 1 fi # Append custom GRUB entry cat >>/etc/grub.d/40_custom <<-EOF menuentry "Reset to Factory Defaults" { set isofile="/boot/iso/recovery.iso" loopback loop (hd0,1)\$isofile linux (loop)/casper/vmlinuz boot=casper iso-scan/filename=\$isofile noprompt noeject initrd (loop)/casper/initrd.lz } EOF echo "GRUB menu entry added. Please update GRUB configuration with 'update-grub'." } system::run_command_in_gnome_user_session() { # Description: Function to do a reverse sudo back to teh original Gnome user who called upon sudo. # This functon expects any number of parameters that would make up the command or commands # to be run in the users session. For example: # system::run_command_in_gnome_user_session script_name.sh parameter one two etc # # Globals: # Arguments: [path/script.sh] [bash command] # Outputs: # Returns: # Usage: system::run_command_in_gnome_user_session script_name.sh # # End of documentation echo -------------------------- Begin as user: 🪪 $SUDO_USER --------------------- echo "$*" sudo -H -u "$SUDO_USER" DISPLAY=$DISPLAY DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/$(id -u "$SUDO_USER")/bus $* echo " " echo -------------------------- End as user: 🪪 $SUDO_USER --------------------- } system::rtd_oem_reseal::_virtualization_context() { # Description: # Detects whether the current system is running as a virtual guest. # Globals: # None # Arguments: # None # Outputs: # Echoes "virtual" if virt-what detects a hypervisor, otherwise "physical". # Returns: # 0 on success. Non-zero only when virt-what invocation fails. # Usage: # local _virt; _virt="$(system::rtd_oem_reseal::_virtualization_context)" # End of documentation local _is_virtual="physical" if hash virt-what 2>/dev/null; then local _virt_result _virt_result="$(virt-what 2>/dev/null | tr -d '\n')" if [[ -n "${_virt_result}" ]]; then _is_virtual="virtual" fi fi echo "${_is_virtual}" } system::rtd_oem_reseal::_install_guest_additions() { # Description: # Ensures guest integration packages are installed regardless of distro. # Globals: # None # Arguments: # None # Outputs: # Status messages via write_status. # Returns: # Always 0; errors are ignored to avoid breaking reseal workflows. # Usage: # system::rtd_oem_reseal::_install_guest_additions # End of documentation write_status "🛠 Installing guest integration tools (qemu-guest-agent, spice-vdagent)..." software::add_native_package qemu-guest-agent spice-vdagent || true } system::rtd_oem_reseal::_prepare_ubuntu_like() { # Description: # Runs OEM reseal prep for Ubuntu-like distributions. # Globals: # None # Arguments: # $1 - virtualization context ("virtual" or "physical"). # Outputs: # Status via write_status/write_information. # Returns: # 0 on success. # Usage: # system::rtd_oem_reseal::_prepare_ubuntu_like "$(system::rtd_oem_reseal::_virtualization_context)" # End of documentation local _virt="${1}" software::add_native_package "oem-config-gtk" oem-config-prepare if [[ "${_virt}" == "virtual" ]]; then system::rtd_oem_reseal::_install_guest_additions fi } system::rtd_oem_reseal::_prepare_debian() { # Description: # Performs Debian-specific reseal tasks (cloud-init cleanup, host keys, etc.). # Globals: # None # Arguments: # $1 - virtualization context ("virtual" or "physical"). # Outputs: # Status via write_status. # Returns: # 0 on success. # Usage: # system::rtd_oem_reseal::_prepare_debian "${_virt}" # End of documentation local _virt="${1}" write_status "🧹 Preparing Debian-based system for reseal" software::add_native_package cloud-init || true cloud-init clean --logs >>"${_LOGFILE}" 2>&1 || cloud-init clean >>"${_LOGFILE}" 2>&1 || true truncate -s 0 /etc/machine-id 2>/dev/null || true rm -f /var/lib/dbus/machine-id ln -sf /etc/machine-id /var/lib/dbus/machine-id rm -f /etc/ssh/ssh_host_* 2>/dev/null || true if [[ "${_virt}" == "virtual" ]]; then system::rtd_oem_reseal::_install_guest_additions fi } system::rtd_oem_reseal::_prepare_fedora() { # Description: # Enables the Fedora/RHEL initial-setup workflow and guest tools. # Globals: # None # Arguments: # $1 - virtualization context ("virtual" or "physical"). # Outputs: # Status via write_status. # Returns: # 0 on success. # Usage: # system::rtd_oem_reseal::_prepare_fedora "${_virt}" # End of documentation local _virt="${1}" write_status "🤞 Preparing Fedora/RedHat-based system for reseal" software::add_native_package initial-setup initial-setup-gui || true systemctl enable initial-setup-graphical.service >>"${_LOGFILE}" 2>&1 || \ systemctl enable initial-setup-text.service >>"${_LOGFILE}" 2>&1 || true if [[ "${_virt}" == "virtual" ]]; then system::rtd_oem_reseal::_install_guest_additions fi } system::rtd_oem_reseal::_prepare_suse() { # Description: # Enables YaST firstboot and guest tools on SUSE-based distributions. # Globals: # None # Arguments: # $1 - virtualization context ("virtual" or "physical"). # Outputs: # Status via write_status. # Returns: # 0 on success. # Usage: # system::rtd_oem_reseal::_prepare_suse "${_virt}" # End of documentation local _virt="${1}" write_status "👽 Preparing SUSE-based system for reseal" software::add_native_package yast2-firstboot || true systemctl enable yast2-firstboot.service >>"${_LOGFILE}" 2>&1 || true if [[ "${_virt}" == "virtual" ]]; then system::rtd_oem_reseal::_install_guest_additions fi } system::rtd_oem_reseal::_finalize_shutdown() { # Description: # Logs the shutdown context and powers the system off. # Globals: # None # Arguments: # $1 - virtualization context ("virtual" or "physical"). # Outputs: # Status via write_status. # Returns: # Does not return; triggers shutdown. # Usage: # system::rtd_oem_reseal::_finalize_shutdown "${_virt}" # End of documentation local _virt="${1}" if [[ "${_virt}" == "virtual" ]]; then write_status "💻 This is a virtual machine. Shutting down..." else write_status "🖥️ This is a physical machine. Shutting down..." fi shutdown -h now } system::rtd_oem_reseal() { # Description: # Function to reseal a newly installed system for delivery to end user. # Globals: Requires distro-specific OEM tools or reseal packages to be available. # Arguments: None # Outputs: Logs reseal steps and powers the system off. # Returns: # Usage: # End of documentation if [[ ! -r /etc/os-release ]]; then write_warning "🤷‍♀️ Unable to determine distribution for reseal (missing /etc/os-release)" return 1 fi # shellcheck source=/dev/null . /etc/os-release local _os_id="${ID,,}" local _os_like="${ID_LIKE,,}" local _virt _virt="$(system::rtd_oem_reseal::_virtualization_context)" case "${_os_id}" in ubuntu | kubuntu | zorin | linuxmint | pop | elementary) system::rtd_oem_reseal::_prepare_ubuntu_like "${_virt}" ;; debian) system::rtd_oem_reseal::_prepare_debian "${_virt}" ;; fedora) system::rtd_oem_reseal::_prepare_fedora "${_virt}" ;; opensuse* | suse | sles*) system::rtd_oem_reseal::_prepare_suse "${_virt}" ;; *) if [[ "${_os_like}" == *"debian"* || "${_os_like}" == *"ubuntu"* ]]; then system::rtd_oem_reseal::_prepare_ubuntu_like "${_virt}" elif [[ "${_os_like}" == *"suse"* ]]; then system::rtd_oem_reseal::_prepare_suse "${_virt}" elif [[ "${_os_like}" == *"fedora"* || "${_os_like}" == *"rhel"* || "${_os_like}" == *"centos"* ]]; then system::rtd_oem_reseal::_prepare_fedora "${_virt}" else write_warning "🤷‍♀️ I dont know of any OEM configuration for this distribution" return 1 fi ;; esac system::rtd_oem_reseal::_finalize_shutdown "${_virt}" } system::create_swapfile() { # Description: Function to create a swapfile and enable it automatically. # # Globals: # Arguments: None # Outputs: # Returns: # Usage: # Usage: [function_name] [options] # Arguments: # [size] : size in MB or GB # [path] : path to file (Default path: /swapfile) # # NOTE: # The Linux Kernel divides RAM into chunks of memories and the swapping process is # when the Linux Kernel uses a hard disk space (swap space) to store information from RAM # and thus releases some RAM space. That is why when you install a Linux distribution, the # installation wizard usually asks you to assign some space for the system and another for the swap. # # Using swap is a very useful way to extend the RAM because it provides the necessary additional # memory when the RAM space has been exhausted and a process has to be continued. It is especially # recommended when you have less than 1Gb of RAM. Although in the end, everything depends on you. # # End of documentation SWAP_SIZE="$1" SWAP_PATH="$2" : "${SWAP_SIZE:=4G}" : "${SWAP_PATH:="/swapfile-${RANDOM}"}" write_information "Swap Size: ${SWAP_SIZE}" write_information "File Path: ${SWAP_PATH}" system::create_swapfile::usage_mesage() { write_host --cyan " Usage: ${FUNCNAME[1]} {size} {path} Example: ${FUNCNAME[1]} 4G (Default path: /swapfile) Usage with optional path: ${FUNCNAME[1]} {size} {path}" } if [[ -z "${SWAP_SIZE}" ]]; then # If we are not given any size to make the swaptfile... well... write_error "Please tell me what size swap file to make!" system::create_swapfile::usage_mesage exit 1 elif [[ ! "${SWAP_SIZE:0:1}" -ge "1" ]]; then # Fail if the argument is not a numer write_error "The size for swap file must be a number!" system::create_swapfile::usage_mesage exit 1 else if [[ -e ${SWAP_PATH} ]]; then write_error "ERROR: the file ${SWAP_PATH} already exists! Please choose a different name." exit 1 fi write_host --cyan "Welcome to Swap setup script! This script will automatically setup a swap file and enable it." fallocate -l "${SWAP_SIZE}" "${SWAP_PATH}" && write_information "Allocated ${SWAP_SIZE} swap file: ${SWAP_PATH}" || ( write_error "Error creting actual swapfile using fallocate! " exit 1 ) chmod 600 "${SWAP_PATH}" && write_information "Sucessfully modified permissions on $SWAP_PATH" || ( write_error "Error changing premissions" exit 1 ) mkswap "${SWAP_PATH}" && ( write_status "Sucessfully formatted ${SWAP_PATH} as a swap partition" file -s ${SWAP_PATH} ) || ( write_error "Error formating ${SWAP_PATH} as a swapspace!" exit 1 ) swapon "${SWAP_PATH}" # Turn on the swap file... if [ $? = 0 ]; then echo "${SWAP_PATH} none swap sw 0 0" | sudo tee /etc/fstab -a # Add to fstab write_status "Done! You now have a ${SWAP_SIZE} swap file at ${SWAP_PATH}" else if df -T ${SWAP_PATH} | grep btrfs >/dev/null; then write_error "ERROR: swapfiles cannot be allocated on BTRFS. \n The root filesystem is BTRFS. Please use an ext4 or xfs partition. This is a BUG and hopefully it will be fixed soon... " exit 1 else write_error "An unknown error occurred with starting the swap partition.." exit 1 fi fi fi } system::tune_system_power_profile() { # Description: Tuned is a Linux feature that monitors a system and optimizes its performance # under certain workloads. Tuned uses profiles to do this. A profile is a set of rules that # defines certain system parameters such as disk settings, kernel parameters, network # optimization settings, and many other aspects of the system. # # This function sets the performance according to laptop/desktop/vm based on what is detected. # Globals: # Arguments: None # Outputs: # Returns: # Usage: simply call function in a script # End of documentation if ! software::check_native_package_dependency tuned ; then system::log_item "${FUNCNAME[0]}: The required software tuned is not available! Cannot continue." return 1 fi systemctl enable --now tuned tuned-adm profile balanced case "${1}" in --quiet | -q) : ${_print="#"} ;; *) unset _print ;; esac if ! software::check_native_package_dependency virt-what ; then system::log_item "${FUNCNAME[0]}: The required software virt-what is not available! Cannot continue." return 1 fi if virt-what; then "${_print}" write_information "${FUNCNAME[0]}: This is likely not running in a virtual machine." local chassis_type chassis_type="$(system::laptop_detect)" case "${chassis_type}" in laptop) "${_print}" write_information "${FUNCNAME[0]}: This is likely a laptop; applying pwersave profile..." tuned-adm profile powersave ;; desktop) "${_print}" write_information "${FUNCNAME[0]}: This is likely NOT a laptop; applying desktop profile..." tuned-adm profile desktop ;; *) "${_print}" write_information "${FUNCNAME[0]}: Unable to determine chassis type; defaulting to balanced profile..." tuned-adm profile balanced ;; esac else "${_print}" write_information "${FUNCNAME[0]}: This is likely a VM; applying virtual-guest profile..." tuned-adm profile virtual-guest fi } system::rtd_oem_find_live_release() { # Description: This function will return the URL for the version of Debian, Ubuntu server or Destktop requested. # 3 parameters are required for this function to know what to do: distro_version/distro_flavor/live_or_not # # Globals: # Arguments: None # Outputs: # Returns: Return a URL to the desired release ISO of Ubuntu or Debian. # Usage: # Example: # Function name : distro_version : distro_flavor : live_or_not : desktop_env (only Debian) # system::rtd_oem_find_live_release 10 debian live kde # system::rtd_oem_find_live_release 10 debian live cinnamon # system::rtd_oem_find_live_release 10 debian net ssh-server # system::rtd_oem_find_live_release 19.04.2 ubuntu live # End of documentation # local distro_version="${1:-12}" # local distro_flavor="${2:-debian}" # local live_or_not="${3:-live}" # local desktop_env="${4:-standard}" while [[ $# -gt 0 ]]; do case "$1" in --server | -s) live_or_not="server" system::log_item "🐧 Requested [server]..." shift ;; --netinst | -n) live_or_not="netinst" system::log_item "🐧 Requested [netinst]..." shift ;; --mini | -m) live_or_not="mini" system::log_item "🐧 Requested [mini]..." shift ;; --desktop | -d) live_or_not="desktop" system::log_item "🐧 Requested [desktop]..." shift ;; --distribution | -dist) distro_flavor="$2" system::log_item "🐧 Requested distribution [$2]..." shift ;; --version | -v) distro_version="$2" system::log_item "🐧 Requested version [$2]..." shift ;; --help | -h) echo "Usage: system::rtd_oem_find_live_release --distribution --version --server --desktop " return 0 ;; *) shift ;; esac done case "$distro_flavor" in Ubuntu | ubuntu | ubuntu-desktop | Ubuntu-desktop) case "$live_or_not" in desktop | live) system::log_item "🌐 Providing requested URL: http://releases.ubuntu.com/${distro_version}/$(curl --silent http://releases.ubuntu.com/${distro_version}/SHA256SUMS | grep -o 'ubuntu-.*-desktop-amd64.iso')" echo "http://releases.ubuntu.com/${distro_version}/$(curl --silent http://releases.ubuntu.com/${distro_version}/SHA256SUMS | grep -o 'ubuntu-.*-desktop-amd64.iso')" ;; server | server-desktop) # 18.04 and later need to use the alternative installer # since the switch to subiquity system::log_item "🌐 Providing requested URL: http://releases.ubuntu.com/${distro_version}/ubuntu-${distro_version}-live-server-amd64.iso" echo "http://releases.ubuntu.com/${distro_version}/ubuntu-${distro_version}-live-server-amd64.iso" ;; *) system::log_item "⛔ Encountered an error: The 4th parameter should be either of: Live | live | current-live | server | netinst | net | mini! Not $live_or_not" $RTD_GUI --backtitle "$BRANDING" --title "ERROR!" --msgbox "You passed the function ${FUNCNAME[0]} the parameters $*. The 4th parameter should be either of: Live | live | current-live | server | netinst | net | mini! Not $live_or_not" 10 60 ;; esac ;; Debian | debian) case "$live_or_not" in Live | live | current-live) # Debian renames the current iso images for each release. To get around this when downloading # the vesion number must be ignored and the desired flavor of the iso should be grabed. case "$desktop_env" in kde | gnome | xfce | cinnamon | lxde | lxqt | mate | standard) rel=https://cdimage.debian.org/debian-cd/current-live/amd64/iso-hybrid/ echo "$rel/$(curl --silent $rel/MD5SUMS | \grep -o "debian-.*$desktop_env*.iso")" ;; *) system::log_item "⛔ Encountered an error: The 4th parameter should be wither of: kde | gnome | xfce | cinnamon | lxde | lxqt | mate | standard! Not $live_or_not " $RTD_GUI --backtitle "$BRANDING" --title "ERROR!" --msgbox "You passed the function ${FUNCNAME[0]} the parameters $*. The 4th parameter should be wither of: kde | gnome | xfce | cinnamon | lxde | lxqt | mate | standard! Not $live_or_not " 10 60 ;; esac ;; netinst | net | mini) # Find and download mini iso... system::log_item "⛔ Encountered an error: The 2nd parameter netinst debian is not logical. Please use the kernel params instead." ;; *) system::log_item "⛔ Encountered an error: The 2nd parameter should be either of: Live | live | current-live | netinst | net | mini ! not $live_or_not" $RTD_GUI --backtitle "$BRANDING" --title "ERROR!" --msgbox "You passed the function ${FUNCNAME[0]} the parameters $*. The 2nd parameter should be either of: Live | live | current-live | netinst | net | mini ! not $live_or_not" 10 60 ;; esac ;; *) $RTD_GUI --backtitle "$BRANDING" --title "⛔ ERROR!" --msgbox "You passed the function ${FUNCNAME[0]} the parameters $*. The 2nd parameter should be either of: Ubuntu | ubuntu Debian | debian! not $distro_flavor" 10 60 ;; esac } system::validate_parameters() { # Description: Function to validate that all parameters match the pattern --option "value". # Globals: None # Arguments: $@ - The parameters to validate. # Outputs: Error messages for any invalid parameters. # Returns: 0 if all parameters are valid, 1 if any invalid parameters are found. # Usage: validate_parameters "$@" # End of documentation # Log the start of parameter validation system::log_item "Validating parameter format..." system::log_item "Parameters: $*" # Initialize an index to iterate through the parameters local index=0 # Iterate through the parameters while [[ $# -gt 0 ]]; do case "$1" in --*) # Check if the next parameter is provided and is not another option if [[ -z "$2" || "$2" == --* ]]; then write_error "⛔ Invalid parameter format: '$1' must be followed by a value." return 1 fi # Log the valid parameter and its value system::log_item "Valid parameter format: '$1' with value: '$2'" shift 2 ;; *) write_error "⛔ Unexpected parameter format: '$1'. Expected an option starting with '--'." return 1 ;; esac index=$((index + 1)) done # Log the successful validation of parameters system::log_item "All $index parameter pairs are valid." return 0 } system::optimize_harddrive() { # Description: # Detect mounted block devices and apply basic performance-oriented tunables # (scheduler, readahead, write cache) appropriate for HDD/SSD/NVMe. # Globals: Uses write_status/write_warning helpers. # Returns: 0 on success, 1 on failures or when not run as root. # Usage: system::optimize_harddrive # End of documentation [[ $EUID -ne 0 ]] && { write_warning "system::optimize_harddrive requires root"; return 1; } local link_target="/usr/local/bin" local dev_path dev name type rotational scheduler_path read_ahead_path desired_scheduler available target ra_kb _set_scheduler() { local sched_file=$1 blk_type=$2 local current available_sched desired sched available_sched=$(tr -d '[]' <"$sched_file" 2>/dev/null || true) case "$blk_type" in nvme) desired=(none noop "mq-deadline") ;; ssd) desired=(none noop "mq-deadline") ;; hdd) desired=("mq-deadline" bfq kyber deadline noop) ;; *) desired=() ;; esac for sched in "${desired[@]}"; do if grep -qw "$sched" <<<"$available_sched"; then echo "$sched" return fi done echo "" } for dev_path in /sys/block/*; do dev=${dev_path##*/} [[ $dev =~ ^(loop|ram|fd|sr|md|dm-|zd).* ]] && continue type="unknown" if [[ $dev == nvme* ]]; then type="nvme" rotational=0 else rotational=$(<"$dev_path/queue/rotational" 2>/dev/null || echo "") if [[ $rotational == "0" ]]; then type="ssd" elif [[ $rotational == "1" ]]; then type="hdd" fi fi scheduler_path="$dev_path/queue/scheduler" read_ahead_path="$dev_path/queue/read_ahead_kb" if [[ -w $scheduler_path ]]; then desired_scheduler=$(_set_scheduler "$scheduler_path" "$type") if [[ -n $desired_scheduler ]]; then echo "$desired_scheduler" >"$scheduler_path" && write_status "Set scheduler $desired_scheduler for /dev/$dev" else write_warning "No suitable scheduler found for /dev/$dev (type $type)" fi else write_warning "Cannot adjust scheduler for /dev/$dev (no permission or file missing)" fi case "$type" in hdd) ra_kb=512 ;; ssd) ra_kb=256 ;; nvme) ra_kb=128 ;; *) ra_kb=256 ;; esac if [[ -w $read_ahead_path ]]; then echo "$ra_kb" >"$read_ahead_path" && write_status "Set readahead ${ra_kb}KB for /dev/$dev" else write_warning "Cannot adjust readahead for /dev/$dev" fi if [[ $type != "nvme" && -x "$(command -v hdparm)" ]]; then hdparm -W1 "/dev/$dev" &>/dev/null || write_warning "Failed to enable write cache on /dev/$dev" fi done } system::optimize_startup() { # Description: # Apply lightweight boot/runtime tunables to improve responsiveness: ensure trim timer, # adjust swap/writeback sysctls, and reuse disk optimizations. # Globals: Uses write_status/write_warning. # Returns: 0 on success, 1 on failures or when not run as root. # Usage: system::optimize_startup # End of documentation [[ $EUID -ne 0 ]] && { write_warning "system::optimize_startup requires root"; return 1; } if command -v systemctl &>/dev/null; then if systemctl list-unit-files | grep -q '^fstrim.timer'; then systemctl enable --now fstrim.timer >/dev/null 2>&1 \ && write_status "Enabled fstrim.timer for periodic SSD/NVMe trimming" \ || write_warning "Could not enable fstrim.timer" fi fi sysctl -w vm.swappiness=10 >/dev/null 2>&1 && write_status "Set vm.swappiness=10" || write_warning "Could not set vm.swappiness" sysctl -w vm.dirty_background_ratio=5 >/dev/null 2>&1 && write_status "Set vm.dirty_background_ratio=5" || write_warning "Could not set vm.dirty_background_ratio" sysctl -w vm.dirty_ratio=10 >/dev/null 2>&1 && write_status "Set vm.dirty_ratio=10" || write_warning "Could not set vm.dirty_ratio" sysctl -w vm.vfs_cache_pressure=50 >/dev/null 2>&1 && write_status "Set vm.vfs_cache_pressure=50" || write_warning "Could not set vm.vfs_cache_pressure" system::optimize_harddrive } system::cleanup_and_finish() { # Description: Function to remove all temporary file locations left over from building # the new netinstall ISO etc. and clean up some variables... # go back to initial directory # Globals: # Arguments: None # Outputs: # Returns: # Usage: # End of documentation cd "$current_dir" && echo "returned to $current_dir" || echo "No directory to return to... never mind." # Cleanup folder structure... rm -r "$tmp_download_dir" rm -r "$tmp_disc_dir" rm -r "$tmp_initrd_dir" rm -r "$script_dir/custom" if [[ "$1" != "nomessage" ]]; then COMPLETION_MESSAGE=" Your ISO image is ready! It is placed in the folder: $put_iso_file_here_when_done/ Next steps: - Burn the ISO to a USB (ex: using MULTIBOOT from Pendrive Linux) - If you are using Virtual Machines select the ISO as install media - Boot from the media created: allow it to install the system - login as the temporary user (temporary password: letmein) - Adjust passwords for the encrypted volume and login... " $RTD_GUI --backtitle "RTD OEM System Builder" --title "DONE" --msgbox "$COMPLETION_MESSAGE" 15 80 else echo "Cleanup..." fi unset bin_7z unset bin_xorriso unset bin_cpio unset isohdpfx_bin unset VOLUME_TITLE unset target_iso_file_name unset _SOURCE_ISO_URL unset ssh_public_key_file unset PRESEED_TEMPLATE unset CONFIG unset PRESEED_FILE unset SRVorVDI clear return } github::clone_repo_user() { # Description: # Function to CLONE all github.com repositories for a specified user. # simply call the function and pass the username as parameter to clone # all github repositories in to the current folder. # # Globals: None # Arguments: User-name # Outputs: # Returns: a download of all repositories for the given user. # Usage: ${FUNCNAME[0]} user-name # End of documentation if [ $# -eq 0 ]; then echo "Usage: ${FUNCNAME[0]} " exit 1 fi for repo in $(curl -s https://api.github.com/users/${1}/repos?per_page=1000 | grep git_url | awk '{print $2}' | sed 's/"\(.*\)",/\1/'); do git clone --depth=1 "https${repo:3}" done } github::list_all_user_repositories() { # Description: # Function to LIST all github.com repositories for a specified user. # simply call the function and pass the username as parameter to show # all github repositories the indicated user. # # Globals: None # Arguments: User-name # Outputs: # Returns: a LIST of all repositories for the given user. # Usage: ${FUNCNAME[0]} user-name # End of documentation if [ $# -eq 0 ]; then echo "Usage: ${FUNCNAME[0]} " return 1 fi for repo in $(curl -s https://api.github.com/users/${1}/repos?per_page=1000 | grep git_url | awk '{print $2}' | sed 's/"\(.*\)",/\1/'); do echo "https${repo:3}" done } oem::rtd_reset_default_environment_config() { # Description: # Function to reset temporary configurations to their original settings as provided by the distributor. # This is critical for systems initially configured under the RTD OEM load process, which might involve # setting less secure or operationally disruptive configurations such as "auto login", "auto sudo", and # "auto running scripts". This function reverses these changes to ensure the system adheres to good security # practices and is ready for normal operations. # # Globals: # _OEM_DIR: Specifies the directory used to store backup configurations and caches during the OEM load process. # SUDO_USER: The username of the user who initiated the sudo command. This is used to identify the user's home directory for .bashrc modifications. # # Arguments: None # # Outputs: # Prints messages to stdout indicating the actions being taken, including backing up and removing specific # configuration files, and any errors or information relevant to the operation of the function. # # Returns: # 0: If all operations were successful. # 1: If an error occurred that prevented the function from completing its task. # # Usage: # This function should be run with root privileges to ensure it has the necessary permissions to modify system # files and configurations. It does not take any arguments. Before running, ensure that the _OEM_DIR global # variable is set to a valid directory path where backup files can be stored. Here is an example of how to # call this function: # # sudo rtd_oem_reset_default_environment_config # # End of documentation # Ensure _OEM_DIR is defined if [[ ! -d "${_OEM_DIR:-"/opt/rtd"}" ]]; then write_error "📁 Error: ${_OEM_DIR:-"/opt/rtd"} does not exitst: nothing to do!" return 1 fi mkdir -p "${_OEM_DIR:-"/opt/rtd"}/cache/" # Backup and remove custom sudoers file if [[ -f /etc/sudoers.d/99_sudo_include_file ]]; then mv /etc/sudoers.d/99_sudo_include_file "${_OEM_DIR:-"/opt/rtd"}/cache/" write_status "📄 Custom sudoers file has been backed up and removed." else write_error "📄 No custom sudoers file found to remove." fi # Backup the current .bashrc for SUDO_USER if [[ -n "${SUDO_USER}" && -f "/home/${SUDO_USER}/.bashrc" ]]; then cp "/home/${SUDO_USER}/.bashrc" "${_OEM_DIR:-"/opt/rtd"}/cache/bashrc" write_status "🔑 User .bashrc has been backed up." # Remove 'xhost local:root' if it exists in the real .bashrc or its symlink target file_path="/home/${SUDO_USER}/.bashrc" real_path=$(readlink -f "$file_path") # -f ensures the real path is returned even if it's not a symlink sed -i '/xhost\ local:root/d' "$real_path" write_status "📜 Removed 'xhost local:root' from .bashrc." else write_error "🚫 Error: SUDO_USER not set or .bashrc not found." fi if type system::add_or_remove_login_script &>/dev/null; then system::add_or_remove_login_script --remove /etc/xdg/autostart/oem-run.desktop write_status "🔑 Login script has been removed." else write_error "👺 system::add_or_remove_login_script function not found. Cannot remove login script." fi if type system::toggle_oem_auto_login &>/dev/null; then system::toggle_oem_auto_login --disable write_status "🔑 Auto login has been disabled." else write_error "👺 system::toggle_oem_auto_login function not found. Cannot disable auto login." fi if type system::toggle_oem_auto_elevated_privilege &>/dev/null; then system::toggle_oem_auto_elevated_privilege --disable write_status "👑 Auto elevated privilege has been disabled." else write_error "👺 system::toggle_oem_auto_elevated_privilege function not found. Cannot disable auto elevated privilege." fi if type system::set_oem_elevated_privilege_gui &>/dev/null; then system::set_oem_elevated_privilege_gui --disable write_status "👑 Elevated privilege GUI has been disabled." else write_error "👺 system::set_oem_elevated_privilege_gui function not found. Cannot disable elevated privilege GUI." fi } oem::distro_release_upgrade() { # Description: # Attempts an OS release upgrade on Ubuntu, Pop!_OS, Debian, SUSE, or Fedora. # Respects "interactive" mode to prompt user. # # Globals: # _LOGFILE, RTD_GUI, ERRMSG # # Arguments: # [interactive] — optional; pauses if unsupported distro or confirmation required # # Returns: # 0 on successful upgrade, 1 on error or unsupported distro # # Usage: # oem::distro_release_upgrade [--interactive] # oem::distro_release_upgrade --interactive # # End of documentation security::ensure_admin local interactive distro_id distro_name current_version latest_version releasever case $1 in --interactive) interactive="interactive" ;; *) interactive="" ;; esac if [[ -r /etc/os-release ]]; then distro_id=$(grep '^ID=' /etc/os-release | cut -d= -f2 | tr -d '"') distro_name=$(grep '^NAME=' /etc/os-release | cut -d= -f2 | tr -d '"') else write_error "Unable to detect Linux distribution." return 1 fi case "$distro_id" in ubuntu | pop | tuxedo | zorin) write_warning "This may take some time. Please be patient." apt clean && apt update -m dpkg --configure -a apt install -f apt dist-upgrade -y apt autoremove --purge -y if ! command -v do-release-upgrade &>/dev/null; then write_status "Installing 'do-release-upgrade' support tools..." software::add_native_package update-manager-core || return 1 fi if [[ "$interactive" == "interactive" ]]; then $RTD_GUI --title "$distro_name Upgrade" \ --yesno "Proceed with upgrading $distro_name to the next release?" 8 78 case $? in 0) do-release-upgrade | tee -a "$_LOGFILE" ;; 1 | 255) return 1 ;; esac else do-release-upgrade --quiet | tee -a "$_LOGFILE" fi ;; debian) write_warning "Debian does not support automatic upgrades between major releases." if [[ "$interactive" == "interactive" ]]; then $RTD_GUI --title "Debian Upgrade" \ --yesno "Would you like to attempt a manual upgrade to the next release?" 10 78 case $? in 0) current_version=$(grep VERSION= /etc/os-release | grep -oP '[0-9]{1,2}') sed -i "s/$(lsb_release -cs)/testing/g" /etc/apt/sources.list apt update && apt upgrade -y && apt full-upgrade -y write_status "Manual upgrade to Debian testing branch attempted." ;; *) return 1 ;; esac fi return 1 ;; suse* | opensuse*) if ! command -v zypper &>/dev/null; then write_error "Zypper not found. Cannot proceed with SUSE upgrade." return 1 fi current_version=$(grep VERSION_ID /etc/os-release | cut -d'"' -f2) latest_version=$(curl -s https://download.opensuse.org/distribution/openSUSE-current/iso/ | grep -Eo '1[5-9]\.[0-9]|20\.[0-9]+' | sort -V | tail -n1) write_status "Current SUSE version: $current_version" write_status "Latest available SUSE version: $latest_version" if [[ "$current_version" != "$latest_version" ]]; then write_warning "Upgrade to $latest_version will begin. This may take time." zypper modifyrepo --enable repo-update zypper refresh zypper --releasever="$latest_version" dup --force-resolution | tee -a "$_LOGFILE" [[ "$interactive" == "interactive" ]] && read -rp "$ERRMSG Press ENTER to reboot:" reboot else write_information "System is already running latest SUSE version." fi ;; fedora) if ! command -v dnf &>/dev/null; then write_error "DNF not found. Cannot proceed with Fedora upgrade." return 1 fi current_version=$(rpm -E %fedora) latest_version=$(curl -s https://dl.fedoraproject.org/pub/fedora/linux/releases/ | grep -Eo 'href="[0-9]+/' | grep -Eo '[0-9]+' | sort -V | tail -n1) if [[ "$interactive" == "interactive" ]]; then releasever=$($RTD_GUI --title "Fedora Upgrade" \ --inputbox "Enter Fedora version to upgrade to (Latest: $latest_version, Current: $current_version)" 10 78 "$latest_version" 3>&1 1>&2 2>&3) else releasever="$latest_version" fi if [[ ! "$releasever" =~ ^[0-9]+$ ]]; then write_error "Invalid release version input." return 1 fi dnf clean all dnf upgrade --refresh -y dnf install -y dnf-plugin-system-upgrade dnf system-upgrade download --refresh --releasever="$releasever" | tee -a "$_LOGFILE" [[ "$interactive" == "interactive" ]] && read -rp "$ERRMSG Press ENTER to reboot and begin upgrade:" dnf system-upgrade reboot ;; *) system::log_item "Unsupported distribution for automated upgrade: $distro_name" [[ "$interactive" == "interactive" ]] && read -rp "Unsupported distribution. Press [ENTER] to continue..." return 1 ;; esac return 0 } oem::remove_non_western_latin_fonts() { # Description: a simple function to remove known # non-western (latin) fonts from a distribution. Notably, Ubuntu provides too many fonts # of all kinds by default, even if the user will never need the fonts. However, the fonts # may cause unexpected font tool behavior and crashes, due to the large number of fonts # and their features. It is there fore a good Idea to remove these fonts if they are not needed. # Globals: # Arguments: None # Outputs: # Returns: # Usage: # Simply call this function by stating its name: # oem::remove_non_western_latin_fonts # # The function will not return any sucess or failure codes. It will do its best and exit. # End of documentation security::ensure_admin local font_list=$NON_WESTERN_FONTS_LIST write_status "Attempting to remove non-western fonts..." count=0 if [[ -z "$font_list" ]]; then write_warning "No non-western fonts defined for removal..." return 1 fi for i in $font_list; do software::remove_native_software_package $i && ((count++)) done write_information "Removed [ $count ] non-western Fonts" } oem::deploy_themes() { # Description: Downloads and deploys themes from GitHub. # The GitHub location is defined in the _locations file for consistency. # # Globals: # - GIT_Theme_URL: The URL of the GitHub repository to clone themes from. # - _THEME_DIR: The directory where the themes will be deployed. # - _TLA: Optional variable used to construct the default theme directory. # Arguments: # - "$1" (optional): The URL of the GitHub repository to clone themes from. Overrides GIT_Theme_URL if provided. # Outputs: # - None # Returns: # - 0 if the themes are deployed successfully, 1 if an error occurs. # # Usage: # oem::deploy_themes [https://www.github.com/myuser/myrepo.git] # # Example: # oem::deploy_themes https://www.github.com/myuser/myrepo.git # # Dependencies: # - git: To clone the repository. # - write_error: To log error messages. # - write_status: To log status messages. # # Notes: # - Ensure the GitHub URL is correct and accessible. # - Ensure the _locations file is properly configured if not passing the URL as an argument. # # End of Documentation local GIT_Theme_URL="${1:-"$GIT_Theme_URL"}" local _THEME_DIR="${_THEME_DIR:-"/opt/${_TLA:-rtd}/themes"}" if ! mkdir -p "${_THEME_DIR}"; then write_error "Failed to create directory ${_THEME_DIR}" return 1 fi if [[ -z "${GIT_Theme_URL}" ]]; then write_error "No theme URL found! It needs to be set in the _locations file or passed as an argument." return 1 fi if git clone --depth=1 "${GIT_Theme_URL}" "${_THEME_DIR}"; then write_status "Cloned ${GIT_Theme_URL} successfully." else write_error "An error occurred while cloning ${GIT_Theme_URL}." return 1 fi } oem::register_all_tools() { # Description: Function to register all OEM scripts on the current device. # It will find all the ${_TLA} powertools locations and make a link for each script to # the system /bin directory. This assures that all tools can be easily accessed from any terminal. # # # Globals: ${_MODS_DIR} # Arguments: None accepted # Outputs: # Returns: default exit status of the last command run. # Usage: oem::register_all_tools # Dependencies: _locations # # End of Documentation # Resolve the target user's home reliably instead of assuming tangarora local _target_user="${_OEM_USER:-${SUDO_USER:-${USER}}}" local _home_dir="" # Prefer passwd database; fall back to current user's $HOME if unknown user is supplied if getent passwd "${_target_user}" >/dev/null 2>&1; then _home_dir=$(getent passwd "${_target_user}" | cut -d: -f6) elif [[ "${_target_user}" == "${USER}" && -n "${HOME}" && -d "${HOME}" ]]; then _home_dir="${HOME}" else write_warning "User ${_target_user} not found; falling back to current user ${USER}" _target_user="${USER}" if [[ -n "${HOME}" && -d "${HOME}" ]]; then _home_dir="${HOME}" else _home_dir=$(getent passwd "${USER}" | cut -d: -f6) fi fi if [[ -z "${_home_dir}" || ! -d "${_home_dir}" ]]; then write_error "Cannot determine home directory for user ${_target_user}" return 1 fi _bashrc="${_home_dir}/.bashrc" [[ -f "${_bashrc}" ]] || touch "${_bashrc}" sed -i s/'# session optional pam_xauth.so'/'session optional pam_xauth.so'/g /etc/pam.d/sudo if ! grep "xhost local:root" "${_bashrc}"; then echo 'xhost local:root >/dev/null' >>"${_bashrc}"; fi if [[ -z "${_MODS_DIR}" || ! -d "${_MODS_DIR}" ]]; then write_error "Location variables not set or modules dir missing!" return 1 fi local link_target="/usr/local/bin" if [[ ! -d $link_target || ! -w $link_target ]]; then link_target="/bin" fi shopt -s nullglob for d in "${_MODS_DIR}"/*/; do write_status "Creating links to scripts in $d" for f in "$d"/${_TLA,,}*; do write_status "Processing: ${f}" if [[ ! -f "${f}" ]]; then continue fi if [[ ! -x "${f}" ]]; then write_status "File ${f} is not executable: setting executable bit" if chmod +x "${f}" ; then write_status "Set executable bit for ${f} successfully" else write_error "Failed to set executable bit for ${f}" fi fi local dest="${link_target}/$(basename "$f")" local target target=$(readlink -f "${dest}" 2>/dev/null || true) if [[ "${target}" == "$(readlink -f "$f")" ]]; then write_status "Link for $BYellow ${f} $Green already points to target $BCyan (${dest})" else write_status "Linking app $BYellow ${f} $Green so its available in the terminal..." ln -sf "${f}" "${dest}" fi if [[ "${link_target}" != "/bin" ]]; then local legacy="/bin/$(basename "$f")" if [[ -L "${legacy}" ]]; then local legacy_target legacy_target=$(readlink -f "${legacy}" 2>/dev/null || true) if [[ "${legacy_target}" == "$(readlink -f "$f")" ]]; then write_status "Removing legacy link $BRed ${legacy} " rm -f "${legacy}" fi fi fi done done shopt -u nullglob write_status "Creating links to ${_OEM_DIR:-"/opt/rtd"}/core/*.sh" for f in "${_OEM_DIR:-"/opt/rtd"}"/core/*.sh; do [[ -e $f ]] || continue chmod +x "$f" || write_warning "Could not set executable on $f" dest="${link_target}/$(basename "$f")" target=$(readlink -f "${dest}" 2>/dev/null || true) if [[ "${target}" == "$(readlink -f "$f")" ]]; then continue fi ln -sf "$f" "$dest" if [[ "${link_target}" != "/bin" ]]; then legacy="/bin/$(basename "$f")" if [[ -L "${legacy}" ]]; then legacy_target=$(readlink -f "${legacy}" 2>/dev/null || true) if [[ "${legacy_target}" == "$(readlink -f "$f")" ]]; then rm -f "${legacy}" fi fi fi done return } oem::rtd_tools_make_launchers() { # Description: # Legacy entry point that now delegates to the launcher definitions stored in _locations.info. # Globals: None (delegated function performs checks) # Arguments: None # Outputs: Whatever the delegated function prints. # Returns: Exit status of oem::rtd_tools_make_launchers_from_locations. # Usage: oem::rtd_tools_make_launchers # End of documentation oem::rtd_tools_make_launchers_from_locations } oem::create_single_launcher() { # Description: Function to create a single .desktop file for an application. # This function is used to create a launcher for the RTD tools. # It takes various parameters to customize the launcher. # Globals: # - _MODS_DIR: The base directory where the modules are located. # Arguments: # 1: Filename base (e.g., "rtd", "rtd-user-bakup") # 2: Name field (e.g., "RTD", "RTD-Backup") # 3: Comment field # 4: Module directory name (e.g., "simple-support-tool.mod") # 5: Executable script name (relative to module dir) # 6: Icon path (relative to module dir) # 7: Terminal bool ("true" or "false") # Outputs: # - Creates a .desktop file in /usr/share/applications/ # - Sets the appropriate permissions on the .desktop file # Returns: # - 0 on success # - 1 on failure # Usage: oem::create_single_launcher # End of documentation local -r file_base="$1" local -r name="$2" local -r comment="$3" local -r mod_dir_name="$4" local -r script_name="$5" local -r icon_path="$6" local -r terminal_bool="$7" local -r output_dir="/usr/share/applications" local output_file="${output_dir}/${file_base}.desktop" local exec_path="${_MODS_DIR}/${mod_dir_name}/${script_name}" local module_path="${_MODS_DIR}/${mod_dir_name}" local icon_full_path="${_MODS_DIR}/${mod_dir_name}/${icon_path}" # Basic validation of inputs if [[ ! -f "${exec_path}" ]]; then write_warning "File not found: ${exec_path} for ${file_base}.desktop" fi if [[ ! -x "${exec_path}" ]]; then write_warning "Executable file is not executable: ${exec_path} for ${file_base}.desktop" write_status "Setting executable permission on ${exec_path}" chmod +x "${exec_path}" || { write_error "Failed to set executable permission on ${exec_path}" ; } fi if [[ ! -f "${icon_full_path}" ]]; then write_warning "Icon file not found: ${icon_full_path} for ${file_base}.desktop" write_status "Setting default icon to ${_MODS_DIR}/../media_files/sys0.ico" icon_full_path="${_MODS_DIR}/../media_files/sys0.ico" fi # Use printf for potentially safer handling than cat << EOF if variables might contain surprises, # but cat is often more readable for simple heredocs. Sticking with cat for now. # Ensure the directory exists and is writable (checked in the main function) cat >"${output_file}" <<-EOF || { write_error "Failed to create ${output_file}"; return 1; } [Desktop Entry] Version=1.0 Name=${name} Comment=${comment} Exec=${exec_path} Path=${module_path} Icon=${icon_full_path} Terminal=${terminal_bool} Type=Application Categories=OEM;RTD; # Added RTD Category for potential grouping EOF chmod +x "${output_file}" || { write_error "Failed to set permissions on ${output_file}"; return 1; } return 0 } oem::rtd_tools_make_launchers_from_locations() { # Description: # Function that creates RTD launchers based on the definitions stored in _locations.info (LAUNCHER_DATA). # Globals: # _MODS_DIR - Root path of RTD modules referenced by launcher definitions. # LAUNCHER_DATA - Key/value launcher specification string from _locations.info. # Arguments: None accepted # Outputs: Launchers created in /usr/share/applications/ # Returns: Default exit status of the last command run. # Usage: oem::rtd_tools_make_launchers_from_locations # End of documentation security::ensure_admin local -r output_dir="/usr/share/applications" if [[ ! -d "${output_dir}" ]]; then write_error "Target directory does not exist: ${output_dir}" return 1 fi if [[ ! -w "${output_dir}" ]]; then write_error "Target directory is not writable: ${output_dir}" return 1 fi # Check if _MODS_DIR is set and is a directory if [[ -z "${_MODS_DIR}" || ! -d "${_MODS_DIR}" ]]; then write_error "_MODS_DIR variable is not set or not a valid directory." return 1 fi # Use $_TLA if defined, otherwise default text local tla_name="${_TLA:-RTD}" write_information "Creating ${tla_name} launchers in ${output_dir}..." if [[ -z "${LAUNCHER_DATA:-}" ]]; then write_error "LAUNCHER_DATA variable is empty. Please define launcher entries in _locations.info." return 1 fi local file_base="" name="" comment="" mod_dir_name="" script_name="" icon_path="" terminal_bool="" local all_successful=true # Track overall success __oem_launcher_flush_entry() { if [[ -z "${file_base}" ]]; then return fi local _terminal="${terminal_bool:-true}" if [[ -z "${name}" || -z "${mod_dir_name}" || -z "${script_name}" ]]; then write_error "Launcher '${file_base}' is missing required fields (name/module_dir/script); skipping." all_successful=false else if ! oem::create_single_launcher "${file_base}" "${name}" "${comment:-${name}}" "${mod_dir_name}" "${script_name}" "${icon_path:-Media_files/sys0.ico}" "${_terminal}"; then all_successful=false fi fi file_base=""; name=""; comment=""; mod_dir_name=""; script_name=""; icon_path=""; terminal_bool="" } # Process the data using process substitution for reliability with IFS while IFS= read -r line || [[ -n "$line" ]]; do line="${line%$'\r'}" line="${line#"${line%%[![:space:]]*}"}" line="${line%"${line##*[![:space:]]}"}" if [[ -z "${line}" ]]; then __oem_launcher_flush_entry continue fi [[ "${line:0:1}" == "#" ]] && continue if [[ "${line}" != *"="* ]]; then write_warning "Ignoring malformed launcher line: ${line}" continue fi key="${line%%=*}" value="${line#*=}" key="${key#"${key%%[![:space:]]*}"}" key="${key%"${key##*[![:space:]]}"}" value="${value#"${value%%[![:space:]]*}"}" value="${value%"${value##*[![:space:]]}"}" # Remove optional surrounding quotes if [[ "${value}" == \"*\" && "${value}" == *\" ]]; then value="${value:1:-1}" elif [[ "${value}" == \'*\' && "${value}" == *\' ]]; then value="${value:1:-1}" fi case "${key,,}" in file_base|file) file_base="${value}" ;; name) name="${value}" ;; comment|description) comment="${value}" ;; module_dir|module|module_directory) mod_dir_name="${value}" ;; script|script_name|exec) script_name="${value}" ;; icon|icon_path) icon_path="${value}" ;; terminal|terminal_bool|terminal_required) terminal_bool="${value,,}" ;; *) write_warning "Unknown launcher key '${key}' ignored." ;; esac done <<< "${LAUNCHER_DATA}" # Flush any remaining entry __oem_launcher_flush_entry unset -f __oem_launcher_flush_entry if [[ "$all_successful" == "true" ]]; then write_information "${tla_name} launchers created successfully." return 0 else write_error "One or more ${tla_name} launchers failed to create. Please check logs." return 1 # Indicate partial or total failure fi } oem::check_boot_splash_screen_enable() { # Description: OEM function to enable splash screen on boot if desktop is indicated in # a configuration file. This file could be an AutoYast.xml, or ks.cfg, or a preseed.cfg. # # Globals: # Arguments: [FILE] # Outputs: # Returns: error level # Usage: oem::check_boot_splash_screen_enable /root/original-ks.cfg # End of documentation if [[ -n "$1" ]]; then if [[ -r "$1" ]]; then if cat $1 | grep -i "desktop"; then software::check_native_package_dependency splash echo 'GRUB_CMDLINE_LINUX_DEFAULT="quiet splash"' >>/etc/default/grub update-grub fi fi fi } oem::setup_brand_splash_screen() { # Function: oem::setup_brand_splash_screen # Description: # Brands the installer splash screen for a customized installer downloaded from a given vendor. # It overlays specified text onto an image file, which is typically used in bootable ISO images. # # Parameters: # $1 (optional): Full path to the image file to be branded. # $2 (optional): Quoted text to overlay on the image. # # Usage: # oem::setup_brand_splash_screen [image_file_to_brand] [text_to_brand_with] # # Examples: # oem::setup_brand_splash_screen # oem::setup_brand_splash_screen /tmp/splash.png "Welcome to the Custom Installer!" # # Note: # - If parameters are not provided, default values are used. # - Requires ImageMagick to be installed. # Validate input parameters using a custom function. # This function should check if required parameters are provided. system::validate_parameters "${@}" # Initialize variables for image file and branding text. image_file_to_brand="${1:-}" text_to_brand_with="${2:-}" # Parse command-line arguments for additional options. while [[ $# -gt 0 ]]; do case $1 in --target-iso) # Set the target ISO file path. target_iso="$2" shift 2 ;; --iso-source-dir) # Set the source directory for the ISO files. iso_source_dir="$2" shift 2 ;; *) # Handle unknown options. write_error "Unknown option: $1" shift ;; esac done # Log the action of ensuring ImageMagick is installed. system::log_item "🔯 Ensure that ImageMagick is installed so images can be manipulated." # Check if ImageMagick is installed; required for image manipulation. software::check_native_package_dependency imagemagick # Determine the default image file to brand if not provided. # Uses splash.png from common directories if no image is specified. : "${image_file_to_brand:="$( if [ -f "$tmp_disc_dir/splash.png" ]; then echo "$tmp_disc_dir/splash.png" elif [ -f "$tmp_disc_dir/isolinux/splash.png" ]; then echo "$tmp_disc_dir/isolinux/splash.png" fi )"}" # Set the default text to brand with if not provided. : "${text_to_brand_with:="RunTime Data OEM Install: $PREFERENCE"}" # Log the image file found and the text to be used for branding. write_status "Found image: $image_file_to_brand" write_status "Branding with text: $text_to_brand_with" # Check if the image file exists before proceeding. if [ -f "$image_file_to_brand" ]; then # Use ImageMagick's convert command to overlay text onto the image. # Input image file. # Position text at the top center. # Apply a semi-transparent black stroke to the text. # Annotate image with stroked text. # Remove stroke, set text fill color to white. # Annotate image with filled text. # Output the edited image to a temporary file. convert "$image_file_to_brand" \ -gravity north \ -stroke '#000C' -strokewidth 3 \ -annotate 0 "$text_to_brand_with" \ -stroke none -strokewidth 3 -fill white \ -annotate 0 "$text_to_brand_with" \ edit-out.png # Remove the original image file. rm "$image_file_to_brand" # Rename the edited image to the original image file name. mv edit-out.png "$image_file_to_brand" || write_error "Image [ $image_file_to_brand ] NOT branded!" else # Log an error if the image file does not exist. write_error "Image file $image_file_to_brand not found!" fi # Clean up by unsetting temporary variables. unset image_file_to_brand unset text_to_brand_with } gnome::set_tilix_ui_tweaks_for_user() { # Description: Configure Tilix and GNOME Terminal with OEM defaults and import # prebuilt profiles via dconf. # Globals: # - HOME: Used to write Tilix settings under ${HOME}/.config/rtd. # Arguments: # - None # Outputs: # - Status/error logs. # - Writes ${HOME}/.config/rtd/gconf-tilix-settings.ini and imports it with dconf. # Returns: # - 0 when configuration commands run (best-effort); 1 if required tools are missing. # Usage: gnome::set_tilix_ui_tweaks_for_user # End of documentation if ! command -v gsettings >/dev/null 2>&1; then write_error "gsettings command not available; cannot configure Tilix." return 1 fi if ! command -v dconf >/dev/null 2>&1; then write_status "Installing dconf-cli so Tilix settings can be imported..." software::check_native_package_dependency dconf-cli || { write_error "Failed to install dconf-cli; aborting Tilix tweaks." return 1 } fi if ! command -v tilix >/dev/null 2>&1; then write_error "Unable to ensure Tilix is installed." return 1 fi if ! fc-list >/dev/null 2>&1; then write_status "📦 Ensuring fontconfig utilities are present..." software::check_native_package_dependency fontconfig || write_warning "fontconfig not available; skipping font detection." fi write_status "📐 Setting Tilix preferences..." gsettings set com.gexperts.Tilix.Settings theme-variant 'dark' || write_warning "Failed to set Tilix theme." gsettings set com.gexperts.Tilix.Settings font 'SauceCodePro Nerd Font 12' || write_warning "Failed to set Tilix font." gsettings set com.gexperts.Tilix.Settings use-system-font false || true gsettings set com.gexperts.Tilix.Settings bold-is-bright true || true # Terminal and Tilix Dark Theme local GNOME_TERMINAL_PROFILE="" GNOME_TERMINAL_PROFILE=$(gsettings get org.gnome.Terminal.ProfilesList default 2>/dev/null | awk -F"'" 'NR==1 {print $2}') if [[ -z "${GNOME_TERMINAL_PROFILE}" ]]; then write_warning "Unable to determine default GNOME Terminal profile; skipping GNOME Terminal tweaks." else local _profile_path="org.gnome.Terminal.Legacy.Profile:/org/gnome/terminal/legacy/profiles:/:${GNOME_TERMINAL_PROFILE}/" gsettings set "${_profile_path}" use-transparent-background true || true gsettings set "${_profile_path}" background-transparency-percent 20 || true gsettings set "${_profile_path}" default-size-columns 100 || true gsettings set "${_profile_path}" use-theme-transparency false || true gsettings set "${_profile_path}" scrollback-lines 10000 || true local _preferred_font="Monospace 12" if command -v fc-list >/dev/null 2>&1 && fc-list | grep -qi "SauceCodePro"; then _preferred_font='SauceCodePro Nerd Font 12' fi gsettings set "${_profile_path}" font "${_preferred_font}" || true gsettings set "${_profile_path}" use-system-font false || true gsettings set "${_profile_path}" bold-is-bright true || true fi # Add Tilix configuration and shell definitions local _tilix_config_dir="${HOME}/.config/rtd" mkdir -p "${_tilix_config_dir}" write_status "📝 Writing Tilix profile definitions to ${_tilix_config_dir}/gconf-tilix-settings.ini" cat >"${_tilix_config_dir}/gconf-tilix-settings.ini" <<-'EOF_TLX' [com/gexperts/Tilix] quake-specific-monitor=0 quake-width-percent=90 terminal-title-style='small' theme-variant='dark' warn-vte-config-issue=false [com/gexperts/Tilix/profiles] list=['2b7c4080-0ddd-46c5-8f23-563fd3ba789d', 'd6401d4b-4b26-42ec-918a-2e7dc977118d', '8000e9d6-6f21-4a4e-a122-ac45607b56f5', '5283b4cf-faa5-4aef-afcd-a29fd5e0335a', '75b21a4c-150c-4f7a-a093-9faaa19626e2', '1824e1f9-3b7e-48d2-b06a-709239d1d6d9'] [com/gexperts/Tilix/profiles/1824e1f9-3b7e-48d2-b06a-709239d1d6d9] background-color='#272822' badge-color-set=false bold-color-set=false cursor-colors-set=false foreground-color='#F8F8F2' highlight-colors-set=false palette=['#272822', '#F92672', '#A6E22E', '#F4BF75', '#66D9EF', '#AE81FF', '#A1EFE4', '#F8F8F2', '#75715E', '#F92672', '#A6E22E', '#F4BF75', '#66D9EF', '#AE81FF', '#A1EFE4', '#F9F8F5'] use-theme-colors=false visible-name='Monokai' [com/gexperts/Tilix/profiles/2b7c4080-0ddd-46c5-8f23-563fd3ba789d] background-color='#000000000000' background-transparency-percent=10 badge-color='#AC7EA8' badge-color-set=true bold-color-set=false cursor-colors-set=false default-size-columns=180 default-size-rows=40 dim-transparency-percent=0 foreground-color='#EFEFEF' highlight-colors-set=false palette=['#000000', '#AA0000', '#00AA00', '#AA5400', '#0000AA', '#AA00AA', '#00AAAA', '#AAAAAA', '#545454', '#FF5454', '#54FF54', '#FFFF54', '#5454FF', '#FF54FF', '#54FFFF', '#FFFFFF'] terminal-title='${id}: ${title}${process}' use-system-font=false use-theme-colors=false visible-name='Linux' [com/gexperts/Tilix/profiles/5283b4cf-faa5-4aef-afcd-a29fd5e0335a] background-color='#FDF6E3' badge-color-set=false bold-color-set=false cursor-colors-set=false foreground-color='#657B83' highlight-colors-set=false palette=['#073642', '#DC322F', '#859900', '#B58900', '#268BD2', '#D33682', '#2AA198', '#EEE8D5', '#002B36', '#CB4B16', '#586E75', '#657B83', '#839496', '#6C71C4', '#93A1A1', '#FDF6E3'] use-theme-colors=false visible-name='Sun Microsystems' [com/gexperts/Tilix/profiles/75b21a4c-150c-4f7a-a093-9faaa19626e2] background-color='#002B36' badge-color-set=false bold-color-set=false cursor-colors-set=false foreground-color='#839496' highlight-colors-set=false palette=['#073642', '#DC322F', '#859900', '#B58900', '#268BD2', '#D33682', '#2AA198', '#EEE8D5', '#002B36', '#CB4B16', '#586E75', '#657B83', '#839496', '#6C71C4', '#93A1A1', '#FDF6E3'] use-theme-colors=false visible-name='Solarized' [com/gexperts/Tilix/profiles/8000e9d6-6f21-4a4e-a122-ac45607b56f5] background-color='#1E1E1E' badge-color-set=false bold-color-set=false cursor-colors-set=false foreground-color='#A7A7A7' highlight-colors-set=false palette=['#1E1E1E', '#CF6A4C', '#8F9D6A', '#F9EE98', '#7587A6', '#9B859D', '#AFC4DB', '#A7A7A7', '#5F5A60', '#CF6A4C', '#8F9D6A', '#F9EE98', '#7587A6', '#9B859D', '#AFC4DB', '#FFFFFF'] use-theme-colors=false visible-name='Twilight' [com/gexperts/Tilix/profiles/d6401d4b-4b26-42ec-918a-2e7dc977118d] palette=['#000000', '#CC0000', '#4D9A05', '#C3A000', '#3464A3', '#754F7B', '#05979A', '#D3D6CF', '#545652', '#EF2828', '#89E234', '#FBE84F', '#729ECF', '#AC7EA8', '#34E2E2', '#EDEDEB'] use-theme-colors=true visible-name='Tango' EOF_TLX if dconf load / <"${_tilix_config_dir}/gconf-tilix-settings.ini"; then write_information "Tilix settings imported via dconf." else write_warning "Failed to import Tilix settings with dconf." fi } gnome::set_power_configuraton_for_user() { # Description: Apply OEM power and interface defaults (sleep, tracker indexer, # battery percentage, clock details, hot corners). # Globals: # - None # Arguments: # - None # Outputs: # - Status logs as gsettings values are applied. # Returns: # - 0 on success. # Usage: gnome::set_power_configuraton_for_user # End of documentation write_status "📐 Setting common power configuration..." gsettings set org.gnome.settings-daemon.plugins.power sleep-inactive-ac-timeout 0 gsettings set org.gnome.settings-daemon.plugins.power sleep-inactive-ac-type 'nothing' gsettings set org.gnome.settings-daemon.plugins.power sleep-inactive-battery-timeout 0 gsettings set org.gnome.settings-daemon.plugins.power sleep-inactive-battery-type 'nothing' # This indexer is nice, but can be detrimental for laptop users battery life gsettings set org.freedesktop.Tracker.Miner.Files index-on-battery false gsettings set org.freedesktop.Tracker.Miner.Files index-on-battery-first-time false gsettings set org.freedesktop.Tracker.Miner.Files throttle 15 write_status "Setting common GNOME UI tweaks..." gsettings set org.gnome.desktop.interface show-battery-percentage true gsettings set org.gnome.desktop.interface clock-show-date true gsettings set org.gnome.desktop.interface clock-show-seconds true gsettings set org.gnome.desktop.interface enable-hot-corners true } gnome::set_better_font_smoothing_for_user() { # Description: Tune GNOME font smoothing (hinting, antialiasing, subpixel order) # for OEM defaults. # Globals: # - None # Arguments: # - None # Outputs: # - Status logs while applying gsettings changes. # Returns: # - 0 on success. # Usage: gnome::set_better_font_smoothing_for_user # End of documentation write_status "📐 Setting common font smoothing..." gsettings set org.gnome.settings-daemon.plugins.xsettings hinting 'slight' gsettings set org.gnome.settings-daemon.plugins.xsettings antialiasing 'rgba' gsettings set org.gnome.settings-daemon.plugins.xsettings rgba-order 'rgb' } gnome::set_better_usability_for_user() { # Description: Apply general GNOME usability tweaks (mouse acceleration, volume, # calendar, window controls, workspace behavior). # Globals: # - None # Arguments: # - None # Outputs: # - Status/error logs via gsettings. # Returns: # - 0 on success. # Usage: gnome::set_better_usability_for_user # End of documentation gsettings set org.gnome.desktop.peripherals.mouse accel-profile 'adaptive' gsettings set org.gnome.desktop.sound allow-volume-above-100-percent false gsettings set org.gnome.desktop.calendar show-weekdate true gsettings set org.gnome.desktop.wm.preferences resize-with-right-button true gsettings set org.gnome.desktop.wm.preferences button-layout 'appmenu:minimize,maximize,close' gsettings set org.gnome.shell.overrides workspaces-only-on-primary false } gnome::configure_dash_to_dock() { # Description: Configure Dash-to-Dock with OEM defaults (layout, indicators, # isolation, size, transparency). # Globals: # - None # Arguments: # - None # Outputs: # - Writes GNOME Shell extension settings via gsettings. # Returns: # - 0 on success. # Usage: gnome::configure_dash_to_dock # End of documentation gsettings set org.gnome.shell.extensions.dash-to-dock click-action 'minimize' gsettings set org.gnome.shell.extensions.dash-to-dock dock-position 'BOTTOM' gsettings set org.gnome.shell.extensions.dash-to-dock apply-custom-theme false gsettings set org.gnome.shell.extensions.dash-to-dock custom-background-color false gsettings set org.gnome.shell.extensions.dash-to-dock custom-theme-customize-running-dots true gsettings set org.gnome.shell.extensions.dash-to-dock custom-theme-running-dots-color '#729fcf' gsettings set org.gnome.shell.extensions.dash-to-dock custom-theme-shrink true gsettings set org.gnome.shell.extensions.dash-to-dock dock-fixed false gsettings set org.gnome.shell.extensions.dash-to-dock extend-height true gsettings set org.gnome.shell.extensions.dash-to-dock force-straight-corner false gsettings set org.gnome.shell.extensions.dash-to-dock icon-size-fixed true gsettings set org.gnome.shell.extensions.dash-to-dock intellihide-mode 'ALL_WINDOWS' gsettings set org.gnome.shell.extensions.dash-to-dock isolate-workspaces true gsettings set org.gnome.shell.extensions.dash-to-dock show-apps-at-top true gsettings set org.gnome.shell.extensions.dash-to-dock unity-backlit-items false gsettings set org.gnome.shell.extensions.dash-to-dock transparency-mode 'FIXED' gsettings set org.gnome.shell.extensions.dash-to-dock running-indicator-style 'SEGMENTED' gsettings set org.gnome.shell.extensions.dash-to-dock background-opacity 0.70000000000000000 gsettings set org.gnome.shell.extensions.dash-to-dock extend-height false } gnome::configure_dash_to_panel() { # Description: Configure Dash-to-Panel with OEM defaults including icon sizing # and a full dconf profile. # Globals: # - None # Arguments: # - None # Outputs: # - Writes GNOME Shell extension settings and loads a dconf dump. # Returns: # - 0 on success. # Usage: gnome::configure_dash_to_panel # End of documentation gsettings set org.gnome.shell.extensions.dash-to-panel click-action 'minimize' gsettings set org.gnome.shell.extensions.dash-to-panel dash-max-icon-size 32 # Configure dash-to-panel temporary_script=$(mktemp) chmod 777 ${temporary_script} cat >${temporary_script} <<-'EOF_D2P' [org/gnome/shell/extensions/dash-to-panel] appicon-margin=4 appicon-padding=4 available-monitors=[0] dot-color-1='#5294e2' dot-color-2='#5294e2' dot-color-3='#5294e2' dot-color-4='#5294e2' dot-color-dominant=false dot-color-override=true dot-color-unfocused-1='#5294e2' dot-color-unfocused-2='#5294e2' dot-color-unfocused-3='#5294e2' dot-color-unfocused-4='#5294e2' dot-color-unfocused-different=false dot-style-focused='METRO' dot-style-unfocused='SEGMENTED' focus-highlight-color='#eeeeee' focus-highlight-dominant=true force-check-update=true group-apps=true hotkeys-overlay-combo='TEMPORARILY' isolate-monitors=false isolate-workspaces=false panel-element-positions='{"0":[{"element":"showAppsButton","visible":true,"position":"stackedTL"},{"element":"activitiesButton","visible":false,"position":"stackedTL"},{"element":"leftBox","visible":true,"position":"stackedTL"},{"element":"taskbar","visible":true,"position":"stackedTL"},{"element":"centerBox","visible":true,"position":"stackedBR"},{"element":"rightBox","visible":true,"position":"stackedBR"},{"element":"dateMenu","visible":true,"position":"stackedBR"},{"element":"systemMenu","visible":true,"position":"stackedBR"},{"element":"desktopButton","visible":false,"position":"stackedBR"}]}' secondarymenu-contains-showdetails=true show-appmenu=false show-favorites=true show-favorites-all-monitors=false stockgs-keep-dash=false stockgs-keep-top-panel=false trans-use-custom-bg=false trans-use-custom-opacity=true tray-size=0 EOF_D2P dconf load / <${temporary_script} && rm ${temporary_script} } gnome::configure_nautilus() { # Description: Apply Nautilus usability defaults (zoom level, executable prompts, # tree view, directory sorting). # Globals: # - None # Arguments: # - None # Outputs: # - Applies gsettings values for Nautilus and GTK file chooser. # Returns: # - 0 on success. # Usage: gnome::configure_nautilus # End of documentation gsettings set org.gnome.nautilus.icon-view default-zoom-level 'standard' gsettings set org.gnome.nautilus.preferences executable-text-activation 'ask' gsettings set org.gtk.Settings.FileChooser sort-directories-first true gsettings set org.gnome.nautilus.list-view use-tree-view true gsettings set org.gnome.nautilus.list-view default-zoom-level 'small' } gnome::organize_overlay_menu() { # Description: Organize the GNOME application grid into OEM categories following # the FreeDesktop specification. # Globals: # - None # Arguments: # - None # Outputs: # - Applies gsettings folder definitions for app folders. # Returns: # - 0 on success. # Usage: gnome::organize_overlay_menu # End of documentation gsettings set org.gnome.desktop.app-folders folder-children "['accessories', 'chrome-apps', 'games', 'graphics', 'internet', 'office', 'programming', 'science', 'sound---video', 'system-tools', 'universal-access', 'wine', 'OEM', '3D-Printing', 'Cloud']" gsettings set org.gnome.desktop.app-folders.folder:/org/gnome/desktop/app-folders/folders/accessories/ name "Accessories" gsettings set org.gnome.desktop.app-folders.folder:/org/gnome/desktop/app-folders/folders/accessories/ categories "['Utility']" gsettings set org.gnome.desktop.app-folders.folder:/org/gnome/desktop/app-folders/folders/chrome-apps/ name "Chrome Apps" gsettings set org.gnome.desktop.app-folders.folder:/org/gnome/desktop/app-folders/folders/chrome-apps/ categories "['chrome-apps']" gsettings set org.gnome.desktop.app-folders.folder:/org/gnome/desktop/app-folders/folders/games/ name "Games" gsettings set org.gnome.desktop.app-folders.folder:/org/gnome/desktop/app-folders/folders/games/ categories "['Game']" gsettings set org.gnome.desktop.app-folders.folder:/org/gnome/desktop/app-folders/folders/graphics/ name "Graphics" gsettings set org.gnome.desktop.app-folders.folder:/org/gnome/desktop/app-folders/folders/graphics/ categories "['Graphics']" gsettings set org.gnome.desktop.app-folders.folder:/org/gnome/desktop/app-folders/folders/internet/ name "Internet" gsettings set org.gnome.desktop.app-folders.folder:/org/gnome/desktop/app-folders/folders/internet/ categories "['Network', 'WebBrowser', 'Email']" gsettings set org.gnome.desktop.app-folders.folder:/org/gnome/desktop/app-folders/folders/office/ name "Office" gsettings set org.gnome.desktop.app-folders.folder:/org/gnome/desktop/app-folders/folders/office/ categories "['Office']" gsettings set org.gnome.desktop.app-folders.folder:/org/gnome/desktop/app-folders/folders/programming/ name "Programming" gsettings set org.gnome.desktop.app-folders.folder:/org/gnome/desktop/app-folders/folders/programming/ categories "['Development']" gsettings set org.gnome.desktop.app-folders.folder:/org/gnome/desktop/app-folders/folders/science/ name "Science" gsettings set org.gnome.desktop.app-folders.folder:/org/gnome/desktop/app-folders/folders/science/ categories "['Science']" gsettings set org.gnome.desktop.app-folders.folder:/org/gnome/desktop/app-folders/folders/sound---video/ name "Sound & Video" gsettings set org.gnome.desktop.app-folders.folder:/org/gnome/desktop/app-folders/folders/sound---video/ categories "['AudioVideo', 'Audio', 'Video']" gsettings set org.gnome.desktop.app-folders.folder:/org/gnome/desktop/app-folders/folders/system-tools/ name "System Tools" gsettings set org.gnome.desktop.app-folders.folder:/org/gnome/desktop/app-folders/folders/system-tools/ categories "['System', 'Settings']" gsettings set org.gnome.desktop.app-folders.folder:/org/gnome/desktop/app-folders/folders/universal-access/ name "Universal Access" gsettings set org.gnome.desktop.app-folders.folder:/org/gnome/desktop/app-folders/folders/universal-access/ categories "['Accessibility']" gsettings set org.gnome.desktop.app-folders.folder:/org/gnome/desktop/app-folders/folders/wine/ name "Wine" gsettings set org.gnome.desktop.app-folders.folder:/org/gnome/desktop/app-folders/folders/wine/ categories "['Wine', 'X-Wine', 'Wine-Programs-Accessories']" gsettings set org.gnome.desktop.app-folders.folder:/org/gnome/desktop/app-folders/folders/OEM/ name "OEM" gsettings set org.gnome.desktop.app-folders.folder:/org/gnome/desktop/app-folders/folders/OEM/ categories "['OEM']" gsettings set org.gnome.desktop.app-folders.folder:/org/gnome/desktop/app-folders/folders/3D-Printing/ name "3D-Printing" gsettings set org.gnome.desktop.app-folders.folder:/org/gnome/desktop/app-folders/folders/3D-Printing/ categories "['3D-Printing', 'CAD', '3D', '3DGraphics']" gsettings set org.gnome.desktop.app-folders.folder:/org/gnome/desktop/app-folders/folders/Cloud/ name "Cloud" gsettings set org.gnome.desktop.app-folders.folder:/org/gnome/desktop/app-folders/folders/Cloud/ categories "['Cloud-Management', 'Cloud', 'aws', 'azure', 'gcp']" } gnome::_apply_gtk4_theme() { # Description: Write a GTK4 settings.ini with the requested theme/icon and dark # preference so GTK4 apps match the shell theme. # Globals: # - XDG_CONFIG_HOME: Used to resolve the GTK4 config directory. # - HOME: Fallback location for .config when XDG_CONFIG_HOME is unset. # Arguments: # 1: GTK theme name (e.g., "WhiteSur-Dark"). # 2: Icon theme name (e.g., "WhiteSur-dark"). # 3: Dark preference flag (1 to prefer dark, 0 otherwise). # Outputs: # - Writes ${XDG_CONFIG_HOME:-${HOME}/.config}/gtk-4.0/settings.ini # Returns: # - 0 on success. # Usage: gnome::_apply_gtk4_theme # End of documentation local theme="$1" icon="$2" prefer_dark="${3:-0}" local gtk4_dir="${XDG_CONFIG_HOME:-${HOME}/.config}/gtk-4.0" local gtk4_file="${gtk4_dir}/settings.ini" mkdir -p "${gtk4_dir}" cat >"${gtk4_file}" <<-EOF [Settings] gtk-theme-name=${theme} gtk-icon-theme-name=${icon} gtk-application-prefer-dark-theme=${prefer_dark} EOF } gnome::_reset_ui_theme_settings() { # Description: Reset GNOME theme-related settings and remove GTK4 overrides to # allow a new theme to apply cleanly. # Globals: # - XDG_CONFIG_HOME: Used to locate GTK4 config directory. # - HOME: Fallback location for .config when XDG_CONFIG_HOME is unset. # Arguments: # - None # Outputs: # - Clears GNOME theme gsettings and deletes GTK4 theme cache/overrides. # Returns: # - 0 on success. # Usage: gnome::_reset_ui_theme_settings # End of documentation gsettings reset org.gnome.desktop.interface gtk-theme || true gsettings reset org.gnome.desktop.interface icon-theme || true gsettings reset org.gnome.desktop.interface color-scheme || true gsettings reset org.gnome.shell.extensions.user-theme name || true # Clean up GTK4 overrides so next theme writes a fresh settings.ini. # Removing the entire gtk-4.0 dir also clears stray gtk.css/asset caches that can pin window buttons. local gtk4_dir="${XDG_CONFIG_HOME:-${HOME}/.config}/gtk-4.0" if [[ -d "${gtk4_dir}" ]]; then rm -rf "${gtk4_dir}" fi } gnome::ensure_window_buttons_visible() { # Description: Force-enable window controls (minimize/maximize/close) and allow # custom ordering through the button-layout string. # Globals: # - None # Arguments: # 1: Optional button layout string (default: appmenu:minimize,maximize,close). # Outputs: # - Writes GNOME window manager preferences via gsettings. # Returns: # - 0 on success. # Usage: gnome::ensure_window_buttons_visible [] # End of documentation local layout="${1:-appmenu:minimize,maximize,close}" gsettings set org.gnome.desktop.wm.preferences button-layout "${layout}" } gnome::_reload_shell_theme() { # Description: Request a GNOME Shell reload so theme changes take effect. No-op # when busctl is unavailable. # Globals: # - None # Arguments: # - None # Outputs: # - Triggers a GNOME Shell restart via busctl Eval call when possible. # Returns: # - 0 on success or when busctl is missing. # Usage: gnome::_reload_shell_theme # End of documentation if command -v busctl &>/dev/null; then busctl --quiet --user call org.gnome.Shell /org/gnome/Shell org.gnome.Shell Eval s \ 'Meta.restart("Reloading GNOME Shell to apply theme changes...")' || true fi } gnome::set_ui_common_tweaks_for_user() { # Description: Convenience alias that forwards to gnome::set_ui_tweaks_for_user. # Globals: # - None # Arguments: # - None # Outputs: # - Pass-through logs from gnome::set_ui_tweaks_for_user. # Returns: # - Whatever gnome::set_ui_tweaks_for_user returns. # Usage: gnome::set_ui_common_tweaks_for_user # End of documentation gnome::set_ui_tweaks_for_user } gnome::_apply_terminal_preferences() { # Description: Apply consistent GNOME Terminal profile settings across the default # and all known profiles. # Globals: # - None # Arguments: # - None # Outputs: # - Status/error logs; writes gsettings values for GNOME Terminal profiles. # Returns: # - 0 on success, 1 if gsettings/GNOME Terminal profiles are unavailable. # Usage: gnome::_apply_terminal_preferences # End of documentation if ! command -v gsettings &>/dev/null; then write_error "gsettings command not available; cannot configure GNOME Terminal." return 1 fi # Detect the default GNOME Terminal profile UUID(s) local profiles default_uuid profiles=$(gsettings get org.gnome.Terminal.ProfilesList list 2>/dev/null | tr -d "[]',") default_uuid=$(gsettings get org.gnome.Terminal.ProfilesList default 2>/dev/null | tr -d "'") # Fall back to the first profile in the list if no default is found if [[ -z "${default_uuid}" || "${default_uuid}" == "''" ]]; then default_uuid=$(printf '%s\n' ${profiles} | head -n1) fi [[ -z "${default_uuid}" ]] && { write_error "No GNOME Terminal profiles found; cannot apply terminal preferences." return 1 ; } local base="org.gnome.Terminal.Legacy.Profile:/org/gnome/terminal/legacy/profiles:/:${default_uuid}/" # Apply preferences gsettings set ${base} font 'Monospace 10' || true gsettings set ${base} use-system-font false || true gsettings set ${base} audible-bell false || true gsettings set ${base} use-theme-colors false || true gsettings set ${base} background-color '#000000' || true gsettings set ${base} foreground-color '#AFAFAF' || true gsettings set ${base} use-transparent-background true || true gsettings set ${base} background-transparency-percent 20 || true gsettings set ${base} bold-is-bright true || true # Apply to all known profiles as well for uuid in ${profiles}; do local b="org.gnome.Terminal.Legacy.Profile:/org/gnome/terminal/legacy/profiles:/:${uuid}/" gsettings set ${b} font 'Monospace 10' || true gsettings set ${b} use-system-font false || true gsettings set ${b} audible-bell false || true gsettings set ${b} use-theme-colors false || true gsettings set ${b} background-color '#000000' || true gsettings set ${b} foreground-color '#AFAFAF' || true gsettings set ${b} use-transparent-background true || true gsettings set ${b} background-transparency-percent 20 || true gsettings set ${b} bold-is-bright true || true done return 0 } gnome::set_ui_tweaks_for_user() { # Description: Apply the core OEM GNOME tweaks for the current desktop user # (Tilix, usability, Nautilus, app folders, extensions). Must be # executed as the target user, not root. # Globals: # - None # Arguments: # - None # Outputs: # - Status/error logs while delegating to helper functions. # Returns: # - Propagates the status of the sub-commands. # Usage: # gnome::set_ui_tweaks_for_user # system::run_command_in_gnome_user_session gnome::set_ui_tweaks_for_user # End of documentation write_status "📐 Seting common OEM tweaks..." gnome::set_tilix_ui_tweaks_for_user #gnome::set_power_configuraton_for_user #gnome::set_better_font_smoothing_for_user gnome::set_better_usability_for_user #gnome::configure_dash_to_dock #gnome::configure_dash_to_panel gnome::configure_nautilus gnome::organize_overlay_menu gnome::set_basic_extensions_enabled } gnome::set_basic_extensions_enabled() { # Description: # - Enables a curated list of GNOME Shell extensions for the current user. # - Tailors the list to the detected Linux distribution type. # # Globals: # - None # # Arguments: # - None # # Outputs: # - Status updates via write_status. # - Error details via write_error. # # Returns: # - 0 when every gsettings command succeeds. # - 1 when prerequisites are missing or a gsettings command fails. # # Dependencies: # - gsettings (runtime) # - system::distribution_type (runtime) # # Usage: # gnome::set_basic_extensions_enabled # # Notes: # - Must be executed as the intended desktop user (not root). # - Requires the listed extensions to already be installed. # # End of documentation if [[ $UID -eq 0 ]]; then write_error "This function must be run as the regular user, not as root." write_error "gsettings modifies the current user's desktop configuration." return 1 fi if ! command -v gsettings &> /dev/null; then write_error "Required command 'gsettings' not found in PATH." return 1 fi # --- Determine Distribution --- local distro_type distro_type=$(system::distribution_type) write_status "Detected distribution type: ${distro_type:-"Unknown"}" write_status "📐 Applying basic GNOME settings and enabling extensions..." local all_successful=true # Track overall success write_status "Ensuring user extensions are enabled globally..." if ! gsettings set org.gnome.shell disable-user-extensions false; then write_error "Failed to set 'disable-user-extensions' to false." all_successful=false fi # 6. Define the list of extensions based on distribution # NOTE: These extensions MUST be installed separately for this to have effect! local extensions_to_enable=( 'user-theme@gnome-shell-extensions.gcampax.github.com' 'caffeine@patapon.info' 'apps-menu@gnome-shell-extensions.gcampax.github.com' ) # Add distribution-specific extensions case "$distro_type" in ubuntu|pop|mint|zorin|elementary) write_status "Adding Ubuntu-specific extensions..." extensions_to_enable+=( 'tiling-assistant@ubuntu.com' 'ubuntu-appindicators@ubuntu.com' ) ;; *) write_status "Adding non-Ubuntu extensions (e.g., gTile)..." # Using gTile as a common tiling alternative extensions_to_enable+=( 'gTile@vibou' 'TopIcons@phocean.net' 'trayIconsReloaded@selfmade.pl' 'desktop-icons@csoriano' ) ;; esac # Format the final list for gsettings local gsettings_list_format gsettings_list_format=$(printf "'%s'," "${extensions_to_enable[@]}") gsettings_list_format="[${gsettings_list_format%,}]" # Remove trailing comma, add brackets # Apply the enabled extensions list write_status "Enabling specific extensions for '${distro_type}' (ensure they are installed):" printf " %s\n" "${extensions_to_enable[@]}" if ! gsettings set org.gnome.shell enabled-extensions "${gsettings_list_format}"; then write_error "Failed to set 'enabled-extensions'." write_error "Ensure all listed extensions are installed and names are correct." all_successful=false fi if [[ "$all_successful" == "true" ]]; then write_status "✅ Basic GNOME extensions configuration applied successfully for ${distro_type}." return 0 else write_error "❌ One or more GNOME settings failed to apply for ${distro_type}." return 1 fi } gnome::set_startup_sound() { # Description: Configure the GNOME startup sound for the current user by # enabling, disabling, or toggling an autostart helper that plays # a provided audio file. # Globals: # - XDG_CONFIG_HOME/HOME: Used for autostart entry and state storage. # - XDG_DATA_HOME/HOME: Used to store the managed sound and helper script. # Arguments: # 1: Optional action: on|off|toggle (default: toggle). # 2: Required sound file path to register and play at login. # Outputs: # - Status/error logs; writes helper script, autostart entry, and state file. # - User prompts via dialog or yad depending on display availability. # Returns: # - 0 on success; non-zero on validation or dependency failure. # Dependencies: # - dialog (required), yad (optional GUI), gsettings (optional), and one of # canberra-gtk-play/paplay/aplay for playback. # Usage: gnome::set_startup_sound [on|off|toggle] /path/to/sound-file # End of documentation local action="toggle" local sound_argument="" local message_title="GNOME Startup Sound" local ui_tool="dialog" local is_remote=0 if [[ -n ${SSH_CONNECTION:-} || -n ${SSH_TTY:-} ]]; then is_remote=1 fi local session_hint="${XDG_CURRENT_DESKTOP:-${DESKTOP_SESSION:-${GDMSESSION:-}}}" local session_hint_lower="${session_hint,,}" if [[ $is_remote -eq 0 ]] && [[ -n ${DISPLAY:-} || -n ${WAYLAND_DISPLAY:-} ]]; then if command -v yad &>/dev/null && [[ $session_hint_lower == *gnome* ]]; then ui_tool="yad" fi fi if [[ $# -eq 0 ]]; then local error_msg="Usage: gnome::set_startup_sound [on|off|toggle] /path/to/sound-file" write_error "$error_msg" case "$ui_tool" in yad) yad --error --center --title="$message_title" --button=OK --text="$error_msg" &>/dev/null ;; *) dialog::display_error "$error_msg" ;; esac return 64 fi case "$1" in on|off|toggle) action="$1" shift ;; *) : ;; esac if [[ $# -lt 1 ]]; then local error_msg="Missing required sound file path." write_error "$error_msg" case "$ui_tool" in yad) yad --error --center --title="$message_title" --button=OK --text="$error_msg" &>/dev/null ;; *) dialog::display_error "$error_msg" ;; esac return 64 fi sound_argument="$1" if [[ $UID -eq 0 ]]; then local error_msg="This function must be run as the desktop user (not root)." write_error "$error_msg" case "$ui_tool" in yad) yad --error --center --title="$message_title" --button=OK --text="$error_msg" &>/dev/null ;; *) dialog::display_error "$error_msg" ;; esac return 1 fi local config_root="${XDG_CONFIG_HOME:-${HOME}/.config}" local autostart_dir="${config_root}/autostart" local autostart_file="${autostart_dir}/rtd-startup-sound.desktop" local state_dir="${config_root}/rtd" local state_file="${state_dir}/startup-sound.state" local data_root="${XDG_DATA_HOME:-${HOME}/.local/share}" local managed_dir="${data_root}/rtd/startup-sound" local helper_script="${managed_dir}/play-startup-sound.sh" local is_enabled=0 [[ -f "$autostart_file" ]] && is_enabled=1 local target_action="" case "$action" in on) target_action="enable" ;; off) target_action="disable" ;; toggle) if [[ $is_enabled -eq 1 ]]; then target_action="disable" else target_action="enable" fi ;; *) target_action="enable" ;; esac local -r timestamp_now="$(date --iso-8601=seconds 2>/dev/null || date '+%Y-%m-%dT%H:%M:%S%z')" local result_message="" if [[ "$target_action" == "enable" ]]; then if [[ ! -f "$sound_argument" ]]; then local error_msg="The sound file '$sound_argument' does not exist or is not a regular file." write_error "$error_msg" case "$ui_tool" in yad) yad --error --center --title="$message_title" --button=OK --text="$error_msg" &>/dev/null ;; *) dialog::display_error "$error_msg" ;; esac return 1 fi if [[ ! -r "$sound_argument" ]]; then local error_msg="The sound file '$sound_argument' is not readable." write_error "$error_msg" case "$ui_tool" in yad) yad --error --center --title="$message_title" --button=OK --text="$error_msg" &>/dev/null ;; *) dialog::display_error "$error_msg" ;; esac return 1 fi local sound_realpath="$sound_argument" if command -v realpath &>/dev/null; then sound_realpath="$(realpath -m "$sound_argument" 2>/dev/null || echo "$sound_argument")" elif command -v readlink &>/dev/null; then sound_realpath="$(readlink -f "$sound_argument" 2>/dev/null || echo "$sound_argument")" fi local extension="" if [[ "$sound_realpath" == *.* ]]; then extension=".${sound_realpath##*.}" extension="${extension,,}" fi local stored_sound="${managed_dir}/startup-sound${extension}" write_status "📁 Preparing directories for startup sound..." mkdir -p "$managed_dir" "$autostart_dir" "$state_dir" write_status "📄 Copying '${sound_realpath##*/}' to '$stored_sound'" if ! install -m 0644 "$sound_realpath" "$stored_sound"; then local error_msg="Failed to copy '$sound_realpath' to '$stored_sound'." write_error "$error_msg" case "$ui_tool" in yad) yad --error --center --title="$message_title" --button=OK --text="$error_msg" &>/dev/null ;; *) dialog::display_error "$error_msg" ;; esac return 1 fi local quoted_sound="" printf -v quoted_sound '%q' "$stored_sound" printf -v quoted_helper '%q' "$helper_script" write_status "🛠️ Writing helper script to '$helper_script'" cat >"$helper_script" <<-EOF #!/bin/bash sound_file=$quoted_sound if command -v canberra-gtk-play >/dev/null 2>&1; then exec canberra-gtk-play --file="\$sound_file" elif command -v paplay >/dev/null 2>&1; then exec paplay "\$sound_file" elif command -v aplay >/dev/null 2>&1; then exec aplay "\$sound_file" fi if command -v logger >/dev/null 2>&1; then logger -t rtd-startup-sound -- "No audio player available to play startup sound: \$sound_file" fi exit 127 EOF chmod 0755 "$helper_script" write_status "🗂️ Creating GNOME autostart entry at '$autostart_file'" cat >"$autostart_file" <<-EOF [Desktop Entry] Type=Application Version=1.0 Name=RTD Startup Sound Comment=Play a custom startup sound when signing into GNOME Exec=$quoted_helper OnlyShowIn=GNOME; X-GNOME-Autostart-enabled=true EOF write_status "📝 Recording startup sound state at '$state_file'" cat >"$state_file" <<-EOF ENABLED=1 SOURCE_PATH="$sound_realpath" STORED_PATH="$stored_sound" UPDATED_AT="$timestamp_now" EOF local player_available=1 if ! command -v canberra-gtk-play &>/dev/null && ! command -v paplay &>/dev/null && ! command -v aplay &>/dev/null; then player_available=0 write_warning "No supported audio playback command found (canberra-gtk-play, paplay, aplay)." fi if command -v gsettings &>/dev/null; then if ! gsettings set org.gnome.desktop.sound event-sounds true &>/dev/null; then write_warning "Unable to enable GNOME event sounds via gsettings." fi else write_warning "'gsettings' command not found; skipping GNOME sound toggle." fi result_message="✅ Startup sound enabled. GNOME will play '${sound_realpath##*/}' at login." if [[ $player_available -eq 0 ]]; then result_message+=$'\n⚠️ Install canberra-gtk-play, paplay, or aplay to ensure playback.' fi result_message+=$'\nManaged copy stored at: '"$stored_sound" else write_status "🧹 Disabling GNOME startup sound and cleaning up autostart entry." rm -f "$autostart_file" mkdir -p "$state_dir" cat >"$state_file" <<-EOF ENABLED=0 UPDATED_AT="$timestamp_now" EOF result_message="🔕 Startup sound disabled. GNOME will no longer play a login sound." fi write_status "$result_message" case "$ui_tool" in yad) yad --info --center --width=420 --title="$message_title" --button=OK --text="$result_message" &>/dev/null ;; *) dialog::display_notice "$result_message" ;; esac return 0 } desktop::enable_firefox_pip_global() { # Description: # Ensures Firefox Picture-in-Picture windows stay on top and across workspaces by configuring devilspie2. # Globals: # Uses $XDG_SESSION_TYPE, $XDG_CURRENT_DESKTOP, $DESKTOP_SESSION, $XDG_CONFIG_HOME, $HOME. # Arguments: None # Outputs: # Writes devilspie2 rule and user unit files, enables the service, logs progress. # Returns: # 0 on success, 1 on failure. # Usage: # desktop::enable_firefox_pip_global # End of documentation security::ensure_admin write_status "Configuring Firefox Picture-in-Picture for global behavior..." local session_type="${XDG_SESSION_TYPE:-unknown}" local de="${XDG_CURRENT_DESKTOP:-${DESKTOP_SESSION:-unknown}}" local de_lc de_lc="$(printf '%s' "${de}" | tr '[:upper:]' '[:lower:]')" write_information "Detected session type: ${session_type}" write_information "Detected desktop environment: ${de}" if [[ "${session_type}" == "wayland" ]]; then case "${de_lc}" in *kde*|*plasma*) write_warning "KDE Plasma on Wayland detected. devilspie2 only affects X11/XWayland windows." write_information "If Firefox runs natively in Wayland, launch it with MOZ_DISABLE_WAYLAND=1 firefox." ;; *gnome*) write_information "GNOME on Wayland detected. Firefox PiP typically runs via XWayland, rules should apply." ;; *sway*|*hyprland*|*river*|*labwc*|*wayfire*) write_warning "wlroots-based compositor detected (${de}). Window rules usually do not work here." write_warning "Aborting configuration to avoid misleading results." return 1 ;; *) write_warning "Unknown Wayland DE (${de}); devilspie2 effectiveness is uncertain." ;; esac else write_information "Non-Wayland (likely X11) session detected; devilspie2 should operate normally." fi if ! software::check_native_package_dependency devilspie2; then write_error "Unable to install devilspie2 through available package manager." return 1 fi local config_home="${XDG_CONFIG_HOME:-$HOME/.config}" local ds2_dir="${config_home}/devilspie2" local unit_dir="${config_home}/systemd/user" local rule_file="${ds2_dir}/firefox_pip.lua" local unit_file="${unit_dir}/devilspie2.service" mkdir -p "${ds2_dir}" "${unit_dir}" cat >"${rule_file}" <<'EOF' -- Firefox Picture-in-Picture global rules -- Matches PiP window titles (contains "Picture-in-Picture") if (get_window_name():match("Picture%-in%-Picture")) then set_on_top(true) -- Always on top stick_window(true) -- Visible on all workspaces -- set_skip_tasklist(true) -- Uncomment to hide from the taskbar/dock end EOF write_information "Created devilspie2 rule: ${rule_file}" cat >"${unit_file}" <<'EOF' [Unit] Description=Devilspie2 window matching daemon [Service] Type=simple ExecStart=/usr/bin/devilspie2 Restart=on-failure [Install] WantedBy=default.target EOF write_information "Created systemd user unit: ${unit_file}" if systemctl --user --version >/dev/null 2>&1; then write_status "Reloading user systemd manager..." systemctl --user daemon-reload || { write_error "systemctl --user daemon-reload failed." return 1 } write_status "Enabling and starting devilspie2.service for the user..." if ! systemctl --user enable --now devilspie2.service; then write_error "Failed to enable/start devilspie2.service (user). Ensure a user systemd session is active." return 1 fi write_information "devilspie2 user service enabled and running." else write_warning "systemctl --user not available. Start devilspie2 manually (e.g., add 'devilspie2 &' to autostart)." fi write_information "Firefox PiP windows should now remain on top and across workspaces (where supported)." return 0 } gnome::set_ui_tweak_no_media_error() { # Description: Notify the user that theme assets are missing and optionally # trigger theme deployment when applying UI presets. # Globals: # - OEM_Hint: Optional custom prompt message shown in zenity. # Arguments: # - None # Outputs: # - zenity question dialog; may call oem::deploy_themes and plus-themes.se. # Returns: # - 0 when user cancels; 1 on unknown response; propagates command errors. # Usage: # gnome::set_ui_tweak_no_media_error # OEM_Hint="My custom message" gnome::set_ui_tweak_no_media_error # End of documentation : ${OEM_Hint:="I cannot find the actual theme files needed. Should I attempt to get these?"} zenity --question --text "${OEM_Hint}" --width=600 2>/dev/null case "$?" in 0) oem::deploy_themes bash ${_THEME_DIR}/plus-themes.se ;; 1) return ;; *) write_error "unknown response received!" return 1 ;; esac } gnome::set_ui_mac_tweaks_for_user() { # Description: Apply a macOS-inspired GNOME theme (WhiteSur) for the current # desktop user. # Globals: # - _OEM_DIR: Used to locate background images. # Arguments: # 1: Optional tone selection: Dark|Light (default: Light). # Outputs: # - Applies gsettings theme/icon/background values, GTK4 theme file, and # terminal preferences; triggers GNOME Shell reload. # Returns: # - 0 on success; propagates errors from missing assets or helper calls. # Usage: # gnome::set_ui_mac_tweaks_for_user Dark # gnome::set_ui_mac_tweaks_for_user Light # End of documentation # Save users keyboard preferences: # KeyboardLanguage=$(gsettings get org.gnome.desktop.input-sources sources ) # Apply common settings: gnome::_reset_ui_theme_settings gnome::set_ui_common_tweaks_for_user gnome::ensure_window_buttons_visible "close,minimize,maximize:" write_status "Setting MAC like tweaks..." UiTone=${1:-"Light"} case ${UiTone} in Dark | dark) if [[ -d /usr/share/themes/WhiteSur-Dark ]]; then gsettings set org.gnome.desktop.interface gtk-theme 'WhiteSur-Dark' gsettings set org.gnome.desktop.interface icon-theme 'WhiteSur-dark' gsettings set org.gnome.shell.extensions.user-theme name "WhiteSur-Dark" gsettings set org.gnome.desktop.interface color-scheme 'prefer-dark' gnome::_apply_gtk4_theme "WhiteSur-Dark" "WhiteSur-dark" 1 gnome::_reload_shell_theme gsettings set org.gnome.desktop.background picture-uri file://$(find ${_OEM_DIR:-"/opt/rtd"} -name MojaveNight.jpg) gsettings set org.gnome.desktop.background picture-uri-dark file://$(find ${_OEM_DIR:-"/opt/rtd"} -name MojaveNight.jpg) #gsettings set org.gnome.Terminal.Legacy.Profile:/org/gnome/terminal/legacy/profiles:/:$GNOME_TERMINAL_PROFILE/ theme-variant 'dark' else gnome::set_ui_tweak_no_media_error fi ;; Light | light) if [[ -d /usr/share/themes/WhiteSur-Light ]]; then gsettings set org.gnome.desktop.interface gtk-theme 'WhiteSur-Light' gsettings set org.gnome.desktop.interface icon-theme 'WhiteSur' gsettings set org.gnome.shell.extensions.user-theme name "WhiteSur-Light" gsettings set org.gnome.desktop.interface color-scheme 'default' gnome::_apply_gtk4_theme "WhiteSur-Light" "WhiteSur" 0 gnome::_reload_shell_theme gsettings set org.gnome.desktop.background picture-uri file://$(find ${_OEM_DIR:-"/opt/rtd"} -name MojaveDay.jpg) gsettings set org.gnome.desktop.background picture-uri-dark file://$(find ${_OEM_DIR:-"/opt/rtd"} -name MojaveDay.jpg) #gsettings set org.gnome.Terminal.Legacy.Profile:/org/gnome/terminal/legacy/profiles:/:$GNOME_TERMINAL_PROFILE/ theme-variant 'light' else gnome::set_ui_tweak_no_media_error fi ;; esac # Set font # gsettings set org.gnome.desktop.interface monospace-font-name 'Bitstream Vera Sans Mono' gsettings set org.gnome.desktop.wm.preferences button-layout 'close,maximize,minimize:appmenu' # Set Extensions for gnome gsettings set org.gnome.shell disable-user-extensions false gsettings set org.gnome.shell enabled-extensions "[\ 'user-theme@gnome-shell-extensions.gcampax.github.com', \ 'caffeine@patapon.info', \ 'TopIcons@phocean.net', \ 'dash-to-dock@micxgx.gmail.com', \ 'desktop-icons@csoriano', \ 'ubuntu-appindicators@ubuntu.com', \ 'CoverflowAltTab@palatis.blogspot.com']" gnome::_apply_terminal_preferences # Restore Keyboard Layout (reset by gnome) # gsettings set org.gnome.desktop.input-sources sources "${KeyboardLanguage}" busctl --quiet --user call org.gnome.Shell /org/gnome/Shell org.gnome.Shell Eval s 'Meta.restart("Reloadig Gnome with the new settings...")' & return } gnome::set_ui_win10_tweaks_for_user() { # Description: Apply a Windows 10-inspired GNOME theme for the current desktop # user, including dash-to-panel configuration. # Globals: # - _OEM_DIR: Used to locate wallpaper assets. # Arguments: # 1: Optional tone selection: Dark|Light (default: Light). # Outputs: # - Applies gsettings theme/icon/background values, GTK4 theme file, dash # tweaks, and terminal preferences; triggers GNOME Shell reload. # Returns: # - 0 on success; propagates errors when assets are missing. # Usage: # gnome::set_ui_win10_tweaks_for_user Dark # gnome::set_ui_win10_tweaks_for_user Light # End of documentation GIT_Profile=${GIT_Profile:-vonschutter} # Save users keyboard preferences: # KeyboardLanguage=$(gsettings get org.gnome.desktop.input-sources sources ) # Apply common settings: gnome::_reset_ui_theme_settings gnome::set_ui_common_tweaks_for_user gnome::ensure_window_buttons_visible "appmenu:minimize,maximize,close" write_status "Setting Windows like tweaks..." UiTone=${1:-"Light"} # Gnome Shell Theming case ${UiTone} in Dark | dark) if [[ -d /usr/share/themes/Windows-10-Dark ]]; then gsettings set org.gnome.desktop.interface gtk-theme 'Windows-10-Dark' gsettings set org.gnome.shell.extensions.user-theme name "Windows-10-Dark" gsettings set org.gnome.desktop.interface icon-theme 'Windows-10-Icons' gsettings set org.gnome.desktop.interface color-scheme 'prefer-dark' gnome::_apply_gtk4_theme "Windows-10-Dark" "Windows-10-Icons" 1 gnome::_reload_shell_theme else gnome::set_ui_tweak_no_media_error fi ;; Light | light) if [[ -d /usr/share/themes/Windows-10 ]]; then gsettings set org.gnome.desktop.interface gtk-theme 'Windows-10-Light' gsettings set org.gnome.shell.extensions.user-theme name "Windows-10-Light" gsettings set org.gnome.desktop.interface icon-theme 'Windows-10-Icons' gsettings set org.gnome.desktop.interface color-scheme 'default' gnome::_apply_gtk4_theme "Windows-10-Light" "Windows-10-Icons" 0 gnome::_reload_shell_theme else gnome::set_ui_tweak_no_media_error fi ;; esac # Set font # gsettings set org.gnome.desktop.interface monospace-font-name 'Bitstream Vera Sans Mono' #Set Extensions for gnome gsettings set org.gnome.shell disable-user-extensions false gsettings set org.gnome.shell enabled-extensions "[\ 'user-theme@gnome-shell-extensions.gcampax.github.com', \ 'caffeine@patapon.info', \ 'TopIcons@phocean.net', \ 'dash-to-panel@jderose9.github.com', \ 'ubuntu-appindicators@ubuntu.com', \ 'apps-menu@gnome-shell-extensions.gcampax.github.com']" # Configure dash-to-panel gsettings set org.gnome.shell.extensions.dash-to-panel trans-use-custom-opacity 'true' gsettings set org.gnome.shell.extensions.dash-to-dock extend-height 'true' gsettings set org.gnome.shell.extensions.dash-to-dock dock-fixed 'true' gnome::_apply_terminal_preferences # Set wallpaper gsettings set org.gnome.desktop.background picture-uri file://$(find ${_OEM_DIR:-"/opt/rtd"} -name Redmond.png) gsettings set org.gnome.desktop.background picture-uri-dark file://$(find ${_OEM_DIR:-"/opt/rtd"} -name Redmond.png) # Restore Keyboard Layout (reset by gnome) # gsettings set org.gnome.desktop.input-sources sources "${KeyboardLanguage}" busctl --quiet --user call org.gnome.Shell /org/gnome/Shell org.gnome.Shell Eval s 'Meta.restart("Reloadig Gnome with the new settings...")' & return } gnome::set_ui_corporate_crisp_tweaks_for_user() { # Description: Apply a clean corporate-inspired GNOME theme (Arc) for the # current desktop user. # Globals: # - _OEM_DIR: Used to locate wallpaper assets. # Arguments: # 1: Optional tone selection: Dark|Light (default: Light). # Outputs: # - Applies gsettings theme/icon/background values, GTK4 theme file, dash # tweaks, and terminal preferences; triggers GNOME Shell reload. # Returns: # - 0 on success; propagates errors when assets are missing. # Usage: # gnome::set_ui_corporate_crisp_tweaks_for_user Dark # gnome::set_ui_corporate_crisp_tweaks_for_user Light # End of documentation GIT_Profile=${GIT_Profile:-vonschutter} # Save users keyboard preferences: # KeyboardLanguage=$(gsettings get org.gnome.desktop.input-sources sources ) # Apply common settings: gnome::_reset_ui_theme_settings gnome::set_ui_common_tweaks_for_user gnome::ensure_window_buttons_visible "appmenu:minimize,maximize,close" write_status "Setting Crisp like tweaks..." UiTone=${1:-"Light"} # Gnome Shell Theming case ${UiTone} in Dark | dark) if [[ -d /usr/share/themes/Arc-Dark ]]; then gsettings set org.gnome.desktop.interface gtk-theme 'Arc-Dark' gsettings set org.gnome.shell.extensions.user-theme name "Arc-Dark" gsettings set org.gnome.desktop.background picture-uri file://$(find ${_OEM_DIR:-"/opt/rtd"} -name RTD_Wallpapers_HQ_Public_Domain_019.jpg) gsettings set org.gnome.desktop.background picture-uri-dark file://$(find ${_OEM_DIR:-"/opt/rtd"} -name RTD_Wallpapers_HQ_Public_Domain_019.jpg) gsettings set org.gnome.desktop.interface color-scheme 'prefer-dark' gnome::_apply_gtk4_theme "Arc-Dark" "Arc-icon-theme" 1 gnome::_reload_shell_theme else gnome::set_ui_tweak_no_media_error fi ;; Light | light) if [[ -d /usr/share/themes/Arc ]]; then gsettings set org.gnome.desktop.interface gtk-theme 'Arc' gsettings set org.gnome.shell.extensions.user-theme name "Arc" gsettings set org.gnome.desktop.background picture-uri file://$(find ${_OEM_DIR:-"/opt/rtd"} -name RTD_Wallpapers_HQ_Public_Domain_020.jpg) gsettings set org.gnome.desktop.background picture-uri-dark file://$(find ${_OEM_DIR:-"/opt/rtd"} -name RTD_Wallpapers_HQ_Public_Domain_020.jpg) gsettings set org.gnome.desktop.interface color-scheme 'default' gnome::_apply_gtk4_theme "Arc" "Arc-icon-theme" 0 gnome::_reload_shell_theme else gnome::set_ui_tweak_no_media_error fi ;; esac if [[ -d /usr/share/icons/Arc-icon-theme ]]; then gsettings set org.gnome.desktop.interface icon-theme 'Arc-icon-theme' else gnome::set_ui_tweak_no_media_error fi # Set font # gsettings set org.gnome.desktop.interface monospace-font-name 'Bitstream Vera Sans Mono' #Set Extensions for gnome gsettings set org.gnome.shell disable-user-extensions false gsettings set org.gnome.shell enabled-extensions "[\ 'user-theme@gnome-shell-extensions.gcampax.github.com', \ 'caffeine@patapon.info', \ 'TopIcons@phocean.net', \ 'dash-to-panel@jderose9.github.com', \ 'ubuntu-appindicators@ubuntu.com', \ 'apps-menu@gnome-shell-extensions.gcampax.github.com']" # Configure dash-to-panel gsettings set org.gnome.shell.extensions.dash-to-panel trans-use-custom-opacity 'true' gsettings set org.gnome.shell.extensions.dash-to-dock extend-height 'true' gsettings set org.gnome.shell.extensions.dash-to-dock dock-fixed 'true' gnome::_apply_terminal_preferences # Restore Keyboard Layout (reset by gnome) # gsettings set org.gnome.desktop.input-sources sources "${KeyboardLanguage}" busctl --quiet --user call org.gnome.Shell /org/gnome/Shell org.gnome.Shell Eval s 'Meta.restart("Reloadig Gnome with the new settings...")' & return } gnome::set_ui_moca_tweaks_for_user() { # Description: Apply an eye-friendly mocha-inspired GNOME theme (Vimix/Flatery) # for the current desktop user. # Globals: # - _OEM_DIR: Used to locate wallpaper assets. # Arguments: # - None # Outputs: # - Applies gsettings theme/icon/background values, GTK4 theme file, dash # tweaks, and terminal preferences; triggers GNOME Shell reload. # Returns: # - 0 on success; propagates errors when assets are missing. # Usage: gnome::set_ui_moca_tweaks_for_user # End of documentation GIT_Profile=${GIT_Profile:-vonschutter} # Save users keyboard preferences: # KeyboardLanguage=$(gsettings get org.gnome.desktop.input-sources sources ) # Apply common settings: gnome::_reset_ui_theme_settings gnome::set_ui_common_tweaks_for_user gnome::ensure_window_buttons_visible "appmenu:minimize,maximize,close" write_status "Setting Eye strain saving tweaks..." if [[ -d /usr/share/themes/vimix-dark-doder ]]; then gsettings set org.gnome.desktop.interface gtk-theme 'vimix-dark-doder' gsettings set org.gnome.shell.extensions.user-theme name "vimix-dark-doder" gsettings set org.gnome.desktop.background picture-uri file://$(find ${_OEM_DIR:-"/opt/rtd"} -name Chocolate_brown_wallpaper.jpg) gsettings set org.gnome.desktop.background picture-uri-dark file://$(find ${_OEM_DIR:-"/opt/rtd"} -name Chocolate_brown_wallpaper.jpg) gsettings set org.gnome.desktop.interface color-scheme 'prefer-dark' gnome::_apply_gtk4_theme "vimix-dark-doder" "Flatery-Black-Dark" 1 gnome::_reload_shell_theme else gnome::set_ui_tweak_no_media_error fi if [[ -d /usr/share/icons/Flatery-Black-Dark ]]; then gsettings set org.gnome.desktop.interface icon-theme 'Flatery-Black-Dark' else gnome::set_ui_tweak_no_media_error fi # Set font # gsettings set org.gnome.desktop.interface monospace-font-name 'Bitstream Vera Sans Mono' #Set Extensions for gnome gsettings set org.gnome.shell disable-user-extensions false gsettings set org.gnome.shell enabled-extensions "[\ 'user-theme@gnome-shell-extensions.gcampax.github.com', \ 'caffeine@patapon.info', \ 'TopIcons@phocean.net', \ 'dash-to-dock@micxgx.gmail.com', \ 'ubuntu-appindicators@ubuntu.com', \ 'apps-menu@gnome-shell-extensions.gcampax.github.com']" # Configure dash-to-panel gsettings set org.gnome.shell.extensions.dash-to-panel trans-use-custom-opacity 'true' # Configure terminal look... gnome::_apply_terminal_preferences # Restore Keyboard Layout (reset by gnome) # gsettings set org.gnome.desktop.input-sources sources "${KeyboardLanguage}" busctl --quiet --user call org.gnome.Shell /org/gnome/Shell org.gnome.Shell Eval s 'Meta.restart("Reloadig Gnome with the new settings...")' & return } oem::register_wallpapers_for_gnome() { # Validate the input directory local _wallpaper_dir="${1:-"${_WALLPAPER_DIR}"}" if [[ ! -d "$_wallpaper_dir" ]]; then echo "Error: Directory '$_wallpaper_dir' does not exist." return 1 fi local xml_file="oem-backgrounds.xml" local dest_dir="/usr/share/gnome-background-properties" local dest_file="${dest_dir}/${xml_file}" # Start with the XML header cat >"$xml_file" <<-EOF EOF # Safely iterate over .jpg and .png files shopt -s nullglob for i in "$_wallpaper_dir"/*.jpg "$_wallpaper_dir"/*.png; do cat >>"$xml_file" <<-EOF $(basename "$i") $i stretched #8f4a1c #8f4a1c solid EOF done shopt -u nullglob # Finish with the XML footer echo "" >>"$xml_file" # Ensure the destination directory exists mkdir -p "$dest_dir" # Use 'mv' instead of 'sed' to place the file to avoid unnecessary complexity # and potential issues with file paths. If further processing is needed, # it should be handled more explicitly. mv "$xml_file" "$dest_file" echo "Wallpapers registered successfully at $dest_file" } rtd_oem_turn_on_gui_network_management() { # Description: Function to set NetworkManager by default to manage networking. # # Globals: # Arguments: None # Outputs: # Returns: # Usage: # The function expects no arguments. # Usage: # rtd_oem_turn_on_gui_network_management # # End of documentation if ls /usr/bin/*session; then system::_turn_on_gui_network_management else write_error "No graphical session appear to be availabl in this system! Skipping netconfig..." fi } system::_enable_service() { # Description: Function to enable and start a systemd service. # This function checks if the service is already enabled and active, then enables # and starts it if necessary. It provides feedback on the status of the service # and handles errors gracefully. # Globals: None # Arguments: $1 - Service name (e.g., "NetworkManager") # Outputs: Status/Error messages via write_status/write_error # Returns: 0 on success, 1 on failure. # Usage: system::_enable_service # End of documentation local service_name="$1" local success=true write_status "Ensuring service '${service_name}' is enabled and active..." if ! systemctl is-enabled --quiet "${service_name}"; then write_status "Enabling ${service_name}..." if ! sudo systemctl enable "${service_name}"; then write_error "Failed to enable ${service_name}." success=false fi else write_status "${service_name} is already enabled." fi # Start the service if it's not active, even if enable failed (might still start) if ! systemctl is-active --quiet "${service_name}"; then write_status "Starting ${service_name}..." # Use restart instead of start to ensure it picks up new configs if it was already running weirdly if ! sudo systemctl restart "${service_name}"; then write_error "Failed to start/restart ${service_name}." success=false fi else # If already active, maybe reload config if relevant? Restart is safer when switching managers. write_status "Restarting ${service_name} to ensure consistency..." if ! sudo systemctl restart "${service_name}"; then write_error "Failed to restart ${service_name}." success=false fi fi # Final check if ! systemctl is-active --quiet "${service_name}"; then write_error "${service_name} is not active after start/restart attempt." success=false fi [[ "$success" == "true" ]] && return 0 || return 1 } system::ensure_directory_exists() { # Description: Function to ensure a directory exists, creating it if necessary. # This function checks if the specified directory exists, and if not, attempts to create it. # It provides feedback on the status of the directory creation and handles errors gracefully. # Globals: None # Arguments: $1 - Directory path to ensure (e.g., "/path/to/directory") # Outputs: Status/Error messages via write_status/write_error # Returns: 0 on success, 1 on failure. # Usage: ensure_directory_exists # End of documentation local dir_path="$1" if ! mkdir -p "$dir_path"; then system::log_item "ERROR: Failed to create directory '${dir_path}'." return 1 fi system::log_item "Directory ensured: ${dir_path}" return 0 } system::_disable_service() { # Description: # Disables and stops a service across multiple init systems (systemd, SysVinit, OpenRC). # # Arguments: # $1 - Service name (e.g., "NetworkManager") # # Outputs: # STDOUT: Status messages # STDERR: Error messages # # Returns: # 0 on success, 1 on any failure local service_name="$1" local success=true local init_system if [[ -z "$service_name" ]]; then write_error "No service name provided... nothing to do!" return 1 fi if command -v systemctl >/dev/null 2>&1; then init_system="systemd" elif [[ -d /etc/init.d ]] && command -v service >/dev/null 2>&1; then init_system="sysvinit" elif command -v rc-service >/dev/null 2>&1; then init_system="openrc" else write_error "No supported init system detected." return 1 fi write_status "Detected init system: $init_system" write_status "🔧 Disabling and stopping service: ${service_name}" case "$init_system" in systemd) if systemctl is-active --quiet "$service_name"; then write_status "Stopping ${service_name} via systemctl..." if ! sudo systemctl stop "$service_name"; then write_error "Failed to stop ${service_name}." success=false fi else write_status "${service_name} is not currently active." fi if systemctl is-enabled --quiet "$service_name"; then write_status "Disabling ${service_name} via systemctl..." if ! sudo systemctl disable "$service_name"; then write_error "Failed to disable ${service_name}." success=false fi else write_status "${service_name} is already disabled." fi if ! sudo systemctl daemon-reexec || ! sudo systemctl daemon-reload; then write_error "Failed to reload systemd." success=false fi ;; sysvinit) if service "$service_name" status >/dev/null 2>&1; then write_status "Stopping ${service_name} via service..." if ! sudo service "$service_name" stop; then write_error "Failed to stop ${service_name}." success=false fi else write_status "${service_name} is not currently active." fi if update-rc.d -f "$service_name" remove >/dev/null 2>&1; then write_status "Disabled ${service_name} via update-rc.d" else write_error "Failed to disable ${service_name} with update-rc.d" success=false fi ;; openrc) if rc-service "$service_name" status >/dev/null 2>&1; then write_status "Stopping ${service_name} via rc-service..." if ! sudo rc-service "$service_name" stop; then write_error "Failed to stop ${service_name}." success=false fi else write_status "${service_name} is not currently active." fi if rc-update del "$service_name" default >/dev/null 2>&1; then write_status "Removed ${service_name} from default runlevel via rc-update" else write_error "Failed to remove ${service_name} from runlevels" success=false fi ;; esac [[ "$success" == "true" ]] && return 0 || return 1 } system::_turn_on_gui_network_management() { # Description: Configures the system to use NetworkManager for network configuration, # adapting the method based on the Linux distribution. Assumes NetworkManager # provides the desired GUI management capabilities. # # Globals: None # Arguments: None # Outputs: Status/Error messages via write_status/write_error. Modifies system network config. # Returns: 0 on success, 1 on failure. # Usage: rtd_oem_turn_on_gui_network_management # Dependencies: systemd, NetworkManager (should be installed), /etc/os-release, sudo privileges. # Potentially netplan-tools (Ubuntu). # End of documentation # --- Pre-flight Checks --- security::ensure_admin if ! command -v systemctl &> /dev/null; then write_error "'systemctl' command not found. This function requires systemd." return 1 fi # Check if NetworkManager service exists (better than checking random session binaries) if ! sudo systemctl list-unit-files NetworkManager.service &> /dev/null; then write_error "NetworkManager.service not found. Please install NetworkManager first." return 1 fi # --- Distribution Detection --- local effective_distro="$(system::distribution_type)" write_status "Detected distribution context: ${effective_distro} " local overall_success=true # --- Distribution-Specific Configuration --- case "$effective_distro" in *ubuntu*) write_status "Applying NetworkManager configuration via Netplan (Ubuntu-style)..." if ! command -v netplan &> /dev/null; then write_error "'netplan' command not found, but system detected as Ubuntu-based. Cannot configure." return 1 fi local netplan_config="/etc/netplan/01-network-manager-all.yaml" local netplan_bak_dir="/etc/netplan/bak" local netplan_content # Using printf for the content to avoid heredoc issues printf -v netplan_content '%s\n' \ '# Let NetworkManager manage all devices on this system' \ 'network:' \ ' version: 2' \ ' renderer: NetworkManager' # Check if config already exists and is correct if [[ -f "$netplan_config" ]] && grep -q "renderer: NetworkManager" "$netplan_config"; then write_status "Netplan already configured for NetworkManager in ${netplan_config}." # Still ensure services are correct below (netplan apply might not fix disabled NM) else write_status "Creating Netplan configuration: ${netplan_config}" # Backup existing configs first if [[ -d /etc/netplan ]] && compgen -G "/etc/netplan/*.yaml" > /dev/null; then write_status "Backing up existing *.yaml files from /etc/netplan to ${netplan_bak_dir}..." if ! sudo mkdir -p "${netplan_bak_dir}"; then write_error "Failed to create backup directory ${netplan_bak_dir}." overall_success=false elif ! sudo mv /etc/netplan/*.yaml "${netplan_bak_dir}/"; then write_error "Failed to move existing .yaml files to backup directory." # Proceed with caution - might overwrite something if mv failed partially overall_success=false fi fi # Create the new config file if [[ "$overall_success" == "true" ]]; then echo "$netplan_content" | sudo tee "$netplan_config" > /dev/null || { write_error "Failed to create ${netplan_config}."; overall_success=false; } sudo chmod 644 "$netplan_config" # Ensure correct permissions fi fi # Apply netplan configuration (regardless of file creation, ensures state) if [[ "$overall_success" == "true" ]]; then write_status "Applying netplan configuration..." if ! sudo netplan apply; then write_error "netplan apply failed. Network configuration may be inconsistent." overall_success=false fi fi # Netplan apply should handle starting/stopping relevant services, but we ensure NM is enabled. system::_enable_service "NetworkManager.service" || overall_success=false # Explicitly disable systemd-networkd just in case netplan apply doesn't system::_disable_service "systemd-networkd.service" # Don't fail overall if this fails (might not exist) ;; # End Ubuntu case *debian*) write_status "Configuring NetworkManager directly (Debian-style)..." local nm_conf_dir="/etc/NetworkManager/conf.d" local nm_managed_conf="${nm_conf_dir}/10-rtd-managed-devices.conf" local nm_managed_content="[main]\nplugins=ifupdown,keyfile\n\n[ifupdown]\nmanaged=true\n" # Ensure NetworkManager manages devices listed in /etc/network/interfaces if [[ -f "$nm_managed_conf" ]] && grep -q 'managed=true' "$nm_managed_conf"; then write_status "NetworkManager already configured to manage devices via ${nm_managed_conf}" else write_status "Creating ${nm_managed_conf} to enable management..." sudo mkdir -p "$nm_conf_dir" || { write_error "Failed to create ${nm_conf_dir}"; overall_success=false; } if [[ "$overall_success" == "true" ]]; then printf '%b' "$nm_managed_content" | sudo tee "$nm_managed_conf" > /dev/null || { write_error "Failed to create ${nm_managed_conf}"; overall_success=false; } sudo chmod 644 "$nm_managed_conf" fi fi # Disable conflicting services and enable NetworkManager if [[ "$overall_success" == "true" ]]; then system::_disable_service "networking.service" # Handles /etc/network/interfaces via ifupdown system::_disable_service "systemd-networkd.service" # Just in case system::_enable_service "NetworkManager.service" || overall_success=false fi ;; # End Debian case *fedora*|*rhel*|*centos*) write_status "Ensuring NetworkManager is primary (Fedora/RHEL-style)..." # NetworkManager is usually default, just ensure systemd-networkd is not conflicting system::_disable_service "systemd-networkd.service" # Don't fail overall if this fails (might not exist/be active) system::_enable_service "NetworkManager.service" || overall_success=false ;; # End Fedora/RHEL case *suse*|*opensuse*) write_status "Ensuring NetworkManager is primary (SUSE-style)..." # Disable wickedd if active/enabled system::_disable_service "wicked.service" system::_disable_service "wickedd.service" # Include daemon too system::_disable_service "systemd-networkd.service" # Just in case system::_enable_service "NetworkManager.service" || { overall_success=false write_error "Failed to enable NetworkManager.service." system::_enable_service "wicked.service" system::_enable_service "wickedd.service" system::_enable_service "systemd-networkd.service" } ;; # End SUSE case *) write_warning "Distribution '${effective_distro}' not specifically handled." write_status "Attempting generic NetworkManager enablement..." # Attempt to disable systemd-networkd and enable NetworkManager as a fallback system::_disable_service "systemd-networkd.service" # Don't fail overall if this fails system::_enable_service "NetworkManager.service" || { overall_success=false write_error "Failed to enable NetworkManager.service." system::_enable_service "systemd-networkd.service" } ;; # End Default case esac # --- Final Result --- if [[ "$overall_success" == "true" ]]; then write_status "✅ Network configuration successfully set to use NetworkManager." return 0 else write_error "❌ Failed to fully configure NetworkManager as the primary network service." return 1 fi } add_gnome3_favorite_app() { # Description: Function to add a new favorite app to the gnome favorites bar. # Globals: none # Arguments: name of the shortcut to create (appname.desktop) # Outputs: # Returns: 0/1 # Usage: add_gnome3_favorite_app [appname.desktop] # # NOTE: This function must be run in the user context. # End of documentation # A potentioal simplification if the escapes cn be figured out: # sudo -iu $SUDO_USER /bin/bash -c \" "gsettings set org.gnome.shell favorite-apps \\\"\$(gsettings get org.gnome.shell favorite-apps | sed s/.\\$//), '${Newapp}']\\\"" \" NewApp="$1" if TMP_FIL=$(mktemp -p "$(mktemp -d)"); then chown -R $SUDO_USER ${TMP_FIL%/*} sudo -iu $SUDO_USER echo "gsettings set org.gnome.shell favorite-apps \"\$(gsettings get org.gnome.shell favorite-apps | sed s/.\$//), '${NewApp}']\"" >${TMP_FIL} sudo -H -u $SUDO_USER DISPLAY=:0 DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/$(id -u "$SUDO_USER")/bus /bin/bash ${TMP_FIL} rm ${TMP_FIL} else return 1 fi } kde::set_wallpaper() { # Description: # Function to set KDE Plasma wallpaper from a script. this function expects one variable # indication what file to set as background. You must provide the full path to the file. # rtd_oem_kde_set_wallpaper /opt/oem/wallpaper.png Supported fule types are: # webm, mp4, png, jpeg, gif, webp. # Globals: # Arguments: None # Outputs: # Returns: # Usage: # "kde::set_wallpaper /path/to/image_or_video [wallpaper_type] [config_key]" # End of documentation echo "Setting wallpaper $1" full_image_path=$(realpath "$1") ext=$(file -b --mime-type "$full_image_path") XDG_RUNTIME_DIR="/run/user/${SUDO_UID}" if [ -z "$2" ]; then # Identify filetype and make changes case $(echo $ext | cut -d'/' -f2) in "mp4" | "webm") type='VideoWallpaper' write='VideoWallpaperBackgroundVideo' ;; "png" | "jpeg" | "jpg") type='org.kde.image' write='Image' ;; "gif" | "webp") type='GifWallpaper' write="GifWallpaperBackgroundGif" ;; esac else type="$2" write="$3" fi wallpaper_set_script="var allDesktops = desktops(); print (allDesktops); for (i=0;i${DIALOGRC} dialog --title "${Title}" --backtitle "${BackTitle}" --msgbox "\n You must provide an ISO file as argument! \n Usage: ${0##*/} ~/filename.iso" 14 90 clear rm ${DIALOGRC} if [[ -e ${DIALOGRC}.bak ]]; then mv ${DIALOGRC}.bak ${DIALOGRC} fi return 1 else # List removable dev : not loop dev: not CD : not nvme HD : filter: header w. name devlist="$(lsblk -ndo name,rm | grep -v loop | grep -v sr | grep -v nvme | grep 1 | cut -f1 -d' ')" if [[ -z ${devlist} ]]; then DIALOGRC=~/.dialogrc echo 'screen_color = (CYAN,RED,ON)' >${DIALOGRC} dialog --title "${Title}" --backtitle "${BackTitle}" --msgbox "\n No removable media could be found! Please insert a thumb drive and try again." 14 90 clear rm ${DIALOGRC} return 1 else declare -A RemovableMediaList index=1 for media in ${devlist}; do SuggestedRemovableDevice="${media}" RemovableMediaList[$index]=$(echo -e \'$(fdisk -l /dev/$media | head -1)\') ((index++)) done DiscoveredItems="\n \n Removable media found: ${#RemovableMediaList[@]} \n -------------------------------------------------------- $( until [ $i -gt ${#RemovableMediaList[@]} ]; do [[ $i -gt 0 ]] && echo -e "Removable Disk $i: ${RemovableMediaList[$i]} \n" ((i = i + 1)) done )" USBTargetDevice=$(dialog --stdout --no-collapse --title "${Title}" --backtitle "${BackTitle}" --inputbox "${DiscoveredItems}" 15 110 /dev/${SuggestedRemovableDevice}) Response=$? clear case ${Response} in 0) dialog --title "${Title}" --backtitle "${BackTitle}" --yesno "Please Confirm:\n Write the ISO file: \n ${target_iso} \n To the drive: \n ${USBTargetDevice}" 25 90 case $? in 0) (dd if=${target_iso} of=${USBTargetDevice} status=progress 2>&1 | dialog --title "${Title}" --backtitle "${BackTitle}" --programbox "Please wait, writing disk now:" 25 90) ;; 1) write_status "Request cancelled" ;; 255) write_status "[ESC] key pressed." return 255 ;; *) write_warning "An unknown event occurred!" return 1 ;; esac clear return ;; 1) echo "Request cancelled" return 0 ;; 255) echo "[ESC] key pressed." return 255 ;; *) echo "An unknown event occurred!" return 1 ;; esac return fi fi return } util::add_iso_to_ventoy() { # Description: Function to add an ISO file to a Ventoy USB drive for multiple boot options. # Globals: None # Arguments: # $1 - The path to the ISO file to be added. # Outputs: Status and error messages. # Returns: 0 if successful, 1 if an error occurs. # Usage: util::add_iso_to_ventoy "/path/to/iso/file" local iso_file="$1" local device_list=() local ventoy_drive local mount_point local mounted_by_script local exit_status local rtd_ventoy_dir="OEMImages" # Check if the ISO file exists if [[ ! -f "$iso_file" ]]; then dialog::display_error "💿 ISO file not found: $iso_file" return 1 fi # Function to build the list of available USB devices, specifically looking for Ventoy drives util::add_iso_to_ventoy::build_device_list() { local devname partname size model label device_list=() # Iterate over all USB devices and their partitions while read -r line; do devname=$(echo "$line" | awk '{print $1}') size=$(echo "$line" | awk '{print $2}') model=$(echo "$line" | awk '{print $3}') # Check each partition of the device for part in $(lsblk -ln -o NAME "/dev/$devname" | awk '{print $1}'); do partname="/dev/$part" label=$(lsblk -no LABEL "$partname") # Check if the label is Ventoy if [[ "$label" == "Ventoy" ]]; then device_list+=("$partname" "$model $size") fi done done < <(lsblk -d -o NAME,SIZE,MODEL,TRAN | grep "usb") } # Check if any USB drives are available before proceeding util::add_iso_to_ventoy::build_device_list system::log_item "Checking for USB drives... (devicelist= ${device_list[*]})" if [[ ${#device_list[@]} -eq 0 ]]; then until [[ ${#device_list[@]} -gt 0 ]]; do dialog::display_notice "🔌 No USB drives found. Please insert a USB drive and press [OK]." util::add_iso_to_ventoy::build_device_list done fi # Prompt the user to select the Ventoy USB drive exec 3>&1 ventoy_drive=$(dialog --backtitle "$BRANDING" --title "💾 Select Ventoy USB drive" --menu "Select Ventoy USB drive to save the ISO in:" "$HEIGHT" "$WIDTH" "$LIST_HEIGHT" "${device_list[@]}" 2>&1 1>&3) exit_status=$? exec 3>&- if [[ $exit_status -ne 0 || -z "$ventoy_drive" ]]; then dialog::display_error "🔌 No Ventoy USB drive selected or dialog was canceled." return 1 fi # Check if the drive is already mounted mount_point=$(lsblk -n -o MOUNTPOINT "$ventoy_drive") # Mount the Ventoy drive if it is not already mounted if [[ -z "$mount_point" ]]; then # If not mounted, create a mount point and mount the drive mount_point=$(mktemp -d) if ! mount "$ventoy_drive" "$mount_point"; then dialog::display_error "💥 Failed to mount $ventoy_drive" rmdir "$mount_point" return 1 fi mounted_by_script=true else mounted_by_script=false fi if [[ -n "$mount_point" ]]; then write_status "Mounted Ventoy drive: $ventoy_drive -> $mount_point" if [[ -f "$mount_point/.config/oem.json" ]]; then write_status "Ventoy configuration file found: .config/oem.json" _oem_id=$(jq -r '.TLA' "$mount_point/.config/oem.json") write_status "OEM ID: $_oem_id" else write_information "Ventoy OEM configuration file not found: $mount_point" write_information "This is a generic Ventoy drive." fi else dialog::display_error "🔌 Ventoy drive is not mounted: $ventoy_drive" return 1 fi if [[ ! -d "$mount_point/$rtd_ventoy_dir" ]]; then write_warning "📁 Ventoy directory not found (!) creating it now: $mount_point/$rtd_ventoy_dir" mkdir -p "$mount_point/$rtd_ventoy_dir" || system::log_item "Failed to create: $mount_point/$rtd_ventoy_dir" fi # Define your variables destination="$mount_point/$rtd_ventoy_dir/$(basename "$iso_file")" # Ensure destination directory exists mkdir -p "$mount_point/$rtd_ventoy_dir" # Copy the ISO file to the Ventoy drive with progress bar if dialog::copy_with_progress "$iso_file" "$destination" "$mount_point"; then dialog --msgbox "✅💿📥 Successfully added ISO to Ventoy drive: $iso_file Boot from the Ventoy drive on another computer to access the new ISO." 10 70 else dialog --msgbox "❌💿📥 Failed to copy ISO to Ventoy drive: $iso_file" 10 70 if $mounted_by_script; then { umount "$mount_point"; } && { rmdir "$mount_point"; } fi exit 1 fi # Unmount the Ventoy drive and clean up if it was mounted by this script if $mounted_by_script; then if umount "$mount_point"; then write_status "✅ ⏏️ Successfully unmounted Ventoy drive." rmdir "$mount_point" else write_error "❌ ⏏️ Failed to unmount Ventoy drive." return 1 fi fi return 0 } rtd_oem_ubuntu_auto_install_iso_builder() { # Description: Function to generate an edited ISO file from a folder. # this function expects a minimum of one parameter: ssh-server, ubuntu-desktop, kubuntu-desktop, xubuntu-desktop, lubuntu-desktop etc. # By default the server ISO image will be downloaded and manipulated. Optionally this function can be told to use the # Ubuntu Desktop ISO instead of the server iso by passing a second parameter: # # Globals: # Arguments: None # Outputs: # Returns: # Usage: # :: function name :: target :: iso version to download # rtd_oem_ubuntu_auto_install_iso_builder ubuntu-desktop desktop ⏭ creates auoinstall iso using the Ubuntu live dvd # rtd_oem_ubuntu_auto_install_iso_builder ubuntu-desktop live ⏭ creates auoinstall iso using the Ubuntu live dvd # rtd_oem_ubuntu_auto_install_iso_builder ubuntu-desktop server ⏭ creates auoinstall iso using the Ubuntu server dvd # # End of Documentation system::log_item "request received for: ${*}" # Check for other input and create them if not defined... kvm::util::read_common_options ${*} # Set specific options for Ubuntu... kvm::util::read_distro_options --distribution ubuntu ${*} CONFIG=${_UserDesktopEnvironmentSelection} system::log_item "Config set to: $CONFIG" ISO_VER=${_role} system::log_item "ISO_VER set to: $ISO_VER" system::prepare_environment_for_iso_creation write_information "Retreive list of available Ubuntu versions..." ubuntu::get_target_version if [[ "$HOME" == "/root" ]]; then HOME=/home/$SUDO_USER; fi ubuntu::download_iso --dir $HOME/Virtual-DVDs/Downloaded --flavor ubuntu --version ${tgt_ubuntu_ver} --type desktop if [[ -z "${iso:-}" || ! -f "$iso" ]]; then write_error "Ubuntu ISO is missing after download attempt (iso=${iso:-unset})" system::prepare_environment_for_iso_creation --cleanup return 1 fi mnt="${tmp_disc_dir:=$(mktemp -d)}" tmp_disc_dir=$mnt write_status "Mounting intallation media..." if ! mount "$iso" "$mnt" -o user,ro; then pause::for_input 1 system::prepare_environment_for_iso_creation --cleanup return 1 fi write_status "Verifying media presence..." cat "$mnt"/.disk/*info || pause::for_input 1 auto="$BRANDING_ORG-$CONFIG-auto-install-from-${BASE}" write_information "Creating Installations media: $auto " if [ ! -e $auto ]; then cp -a "$mnt" "$auto" chmod -R +w "$auto" fi write_status "Modifying startup instructions... " for f in splash.pcx splash.png; do oem::setup_brand_splash_screen "$auto/isolinux/$f" done pushd "./$auto/isolinux/" find splash.pcx | cpio -ov >>bootlogo find splash.png | cpio -ov >>bootlogo popd write_status "Unmounting $mnt... " umount "$mnt" || pause::for_input 1 case "$2" in desktop | Desktop | live | Live) #configure_auto_unattended_ubuntu_live_boot_media $auto $CONFIG && echo -e "$GREEN OK! $ENDCOLOR" || pause::for_input 1 write_status "Modifying installation instructions in live media... " cat >"$auto/isolinux/txt.cfg" <<-EOF default live-install label live-install menu label ^^Auto Install Ubuntu $CONFIG kernel /casper/vmlinuz append file=/cdrom/preseed.cfg auto=true priority=critical debian-installer/locale=en_US keyboard-configuration/layoutcode=us console-setup/ask_detect=false ubiquity/reboot=true languagechooser/language-name=English countrychooser/shortlist=US localechooser/supported-locales=en_US.UTF-8 boot=casper automatic-ubiquity initrd=/casper/initrd quiet splash noprompt noshell --- EOF system::make_preseed_cfg "$auto" "$CONFIG" ;; *) #configure_auto_unattended_ubuntu_server_boot_media $auto $CONFIG && echo -e "$GREEN OK! $ENDCOLOR" || pause::for_input 1 write_status "Modifying installation instructions in server image... " cat >"$auto/isolinux/isolinux.cfg" <<-EOF default install label install gfxmode=791 ui gfxboot bootlogo path timeout 100 menu label ^Auto Install Ubuntu $CONFIG kernel /install/vmlinuz append file=/cdrom/preseed/ubuntu-server.seed initrd=/install/initrd.gz ks=cdrom:/ks.cfg preseed/file=/cdrom/preseed.cfg -- EOF touch "$auto/preseed.cfg" cat >"$auto/boot/grub/grub.cfg" <<-'EOF' set timeout=10 GRUB_GFXMODE="1024x768" set menu_color_normal=white/black set menu_color_highlight=black/light-gray EOF system::make_preseed_cfg ${*} ;; esac target_iso="$put_iso_file_here_when_done/$auto.iso" if [[ -f $target_iso ]]; then rm "$target_iso" fi $bin_xorriso -as mkisofs -isohybrid-mbr "$isohdpfx_bin" \ -c isolinux/boot.cat -b isolinux/isolinux.bin \ -no-emul-boot -boot-load-size 4 -boot-info-table \ -eltorito-alt-boot -e boot/grub/efi.img -no-emul-boot \ -isohybrid-gpt-basdat -o "$put_iso_file_here_when_done/$auto.iso" $auto pause::for_input $? umount -l "$mnt" rm -rf "$mnt" "$auto" if ($RTD_GUI --title "Media ready" --yesno "Created $auto.iso Would you like to test the new image?." 0 0); then software::check_native_package_dependency qemu-kvm qemu-img create -f qcow2 "$put_qcow_file_here_when_done/$auto.qcow2" 10G qemu-system-x86_64 -smp 2 -enable-kvm -m 4G --cdrom "$target_iso" "$put_qcow_file_here_when_done/$auto.qcow2" else echo "User selected No, exit status was $?." fi system::prepare_environment_for_iso_creation --cleanup } system::process_vm_opt_args() { # Description: # Function to evaluate input fiven to a VM build finction and set configuration override variables. # # Globals: # Arguments: No[-c <#CPU>, -m , -d , -r , -t ]ne # Outputs: # Returns: variables; _cpu _mem _dsk _task _role # Usage: # # system::process_vm_opt_args [-c <#CPU>, -m , -d , -r , -t ] # # Supported overrides: # Role: -r ${role} # Task: -t ${CONFIG} # CPU: -c ${_cpu} VCPU # Memory: -m ${_mem} GB # Disk: -d ${_dsk} GB # # # call from another function or script: # system::process_vm_opt_args $@ # # This will call this function and pass all parameters received here. Any parameters that match the expected # format will set the variable accordingly. # # End of documentation # Check for optional default T-Shirts size overrides... write_information "Set custom values for VM T-Shirts sizes, tasks and roles..." local OPTIND o a while getopts ':c:m:d:t:r:*' OPTION; do case "$OPTION" in c) _cpu="${OPTARG}" write_information "Custom CPU set to: ${OPTARG}" ;; m) _mem="${OPTARG}" write_information "Custom memory set to: ${OPTARG}" ;; d) _dsk="${OPTARG}" write_information "Custom disk size set to: ${OPTARG}" ;; t) _task="${OPTARG}" write_information "Custom task set to: ${OPTARG}" ;; r) _role="${OPTARG}" write_information "Custom role set to: ${OPTARG}" ;; ?) write_information "Usage: ${FUNCNAME[1]} or ${FUNCNAME[0]} [-c <#CPU>, -m , -d , -r , -t ]" return 0 ;; *) echo "Using default VM settings..." return 0 ;; esac done unset OPTIND } system::find_download_ubuntu_iso() { ubuntu_flavor="${1}" write_information "🔎 Retreive list of available $ubuntu_flavor versions..." case "$ubuntu_flavor" in ubuntu) declare -a all_lts_versions=($(wget -O- releases.ubuntu.com -q | perl -ne '/Ubuntu (\d+.\d+.\d+)/ && print "$1\n"' | sort -Vu)) dist_logo="$ubuntulogo" ;; kubuntu) declare -a all_lts_versions=($(wget -O- cdimage.ubuntu.com/kubuntu/releases -q | perl -ne '/ (\d+.\d+.\d+)/ && print "$1\n"' | sort -Vu)) dist_logo="$kubuntulogo" ;; *) declare -a all_lts_versions=($(wget -O- releases.ubuntu.com -q | perl -ne '/Ubuntu (\d+.\d+.\d+)/ && print "$1\n"' | sort -Vu)) dist_logo="$ubuntulogo" ;; esac tgt_ubuntu_ver=$($RTD_GUI --colors --title "Select Release Version of $ubuntu_flavor " --inputbox "\n 👍 Please pick an available \Z1 $ubuntu_flavor \Zn version by entering it below. Long Term Support Versions to choose from are the following:\Z4 ${all_lts_versions[*]} \Zn You may also enter an inbetween release version by typing its release number below as well. If you are not sure just let me choose intelligently for you... \n \Z1 ${dist_logo} \Zn" 30 90 "${all_lts_versions[-1]}" 3>&1 1>&2 2>&3) case $? in "$DIALOG_CANCEL") return ;; "$DIALOG_ESC") return ;; esac clear [ "$tgt_ubuntu_ver" ] || tgt_ubuntu_ver=${all_lts_versions[-1]} : ${ubuntu_iso_url=$(system::rtd_oem_find_live_release $tgt_ubuntu_ver $ubuntu_flavor live)} : ${iso_filename:="$(basename $ubuntu_iso_url)"} : ${permanent_download_dir:="/var/lib/libvirt/boot/"} write_status "Checking if $iso_filename already downloaded..." iso=$(find "$permanent_download_dir" -name "$iso_filename") if [ ! -e "$iso" ]; then write_warning "$iso_filename is not in cache, downloading..." wget -nc $ubuntu_iso_url -P "$permanent_download_dir" || read -p "Failure to download ISO file" iso="$permanent_download_dir/$iso_filename" fi } system::create_physical_media_from_ubuntu_iso() { # Description: # Courtesy of 'covertsh': ubuntu-autoinstall-generator # Function to automatically write ISO (virtual DVD/CD/BlueRay) to a thumb drive. No argumenst are # required, but a source ISO file location can may be provided when calling the function. # If information is required, but not provided, the end user will be interactively prompted. # This function requires elevated priviledges to be able to write media to a thumb drive # (boot sector) and other system restrictied activities. If the function is NOT called in a # script with elevated priviledges, it will attempt to elevate priviledges and prompt for a password. # Since writing a new bootable thumb drive is inherrently an interactive activity; a prompt will # be displayed asking what media to write to. If no media is present, a warning error # message will be displayed. # # Globals: # Arguments: None # Outputs: # Returns: # Usage: # # "system::create_physical_media_from_ubuntu_iso" or "create_physical_media_from_iso path/to/file.iso" # # End of documentation cleanup() { trap - SIGINT SIGTERM ERR EXIT if [ -n "${tmpdir+x}" ]; then rm -rf "$tmpdir" log "🚽 Deleted temporary working directory $tmpdir" fi } trap cleanup SIGINT SIGTERM ERR EXIT script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" &>/dev/null && pwd -P) [[ ! -x "$(command -v date)" ]] && echo "💥 date command not found." && exit 1 today=$(date +"%Y-%m-%d") log() { write_host --cyan "[$(date +"%Y-%m-%d %H:%M:%S")] ${1-}" } die() { local msg=$1 local code=${2-1} # Bash parameter expansion - default exit status 1. See https://wiki.bash-hackers.org/syntax/pe#use_a_default_value log "$msg" exit "$code" } usage() { create_physical_media_from_ubuntu_iso_usage=" Usage: $(basename "${BASH_SOURCE[0]}") [-h] [-v] [-a] [-e] [-u user-data-file] [-m meta-data-file] [-k] [-c] [-r] [-s source-iso-file] [-d destination-iso-file] 💁 This script will create fully-automated Ubuntu 20.04 Focal Fossa installation media. Available options: -h, --help Print this help and exit -v, --verbose Print script debug info -a, --all-in-one Bake user-data and meta-data into the generated ISO. By default you will need to boot systems with a CIDATA volume attached containing your autoinstall user-data and meta-data files. For more information see: https://ubuntu.com/server/docs/install/autoinstall-quickstart -e, --use-hwe-kernel Force the generated ISO to boot using the hardware enablement (HWE) kernel. Not supported by early Ubuntu 20.04 release ISOs. -u, --user-data Path to user-data file. Required if using -a -m, --meta-data Path to meta-data file. Will be an empty file if not specified and using -a -k, --no-verify Disable GPG verification of the source ISO file. By default SHA256SUMS-$today and SHA256SUMS-$today.gpg in ${script_dir} will be used to verify the authenticity and integrity of the source ISO file. If they are not present the latest daily SHA256SUMS will be downloaded and saved in ${script_dir}. The Ubuntu signing key will be downloaded and saved in a new keyring in ${script_dir} -c, --no-md5 Disable MD5 checksum on boot -r, --use-release-iso Use the current release ISO instead of the daily ISO. The file will be used if it already exists. -s, --source Source ISO file. By default the latest daily ISO for Ubuntu 20.04 will be downloaded and saved as ${script_dir}/ubuntu-original-$today.iso That file will be used by default if it already exists. -d, --destination Destination ISO file. By default ${script_dir}/ubuntu-autoinstall-$today.iso will be created, overwriting any existing file." echo $create_physical_media_from_ubuntu_iso_usage } parse_params() { # default values of variables set from params user_data_file='' meta_data_file='' ubuntu_flavor="ubuntu" dist_logo="$ubuntulogo" tgt_ubuntu_ver=$($RTD_GUI --colors --title "Select Release Version of $ubuntu_flavor " --inputbox "\n 👍 Please pick an available \Z1 $ubuntu_flavor \Zn version by entering it below. Long Term Support Versions to choose from are the following:\Z4 ${all_lts_versions[*]} \Zn You may also enter an inbetween release version by typing its release number below as well. If you are not sure just let me choose intelligently for you... \n \Z1 ${dist_logo} \Zn" 30 90 "${all_lts_versions[-1]}" 3>&1 1>&2 2>&3) case $? in "$DIALOG_CANCEL") return ;; "$DIALOG_ESC") return ;; esac clear : ${ubuntu_iso_url=$(system::rtd_oem_find_live_release $tgt_ubuntu_ver $ubuntu_flavor live)} download_url="https://cdimage.ubuntu.com/ubuntu-server/focal/daily-live/current" download_iso="focal-live-server-amd64.iso" original_iso="ubuntu-original-$today.iso" source_iso="${script_dir}/${original_iso}" destination_iso="${script_dir}/ubuntu-autoinstall-$today.iso" sha_suffix="${today}" gpg_verify=1 all_in_one=0 use_hwe_kernel=0 md5_checksum=1 use_release_iso=0 while :; do case "${1-}" in -h | --help) usage ;; -v | --verbose) set -x ;; -a | --all-in-one) all_in_one=1 ;; -e | --use-hwe-kernel) use_hwe_kernel=1 ;; -c | --no-md5) md5_checksum=0 ;; -k | --no-verify) gpg_verify=0 ;; -r | --use-release-iso) use_release_iso=1 ;; -u | --user-data) user_data_file="${2-}" shift ;; -s | --source) source_iso="${2-}" shift ;; -d | --destination) destination_iso="${2-}" shift ;; -m | --meta-data) meta_data_file="${2-}" shift ;; -?*) die "Unknown option: $1" ;; *) break ;; esac shift done log "👶 Starting up..." # check required params and arguments if [ ${all_in_one} -ne 0 ]; then [[ -z "${user_data_file}" ]] && die "💥 user-data file was not specified." [[ ! -f "$user_data_file" ]] && die "💥 user-data file could not be found." [[ -n "${meta_data_file}" ]] && [[ ! -f "$meta_data_file" ]] && die "💥 meta-data file could not be found." fi if [ "${source_iso}" != "${script_dir}/${original_iso}" ]; then [[ ! -f "${source_iso}" ]] && die "💥 Source ISO file could not be found." fi if [ "${use_release_iso}" -eq 1 ]; then download_url="https://releases.ubuntu.com/focal" log "🔎 Checking for current release..." download_iso=$(curl -sSL "${download_url}" | grep -oP 'ubuntu-20\.04\.\d*-live-server-amd64\.iso' | head -n 1) original_iso="${download_iso}" source_iso="${script_dir}/${download_iso}" current_release=$(echo "${download_iso}" | cut -f2 -d-) sha_suffix="${current_release}" log "💿 Current release is ${current_release}" fi destination_iso=$(realpath "${destination_iso}") source_iso=$(realpath "${source_iso}") return 0 } ubuntu_gpg_key_id="843938DF228D22F7B3742BC0D94AA3F0EFE21092" parse_params "$@" tmpdir=$(mktemp -d) if [[ ! "$tmpdir" || ! -d "$tmpdir" ]]; then die "💥 Could not create temporary working directory." else log "📁 Created temporary working directory $tmpdir" fi log "🔎 Checking for required utilities..." [[ ! -x "$(command -v xorriso)" ]] && die "💥 xorriso is not installed. On Ubuntu, install the 'xorriso' package." [[ ! -x "$(command -v sed)" ]] && die "💥 sed is not installed. On Ubuntu, install the 'sed' package." [[ ! -x "$(command -v curl)" ]] && die "💥 curl is not installed. On Ubuntu, install the 'curl' package." [[ ! -x "$(command -v gpg)" ]] && die "💥 gpg is not installed. On Ubuntu, install the 'gpg' package." [[ ! -f "/usr/lib/ISOLINUX/isohdpfx.bin" ]] && die "💥 isolinux is not installed. On Ubuntu, install the 'isolinux' package." log "👍 All required utilities are installed." if [ ! -f "${source_iso}" ]; then log "🌎 Downloading ISO image for Ubuntu 20.04 Focal Fossa..." curl -NsSL "${download_url}/${download_iso}" -o "${source_iso}" log "👍 Downloaded and saved to ${source_iso}" else log "☑️ Using existing ${source_iso} file." if [ ${gpg_verify} -eq 1 ]; then if [ "${source_iso}" != "${script_dir}/${original_iso}" ]; then log "⚠️ Automatic GPG verification is enabled. If the source ISO file is not the latest daily or release image, verification will fail!" fi fi fi if [ ${gpg_verify} -eq 1 ]; then if [ ! -f "${script_dir}/SHA256SUMS-${sha_suffix}" ]; then log "🌎 Downloading SHA256SUMS & SHA256SUMS.gpg files..." curl -NsSL "${download_url}/SHA256SUMS" -o "${script_dir}/SHA256SUMS-${sha_suffix}" curl -NsSL "${download_url}/SHA256SUMS.gpg" -o "${script_dir}/SHA256SUMS-${sha_suffix}.gpg" else log "☑️ Using existing SHA256SUMS-${sha_suffix} & SHA256SUMS-${sha_suffix}.gpg files." fi if [ ! -f "${script_dir}/${ubuntu_gpg_key_id}.keyring" ]; then log "🌎 Downloading and saving Ubuntu signing key..." gpg -q --no-default-keyring --keyring "${script_dir}/${ubuntu_gpg_key_id}.keyring" --keyserver "hkp://keyserver.ubuntu.com" --recv-keys "${ubuntu_gpg_key_id}" log "👍 Downloaded and saved to ${script_dir}/${ubuntu_gpg_key_id}.keyring" else log "☑️ Using existing Ubuntu signing key saved in ${script_dir}/${ubuntu_gpg_key_id}.keyring" fi log "🔐 Verifying ${source_iso} integrity and authenticity..." gpg -q --keyring "${script_dir}/${ubuntu_gpg_key_id}.keyring" --verify "${script_dir}/SHA256SUMS-${sha_suffix}.gpg" "${script_dir}/SHA256SUMS-${sha_suffix}" 2>/dev/null if [ $? -ne 0 ]; then rm -f "${script_dir}/${ubuntu_gpg_key_id}.keyring~" die "👿 Verification of SHA256SUMS signature failed." fi rm -f "${script_dir}/${ubuntu_gpg_key_id}.keyring~" digest=$(sha256sum "${source_iso}" | cut -f1 -d ' ') set +e grep -Fq "$digest" "${script_dir}/SHA256SUMS-${sha_suffix}" if [ $? -eq 0 ]; then log "👍 Verification succeeded." set -e else die "👿 Verification of ISO digest failed." fi else log "🤞 Skipping verification of source ISO." fi log "🔧 Extracting ISO image..." xorriso -osirrox on -indev "${source_iso}" -extract / "$tmpdir" &>/dev/null chmod -R u+w "$tmpdir" rm -rf "$tmpdir/"'[BOOT]' log "👍 Extracted to $tmpdir" if [ ${use_hwe_kernel} -eq 1 ]; then if grep -q "hwe-vmlinuz" "$tmpdir/boot/grub/grub.cfg"; then log "☑️ Destination ISO will use HWE kernel." sed -i -e 's|/casper/vmlinuz|/casper/hwe-vmlinuz|g' "$tmpdir/isolinux/txt.cfg" sed -i -e 's|/casper/initrd|/casper/hwe-initrd|g' "$tmpdir/isolinux/txt.cfg" sed -i -e 's|/casper/vmlinuz|/casper/hwe-vmlinuz|g' "$tmpdir/boot/grub/grub.cfg" sed -i -e 's|/casper/initrd|/casper/hwe-initrd|g' "$tmpdir/boot/grub/grub.cfg" sed -i -e 's|/casper/vmlinuz|/casper/hwe-vmlinuz|g' "$tmpdir/boot/grub/loopback.cfg" sed -i -e 's|/casper/initrd|/casper/hwe-initrd|g' "$tmpdir/boot/grub/loopback.cfg" else log "⚠️ This source ISO does not support the HWE kernel. Proceeding with the regular kernel." fi fi log "🧩 Adding autoinstall parameter to kernel command line..." sed -i -e 's/---/ autoinstall ---/g' "$tmpdir/isolinux/txt.cfg" sed -i -e 's/---/ autoinstall ---/g' "$tmpdir/boot/grub/grub.cfg" sed -i -e 's/---/ autoinstall ---/g' "$tmpdir/boot/grub/loopback.cfg" log "👍 Added parameter to UEFI and BIOS kernel command lines." if [ ${all_in_one} -eq 1 ]; then log "🧩 Adding user-data and meta-data files..." mkdir "$tmpdir/nocloud" cp "$user_data_file" "$tmpdir/nocloud/user-data" if [ -n "${meta_data_file}" ]; then cp "$meta_data_file" "$tmpdir/nocloud/meta-data" else touch "$tmpdir/nocloud/meta-data" fi sed -i -e 's,---, ds=nocloud;s=/cdrom/nocloud/ ---,g' "$tmpdir/isolinux/txt.cfg" sed -i -e 's,---, ds=nocloud\\\;s=/cdrom/nocloud/ ---,g' "$tmpdir/boot/grub/grub.cfg" sed -i -e 's,---, ds=nocloud\\\;s=/cdrom/nocloud/ ---,g' "$tmpdir/boot/grub/loopback.cfg" log "👍 Added data and configured kernel command line." fi if [ ${md5_checksum} -eq 1 ]; then log "👷 Updating $tmpdir/md5sum.txt with hashes of modified files..." md5=$(md5sum "$tmpdir/boot/grub/grub.cfg" | cut -f1 -d ' ') sed -i -e 's,^.*[[:space:]] ./boot/grub/grub.cfg,'"$md5"' ./boot/grub/grub.cfg,' "$tmpdir/md5sum.txt" md5=$(md5sum "$tmpdir/boot/grub/loopback.cfg" | cut -f1 -d ' ') sed -i -e 's,^.*[[:space:]] ./boot/grub/loopback.cfg,'"$md5"' ./boot/grub/loopback.cfg,' "$tmpdir/md5sum.txt" log "👍 Updated hashes." else log "🗑️ Clearing MD5 hashes..." echo >"$tmpdir/md5sum.txt" log "👍 Cleared hashes." fi log "📦 Repackaging extracted files into an ISO image..." cd "$tmpdir" xorriso -as mkisofs -r -V "ubuntu-autoinstall-$today" -J -b isolinux/isolinux.bin -c isolinux/boot.cat -no-emul-boot -boot-load-size 4 -isohybrid-mbr /usr/lib/ISOLINUX/isohdpfx.bin -boot-info-table -input-charset utf-8 -eltorito-alt-boot -e boot/grub/efi.img -no-emul-boot -isohybrid-gpt-basdat -o "${destination_iso}" . &>/dev/null cd "$OLDPWD" log "👍 Repackaged into ${destination_iso}" die "✅ Completed." 0 } tool::compress_all_items_here() { # Description: Function to compress the contents of the present working directory. # # Purpose: To compress all files and folders individually found in the current folder. # The current folder refers to the present working directory "PWD". Compression method # is 7z. This privides a high level of compression. # # Globals: # Arguments: --noprompt, --encrypt (mutually exclusive) # Outputs: # Returns: # Usage: compress_all_items_here [--compress] [--noprompt] # Usage: Simply call this function to accomplish this task. # # Arguments: No parameters required for basic function (user will be prompted). # # End of documentation dependency::command_exists 7z if [[ ! "$1" == "--noprompt" ]]; then if hash dialog 2>/dev/null; then if (dialog --backtitle "${BRANDING:-"${FUNCNAME[0]}"} ${1}" --title "Compress Content Here" --colors --cr-wrap --no-collapse --no-button "NO: Quit" --yes-button "YES: Compress" --yesno "Hello ${USER}... I am going to compress each file or folder that I find here: $(pwd) These are: \Z5 \n$(ls -h1 -F -I "*.7z")" 20 90); then clear else clear exit fi else clear write_host --cyan "Hello ${USER}... I am going to compress each file or folder that I find in this folder. These are:" ls -I "*.7z" --color=always echo -e " \n" write_information "To cancel this, just close the terminal or press [CRTL] + [C]." read -p "Press the [ENTER] key to continue..." fi fi if echo ${*} | grep "encrypt"; then passtoken=$(dialog --title "${Title:="$(basename $0)"}" --backtitle "${BRANDING:-"${FUNCNAME[0]}"}" --stdout --insecure --passwordbox "\n Please provide a passphrase for the encryption. \n Please do not forget it!" 10 90) ret=$? clear case $ret in 0) SAVEIFS=$IFS IFS=$(echo -en "\n\b") for line in $(ls -I "*.7z"); do 7z a -t7z -m0=lzma2 -mx=5 -mfb=64 -md=64m -ms=on -mhe=on -p$passtoken $line.7z $line done IFS=$SAVEIFS result=" \n Created the following archives encrypted with a pass-phrase: \n \Z1 $(ls *.7z)" dialog::display_result "Done creating encrypted archives" ;; 1) echo "Request cancelled" exit 1 ;; 255) echo "[esc] Request aborted" exit 255 ;; *) exit 1 ;; esac else SAVEIFS=$IFS IFS=$(echo -en "\n\b") for line in $(ls -I "*.7z"); do 7z a -t7z -m0=lzma2 -mx=9 -mfb=64 -md=64m -ms=on -mhe=on $line.7z $line done IFS=$SAVEIFS result=" Created the following archives un-encrypted: \n \Z1 $(ls *.7z)" dialog::display_result "Done Compressing" fi } tool::compress_provided_items() { # Description: Function to compress the contents of the present working directory. # # Purpose: To compress all files and folders individually found in the current folder. # The current folder refers to the present working directory "PWD". Compression method # is 7z. This privides a high level of compression. # # Globals: # Arguments: --noprompt, --encrypt (mutually exclusive) # Outputs: # Returns: # Usage: compress_provided_items [--encrypt] [--noprompt] # Usage: Simply call this function to accomplish this task. # # Arguments: No parameters required for basic function (user will be prompted). # # End of documentation dependency::command_exists 7z list="$*" if echo "${@}" | grep "--encrypt"; then passtoken=$(dialog --title "${Title:="$(basename $0)"}" \ --backtitle "${BRANDING:-"${FUNCNAME[0]}"}" \ --stdout \ --insecure \ --passwordbox "\n To encrypt the content you have to give me a phrase to encrypt it with. \n This can be anything in one long word, but never forget it!" \ 10 90) ret=$? case $ret in 0) echo "☕ Please be patient, Large sets of files may take serious time..." list="${list:9:1000}" SAVEIFS=$IFS IFS=$(echo -en "\n\b") for line in $list; do 7z a -t7z -m0=lzma2 -mx=9 -mfb=64 -md=64m -ms=on -mhe=on -p$passtoken $line.7z $line done IFS=$SAVEIFS result=" \n♹ Created the following archive(s) encrypted: \n \n STATUS FILENAME $( echo \\n for d in $list; do [[ -e $d.7z ]] && echo "Confirmed : $d.z7 \\n" || echo " Failed! : $d.z7 \\n" done ) \n Please review the above: ⏫" dialog::display_result "Done creating encrypted archives" ;; 1) echo "Request cancelled" exit 1 ;; 255) echo "[esc] Request aborted" exit 255 ;; *) exit 1 ;; esac else SAVEIFS=$IFS IFS=$(echo -en "\n\b") write_information "☕ Please be patient, Large sets of files may take serious time..." for item in ${list}; do case $item in --noprompt) echo skipp >/dev/null ;; *) 7z a -t7z -m0=lzma2 -mx=5 -mfb=64 -md=64m -ms=on -mhe=on $item.7z $item ;; esac done IFS=$SAVEIFS result=" Created the following archive(s): \n \n STATUS FILENAME $( echo \\n for d in ${list}; do case $d in --noprompt) echo skipp >/dev/null ;; *) [[ -e $d.7z ]] && echo "Confirmed : $d.z7 \\n" || echo " Failed! : $d.z7 \\n" ;; esac done ) \n Please review the above: ⏫" if echo ${*} | grep "--noprompt"; then echo $result else dialog::display_result "Done creating encrypted archives" fi fi } tool::recompress_all_items_in_folder() { # Description: Function to recompress all archive files in the current directory to a specified format. # # Purpose: To convert all archive files found in the current directory into a different archive format. # This function supports converting to and from 7z, zip, rar, tar, gz, and bz2 formats. It aims # to make file management more efficient by allowing users to standardize their archive formats. # # Globals: None. # Arguments: 1. target_format - The archive format to which all files will be recompressed. Supported formats # are 7z, zip, rar, tar, gz, and bz2. # Outputs: Outputs are the newly created archive files in the specified format. Original files can be optionally # removed after successful recompression. # Returns: Returns 0 on success, or 1 if an unsupported format is specified or if any errors occur during # the recompression process. # Usage: recompress_all_items_here # Example: recompress_all_items_here zip # This will recompress all supported archive files in the current directory to zip format. # # Notes: - This function requires the corresponding command-line utilities for the specified formats # (e.g., 7z, zip, rar, tar, gzip, bzip2) to be installed and accessible in the system's PATH. # - It creates temporary directories for decompression and subsequent recompression processes, # which are removed upon completion. # # End of documentation dependency::command_exists 7z local tgt_format="${1:-"7z"}" case $tgt_format in 7z) write_information "Recompressing all items in the current folder to 7z format..." for i in *.zip *.rar *.tar *.gz *.bz2; do base="${i%%.*}" mkdir "$base" && 7z x "$i" -o"$base"/ && 7z a -t7z -m0=lzma2 -mx=5 -mfb=64 -md=64m -ms=on -mhe=on "$base.7z" "$base" && rm -r "$base" done ;; zip) write_information "Recompressing all items in the current folder to zip format..." for i in *.7z *.rar *.tar *.gz *.bz2; do base="${i%%.*}" mkdir "$base" && 7z x "$i" -o"$base"/ && zip -r "$base.zip" "$base" && rm -r "$base" done ;; rar) write_information "Recompressing all items in the current folder to rar format..." for i in *.7z *.zip *.tar *.gz *.bz2; do base="${i%%.*}" mkdir "$base" && 7z x "$i" -o"$base"/ && rar a "$base.rar" "$base" && rm -r "$base" done ;; tar) write_information "Recompressing all items in the current folder to tar format..." for i in *.7z *.zip *.rar *.gz *.bz2; do base="${i%%.*}" mkdir "$base" && 7z x "$i" -o"$base"/ && tar cvf "$base.tar" "$base" && rm -r "$base" done ;; gz) write_information "Recompressing all items in the current folder to gzip format..." for i in *.7z *.zip *.rar *.tar *.bz2; do base="${i%%.*}" mkdir "$base" && 7z x "$i" -o"$base"/ && tar cvzf "$base.tar.gz" "$base" && rm -r "$base" done ;; bz2) write_information "Recompressing all items in the current folder to bzip2 format..." for i in *.7z *.zip *.rar *.tar *.gz; do base="${i%%.*}" mkdir "$base" && 7z x "$i" -o"$base"/ && tar cvjf "$base.tar.bz2" "$base" && rm -r "$base" done ;; *) write_error "Unknown format: $tgt_format" return 1 ;; esac } tool::recompress_provided_items() { # Description: Function to recompress specified files to a target compression format. # # Purpose: To recompress a list of files (specified individually or by wildcard patterns) into a specified target format. # This function allows for converting between popular compression formats such as zip, rar, and 7z, optimizing # for different compression needs or compatibility requirements. # # Globals: None explicitly used within the function, but assumes the existence of external logging functions # `write_information` and `write_error` for output messages. # # Arguments: # $1: A list of files or a wildcard pattern specifying the files to be recompressed (e.g., "*.zip" or "file1.zip file2.7z"). # $2: The target compression format (supported formats include "7z", "zip", "rar"). # # Outputs: Informational and error messages regarding the recompression process. # # Returns: # 0: If the recompression process completes successfully for all specified files. # 1: If an unknown target format is specified or if any errors occur during the recompression process. # # Usage: recompress_all_items_here "*.zip" 7z # This command would find all .zip files in the current directory and recompress them into the 7z format. # # recompress_all_items_here "archive.7z" zip # This command would recompress the specified "archive.7z" file into the zip format. # # Note: The function relies on external tools (7z, zip, rar) for compression tasks. Ensure these are installed # and accessible in the system's PATH for the function to work correctly. The function also makes use of # temporary directories for the recompression process, which are cleaned up after use. # # End of documentation dependency::command_exists 7z local files=($1) # The list of files to be recompressed, supports wildcards or specific filenames local tgt_format="$2" # The target format for recompression case $tgt_format in 7z) write_information "Recompressing specified items to 7z format..." for i in "${files[@]}"; do local base="${i%.*}" mkdir "${base}" && 7z x "${i}" -o"${base}/" && 7z a -t7z -m0=lzma2 -mx=5 -mfb=64 -md=64m -ms=on -mhe=on "${base}.7z" "${base}/" && rm -r "${base}" done ;; zip) write_information "Recompressing specified items to zip format..." for i in "${files[@]}"; do local base="${i%.*}" mkdir "${base}" && 7z x "${i}" -o"${base}/" && zip -r "${base}.zip" "${base}/" && rm -r "${base}" done ;; rar) write_information "Recompressing specified items to rar format..." for i in "${files[@]}"; do local base="${i%.*}" mkdir "${base}" && 7z x "${i}" -o"${base}/" && rar a "${base}.rar" "${base}/" && rm -r "${base}" done ;; *) write_error "Unknown format: $tgt_format" return 1 ;; esac } tool::up_2_date() { # Description: Function to simplify updating system completely. At present this function sets the # highest preference to use "pkcon", the command line interface of "Package Kit" since # this is the most consistent across different distrbutions. It may even work on BSD. # Only if "pkcon" is not available, will the function use the identified distro native # software manager: apt, yum, zypper etc. This is to maintain stability since a distro may # alias another distributions command (like SuSE does with apt) to be helpfull. # This is one of the first update functions written and is kept for compatibility. # Globals: # Arguments: None # Outputs: # Returns: 0/1/3 where 1/0 = sucess/fail and 3 = package manager NOT found # Usage: tool::up_2_date # End of documentation security::ensure_admin write_status "Running up2date function:" if hash pkcon 2>/dev/null; then write_status "Using PackageKit to update system." pkcon refresh --noninteractive pkcon update --noninteractive --autoremove elif hash yum 2>/dev/null; then write_status "Using YUM to update system." yum update -y elif hash apt 2>/dev/null; then write_status "Using APT to update system." apt-get update apt-get upgrade -y apt-get autoremove -y elif hash zypper 2>/dev/null; then write_status "Using Zypper to update system." zypper --terse --color --table-style=6 refresh zypper --terse --color --table-style=6 list-updates zypper --terse --color --table-style=6 update -y else write_error "No package manager found. Please update your system manually." return 3 fi } tool::test_iso_boot_media() { # Description: Function to test a created ISO file by booting it in a temporary VM using QEMU. # This function requires one argument or one variable (iso_test) to be set; since it needs # to now what media to test. # # QEMU is a free and open-source emulator and virtualizer that # can perform hardware virtualization. QEMU is a hosted virtual machine monitor: it emulates # the machine's processor through dynamic binary translation and provides a set of different # hardware and device models for the machine, enabling it to run a variety of # guest operating systems. # # Globals: # Arguments: None # Outputs: # Returns: # Usage: # # tool::test_iso_boot_media /path/to/media # OR # iso_test=/path/to/media # tool::test_iso_boot_media # # End of Documentation iso_test="${1}" if [[ -z "${iso_test}" ]]; then dialog::display_error "${FUNCNAME[0]}: requires one argument (ISO file to test). Please read ${_LOGFILE} for more information." return 1 fi if ! dependency::virtualization ; then dialog::display_error "${FUNCNAME[0]}: Virtualization support is not available on this system. Please read ${_LOGFILE} for more information." return 1 fi if [[ ! -f "${iso_test}" ]]; then dialog::display_error "${FUNCNAME[0]}: The provided ISO file ${iso_test} does not exist. Please read ${_LOGFILE} for more information." return 1 fi write_information "SUDO_USER is set to: ${SUDO_USER:-not set}" write_information "USER is set to: ${USER:-not set}" local user_name="${SUDO_USER:-${USER}}" write_information "Determined user name as: ${user_name:-not set}" local home_dir="${HOME}" write_information "Determined home directory as: ${home_dir:-not set}" if [[ -z "${user_name}" ]]; then user_name="$(id -un 2>/dev/null || true)" fi system::log_item "Testing ${iso_test}..." if ($RTD_GUI --title "BOOT Media Testing" --yesno "\n Would you like to test your boot media ${iso_test} to see if it works?" 10 80); then clear write_information "TESTING ${iso_test}." if ! hash qemu-system-x86_64; then software::check_native_package_dependency qemu-system-x86_64; fi : "${bin_qemu_img:=$(type -P qemu-img)}" : "${bin_qemu_system_x86_64:=$(type -P qemu-system-x86_64)}" # Build output paths locally to avoid stale globals bleeding in local put_qcow_file_here_when_done="${home_dir}/Virtual-HDs" local qcow2_file="" write_information "Will attempt to create QCOW2 file in: ${put_qcow_file_here_when_done}" if ! mkdir -p "${put_qcow_file_here_when_done}"; then write_error "Failed to create target directory: ${put_qcow_file_here_when_done}" put_qcow_file_here_when_done="/tmp/${user_name:-rtd}/Virtual-HDs" if ! mkdir -p "${put_qcow_file_here_when_done}"; then write_error "Fallback directory also failed: ${put_qcow_file_here_when_done}" return 1 fi write_information "Using fallback directory: ${put_qcow_file_here_when_done}" fi qcow2_file="${put_qcow_file_here_when_done}/$(basename "$iso_test" .iso).qcow2" system::log_item "executing command: ${bin_qemu_img} create -f qcow2 ${qcow2_file} 10G" [[ -f ${qcow2_file} ]] && rm -f "${qcow2_file}" if ${bin_qemu_img} create -f qcow2 "${qcow2_file}" 10G 2>&1 | tee -a "${_LOGFILE}"; then write_information "Created QCOW2 file: ${qcow2_file}" else write_error "Failed to create QCOW2 file: ${qcow2_file}" fi system::log_item "Executing command: ${bin_qemu_system_x86_64} -smp 2 -enable-kvm -m 4G --cdrom ${iso_test} ${qcow2_file}" local qemu_start_ts qemu_end_ts qemu_runtime rc qemu_start_ts="$(date +%s 2>/dev/null || true)" if ${bin_qemu_system_x86_64} -smp 2 -enable-kvm -m 4G --cdrom "${iso_test}" "${qcow2_file}" 2>&1 | tee -a "${_LOGFILE}"; then rc=0 else rc=$? fi qemu_end_ts="$(date +%s 2>/dev/null || true)" if [[ -n "${qemu_start_ts}" && -n "${qemu_end_ts}" ]]; then qemu_runtime=$((qemu_end_ts - qemu_start_ts)) fi if [[ ${rc:-1} -ne 0 ]]; then write_error "Failed to test ${iso_test} (qemu exit ${rc:-1})." elif [[ -n "${qemu_runtime}" && ${qemu_runtime} -lt 5 ]]; then write_warning "QEMU exited after ${qemu_runtime}s; VM may not have booted properly." else write_information "Successfully tested ${iso_test}." fi else clear dialog::display_notice "User selected No, exit status was $?." system::log_item "User selected No, exit status was $?." fi } ################################################################################################ # ........ .......... ........ # ..';;::::;,'. ..,;::::::;,.. .',;::::;;'.. # .';::::::::::;. .,::::::::::::,. .;::::::::::;.. # .;::::::::::::,. .'::::::::::::::'. .,::::::::::::;. # .:::::::::::::;. .,::::::::::::::,. .;:::::::::::::. # .,::::::::::::'. ..;::::::::::::;.. .,::::::::::::,. # .,;::::::::;'. ..;::::::::::,.. .';:::::::::,. # ..',,;;,,... ..',;;;,,'.. ..,,;;,,'.. # .... ...... .... # # # MMMMMMMMMMMWWWNNNNNXXXXXXXXXXXXNNNNNWWMMMMMMMMMMMM # MMMMMWWNXXXK00OkxddooollllllooodxxkO0KKXXNNWWMMMMM # MWNXXK0kdlc:;,'....................',;:loxk0KXXNWM # NK0xl:,..,c:,;cc,,cc,,,.,;;:c:.';,:c;;'';'';:lxOKN # X0k;....,OdcoOk::kxdxkd;xOkOdxddk;x0;lkkd'....'d0X # X0x'.....lollOk:lkc';xd;x0kOkkddx.ok..kK:......l0K # K0d.....'clkO0x;:kdoxxxokkkOodxxx.ok..ok'......l0K # K0o......:lc;:lc;;cl,'cl;',;',;;;.,;..,;.......lOK # K0o........... ...............................o0K # K0o.........',;::cloolllloooddolc:;;'..........d0K # K0o......:lodxxxxl:,'.......,;ldxxxxxolc,.....,k0X # K0d.....;dxddxxc'. .''''''''. .'cdxdxddkl.....cO0N # X0x,....,dxdxd,. .'''......',,.. 'oxddxx:.....o0KW # NKO:.....oxxd, .,..';:,. ....,'. 'oxdxd'....;k0XM # WK0o.....cxxc. .,. .,dOo. .'..,. .:xxxc.....o0KWM # MX0k;....,dk:. ',. .oxdc.......'' ;xxo'....:k0NMM # MWKOo.....ckc. .,. .:dd;. ...,. .:xx:....'x0XWMM # MMN0Oc....'ox, .,....',......,'. 'dxc.....oOKWMMM # MMWX0k;....,dd,. .'''.......''...,okl.....lOKNMMMM # MMMWK0x,....,dxc'. ..''''''.. .'cdxl'....lOKNMMMMM # MMMMWK0d'....'oxxo:;'......';:ldxxc.....lO0NMMMMMM # MMMMMWK0d'.....cdxxxxddodddxxxdxd:....'oOKNWMMMMMM # MMMMMMWK0x,.....;dxxdddddddxdxxl'....,x0KNMMMMMMMM # MMMMMMMWX0k:.....'cdxdxxxxxxxo;.....ck0XWMMMMMMMMM # MMMMMMMMWX0Ol'.....,ldxdxxxo;.....,oOKNWMMMMMMMMMM # MMMMMMMMMWNKOx;......,lolc;......ck0XNWMMMMMMMMMMM # MMMMMMMMMMMWX0Oo,..............:x0KNWMMMMMMMMMMMMM # MMMMMMMMMMMMWNX0kl'..........;dOKXWMMMMMMMMMMMMMMM # MMMMMMMMMMMMMMWNK0kl'......:dOKXWMMMMMMMMMMMMMMMMM # MMMMMMMMMMMMMMMMWNK0ko;,cox0KNWMMMMMMMMMMMMMMMMMMM # MMMMMMMMMMMMMMMMMWWWXK0OKKXNWMMMMMMMMMMMMMMMMMMMMM # MMMMMMMMMMMMMMMMMMMMMWNNWWMMMMMMMMMMMMMMMMMMMMMMMM # ################################################################################################ # # .d8888b. d8b 888 # d88P Y88b Y8P 888 # Y88b. 888 # "Y888b. .d88b. .d8888b 888 888 888d888 888 888888 888 888 # "Y88b. d8P Y8b d88P" 888 888 888P" 888 888 888 888 # "888 88888888 888 888 888 888 888 888 888 888 # Y88b d88P Y8b. Y88b. Y88b 888 888 888 Y88b. Y88b 888 # "Y8888P" "Y8888 "Y8888P "Y88888 888 888 "Y888 "Y88888 # 888 # Y8b d88P # "Y88P" # ################################################################################################ ssh::ensure_user_ssh_key() { # Description: # Check if the current user has a valid SSH keypair (ed25519 preferred). # If missing, prompt to create one using dialog and generate it if agreed. # # Usage: # ssh::ensure_user_ssh_key # # Returns: # 0 if key exists or was successfully created, 1 if user declined or error occurred. # # Globals: # Uses dialog, ssh-keygen, current user environment # # Dependencies: # dialog, ssh-keygen # # End of Documentation if ! dependency::command_exists dialog ssh-keygen; then write_error "⛔ ERROR: dialog or ssh-keygen not found and could not be installed." return 1 fi local user; user=$(id -un) local home_dir; home_dir=$(eval echo "~$user") local ssh_dir="$home_dir/.ssh" local key_path="$ssh_dir/id_ed25519" local pub_key_path="${key_path}.pub" if [[ -f "$pub_key_path" && -f "$key_path" ]]; then write_status "🔐 SSH key already exists for user '$user': $pub_key_path" return 0 fi dialog --title "🔐 SSH Key Missing" --yesno \ "User '$user' does not have an SSH keypair in: $ssh_dir Would you like to generate a new ed25519 SSH key now?" 12 70 local response=$? if [[ "$response" -ne 0 ]]; then write_warning "⚠️ SSH key creation declined by user." return 1 fi if [[ ! -d "$ssh_dir" ]]; then if ! mkdir -p "$ssh_dir"; then dialog::display_error "⛔ Failed to create $ssh_dir" return 1 fi chmod 700 "$ssh_dir" fi if ! ssh-keygen -t ed25519 -N '' -f "$key_path" -C "$user@$(hostname)"; then dialog::display_error "⛔ Failed to generate SSH key for $user." return 1 fi chown "$user:$user" "$key_path" "$pub_key_path" dialog::display_notice "✅ SSH key successfully generated: $pub_key_path" return 0 } ssh::ensure_key_installed_on_remote() { # Description: # Verifies if the user's SSH public key is installed on a specified remote host. # If not, prompts the user (via dialog) to install it using ssh-copy-id. # # Usage: # ssh::ensure_key_installed_on_remote # # Returns: # 0 if key is installed or was successfully copied, 1 if declined or failed. # # Globals: # Uses dialog, ssh, ssh-copy-id, current user environment # # Dependencies: # dialog, ssh, ssh-copy-id # # End of Documentation local remote_user="$1" local remote_host="$2" if [[ -z "$remote_user" || -z "$remote_host" ]]; then write_error "⛔ ERROR: Missing arguments. Usage: ssh::ensure_key_installed_on_remote " return 1 fi if ! dependency::command_exists ssh ssh-copy-id dialog; then write_error "⛔ ERROR: Required commands (ssh, ssh-copy-id, dialog) are missing or cannot be installed." return 1 fi if ! ssh::ensure_user_ssh_key; then return 1 fi local user; user=$(id -un) local home_dir; home_dir=$(eval echo "~$user") local pub_key for key_type in ed25519 rsa ecdsa; do if [[ -f "$home_dir/.ssh/id_${key_type}.pub" ]]; then pub_key="$home_dir/.ssh/id_${key_type}.pub" break fi done if [[ -z "$pub_key" ]]; then dialog::display_error "⛔ No SSH public key found in $home_dir/.ssh/. Please generate one first." return 1 fi if ssh -o BatchMode=yes -o PasswordAuthentication=no -o ConnectTimeout=5 "${remote_user}@${remote_host}" "exit" &>/dev/null; then write_status "✅ SSH key already installed and working for ${remote_user}@${remote_host}" return 0 fi dialog --title "🔐 SSH Key Not Installed" --yesno \ "The SSH key for user '$user' does not appear to be installed on: ${remote_user}@${remote_host} Would you like to install it now using ssh-copy-id? You may be prompted for the remote password." 12 70 if [[ $? -ne 0 ]]; then write_warning "⚠️ SSH key installation to $remote_host declined by user." return 1 fi if ! ssh-copy-id -i "$pub_key" "${remote_user}@${remote_host}"; then dialog::display_error "⛔ ssh-copy-id failed. Please check network or credentials." return 1 fi if ssh -o BatchMode=yes -o PasswordAuthentication=no -o ConnectTimeout=5 "${remote_user}@${remote_host}" "exit" &>/dev/null; then dialog::display_notice "✅ SSH key successfully installed and verified." return 0 else dialog::display_error "⛔ Key installed but connection still fails. Manual check recommended." return 1 fi } security::encrypt_disk_with_luks() { # Description: # Securely encrypts a block device using LUKS2 with hardened parameters. # Wipes the block device, configures strong encryption, formats it with ext4, and mounts it. # Will prompt for a strong passphrase and validate it. # # Globals: # None # # Arguments: # --device Path to the block device (e.g. /dev/sdX) # --mountpoint Path to mount the encrypted volume (e.g. /mnt/secure) # # Outputs: # Uses write_information, write_status, write_error # # Returns: # 0 if successful, 1 on failure # # Usage: # security::encrypt_disk_with_luks --device /dev/sdX --mountpoint /mnt/secure # # End of Documentation local device= mountpoint= passphrase confirm luks_name while [[ $# -gt 0 ]]; do case "$1" in --device) device="$2"; shift 2 ;; --mountpoint) mountpoint="$2"; shift 2 ;; *) write_error "Unknown argument: $1" return 1 ;; esac done if [[ -z "$device" || -z "$mountpoint" ]]; then write_error "❯ Missing required arguments. Usage: --device --mountpoint " return 1 fi if [[ ! -b "$device" ]]; then write_error "Target device is not a block device: $device" return 1 fi if ! dependency::command_exists --auto cryptsetup mkfs.ext4 dd ; then write_error "Failed to install 'cryptsetup'. Please install it manually." return 1 fi read -rsp "WARNING: This will erase ALL data on $device. Type 'YES' to continue: " confirm; echo if [[ "$confirm" != "YES" ]]; then write_status "Operation aborted by user." return 1 fi read -rsp "Enter a strong passphrase for LUKS encryption: " passphrase; echo if [[ ${#passphrase} -lt 12 ]]; then write_error "Passphrase too short. Minimum length is 12 characters." return 1 fi write_status "Wiping $device with random data. This may take a while..." if ! dd if=/dev/urandom of="$device" bs=1M status=progress; then write_error "Failed to wipe device: $device" return 1 fi write_status "Creating LUKS2 container with hardened configuration..." if ! echo "$passphrase" | cryptsetup luksFormat "$device" \ --type luks2 \ --cipher aes-xts-plain64 \ --key-size 512 \ --hash sha512 \ --iter-time 5000 \ --use-random \ --batch-mode \ --pbkdf argon2id \ --label "SECURE_DISK" \ --verify-passphrase; then write_error "Failed to create LUKS container." return 1 fi luks_name="secure_disk_$(basename "$device")" write_status "Opening encrypted container as $luks_name..." if ! echo "$passphrase" | cryptsetup open "$device" "$luks_name" --key-file=-; then write_error "Failed to open encrypted container." return 1 fi write_status "Formatting encrypted container with ext4 filesystem..." if ! mkfs.ext4 "/dev/mapper/$luks_name"; then cryptsetup close "$luks_name" write_error "Failed to format container." return 1 fi mkdir -p "$mountpoint" || { write_error "Failed to create mount point: $mountpoint" cryptsetup close "$luks_name" return 1 } write_status "Mounting container at $mountpoint..." if ! mount "/dev/mapper/$luks_name" "$mountpoint"; then write_error "Failed to mount container at $mountpoint" cryptsetup close "$luks_name" return 1 fi write_information "✅ Secure LUKS encryption setup complete." write_status "Encrypted device: $device" write_status "Mapped as: /dev/mapper/$luks_name" write_status "Mounted at: $mountpoint" return 0 } disk::reencrypt_device() { # Description: # Securely encrypts an existing unencrypted block device using LUKS. # This will backup the contents, LUKS format the target device, then restore the data. # # Usage: # disk::reencrypt_device --device /dev/sdX --mountpoint /mnt/target --backup-dir /mnt/usb # # Globals: # None # # Returns: # 0 on success, 1 on failure # # End of Documentation local device= mountpoint= backup_dir= luks_name= mapper_path= while [[ $# -gt 0 ]]; do case "$1" in --device) device="$2"; shift 2 ;; --mountpoint) mountpoint="$2"; shift 2 ;; --backup-dir) backup_dir="$2"; shift 2 ;; *) write_error "❌ Unknown option: $1" return 1 ;; esac done if [[ -z "$device" || -z "$mountpoint" || -z "$backup_dir" ]]; then write_error "❌ Missing required arguments: --device, --mountpoint, --backup-dir" return 1 fi if [[ ! -b "$device" ]]; then write_error "❌ Device does not exist or is not a block device: $device" return 1 fi if [[ ! -d "$mountpoint" ]]; then write_error "❌ Mountpoint does not exist: $mountpoint" return 1 fi if [[ ! -d "$backup_dir" || ! -w "$backup_dir" ]]; then write_error "❌ Backup directory is not valid or not writable: $backup_dir" return 1 fi if mount | grep -q "on $backup_dir "; then write_information "✔ Backup directory is mounted: $backup_dir" else write_warning "⚠ Backup directory is not a mountpoint. Make sure it's not on the target device!" fi if mount | grep -q "$device"; then write_error "❌ Device appears to be mounted. Please unmount it first: $device" return 1 fi if ! dependency::command_exists --auto cryptsetup mkfs.ext4 dd tar ; then write_error "Failed to install 'cryptsetup'. Please install it manually." return 1 fi write_information "📁 Backing up data from device $device to $backup_dir..." local label; label="RECRYPT_$(date +%s)" local backup_tar; backup_tar="${backup_dir}/reencrypt_backup_${label}.tar.gz" if ! mkdir -p "$mountpoint"; then write_error "❌ Failed to prepare mountpoint directory: $mountpoint" return 1 fi if ! mount "$device" "$mountpoint"; then write_error "❌ Failed to mount $device to $mountpoint" return 1 fi if ! tar -czpf "$backup_tar" -C "$mountpoint" .; then umount "$mountpoint" write_error "❌ Failed to backup data to $backup_tar" return 1 fi write_information "📦 Backup complete: $backup_tar" write_status "🔓 Preparing to format $device with LUKS encryption..." umount "$mountpoint" read -r -p "This will erase all data on $device. Type YES to continue: " confirm if [[ "$confirm" != "YES" ]]; then write_information "Aborted by user." return 1 fi if ! cryptsetup luksFormat "$device"; then write_error "❌ Failed to LUKS format the device." return 1 fi luks_name="luks_${label}" if ! cryptsetup open "$device" "$luks_name"; then write_error "❌ Failed to open LUKS container." return 1 fi mapper_path="/dev/mapper/$luks_name" if ! mkfs.ext4 "$mapper_path"; then cryptsetup close "$luks_name" write_error "❌ Failed to format LUKS container with ext4" return 1 fi if ! mount "$mapper_path" "$mountpoint"; then cryptsetup close "$luks_name" write_error "❌ Failed to mount encrypted device." return 1 fi write_information "📂 Restoring backup from $backup_tar to $mountpoint..." if ! tar -xzf "$backup_tar" -C "$mountpoint"; then umount "$mountpoint" cryptsetup close "$luks_name" write_error "❌ Failed to restore backup." return 1 fi sync umount "$mountpoint" cryptsetup close "$luks_name" write_status "✅ Re-encryption complete. Data restored securely." write_information "🧾 Encrypted device: $device" write_information "📦 Backup archive retained at: $backup_tar" return 0 } security::enable_firewall() { # Description: # Enables and configures the system firewall using either UFW (Uncomplicated Firewall) or firewalld. # Automatically detects the firewall tool present on the system and applies appropriate rules. # Allows incoming SSH traffic and optionally opens Minecraft-related ports (25565 and 4445), or a custom TCP port. # Prevents accidental SSH lockout by always ensuring SSH access is allowed. # Requires root privileges and supports reentrant invocation to safely add additional ports. # # Globals: # None # # Arguments: # --minecraft, -mc Enables default Minecraft ports (25565, 4445) # --port , -p Specifies a custom port to allow through the firewall # # Outputs: # Informational and error messages sent to stdout/stderr # # Returns: # 0 if the firewall rules were successfully applied # 1 if no firewall tool is found, arguments are invalid, or a command fails # # Usage: # security::enable_firewall --minecraft # security::enable_firewall --port 8080 # security::enable_firewall -mc -p 8080 # End of documentation security::ensure_admin write_status "Running security::enable_firewall function..." case "$1" in --minecraft | -mc) write_status "Using default Minecraft port: 25565 and 4445" local minecraft minecraft=1 shift 2 ;; --port | -p) if [[ "$2" =~ ^[0-9]+$ ]]; then write_status "Using custom port: $2" local port port="$2" shift else write_error "Invalid argument: $2. Must be an integer between 1 and 65535." return 1 fi ;; esac if ! [[ "$port" =~ ^[0-9]+$ ]] || (( port < 1 || port > 65535 )); then write_warning "Invalid port: %s. Must be an integer between 1 and 65535 - $port" return 1 fi write_status "Configuring Firewall..." if command -v ufw >/dev/null 2>&1; then write_status "Using UFW to configure firewall rules..." if ! ufw status | grep -q 'Status: active'; then if ! ufw --force enable; then write_error "Failed to enable UFW. " return 1 fi fi ufw default deny incoming || return 1 ufw default allow outgoing || return 1 # Allow SSH connections always so that you do not get locked out of a remote system ufw allow ssh || return 1 if [[ -n $port ]] ; then ufw allow "$port" || return 1 fi if [[ $minecraft -eq 1 ]]; then ufw allow 25565 || return 1 ufw allow 4445 || return 1 fi ufw reload || return 1 elif command -v firewall-cmd >/dev/null 2>&1; then write_status "Using firewalld to configure firewall rules..." if ! firewall-cmd --state | grep -q running; then write_warning "firewalld is not running." if ! systemctl start firewalld; then write_error "Failed to start firewalld." return 1 fi fi firewall-cmd --set-default-zone=public || return 1 firewall-cmd --permanent --zone=public --set-target=DROP || return 1 if ! firewall-cmd --permanent --zone=public --add-service=ssh; then firewall-cmd --permanent --zone=public --add-port=22/tcp || return 1 write_status "Added SSH service to firewall rules." else write_status "SSH service already exists in firewall rules." fi if [[ -n $port ]]; then firewall-cmd --permanent --zone=public --add-port="${port}"/tcp || return 1 write_status "Added custom port (${port}) to firewall rules." fi if [[ $minecraft -eq 1 ]]; then firewall-cmd --permanent --zone=public --add-port=25565/tcp || return 1 firewall-cmd --permanent --zone=public --add-port=4445/tcp || return 1 write_status "Added Minecraft ports to firewall rules." fi if ! firewall-cmd --reload ; then write_error "Failed to reload firewalld." return 1 fi firewall-cmd --list-all || return 1 write_status "Firewall rules applied successfully." else write_error "No recognized firewall tool (ufw or firewalld) found on the system." write_error "Please configure the firewall and SSH access manually." return 1 fi } security::check_if_password_pOwned() { # Description: # Function to query a user for a password suggestion and check it against P0wned data base online. # Globals: $RTD_GUI # Arguments: None # Outputs: dialog box # Returns: # Usage: security::check_if_password_pOwned # End of documentation for i in curl sha1sum grep tr cut dialog; do if ! command -v $i >/dev/null 2>&1; then write_status "Checking for dependency: $i" dependency::command_exists --auto $i if [[ $? -ne 0 ]]; then write_error "Missing dependency: $i. Please install it to use this function." return 1 fi else write_status "Dependency $i found." fi done pass_str=$($RTD_GUI --passwordbox "Please enter your intended password to validate your new password against P0wned DB" 8 78 --title "" 3>&1 1>&2 2>&3) result=$( sha1=$(echo -n $pass_str | tr -d '\n' | sha1sum) echo "Hash prefix: ${sha1:0:5}" echo "Hash suffix: ${sha1:5:35}" query_result=$(curl https://api.pwnedpasswords.com/range/${sha1:0:5} 2>/dev/null | grep "$(echo "${sha1:5:35}" | tr '[:lower:]' '[:upper:]')") write_status "Your password appeared %d times for sale in the Darknet per the P0wned online database...." "${query_result#*:}" 2>/dev/null unset pass_str ) dialog::display_result "Password P0wned Status" } security::scan_for_malware() { # Description: # Scans a given directory for malware using ClamAV and moves infected files to a quarantine directory. # Ensures ClamAV is installed, validates input, creates necessary directories, and logs the scan. # # Globals: # None # # Arguments: # $@ - One or more directory paths to scan # # Outputs: # STDOUT: Status messages and scan results # STDERR: Errors and warnings # # Returns: # 0 if scan completed successfully, 1 on error # # Usage: # security::scan_for_malware /path/to/dir1 [/path/to/dir2 ...] # # End of documentation local quarantinedir logfile scandirs valid_scandirs dir; quarantinedir="$HOME/VIRUS" logfile="$HOME/clamscan.log" scandirs=("$@") valid_scandirs=() if [[ ${#scandirs[@]} -eq 0 ]]; then printf "Error: No directories specified for scanning.\n" >&2 return 1 fi for dir in "${scandirs[@]}"; do if [[ -d "$dir" && -r "$dir" ]]; then valid_scandirs+=("$dir") else printf "Warning: Skipping invalid or unreadable directory: %s\n" "$dir" >&2 fi done if [[ ${#valid_scandirs[@]} -eq 0 ]]; then printf "Error: No valid directories to scan.\n" >&2 return 1 fi if ! command -v clamscan >/dev/null 2>&1; then if ! software::check_native_package_dependency clamscan; then printf "Error: 'clamscan' not available and could not be installed.\n" >&2 return 1 fi fi mkdir -p "$quarantinedir" || { printf "Error: Failed to create quarantine directory: %s\n" "$quarantinedir" >&2 return 1 } write_host --purple " ___________________________________________________ [ Setting ] [ Value ] Logging to: $logfile Quarantine in: $quarantinedir ___________________________________________________ Starting scan.... Scanning folder(s): ${valid_scandirs[*]}" if ! clamscan "${valid_scandirs[@]}" -r -a -i \ -l "$logfile" \ --bytecode=yes \ --stdout \ --phishing-sigs \ --bell \ --algorithmic-detection=yes \ --move="$quarantinedir"; then printf "Malware scan encountered errors. Check the log at %s for details.\n" "$logfile" >&2 return 1 fi } security::change_disk_pass() { # Description: Function to change the passphrase fo an encrypted storage device. # It will offer to change the first encrypted volume found by blockid. # It is unclear how this will work if there are multiple encrypted volumes found. # The function does not expect an argument. It will prompt for a password. # # Globals: # Arguments: None accepted # Outputs: Interactive # Returns: default exit status of the last command run. # Usage: change_disk_pass # # # End of Documentation security::ensure_admin clear write_host --red "Changing the disk encryption password for your hard drive:" cryptsetup luksChangeKey "$(blkid | grep crypto_LUKS | cut -d : -f 1)" read -p "Press [ ENTER ] to continue:" } security::harden_ubuntu() { # Description: # Presents an interactive dialog with checkboxes to select hardening functions for Ubuntu. # Executes only the selected security functions. Uses in-memory command substitution, no temp file. # # Globals: # None # # Arguments: # None # # Outputs: # STDOUT: Status messages # STDERR: Error messages # # Returns: # 0 if all selected functions succeed, 1 otherwise # # Usage: # security::harden_ubuntu # # End of documentation security::ensure_admin local menu_output func status local -a failed_functions # Map option numbers to hardening functions declare -A FUNC_MAP=( [1]=security::enable_firewall [2]=security::install_clamav [3]=security::install_rkhunter [4]=security::install_auditd [5]=security::sysctl_hardening [6]=security::secure_password_policy [7]=security::install_fail2ban ) # Capture output from dialog directly into a variable using command substitution menu_output=$(dialog --stdout --checklist "Select hardening steps to apply:" 20 70 10 \ 1 "Enable Firewall" on \ 2 "Install ClamAV" on \ 3 "Install RKHunter" on \ 4 "Install Auditd" on \ 5 "Apply Sysctl Hardening" on \ 6 "Secure Password Policy" on \ 7 "Install Fail2Ban" on) if [[ $? -ne 0 ]]; then printf "User cancelled selection.\n" >&2 return 1 fi write_information "[INFO] Starting selected Ubuntu hardening tasks..." for status in $menu_output; do status=${status//\"/} func=${FUNC_MAP["$status"]} if [[ -n "$func" ]]; then write_information "[INFO] Executing: $func" if ! "$func"; then printf "Error: Function %s failed.\n" "$func" >&2 failed_functions+=("$func") fi fi done if [[ ${#failed_functions[@]} -gt 0 ]]; then printf "\n[ERROR] The following functions failed:\n" >&2 for func in "${failed_functions[@]}"; do printf " - %s\n" "$func" >&2 done return 1 fi write_information "[INFO] Ubuntu system hardened successfully." } security::configure_clamav() { # Description: # Installs and securely configures ClamAV only if not already installed. # Uses cross-distro detection via software::is_native_package_installed. # Ensures freshclam auto-updates, configures clamd, enables and starts services. # # Globals: # None # # Arguments: # None # # Outputs: # STDOUT: Status messages # STDERR: Errors and failure notifications # # Returns: # 0 if successful, 1 on error # # Usage: # security::configure_clamav # # End of documentation local clamav_conf freshclam_conf clamd_conf clamav_conf="/etc/clamav" freshclam_conf="$clamav_conf/freshclam.conf" clamd_conf="$clamav_conf/clamd.conf" security::ensure_admin # Check if ClamAV is installed and install if not software::check_native_package_dependency clamav software::check_native_package_dependency clamav-daemon software::check_native_package_dependency clamav-freshclam write_information "[INFO] Stopping ClamAV services for configuration..." systemctl stop clamav-freshclam.service clamav-daemon.service >/dev/null 2>&1 write_information "[INFO] Configuring freshclam..." if [[ -f "$freshclam_conf" ]]; then sed -i 's/^Example/#Example/' "$freshclam_conf" || { write_warning "Error: Failed to modify $freshclam_conf" } fi write_information "[INFO] Updating virus definitions..." if ! freshclam; then write_warning "Failed to run freshclam update" fi write_information "[INFO] Configuring clamd..." if [[ -f "$clamd_conf" ]]; then sed -i 's/^Example/#Example/' "$clamd_conf" sed -i 's/^#LogFile /LogFile /' "$clamd_conf" sed -i 's|^#LogFile /var/log/clamav/clamd.log|LogFile /var/log/clamav/clamd.log|' "$clamd_conf" sed -i 's/^#LogTime/LogTime/' "$clamd_conf" sed -i 's/^#LogFileUnlock/LogFileUnlock/' "$clamd_conf" sed -i 's/^#LogFileMaxSize/LogFileMaxSize/' "$clamd_conf" sed -i 's/^#TCPSocket /TCPSocket /' "$clamd_conf" sed -i 's/^#TCPAddr /TCPAddr /' "$clamd_conf" fi write_information "[INFO] Enabling and starting ClamAV services..." if ! systemctl enable clamav-freshclam.service clamav-daemon.service; then write_warning "Failed to enable ClamAV services." else write_information "[INFO] ClamAV services enabled." fi if ! systemctl start clamav-freshclam.service clamav-daemon.service; then write_warning "Failed to start ClamAV services." else write_information "[INFO] ClamAV services started." fi if ! systemctl is-active --quiet clamav-daemon; then write_warning "ClamAV daemon is not running properly." else write_information "[INFO] ClamAV daemon is running." fi write_status "[OK] ClamAV installed, configured, and running securely." } security::configure_fail2ban() { # Description: # Installs and configures Fail2Ban with a secure default jail for SSH protection. # Ensures the service is enabled and running, and applies hardened configuration. # # Globals: # None # # Arguments: # None # # Outputs: # STDOUT: Installation and configuration status # STDERR: Errors and failure messages # # Returns: # 0 if successful, 1 on failure # # Usage: # security::install_fail2ban # # End of documentation security::ensure_admin local jaildir jailconf write_information "[INFO] Checking if fail2ban is installed..." if ! software::check_native_package_dependency fail2ban ; then write_error "Error: Fail2Ban is not installed. Please install it first." return 1 fi if ! jaildir=$(find /etc -type d -name "fail2ban" -print -quit 2>/dev/null); then write_error "Could not locate the Fail2Ban configuration directory using 'find'." return 1 fi if [[ -z "$jaildir" || ! -d "$jaildir" ]]; then write_error "Fail2Ban configuration directory not found in /etc." return 1 fi if ! jailconf=$(find "$jaildir" -maxdepth 1 -type f -name "jail.local" -print -quit 2>/dev/null); then write_warning "Could not locate jail.local inside $jaildir, assuming it does not exist yet." jailconf="${jaildir}/jail.local" fi if [[ -z "$jailconf" ]]; then write_warning "jail.local not found; will attempt to create one in: $jaildir" jailconf="${jaildir}/jail.local" touch "$jailconf" || { write_error "Error: Failed to create jail.local file." return 1 } chown root:root "$jailconf" || { write_error "Error: Failed to set ownership for jail.local file." return 1 } fi write_status "Fail2Ban jail.local path: $jailconf" write_information "[INFO] Creating hardened SSH jail configuration for Fail2Ban..." if ! sudo tee "$jailconf" >/dev/null <<-EOF [DEFAULT] bantime = 1h findtime = 10m maxretry = 4 backend = systemd usedns = no banaction = iptables-multiport ignoreip = 127.0.0.1/8 [sshd] enabled = true port = ssh logpath = %(sshd_log)s EOF then write_error "Error: Failed to write jail.local configuration." return 1 fi write_information "[INFO] Enabling and restarting Fail2Ban..." if ! sudo systemctl enable fail2ban; then write_error "Error: Failed to enable Fail2Ban." return 1 fi if ! sudo systemctl restart fail2ban; then write_error "Error: Failed to restart Fail2Ban." return 1 fi if ! sudo fail2ban-client status sshd >/dev/null 2>&1; then write_error "Error: SSH jail is not active. Check configuration in $jailconf" return 1 fi write_status "[OK] Fail2Ban installed and configured with SSH jail." } security::configure_rkhunter() { # Description: Function to install and configure RKHunter (Rootkit Hunter) for system security. # RKHunter is a popular open-source security tool that scans for rootkits, backdoors, and other # security vulnerabilities on the system. This function installs RKHunter and updates its database # to ensure the latest security checks are available. # # Globals: # - None # # Arguments: # - None # # Outputs: # - Status messages indicating the progress of the installation and configuration. # # Returns: # - Exits with status 1 if the RKHunter installation or configuration fails. # # Usage: # - security::configure_rkhunter # # End of documentation security::ensure_admin local rkh_conf # Ensure that RKHunter is installed write_information "[INFO] Checking if RKHunter is installed and adding if not...]" if ! software::check_native_package_dependency rkhunter; then write_error "RKHunter is not installed. Please install it first." return 1 fi # Locate the RKHunter configuration file if ! rkh_conf=$(find /etc /usr/local/etc -type f -name "rkhunter.conf" -print -quit 2>/dev/null); then write_error "Failed to locate rkhunter.conf using 'find'." return 1 fi # Double check if the file exists and is a regular file if [[ -z "$rkh_conf" || ! -f "$rkh_conf" ]]; then write_error "Could not find a valid rkhunter.conf configuration file." return 1 else write_information "[INFO] Found RKHunter configuration file at: $rkh_conf" fi # Update the configuration file to allow remote database updates write_information "[INFO] Ensuring remote database updates are allowed..." sudo sed -i 's/^UPDATE_MIRRORS=0/UPDATE_MIRRORS=1/' "$rkh_conf" sudo sed -i 's/^MIRRORS_MODE=0/MIRRORS_MODE=1/' "$rkh_conf" sudo sed -i 's/^WEB_CMD=".*"/WEB_CMD="wget"/' "$rkh_conf" # Update RKHunter database write_information "[INFO] Updating RKHunter database..." for attempt in {1..3}; do if rkhunter --update --nocolors; then write_status "RKHunter database updated successfully." break else write_warning "RKHunter update failed. Retrying ($attempt/3)..." sleep 5 fi done if ! rkhunter --update --nocolors; then write_error "RKHunter failed to update after multiple attempts." return 1 fi write_information "[INFO] Updating RKHunter file properties database..." if ! rkhunter --propupd --nocolors --sk; then write_error "Failed to update RKHunter file properties database." return 1 fi write_status "RKHunter installation and configuration completed successfully." } security::configure_auditd() { # Description: Function to install and configure auditd for system auditing. # auditd is a Linux security tool that provides system auditing capabilities to track # security-relevant events on the system. This function installs auditd and configures # basic auditing rules to monitor system activity. # # Globals: # - None # # Arguments: # - None # # Outputs: # - Status messages indicating the progress of the installation and configuration. # # Returns: # - Exits with status 1 if the auditd installation or configuration fails. # # Usage: # - security::configure_auditd # # End of documentation security::ensure_admin # Check (ensure) if auditd is installed and install if not write_information "[INFO] Checking if auditd is installed..." software::check_native_package_dependency auditd write_information "[INFO] Enabling auditd service..." systemctl enable auditd write_information "[INFO] Starting auditd service..." systemctl start auditd security::configure_auditd::ensure_audit_rules_dir() { local rules_dir="/etc/audit/rules.d" if [[ ! -d "$rules_dir" ]]; then write_warning "Audit rules directory not found at $rules_dir. Creating..." if ! mkdir -p "$rules_dir"; then write_error "Failed to create audit rules directory at $rules_dir" return 1 fi fi printf '%s\n' "$rules_dir" } local audit_dir if ! audit_dir=$(security::configure_auditd::ensure_audit_rules_dir); then write_error "Audit rules directory could not be validated or created." return 1 fi write_status "Writing hardening rules to: $audit_dir/hardening.rules" cat <<-EOF | tee "$audit_dir/hardening.rules" >/dev/null -w /etc/passwd -p wa -k passwd_changes -w /etc/group -p wa -k group_changes -w /etc/shadow -p wa -k shadow_changes EOF write_information "Reloading audit rules..." if command -v augenrules >/dev/null 2>&1; then augenrules --load || write_warning "augenrules --load failed" elif command -v auditctl >/dev/null 2>&1; then auditctl -R "$audit_dir/hardening.rules" || write_warning "auditctl load failed" else write_warning "No audit rule loader (augenrules or auditctl) found." fi write_information "[INFO] auditd installation and configuration completed successfully." } security::sysctl_hardening() { # Description: # Applies hardened sysctl settings to improve system security. # Validates sysctl presence, configuration directory, and applies settings cleanly. # # Globals: # None # # Arguments: # None # # Outputs: # STDOUT/STDERR via logging functions # # Returns: # 0 if settings applied successfully, 1 on failure # # Usage: # security::sysctl_hardening # # End of documentation local sysctl_bin sysctl_dir sysctl_file sysctl_bin=$(command -v sysctl 2>/dev/null) if [[ -z "$sysctl_bin" ]]; then write_error "sysctl command not found. Cannot apply kernel hardening settings." return 1 fi sysctl_dir="/etc/sysctl.d" sysctl_file="$sysctl_dir/99-hardening.conf" if [[ ! -d "$sysctl_dir" ]]; then write_warning "Sysctl configuration directory not found. Attempting to create: $sysctl_dir" if ! sudo mkdir -p "$sysctl_dir"; then write_error "Failed to create sysctl configuration directory: $sysctl_dir" return 1 fi fi write_information "[INFO] Writing hardened sysctl rules to: $sysctl_file" if ! sudo tee "$sysctl_file" >/dev/null <<-EOF # Harden network parameters net.ipv4.ip_forward = 0 net.ipv4.conf.all.send_redirects = 0 net.ipv4.conf.default.send_redirects = 0 net.ipv4.conf.all.accept_redirects = 0 net.ipv4.conf.default.accept_redirects = 0 net.ipv4.conf.all.accept_source_route = 0 net.ipv4.conf.default.accept_source_route = 0 net.ipv6.conf.all.accept_ra = 0 net.ipv6.conf.default.accept_ra = 0 net.ipv6.conf.all.accept_redirects = 0 net.ipv6.conf.default.accept_redirects = 0 net.ipv6.conf.all.disable_ipv6 = 1 net.ipv6.conf.default.disable_ipv6 = 1 # Harden memory management vm.swappiness = 10 vm.dirty_ratio = 60 vm.dirty_background_ratio = 2 vm.vfs_cache_pressure = 50 vm.mmap_min_addr = 4096 # Harden core dumps fs.suid_dumpable = 0 fs.protected_hardlinks = 1 fs.protected_symlinks = 1 # Harden kernel security kernel.exec-shield = 1 kernel.randomize_va_space = 2 kernel.kptr_restrict = 1 kernel.dmesg_restrict = 1 kernel.yama.ptrace_scope = 1 kernel.unprivileged_bpf_disabled = 1 EOF then write_error "Failed to write sysctl configuration file: $sysctl_file" return 1 fi write_information "[INFO] Reloading sysctl settings from all configuration sources..." if ! sudo "$sysctl_bin" --system; then write_error "Failed to apply sysctl settings using: $sysctl_bin --system" return 1 fi write_status "Sysctl hardening settings applied successfully." } security::secure_password_policy() { # Description: Function to enforce a secure password policy on the system. # This function configures the system to enforce a secure password policy that # requires users to create strong passwords with specific characteristics such # as length, complexity, and expiration. It sets password policy rules to enhance # system security and protect user accounts from unauthorized access. # # Globals: # - None # # Arguments: # - None # # Outputs: # - Status messages indicating the progress of the password policy configuration. # # Returns: # - Exits with status 1 if the password policy configuration fails. # # Usage: # - security::secure_password_policy # # End of documentation security::ensure_admin write_information "[INFO] Configuring secure password policy..." cat <<-EOF > /etc/security/pwquality.conf minlen = 14 dcredit = -1 ucredit = -1 ocredit = -1 lcredit = -1 minclass = 4 minrepeat = 3 maxrepeat = 3 maxsequence = 3 gecoscheck = 1 difok = 8 EOF cat <<-EOF > /etc/pam.d/common-password password requisite pam_pwquality.so retry=3 password requisite pam_cracklib.so retry=3 difok=8 minlen=14 ucredit=-1 lcredit=-1 dcredit=-1 ocredit=-1 minclass=4 minrepeat=3 maxrepeat=3 maxsequence=3 gecoscheck=1 password [success=1 default=ignore] pam_unix.so obscure sha512 remember=5 password requisite pam_deny.so password required pam_permit.so EOF write_information "[INFO] Password policy configured successfully." } security::install_AIDE() { # Description: Function to install and configure AIDE (Advanced Intrusion Detection Environment). # AIDE is a host-based intrusion detection system that scans the system for file integrity # and security vulnerabilities. This function installs AIDE and initializes the database # to monitor system files and directories for changes. # # Globals: # - None # # Arguments: # - None # # Outputs: # - Status messages indicating the progress of the AIDE installation and configuration. # # Returns: # - Exits with status 1 if the AIDE installation or configuration fails. # # Usage: # - security::install_AIDE # # End of documentation security::ensure_admin write_information "[INFO] Checking if AIDE is installed and adding if not..." if ! software::check_native_package_dependency aide ; then write_error "Unable to install AIDE, AIDE is not installed. Please install it first." return 1 fi write_information "[INFO] Initializing AIDE database..." if ! aideinit; then write_error "[ERROR] Failed to initialize AIDE database. Exiting..." return 1 else write_information "[INFO] AIDE database initialized successfully." mv /var/lib/aide/aide.db.new /var/lib/aide/aide.db write_information "[INFO] AIDE database moved to /var/lib/aide/aide.db." fi write_information "[INFO] AIDE installation and configuration completed successfully." } security::ensure_admin() { # Description: Ensures the script is executed with administrative (root) privileges, allowing # it to perform system-wide settings or configurations that require elevated access. # # The function intelligently selects the most appropriate method to elevate privileges based # on the environment: # - In graphical environments (X11 or Wayland), it uses `pkexec` to provide a graphical # authentication prompt for privilege escalation. # - In non-graphical environments (TTY or terminal), it tries `dialog` for a text-based # password prompt; if `dialog` is unavailable, it falls back to using plain `sudo`. # # This approach ensures a user-friendly escalation process that respects the context # (GUI vs. CLI) and minimizes disruption by providing suitable prompts. # # Globals: # - $EUID: The effective user ID of the current user. Used to check if the script is already # running as root. # - $DISPLAY: The X display identifier. Indicates if the script is running in an X11 session. # - $WAYLAND_DISPLAY: The Wayland display identifier. Indicates if the script is running in # a Wayland session. # - $XAUTHORITY: Path to the X authentication file, necessary for access to the X server. # - $DBUS_SESSION_BUS_ADDRESS: D-Bus session address, required for `pkexec` authentication. # - $SUDO_USER: The username of the original user who invoked the script, maintained when # relaunching the script with elevated privileges. # # Arguments: # - None. The function expects no arguments but will prompt for the system password if needed. # # Outputs: # - GUI or CLI password prompts for privilege escalation. # - Logs to the system or console indicating the detected environment and the actions taken. # # Dependencies: # - Soft dependency on `dialog` (optional). Falls back to CLI if `dialog` is not installed. # - `pkexec` for graphical privilege escalation. # - `sudo` for CLI-based privilege escalation. # # Returns: # - Relaunches the script with elevated privileges or confirms root access if already achieved. # # Usage: # - security::ensure_admin # - security::ensure_admin || exit 1 # # End of documentation # Check if we are already root if [ "$EUID" -eq 0 ]; then system::log_item "✅ Administrative access confirmed for: 🪪 ${SUDO_USER:-"❓ unknown user"} as requested by ${FUNCNAME[1]} ..." return fi local _ensure_admin_should_exit=1 [[ $- == *i* ]] && _ensure_admin_should_exit=0 # Determine if we are in a graphical session if [ -n "$XDG_SESSION_TYPE" ] && [ "$XDG_SESSION_TYPE" = "wayland" ]; then system::log_item "👌 A Wayland session is running." graphical_session=true elif [ -n "$DISPLAY" ]; then system::log_item "👌 An X session is running." graphical_session=true else system::log_item "👌 No graphical session detected." graphical_session=false fi # Secret (undocumented) options for testing case $1 in --auto) system::log_item "🔐 Authenticating using automatic determination of metod..." ;; --pexec | --gtk ) system::log_item "🔐 Authenticating using 'pkexec'..." graphical_session=true shift ;; --dialog | --tty) system::log_item "🔐 Authenticating using 'dialog'..." graphical_session=false shift ;; --sudo) system::log_item "🔐 Authenticating using 'sudo'..." write_warning "🔐 This script needs administrative access..." local preserve_env_list="DISPLAY,DBUS_SESSION_BUS_ADDRESS,XAUTHORITY,XDG_RUNTIME_DIR,WAYLAND_DISPLAY" shift # When sourced interactively, $0 is usually the bash binary; re-running that path as a script # causes "cannot execute binary file". In that case, just run the requested command with sudo. if [[ "$(basename "$0")" == "bash" ]]; then sudo --preserve-env=${preserve_env_list} -H env SUDO_USER="$USER" "$@" || pause::for_input 1 "Thre was an error running the requested command with sudo." else sudo --preserve-env=${preserve_env_list} -H env SUDO_USER="$USER" bash "$(readlink -f "$0")" "$@" || pause::for_input 1 "There was an error running the requested command with sudo." fi local rc=$? if (( _ensure_admin_should_exit )); then exit $rc else return $rc fi ;; esac # Attempt to elevate privileges if [ "$graphical_session" = true ]; then system::log_item "🔐 Using pkexec for privilege escalation..." local pkexec_env=(env DISPLAY="$DISPLAY") [[ -n "${XAUTHORITY:-}" ]] && pkexec_env+=("XAUTHORITY=$XAUTHORITY") [[ -n "${DBUS_SESSION_BUS_ADDRESS:-}" ]] && pkexec_env+=("DBUS_SESSION_BUS_ADDRESS=$DBUS_SESSION_BUS_ADDRESS") [[ -n "${XDG_RUNTIME_DIR:-}" ]] && pkexec_env+=("XDG_RUNTIME_DIR=$XDG_RUNTIME_DIR") [[ -n "${WAYLAND_DISPLAY:-}" ]] && pkexec_env+=("WAYLAND_DISPLAY=$WAYLAND_DISPLAY") pkexec_env+=("SUDO_USER=$USER") # If sourced, $0 is the bash binary; re-running it as a script fails. Run the requested # command directly under pkexec in that case. if [[ "$(basename "$0")" == "bash" ]]; then pkexec "${pkexec_env[@]}" "$@" || pause::for_input 1 else pkexec "${pkexec_env[@]}" bash "$(readlink -f "$0")" "$@" || pause::for_input 1 fi local rc=$? if (( _ensure_admin_should_exit )); then exit $rc else return $rc fi else # Use dialog if available, else fallback to sudo # sudo --preserve-env=DISPLAY,DBUS_SESSION_BUS_ADDRESS,XAUTHORITY,XDG_RUNTIME_DIR -H bash "$(readlink -f "$0")" "$@" if command -v dialog &>/dev/null; then token=$(dialog --title "${Title:-"sudo"}" --backtitle "Authentication Required ${BackTitle:-"$(basename "$0")"}" --insecure --stdout --passwordbox "\n This functionality requires elevated privileges. \n Please provide your (sudo) password" 10 90) ret=$? clear local rc=0 case $ret in 0) system::log_item "🔐 Authenticating using dialog..." export HISTIGNORE='*sudo -S*' echo "${token}" | sudo -S -v || security::ensure_admin local preserve_env_list="DISPLAY,DBUS_SESSION_BUS_ADDRESS,XAUTHORITY,XDG_RUNTIME_DIR,WAYLAND_DISPLAY" sudo --preserve-env=${preserve_env_list} -H env SUDO_USER="$USER" bash "$(readlink -f "$0")" "$@" || pause::for_input 1 rc=$? ;; 1) echo "Request cancelled" ; rc=1 ;; 255) echo "[esc] Request aborted" ; rc=255 ;; *) rc=1 ;; esac if (( _ensure_admin_should_exit )); then exit $rc else return $rc fi else system::log_item "🔐 No dialog installed: authenticating using plain sudo..." write_warning "🔐 This script needs administrative access..." local preserve_env_list="DISPLAY,DBUS_SESSION_BUS_ADDRESS,XAUTHORITY,XDG_RUNTIME_DIR,WAYLAND_DISPLAY" sudo --preserve-env=${preserve_env_list} -H env SUDO_USER="$USER" bash "$(readlink -f "$0")" "$@" || pause::for_input 1 local rc=$? if (( _ensure_admin_should_exit )); then exit $rc else return $rc fi fi fi } ########################################################################################### # # .88888888:. # 88888888.88888. # .8888888888888888. # 888888888888888888 # 88' _`88'_ `88888 # 88 88 88 88 88888 # 88_88_::_88_:88888 # 88:::,::,:::::8888 # 88`:::::::::'`8888 # .88 `::::' 8:88. # 8888 `8:888. # .8888' `888888. # .8888:.. .::. ...:'8888888:. # .8888.' :' `'::`88:88888 # .8888 ' `.888:8888. # 888:8 . 888:88888 # .888:88 .: 888:88888: # 8888888. :: 88:888888 # `.::.888. :: .88888888 # .::::::.888. :: :::`8888'.:. # ::::::::::.888 ' .:::::::::::: # ::::::::::::.8 ' .:8::::::::::::. # .::::::::::::::. .:888::::::::::::: # :::::::::::::::88:.__..:88888:::::::::::' # `'.:::::::::::88888888888.88:::::::::' # `':::_:' -- '' -'-' `':_::::'` ########################################################################################### # .d8888b. .d888 888 # d88P Y88b d88P" 888 # Y88b. 888 888 # "Y888b. .d88b. 888888 888888 888 888 888 8888b. 888d888 .d88b. # "Y88b. d88""88b 888 888 888 888 888 "88b 888P" d8P Y8b # "888 888 888 888 888 888 888 888 .d888888 888 88888888 # Y88b d88P Y88..88P 888 Y88b. Y88b 888 d88P 888 888 888 Y8b. # "Y8888P" "Y88P" 888 "Y888 "Y8888888P" "Y888888 888 "Y8888 # 888b d888 888 # 8888b d8888 888 # 88888b.d88888 888 # 888Y88888P888 8888b. 88888b. 8888b. .d88b. 88888b.d88b. .d88b. 88888b. 888888 # 888 Y888P 888 "88b 888 "88b "88b d88P"88b 888 "888 "88b d8P Y8b 888 "88b 888 # 888 Y8P 888 .d888888 888 888 .d888888 888 888 888 888 888 88888888 888 888 888 # 888 " 888 888 888 888 888 888 888 Y88b 888 888 888 888 Y8b. 888 888 Y88b. # 888 888 "Y888888 888 888 "Y888888 "Y88888 888 888 888 "Y8888 888 888 "Y888 # 888 # Y8b d88P # "Y88P" ########################################################################################### software::package_kit::add_package() { # Description: Function to simplify the installation of software by including all display and # installation logic for a given software provider. This function should work consistently # on any distribution with the back end: # Advanced Packaging Tool (APT) # Conary # libdnf[10] & librepo,[11] the libraries upon which DNF, (the successor to yum) builds # Entropy # Opkg # pacman # PiSi # Portage # Smart Package Manager # urpmi # YUM # ZYpp # PackageKit is a free and open-source suite of software applications designed to provide a # consistent and high-level front end for a number of different package management systems. # PackageKit was created by Richard Hughes in 2007,[2][3] and first introduced into an # operating system as a default application in May 2008 with the release of Fedora 9.[4] # # The suite is cross-platform, though it is primarily targeted at Linux distributions which # follow the interoperability standards set out by the freedesktop.org group. It uses the # software libraries provided by the D-Bus and Polkit projects to handle inter-process # communication and privilege negotiation respectively. # Arguments: package_name # Outputs: # Returns: Does not return error as it is designed for best effort smooth operation. # Usage: Call the function with one arguments matching then name of the package you need to install. # # End of documentation security::ensure_admin : ${_LOGFILE:="/var/log/rtd/$(basename "$0".log)"} if pkcon resolve "${1}" --filter installed | grep "${1}" &>/dev/null; then write_status "The software package ${1} is Allready installed... " system::log_item "The software package ${1} is Allready installed... " else if pkcon resolve "${1}" | grep "${1}" &>/dev/null; then write_status "The software package ${1} found in repository, installing... " system::log_item "The software package ${1} found in repository, installing... " pkcon install "${1}" --noninteractive --cache-age 5000 if [ $? = 0 ]; then write_status "The software package ${1} transaction appears to have been sucessful..." system::log_item "The software package ${1} transaction appears to have been sucessful..." else write_warning "There appears to have been some problem installing the package ${1}..." system::log_item "There appears to have been some problem installing the package ${1}..." fi else write_error "The software package ${1} is not available in any configured repository..." system::log_item "The software package ${1} is not available in any configured repository..." fi fi } software::package_kit::remove_package() { # Description: Function to simplify the installation of software by including all display and # installation logic for a given software provider. This should be expanded to include flatpak # snap for example. It should be the same consistent way no matter flavor of Linux. # This function uses the "$_INSTCMD" to install software. "$_INSTCMD" is defined separatley # in the "software::set_install_command" function (a dependency to this function) and could # be an RPM or deb string, or even snap or flatpak. A preference order between package system and # snap or flatpak use. # End of documentation security::ensure_admin : ${_LOGFILE:="/var/log/rtd/$(basename "$0".log)"} ( if pkcon resolve "${1}" --filter installed | grep "${1}" &>/dev/null; then write_status " ✓ The software package ${1} is installed, removing as requested... " pkcon remove "${1}" --noninteractive if [ $? = 0 ]; then write_status "The software package ${1} transaction appears to have been sucessful..." else write_warning "There appears to have been some problem removing the package ${1}..." fi else write_information "The software package ${1} is not installed, no need for action..." fi ) | tee -a ${_LOGFILE} } software::list_bundles() { # Description: Function to list all software bundles defined in _rtd_recipes that are either installable or removable. # Globals: # Arguments: none or [--zformat] # Outputs: Standard out, list of functions either formatted for zenity or not. Output may be used to populate a zenity menu or dialog. # Returns: # Usage: software::list_bundles [] [--zformat-installable | --zformat-removable] # End of documentation local true_false Description completed_bundles_list : "${_CONFIG_DIR:=${HOME}/.config/${_TLA:-"rtd"}}" : "${completed_bundles_list:=${_CONFIG_DIR}/completed-bundles.info}" if [[ ! -f ${completed_bundles_list} ]]; then mkdir -p "${completed_bundles_list%/*}" touch "${completed_bundles_list}" else if [[ -s ${completed_bundles_list} ]]; then local _dedupe_tmp if _dedupe_tmp=$(mktemp 2>/dev/null); then if awk 'NF && !seen[$0]++ {print}' "${completed_bundles_list}" >"${_dedupe_tmp}"; then mv "${_dedupe_tmp}" "${completed_bundles_list}" else rm -f "${_dedupe_tmp}" fi fi fi fi case "$1" in --zformat-installable) # List software bundles for use with zenity for _bundle in $(declare -F | awk '{print $NF}' | sort | egrep -v "^_" | grep "${bundle_prefix}" | grep -v "Single_Install"); do # Build each line to be displayed in the bundle installer... true_false="$(grep -A 2 "$_bundle ()" "${_OEM_DIR:-"/opt/rtd"}/core/${_rtd_recipies_info}" )" true_false="$(echo $true_false | grep "DEFAULT=TRUE" &>/dev/null && echo "true" || echo "false")" Description="$(grep -A 1 "$_bundle ()" "${_OEM_DIR:-"/opt/rtd"}/core/${_rtd_recipies_info}" | tail -1)" Description="${Description/"# "/": "}" Description="${Description/" "/""}" grep -Fxq "${_bundle/recipe:_/}" "${completed_bundles_list}" || echo -en "${true_false}\n ${_bundle/recipe:_/}\n ${Description}\n" done ;; --zformat-removable) # List software bundles for use with zenity while IFS= read -r _installed_bundle; do [[ -z "${_installed_bundle}" ]] && continue # Build each line to be displayed in the bundle installer... Description=$(grep -A 1 "recipe:_$_installed_bundle ()" "${_OEM_DIR:-"/opt/rtd"}/core/${_rtd_recipies_info}" | tail -1) Description="${Description/"# "/": "}" Description="${Description/" "/""}" echo -e "false\n ${_installed_bundle}\n ${Description}\n" done <"${completed_bundles_list}" ;; *) # Default list format i=1 for index in $(declare -F | awk '{print $NF}' | sort | egrep -v "^_" | grep ${bundle_prefix:-"recipe_"} | grep -v "Single_Install"); do echo "$index #$i" ((i = i + 1)) done ;; esac } software::ensure_gnome_software_store_available() { # Description: Function to ensure that flatpak is installed and flathup enabled. # Globals: none # Dependencies: [distro pakcage managment] [function: software::check_native_package_dependency] [Internet access] # Arguments: None # Outputs: # Returns: Standard return codes 1/0 # Usage: Call the function with no arguments # GNOME Software is a utility for installing applications and updates on Linux. # It is part of the GNOME Core Applications, and was introduced in GNOME 3.10.[3] # # It is the GNOME front-end to the PackageKit, in turn a front-end to several package management # systems, which include systems based on both RPM and DEB. # # The program is used to add and manage software repositories as well as Ubuntu # Personal Package Archives (PPA). Ubuntu replaced its previous Ubuntu Software Center # program with GNOME Software starting with Ubuntu 16.04 LTS,[5] and re-branded it as "Ubuntu Software". # # It also supports fwupd for servicing of system firmware.[6] # # GNOME Software removed Snap support in July 2019, due to code quality issues, lack of integration # (specifically, the user can't tell what snap is doing after they click "install" and that it # generally ignores GNOME's settings), and the fact that it competes with the GNOME-supported Flatpak standard.[7] # # End of documentation case "$DESKTOP_SESSION" in plasma) # If we find any plasma session then install the plasma store addons... write_information "${FUNCNAME[0]}: plasma on anything..". software::check_native_package_dependency plasma-discover software::check_native_package_dependency plasma-discover-backend-flatpak software::check_native_package_dependency plasma-discover-backend-snap ;; "/usr/share/xsessions/default") # Special case for suse again... if cat /usr/share/xsessions/default.desktop | grep "Plasma"; then write_information "${FUNCNAME[0]}: kde on suse" software::check_native_package_dependency plasma-discover software::check_native_package_dependency discover6-backend-flatpak software::check_native_package_dependency plasma-discover-backend-snapd else write_information "${FUNCNAME[0]}: gnome on suse" software::check_native_package_dependency gnome-software software::check_native_package_dependency discover6-backend-flatpak software::check_native_package_dependency gnome-software-plugin-snapd fi ;; *) # in any other case, install the gnome versions of store and plugins... write_information "${FUNCNAME[0]}: case = *" software::check_native_package_dependency gnome-software software::check_native_package_dependency gnome-software-plugin-flatpak software::check_native_package_dependency gnome-software-plugin-snap ;; esac } software::ensure_flatpak_package_managment() { # Description: Function to ensure that flatpak is installed and flathup enabled. # Globals: none # Dependencies: [distro pakcage managment] [function: software::check_native_package_dependency] [Internet access] # Arguments: None # Outputs: # Returns: Standard return codes 1/0 # Usage: Call the function with no arguments # NOTE: https://www.fosslinux.com/42410/snap-vs-flatpak-vs-appimage-know-the-differences-which-is-better.htm # Like Snap, Flatpak is another distribution independent package format aimed to simplify overall # app distribution and usage in Linux systems. Previously known as xdg-app, the framework was based # on the concept of running applications in a secure virtual sandbox without requiring root privileges # or posing a security threat to the system. # Flatpak was officially released in 2015 with a reliable backup from Red Hat, Endless Computers, and # Collabora. It targeted primarily three Desktop Environments. That is FreeDesktop, KDE, and GNOME. The # Linux distributions currently having this framework are arch Linux, Debian, Fedora, Mageia, # Solus, and Ubuntu.The Flatpak framework itself is developed in C programming and released under the # LGPL license. The lead developer is Alexander Larsson – a Red Hat employee. Like Snapcraft for Snap, # Flatpak also has the Flathub app store where users can find and install all Flatpak packages. # Initially, Flathub only allowed open-source publishing applications on the website but # has recently approved the publishing of proprietary apps. # Additionally, unlike Snap, where we have a single repository controlled by Canonical to install and update # software packages, Flatpak supports the use of multiple repos. The one significant disadvantage with this # package is the lack of support for Servers. # # * --- PREFFERRED Software install (Security + Control) ---- * # # CAPABILITY TABLE: # +----------------------------------------------------+-------------+-------------+--------------+ # | Features | Snap | Flatpak | AppImage | # +----------------------------------------------------+-------------+-------------+--------------+ # | Permission Controls Toggles | Yes | Yes | No | # | Sandboxing Support | Yes | Yes | Yes | # | Sandboxing Mandatory | Yes | Yes | No | # | App Portability | Yes | Yes | No | # | Native Theme Support | Yes (caveat)| Yes (caveat)| Yes (caveat) | # | Support for Bundled Libraries | Yes | Yes | Yes | # | Fully Contained Single Executable Support | No | No | Yes | # | Online App Store | Yes | Yes | Yes | # | Multi-version Parallel Apps Support | Yes | Yes | Yes | # | Automatic Updates | Yes | Yes | Yes (caveat) | # | Support for Chrome OS (through Crostini containers)| Yes | Yes | Yes | # | App Size | higher than | higher than | Lowest | # | Number of Applications Available in the App Store | Highest | Lowest | Somewhere in | # | Plugins for Desktop App Store Software | Yes | Yes | No | # +----------------------------------------------------+-------------+-------------+--------------+ # End of documentation if [[ $(echo "$OSTYPE" | grep "linux") ]] &>/dev/null; then # these tests focus on the latest versions of the distros evaluated... write_information "Ensuring flatpak software distribution system is available..." if hash flatpak &>/dev/null; then write_status "flatpak command is available... " if flatpak remotes | grep flathub &>/dev/null; then write_status "flathub remote is present..." export PATH=$PATH:/var/lib/flatpak/exports/bin else write_warning "flathub remote is not present... adding now... " security::ensure_admin flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo || write_error "Failed to add flatpak repo!" export PATH=$PATH:/var/lib/flatpak/exports/bin fi if flatpak list | grep com.github.tchx84.Flatseal &>/dev/null; then write_status "flatseal security management is also present..." else security::ensure_admin write_status "flatseal security manager not found, adding now..." flatpak install flathub com.github.tchx84.Flatseal --noninteractive -y fi else security::ensure_admin write_information "Ensuring Flatpak software distribution system is available..." if software::check_native_package_dependency flatpak; then flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo || write_error "Failed to add flatpak repo!" export PATH=$PATH:/var/lib/flatpak/exports/bin else write_error "Failed to satisfy dependecy flatpak!" fi if flatpak list | grep com.github.tchx84.Flatseal &>/dev/null; then write_status "flatseal security management is also present..." else security::ensure_admin write_status "flatseal security manager not found, adding now..." flatpak install flathub com.github.tchx84.Flatseal --noninteractive -y fi return $? fi elif [[ "$OSTYPE" == "darwin"* ]]; then write_error "${FUNCNAME[0]}: Mac OSX is currently not supported with snap..." return 1 elif [[ "$OSTYPE" == "cygwin" ]]; then write_error "${FUNCNAME[0]}: CYGWIN is currently unsupported with snap..." return 1 elif [[ "$OSTYPE" == "msys" ]]; then write_error "${FUNCNAME[0]}: Lightweight shell is currently unsupported with snap... " return 1 elif [[ "$OSTYPE" == "freebsd"* ]]; then write_error "${FUNCNAME[0]}: Free BSD is currently unsupported with snap... " return 1 else echo "I have no Idea what this system is" return 1 fi } software::update_system_txt() { # Description: # System update function. The purpose of this function is to update installed software from # distribution reopositories and the newer self contained universal software stores like # snap and flatpak with normal text only default output. # Function does not expect any argument. However it will respect one argument \"simple\" # The simple argument instructs this functions [pkcon] action to display simple output # rather than more user friendly output. # Update native software packages; since it is not known what distribution # this script is being executed on, it is best to check and see if we are able # to perform an update and then do the update... # One could choose to check for a supported exact version: e.g. Ubuntu, but we would # like to be nice and add value for as many as we can. Specially if it is this easy! :) # Globals: # Arguments: None # Outputs: # Returns: # Usage: software::update_system_txt # End of documentation security::ensure_admin PUBLICATION="$BRANDING Update Manager for Linux" VERSION="1.14 (built in)" DIALOGRC="~/.config/rtd/dialogrc" _LOGFILE="${_LOGFILE:-$0.log}" write_status "${PUBLICATION}: Version ${VERSION}" write_status "------------------------------------------------------------------" write_host --cyan "I am updating software from all channels I can find on the system." echo -e " \n" ( if hash pkcon &>/dev/null; then pkcon -y refresh pkcon -y update --autoremove pkcon -y update --autoremove elif hash dnf &>/dev/null; then dnf -y upgrade elif hash zypper 2>/dev/null; then zypper up -y zypper dup -y --auto-agree-with-licenses --allow-vendor-change elif hash apt &>/dev/null; then apt-get update UPGRADABLE=$(LANG=C apt-get upgrade -s | grep -P '^\d+ upgraded' | cut -d" " -f1) if [ "$UPGRADABLE" -eq 0 ]; then write_information "📦 $UPGRADABLE packages that need updates; Skipping update task... " else write_information "📦 There are $UPGRADABLE packages that need updates..." dpkg --configure -a apt-get upgrade -y apt-get full-upgrade -y apt-get --purge autoremove -y apt-get upgrade -y apt-get full-upgrade -y apt-get --purge autoremove -y apt-get clean fi else write_error "❌ This system does not seem to have a software managment system" return 1 fi ) #| tee -a "$_LOGFILE" write_status "updating snaps if snap is present on the system..." if hash snap 2>/dev/null; then write_status "Updating user installed snaps... " snap refresh else write_warning "--- snap software is not present on this system... skipping..." fi write_status "updating flatpaks if flatpak is present on system..." if hash flatpak 2>/dev/null; then write_status "Updating user installed flatpaks... " flatpak update --user -y write_status "Updating system installed flatpaks... " flatpak update --system -y else write_warning "--- flatpak software is not present on this system... skipping..." fi write_information "All updates have been processed.\n" echo " 📦 NATIVE PACKAGES: $( TODAY=$(date +%Y-%m-%d) if hash apt; then zgrep "^${TODAY}.* upgrade " /var/log/dpkg.log /var/log/dpkg.log.1 elif hash dnf; then sudo zgrep "^${TODAY}.* Upgraded: " /var/log/dnf.log* /var/log/dnf.rpm.log* elif hash zypper; then cat /var/log/zypp/history | grep "patch" |grep "${TODAY}" elif hash pkcon; then pkcon get-updates fi ) 📦 SNAPS: $(snap changes) 📦 FLATPAKS: $( flatpak history --system --since=1d flatpak history --user --since=1d ) 📦 FLATPAKS (USER PRIVATE): $( flatpak history --system --since=1d flatpak history --user --since=1d )" | tee -a "$LOGFILE" } software::update_all_ui() { security::ensure_admin local TERMUITXT="nocolor" local PUBLICATION="$BRANDING Update Manager for Linux" local VERSION="1.15 (built in with dialog)" local DIALOGRC="~/.config/rtd/dialogrc" local _LOGFILE="${_LOGFILE:-$0.log}" local dialog_ui=${RTD_GUI:-"dialog --erase-on-exit --colors "} Native_Software_Packages() { ( if hash zypper 2>/dev/null; then zypper up -y write_information "📦 Update task complete." elif hash apt 2>/dev/null; then apt-get update UPGRADABLE=$(LANG=C apt-get upgrade -s | grep -P '^\d+ upgraded' | cut -d" " -f1) if [ "$UPGRADABLE" -eq 0 ]; then write_information "📦 $UPGRADABLE packages that need updates; Skipping update task... " else write_information "📦 There are $UPGRADABLE packages that need updates..." dpkg --configure -a apt-get upgrade -y apt-get full-upgrade -y apt-get --purge autoremove -y apt-get clean fi write_information "📦 Update task complete." elif hash dnf 2>/dev/null; then dnf -y upgrade write_information "📦 Update task complete." elif hash pkcon 2>/dev/null; then pkcon -y refresh pkcon get-updates pkcon -y update --autoremove write_information "📦 Update task complete." else write_error "❌ This system does not seem to have a software managment system" return 1 fi ) | tee -a "$_LOGFILE" | $dialog_ui --backtitle "\Zb$PUBLICATION $VERSION\ZB" --progressbox "Updating all natively installed software..." 25 120 } Snap_Apps() { ( write_status "updating snaps if snap is present on the system..." if hash snap 2>/dev/null; then snap refresh write_information "Update task complete." else write_warning "!snap software is not present on this system... skipping..." fi ) | tee -a "$_LOGFILE" | $dialog_ui --backtitle "\Zb$PUBLICATION $VERSION\ZB" --progressbox "Updating all Contained Snap Apps..." 25 120 } Flatpak_Apps() { ( write_status "updating flatpaks if flatpak is present on system..." if hash flatpak 2>/dev/null; then flatpak update --user --noninteractive -y flatpak update --system --noninteractive -y write_information "Update task complete." else write_warning "!flatpak software is not present on this system... skipping..." fi ) | tee -a "$_LOGFILE" | $dialog_ui --backtitle "\Zb$PUBLICATION $VERSION\ZB" --progressbox "Updating all Contained Flatpaks Apps..." 25 120 } while true; do exec 3>&1 local _proglist=""Native_Software_Packages" 1 "on" "Snap_Apps" 2 "on" "Flatpak_Apps" 3 "on"" _run=$($dialog_ui --ok-label Update --cancel-label QUIT --backtitle "\Zb$PUBLICATION $VERSION\ZB" --checklist "Select what to update:" 20 80 10 ${_proglist} 2>&1 1>&3) exit_status=$? exec 3>&- case $exit_status in 1) clear && echo [Quit] && return ;; 255) clear && echo [ESC] && exit ;; esac case $_run in $_run) for i in $_run; do $i done $dialog_ui --backtitle "\Zb$PUBLICATION $VERSION\ZB" \ --title "Updates Complete" \ --msgbox " All updates have been processed. NATIVE PACKAGES: $( if hash apt; then cat /var/log/dpkg.log | grep "\ upgrade\ " elif hash dnf; then cat /var/log/dnf.log | grep "\ upgrade\ " elif hash zypper; then cat /var/log/zypp/history | grep "\ upgrade\ " elif hash pkcon; then pkcon get-updates fi ) SNAPS: $(snap changes) FLATPAKS: $( flatpak history --system --since=1h flatpak history --user --since=1h ) " \ 20 120 ;; *) write_warning "No update actions were requested." exit ;; esac done } software::from_flathub.org() { # Description: Function to simplify and streamline the installation and removal of flatpaks. # Globals: none # Dependencies: [distro pakage managment] [function: software::check_native_package_dependency] [Internet access] # Arguments: # -- Default action is to add software title matching passed parameter. # -- An optioin "--remove" may be passed to atempt removal of the software title. # # Outputs: # Returns: Standard return codes 1/0 # Usage: software::from_flathub.org [ title-name | title-name --remove ] # # # NOTE: https://www.fosslinux.com/42410/snap-vs-flatpak-vs-appimage-know-the-differences-which-is-better.htm # Like Snap, Flatpak is another distribution independent package format aimed to simplify overall # app distribution and usage in Linux systems. Previously known as xdg-app, the framework was based # on the concept of running applications in a secure virtual sandbox without requiring root privileges # or posing a security threat to the system. # # * --- PREFFERRED Software install for GUI apps ---- * # # CAPABILITY TABLE: # +----------------------------------------------------+-------------+-------------+--------------+ # | Features | Snap | Flatpak | AppImage | # +----------------------------------------------------+-------------+-------------+--------------+ # | Permission Controls Toggles | Yes | Yes | No | # | Sandboxing Support | Yes | Yes | Yes | # | Sandboxing Mandatory | Yes | Yes | No | # | App Portability | Yes | Yes | No | # | Native Theme Support | Yes (caveat)| Yes (caveat)| Yes (caveat) | # | Support for Bundled Libraries | Yes | Yes | Yes | # | Fully Contained Single Executable Support | No | No | Yes | # | Online App Store | Yes | Yes | Yes | # | Multi-version Parallel Apps Support | Yes | Yes | Yes | # | Automatic Updates | Yes | Yes | Yes (caveat) | # | Support for Chrome OS (through Crostini containers)| Yes | Yes | Yes | # | App Size | higher than | higher than | Lowest | # | Number of Applications Available in the App Store | Highest | Lowest | Somewhere in | # | Plugins for Desktop App Store Software | Yes | Yes | No | # +----------------------------------------------------+-------------+-------------+--------------+ # End of documentation local app_name="" local remove_flag="" local flatpak_temp_dir="" software::from_flathub.org::_cleanup_tmp() { if [[ -n "$flatpak_temp_dir" && -d "$flatpak_temp_dir" ]]; then rm -rf "$flatpak_temp_dir" system::log_item "Cleaned up Flatpak temp directory: $flatpak_temp_dir" flatpak_temp_dir="" fi } software::from_flathub.org::_flatpak_cmd() { local status if [[ -z "$flatpak_temp_dir" ]]; then flatpak_temp_dir=$(mktemp -d /tmp/rtd-flatpak.XXXXXX 2>/dev/null) || flatpak_temp_dir="" if [[ -n "$flatpak_temp_dir" ]]; then system::log_item "Using Flatpak temp directory: $flatpak_temp_dir" else system::log_item "Unable to create dedicated Flatpak temp directory; falling back to default TMPDIR." fi fi if [[ -n "$flatpak_temp_dir" ]]; then TMPDIR="$flatpak_temp_dir" flatpak "$@" status=$? else flatpak "$@" status=$? fi software::from_flathub.org::_cleanup_tmp return $status } # Parse command line arguments while [[ $# -gt 0 ]]; do case "$1" in --remove) remove_flag="--remove" shift ;; *) if [[ -z "$app_name" ]]; then app_name="$1" else write_error "Error: Invalid parameter '$1'" write_information "Usage: software::from_flathub.org [app_name] [--remove]" return 1 fi shift ;; esac done software::ensure_flatpak_package_managment if [[ -z "$app_name" ]]; then write_error "Error: App name is missing." write_information "Usage: software::from_flathub.org [app_name] [--remove]" return 1 fi if [[ "$remove_flag" == "--remove" ]]; then system::log_item "Request to remove Flatpak app '$app_name'" if software::from_flathub.org::_flatpak_cmd uninstall -y "$app_name"; then system::log_item "Flatpak app '$app_name' has been uninstalled." else system::log_item "Error: Failed to uninstall Flatpak app '$app_name'" return 1 fi else system::log_item "Request to install Flatpak app '$app_name'" if flatpak info "$app_name" >/dev/null 2>&1; then write_information "Flatpak app '$app_name' is already installed." else if software::from_flathub.org::_flatpak_cmd install -y "$app_name"; then system::log_item "Flatpak app '$app_name' has been installed." else system::log_item "Error: Failed to install Flatpak app '$app_name'" return 1 fi fi fi } software::from_snapcraft.io() { # Description: Function to ensure that snap is installed. # Globals: none # Dependencies: [distro pakcage managment] [function: software::check_native_package_dependency] [Internet access] # Arguments: None # Outputs: stdout # Returns: Standard return codes 1/0 # Usage: Call the function with no arguments # NOTE: # https://www.fosslinux.com/42410/snap-vs-flatpak-vs-appimage-know-the-differences-which-is-better.htm # Snap is a distribution independent package format developed by Canonical and first released # in 2014. It was initially developed for Ubuntu but has been adopted by other Linux distributions # like Arch, Linux Mint, CentOS, Gentoo, and Fedora, and also included support for the Snap # framework. The main aim behind this package format development was to come up with a # single unified format for software packages to run in a wide range of devices. # That includes IoT (IoT), embedded devices running Ubuntu Core (a minimalistic # version of Ubuntu), and computer systems that ran some Ubuntu version. # Snap also offers an online app store – Snapcraft, where users can find and # install the software packages. It creates a large pool where users can find all # available a snap package. Snapcraft is itself controlled and maintained by the # Canonical team. # # # * --- PREFFERRED Software install for CLI apps ---- * # # CAPABILITY TABLE: # +----------------------------------------------------+-------------+-------------+--------------+ # | Features | Snap | Flatpak | AppImage | # +----------------------------------------------------+-------------+-------------+--------------+ # | Permission Controls Toggles | Yes | Yes | No | # | Sandboxing Support | Yes | Yes | Yes | # | Sandboxing Mandatory | Yes | Yes | No | # | App Portability | Yes | Yes | No | # | Native Theme Support | Yes (caveat)| Yes (caveat)| Yes (caveat) | # | Support for Bundled Libraries | Yes | Yes | Yes | # | Fully Contained Single Executable Support | No | No | Yes | # | Online App Store | Yes | Yes | Yes | # | Multi-version Parallel Apps Support | Yes | Yes | Yes | # | Automatic Updates | Yes | Yes | Yes (caveat) | # | Support for Chrome OS (through Crostini containers)| Yes | Yes | Yes | # | App Size | higher than | higher than | Lowest | # | Number of Applications Available in the App Store | Highest | Lowest | Somewhere in | # | Plugins for Desktop App Store Software | Yes | Yes | No | # +----------------------------------------------------+-------------+-------------+--------------+ # End of documentation local app_name="" local remove_flag="" local requires_classic="" local snap_temp_dir="" software::from_snapcraft.io::_cleanup_tmp() { if [[ -n "$snap_temp_dir" && -d "$snap_temp_dir" ]]; then rm -rf "$snap_temp_dir" system::log_item "Cleaned up Snap temp directory: $snap_temp_dir" snap_temp_dir="" fi } software::from_snapcraft.io::_install_snap() { local status if [[ -z "$snap_temp_dir" ]]; then snap_temp_dir=$(mktemp -d /tmp/rtd-snap-install.XXXXXX 2>/dev/null) || snap_temp_dir="" if [[ -n "$snap_temp_dir" ]]; then system::log_item "Using Snap temp directory: $snap_temp_dir" else system::log_item "Unable to create dedicated Snap temp directory; falling back to default TMPDIR." fi fi if [[ -n "$snap_temp_dir" ]]; then if snap list "$app_name" >/dev/null 2>&1; then write_status "$app_name is allready installed" return 0 fi TMPDIR="$snap_temp_dir" SNAP_DOWNLOAD_DIR="$snap_temp_dir" snap install "$@" status=$? else if snap list "$app_name" >/dev/null 2>&1; then write_status "$app_name is allready installed" return 0 fi snap install "$@" status=$? fi software::from_snapcraft.io::_cleanup_tmp return $status } # Parse command line arguments while [[ $# -gt 0 ]]; do case "$1" in --remove) remove_flag="--remove" shift ;; *) if [[ -z "$app_name" ]]; then app_name="$1" else write_error "Error: Invalid parameter '$1'" write_information "Usage: install_or_remove_snap_app [app_name] [--remove]" return 1 fi shift continue # Skip the remaining part of the loop ;; esac done if [[ -z "$app_name" ]]; then write_error "Error: App name is missing." write_information "Usage: ${FUNCNAME[0]} [app_name] [--remove]" return 1 fi if ! hash snap &>/dev/null; then write_information "Snap package management system is not present; installing now..." software::ensure_snap_package_managment || { write_error "Failed to install snapd; cannot proceed with snap app management." return 1 } system::log_item "Snap package management system has been installed." fi if [[ "$remove_flag" == "--remove" ]]; then system::log_item "Request to remove Snap app '$app_name'" if snap remove "$app_name"; then system::log_item "Snap app '$app_name' has been removed." else system::log_item "Error: Failed to remove Snap app '$app_name'" fi else system::log_item "Request to install Snap app '$app_name'" if snap list "$app_name" >/dev/null 2>&1; then system::log_item "Snap app '$app_name' is already installed." else requires_classic=$(snap info "$app_name" | grep "latest/stable" | grep -o " classic") if [[ -n "$requires_classic" ]]; then system::log_item "The app '$app_name' requires classic confinement (less confined)..." if software::from_snapcraft.io::_install_snap --classic "$app_name"; then system::log_item "Snap app '$app_name' has been installed." else system::log_item "Error: Failed to install Snap app '$app_name'" fi else if software::from_snapcraft.io::_install_snap "$app_name"; then system::log_item "Snap app '$app_name' has been installed." else system::log_item "Error: Failed to install Snap app '$app_name'" fi fi fi fi } software::add_software_task() { # Description: Display executed task and echo ON/NOK based on sucess # This function is created to reduce the terminal output and create a more poliched output of # the tasks executed. This function has no idea shat it is doing, it will simply call the function # it was asked to call or execute the command ist was asked to, and echo OK or FAIL based on the # return code. # Globals: # Arguments: function or command to execute # Outputs: # Returns: # Usage: software::add_software_task function_name # End of documentation PAD="---------------------------------------------------------" LINE=$(printf "%s %s" "$@" "$PAD" | cut -c 1-${#PAD}) #display_spinner start echo -ne "Task: ${LINE/recipe_/}:" & ${*} &>>${_LOGFILE:-"/var/log/rtd/${FUNCNAME[0]}$(date +%Y-%m-%d-%H)-oem-setup.log"} && printf "%b%-6s%b [ \xE2\x9C\x94 ] $GREEN OK!$ENDCOLOR \n" || printf "%b%-6s%b [ ! ] ${RED} FAILED $ENDCOLOR \n" #display_spinner stop } software::ensure_restricted_codecs() { # Description: simple function to ensure that restricted codecs are available on the system. # Globals: expects zypper, dnf, or apt available. # Arguments: none # Outputs: # Returns: # Usage: software::ensure_restricted_codecs # End of documentation write_status "Install all the required multimedia codecs..." for i in libdvdcss2 ffmpeg; do software::check_native_package_dependency $i || write_error "Failed to install restricted codecs..." done } software::vendor_download_and_install() { # Description: Function by Nate Beaken to ease and make consistent the downloading of the non repository # packages and install them. It can install any package that Package Kit can manage (available on most Linux dists) # It expects one parameter: the complete URL to download from. # Globals: # Arguments: URL to rpm or deb file and possibly some other formats of distributions that use package kit. # Outputs: # Depends: # - Functions; write_error, software::check_native_package_dependency # - Software: wget, pkcon # Returns: 0/1 # Usage: # software::vendor_download_and_install https://download.teamviewer.com/download/linux/teamviewer_amd64.deb # software::vendor_download_and_install https://download.teamviewer.com/download/linux/teamviewer.x86_64.rpm # End of documentation download_url="$1" fetch_file() { wget -P ${_OEM_DIR:-"/opt/rtd"}/cache "${download_url}"; } echo "${FUNCNAME[0]}: getting package ${download_url}..." if hash wget; then fetch_file elif software::check_native_package_dependency wget; then fetch_file else write_error "Unable to download requested file: ${download_url}..." write_error "Please check if wget is available since this is used to download files..." write_error "You may also want to check that the internet is reachable from this computer?" return 1 fi echo "${FUNCNAME[0]}: Installing package $(basename ${download_url}) ..." if hash pkcon; then pkcon install-local -y ${_OEM_DIR:-"/opt/rtd"}/cache/"$(basename "${download_url}")" return $? else software::check_native_package_dependency packagekit-tools pkcon install-local -y ${_OEM_DIR:-"/opt/rtd"}/cache/"$(basename "${download_url}")" return $? fi } software::is_native_package_available() { # Description: Function to chek if a package is available in whetever repository, and # if there return a "0" and if not return a "1" or a "3" if there is no recognizable # package manager found. the function expects the name of the package # to be a parameter passed to this fuction. Call this function by: # software::is_native_package_installed "package name" # or # software::is_native_package_installed "$1" if calling this function from a script # or another function receiving a parameter. # # Supported base distributions: Fedora . SuSE . Debian # End of documentation if hash yum 2>/dev/null; then yum list available "$1" &>/dev/null && return 0 || return 1 elif hash zypper 2>/dev/null; then if zypper se "${1}" | grep "${1}" &>/dev/null; then return 0; else return 1; fi elif hash apt 2>/dev/null; then apt -qq list "$1" 2>/dev/null | grep -q "$1" && return 0 || return 1 else return 3 fi } InstallSoftwareFromRepo() { # Description: Function to simplify the installation of software by including all display and # installation logic for a given software provider. This should be expanded to include flatpak # snap for example. It should be the same consistent way no matter flavor of Linux. # This function uses the "$_INSTCMD" to install software. "$_INSTCMD" is defined separatley # in the "software::set_install_command" function (a dependency to this function) and could # be an RPM or deb string, or even snap or flatpak. A preference order between package system and # snap or flatpak use. # End of documentation local PAD LINE PRE _return software::set_install_command &>>"${_LOGFILE}" PAD="------------------------------------------------" LINE=$(printf "%s %s" "$@" "$PAD" | cut -c 1-${#PAD}) PRE=" - Installing $LINE :" display_message() { local message="$1" color="$2" symbol="$3" printf "%b%-6s%b [ $symbol ] $color $message $ENDCOLOR \n" } if software::is_native_package_installed "${1}"; then display_message "Already installed..." "$YELLOW" "\xE2\x9C\x94" return 1 elif software::is_native_package_available "${1}"; then if printf "${PRE}" & software::add_native_package "${1}" &>>"${_LOGFILE}" then display_message "OK!" "$GREEN" "\xE2\x9C\x94" return 0 else display_message "FAILED" "$RED" "!" return 1 fi else display_message "Not available..." "$YELLOW" "\xE2\x9C\x94" return 1 fi } software::determine_package_name_from_cmd() { # Description: Function to determine the package name from the command line. # This function is used to determine the package name from the command line # and is used in the software::add_native_package function. # Globals: # Arguments: None # Outputs: # Returns: # Usage: software::determine_package_name_from_cmd # End of documentation local cmd="$1" local pkg_name distro logfile if [[ -z "$cmd" ]]; then write_error "No command provided to determine package name." return 1 fi distro=$(system::distribution_type) logfile=$(system::determine_logfile) system::log_item "Checking for command-not-found package so that package name may be determined..." if ! command -v command_not_found_handle ; then write_warning "command_not_found_handle not found. Attempting to install command-not-found package..." software::add_native_package command-not-found || { write_error "Unable to install command-not-found package, unable to determine package name from command" write_error "Please determine the name of the required package to satify the $cmd dependency, and use the software::add_native_package to install the missing package instead" return 1 } fi case "$distro" in "ubuntu" | "debian") pkg_name="$(command_not_found_handle "$cmd" 2>&1 | grep -i "apt" | awk '{print $3}' | head -n1)" &>>$logfile ;; "fedora" | "centos" | "rhel") pkg_name="$(command_not_found_handle "$cmd" 2>&1 | grep -i "dnf" | awk '{print $3}' | head -n1)" &>>$logfile ;; "suse") pkg_name="$(command_not_found_handle "$cmd" 2>&1 | grep -i "zypper" | awk '{print $3}' | head -n1)" &>>$logfile ;; *) write_error "Unsupported distribution: $distro" return 1 ;; esac system::log_item "Package name for command '$cmd' is '$pkg_name'" echo "$pkg_name" } software::add_native_package() { # Description: # Installs one or more native packages individually using the system's package manager. # Supports apt, dnf, zypper, and pkcon. Logs any failures while allowing others to proceed. # This is a global install command for portability and convenience. This function # allows for scripts to add software across many systems. # Primarily for Linux: this function will look for the common package managers # apt (all debina and ubuntu based distributions), dnf (RedHat, Fedora, Mandriva, and others) # zypper (SuSE and Open SUSE) or alternatively pkcon (PackageKit) which is a wrapper for # the package manager. # # Globals: # None # # Arguments: # $@ - One or more package names to install # # Outputs: # STDOUT: Installation status messages # STDERR: Detailed errors # # Returns: # 0 if all packages are installed successfully, 1 if any package failed # # Usage: # software::add_native_package curl git htop # End of documentation security::ensure_admin local pkgmgr install_cmd local -a raw_packages packages filtered_packages failed_packages local pkg mapfile -t raw_packages < <(printf '%s\n' "$@" | xargs -n1 | grep -Ev '^\s*$') system::log_item "Called by ${FUNCNAME[0]} with arguments: ${raw_packages[*]}" system::log_item "Filtering already installed native package(s): ${raw_packages[*]}" dependency::os_linux || { write_error "This function is only supported on Linux systems." return 1 } if command -v zypper >/dev/null 2>&1; then pkgmgr="ZYPPER" install_cmd="sudo zypper install -y" elif command -v apt-get >/dev/null 2>&1; then pkgmgr="APT" install_cmd="sudo apt-get -y install" elif command -v dnf >/dev/null 2>&1; then pkgmgr="DNF" install_cmd="sudo dnf -y install" elif command -v pkcon >/dev/null 2>&1; then pkgmgr="PKCON" install_cmd="pkcon install -y" else write_error "No supported package manager found on this system." return 3 fi write_status "Detected package manager: ${pkgmgr}" if [[ ${#raw_packages[@]} -eq 0 ]]; then write_error "No package names provided for installation." return 1 fi for pkg in "${raw_packages[@]}"; do if [[ -z "$pkg" ]]; then continue fi if software::is_native_package_installed "$pkg"; then write_information "Package '$pkg' is already installed. Skipping." continue fi filtered_packages+=("$pkg") done if [[ ${#filtered_packages[@]} -eq 0 ]]; then write_information "All packages already installed. Nothing to do." return 0 fi for pkg in "${filtered_packages[@]}"; do if [[ -z "$pkg" ]]; then continue fi if ! software::is_native_package_available "$pkg"; then write_error "Package '$pkg' is not available in the repositories." failed_packages+=("$pkg") continue fi write_status "📦 Installing: ${pkg} using ${pkgmgr}" if ! ${install_cmd} "$pkg"; then write_error "❌ Failed to install: ${pkg}" failed_packages+=("$pkg") continue fi write_information "✅ Successfully installed: ${pkg}" case "$pkgmgr" in "APT") apt-get clean apt-get --purge autoremove -y ;; "DNF") dnf clean all ;; "ZYPPER") zypper clean ;; "PKCON") pkcon clean ;; esac done if [[ ${#failed_packages[@]} -gt 0 ]]; then write_error "Some packages failed to install:" for pkg in "${failed_packages[@]}"; do write_error " - $pkg" done return 1 fi return 0 } software::native_management_availability_check() { # Description: # Detects the native package manager and checks if it's currently locked or in use. # Waits until it becomes available to avoid conflicts with other package operations. # # Globals: # None # # Arguments: # None # # Outputs: # Status messages to stdout and stderr # # Returns: # 0 if the package manager becomes available, 1 if unsupported or error # # End of Documentation local manager lock_files=() msg i=0 j if command -v apt >/dev/null 2>&1; then manager="apt" lock_files=( "/var/lib/dpkg/lock" "/var/lib/apt/lists/lock" "/var/cache/apt/archives/lock" ) elif command -v dnf >/dev/null 2>&1; then manager="dnf" lock_files=("/var/cache/dnf/lockfile") elif command -v yum >/dev/null 2>&1; then manager="yum" lock_files=("/var/run/yum.pid") elif command -v zypper >/dev/null 2>&1; then manager="zypper" lock_files=("/var/run/zypp.pid") else write_error "No supported package manager detected (APT, DNF, YUM, Zypper)." return 1 fi write_status "Detected package manager: $manager" write_status "Checking availability of the package manager..." tput sc while :; do local locked=0 for lock in "${lock_files[@]}"; do if [[ -e "$lock" ]] && fuser "$lock" >/dev/null 2>&1; then locked=1 break fi done if [[ "$locked" -eq 0 ]]; then break fi case $((i % 4)) in 0) j='-' ;; 1) j='\\' ;; 2) j='|' ;; 3) j='/' ;; esac tput rc printf "\r[%s] Waiting for %s to become available..." "$j" "$manager" sleep 2 ((i++)) done printf "\r[✔] %s is now available. \n" "$manager" return 0 } software::ensure_snap_package_managment() { # Description: Function to ensure that snap is installed. # Globals: none # Dependencies: [distro pakcage managment] [function: software::check_native_package_dependency] [Internet access] # Arguments: None # Outputs: stdout # Returns: Standard return codes 1/0 # Usage: Call the function with no arguments # NOTE: # https://www.fosslinux.com/42410/snap-vs-flatpak-vs-appimage-know-the-differences-which-is-better.htm # Snap is a distribution independent package format developed by Canonical and first released # in 2014. It was initially developed for Ubuntu but has been adopted by other Linux distributions # like Arch, Linux Mint, CentOS, Gentoo, and Fedora, and also included support for the Snap # framework. The main aim behind this package format development was to come up with a # single unified format for software packages to run in a wide range of devices. # That includes IoT (IoT), embedded devices running Ubuntu Core (a minimalistic # version of Ubuntu), and computer systems that ran some Ubuntu version. # Snap also offers an online app store – Snapcraft, where users can find and # install the software packages. It creates a large pool where users can find all # available a snap package. Snapcraft is itself controlled and maintained by the # Canonical team. # # # * --- PREFFERRED Software install (Security + Control) ---- * # # CAPABILITY TABLE: # +----------------------------------------------------+-------------+-------------+--------------+ # | Features | Snap | Flatpak | AppImage | # +----------------------------------------------------+-------------+-------------+--------------+ # | Permission Controls Toggles | Yes | Yes | No | # | Sandboxing Support | Yes | Yes | Yes | # | Sandboxing Mandatory | Yes | Yes | No | # | App Portability | Yes | Yes | No | # | Native Theme Support | Yes (caveat)| Yes (caveat)| Yes (caveat) | # | Support for Bundled Libraries | Yes | Yes | Yes | # | Fully Contained Single Executable Support | No | No | Yes | # | Online App Store | Yes | Yes | Yes | # | Multi-version Parallel Apps Support | Yes | Yes | Yes | # | Automatic Updates | Yes | Yes | Yes (caveat) | # | Support for Chrome OS (through Crostini containers)| Yes | Yes | Yes | # | App Size | higher than | higher than | Lowest | # | Number of Applications Available in the App Store | Highest | Lowest | Somewhere in | # | Plugins for Desktop App Store Software | Yes | Yes | No | # +----------------------------------------------------+-------------+-------------+--------------+ # End of documentation if [[ $(echo "$OSTYPE" | grep "linux") ]] &>/dev/null; then write_information "${FUNCNAME[0]}: OK! Detected Linux..." elif [[ "$OSTYPE" == "darwin"* ]]; then write_error "${FUNCNAME[0]}: Mac OSX is currently not supported with snap..." return 1 elif [[ "$OSTYPE" == "cygwin" ]]; then write_error "${FUNCNAME[0]}: CYGWIN is currently unsupported with snap..." return 1 elif [[ "$OSTYPE" == "msys" ]]; then write_error "${FUNCNAME[0]}: Lightweight shell is currently unsupported with snap... " return 1 elif [[ "$OSTYPE" == "freebsd"* ]]; then write_error "${FUNCNAME[0]}: Free BSD is currently unsupported with snap... " return 1 else echo "I have no Idea what this system is" return 1 fi # these tests focus on the latest versions of the distros evaluated... system::log_item "ensuring SNAP software distribution system is available..." local distro_type distro_type=$(system::distribution_type) if hash snap &>/dev/null; then system::log_item "Snap is already present..." if [[ -e /snap ]]; then system::log_item "SNAP classic support present... " return $? else system::log_item "SNAP classic support NOT present... " system::log_item "adding links... " ln -s /var/lib/snapd/snap /snap return $? fi else case $distro_type in suse ) system::log_item "Special case for suse..." security::ensure_admin if hostnamectl | grep -i "tumbleweed"; then local _repo="https://download.opensuse.org/repositories/system:/snappy/openSUSE_Tumbleweed" system::log_item "Detected OpenSUSE Tumbleweed. Adding repo: ${_repo}" software::zypper_add_repo "${_repo}" snappy else _repo=$(echo https://download.opensuse.org/repositories/system:/snappy/openSUSE_Leap_$(cat /etc/os-release | grep VERSION_ID | cut -d= -f2 | cut -d\" -f2)) system::log_item "Detected OpenSUSE Leap. Adding repo:${_repo}" software::zypper_add_repo "${_repo} snappy" fi zypper --gpg-auto-import-keys refresh || ( write_error "Failed to gpg keys!" return 1 ) zypper dup --from snappy || ( write_error "Failed to update snap repository!" return 1 ) software::check_native_package_dependency snapd || ( write_error "Failed to satisfy dependecy SNAP!" return 1 ) systemctl enable --now snapd systemctl enable --now snapd.apparmor systemctl start --now snapd systemctl is-active --quiet snapd || ( write_error "Service snapd not started sucessfully!" return 1 ) ln -s /var/lib/snapd/snap /snap return $? ;; * ) # For distributions that do not need to add a repository to install snap # But would need to install it from their standard reopsitory.... if software::check_native_package_dependency snapd; then local snap_units=() if systemctl list-unit-files | grep -q '^snapd.socket'; then snap_units+=("snapd.socket") fi if systemctl list-unit-files | grep -q '^snapd.service'; then snap_units+=("snapd.service") fi if systemctl list-unit-files | grep -q '^snapd.apparmor'; then snap_units+=("snapd.apparmor") fi for unit in "${snap_units[@]}"; do systemctl enable "$unit" >/dev/null 2>&1 || true systemctl start "$unit" >/dev/null 2>&1 || true done if ! snap version >/dev/null 2>&1; then write_status "snapd not ready yet; waiting for service socket..." systemctl restart snapd.socket >/dev/null 2>&1 || true systemctl restart snapd.service >/dev/null 2>&1 || true sleep 3 fi if ! snap version >/dev/null 2>&1; then write_error "snapd service is not responding." return 1 fi if [[ -e /snap ]]; then write_information "SNAP classic support present... " else ln -s /var/lib/snapd/snap /snap fi return 0 else write_error "Failed to satisfy dependecy SNAP!" fi ;; esac fi } software::zypper_add_repo() { # Description: # Adds a Zypper repository safely and logs reults accordingly. # Logs actions and outcomes to a specified log file. # # Globals: # ZYP_LOGFILE - Path to the log file (default: /var/log/zypper-add-repo.log) # Arguments: # $1 - Repository URL # $2 - Repository alias # $3 - (Optional) Refresh flag (default: --refresh) # Outputs: # Logs actions and outcomes to the specified log file. # Returns: # 0 - Repository added successfully or already exists # 1 - Failed to add repository # 2 - Missing required arguments # Usage: # software::zypper_add_repo "http://example.com/repo" "example-repo" "--no-refresh" # End of documentation local url="$1" local alias="$2" local refresh_flag="${3:-"--refresh"}" local log="${ZYP_LOGFILE:-/var/log/zypper-add-repo.log}" # Validate inputs if [[ -z "$url" || -z "$alias" ]]; then system::log_item "ERROR: url and alias are required." return 2 fi # 1) Exists by alias? if zypper --non-interactive --quiet lr "$alias" &>/dev/null; then system::log_item "INFO: Repo alias '$alias' already exists. Skipping add." return 0 fi # 2) Exists by URL (possibly under another alias)? # Parse the last column from `zypper lr -u` (URI). if zypper --non-interactive lr -u | awk 'NR>2 {print $NF}' | grep -Fxq "$url"; then # Find its alias to report existing_alias="$(zypper lr -u | awk -v u="$url" 'NR>2 && $NF==u {print $3}')" system::log_item "INFO: Repo with URL '$url' already exists (alias: '$existing_alias'). Skipping add." return 0 fi # 3) Try to add if zypper --non-interactive ar "$refresh_flag" "$url" "$alias"; then system::log_item "OK: Added repo '$alias' ($url) successfully." return 0 else system::log_item "ERROR: Failed to add repo '$alias' ($url)." return 1 fi } software::is_native_package_installed() { # Description: Function to check if a piece of software is installed. This function will first check # if the package manager is deb, zypper or rpm, and then take one parameter passed # and evauate if a software package by that name is installed. This function # will return a "0" or "1" return based on the package managers return code. # Globals: # Arguments: None # Outputs: # Returns: 1/0 where 0 = package is installed/1 = package is NOT installed # Usage: # Call this function by: # software::is_native_package_installed "package name" # or # software::is_native_package_installed "$1" if calling this function from a script # or another function receiving a parameter. # # End of documentation if hash dnf &>/dev/null; then rpm -q "$1" &>/dev/null && return 0 || return 1 elif hash zypper &>/dev/null; then zypper se -i "$1" &>/dev/null || return 1 && return 0 elif hash apt &>/dev/null; then dpkg -l "$1" | tail -1 | grep "ii $1" &>/dev/null && return 0 || return 1 else return 3 fi } software::set_install_command() { # Description: # add global install command for portability and convenience. This function # allows for scripts to add software across many systems. # Primarily for Linux: this function will look for the common package managers # apt (all debina and ubuntu based distributions), dnf (RedHat, Fedora, Mandriva, and others) # zypper (SuSE and Open SUSE). If neither of those package manages are available many systems # use package kit; and this will be registered as an sinstall option. # # Globals: # Arguments: None # Outputs: # Returns: # Usage: "software::set_install_command" # # Arguments: none. # # Return Codes: # software::set_install_command will return a global variable ${_INSTCMD} # ${_INSTCMD} will expand to teh appropriate install cmmand including options # for non interactive install of a package. ${_INSTCMD} package.name will install said pacakge. # # A return code of "1" will be returned the function was not able to set the install command. # # # End of documentation if [[ $(echo $OSTYPE | grep "linux") ]]; then if hash dnf 2>/dev/null; then # Try dnf (RedHat, Cent OS, Fedora) write_status "Setting install options for DNF" export _INSTCMD="sudo dnf -y install" elif hash zypper 2>/dev/null; then # Try zypper (Open SUSE) write_status "Setting install options for ZYPPER" export _INSTCMD="sudo zypper install -y " elif hash apt 2>/dev/null; then # Try apt (Debian, Ubuntu, and all derivatives) write_status "Setting install options for DEB" export _INSTCMD="sudo apt-get -y install" elif hash pkcon 2>/dev/null; then # If there is no apt, zypper, dnf write_status "Setting install options for Package Kit" export _INSTCMD="sudo pkcon -y install" else write_error "This system does not seem to have a software managment system" return 1 fi elif [[ "$OSTYPE" == "darwin"* ]]; then write_error "Mac OSX is currently not supported..." elif [[ "$OSTYPE" == "cygwin" ]]; then write_error "CYGWIN is currently unsupported..." elif [[ "$OSTYPE" == "msys" ]]; then write_error "Lightweight shell is currently unsupported... " elif [[ "$OSTYPE" == "freebsd"* ]]; then write_error "Free BSD is currently unsupported... " else echo "I have no Idea what this system is" export _INSTCMD="Whatever!" return 1 fi return $? } software::add_gnome_extensions() { # Description: # Description: # Function to add GNOME extensions to the system. This function takes one or more extension names # as parameters, checks if each extension is installed, and if not, installs them. # # Globals: # _TLA - Expected to be set to a unique identifier for the script or application. # _LOGFILE - Optional global variable to specify a custom log file path. # # Arguments: # Extension names (one or more). # # Outputs: # Writes status messages and errors to stdout or stderr, and logs installation details. # # Returns: # 0 if all specified extensions were installed successfully, 1 otherwise. # # Usage: # software::add_gnome_extensions "dash-to-dock" "some-other-extension" # # Example: # software::add_gnome_extensions "dash-to-dock" # # End of documentation if [ $# -eq 0 ]; then write_warning "Error: No GNOME extensions were provided for me to install." return 1 fi local extension_installer_path="/opt/${_TLA,,}" local extension_installer="$(find "$extension_installer_path" -name 'rtd-gnome-shell-extension-installer')" local gnome_shell_ver="$(gnome-shell --version | awk '{print $3}')" local _logfile="${_LOGFILE:-"/var/log/${_TLA,,}/$(basename ${0})"}" if [[ -z "$gnome_shell_ver" && -n "$SUDO_USER" ]]; then gnome_shell_ver=$(sudo -H -u $SUDO_USER XDG_RUNTIME_DIR="/run/user/$(id -u $SUDO_USER)" gnome-shell --version | awk '{print $3}') fi if [[ -z "$extension_installer" ]]; then write_error "Error: GNOME extension installer not found in $extension_installer_path." return 1 fi # "Dont run if gnome is not running..." if [[ -z "$gnome_shell_ver" ]]; then write_error "Error: Unable to determine GNOME Shell version." return 1 fi local install_status=0 local extensions=("$@") # Capture all arguments as an array write_status "Status: Installing GNOME extension(s): $* ..." for extension in "${extensions[@]}"; do write_status "Status: Installing GNOME extension: $extension." bash "$extension_installer" "$extension" --yes 2>&1 | tee -a "${_logfile}" local installer_rc=${PIPESTATUS[0]} local tee_rc=${PIPESTATUS[1]} if (( tee_rc != 0 )); then write_warning "Warning: Failed to write to log file ${_logfile} (tee exit ${tee_rc})." fi if (( installer_rc != 0 )); then write_error "Error: Failure to install GNOME extensgnome::set_ui_moca_tweaks_for_userion: $extension." install_status=1 else write_status "Status: Successfully installed GNOME extension: $extension." fi done if (( install_status == 0 )); then if [[ "${XDG_SESSION_TYPE}" == "wayland" ]]; then write_status "Status: GNOME extension(s) installed. Please log out/in to reload GNOME Shell (Wayland session)." else write_status "Status: GNOME extension(s) installed. Reloading GNOME Shell..." if command -v dbus-send >/dev/null 2>&1; then if [[ -n "$SUDO_USER" ]]; then sudo -H -u "$SUDO_USER" XDG_RUNTIME_DIR="/run/user/$(id -u "$SUDO_USER")" dbus-send --session --type=method_call --dest=org.gnome.Shell /org/gnome/Shell org.gnome.Shell.Eval string:"global.reexec_self();" \ || write_warning "Warning: Failed to restart GNOME Shell automatically." else dbus-send --session --type=method_call --dest=org.gnome.Shell /org/gnome/Shell org.gnome.Shell.Eval string:"global.reexec_self();" \ || write_warning "Warning: Failed to restart GNOME Shell automatically." fi else write_warning "Warning: dbus-send not available; cannot restart GNOME Shell automatically." fi fi fi return $install_status } software::check_native_package_dependency() { # Description: # Ensures that all provided native software package dependencies are installed. # If any are missing, attempts to install them using the system's package manager. # # Globals: # - EUID - Checks if the script is run as root or requires admin privileges. # - YELLOW, BLUE, GREEN, RED - Color codes for output formatting (non-critical). # # Arguments: # $@ - One or more package names to check and install if missing. # # Function Dependencies: # - security::ensure_admin # - write_status # - write_information # - write_warning # - write_error # - software::is_native_package_installed # - software::add_native_package # # Returns: # 0 if all packages are installed or successfully installed # 1 if any package could not be installed # # Usage: # software::check_native_package_dependency curl git htop # # End of documentation local all_installed=true local pkg local use_color=1 [[ "${TERMUITXT}" == "nocolor" ]] && use_color=0 for pkg in "$@"; do [[ -z "$pkg" ]] && continue local pkg_status pkg_installed pkg_missing pkg_failed if [[ $use_color -eq 1 ]]; then printf -v pkg_status '%b%s%b' "$YELLOW" "$pkg" "$ENDCOLOR" printf -v pkg_installed '%b%s%b' "$YELLOW" "$pkg" "$ENDCOLOR" printf -v pkg_missing '%b%s%b' "$RED" "$pkg" "$ENDCOLOR" printf -v pkg_failed '%b%s%b' "$YELLOW" "$pkg" "$ENDCOLOR" else pkg_status="$pkg" pkg_installed="$pkg" pkg_missing="$pkg" pkg_failed="$pkg" fi write_status "📦 Checking for required software: ${pkg_status}..." if software::is_native_package_installed "$pkg"; then write_information "📦 Dependency is already installed: ${pkg_installed}..." else write_warning "❗📦 Dependency ${pkg_missing} is missing. Attempting to install..." if software::add_native_package "$pkg"; then write_status "📦 Installed successfully: ${pkg_status}..." else write_error "❌ 📦Failed to install ${pkg_failed}. Please install it manually." all_installed=false fi fi done $all_installed && return 0 || return 1 } software::rtd_ppa_checker() { # Description: # Function to check status of Ubuntu and derivatives' PPA aarchives. # This function will take one optional argument to toggle the "delete" option. If the delet option # is requested rtd_ppa_checker will delete any PPA's that are not used (no software is installed from it) # # PPA stands for Personal Package Archive. The PPA allows application developers and # Linux users to create their own repositories to distribute software. With PPA, you can # easily get newer software version or software that are not available via the official Ubuntu # repositories. # # Globals: # Arguments: None # Outputs: # Returns: # Usage: # # rtd_ppa_checker [] [--delete] # # End of documentation ensure_admin distro=$(system::distribution_type) if [[ "$distro" != "ubuntu" && "$distro" != "linuxmint" && "$distro" != "debian" ]]; then write_error "This function is only supported on Ubuntu and derivatives." return 1 fi for f in /etc/apt/sources.list.d/*.list; do grep -Po "(?<=^deb\s).*?(?=#|$)" "$f" | while read -r ENTRY; do echo "ENTRY: $ENTRY" HOST=$(cut -d/ -f3 <<<"$ENTRY") if [ "ppa.launchpad.net" = "$HOST" ]; then USER=$(cut -d/ -f4 <<<"$ENTRY") PPA=$(cut -d/ -f5 <<<"$ENTRY") packageCount=$(awk '$1=="Package:" {if (a[$2]++ == 0) {system("dpkg -l "$2)}}' /var/lib/apt/lists/*"$USER"*"$PPA"*Packages 2>/dev/null | awk '/^ii/' | wc -l) echo "PPA: ppa:$USER/$PPA" echo "FILENAME: $f" echo "$packageCount package(s) installed" if [ "$packageCount" -eq 0 ] && [ "$1" == "--delete" ]; then sudo rm "$f" && echo "$f deleted" fi else USER=$(cut -d/ -f3 <<<"$ENTRY") PPA=$(cut -d/ -f4 <<<"$ENTRY") packageCount=$(awk '$1=="Package:" {if (a[$2]++ == 0) {system("dpkg -l "$2)}}' /var/lib/apt/lists/*"$USER"*Packages 2>/dev/null | awk '/^ii/' | wc -l) echo "REPOSITORY: $USER/$PPA" echo "FILENAME: $f" echo "$packageCount package(s) installed" if [ "$packageCount" -eq 0 ] && [ "$1" == "--delete" ]; then sudo rm "$f" && echo "$f deleted" fi fi done done } software::remove_native_software_package() { # Description: # Removes a specified software package using the native package management # system found on the system. This function is designed for portability and # convenience, supporting various Linux distributions by utilizing their # default package managers (dnf, yum, zypper, apt, pkcon). It's intended to # simplify the process of removing software packages across different # environments without needing to manually identify the package manager. # # Globals: # None # # Arguments: # $1 - The name of the package to be removed. This argument is required. # # Outputs: # Outputs various log messages to indicate the status of operations, including # detection of the package manager, attempts to remove the package, and # error messages if the package manager is not found or if no package name # is provided. # # Returns: # 0 - If the package removal operation is successful. # 1 - If the package removal operation fails, if no package name is provided, # if no known package manager is found on the system, or if the function # encounters any other errors during execution. # # Usage: # To remove a package named "example-package", you would call the function as follows: # software::remove_native_software_package example-package # # It is recommended to use this function with a single package name for reliability. # If there is a need to remove multiple packages, consider calling this function # in a loop, passing one package name at a time. This approach retains the benefit # of multi-distribution support while ensuring that each package is correctly handled. # # End of documentation if [[ -z "$1" ]]; then system::log_item "No package name provided for removal." return 1 fi local package_manager local package_name="$1" # Assuming a single package for safety and clarity system::log_item "Requested to remove package $package_name" security::ensure_admin # Detect package manager if hash dnf 2>/dev/null; then package_manager="dnf" elif hash yum 2>/dev/null; then package_manager="yum" elif hash zypper 2>/dev/null; then package_manager="zypper" elif hash apt 2>/dev/null; then package_manager="apt" elif hash pkcon 2>/dev/null; then package_manager="pkcon" else system::log_item "No known package manager found on this system." return 1 fi # Remove package case $package_manager in dnf | yum) system::log_item "$package_manager package manager detected; attempting to remove package $package_name" $package_manager -y remove "$package_name" ;; zypper) system::log_item "$package_manager package manager detected; attempting to remove package $package_name" $package_manager remove -y "$package_name" ;; apt) system::log_item "$package_manager package manager detected; attempting to remove package $package_name" $package_manager -y remove "$package_name" && $package_manager -y autoremove ;; pkcon) system::log_item "$package_manager package manager detected; attempting to remove package $package_name" $package_manager remove "$package_name" -y --autoremove ;; esac if [[ $? -eq 0 ]]; then system::log_item "Package $package_name has been removed successfully." return 0 else system::log_item "Failed to remove package $package_name." return 1 fi } system::determine_logfile() { # Description: # Determines the path to a log file for the current script based on: # 1) A globally set variable (_LOGFILE). # 2) If _LOGFILE is not set, defaults to either: # - ~/.config/rtd/logs/.log if non-root user # - /var/log//.log if running as root # (or uses a custom location if _LOG_DIR is defined). # 'tla' is derived from the first three letters of the script name. # # USAGE: # system::determine_logfile # Called with no arguments; outputs the determined log file path. # # OUTPUTS: # Writes the full log file path to stdout. # # RETURNS: # 0 on success (the path is always determined). # # NOTES: # - Respects _LOGFILE if already set. # # EXAMPLE CALL: # logfile=$(system::determine_logfile) # echo "Log file is: $logfile" # # Now you can use $logfile in your script for logging purposes. # # - This function is useful for scripts that need to log their output # consistently, regardless of where they are run or who runs them. # - It helps in maintaining a clean logging structure, especially when # scripts are run by different users or in different environments. # - The log file will be named after the script, making it easy to identify # which log belongs to which script. # - The log file will be located in a user-specific directory if the script # is run by a non-root user, or in a system-wide directory if run by root. # - The function is designed to be portable and should work across different # Linux distributions and environments. # - The function is intended to be used in scripts that are part of a larger # system or application, where consistent logging is important for # debugging and monitoring purposes. # # End of documentation local scriptname=$(basename "${BASH_SOURCE[0]}") local tla=${_TLA} ; if [[ -z "$tla" ]]; then tla=$(echo "${scriptname}" | cut -c 1-3); fi local logfile local log_dir local tmpfile # If already determined, reuse cached value to avoid repeated mktemp checks. if [[ -n "${_LOGFILE:-}" ]]; then echo "$_LOGFILE" return 0 fi if [[ -z $_LOGFILE ]]; then # Determine the log directory based on the user ID if [[ $EUID -ne 0 ]]; then log_dir="${HOME}/.config/${tla}/logs" else log_dir="${_LOG_DIR:-"/var/log/${tla,,}"}" fi if [[ ! -d "$log_dir" ]]; then # Attempt to create the preferred directory, otherwise fall back to a tmp path if ! mkdir -p "$log_dir" 2>/dev/null; then log_dir="${TMPDIR:-/tmp}/${tla,,}" mkdir -p "$log_dir" 2>/dev/null || log_dir="${TMPDIR:-/tmp}" fi fi # Verify we can actually write to the directory; fall back if sandboxed if ! tmpfile=$(mktemp "${log_dir}/.rtd-logtest.XXXXXX" 2>/dev/null); then log_dir="${TMPDIR:-/tmp}/${tla,,}" mkdir -p "$log_dir" 2>/dev/null || log_dir="${TMPDIR:-/tmp}" else # Use command to bypass user aliases (e.g., rm -> trash) and silence output. command rm -f -- "$tmpfile" 2>/dev/null fi logfile="${log_dir}/${scriptname}.log" else logfile="${_LOGFILE}" fi # Ensure the log file exists and is writable; fall back to tmp if needed if [[ ! -e "$logfile" ]]; then if ! touch "$logfile" 2>/dev/null; then logfile="${TMPDIR:-/tmp}/${scriptname}.log" touch "$logfile" 2>/dev/null || return 1 fi fi if [[ ! -w "$logfile" ]]; then logfile="${TMPDIR:-/tmp}/${scriptname}.log" touch "$logfile" 2>/dev/null || return 1 fi _LOGFILE="$logfile" export _LOGFILE echo "$logfile" } system::log_item() { # Description: # The purpose of this function is to consistently write script output to the right log file. # By default, the log file will be /var/log/$(basename "$0").log. If the log file is not set # the function will set it to the default. If the log file is set, it will use the set log file. # # This function will log items by prepending the time and date, as well as the calling function. # If the calling function is write_warning, write_error, write_information, or write_status, # the function will prepend the log item with the preceding calling function name. Otherwise, # it will simply prepend the calling function name. # # Globals: _LOGFILE # Arguments: "Text to log" # Outputs: Writes to log file # # Example: # 2024/01/17 11:43 --- : 🧩 source: 🧩 _LOG_DIR=/home/tangaroa/.config/rtd/logs # 2024/01/17 11:43 --- : 🧩 source: 🧩 _LOGFILE=/home/tangaroa/.config/rtd/logs/_rtd_library.log # 2024/01/17 11:43 --- : 🧩 ⚠ source: Library is sourced from a script or terminal! # 2024/01/17 11:43 --- : 🧩 🛈 source: Script is sourced from bash in a terminal: /bin/bash # 2024/01/17 11:43 --- : 🧩 dependency::search_local: Requested dependency file: _branding ... # # Returns: None # Usage: # # Call this function by: # system::log_item "Text to log" # # End of documentation local logfile=$(system::determine_logfile) local date="$(date '+%Y/%m/%d %H:%M')" # Format the log item based on the calling function for clear reading local log_prefix="${date} ---" local log_type="" local log_message="" case "${FUNCNAME[1]}" in write_error) log_type="ERR!" log_ico="💥" log_message="$*" ;; write_warning) log_type="WARN" log_icon="⚠ " log_message="$*" ;; write_information) log_type="INFO" log_icon="🛈 " log_message="$*" ;; write_host) log_type="HOST" log_icon="💻" log_message="$*" ;; write_status) log_type="STAT" log_icon="✔ " log_message="$*" ;; *) log_type="LOGD" log_icon="📜" log_message="${FUNCNAME[1]}: $*" ;; esac if [[ ! -f "$logfile" ]]; then return 1 fi local log_entry="${log_prefix} ${log_icon} : ${log_type} : ${log_message}" if ! printf '%s\n' "$log_entry" >>"$logfile" 2>/dev/null; then local fallback="${TMPDIR:-/tmp}/$(basename "$logfile")" if [[ "$fallback" != "$logfile" ]]; then if touch "$fallback" 2>/dev/null && printf '%s\n' "$log_entry" >>"$fallback" 2>/dev/null; then _LOGFILE="$fallback" return 0 fi fi return 1 fi } software::update_system() { # Description: # System update function. The purpose of this function is to update installed software from # distribution reopositories and the newer self contained universal software stores like # snap and flatpak with a cleaned up oputput only displaying status. # Function does not expect any argument. However it will respect one argument \"simple\" # The simple argument instructs this functions [pkcon] action to display simple output # rather than more user friendly output. # Update native software packages; since it is not known what distribution # this script is being executed on, it is best to check and see if we are able # to perform an update and then do the update... # One could choose to check for a supported exact version: e.g. Ubuntu, but we would # like to be nice and add value for as many as we can. Specially if it is this easy! :) # Globals: # Arguments: None # Outputs: # Returns: # Usage: software::update_system # End of documentation security::ensure_admin PUBLICATION="Update Manager for Linux" VERSION="1.15 (built in)" DIALOGRC="~/.config/rtd/dialogrc" _LOGFILE="${_LOGFILE:-$0.log}" write_host --cyan "${PUBLICATION}: Version ${VERSION}" write_host --cyan "------------------------------------------------------------------" write_host --yellow "I am updating software from all channels I can find on the system." write_host --yellow "I will update via the native package manager as well as newer formats" write_host --yellow "like snap and flatpak ..." write_host --cyan "------------------------------------------------------------------" echo -e " \n" if hash dnf 2>/dev/null; then term::task_exec_list_output dnf -y upgrade elif hash zypper 2>/dev/null; then term::task_exec_list_output zypper up -y elif hash apt 2>/dev/null; then term::task_exec_list_output apt-get update UPGRADABLE=$(LANG=C apt-get upgrade -s | grep -P '^\d+ upgraded' | cut -d" " -f1) if [ "$UPGRADABLE" -eq 0 ]; then write_information "$UPGRADABLE packages that need updates; Skipping update task... " else write_information "There are $UPGRADABLE packages that need updates..." term::task_exec_list_output dpkg --configure -a term::task_exec_list_output apt-get upgrade -y term::task_exec_list_output apt-get full-upgrade -y term::task_exec_list_output apt-get --purge autoremove -y term::task_exec_list_output apt-get clean fi elif hash pkcon 2>/dev/null; then term::task_exec_list_output pkcon -y refresh term::task_exec_list_output pkcon get-updates term::task_exec_list_output pkcon -y update --autoremove else write_error "This system does not seem to have a software managment system" return 1 fi write_status "updating snaps if snap is present on the system..." if hash snap 2>/dev/null; then term::task_exec_list_output snap refresh else write_warning "--- snap software is not present on this system... skipping..." fi write_status "updating flatpaks if flatpak is present on system..." if hash flatpak 2>/dev/null; then term::task_exec_list_output flatpak update --user --noninteractive -y term::task_exec_list_output flatpak update --system --noninteractive -y else write_warning "--- flatpak software is not present on this system... skipping..." fi } display_software_installation_choices_gtk() { # Function to display the software install otions. All software option are listed in # the _rtd_recipes recipe book. Leveraging all the useful function in _rtd_library. # NOTE: adding a function in _rtd_recipes named "recipes_* ()" will automaticall populete # the menu displayed to the user. # # Globals: ${_BACKTITLE} ${zstatus} # Dependencies: _rtd_recipes # Arguments: None # Outputs: interactive screen # Returns: 0/1 last command executed. # Usage: simply call the function by its name. # Arguments: none # DEPRECATED # # End of documentation DisplayMenu=(zenity --list --cancel-label="Go Back" --timeout 120 --width=800 --height=600 --text "$_BACKTITLE" --checklist --column "ON/OFF" --column "Select Software to add:" --separator " ") SoftwareList=$(library::list_loaded_software_functions --nonum) MenuOptions=($(for i in $SoftwareList; do echo -e "${zstatus:=false} ${i}"; done)) declare -A choices choices=$("${DisplayMenu[@]}" "${MenuOptions[@]}" 2>/dev/null) for choice in ${choices}; do # call each chosen software install (by function) & add recepie (removed for display purposes) software::add_software_task recipe_${choice} done } software::display_bundle_install_choices_gtk() { # Function to display the software install otions. All software option are listed in # the _rtd_recipes recipe book. Leveraging all the useful function in _rtd_library. # NOTE: adding a function in _rtd_recipes named "recipes_* ()" will automaticall populete # the menu displayed to the user. # # Globals: ${_BACK_TITLE} ${zstatus} # Dependencies: _rtd_recipes # Arguments: None # Outputs: interactive screen # Returns: 0/1 last command executed. # Usage: simply call the function by its name. # Arguments: none # # # End of documentation local DisplayGUI="zenity" system::log_item "Called by ${FUNCNAME[1]} with the following arguments: $*" : "${_CONFIG_DIR:=${HOME}/.config/${_tla:-"rtd"}}" : "${completed_bundles_list:=${_CONFIG_DIR}/completed-bundles.info}" mkdir -p "${completed_bundles_list%/*}" touch "${completed_bundles_list}" if [[ -s ${completed_bundles_list} ]]; then local _dedupe_tmp if _dedupe_tmp=$(mktemp 2>/dev/null); then if awk 'NF && !seen[$0]++ {print}' "${completed_bundles_list}" >"${_dedupe_tmp}"; then mv "${_dedupe_tmp}" "${completed_bundles_list}" else rm -f "${_dedupe_tmp}" fi else system::log_item "WARN: Unable to create temporary file to deduplicate ${completed_bundles_list}." fi fi while [[ $# -gt 0 ]]; do case "$1" in --zenity ) DisplayGUI="zenity" system::log_item "GUI requested: ${DisplayGUI}" shift ;; --yad ) DisplayGUI="yad" system::log_item "GUI requested: ${DisplayGUI}" shift ;; * ) system::log_item "GUI DEFAULT: ${DisplayGUI}" shift ;; esac done while true; do exec 3>&1 local DisplayList=$(software::list_bundles --zformat-installable) local IFS_SAV=$IFS local IFS=$(echo -en "\n\b") selection=$(${DisplayGUI} --list --title="${_TITLE_MAIN:-"Select Task"}" --cancel-label="Go Back" --checklist \ --text=" Select a Software Bundle below that you want to add to this computer: \n NOTE: only bundles that are not yet installed are shown. \n" \ --height=600 --width=980 \ --print-column=2 --column="Selected" --column="Bundle" --column="Description" \ --separator=" " ${DisplayList} 2>/dev/null) exit_status=$? IFS=$IFS_SAV exec 3>&- case $exit_status in 1) echo [Cancel] && break ;; 255) echo [ESC] && return ;; esac case "${selection}" in "${selection}") if [[ -z "${selection// /}" ]]; then system::log_item "No software bundles were selected for installation." continue fi local previously_completed_bundles="" local previously_completed_count=0 local updated_completed_count=0 local completed_bundles_display="No bundles were installed during this run." local previous_display="No bundles were installed prior to this run." local -a selection_list=(${selection}) local total_count=${#selection_list[@]} local _count=0 local current_bundle="" local snapshot_file snapshot_file=$(mktemp 2>/dev/null) || snapshot_file="${completed_bundles_list}.snapshot" if [[ -s ${completed_bundles_list} ]]; then cat "${completed_bundles_list}" > "${snapshot_file}" else : > "${snapshot_file}" fi if [[ -s ${completed_bundles_list} ]]; then previously_completed_bundles="$(cat "${completed_bundles_list}")" previously_completed_count=$(awk 'END{print NR}' "${completed_bundles_list}") previous_display="${previously_completed_bundles}" fi run_bundle_install() { system::log_item "Installing Software Productivity Bundle(s): ${selection}" for current_bundle in "${selection_list[@]}"; do if [[ -z "${current_bundle}" ]]; then continue fi ((_count = _count + 1)) system::log_item "Installing Software Productivity Bundle: ${current_bundle}" echo "# Installing bundle ${_count} of ${total_count}: ${current_bundle}..." local current_bundle_full_name=${bundle_prefix:-"recipe:_"}${current_bundle} if ${current_bundle_full_name} &>/dev/null; then if ! grep -Fxq "${current_bundle}" "${completed_bundles_list}"; then printf '%s\n' "${current_bundle}" >>"${completed_bundles_list}" fi system::log_item "Progress so far: $((100 * _count / total_count))% of bundles installed." else system::log_item "ERROR: ${_count} of ${total_count}: ${current_bundle} failed to install. Leaving available for retry." fi echo $((100 * _count / total_count)) done } run_bundle_install | ${DisplayGUI} --progress --no-cancel --auto-close \ --title="Installing Software Prodictivity Bundles" \ --text="Please wait for the bundle installs to complete..." \ --percentage=10 --height=200 --width=600 if [[ -s ${completed_bundles_list} ]]; then updated_completed_count=$(awk 'END{print NR}' "${completed_bundles_list}") fi if [[ -s ${completed_bundles_list} ]]; then if [[ -s "${snapshot_file}" ]]; then completed_bundles_display=$(grep -Fvx -f "${snapshot_file}" "${completed_bundles_list}" || true) [[ -n "${completed_bundles_display}" ]] || completed_bundles_display="No bundles were installed during this run." else completed_bundles_display=$(cat "${completed_bundles_list}") fi fi rm -f "${snapshot_file}" ${DisplayGUI} --info --timeout 120 \ --title "Bundles Installed: ${updated_completed_count} Bundles Previously Installed: ${previously_completed_count}" \ --text="The following Software bundles have been installed: \n \n ${completed_bundles_display} \n \n Previously installed bundles: \n \n ${previous_display}" \ --height=600 \ --width=800 for i in previously_completed_bundles previously_completed_count updated_completed_count completed_bundles_display previous_display selection_list total_count current_bundle _count snapshot_file; do unset ${i}; done ;; *) # Trap unknown conditions so as to not do harm return ;; esac done return } software::display_bundle_removal_choices_gtk() { # Function to display the Software Prodictivity Bundles removal otions. All software options are listed in # the _rtd_recipes recipe book. Leveraging all the useful function in _rtd_library. # NOTE: adding a function in _rtd_recipes named "recipes_* ()" will automaticall populste # the menu displayed to the user. # # Globals: ${_BACK_TITLE} ${zstatus} # Dependencies: file: _rtd_recipes # Arguments: None # Outputs: interactive screen # Returns: 0/1 last command executed. # Usage: simply call the function by its name. # Arguments: none # # # End of documentation local DisplayGUI="zenity" system::log_item "Called by ${FUNCNAME[1]} with the following arguments: $*" : "${_CONFIG_DIR:=${HOME}/.config/${_TLA:-"rtd"}}" : "${completed_bundles_list:=${_CONFIG_DIR}/completed-bundles.info}" mkdir -p "${completed_bundles_list%/*}" touch "${completed_bundles_list}" while [[ $# -gt 0 ]]; do case "$1" in --zenity ) DisplayGUI="zenity" system::log_item "GUI requested: ${DisplayGUI}" shift ;; --yad ) DisplayGUI="yad" system::log_item "GUI requested: ${DisplayGUI}" shift ;; * ) system::log_item "GUI DEFAULT: ${DisplayGUI}" shift ;; esac done while true; do exec 3>&1 DisplayList=$(software::list_bundles --zformat-removable) IFS_SAV=$IFS IFS=$(echo -en "\n\b") selection=$(${DisplayGUI} --list --title="${_TITLE_MAIN:-"Select Task"}" --cancel-label="Go Back" --checklist \ --text=" Select a Software Bundle below that you want to REMOVE from this computer: \n \n" \ --height=600 --width=980 \ --print-column=2 --column="Selected" --column="Bundle" --column="Description" --separator=" " ${DisplayList}) exit_status=$? IFS=$IFS_SAV exec 3>&- case $exit_status in 1) echo [Cancel] && return ;; 255) echo [ESC] && return ;; esac case "${selection}" in "${selection}") local d=0 while IFS= read -r _existing_bundle; do [[ -z "${_existing_bundle}" ]] && continue ((d = d + 1)) done < "${completed_bundles_list}" local c=0 selection_list=(${selection}) total_count=${#selection_list[@]} removed_bundle_list="$(mktemp)" || removed_bundle_list="/tmp/rtd-removed-bundles.$$" run_bundle_uninstall() { system::log_item "Removing Software Prodictivity Bundle(s): ${selection}" for i in ${selection}; do system::log_item "Removing Software Prodictivity Bundles: ${i}" current_bundle="${i}" ((c = c + 1)) echo "# Removing bundle ${c} of ${total_count}: ${current_bundle}..." local current_bundle_full_name=${bundle_prefix:-"recipe:_"}${current_bundle} ${current_bundle_full_name} --remove sed -i "/${current_bundle}/d" "${completed_bundles_list}" echo "${current_bundle}" >>"${removed_bundle_list}" echo $((100 * ${c} / ${total_count})) done } run_bundle_uninstall | ${DisplayGUI} --progress --no-cancel --auto-close \ --title="Removing Software Prodictivity Bundles" \ --text="Please wait for the bundle removals to complete..." \ --percentage=10 --height=200 --width=600 ${DisplayGUI} --info \ --timeout 120 \ --title "Bundles Removed: $c " \ --text="The following Software bundles have been REMOVED: \n \n $(cat ${removed_bundle_list}) " \ --height=600 \ --width=800 ;; *) # Trap unknown conditions so as to not do harm return ;; esac for i in DisplayList selection_list total_count current_bundle current_bundle_full_name removed_bundle_list; do unset ${i}; done done return } rtd_server_setup_choices_services() { # Description: Function to display server installation options. This will install software that is usefull # on a server, or a remote system only accessible via SSH. # # Globals: # Arguments: None # Outputs: # Returns: # Usage: simply call the function by its name. # # Arguments: none # # # End of documentation # List Options to be available for choice in the RTD System Configurator... cmd=(dialog --backtitle "${TLA} OEM System Configuraton Menu" --title "Server Role Options Menu" --separate-output --checklist "Please Select a role for this system below:" 22 85 16) options=(1 "Install Minecraft Server" off 2 "Install LAMP Server" off ) choices=$("${cmd[@]}" "${options[@]}" 2>&1 >/dev/tty) clear for choice in $choices; do case $choice in 1) write_status "Installing and configuring Minecraft server, launcher, auto startup, etc... " if java --version; then echo "java of some version is present. I respect your choice... and will try to run! --- OK!" else software::check_native_package_dependency default-jre fi if cat /home/$SUDO_USER/minecraft-server | grep SLS; then write_information "minecraft server launcher is already present..." else system::minecraft_server_launcher "/home/$SUDO_USER/minecraft-server" fi if cat /etc/systemd/system/getty@tty1.service.d/override.conf | grep tangarora; then write_information "Service auto login already setup..." else write_warning "Minecraft server startup is not enabled: seting up now..." toggle_oem_auto_login --enable fi if cat /home/$SUDO_USER/.bashrc | grep minecraft-server; then write_information "minecraft server is ready to start on login" else sudo -H -u tangarora bash -c 'echo ~/minecraft-server >> ~/.bashrc' fi dialog --backtitle "$BRANDING" --title "Install Status" --yesno "I am done installing Minecraft Server. What Do you want to run the Minecraft Server now? " "$HEIGHT" "$WIDTH" exit_status=$? case $exit_status in 0) sudo -H -u tangarora bash -c 'bash ~/minecraft-server' ;; 1) return ;; esac ;; 2) for i in apache2 mysql-server php libapache2-mod-php php-mcrypt php-mysql phpmyadmin nodejs; do InstallSoftwareFromRepo $i; done ;; esac done } rtd_server_setup_choices_productivity() { # Description: Function to display terminal software installation options. This will install # software that is usefull on a server, or a remote system only accessible via SSH. # # Globals: # Arguments: None # Outputs: # Returns: # Usage: simply call the function by its name. # # Arguments: none # # NOTE: # This may be a good usecase if you happen to be in an opressed geographic locaiton where # your only option is to ssh to a remote server and access email and web that way. # Perfomance in these cases may be very poor over the internet (as for example with # the great firewall of China). In Such a case it would be usefull to have the good # old CLI software! :) # End of documentation cmd=(dialog --backtitle "${TLA} OEM System Configuraton Menu" --title "Terminal Productivity Software Options" --separate-output --checklist "We did not find a graphical interface. No matter, you can be productive in the cli environment. Please Select Software and Configuration below:" 22 85 16) options=(1 "Base RTD OEM Software" on 2 "Alpine email client" on 3 "Vim text editor" on 4 "Finch multi protocol chat" on 5 "Word Grinder word precessor" on 6 "Spreadsheet Calculator" on 7 "TPP Presentation Program" on 8 "Midnight Commander file manager (Norton Commander)" on 9 "Cmus Music Player" on 10 "Byobu Terminal Window Manger" on 11 "W3M web Browser" on 12 "LYNX Web Browser" on 13 "Mega.nz command line tools (Mega-CMD)" on 14 "Rtorrent torrent download software" off 15 "Install the OpenVpn client Software" on 16 "Games: Freesweep mine sweep game" on 17 "Games: Bastet Tetris Game" on 18 "OEM Customizatons" on ) choices=$("${cmd[@]}" "${options[@]}" 2>&1 >/dev/tty) clear for choice in $choices; do case $choice in 1) for i in htop powertop iftop monit nethogs bmon darkstat mtr glances nmap multitail ncdu; do InstallSoftwareFromRepo $i; done ;; 2) InstallSoftwareFromRepo alpine ;; 3) InstallSoftwareFromRepo vim ;; 4) InstallSoftwareFromRepo finch ;; 5) InstallSoftwareFromRepo wordgrinder ;; 6) InstallSoftwareFromRepo sc ;; 7) InstallSoftwareFromRepo tpp ;; 8) InstallSoftwareFromRepo mc ;; 9) InstallSoftwareFromRepo cmus ;; 10) InstallSoftwareFromRepo byobu ;; 11) InstallSoftwareFromRepo w3m ;; 12) InstallSoftwareFromRepo lynx ;; 13) Single_Install_MEGA.nz_Encrypted_Cloud_Storage megacmd ;; 14) InstallSoftwareFromRepo rtorrent ;; 15) InstallSoftwareFromRepo openvpn ;; 16) InstallSoftwareFromRepo freesweep ;; 17) InstallSoftwareFromRepo bastet ;; 18) echo 'export PS1="\[\033[35m\]\t\[\033[m\]-\[\033[36m\]\u\[\033[m\]@\[\033[32m\]\h:\[\033[33;1m\]\w\[\033[m\]\$ "' >>~/.bashrc ;; esac done } oem::setup_choices_server() { # Description: Function to display legacy installation options. This will install software that is usefull # on a really old system, or a remote system only accessible via SSH. This may be a good # usecase if oyu happen to be in an opressed geographic locaiton where your only option is # to ssh to a remote server and access email and web that way. Perfomance in these cases # may be very poor over the internet (as for example with the great firewall of China). In # Such a case it would be usefull to have the good old CLI software! :) # Globals: # Arguments: None # Outputs: # Returns: # Usage: # End of documentation # List Options to be available for choice in the RTD System Configurator... cmd=(dialog --backtitle "${TLA} OEM System Configuraton Menu" --title "System Software Chooser" --menu "It looks like this is a server to me. Please Select a role for htis system below:" "$HEIGHT" "$WIDTH" "$LIST_HEIGHT") options=(1 "Select terminal apps to install (i.e. vim, Lynx, Midnight Commander etc.)" 2 "Select Role (i.e. SSH server, Minecraft Server, etc.)" ) choices=$("${cmd[@]}" "${options[@]}" 2>&1 >/dev/tty) clear for choice in $choices; do case $choice in 1) rtd_server_setup_choices_productivity ;; 2) rtd_server_setup_choices_services ;; esac done } fedora::get_fedora_netinst_iso_url() { # Description: # # Retrieves the full download URL for the Fedora "Everything" NetInstall ISO # for a specific version and x86_64 architecture. # # This function validates the version format, constructs the expected download # directory URL, fetches the directory listing using curl, extracts the # latest NetInstall ISO filename, and prepends the directory URL to create # the full download link. # # Globals: # None # # Arguments: # --version Required. The Fedora release version number (e.g., 40). # # Outputs: # STDOUT: Full URL to the latest Fedora Everything NetInstall ISO for the # specified version if found. # STDERR: Error messages, warnings, and informational logs (via system::log_item). # # Returns: # 0: If the URL was successfully found and printed to STDOUT. # 1: If any error occurred (e.g., invalid arguments, failed download, ISO not found). # # Dependencies: # curl, grep, sort, awk, mktemp # # Usage Example: # local latest_ver # latest_ver=$(fedora::get_latest_release_version) # Assuming this function exists # local iso_url # if ! iso_url=$(fedora::get_fedora_netinst_iso_url --version "${latest_ver}"); then # printf "ERROR: Failed to get Fedora %s netinstall ISO URL.\n" "${latest_ver}" >&2 # # Handle error appropriately # fi # printf "Latest ISO URL: %s\n" "${iso_url}" # End of Documentation local version="" local -r base_url="https://download.fedoraproject.org/pub/fedora/linux/releases" local iso_page_url="" local temp_output="" # Initialize for trap safety local http_code="" local iso_page_content="" local iso_filename="" local iso_full_url="" while [[ $# -gt 0 ]]; do local arg="$1" case "${arg}" in --version) if [[ -z "${2-}" ]]; then # Check if $2 exists system::log_item "ERROR: --version flag requires an argument." return 1 fi version="$2" shift # past argument shift # past value ;; *) # Unknown option system::log_item "ERROR: Unknown argument: ${arg}" system::log_item "Usage: ${FUNCNAME[0]} --version " return 1 ;; esac done if [[ -z "${version}" ]]; then system::log_item "ERROR: Missing required argument: --version" system::log_item "Usage: ${FUNCNAME[0]} --version " return 1 fi if [[ ! "${version}" =~ ^[0-9]+$ ]]; then system::log_item "ERROR: Invalid version format: '${version}'. Must be a number." return 1 fi iso_page_url="${base_url}/${version}/Everything/x86_64/iso/" temp_output=$(mktemp) || { system::log_item "ERROR: Failed to create temporary file." return 1 } trap 'rm -f -- "$temp_output"' EXIT INT TERM HUP &>/dev/null # Ensure cleanup on exit system::log_item "INFO: Fetching Fedora ISO directory index: ${iso_page_url}" http_code=$(curl --connect-timeout 10 -sL -w "%{http_code}" -o "${temp_output}" "${iso_page_url}") local curl_exit_code=$? if [[ ${curl_exit_code} -ne 0 ]]; then system::log_item "ERROR: curl command failed with exit code ${curl_exit_code} while fetching ${iso_page_url}" return 1 fi if [[ "${http_code}" -ne 200 ]]; then system::log_item "ERROR: Failed to access ISO directory. HTTP status: ${http_code}. URL: ${iso_page_url}" return 1 fi iso_page_content=$(<"${temp_output}") rm -f -- "${temp_output}" &>/dev/null trap - EXIT INT TERM HUP &>/dev/null # Remove the trap if [[ -z "${iso_page_content}" ]]; then system::log_item "ERROR: ISO directory listing was empty. URL: ${iso_page_url}" return 1 fi iso_filename=$( echo "${iso_page_content}" | \ grep -Eo "Fedora-Everything-netinst-x86_64-${version}-[0-9.]+\.iso" | \ sort -V | \ tail -n 1 ) if [[ -z "${iso_filename}" ]]; then system::log_item "ERROR: Could not find NetInstall ISO filename for version ${version} matching pattern in ${iso_page_url}." return 1 fi iso_full_url="${iso_page_url}${iso_filename}" system::log_item "INFO: Found Fedora NetInstall ISO URL: ${iso_full_url}" echo "${iso_full_url}" return 0 } fedora::get_latest_release_version() { # Description: # Retrieves the latest available Fedora release version from the official Fedora project. # This function scans the Fedora mirror directory listing and determines the latest # numerical release version by parsing subdirectory names. # # Globals: # None # # Arguments: # None # # Outputs: # STDOUT: Latest Fedora release version (e.g., 40, 41, 42) # STDERR: Errors encountered during retrieval or parsing # # Returns: # 0 if the latest version is successfully determined # 1 if an error occurs during HTTP requests, parsing, or no version found # # Dependencies: # curl, grep, sort, tail, mktemp # # Usage: # latest_version=$(fedora::get_latest_release_version) || printf "Failed to retrieve latest Fedora version.\n" # # End of Documentation: local url="https://download.fedoraproject.org/pub/fedora/linux/releases/" local http_code local dir_listing local latest_version local temp_output system::log_item "Fetching directory listing from $url..." temp_output=$(mktemp) http_code=$(curl --connect-timeout 10 -sL -w "%{http_code}" -o "$temp_output" "$url") dir_listing=$(<"$temp_output") rm "$temp_output" &>/dev/null if [[ "$http_code" -ne 200 ]]; then system::log_item "Error: Failed to fetch Fedora release directory (HTTP code: $http_code)" return 1 fi if [[ -z "$dir_listing" ]]; then system::log_item "Error: Empty directory listing retrieved from $url" return 1 fi latest_version=$( echo "$dir_listing" | \ grep -oP '/dev/null ; then sed -i "s/^GRUB_CMDLINE_LINUX_DEFAULT=.*/GRUB_CMDLINE_LINUX_DEFAULT=\"quiet splash\"/" /etc/default/grub if ! update-initramfs -u; then write_warning "update-initramfs -u finished with a non-zero exit code. Check output." fi if ! update-grub; then # Check alternative command if update-grub fails (e.g., UEFI systems) if ! grub-mkconfig -o /boot/grub/grub.cfg; then write_error "Both 'update-grub' and 'grub-mkconfig' failed. GRUB configuration may be broken." overall_success=false else write_status "'grub-mkconfig' successful." local overall_success=true fi else local overall_success=true fi fi # --- Final Result --- if [[ "$overall_success" == "true" ]]; then write_status "✅ Plymouth theme and GRUB configuration verified/updated successfully." return 0 else write_error "❌ One or more steps failed during Plymouth/GRUB configuration or plymouth may not be installed..." return 1 fi } ubuntu::get_target_version() { # Description: # Interactively or non-interactively determine which Ubuntu version to use for installation. # Fetches and parses all available LTS and regular Ubuntu versions from releases.ubuntu.com, # provides a dialog menu (if _ask=yes) for user selection, and validates the chosen version. # # Globals: # _tgt_ubuntu_ver - (input/output) Target Ubuntu version (used and/or updated by this function) # _ask - Flag indicating whether to prompt the user interactively # ubuntu_flavor - Ubuntu flavor label for display # _role - Role label (desktop/server/etc.) for display # ubuntulogo - ASCII or text logo used in the dialog box # RTD_GUI - Path to GUI wrapper (e.g., whiptail/dialog wrapper) # DIALOG_CANCEL - Exit code when user cancels dialog # DIALOG_ESC - Exit code when user escapes dialog # # Arguments: # None # # Outputs: # STDOUT: Dialog and informational messages # STDERR: Errors # # Returns: # 0 if version is chosen and valid # 1 if version is invalid, canceled, or selection failed # # Dependencies: # wget, perl, dialog/whiptail # # Usage: # ubuntu::get_target_version # printf "Selected version: %s\n" "$tgt_ubuntu_ver" # # End of Documentation: # Determine available versions of Ubuntu to offer... system::log_item "🔎 Retreive list of available $ubuntu_flavor $_role versions..." declare -a all_lts_versions=($(wget -O- releases.ubuntu.com -q | perl -ne '/Ubuntu (\d+.\d+.\d+)/ && print "$1\n"' | sort -Vu)) declare -a all_normal_versions=($(wget -O- releases.ubuntu.com -q | perl -ne '/Ubuntu (\d+.\d+)/ && print "$1\n"' | sort -Vu)) dist_logo="${ubuntulogo}" # Check if a version of Ubuntu was specified, otherwise make sure it is (default to latest LTS)... if [[ -z ${_tgt_ubuntu_ver} ]]; then write_information "🐧 - No version of Ubuntu specified as a parameter..." case ${_ask} in "yes" | "YES" | "Yes" | "y" | "Y") write_information "🐧 - Requested to ask for Ubuntu version; prompting for version of Ubuntu to use..." extra_option="Manually_specify" # Display a menu to choose an Ubuntu version, including an option for manual entry _tgt_ubuntu_ver=$($RTD_GUI --colors --title "Select Release Version of $ubuntu_flavor $_role" --menu "\n \Z1\n$dist_logo\Zn \n 👍 Please pick an available \Z1$ubuntu_flavor\Zn version from the list below or choose to manually specify a version. Long Term Support (LTS) Versions to choose from are as follows:\n\n " 35 90 $((${#all_lts_versions[@]} + 1)) \ $(for i in "${!all_lts_versions[@]}"; do echo "${all_lts_versions[$i]}" "$i"; done) "$extra_option" "$((${#all_lts_versions[@]} + 1))" 3>&1 1>&2 2>&3) dialog_status=$? # Handle the user's menu selection or cancellation based on the captured status case $dialog_status in $DIALOG_CANCEL) echo "Dialog was cancelled. ($dialog_status)" write_information "🐧 - No version of Ubuntu specified, so using the latest LTS version: ${all_lts_versions[-1]}" _tgt_ubuntu_ver=${all_lts_versions[-1]} return 1 ;; $DIALOG_ESC) echo "Dialog was escaped. ($dialog_status)" write_information "🐧 - No version of Ubuntu specified, so using the latest LTS version: ${all_lts_versions[-1]}" _tgt_ubuntu_ver=${all_lts_versions[-1]} return 1 ;; esac # Check if the user chose to manually specify a version if [[ "${_tgt_ubuntu_ver}" == "${extra_option}" ]]; then _tgt_ubuntu_ver=$($RTD_GUI --colors --title "Manual Version Input" --inputbox "\nPlease enter the desired Ubuntu version (e.g., 22.10):" 10 60 3>&1 1>&2 2>&3) # Capture and handle the exit status of the input dialog case $? in $DIALOG_CANCEL) echo "Input was cancelled. ($dialog_status)" write_information "🐧 - No version of Ubuntu specified, so using the latest LTS version: ${all_lts_versions[-1]}" _tgt_ubuntu_ver=${all_lts_versions[-1]} return 1 ;; $DIALOG_ESC) echo "Input was escaped. ($dialog_status)" write_information "🐧 - No version of Ubuntu specified, so using the latest LTS version: ${all_lts_versions[-1]}" _tgt_ubuntu_ver=${all_lts_versions[-1]} return 1 ;; esac fi clear ;; "no" | "NO" | "No" | "n" | "N") write_information "🐧 - No version of Ubuntu specified, so using the latest LTS version: ${all_lts_versions[-1]}" _tgt_ubuntu_ver=${all_lts_versions[-1]} ;; esac fi # Validate Ubuntu LTS version selections against available versions (manual selection may have occurred)... for lts_version in "${all_lts_versions[@]}"; do if [[ "${lts_version}" == "${_tgt_ubuntu_ver}" ]]; then local _selection_is_valid="yes" write_information "🐧 - The version of Ubuntu selected is valid: ${_tgt_ubuntu_ver}" fi done # Validate Ubuntu normal version selections against available versions (manual selection may have occurred)... for normal_version in "${all_normal_versions[@]}"; do if [[ "${normal_version}" == "${_tgt_ubuntu_ver}" ]]; then local _selection_is_valid="yes" write_information "🐧 - The version of Ubuntu selected is valid: ${_tgt_ubuntu_ver}" fi done if [[ "${_selection_is_valid}" != "yes" ]]; then write_error "⛔ - The version of Ubuntu selected [${_tgt_ubuntu_ver}] is not valid. Please select a valid version list. Alternatively, you may let me choose for you..." read -p "Press [ENTER] to continue..." return 1 fi # Return the version number selected _tgt_ubuntu_ver=${_tgt_ubuntu_ver:-${all_lts_versions[-1]}} system::log_item "Exporting selection: $tgt_ubuntu_ver" export tgt_ubuntu_ver } ubuntu::get_latest_release_version() { # Description: # Retrieves the latest available Ubuntu release version from the official archive. # This function scans the Ubuntu archive index, detects release version numbers, # and extracts the highest valid version number using LTS and regular versions. # # Globals: # None # # Arguments: # None # # Outputs: # STDOUT: Latest Ubuntu release version in format YY.MM # STDERR: Errors encountered during retrieval or parsing # # Returns: # 0 if the latest version is successfully determined # 1 if an error occurs during HTTP requests, parsing, or no version found # # Dependencies: # wget, perl, sort, tail # # Usage: # latest_version=$(ubuntu::get_latest_release_version) || printf "Failed to retrieve latest Ubuntu version.\n" # # End of Documentation local url="https://releases.ubuntu.com" local latest_version="" local output local -a all_lts_versions local -a all_normal_versions local -a all_versions dependency::command_exists wget perl sort tail if ! output=$(wget -q -O- "$url"); then system::log_item "ERROR: Failed to retrieve directory listing from $url" return 1 fi mapfile -t all_lts_versions < <(printf '%s\n' "$output" | perl -ne '/Ubuntu (\d+\.\d+\.\d+)/ && print "$1\n"' | sort -Vu) mapfile -t all_normal_versions < <(printf '%s\n' "$output" | perl -ne '/Ubuntu (\d+\.\d+)/ && print "$1\n"' | sort -Vu) all_versions=("${all_lts_versions[@]}" "${all_normal_versions[@]}") if [[ ${#all_versions[@]} -eq 0 ]]; then system::log_item "ERROR: No versions found in the Ubuntu archive." return 1 fi latest_version=$(printf '%s\n' "${all_versions[@]}" | sort -V | tail -n 1) if [[ -z "$latest_version" ]]; then system::log_item "ERROR: No valid version found in the Ubuntu archive." return 1 fi if [[ "$latest_version" =~ ^([0-9]{2})\.([0-9]{2}) ]]; then local year="${BASH_REMATCH[1]}" local month="${BASH_REMATCH[2]}" if (( month > 12 )); then system::log_item "ERROR: Invalid month detected in version: $latest_version" return 1 fi printf '%s.%s\n' "$year" "$month" return 0 else system::log_item "ERROR: Failed to parse the latest version: $latest_version" return 1 fi } ubuntu::download_iso() { # Description: Downloads the specified Ubuntu ISO (Desktop or Server) based on the provided flavor, version, and type. # # This function allows you to download various Ubuntu ISOs, including Ubuntu Desktop, Ubuntu Server, # Kubuntu, and Xubuntu. You can specify the directory to save the ISO, the flavor of Ubuntu, # the version, and whether you want the Desktop or Server ISO. # # Globals: # None # # Arguments: # --dir [directory]: The directory where the ISO should be saved. Defaults to /var/lib/libvirt/boot. # --flavor [ubuntu|kubuntu|xubuntu]: The flavor of Ubuntu to download. Defaults to 'ubuntu'. # --version [version]: The version of Ubuntu to download (e.g., 20.04.2). Defaults to 20.04.2. # --type [server|desktop]: The type of Ubuntu ISO to download. Valid only for Ubuntu flavor. Defaults to 'server'. # # Outputs: # - Downloads the specified ISO to the specified directory. # - Logs status messages regarding the progress and success of the download. # # Returns: # - 0 on success. # - 1 on error (e.g., invalid arguments, failure to download ISO). # # Usage: # ubuntu::download_iso --dir /path/to/save --flavor ubuntu --version 22.04.1 --type desktop # # Example: # To download the Ubuntu 22.04.1 Desktop ISO to /var/lib/libvirt/boot: # ubuntu::download_iso --dir /var/lib/libvirt/boot --flavor ubuntu --version 22.04.1 --type desktop # # Details: # - The function constructs the appropriate ISO filename and download URL based on the provided # flavor, version, and type. # - If the ISO is already present in the specified directory, the function logs a message and # skips the download. # - The downloaded ISO path is exported as the variable 'iso' for use in other scripts or commands. # # End of Documentation # Initialize default values local permanent_download_dir="/var/lib/libvirt/boot" # where to put the downloaded media local media_type="server" # Default to server, can be overridden by --media-type parameter local tgt_ubuntu_ver="20.04.2" # Default version, can be overridden by --version parameter # Parse arguments while [[ "$#" -gt 0 ]]; do case $1 in --dir) if [[ -n $2 && $2 != --* ]]; then permanent_download_dir="$2" shift else system::log_item "Error: --dir requires a non-empty argument." return 1 fi ;; --version) if [[ -n $2 && $2 != --* ]]; then tgt_ubuntu_ver="$2" shift else system::log_item "Error: --version requires a non-empty argument." return 1 fi ;; --media-type) if [[ -n $2 && $2 != --* ]]; then media_type="$2" shift else system::log_item "Error: --type requires a non-empty argument." return 1 fi ;; *) system::log_item "Unknown parameter passed: $1" return 1 ;; esac shift done case $media_type in server) iso_filename="ubuntu-${tgt_ubuntu_ver}-live-server-amd64.iso" ubuntu_iso_url="http://releases.ubuntu.com/${tgt_ubuntu_ver}/ubuntu-${tgt_ubuntu_ver}-live-server-amd64.iso" ;; desktop) iso_filename="ubuntu-${tgt_ubuntu_ver}-desktop-amd64.iso" ubuntu_iso_url="http://releases.ubuntu.com/${tgt_ubuntu_ver}/ubuntu-${tgt_ubuntu_ver}-desktop-amd64.iso" ;; *) system::log_item "Invalid media type specified. Valid options for Ubuntu are: server, desktop." return 1 ;; esac # Ensure that the file is available for VM creation... system::log_item "Checking if $iso_filename is already downloaded in $permanent_download_dir..." iso=$(find "$permanent_download_dir" -name "$iso_filename") if [[ ! -e "$iso" ]]; then system::log_item "$iso_filename is not in cache, downloading..." wget -nc "$ubuntu_iso_url" -P "$permanent_download_dir" || { system::log_item "FATAL Problem: Failure to download ISO file $ubuntu_iso_url" return 1 } iso="$permanent_download_dir/$iso_filename" else system::log_item "💿 - $iso_filename already in cache, using it for greenfield creation..." fi system::log_item "Exporting ISO value: iso = $iso" export iso } ########################################################################################### # # -----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+----- # . _..::__: ,-"-"._ |7 , _,.__ # _.___ _ _<_>`!(._`.`-. / _._ `_ ,_/ ' '-._.---.-.__ # >.{ " " `-==,',._\{ \ / {) / _ ">_,-' ` mt-2_ # \_.:--. `._ )`^-. "' , [_/( __,/-' # '"' \ " _L oD_,--' ) /. (| # | ,' _)_.\\._<> 6 _,' / ' # `. / [_/_'` `"( <'} ) # \\ .-. ) / `-'"..' `:.# _) ' # ` \ ( `( / `:\ > \ ,-^. /' ' # `._, "" | \`' \| ?_) {\ # `=.---. `._._ ,' "` |' ,- '. # | `-._ | / `:`<_|h--._ # ( > . | , `=.__.`-'\ # `. / | |{| ,-.,\ . # | ,' \ / `' ," \ # | / |_' | __ / # | | '-' `-' \. # |/ " / # \. ' # ,/ ______._.--._ _..---.---------._ # ,-----"-..?----_/ ) __,-'" " ( # -.._( `-----' `- # -----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+----- # # ########################################################################################### # # 888 888 d8b 888 888 # 888 888 Y8P 888 888 # 888 888 888 888 # Y88b d88P 888 888d888 888888 888 888 8888b. 888 # Y88b d88P 888 888P" 888 888 888 "88b 888 # Y88o88P 888 888 888 888 888 .d888888 888 # Y888P 888 888 Y88b. Y88b 888 888 888 888 # Y8P 888 888 "Y888 "Y88888 "Y888888 888 # # # # 888b d888 888 d8b # 8888b d8888 888 Y8P # 88888b.d88888 888 # 888Y88888P888 8888b. .d8888b 88888b. 888 88888b. .d88b. # 888 Y888P 888 "88b d88P" 888 "88b 888 888 "88b d8P Y8b # 888 Y8P 888 .d888888 888 888 888 888 888 888 88888888 # 888 " 888 888 888 Y88b. 888 888 888 888 888 Y8b. # 888 888 "Y888888 "Y8888P 888 888 888 888 888 "Y8888 # # # # 888b d888 888 # 8888b d8888 888 # 88888b.d88888 888 # 888Y88888P888 8888b. 88888b. 8888b. .d88b. .d88b. 88888b.d88b. .d88b. 88888b. 888888 # 888 Y888P 888 "88b 888 "88b "88b d88P"88b d8P Y8b 888 "888 "88b d8P Y8b 888 "88b 888 # 888 Y8P 888 .d888888 888 888 .d888888 888 888 88888888 888 888 888 88888888 888 888 888 # 888 " 888 888 888 888 888 888 888 Y88b 888 Y8b. 888 888 888 Y8b. 888 888 Y88b. # 888 888 "Y888888 888 888 "Y888888 "Y88888 "Y8888 888 888 888 "Y8888 888 888 "Y888 # 888 # Y8b d88P # "Y88P" ########################################################################################### kvm::util::validate_vm_name() { # Description: Validate a VM name according to hostname rules. # Arguments: $1 - Candidate VM name. # Returns: 0 if valid, 1 otherwise. # End of documentation local candidate="$1" if [[ -z "$candidate" ]]; then return 1 fi if (( ${#candidate} > 255 )); then system::log_item "${candidate}: Invalid hostname (too long)" return 1 fi system::log_item "${candidate}: Is a valid hostname" return 0 } kvm::util::set_vm_name() { # Description: Set the VM name consistently across functions. # Arguments: # --var - Target variable name (default: vm_name). # --name - Explicit VM name to use. # --force - Force override even if already set. # --no-timestamp - Do not append a timestamp. # --timestamp-format - Override timestamp format (default: %Y-%m-%d-%H-%M). # --no-dedupe - Do not deduplicate overlapping name parts. # [additional parts] - Parts used to build a default name (joined with '_'). # Returns: 0 on success, 1 on failure. # End of documentation local target_var="vm_name" local explicit_name="" local include_timestamp=1 local timestamp_format="%Y-%m-%d-%H-%M" local force_override=0 local enable_dedupe=1 local parts=() while [[ $# -gt 0 ]]; do case "$1" in --var) target_var="$2" shift 2 ;; --name | --explicit) explicit_name="$2" shift 2 ;; --force) force_override=1 shift ;; --no-timestamp) include_timestamp=0 shift ;; --no-dedupe) enable_dedupe=0 shift ;; --timestamp-format) timestamp_format="$2" shift 2 ;; --) shift while [[ $# -gt 0 ]]; do parts+=("$1") shift done ;; *) parts+=("$1") shift ;; esac done local current_value="${!target_var:-}" if [[ -n "$explicit_name" ]]; then if ! kvm::util::validate_vm_name "$explicit_name"; then write_error "⛔ Invalid VM name requested: ${explicit_name}" return 1 fi printf -v "$target_var" '%s' "$explicit_name" system::log_item "VM name (${target_var}) set explicitly: ${!target_var}" return 0 fi if [[ -n "$current_value" && $force_override -eq 0 ]]; then system::log_item "VM name (${target_var}) already set: ${current_value}" return 0 fi if [[ ${#parts[@]} -eq 0 ]]; then write_error "⛔ Unable to generate VM name: no default parts provided." return 1 fi local filtered_parts=() for part in "${parts[@]}"; do [[ -n "$part" ]] && filtered_parts+=("$part") done if (( include_timestamp )); then filtered_parts+=("$(date +"$timestamp_format")") fi local deduped_parts=() for element in "${filtered_parts[@]}"; do if (( enable_dedupe )); then local skip=0 local el_lower=${element,,} for existing in "${deduped_parts[@]}"; do local ex_lower=${existing,,} if [[ "$el_lower" == "$ex_lower" || "$el_lower" == *"$ex_lower"* || "$ex_lower" == *"$el_lower"* ]]; then skip=1 break fi done (( skip )) && continue fi deduped_parts+=("$element") done local generated="" for element in "${deduped_parts[@]}"; do if [[ -z "$generated" ]]; then generated="$element" else generated+="_${element}" fi done if ! kvm::util::validate_vm_name "$generated"; then write_error "⛔ Generated VM name '${generated}' is invalid." return 1 fi printf -v "$target_var" '%s' "$generated" system::log_item "VM name (${target_var}) set to default: ${!target_var}" # Keep hostname aligned with the generated VM name so roles are reflected in both. vm_hostname="${!target_var}" return 0 } kvm::util::config_label() { # Description: Build a human-friendly configuration label (e.g. "Minecraft-Server") from a raw token. # Arguments: # $1 - raw configuration/role string (e.g. minecraft, lamp-server, gnome-desktop). # $2 - optional role hint (e.g. server) to append "-Server" when missing. # Returns: # Prints the formatted label to stdout. local raw="${1:-}" local role_hint="${2:-}" [[ -z "$raw" ]] && return 0 # Normalize separators to spaces for easier word parsing. local cleaned="${raw//[_]/ }" cleaned=${cleaned//-/ } cleaned=${cleaned//./ } cleaned=${cleaned//\// } cleaned=$(tr -s '[:space:]' ' ' <<<"$cleaned") local words=() for word in $cleaned; do [[ -z "$word" ]] && continue word=${word,,} word=${word^} words+=("$word") done local label if (( ${#words[@]} )); then label=$(IFS=-; echo "${words[*]}") else label="$raw" fi if [[ "${role_hint,,}" == "server" && "${label,,}" != *"server"* ]]; then label="${label}-Server" fi echo "$label" } kvm::util::read_common_options() { # Description: Function to read common options for KVM virtual machine creation. This function processes # various parameters such as role, memory, CPU, disk size, and t-shirt size, setting them for the VM. # The function also validates the parameters and sets the prompt option for interactive configuration # if requested by the user. # # Globals: _TSHIRT_CPU_* _TSHIRT_MEM_* _TSHIRT_DSK_* for small|medioum|large|extra-large|extra-extra-large # as is saved in a configuration file for the user VM preferences. # Arguments: $@ - The parameters to process. # Outputs: Status and error messages # Returns: 0 if successful, 1 if an error occurs # Usage: kvm::util::read_common_options --role workstation|desktop|VDI|server --cpu 2 --memory 2048 --disk 100 # --tshirt-size small|medium|large|extra-large|extra-extra-large --ask YES|NO # End of documentation # Log the start of reading common options for KVM VM creation system::log_item "Reading common options for KVM virtual machine creation..." system::log_item "Parameters: $*" ALL_PARAMS=("$@") # Ensure that the parameters are in a valid format system::validate_parameters "${ALL_PARAMS[@]}" # Process each argument passed to the function while [[ $# -gt 0 ]]; do case "$1" in --vmname | --VMName | --vm-name | --vm_name | --hostname | --HostName | --host-name | --Host-Name) if [[ -z "$2" ]]; then write_error "⛔ Missing value for VM name." return 1 fi if ! kvm::util::set_vm_name --name "$2"; then return 1 fi write_status "🖥️ VM Name: ${vm_name}" shift 2 ;; --role | --Role) # Handle the role option: validate and set the role if valid if [[ $2 == "workstation" || $2 == "desktop" || $2 == "VDI" || $2 == "server" ]]; then _role="${2}" write_status "🎭 Selecting role: ${2}" else write_error "⛔ Unknown role selection: ${2} ==> Use workstation, desktop, VDI, or server" return 1 fi shift 2 ;; --ask | --prompt | --interactive) # Handle the interactive prompt option: validate and set the prompt option if valid if [[ $2 == "YES" || $2 == "yes" || $2 == "Yes" || $2 == "NO" || $2 == "No" || $2 == "no" ]]; then _ask="${2}" write_status "🤔 Enabled prompting for configuration options..." else write_error "⛔ Incorrect option: ${2} ==> Use YES or NO" return 1 fi shift 2 ;; --ram | --memory | -m) # Handle the memory option: set the memory size _mem="${2}" write_status "🖥️ Memory: ${_mem}" shift 2 ;; --cpu | -c) # Handle the CPU option: set the number of CPUs _cpu="${2}" write_status "🖥️ CPU's: ${_cpu}" shift 2 ;; --disk | -d) # Handle the disk option: set the disk size _dsk="${2}" write_status "🖥️ Disk Size: ${_dsk}" shift 2 ;; --tshirt-size | -t) # Handle the T-shirt size option: set CPU, memory, and disk size based on the selected size case "${2}" in "small" | "S" | "s") _cpu=${_TSHIRT_CPU_SMALL:-"2"} _mem=${_TSHIRT_MEM_SMALL:-"2048"} _dsk=${_TSHIRT_DSK_SMALL:-"100"} write_status "👕 T-Shirt Size Small: CPU: ${_cpu} Memory: ${_mem} Disk: ${_dsk}" ;; "medium" | "M" | "m") _cpu=${_TSHIRT_CPU_MEDIUM:-"4"} _mem=${_TSHIRT_MEM_MEDIUM:-"4096"} _dsk=${_TSHIRT_DSK_MEDIUM:-"100"} write_status "👕 T-Shirt Size Medium: CPU: ${_cpu} Memory: ${_mem} Disk: ${_dsk}" ;; "large" | "L" | "l") _cpu=${_TSHIRT_CPU_LARGE:-"8"} _mem=${_TSHIRT_MEM_LARGE:-"8192"} _dsk=${_TSHIRT_DSK_LARGE:-"100"} write_status "👕 T-Shirt Size Large: CPU: ${_cpu} Memory: ${_mem} Disk: ${_dsk}" ;; "extra-large" | "XL" | "xl") _cpu=${_TSHIRT_CPU_XL:-"16"} _mem=${_TSHIRT_MEM_XL:-"16384"} _dsk=${_TSHIRT_DSK_XL:-"100"} write_status "👕 T-Shirt Size Extra Large: CPU: ${_cpu} Memory: ${_mem} Disk: ${_dsk}" ;; "extra-extra-large" | "XXL" | "xxl") _cpu=${_TSHIRT_CPU_XXL:-"32"} _mem=${_TSHIRT_MEM_XXL:-"32768"} _dsk=${_TSHIRT_DSK_XXL:-"100"} write_status "👕 T-Shirt Size Extra Extra Large: CPU: ${_cpu} Memory: ${_mem} Disk: ${_dsk}" ;; esac shift 2 ;; *) # Log any unknown parameters and ignore them system::log_item "Unknown common parameter:[$1] - ignoring..." shift 2 ;; esac done return 0 } kvm::util::read_distro_options() { # Description: Function to read distribution specific options for KVM virtual machine creation. This function processes # various parameters such as desktop environment, server role, and source URL, setting them for the VM. # It also handles interactive prompts for configuration options if specified. # Globals: None # Arguments: $@ - The parameters to process. # Outputs: Status and error messages # Returns: 0 if successful, 1 if an error occurs # Usage: kvm::util::read_distro_options --desktop_environment @deepin-desktop-environment --server_role @web-server-environment --source https://dl.fedoraproject.org/pub/fedora/linux/releases/39/Everything/x86_64/os/ # # Please note that the desktop environment and server role options are distribution specific and may not be available for all distributions. # Therefore the function wiill attempt to determine the distribution from the calling function name and set the options accordingly, if # the distribution is not set explicitly. # # End of documentation # Log the start of reading distribution specific options for KVM VM creation system::log_item "Reading distribution specific options for KVM virtual machine creation..." system::log_item "Parameters: $*" ALL_PARAMS=("$@") # Ensure that the parameters are in a valid format system::validate_parameters "${ALL_PARAMS[@]}" # process each argument passed to the function and find the distributoin option if it exists system::log_item "Reading if distribution has been explicitly requested..." while [[ $# -gt 0 ]]; do case "$1" in --distribution | --distro | --dist | -d) # Handle the distribution option: set the distribution _distribution="$2" write_status "🐧 Distribution requested explicitly: ${_distribution}" shift 2 ;; --winver | --windows-version | --windows | --win | -w) # Handle the Windows version option: set the Windows version _distribution="Windows" write_status "🪟 Distribution requested explicitly: ${_distribution}" shift 2 ;; *) shift 2 ;; esac done # If a distribution is not set, try to determine it from the calling function name if [[ -z "${_distribution}" ]]; then # List of distributions to check system::log_item "Determining distribution from function name; looking for fedora, ubuntu, debian, suse, centos, rhel, zorin, or microsoft..." distributions=("fedora" "ubuntu" "debian" "suse" "centos" "rhel" "microsoft" "zorin") # Iterate over the distributions and check if the function name contains one of them for distro in "${distributions[@]}"; do if [[ ${FUNCNAME[1]} == *"$distro"* ]]; then _distribution="$distro" system::log_item "Distribution determined from function name: ${_distribution}" break fi done else system::log_item "Distribution set by caller: ${_distribution}" fi # Finally check if the distribution is set, if not log an error if [[ -z "${_distribution}" ]]; then # Log an error if the distribution is not set write_error "⛔ Distribution not set or determined, ingoring any speciffic settings and continuing with defaults..." fi # Process known distribution speciffic options kvm::util::read_distro_options::process_all_params() { case "${_distribution}" in "fedora" | "Fedora" | "FEDORA") system::log_item "🐧 Set the requested options for Fedora..." while [[ $# -gt 0 ]]; do case "${1}" in --desktop_environment | --DE) if [[ $2 == "@deepin-desktop-environment" || $2 == "@sway-desktop-environment" || $2 == "@basic-desktop-environment" || $2 == "@i3-desktop-environment" || $2 == "@developer-workstation-environment" || $2 == "@budgie-desktop-environment" || $2 == "@sugar-desktop-environment" || $2 == "@mate-desktop-environment" || $2 == "@kde-desktop-environment" || $2 == "@xfce-desktop-environment" || $2 == "@lxde-desktop-environment" || $2 == "@lxqt-desktop-environment" || $2 == "@cinnamon-desktop-environment" || $2 == "@mate-desktop-environment" || $2 == "@workstation-product-environment" ]]; then _UserDesktopEnvironmentSelection="${2}" write_host --cyan "🖥️ Selecting desktop environment: ${_UserDesktopEnvironmentSelection}" else write_error "⛔ Unknown Desktop Environment: $2 ==> Use deepin-desktop-environment, sway-desktop-environment, basic-desktop-environment, i3-desktop-environment, developer-workstation-environment, budgie-desktop-environment, sugar-desktop-environment, mate-desktop-environment, kde-desktop-environment, xfce-desktop-environment, lxde-desktop-environment, lxqt-desktop-environment, cinnamon-desktop-environment, mate-desktop-environment, workstation-product-environment" return 1 fi shift 2 ;; --server_role | --ServerRole | --SR) if [[ $2 == "@minimal-environment" || $2 == "@web-server-environment" || $2 == "@freeipa-server" || $2 == "@network-server" || $2 == "@cloud-server-environment" || $2 == "@infrastructure-server-environment" ]]; then _UserServerEnvironemtSelection="${2}" write_status "🎭 Selecting server role: ${2}" else write_error "⛔ Unknown server role selection: ${2} ==> Use web-server-environment, freeipa-server, network-server, cloud-server-environment, infrastructure-server-environment" return 1 fi shift 2 ;; --source | --URL | --url) _repo_url="${2}" write_status "🌐 Using URL: ${_repo_url}" shift 2 ;; --pre-config | --preconfig | --pre-config | --preconfigure | --pre-configure | --pre-configuration | --preconfiguration) pre_config="$2" system::log_item "🔧 Pre-configuration requested: ${pre_config}" shift 2 ;; *) shift 2 ;; esac done ;; "ubuntu" | "Ubuntu" | "UBUNTU") while [[ $# -gt 0 ]]; do case "${1}" in --CloudConfigDir | cloudconfigdir) system::log_item "Save cloud config files to incerted into the VM: ${2}" CloudConfigDir="$2" shift 2 ;; --saveto | --location | --path | --dir | --directory | -p) _saveto="$2" system::log_item "📜 Directed to create preseed instructions file: ${_saveto}/preseed.cfg" shift 2 ;; --file | --preseed | --preseed_file | --preseed_cfg | --preseed_cfg_file | --preseed_file) _preseed_file="$2" system::log_item "📜 Directed to create preseed instructions file: ${_preseed_file}" shift 2 ;; --flavor | --Flavor | --FLAVOR | --flavour | --Flavour | --FLAVOUR) # Ubuntu flavor to use (e.g. ubuntu, kubuntu, lubuntu, xubuntu, etc.) _flavor="$2" shift 2 ;; --desktop_environment | --DesktopEnvironment | --desktop-environment | --Desktop-Environment | --DE | --de) if [[ $2 == "ubuntu-desktop" || $2 == "kubuntu-desktop" || $2 == "xubuntu-desktop" || $2 == "lubuntu-desktop" || $2 == "ubuntu-mate-desktop" || $2 == "ubuntu-budgie-desktop" || $2 == "cinnamon-desktop-environment" ]]; then _UserDesktopEnvironmentSelection="${2}" system::log_item "🖥️ Selecting desktop environment: ${_UserDesktopEnvironmentSelection}" else system::log_item "⛔ Unknown Desktop Environment: [${2}] ==> Use ubuntu-desktop, kubuntu-desktop, xubuntu-desktop, lubuntu-desktop, ubuntu-mate-desktop, ubuntu-budgie-desktop" return 1 fi shift 2 ;; --server-role | --Server-Role | --server_role | --ServerRole | --SR | --sr) if [[ $2 == "ubuntu-server" || $2 == "server-desktop" || $2 == "minecraft" || $2 == "lamp-server" || $2 == "postfix" || $2 == "bind9" || $2 == "dnsmasq" || $2 == "dhcp-server" || $2 == "print-server" || $2 == "samba" ]]; then _UserServerEnvironemtSelection="${2}" system::log_item "🎭 Selecting server role: ${2}" else system::log_item "⛔ Unknown server role selection: ${2} ==> Use open-ssh-server, server-desktop, lamp-server, postfix, bind9, dnsmasq, dhcp-server, print-server, samba" return 1 fi shift 2 ;; --server-app | --Server-Application | --server_application | --ServerApplications | --SA | --sa) server_app="${2}" system::log_item "🖥️ Selecting server app: ${server_app}" shift 2 ;; --version | -v) _tgt_ubuntu_ver="$2" system::log_item "🐧 Ubuntu version requested: ${_tgt_ubuntu_ver}" shift 2 return 0 ;; --pre-config | --preconfig | --pre-config | --preconfigure | --pre-configure | --pre-configuration | --preconfiguration) pre_config="$2" system::log_item "🔧 Pre-configuration requested: ${pre_config}" shift 2 ;; *) system::log_item "Unknown option: $1 - ignoring..." shift 2 ;; esac done ;; "debian" | "Debian" | "DEBIAN") while [[ $# -gt 0 ]]; do case "${1}" in --desktop_environment | --DE) if [[ $2 == "gnome-desktop" || $2 == "standard" || $2 == "kde-desktop" || $2 == "lxde-desktop" || $2 == "xfce-desktop" || $2 == "mate-desktop" || $2 == "cinnamon-desktop" ]]; then _UserDesktopEnvironmentSelection="${2}" write_host --cyan "🖥️ Selecting desktop environment: ${_UserDesktopEnvironmentSelection}" else write_error "⛔ Unknown Desktop Environment: $2 ==> Use standard, gnome-desktop, kde-desktop, lxde-desktop, xfce-desktop, mate-desktop, cinnamon-desktop" return 1 fi shift 2 ;; --server_role | --ServerRole | --SR) if [[ $2 == "openssh-server" || $2 == "lamp-server" || $2 == "postfix" || $2 == "bind9" || $2 == "dnsmasq" || $2 == "dhcp-server" || $2 == "print-server" ]]; then _UserServerEnvironemtSelection="${2}" write_status "🎭 Selecting server role: ${2}" else write_error "⛔ Unknown server role selection: ${2} ==> Use web-server-environment, freeipa-server, network-server, cloud-server-environment, infrastructure-server-environment" return 1 fi shift 2 ;; --saveto | --location | --path | --dir | --directory | -p) _saveto="$2" write_host --cyan "📜 Directed to create preseed instructions file: ${_saveto}/preseed.cfg" shift 2 ;; --file | --preseed | --preseed_file | --preseed_cfg | --preseed_cfg_file | --preseed_file) _preseed_file="$2" write_host --cyan "📜 Directed to create preseed instructions file: ${_preseed_file}" shift 2 ;; --pre-config | --preconfig | --pre-config | --preconfigure | --pre-configure | --pre-configuration | --preconfiguration) pre_config="$2" system::log_item "🔧 Pre-configuration requested: ${pre_config}" shift 2 ;; --pkg_list | --pkglist | --pkg-list | --package_list | --package-list | --package_list) _pkg_list="$2" system::log_item "📦 Package list requested: ${_pkg_list}" pkgsel_include_string="d-i pkgsel/include string ${_pkg_list}" shift 2 ;; *) shift 2 ;; esac done ;; "suse" | "SUSE" | "Suse" | "openSUSE" | "OpenSUSE" | "opensuse" | "OpenSuse" | "OPENSUSE" | "OPENSuse" | "OPENSUSE") while [[ $# -gt 0 ]]; do case "${1}" in --source_url | --url | --source | --URL) source_url="${2}" system::log_item "📋 instructed to use source: ${_filename}" shift 2 ;; --filename | --file) _autoyast_filename="$2" system::log_item "AutoYaST file will be saved as: ${_autoyast_filename}" shift 2 ;; --desktop_environment | --DE) if [[ $2 == "kde" || $2 == "gnome" || $2 == "xfce" || $2 == "lxde" || $2 == "mate" || $2 == "cinnamon" || $2 == "enlightenment" || $2 == "lxqt" ]]; then _UserDesktopEnvironmentSelection="${2}" system::log_item "Desktop selection: ${_UserDesktopEnvironmentSelection}" else write_error "⛔ Unknown Desktop Environment: $2" return 1 fi shift 2 ;; --server_role | --ServerRole | --SR) if [[ $2 == "lamp" || $2 == "dns" || $2 == "dhcp_dns_server" || $2 == "file_server" || $2 == "print_server" || $2 == "mail_server" || $2 == "monitoring" || $2 == "desktop" || $2 == "gateway_server" || $2 == "kvm_server" || $2 == "sap-bone" || $2 == "sap-hana" || $2 == "sap-nw" || $2 == "sap_server" || $2 == "xen_server" || $2 == "yast2_server" || $2 == "directory_server" ]]; then _UserServerEnvironemtSelection="${2}" system::log_item "Server selection: ${_UserServerEnvironemtSelection}" else write_error "⛔ Unknown Server selection: $2" return 1 fi shift 2 ;; --product ) if [[ $2 == "Leap" || $2 == "SLES" || $2 == "Tumbleweed" ]]; then _UserProductSelection="$2" system::log_item "Product selection: ${_UserProductSelection}" # Set the default source URL for the SUSE version if not already set... # NOTE: Using --source_url first, then the global variable (from _locations.info), then the default. case ${_UserProductSelection} in "Leap") : ${source_url:="${_SUSE_LEAP_SOURCE:-"https://download.opensuse.org/distribution/openSUSE-current/repo/oss/"}"} _Product="Leap" ;; "SLES") : ${source_url:="${_SUSE_SLES_SOURCE:-"https://download.opensuse.org/distribution/leap/15.5/repo/oss/"}"} _Product="SUSE" ;; "Tumbleweed") : ${source_url:="${_SUSE_TUMBLEWEED_SOURCE:-"https://download.opensuse.org/tumbleweed/repo/oss/"}"} _Product="openSUSE" ;; *) : ${source_url:="${_SUSE_LEAP_SOURCE:-"https://download.opensuse.org/distribution/openSUSE-current/repo/oss/"}"} _Product="Leap" ;; esac else write_error "⛔ Unknown product selection: $2" return 1 fi shift 2 ;; --disk_encryption | --DC) if [[ $2 == "YES" || $2 == "NO" ]]; then Preference_DiskEncryption="$2" system::log_item "Disk encryption: ${Preference_DiskEncryption}" else write_error "⛔ Unknown disk encryption selection: $2" return 1 fi shift 2 ;; --disk_password | --DP) Preference_Disk_Password="$2" system::log_item "Disk encryption password set." shift ;; --saveto | --location | --path | --dir | --directory | -p) _saveto="$2" write_host --cyan "📜 Directed to create instructions file: ${_saveto}/preseed.cfg" shift 2 ;; --file) _preseed_file="$2" write_host --cyan "📜 Directed to create instructions file: ${_preseed_file}" shift 2 ;; --pre-config | --preconfig | --pre-config | --preconfigure | --pre-configure | --pre-configuration | --preconfiguration) pre_config="$2" system::log_item "🔧 Pre-configuration requested: ${pre_config}" shift 2 ;; *) shift 2 ;; esac done ;; "rhel" | "RHEL" | "RedHat" | "REDHAT" | "Almalinux" | "ALMALINUX" | "RockyLinux" | "ROCKYLINUX" | "CentOS" | "CENTOS" | "centos" ) system::log_item "🐧 Set the requested options for RedHat..." while [[ $# -gt 0 ]]; do case "${1}" in --desktop_environment | --DE) if [[ $2 == "@workstation-product" ]]; then _UserDesktopEnvironmentSelection="${2}" system::log_item --cyan "🖥️ Selecting desktop environment: ${_UserDesktopEnvironmentSelection}" else system::log_item "⛔ Unknown Desktop Environment: $2 ==> Use workstation-product" return 1 fi shift 2 ;; --server_role | --ServerRole | --SR) if [[ $2 == "@minimal-environment" || $2 == "@web-server-environment" || $2 == "@freeipa-server" || $2 == "@network-server" || $2 == "@cloud-server-environment" || $2 == "@infrastructure-server-environment" ]]; then _UserServerEnvironemtSelection="${2}" system::log_item "🎭 Selecting server role: ${2}" else system::log_item "⛔ Unknown server role selection: ${2} ==> Use web-server-environment, freeipa-server, network-server, cloud-server-environment, infrastructure-server-environment" return 1 fi shift 2 ;; --source | --URL | --url) _repo_url="${2}" write_status "🌐 Using URL: ${_repo_url}" shift 2 ;; *) shift 2 ;; esac done ;; "Windows" | "WINDOWS" | "Win" | "Win11" | "Win10" | "Microsoft" | "microsoft" | windows) system::log_item "🪟 Set the requested options for Windows..." while [[ $# -gt 0 ]]; do case "$1" in --winver) target_winver="$2" system::log_item "🪟 Custom Windows version requested and set to: ${target_winver}" shift 2 ;; *) shift 2 ;; esac done ;; *) # Log any unknown distribution and set the default values system::log_item "⛔ No valid distribution requested or found, ignoring distribution speciffic VM settings..." ;; esac } kvm::util::read_distro_options::process_all_params "${ALL_PARAMS[@]}" } kvm::get_vm_config_preferences() { # Description: Retrieves VM configuration values from a configuration file or defaults. # # This function loads VM configuration values from a specified configuration file or uses # default values if the configuration file is not found. It can also prompt the user to # input these values interactively. # # Globals: # - _OEM_USER_VM_PREFERENCES: Path to the user VM preferences file. # - _TLA: Three-letter acronym for the project. # Arguments: # - --ask (optional): Prompts the user to input configuration values interactively. # Outputs: # - Configuration values loaded into variables. # Returns: # - 0 on success. # - 1 on error or user cancellation. # # Usage: # kvm::get_vm_config_preferences # kvm::get_vm_config_preferences --ask # # Example: # kvm::get_vm_config_preferences # kvm::get_vm_config_preferences --ask # # Notes: # - The function uses a configuration file to load saved preferences or defaults to predefined values. # - If the --ask flag is provided, it will prompt the user to input configuration values interactively using dialog. # - Configuration values are logged and saved for troubleshooting and reuse. # # End of Documentation local _config_file="${_OEM_USER_VM_PREFERENCES:-"/home/${SUDO_USER}/.config/rtd/build_defaults.inf"}" if [[ "$1" == "--ask" ]]; then local _ask="YES" else local _ask="NO" fi system::log_item "Loading Global Config if available otherwise use defaults..." Preference_DiskEncryption="${_ENCRYPT_DISK:-"YES"}" Preference_Disk_Password="${_TEMP_DISK_PASSWORD:-"letmein1234"}" Preference_InitialUser="${_OEM_USER:-"tangarora"}" Preference_InitialUserFullName="${_OEM_USER_FULLNAME:-"RTD User"}" Preference_InitialUserGroup="${_OEM_USER_GROUP:-"sudo"}" Preference_InitialUserPassword="${_OEM_USER_PASSWORD:-"letmein1234"}" Preference_InitialKeyboardLayout="${_OEM_USER_KEYBOARD_LAYOUT:-"us"}" Preference_InitialLanguage="${_OEM_USER_LANGUAGE:-"en_US"}" Preference_InitialLanguageEncoding="${_OEM_USER_LANGUAGE:-"UTF8"}" Preference_InitialTimeZone="${_OEM_USER_TIMEZONE:-"Etc/UTC"}" Preference_Wireless_ID="${_OEM_WIRELESS_ID:-"loader"}" Preference_Wireless_Password="${_OEM_WIRELESS_PASSWORD:-"letmein1234"}" Preference_Role="${_role:-"default"}" Preference_CopySSHKey="${_OEM_COPY_SSH_KEY:-"NO"}" system::log_item "Load saved preferences from the config file everriding the _branding global config or defaults for preseed file..." [[ -f "${_config_file}" ]] && system::read_config "${_config_file}" || system::log_item "Failed to load config file: ${_config_file}" # If OS Role is set (by function for auto install), then override the config file with the role: [[ -n $_role ]] && Preference_Role="${_role}" # If the user has requested to be asked for the preseed file, then ask them: if [[ "$_ask" == "YES" ]]; then system::log_item "'ask' is requested, requesting user input for the preseed file:" preseed_choices=$(dialog --backtitle "$BRANDING" --title "RTD Preseed Creator" --mixedform "Enter your preferences in to the form below. Please note that the password fields do not show anything but will capture what you type in" "${HEIGHT:-"22"}" "${WIDTH:-"80"}" "${LIST_HEIGHT:-"0"}" \ "DiskEncryption (YES/NO):" 1 1 "${Preference_DiskEncryption}" 1 40 40 0 0 \ "Disk Unlock Password (temp):" 2 1 "${Preference_Disk_Password}" 2 40 40 0 0 \ "Admin User Name:" 3 1 "${Preference_InitialUser}" 3 40 40 0 0 \ "Admin User Full Name:" 4 1 "${Preference_InitialUserFullName}" 4 40 40 0 0 \ "Admin User Initial UserGroup:" 5 1 "${Preference_InitialUserGroup}" 5 40 40 0 0 \ "Admin User Password (8 30 char):" 6 1 "${Preference_InitialUserPassword}" 6 40 128 0 0 \ "Preferred Keyboard Layout:" 7 1 "${Preference_InitialKeyboardLayout}" 7 40 40 0 0 \ "Preferred Language" 8 1 "${Preference_InitialLanguage}" 8 40 40 0 0 \ "Preferred Encoding" 9 1 "${Preference_InitialLanguageEncoding}" 9 40 40 0 0 \ "Initial Time Zone:" 10 1 "${Preference_InitialTimeZone}" 10 40 40 0 0 \ "Wireless Network SSID:" 11 1 "${Preference_Wireless_ID}" 11 40 40 0 0 \ "Wireless Network Password:" 12 1 "${Preference_Wireless_Password}" 12 40 40 0 0 \ "Role (Default, KVM, Minecraft):" 13 1 "${Preference_Role}" 13 40 40 0 0 \ "Copy you ssh public key to host?" 14 1 "${Preference_CopySSHKey}" 14 40 40 0 0 \ 3>&1 1>&2 2>&3) exit_status=$? clear case $exit_status in 0) system::log_item "Save coices and continue..." ;; 1) system::log_item "[Cancel] pressed; not updating or changing any configuration items..." return 1 ;; 255) system::log_item "[ESC] pressed, exiting the dialog immediatley..." return 1 ;; *) system::log_item "[ERROR] Dialog closed or unexpected error." pause::for_input 1 "Dialog closed or unexpected error." ;; esac # Convert the dialog output to an array and asign the variables: IFS=$'\n' read -r -d '' -a input_array <<<"$preseed_choices" Preference_DiskEncryption=${input_array[0]} Preference_Disk_Password=${input_array[1]} Preference_InitialUser=${input_array[2]} Preference_InitialUserFullName=${input_array[3]} Preference_InitialUserGroup=${input_array[4]} Preference_InitialUserPassword=${input_array[5]} Preference_InitialKeyboardLayout=${input_array[6]} Preference_InitialLanguage=${input_array[7]} Preference_InitialLanguageEncoding=${input_array[8]} Preference_InitialTimeZone=${input_array[9]} Preference_Wireless_ID=${input_array[10]} Preference_Wireless_Password=${input_array[11]} Preference_Role=${input_array[12]} Preference_CopySSHKey=${input_array[13]} # Encrypt the user/admin password before saving & for use in the preseed file # If the password is already encrypted, then skip this step: if [[ ${#Preference_InitialUserPassword} -ge 30 ]]; then system::log_item "Password appears to be encrypted already so skipping encryption..." else system::log_item "Password appears to be clear text, encrypting password..." hash mpasswd || system::install_missing_commands "mkpasswd" Preference_InitialUserPassword=$(printf "%s" "$Preference_InitialUserPassword" | mkpasswd -s -m sha-512) if system::update_config "$_config_file" "Preference_InitialUserPassword" "${Preference_InitialUserPassword}"; then system::log_item "Stored encrypted password in user preferences: $_config_file" else system::log_item "[ERROR] Failed to store encrypted password in user preferences: $_config_file" fi fi system::log_item "Save as defaults and continue..." write_information "Saving preferences to: $_config_file" system::update_config "$_config_file" \ "Preference_DiskEncryption" "${Preference_DiskEncryption}" \ "Preference_Disk_Password" "${Preference_Disk_Password}" \ "Preference_InitialUser" "${Preference_InitialUser}" \ "Preference_InitialUserFullName" "${Preference_InitialUserFullName}" \ "Preference_InitialUserGroup" "${Preference_InitialUserGroup}" \ "Preference_InitialKeyboardLayout" "${Preference_InitialKeyboardLayout}" \ "Preference_InitialLanguage" "${Preference_InitialLanguage}" \ "Preference_InitialLanguageEncoding" "${Preference_InitialLanguageEncoding}" \ "Preference_InitialTimeZone" "${Preference_InitialTimeZone}" \ "Preference_Wireless_ID" "${Preference_Wireless_ID}" \ "Preference_Wireless_Password" "${Preference_Wireless_Password}" \ "Preference_Role" "${Preference_Role}" \ "Preference_CopySSHKey" "${Preference_CopySSHKey}" return $? else write_information "Prompting for VM settings is not requested: ${_role} ${_task} automatically using defaults or parameter instructions..." return 0 fi } kvm::get_vm_size_preferences() { # Description: Loads VM sizing defaults from _locations.info or internal fallbacks and # optionally prompts the user to update them, persisting any changes to the user config. # # This helper mirrors kvm::get_vm_config_preferences but focuses on T-shirt sizing values # used when creating new virtual machines. It prefers values defined in core/_locations.info, # falls back to hard-coded defaults when unavailable, and allows interactive updates when # invoked with --ask. Updated values are written to the user preference file so future runs # reuse the selections. # # Globals: # - _OEM_USER_VM_SIZE_PREFERENCES: Path to the user VM preferences file. # - _TLA: Three-letter acronym for the project. # Arguments: # - --ask (optional): Prompts the user to input sizing values interactively. # Outputs: # - Populates _TSHIRT_CPU_*, _TSHIRT_MEM_*, _TSHIRT_DSK_* and matching Preference_* globals. # Returns: # - 0 on success. # - 1 on dialog cancel or error. # # Usage: # kvm::get_vm_size_preferences # kvm::get_vm_size_preferences --ask # # Notes: # - Requires the dialog utility if --ask is used. # - Persists updates via system::update_config using the same file as other VM preferences. # # End of documentation if [[ $EUID -ne 0 ]]; then SUDO_USER=$USER fi local _config_file=${_OEM_USER_VM_SIZE_PREFERENCES:-"${_CONFIG_DIR}/vm_user_build_defaults.info"} local _ask="NO" [[ "$1" == "--ask" ]] && _ask="YES" local -a _sizes=("SMALL" "MEDIUM" "LARGE" "XL" "XXL") local -a _metrics=("CPU" "MEM" "DSK") local -A _defaults=( ["CPU_SMALL"]="2" ["MEM_SMALL"]="2048" ["DSK_SMALL"]="100" ["CPU_MEDIUM"]="4" ["MEM_MEDIUM"]="4096" ["DSK_MEDIUM"]="100" ["CPU_LARGE"]="8" ["MEM_LARGE"]="8192" ["DSK_LARGE"]="100" ["CPU_XL"]="16" ["MEM_XL"]="16384" ["DSK_XL"]="100" ["CPU_XXL"]="32" ["MEM_XXL"]="32768" ["DSK_XXL"]="100" ) # Initialize T-shirt sizing variables with defaults if not already set for size in "${_sizes[@]}"; do for metric in "${_metrics[@]}"; do local key="${metric}_${size}" local tshirt_var="_TSHIRT_${key}" local pref_var="Preference_TShirt_${key}" local default_value="${_defaults[$key]}" local current="${!tshirt_var}" if [[ -z "$current" ]]; then printf -v "$tshirt_var" '%s' "$default_value" current="$default_value" fi local pref_current="${!pref_var}" if [[ -z "$pref_current" ]]; then printf -v "$pref_var" '%s' "$current" fi done done [[ -f "${_config_file}" ]] && system::read_config "${_config_file}" for size in "${_sizes[@]}"; do for metric in "${_metrics[@]}"; do local key="${metric}_${size}" local tshirt_var="_TSHIRT_${key}" local pref_var="Preference_TShirt_${key}" local pref_value="${!pref_var}" local fallback="${!tshirt_var}" if [[ -n "$pref_value" ]]; then printf -v "$tshirt_var" '%s' "$pref_value" elif [[ -n "$fallback" ]]; then printf -v "$pref_var" '%s' "$fallback" else local default_value="${_defaults[$key]}" printf -v "$tshirt_var" '%s' "$default_value" printf -v "$pref_var" '%s' "$default_value" fi done done if [[ "$_ask" == "YES" ]]; then system::log_item "'ask' is requested, requesting user input for VM sizing defaults:" local sizing_choices exit_status sizing_choices=$(dialog --backtitle "$BRANDING" --title "RTD VM Sizing Defaults" \ --mixedform "Update the default T-Shirt sizing values (CPU cores, Memory MB, Disk GB)." 24 80 0 \ "Small CPU Cores:" 1 1 "${Preference_TShirt_CPU_SMALL}" 1 40 10 0 0 \ "Small Memory (MB):" 2 1 "${Preference_TShirt_MEM_SMALL}" 2 40 10 0 0 \ "Small Disk (GB):" 3 1 "${Preference_TShirt_DSK_SMALL}" 3 40 10 0 0 \ "Medium CPU Cores:" 5 1 "${Preference_TShirt_CPU_MEDIUM}" 5 40 10 0 0 \ "Medium Memory (MB):" 6 1 "${Preference_TShirt_MEM_MEDIUM}" 6 40 10 0 0 \ "Medium Disk (GB):" 7 1 "${Preference_TShirt_DSK_MEDIUM}" 7 40 10 0 0 \ "Large CPU Cores:" 9 1 "${Preference_TShirt_CPU_LARGE}" 9 40 10 0 0 \ "Large Memory (MB):" 10 1 "${Preference_TShirt_MEM_LARGE}" 10 40 10 0 0 \ "Large Disk (GB):" 11 1 "${Preference_TShirt_DSK_LARGE}" 11 40 10 0 0 \ "XL CPU Cores:" 13 1 "${Preference_TShirt_CPU_XL}" 13 40 10 0 0 \ "XL Memory (MB):" 14 1 "${Preference_TShirt_MEM_XL}" 14 40 10 0 0 \ "XL Disk (GB):" 15 1 "${Preference_TShirt_DSK_XL}" 15 40 10 0 0 \ "XXL CPU Cores:" 17 1 "${Preference_TShirt_CPU_XXL}" 17 40 10 0 0 \ "XXL Memory (MB):" 18 1 "${Preference_TShirt_MEM_XXL}" 18 40 10 0 0 \ "XXL Disk (GB):" 19 1 "${Preference_TShirt_DSK_XXL}" 19 40 10 0 0 \ 3>&1 1>&2 2>&3) exit_status=$? clear case $exit_status in 0) system::log_item "Saving updated VM sizing defaults..." ;; 1) system::log_item "[Cancel] pressed; retaining existing VM sizing defaults..." return 1 ;; 255) system::log_item "[ESC] pressed; exiting VM sizing dialog..." return 1 ;; *) system::log_item "[ERROR] Dialog closed unexpectedly when updating VM sizing defaults." return 1 ;; esac local -a input_array IFS=$'\n' read -r -d '' -a input_array <<<"$sizing_choices"$'\0' local index=0 for size in "${_sizes[@]}"; do for metric in "${_metrics[@]}"; do local pref_var="Preference_TShirt_${metric}_${size}" local tshirt_var="_TSHIRT_${metric}_${size}" local value="${input_array[$index]}" local fallback="${!tshirt_var}" if [[ -z "$value" ]]; then value="$fallback" elif ! [[ "$value" =~ ^[0-9]+$ ]]; then system::log_item "⚠️ Invalid value '${value}' for ${pref_var}; keeping previous setting '${fallback}'." value="$fallback" fi printf -v "$pref_var" '%s' "$value" printf -v "$tshirt_var" '%s' "$value" ((index++)) done done local -a update_args=() for size in "${_sizes[@]}"; do for metric in "${_metrics[@]}"; do local pref_var="Preference_TShirt_${metric}_${size}" update_args+=("$pref_var" "${!pref_var}") done done system::update_config "${_config_file}" "${update_args[@]}" write_information "Saved VM sizing defaults to: ${_config_file}" else write_information "Using saved or default VM sizing preferences without prompting..." fi return 0 } kvm::get_kali_iso() { # Description: # Fetches the newest Kali Linux installer ISO (64-bit) from cdimage.kali.org # and stores it under /var/lib/libvirt/boot/. If the newest ISO already exists, # the function skips downloading. Otherwise, it downloads the ISO and prints # the local file path. # # Usage: # kvm::get_kali_iso # # Dependencies: # - curl # - wget # - system::log_item function (for logging) # # Globals: # - None directly changed, but reads/writes to the file system at: # /var/lib/libvirt/boot/ # # Outputs: # - Echos the path to the downloaded ISO on success. # - Logs progress and errors via system::log_item. # # Returns: # - 0 if successful or if ISO already exists # - 1 if the script cannot find or download the ISO # # Notes: # - Parses the top-level Kali “cdimage” directory to find the newest release folder # (e.g. kali-2024.4/) and then looks for the “installer-amd64.iso” file inside. # - Uses version-aware sorting (sort -rV) to pick the highest release number. # - If successful, the ISO file path is written to stdout. # - Make sure you have the needed permissions to write to /var/lib/libvirt/boot/. # # Example: # if kvm::get_kali_iso; then # echo "Kali ISO is ready at $(< someTempFile )" # else # echo "Failed to get the Kali ISO" # fi # End of documentation local BASE_URL="https://cdimage.kali.org" system::log_item "Finding the newest Kali release directory..." # 1) Grab all directories named "kali-YYYY.X/", sort them descending, pick the first local LATEST_DIR LATEST_DIR="$(curl -s "$BASE_URL" \ | grep -Eo 'kali-[0-9]{4}\.[0-9]+/' \ | sort -rV \ | head -n 1 )" if [[ -z "$LATEST_DIR" ]]; then system::log_item "Could not find a matching release directory on $BASE_URL" >&2 return 1 fi # Construct the subdirectory URL (e.g. https://cdimage.kali.org/kali-2024.4/) local RELEASE_URL="${BASE_URL}/${LATEST_DIR}" system::log_item "Newest directory is: $RELEASE_URL" system::log_item "Looking for the installer ISO..." # 2) From that directory, find "kali-linux-YYYY.X-installer-amd64.iso", pick the newest local LATEST_ISO LATEST_ISO="$(curl -s "$RELEASE_URL" \ | grep -Eo 'kali-linux-[0-9]{4}\.[0-9]+-installer-amd64\.iso' \ | sort -rV \ | head -n 1 )" if [[ -z "$LATEST_ISO" ]]; then system::log_item "Could not find an installer ISO in $RELEASE_URL" >&2 return 1 fi # Final download URL (e.g. https://cdimage.kali.org/kali-2024.4/kali-linux-2024.4-installer-amd64.iso) local ISO_URL="${RELEASE_URL}${LATEST_ISO}" local DEST="/var/lib/libvirt/boot/${LATEST_ISO}" # 3) Check if the file already exists locally if [[ -f "$DEST" ]]; then system::log_item "The latest Kali ISO already exists at: $DEST" system::log_item "Skipping download." iso=$DEST write_information "$iso" return 0 else # 4) Download if missing system::log_item "Downloading $ISO_URL -> $DEST" wget -q --show-progress -O "$DEST" "$ISO_URL" || { system::log_item "Failed to download $ISO_URL" >&2 return 1 } fi iso=$DEST write_information "$iso" return 0 } mint::get_mint_lmde_iso() { # Description: # Retrieve the latest Linux Mint Debian Edition (LMDE) Cinnamon 64-bit ISO and # store it in /var/lib/libvirt/boot/. The resulting path is echoed and exported # via the global `iso` variable for the calling VM helper to consume. # # Usage: # mint::get_mint_lmde_iso # # Returns: # 0 if the ISO is available locally (either reused or freshly downloaded), # 1 on failure to discover or download the ISO. # # End of documentation local BASE_URL="${_MINT_LMDE_MIRROR:-${MINT_LMDE_MIRROR:-https://mirrors.edge.kernel.org/linuxmint/debian/}}" local DEST_DIR="/var/lib/libvirt/boot" local listing latest_iso iso_url dest system::log_item "Discovering latest LMDE ISO from: ${BASE_URL}" listing=$(curl -fsSL --connect-timeout 10 --max-time 60 --retry 3 --retry-delay 5 --retry-connrefused \ -H "User-Agent: Mozilla/5.0 (compatible; RTD-Setup/1.0)" "${BASE_URL}") || { system::log_item "Failed to query LMDE ISO directory (${BASE_URL})." >&2 return 1 } latest_iso=$(printf '%s\n' "$listing" | grep -oE 'lmde-[0-9A-Za-z.-]+-cinnamon-64bit\.iso' | sort -V | tail -1) if [[ -z "$latest_iso" ]]; then system::log_item "Could not determine the latest LMDE ISO from ${BASE_URL}" >&2 return 1 fi iso_url="${BASE_URL%/}/${latest_iso}" dest="${DEST_DIR%/}/${latest_iso}" if [[ -f "$dest" ]]; then system::log_item "LMDE ISO already present: ${dest}" iso="$dest" write_information "$iso" return 0 fi if ! mkdir -p "$DEST_DIR"; then system::log_item "Unable to create ISO destination directory: ${DEST_DIR}" >&2 return 1 fi system::log_item "Downloading ${iso_url} -> ${dest}" if ! wget -q --show-progress -O "$dest" "$iso_url"; then system::log_item "Failed to download LMDE ISO from ${iso_url}" >&2 rm -f "$dest" return 1 fi iso="$dest" write_information "$iso" return 0 } kvm::get_almalinux_boot_iso() { # Description: # Download the AlmaLinux network boot ISO for a specific version into # /var/lib/libvirt/boot/ (if not already present) and expose the local path via `iso`. # # Globals: # _ALMALINUX_ISO_MIRROR / ALMALINUX_ISO_MIRROR overrides; otherwise defaults to the official mirror. # # Arguments: # --version AlmaLinux release to target (e.g. 9.4). # # Outputs: # Echoes the local ISO path on success. # # Returns: # 0 on success (existing or downloaded ISO), 1 on failure. # # End of documentation local version="" while [[ $# -gt 0 ]]; do case "$1" in --version) version="$2" shift 2 ;; *) shift ;; esac done if [[ -z "$version" ]]; then write_error "⛔ kvm::get_almalinux_boot_iso: Missing required --version argument." return 1 fi local base_url="${_ALMALINUX_ISO_MIRROR:-${ALMALINUX_ISO_MIRROR:-https://repo.almalinux.org/almalinux}}" local iso_dir="${base_url%/}/${version}/isos/x86_64/" local listing boot_iso minimal_iso selected_iso dest local dest_dir="/var/lib/libvirt/boot" system::log_item "Discovering AlmaLinux boot ISO from: ${iso_dir}" listing=$(curl -fsSL --connect-timeout 10 --max-time 60 --retry 3 --retry-delay 5 --retry-connrefused \ -H "User-Agent: Mozilla/5.0 (compatible; RTD-Setup/1.0)" "${iso_dir}") || { system::log_item "Failed to query AlmaLinux ISO directory (${iso_dir})." >&2 return 1 } boot_iso=$(printf '%s\n' "$listing" | grep -oE 'AlmaLinux-[0-9]+(\.[0-9]+)?-latest-x86_64-boot\.iso' | sort -V | tail -1) minimal_iso=$(printf '%s\n' "$listing" | grep -oE 'AlmaLinux-[0-9]+(\.[0-9]+)?-latest-x86_64-minimal\.iso' | sort -V | tail -1) if [[ -n "$boot_iso" ]]; then selected_iso="$boot_iso" elif [[ -n "$minimal_iso" ]]; then selected_iso="$minimal_iso" else system::log_item "Unable to locate AlmaLinux boot/minimal ISO in ${iso_dir}" >&2 return 1 fi dest="${dest_dir%/}/${selected_iso}" if [[ -f "$dest" ]]; then system::log_item "AlmaLinux ISO already present: ${dest}" iso="$dest" write_information "$iso" return 0 fi if ! mkdir -p "$dest_dir"; then system::log_item "Unable to create ISO destination directory: ${dest_dir}" >&2 return 1 fi system::log_item "Downloading ${iso_dir}${selected_iso} -> ${dest}" if ! wget -q --show-progress -O "$dest" "${iso_dir}${selected_iso}"; then system::log_item "Failed to download AlmaLinux ISO from ${iso_dir}${selected_iso}" >&2 rm -f "$dest" return 1 fi iso="$dest" write_information "$iso" return 0 } kvm::get_fedora_workstation_iso() { # Description: # Downloads the specified or latest Fedora Workstation x86_64 ISO. # Stores under /var/lib/libvirt/boot/, skips download if already present. # # Usage: # kvm::get_fedora_workstation_iso [--version ] # # Dependencies: # - curl # - wget # - system::log_item # # Outputs: # - Echos the local ISO path. # - Sets global `iso` to the local file path. # # Returns: # - 0 if successful or already present, 1 on error. # # Notes: # - If --version is provided, that version is used. # - Otherwise, the highest available release is selected. # - ISO format is: Fedora-Workstation-Live--1.1.x86_64.iso # - The function logs progress and errors using system::log_item. # # Example: # kvm::get_fedora_workstation_iso --version 38 # # kvm::get_fedora_workstation_iso # # Automatically detects the latest version and downloads it. # End of documentation system::log_item "Fetching Fedora Workstation ISO..." local BASE_URL="https://dl.fedoraproject.org/pub/fedora/linux/releases" local DEST_DIR="/var/lib/libvirt/boot" local VERSION="" iso="" # Parse arguments while [[ $# -gt 0 ]]; do case "$1" in --version) shift VERSION="$1" ;; *) system::log_item "Unknown argument: $1" >&2 return 1 ;; esac shift done # Auto-detect version if not specified if [[ -z "$VERSION" ]]; then system::log_item "No version provided, auto-detecting latest release..." VERSION="$(curl -s "${BASE_URL}/" | grep -Eo 'href="[0-9]+/' | grep -Eo '[0-9]+' | sort -rV | head -n1)" if [[ -z "$VERSION" ]]; then system::log_item "Failed to detect latest Fedora release version." >&2 return 1 fi system::log_item "Detected version: $VERSION" else system::log_item "Using specified version: $VERSION" fi local ISO_DIR_URL="${BASE_URL}/${VERSION}/Everything/x86_64/iso/" system::log_item "Looking up ISO in: ${ISO_DIR_URL}" local ISO_NAME ISO_NAME=$( curl -fsSL "$ISO_DIR_URL" | grep -Eo 'href="Fedora-Everything-netinst-x86_64-'"${VERSION}"'-[0-9]+\.[0-9]+\.iso"' | sed -E 's/^href="//; s/"$//' | sort -Vu | tail -n1 ) write_information "Found ISO: ${ISO_NAME}" if [[ -z "$ISO_NAME" ]]; then system::log_item "Could not find ISO for version $VERSION in $ISO_DIR_URL" >&2 return 1 fi local ISO_URL="${ISO_DIR_URL}${ISO_NAME}" local DEST="${DEST_DIR}/${ISO_NAME}" if [[ -f "$DEST" ]]; then system::log_item "ISO already exists: $DEST" iso=$DEST write_information "$iso" return 0 fi system::log_item "Downloading ${ISO_URL} -> ${DEST}" wget -q --show-progress -O "$DEST" "$ISO_URL" || { system::log_item "Failed to download ISO: $ISO_URL" >&2 return 1 } system::log_item "Download completed." iso=$DEST write_information "$iso" return 0 } kvm::get_zorin_iso() { # Description: Downloads the latest Zorin OS ISO file for KVM if not already present. # # This function checks the Zorin OS mirror for the latest directory and the highest version number file # within that directory. If the file does not already exist in /var/lib/libvirt/boot, it downloads the file. # # Globals: # - None # Arguments: # - Version prefix / product channel (e.g., Core, Education, Pro). Defaults to Core and can be supplied # positionally or via --product/--edition. # - Optional major version (e.g., 17, 18) to pin to a specific release directory (positional arg or --version, # also accepts the literal value "latest" for parity with tools that prefer --version latest). # - --latest forces discovery of the newest available release (ignores any numeric version argument). # Outputs: # - The path to the ISO file. # Returns: # - 0 on success. # - 1 if no directories or files are found, or if the download fails. # # Usage: # kvm::get_zorin_iso [--product ] [--version ] [--latest] # [version_prefix] [major_version] # # Example: # kvm::get_zorin_iso # kvm::get_zorin_iso Education # kvm::get_zorin_iso --product Education --version 17 # kvm::get_zorin_iso --version latest # kvm::get_zorin_iso --latest # kvm::get_zorin_iso Core 18 # explicit edition + version # # Notes: # - This function uses curl to fetch directory and file listings from the Zorin OS mirror. # - The downloaded file is saved to /var/lib/libvirt/boot. # - The function logs progress and errors using system::log_item. # # End of Documentation # Namespaced helpers keep global function table collision-free. kvm__get_zorin_iso__get_latest_directory() { local base_url=$1 local requested_directory=$2 local listing local directories listing=$(curl -fsSL --connect-timeout 10 --max-time 60 --retry 3 --retry-delay 5 --retry-connrefused -H "User-Agent: Mozilla/5.0 (compatible; RTD-Setup/1.0)" "${base_url}" 2>/dev/null) || return 1 directories=$(printf '%s\n' "$listing" | grep -oE 'href="[0-9][0-9.]*/"' | sed -E 's/.*href="([0-9][0-9.]*)\/".*/\1/' | sort -V) [ -z "$directories" ] && return 1 if [ -n "$requested_directory" ]; then if printf '%s\n' "$directories" | grep -Fx "$requested_directory" >/dev/null; then printf '%s\n' "$requested_directory" return 0 fi return 2 fi printf '%s\n' "$directories" | tail -1 } kvm__get_zorin_iso__get_highest_version_file() { local base_url=$1 local directory=$2 local version_prefix=${3:-Core} local listing local files non_beta candidate listing=$(curl -fsSL --connect-timeout 10 --max-time 60 --retry 3 --retry-delay 5 --retry-connrefused -H "User-Agent: Mozilla/5.0 (compatible; RTD-Setup/1.0)" "${base_url%/}/${directory%/}/" 2>/dev/null) || return 1 files=$(printf '%s\n' "$listing" | grep -oE "Zorin-OS-[0-9.]+-${version_prefix}-[0-9A-Za-z-]+\.iso") [ -z "$files" ] && return 1 non_beta=$(printf '%s\n' "$files" | grep -vi 'beta') if [ -n "$non_beta" ]; then candidate=$(printf '%s\n' "$non_beta" | sort -V | tail -1) if [ -n "$candidate" ]; then printf '%s\n' "$candidate" return 0 fi fi printf '%s\n' "$files" | sort -V | tail -1 } kvm__get_zorin_iso__download_file() { local base_url=$1 local directory=$2 local file=$3 local url="${base_url%/}/${directory%/}/$file" local destination="/var/lib/libvirt/boot/$file" system::log_item "Downloading $file to $destination from $url..." if curl -fSL --connect-timeout 10 --max-time 600 --retry 3 --retry-delay 5 --retry-connrefused -H "User-Agent: Mozilla/5.0 (compatible; RTD-Setup/1.0)" -o "$destination" "$url"; then system::log_item "Download completed successfully." iso="$destination" echo "$destination" return 0 fi rm -f "$destination" system::log_item "Download failed." return 1 } local version_prefix="Core" local requested_version="" local version_prefix_from_flag=0 local version_from_flag=0 local -a positional_args=() local version_pattern='^[0-9]+([.][0-9]+)?$' while [[ $# -gt 0 ]]; do case "$1" in --product|--edition) if [[ -z "${2:-}" ]]; then write_error "Option $1 requires a value (e.g., --product Core)." >&2 return 1 fi version_prefix="$2" version_prefix_from_flag=1 shift 2 ;; --version) if [[ -z "${2:-}" ]]; then write_error "Option --version requires a value (e.g., --version 18 or --version latest)." >&2 return 1 fi local version_value="$2" local version_value_lc version_value_lc=$(printf '%s' "$version_value" | tr '[:upper:]' '[:lower:]') if [[ "$version_value_lc" == "latest" ]]; then requested_version="" else if [[ ! "$version_value" =~ $version_pattern ]]; then write_error "Invalid version '${version_value}'. Expected a number such as 17 or 18.1, or the word 'latest'." >&2 return 1 fi requested_version="$version_value" fi version_from_flag=1 shift 2 ;; --latest) requested_version="" version_from_flag=1 shift ;; --help|-h) cat <<'EOF' Usage: kvm::get_zorin_iso [--product ] [--version ] [--latest] kvm::get_zorin_iso [version_prefix] [major_version] Examples: kvm::get_zorin_iso --product Education --version 17 kvm::get_zorin_iso --version latest kvm::get_zorin_iso --latest kvm::get_zorin_iso Pro 18 EOF return 0 ;; --*) write_error "Unknown option: $1" >&2 return 1 ;; *) positional_args+=("$1") shift ;; esac done if [ ${#positional_args[@]} -gt 0 ]; then for token in "${positional_args[@]}"; do local token_lc token_lc=$(printf '%s' "$token" | tr '[:upper:]' '[:lower:]') if [[ "$token_lc" == "latest" ]]; then if [ $version_from_flag -eq 0 ]; then requested_version="" version_from_flag=1 fi elif [[ "$token" =~ $version_pattern ]]; then if [ $version_from_flag -eq 0 ]; then requested_version="$token" version_from_flag=1 fi else if [ $version_prefix_from_flag -eq 0 ]; then version_prefix="$token" version_prefix_from_flag=1 fi fi done fi local -a default_base_urls=( "https://distro.ibiblio.org/pub/linux/distributions/zorinos/" "https://mirror.clarkson.edu/zorinos/isos/" "https://mirror.webworld.ie/zorinos/" "https://mirror.metanet.ch/zorinos/" "https://ftp.halifax.rwth-aachen.de/zorinos/" "https://mirror.23m.com/zorinos/isos/" "https://mirror.dogado.de/zorinos/" "https://mirror.serverion.com/zorinos/" "https://mirrors.dotsrc.org/zorinos/" "https://ftpmirror2.infania.net/mirror/zorinos/isos/" "https://mirror.kumi.systems/zorinos/" "https://mirror.ihost.md/zorinos/" "https://mirror.hosthink.net/zorinos/" "https://mirror.nju.edu.cn/zorinos/" ) local -a base_urls=() # Prefer mirrors defined in RTD-Setup/core/_locations.info if that file has been sourced. if declare -p KVM_ZORIN_ISO_MIRRORS >/dev/null 2>&1; then local mirror_decl mirror_decl=$(declare -p KVM_ZORIN_ISO_MIRRORS 2>/dev/null) if [[ $mirror_decl == "declare -a"* ]]; then if [ ${#KVM_ZORIN_ISO_MIRRORS[@]} -gt 0 ]; then base_urls=("${KVM_ZORIN_ISO_MIRRORS[@]}") fi elif [[ -n "$KVM_ZORIN_ISO_MIRRORS" ]]; then read -r -a base_urls <<<"$KVM_ZORIN_ISO_MIRRORS" fi fi # Fall back to the baked-in list when no shared configuration is available (or it is empty). if [ ${#base_urls[@]} -eq 0 ]; then base_urls=("${default_base_urls[@]}") fi local latest_directory highest_version_file file_path directory_status for base_url in "${base_urls[@]}"; do write_status "Checking Zorin mirror: $base_url" latest_directory=$(kvm__get_zorin_iso__get_latest_directory "$base_url" "$requested_version") directory_status=$? if [ $directory_status -eq 2 ]; then if [ -n "$requested_version" ]; then write_warning "Requested Zorin version '$requested_version' not found at $base_url" fi continue elif [ $directory_status -ne 0 ] || [ -z "$latest_directory" ]; then write_warning "No directories found at $base_url" continue fi highest_version_file=$(kvm__get_zorin_iso__get_highest_version_file "$base_url" "$latest_directory" "$version_prefix") if [ -z "$highest_version_file" ]; then write_warning "No files found for version prefix '$version_prefix' in directory '$latest_directory' at $base_url" continue fi file_path="/var/lib/libvirt/boot/$highest_version_file" if [ -f "$file_path" ]; then write_status "File already exists: $file_path" iso="$file_path" echo "$file_path" return 0 fi write_information "Selected version file: $highest_version_file" if kvm__get_zorin_iso__download_file "$base_url" "$latest_directory" "$highest_version_file"; then return 0 fi done write_error "Failed to locate or download Zorin ISO using known mirrors." return 1 } kvm::mount_local_folder_to_guest() { # Description: # Mounts a local host folder into a running KVM virtual machine (guest) # using virtiofs passthrough filesystem. Automatically validates inputs, # checks VM existence, creates temporary XML mount config, and applies # the configuration using `virsh attach-device`. # # Usage: # kvm::mount_local_folder_to_guest --vm [--mount ] \ # [--guest-dir ] \ # [--mount-tag ] # # Parameters: # --vm | --vm-name | --vmname # (Required) Name of the KVM guest VM to mount the folder into. # # --mount # (Optional) Path of the directory on the host to be mounted. # Default: /opt/rtd # # --guest-dir | --guestdir | --guest # (Optional) Mount point inside the guest VM. # Default: /opt/rtd # # --mount-tag | --tag # (Optional) Tag label to uniquely identify the mounted share. # Default: host-rtd-share # # --help | -h # Display help information about this function. # # Returns: # 0 on success, 1 on failure. Detailed error messages are printed to stderr. # # Example: # kvm::mount_local_folder_to_guest --vm myvm --mount /data \ # --guest-dir /mnt/data --mount-tag myshare # # Dependencies: # Requires `virsh`, `mktemp`, and libvirt with virtiofs support. # # Notes: # - Skips mounting if the same host directory is already configured. # - Uses temporary XML device configuration for persistent attachment. # End of documentation while [[ $# -gt 0 ]]; do case "$1" in --mount) if [[ -z "$2" ]]; then write_error "Error: You must provide a VM name." >&2 write_information "Usage: $0 " >&2 return 1 fi host_dir="$2" ;; --vm-name | --vmname | --vm) if [[ -z "$2" ]]; then write_error "Error: You must provide a VM name." >&2 write_information "Usage: $0 " >&2 return 1 fi vm_name="$2" ;; --mount-tag | --tag) if [[ -z "$2" ]]; then write_error "Error: You must provide a mount tag." >&2 write_information "Usage: $0 --mount-tag " fi mount_tag="$2" ;; --guest-dir | --guestdir | --guest) if [[ -z "$2" ]]; then write_error "Error: You must provide a guest directory." >&2 write_information "Usage: $0 --guest-dir " return 1 fi guest_dir="$2" ;; --help | -h) write_information "Usage: kvm::mount_local_folder_to_guest " write_information "Mounts a local folder to a KVM guest VM." return 0 ;; *) break ;; esac done local vm_name="$1" local host_dir="/opt/rtd" local guest_dir="/opt/rtd" # Same location inside the guest local mount_tag="host-rtd-share" # A unique label for the share # --- 1. Input Validation and Prerequisite Checks --- if [[ -z "$vm_name" ]]; then write_error "Error: You must provide a VM name." >&2 write_information "Usage: $0 " >&2 return 1 fi if ! virsh dominfo "$vm_name" &>/dev/null; then write_error "Error: VM '$vm_name' not found by virsh." >&2 return 1 fi if [[ ! -d "$host_dir" ]]; then write_warning "Warning: Host directory '$host_dir' does not exist." return 1 fi # Check if the filesystem is already configured if virsh dumpxml "$vm_name" | grep -q ""; then write_information "Info: A filesystem sharing '$host_dir' is already configured for '$vm_name'." write_information "Skipping host configuration." else # --- 2. Create the temporary XML for the device --- # Using a heredoc is clean and avoids escaping issues. local temp_xml_file temp_xml_file=$(mktemp) cat > "$temp_xml_file" <<-EOF EOF # --- 3. Attach the Device to the VM (Persistently) --- write_information "Attaching host directory '$host_dir' to VM '$vm_name'..." if ! virsh attach-device "$vm_name" "$temp_xml_file" --persistent; then write_error "Error: Failed to attach device to VM '$vm_name'." rm -f "$temp_xml_file" return 1 fi write_information "Device attached successfully." rm -f "$temp_xml_file" fi return 0 } kvm::create_expanding_vm_hardisk() { # Description: # Creates a non-preallocated, dynamically expanding qcow2 disk image for a VM. # Logs all actions and returns the full path of the disk image created. # # Globals: # _LOGFILE - Path to the log file used for system logging. # # Arguments: # $1 - Full VM name (used to name the disk file). # $2 - Disk size in GB (integer only). # $3 - Optional: target directory (default: /var/lib/libvirt/images). # # Outputs: # STDOUT: Full path of created disk image. # STDERR: Error messages. # # Returns: # 0 on success, 1 on failure. # End of documentation local vm_name="$1" local disk_size_gb="$2" local target_dir="${3:-/var/lib/libvirt/images}" local disk_path if [[ -z "$vm_name" || -z "$disk_size_gb" ]]; then printf "Usage: kvm::create_expanding_vm_hardisk [target_dir]\n" >&2 return 1 fi if ! command -v qemu-img >/dev/null; then printf "❌ qemu-img not found. Please install qemu-utils.\n" >&2 return 1 fi if [[ ! "$disk_size_gb" =~ ^[0-9]+$ ]]; then printf "❌ Disk size must be an integer (GB).\n" >&2 return 1 fi mkdir -p "$target_dir" || { printf "❌ Failed to create target directory: %s\n" "$target_dir" >&2 return 1 } disk_path="${target_dir}/${vm_name}.qcow2" system::log_item "🖴 Creating dynamic qcow2 disk at: $disk_path (${disk_size_gb}G)..." if ! qemu-img create -f qcow2 -o preallocation=off "$disk_path" "${disk_size_gb}G" >> "${_LOGFILE}" 2>&1; then system::log_item "❌ Failed to create disk image: $disk_path" return 1 fi # Optionally preallocate an initial amount of space so guest operating systems see at least # `initial_alloc_gb` available immediately, while still allowing the qcow2 image to grow # dynamically up to disk_size_gb. This mitigates low disk warnings caused by sparse images. local initial_alloc_gb="${INITIAL_VM_DISK_ALLOC_GB:-32}" if (( disk_size_gb > initial_alloc_gb )); then if dependency::command_exists fallocate ; then if ! fallocate -l "${initial_alloc_gb}G" "$disk_path" >> "${_LOGFILE}" 2>&1; then system::log_item "⚠️ Unable to preallocate ${initial_alloc_gb}G for $disk_path (fallocate failed)." else system::log_item "📦 Preallocated ${initial_alloc_gb}G for $disk_path to avoid early disk exhaustion." fi else system::log_item "ℹ️ fallocate not available; skipping initial disk preallocation for $disk_path." fi fi system::log_item "✅ Disk image created: $disk_path" printf '%s\n' "$disk_path" return 0 } kvm::wait_for_domain_definition() { # Description: # Polls libvirt until the specified domain is defined (or a timeout expires). Useful after # launching `virt-install --wait --noautoconsole` so UI messaging can be deferred until the VM actually exists. # # Arguments: # $1 - Domain name to check (required). # $2 - Optional timeout in seconds (default: 60). # $3 - Optional delay between checks in seconds (default: 1). # # Outputs: # None on success; warning messages on missing arguments or timeout. # # Returns: # 0 when the domain is found via `virsh dominfo`. # 1 when called without a domain name or when the timeout is exceeded. # End of documentation local domain="$1" local timeout="${2:-60}" local delay="${3:-1}" system::log_item "waiting for domain $domain definition with timeout $timeout and delay $delay" if [[ -z "$domain" ]]; then write_warning "kvm::wait_for_domain_definition called without a domain name." return 1 fi if ! [[ "$timeout" =~ ^[0-9]+$ ]] || (( timeout <= 0 )); then write_warning "kvm::wait_for_domain_definition requires a positive timeout value (received: $timeout)." return 1 fi system::log_item "🕒 Waiting up to ${timeout}s for domain '$domain' to be defined..." local elapsed=0 while (( elapsed < timeout )); do if virsh dominfo "$domain" >/dev/null 2>&1; then system::log_item "✅ Domain '$domain' is now defined." return 0 fi sleep "$delay" elapsed=$((elapsed + delay)) done write_warning "VM '$domain' not visible after ${timeout}s; continuing." return 1 } kvm::wait_for_domain_start() { # Description: # Blocks until the specified libvirt domain emits a lifecycle "Started" event or until a timeout # expires. Useful after `virsh start --console` to ensure the guest is actually booting before # proceeding with follow-up steps. # # Arguments: # --domain : Required domain name to monitor. # --timeout : Optional timeout in seconds (default: 120). # # Outputs: # Informational and warning messages indicating progress or errors. # # Returns: # 0 when a start event (or currently running state) is detected. # 1 when input is invalid, virsh fails, or the timeout is exceeded. # End of documentation local domain="" local timeout=120 while [[ $# -gt 0 ]]; do case "$1" in --domain) domain="${2:-}" shift 2 ;; --timeout) timeout="${2:-}" shift 2 ;; *) write_warning "kvm::wait_for_domain_start received unknown argument: $1" return 1 ;; esac done if [[ -z "$domain" ]]; then write_warning "kvm::wait_for_domain_start requires --domain ." return 1 fi if ! [[ "$timeout" =~ ^[0-9]+$ ]] || (( timeout <= 0 )); then write_warning "kvm::wait_for_domain_start requires a positive --timeout value (received: $timeout)." return 1 fi local state if state=$(virsh domstate "$domain" 2>/dev/null) && [[ "${state,,}" == "running" ]]; then system::log_item "🟢 Domain '$domain' already running; skipping event wait." return 0 fi system::log_item "🕒 Waiting up to ${timeout}s for domain '$domain' to emit a start event..." local event_output if ! event_output=$(virsh event --domain "$domain" --event lifecycle --timeout "$timeout" 2>&1); then write_warning "virsh event failed for domain '$domain': $event_output" return 1 fi if grep -qi "Started" <<< "$event_output"; then system::log_item "🚀 Domain '$domain' reported a lifecycle start event." return 0 fi # virsh event exits after the timeout if nothing was received, so issue a final domstate check # in case the guest raced to running but did not emit a start lifecycle event in time. if state=$(virsh domstate "$domain" 2>/dev/null) && [[ "${state,,}" == "running" ]]; then system::log_item "⚠️ Domain '$domain' running but no start event observed; continuing." return 0 fi write_warning "Domain '$domain' did not emit a start event within ${timeout}s (output: ${event_output:-})." return 1 } kvm::cleanup_unused_qcow_images() { # Description: # Removes qcow/qcow2 disk images in /var/lib/libvirt/images (or a supplied directory) that are not # currently attached to any libvirt domain. Useful for reclaiming space after template builds. # # Arguments: # --dir : Directory to scan (default: /var/lib/libvirt/images). # --dry-run : Report candidate deletions without removing files. # # Outputs: # Logs of kept/removed images and a final summary line. # # Returns: # 0 on success, 1 on invalid arguments or directory issues. # End of documentation local target_dir="/var/lib/libvirt/images" local dry_run=0 while [[ $# -gt 0 ]]; do case "$1" in --dir) if [[ -z "${2:-}" ]]; then write_error "⛔ --dir requires a path argument." return 1 fi target_dir="$2" shift 2 ;; --dry-run) dry_run=1 shift ;; *) write_error "⛔ Unknown option '$1'." return 1 ;; esac done if [[ ! -d "$target_dir" ]]; then write_error "Target directory '$target_dir' does not exist." return 1 fi system::log_item "QCOW cleanup starting. Dir=${target_dir} DryRun=${dry_run}" if ! command -v virsh >/dev/null 2>&1; then write_error "virsh command not available; cannot determine in-use disks." return 1 fi local -A referenced_map=() local domain while IFS= read -r domain; do [[ -z "$domain" ]] && continue for domblk_flag in "" "--inactive"; do while IFS= read -r disk_path; do [[ -z "$disk_path" ]] && continue if [[ -f "$disk_path" ]]; then local resolved resolved=$(readlink -f "$disk_path" 2>/dev/null || printf '%s\n' "$disk_path") referenced_map["$resolved"]=1 fi done < <(virsh domblklist "$domain" --details $domblk_flag 2>/dev/null | awk 'NR>2 && $3 == "disk" {print $4}') done done < <(virsh list --all | awk 'NR>2 {print $2}' | grep -v '^$') system::log_item "QCOW cleanup catalogued ${#referenced_map[@]} in-use disks" local removed=0 kept=0 processed=0 local file resolved_path while IFS= read -r file; do [[ -z "$file" ]] && continue processed=$((processed + 1)) system::log_item "QCOW cleanup examining: $file" resolved_path=$(readlink -f "$file" 2>/dev/null || printf '%s\n' "$file") if [[ -n "${referenced_map[$resolved_path]:-}" ]]; then kept=$((kept + 1)) continue fi if (( dry_run )); then write_information "[dry-run] Would remove unused disk: $file" else if rm -f -- "$file"; then write_status "Removed unused disk: $file" removed=$((removed + 1)) else write_warning "Failed to remove $file" fi fi done < <(find "$target_dir" -maxdepth 1 -type f \( -name '*.qcow2' -o -name '*.qcow' \) -print || true) if (( processed == 0 )); then system::log_item "QCOW cleanup found no qcow images in ${target_dir}" fi write_information "QCOW cleanup completed. Removed=${removed} Kept=${kept} Directory=${target_dir} DryRun=${dry_run}" system::log_item "QCOW cleanup completed. Removed=${removed} Kept=${kept} Directory=${target_dir} DryRun=${dry_run}" return 0 } kvm::cleanup_virtual_floppy_images() { # Description: # Removes virtual floppy disk images generated by RTD tooling (files named win*.img) from # /var/lib/libvirt/boot (default). Other directories may be supplied explicitly. # # Arguments: # --dir : Directory to scan (default: /var/lib/libvirt/boot). # --dry-run : Report deletions without removing files. # # Returns: # 0 on success, 1 on invalid parameters or directory errors. # End of documentation local target_dir="/var/lib/libvirt/boot" local dry_run=0 while [[ $# -gt 0 ]]; do case "$1" in --dir) if [[ -z "${2:-}" ]]; then write_error "⛔ --dir requires a path argument." return 1 fi target_dir="$2" shift 2 ;; --dry-run) dry_run=1 shift ;; *) write_error "⛔ Unknown option '$1'." return 1 ;; esac done if [[ ! -d "$target_dir" ]]; then write_error "Target directory '$target_dir' does not exist." return 1 fi system::log_item "Floppy cleanup starting. Dir=${target_dir} DryRun=${dry_run}" local removed=0 skipped=0 processed=0 while IFS= read -r floppy; do [[ -z "$floppy" ]] && continue processed=$((processed + 1)) system::log_item "Floppy cleanup examining: $floppy" if (( dry_run )); then write_information "[dry-run] Would remove floppy image: $floppy" skipped=$((skipped + 1)) else if rm -f -- "$floppy"; then write_status "Removed floppy image: $floppy" removed=$((removed + 1)) else write_warning "Failed to remove $floppy" fi fi done < <(find "$target_dir" -maxdepth 1 -type f \( -iname 'win*.img' -o -iname '*.vfd' -o -iname '*.flp' \) -print || true) if (( processed == 0 )); then system::log_item "Floppy cleanup found no floppy images in ${target_dir}" fi local examined_total=$((removed + skipped)) write_information "Floppy cleanup completed. Removed=${removed} Examined=${examined_total} Directory=${target_dir} DryRun=${dry_run}" system::log_item "Floppy cleanup completed. Removed=${removed} Examined=${examined_total} Directory=${target_dir} DryRun=${dry_run}" return 0 } kvm::make_vm_now_from_fedora_org() { # Description: Function to create a Fedora KVM virtual michine disk and define a VM. This function should be # used on a Qemu/KVM virtual host that the virtual guest is to be used on. # NOTE: QCOW2 is a storage format for virtual . # machine disk images QCOW stands for QEMU copy on write. The QCOW2 format decouples the physical storage # layer from the virtual layer by adding a mapping between logical and physical blocks. # NOTE2: Kernel-based Virtual Machine (KVM) is an open source virtualization technology built into Linux®. # Specifically, KVM lets you turn Linux into a hypervisor that allows a host machine to run multiple, # isolated virtual environments called guests or virtual machines (VMs). KVM is part of Linux. # For complete info: https://www.redhat.com/en/topics/virtualization/what-is-KVM # set -e # Use the software::check_native_package_dependency function to make sure the needed software is available. # Usage: Either simply call this function to use the defaults or: # Call this function with "function" "RAM_SIZE" "DISK_SIZE" "DISK_FORMAT" "DISK_FILE" # Omitting any of the arguments will us the default instead. # # Globals: ${FedoraVersion} ${PRESEED_FILE} # Optional arguments: workstation|ssh-server|ansible-server [-c <#CPU>, -m , -d ] # Outputs: std err # Returns: # Usage: # kvm::make_vm_now_from_fedora_org # or # kvm::make_vm_now_from_fedora_org -c 4 -m 4096 -d 100 # End of Documentation # Set default values for VM T-Shirts sizes... : ${_cpu="${_cpu:-2}"} : ${_mem="${_mem:-2048}"} : ${_dsk="${_dsk:-100}"} # Set default values for VM T-Shirts sizes... kvm::util::read_common_options ${*} # Set specific options for Fedora... kvm::util::read_distro_options --distribution fedora ${*} # Check that virtualization is supported on the host dependency::virtualization # Lookup specific binaries: use full path since they may just have been added and not in $PATH yet. : "${virt_net="$(system::find_vm_bridge)"}" if [[ "$virt_net" == "NONE" ]]; then write_error "No bridge interface detected. Supply --network explicitly (e.g., bridge=br0 or network=default) before proceeding." return 1 fi : "${BIN_VIRT_INSTALL:=$(type -p virt-install)}" : "${_DEFAULT_FEDORA_VER:="$(curl -s https://dl.fedoraproject.org/pub/fedora/linux/releases/ | grep -oP 'href="\K[0-9]+(?=/")' | sort -n | tail -1)"}" while [[ $# -gt 0 ]]; do system::log_item "Processing argument: $1" case "$1" in --fedora-version|-v|--version) if [[ "$2" == "auto" ]]; then _skip_prompt="TRUE" fi shift 2 ;; *) shift ;; esac done if [[ "$_skip_prompt" != "TRUE" ]]; then : "${_os_version=$($RTD_GUI --title "🐧 Select Release Version of Fedora" --inputbox "Please pick an available OS version by entering it below. \ If you are not sure just let me choose for you... Please NOTE: fedora is sort of a permanent beta and the anaconda installer may behave in unexpected ways. \n ${fedoralogo} " 25 110 "${_DEFAULT_FEDORA_VER:-"40"}" 3>&1 1>&2 2>&3)}" case $? in "$DIALOG_CANCEL") return ;; "$DIALOG_ESC") return ;; esac clear fi : "${_os_version:="${_DEFAULT_FEDORA_VER}"}" : "${_preseed_file="$(mktemp -d)/ks.cfg"}" # Get Fedora media if not already present: if kvm::get_fedora_workstation_iso --version ${_os_version} ; then system::log_item "Using Fedora Workstation ISO: ${iso}" else write_error "Failed to fetch Fedora ${_os_version} Workstation ISO. Please check your network connection or the Fedora repository." return 1 fi # Check if target is workstation or server: if [[ ${_role} == "workstation" || ${_role} == "VDI" ]]; then local _service="VDI" system::generate_ks_cfg_file --source "${_source_url}" --role "${_role}" --file "${_preseed_file}" --DE "${_UserDesktopEnvironmentSelection:="@basic-desktop-environment"}" elif [[ ${_role} == "server" ]]; then local _service="SRV" system::generate_ks_cfg_file --source "${_source_url}" --role "${_role}" --file "${_preseed_file}" --SR "${_UserServerEnvironemtSelection:="@network-server"}" else write_error "Unknown role selection: ${_role} ==> Use workstation or server" read -p "💥 - An ERROR has occurred. Please press [ENTER] to continue..." return 1 fi if [[ -z "${iso}" ]]; then write_error "No ISO file found. Please run kvm::get_fedora_workstation_iso first." return 1 else system::log_item "Using ISO file: ${iso}" local vm_kernel="${fedora_iso_mount_point}/boot/grub2/i386-pc/kernel.exec" local vm_initrd="${fedora_iso_mount_point}/boot/grub2/i386-pc/lnxboot.image" fi local _existing_vm_name="${vm_name:-}" local vm_name="" local config_label="" if [[ ${_role} == "server" ]]; then config_label=$(kvm::util::config_label "${_UserServerEnvironemtSelection:-${_service}}" "${_role}") else config_label=$(kvm::util::config_label "${_UserDesktopEnvironmentSelection:-${_service}}" "${_role}") fi [[ -z "$config_label" ]] && config_label="${_service}" if [[ -n "$_existing_vm_name" ]]; then kvm::util::set_vm_name --var vm_name --name "$_existing_vm_name" || return 1 else kvm::util::set_vm_name --var vm_name "Template_Fedora" "${config_label}" || return 1 fi local disk_path disk_path=$(kvm::create_expanding_vm_hardisk "$vm_name" "$_dsk") || { write_error "❌ Failed to create VM disk image. Aborting VM creation." return 1 } _summary_message="The virtual machine (${vm_name}) \n 📋 - Using the instructions in Kickstart: ${_preseed_file} \n 🔧 - Using this source for the packages and files to download: \n 🌎 - ${iso} \n 🖥️ - Using the network: ${virt_net:=default} \n 🖥️ - Memory: ${_mem} \n 🖥️ - CPU's: ${_cpu} \n 🖥️ - Disk Size: ${_dsk} \n You may attach to this server and see the progress at IP: $(hostname -I)" write_information "Creating VM: ${vm_name}..." { "${BIN_VIRT_INSTALL}" --connect qemu:///system --name "${vm_name}" \ --vcpus "${_cpu}" \ --memory "${_mem}" \ --network "${virt_net:=default}" \ --disk path="${disk_path},format=qcow2" \ --os-variant detect=on \ --initrd-inject="${_preseed_file}" \ --location="${iso}" \ --video model=virtio \ --graphics spice,listen=127.0.0.1 \ --controller type=virtio-serial \ --extra-args "inst.ks=file:/$(basename $_preseed_file)" \ --wait --noautoconsole \ || (read -p "💥 - An ERROR has occurred. Please press [ENTER] to continue..." && return 1) } >>"${_LOGFILE}" 2>&1 & disown kvm::wait_for_domain_definition "${vm_name}" 60 1 || true if ! kvm::wait_for_domain_start --domain "${vm_name}"; then write_warning "Domain '${vm_name}' did not emit a start event within the expected window." fi dialog::display_summary_message "NOTICE!" # Cleanup unset _preseed_file vm_kernel vm_initrd _source_url _config _mirrorlist_url _os_version _summary_message distro _UserServerEnvironemtSelection _UserDesktopEnvironmentSelection _role _service disk_path } kvm::make_vm_now_from_redhat_com() { # Description: Function to create a Alma Linux KVM virtual michine disk and define a VM. This function should be # used on a Qemu/KVM virtual host that the virtual guest is to be used on. # NOTE: QCOW2 is a storage format for virtual . # machine disk images QCOW stands for QEMU copy on write. The QCOW2 format decouples the physical storage # layer from the virtual layer by adding a mapping between logical and physical blocks. # NOTE2: Kernel-based Virtual Machine (KVM) is an open source virtualization technology built into Linux®. # Specifically, KVM lets you turn Linux into a hypervisor that allows a host machine to run multiple, # isolated virtual environments called guests or virtual machines (VMs). KVM is part of Linux. # For complete info: https://www.redhat.com/en/topics/virtualization/what-is-KVM # set -e # Use the software::check_native_package_dependency function to make sure the needed software is available. # Usage: Either simply call this function to use the defaults or: # Call this function with "function" "RAM_SIZE" "DISK_SIZE" "DISK_FORMAT" "DISK_FILE" # Omitting any of the arguments will us the default instead. # # Globals: ${FedoraVersion} ${PRESEED_FILE} # Arguments: None # Outputs: # Returns: # Usage: # End of Documentation # Set default values for VM T-Shirts sizes... : ${_cpu="${_cpu:-2}"} : ${_mem="${_mem:-2048}"} : ${_dsk="${_dsk:-100}"} # Set default values for VM T-Shirts sizes... kvm::util::read_common_options ${*} # Set specific options for RedHat... kvm::util::read_distro_options --distribution redhat ${*} # Check that virtualization is supported on the host dependency::virtualization # Lookup specific binaries: use full path since they may just have been added and not in $PATH yet. : ${virt_net="$(system::find_vm_bridge)"} if [[ "$virt_net" == "NONE" ]]; then write_error "No bridge interface detected. Supply --network explicitly (e.g., bridge=br0 or network=default) before proceeding." return 1 fi : "${BIN_VIRT_INSTALL:=$(type -p virt-install)}" local alma_base_url="https://repo.almalinux.org/almalinux/" local latest_alma_version latest_alma_version=$(curl -fsSL --connect-timeout 10 --max-time 60 --retry 3 --retry-delay 5 --retry-connrefused \ -H "User-Agent: Mozilla/5.0 (compatible; RTD-Setup/1.0)" "${alma_base_url}" | \ grep -oE 'href="[0-9]+(\.[0-9]+)*/"' | sed -E 's/href="([0-9]+(\.[0-9]+)*)\/"/\1/' | sort -V | tail -1) if [[ -z "$latest_alma_version" ]]; then system::log_item "Unable to determine latest AlmaLinux version from ${alma_base_url}, falling back to ${_DEFAULT_REDHAT_VER}" latest_alma_version="${_DEFAULT_REDHAT_VER}" fi : ${_os_version:-$($RTD_GUI --title "Select Release Version of AlmaLinux" --inputbox "Please pick an available OS version by entering it below. \ If you are not sure just let me choose for you... \n ${fedoralogo} " 25 110 "${latest_alma_version}" 3>&1 1>&2 2>&3)} case $? in "$DIALOG_CANCEL") return ;; "$DIALOG_ESC") return ;; esac clear : ${_os_version:="${_DEFAULT_REDHAT_VER}"} : ${_mirrorlist_url="https://mirrors.almalinux.org/mirrorlist/${_os_version}/baseos"} : ${_source_url:="https://repo.almalinux.org/almalinux/${_os_version}/BaseOS/x86_64/os/"} : ${_preseed_file:="$(mktemp -d)/ks.cfg"} : ${_config="${1:-"workstation"}"} if ! kvm::get_almalinux_boot_iso --version "${_os_version}"; then write_error "Unable to retrieve AlmaLinux ${_os_version} boot ISO." return 1 fi system::log_item "Using AlmaLinux boot ISO: ${iso}" local _existing_vm_name="${vm_name:-}" local vm_name="" local _service local config_label="" if [[ ${_role} == "workstation" || ${_role} == "VDI" ]]; then _service="VDI" system::generate_ks_cfg_file --source "${_source_url}" --role "${_role}" --file "${_preseed_file}" --DE "${_UserDesktopEnvironmentSelection:="@workstation-product"}" elif [[ ${_role} == "server" ]]; then _service="SRV" system::generate_ks_cfg_file --source "${_source_url}" --role "${_role}" --file "${_preseed_file}" --SR "${_UserServerEnvironemtSelection:="@network-server"}" else write_error "Unknown role selection: ${_role} ==> Use workstation or server" return 1 fi if [[ ${_role} == "server" ]]; then config_label=$(kvm::util::config_label "${_UserServerEnvironemtSelection:-${_service}}" "${_role}") else config_label=$(kvm::util::config_label "${_UserDesktopEnvironmentSelection:-${_service}}" "${_role}") fi [[ -z "$config_label" ]] && config_label="${_service}" if [[ -n "$_existing_vm_name" ]]; then kvm::util::set_vm_name --var vm_name --name "$_existing_vm_name" || return 1 else kvm::util::set_vm_name --var vm_name "Template" "AlmaLinux" "${config_label}" || return 1 fi local disk_path disk_path=$(kvm::create_expanding_vm_hardisk "$vm_name" "$_dsk") || { write_error "❌ Failed to create VM disk image. Aborting VM creation." return 1 } local alma_major="${_os_version%%.*}" local os_variant="almalinux${alma_major}" _summary_message="The virtual machine (${vm_name}) \n 📋 - Using the instructions in Kickstart: ${_preseed_file} \n 🔧 - Using this source for the packages and files to download: \n 🌎 - ${_source_url} \n 🌎 - Boot ISO: ${iso} \n 🖥️ - Using the network: ${virt_net:=default} \n 🖥️ - Memory: ${_mem} \n 🖥️ - CPU's: ${_cpu} \n 🖥️ - Disk Size: ${_dsk} \n You may attach to this server and see the progress at IP: $(hostname -I)" write_information "Creating VM: ${vm_name}..." { "${BIN_VIRT_INSTALL}" --connect qemu:///system --name "${vm_name}" \ --vcpus "${_cpu}" \ --memory "${_mem}" \ --network "${virt_net}" \ --disk path="${disk_path},format=qcow2" \ --os-variant="${os_variant}" \ --initrd-inject="${_preseed_file}" \ --location="${iso}" \ --extra-args "inst.ks=file:/$(basename $_preseed_file)" \ --wait --noautoconsole \ || (read -p "💥 - An ERROR has occurred. Please press [ENTER] to continue..." && return 1) } >>"${_LOGFILE}" 2>&1 & disown kvm::wait_for_domain_definition "${vm_name}" 60 1 || true if ! kvm::wait_for_domain_start --domain "${vm_name}"; then write_warning "Domain '${vm_name}' did not emit a start event within the expected window." fi dialog::display_summary_message "NOTICE!" for i in _preseed_file _source_url _config _mirrorlist_url _os_version distro disk_path iso; do unset $i; done } kvm::make_vm_now_from_opensuse_org() { # Description: Creates a SUSE KVM virtual machine disk and defines a VM. This function is intended to be # used on a Qemu/KVM virtual host where the virtual guest will run. # # QCOW2 (QEMU Copy On Write) is a storage format for virtual machine disk images. It decouples the physical # storage layer from the virtual layer by adding a mapping between logical and physical blocks. # # Kernel-based Virtual Machine (KVM) is an open source virtualization technology built into Linux. KVM allows # a host machine to run multiple, isolated virtual environments called guests or virtual machines (VMs). # For more information: https://www.redhat.com/en/topics/virtualization/what-is-KVM # # Use the software::check_native_package_dependency function to ensure the required software is available. # # Usage: # - Call this function with no arguments to use default values. # - Or call this function with the following arguments: # "function" "RAM_SIZE" "DISK_SIZE" "DISK_FORMAT" "DISK_FILE" # Omitting any arguments will use the default values. # # Globals: # None required but read from the _locations.info file. # _SUSE_LEAP_SOURCE # _SUSE_SLES_SOURCE # _SUSE_TUMBLEWEED_SOURCE # # Arguments: # --filename : Specifies the path and filename where the AutoYaST XML file will be saved. # --desktop_environment : Selects the desktop environment to install (kde, gnome, xfce, lxde, mate, cinnamon, enlightenment, lxqt). # --server_role : Specifies the server role (lamp, dns, dhcp_dns_server, file_server, print_server, mail_server, monitoring, desktop). # --disk_encryption : Enables or disables disk encryption. # --disk_password : Sets the disk encryption password (ignored if disk encryption is NO). # --initial_user : Sets the username for the initial user account. # --initial_user_password : Sets the password for the initial user account. # --help | -h : Displays usage information for the function. # --source_url | --source | --URL : The source URL for the SUSE version. # # Outputs: # Information about the created virtual machine. # # Returns: # 0 on success, 1 on error. # # Example: # # End of Documentation # Set default values for VM T-Shirts size... : ${_cpu="${_cpu:-2}"} : ${_mem="${_mem:-2048}"} : ${_dsk="${_dsk:-100}"} : ${CONFIG:="VDI"} : ${_autoyast_filename:="$(mktemp -d)/autoyast.xml"} # Check that virtualization is supported on the host dependency::virtualization # Set default values for VM T-Shirts sizes, overriding defaults if so instructed... kvm::util::read_common_options ${*} # Generate the AutoYaST file based on parameters provided... system::generate_autoyast_file ${*} # Lookup specific binaries: use full path since they may just have been added and not in $PATH yet. : "${BIN_VIRT_INSTALL:=$(type -p virt-install)}" # Discover if the appropriate VM net interface is (default or br'0 - n') : ${virt_net="$(system::find_vm_bridge)"} if [[ "$virt_net" == "NONE" ]]; then write_error "No bridge interface detected. Supply --network explicitly (e.g., bridge=br0 or network=default) before proceeding." return 1 fi local _existing_vm_name="${vm_name:-}" local vm_name="" local config_label="" if [[ ${_role} == "server" ]]; then config_label=$(kvm::util::config_label "${_UserServerEnvironemtSelection:-${CONFIG}}" "${_role}") else config_label=$(kvm::util::config_label "${CONFIG}" "${_role}") fi [[ -z "$config_label" ]] && config_label="${CONFIG}" if [[ -n "$_existing_vm_name" ]]; then kvm::util::set_vm_name --var vm_name --name "$_existing_vm_name" || return 1 else kvm::util::set_vm_name --var vm_name "Template_SUSE" "${config_label}" || return 1 fi local disk_path disk_path=$(kvm::create_expanding_vm_hardisk "$vm_name" "$_dsk") || { write_error "❌ Failed to create VM disk image. Aborting VM creation." return 1 } _summary_message=" The virtual machine (${vm_name}) \n 📋 - Using the instructions in: ${_preseed_file} \n 🔧 - Using this source for the packages and files to download: \n 🌎 - ${_source_url%%/*pub*} \n 🖥️ - Using the network: ${virt_net:=default} \n 🖥️ - Memory: ${_mem} \n 🖥️ - CPU's: ${_cpu} \n 🖥️ - Disk Size: ${_dsk} \n NOTE: You may connecct to the VM and see the progress at IP: $(hostname -I) \n After the build is complete you may reseal the VM and use it as a template for future VMs. \n" write_status "Creating virtual machine (${vm_name})" { "${BIN_VIRT_INSTALL}" --connect qemu:///system --name "${vm_name}" \ --vcpus "${_cpu}" \ --memory "${_mem}" \ --network "${virt_net}" \ --disk path="${disk_path},format=qcow2" \ --os-variant=opensusetumbleweed \ --initrd-inject="${_autoyast_filename}" \ --location="${source_url}" \ --extra-args "AutoYaST=file:///$(basename ${_autoyast_filename})" \ --wait --noautoconsole \ || (read -p "💥 - An ERROR has occurred. Please press [ENTER] to continue..." && return 1) } >>"${_LOGFILE}" 2>&1 & disown kvm::wait_for_domain_definition "${vm_name}" 60 1 || true if ! kvm::wait_for_domain_start --domain "${vm_name}"; then write_warning "Domain '${vm_name}' did not emit a start event within the expected window." fi dialog::display_summary_message "NOTICE!" for i in PRESEED_FILE vm_kernel vm_initrd source_url CONFIG distro _UserProductSelection _autoyast_filename disk_path ; do unset $i; done } kvm::make_vm_now_from_debian_org() { # Description: Function to create a Debian KVM virtual michine disk and define a VM. This function should be # used on a Qemu/KVM virtual host that the virtual guest is to be used on. # NOTE: QCOW2 is a storage format for virtual . # machine disk images QCOW stands for QEMU copy on write. The QCOW2 format decouples the physical storage # layer from the virtual layer by adding a mapping between logical and physical blocks. # NOTE2: Kernel-based Virtual Machine (KVM) is an open source virtualization technology built into Linux®. # Specifically, KVM lets you turn Linux into a hypervisor that allows a host machine to run multiple, # isolated virtual environments called guests or virtual machines (VMs). KVM is part of Linux. # For complete info: https://www.redhat.com/en/topics/virtualization/what-is-KVM # # Use the software::check_native_package_dependency function to make sure the needed software is available. # Usage: Either simply call this function to use the defaults or: # Call this function with "function" "RAM_SIZE" "DISK_SIZE" "DISK_FORMAT" "DISK_FILE" # Omitting any of the arguments will us the default instead. # # Globals: # Arguments: None # Outputs: # Returns: # Usage: # End of Documentation # Set default values for VM T-Shirts sizes... : ${_cpu="${_cpu:-2}"} : ${_mem="${_mem:-2048}"} : ${_dsk="${_dsk:-100}"} : ${_role:="desktop"} : ${_task:="ssh-server"} : ${ui_diplay_style:="theme=light"} # Set default values for VM T-Shirts sizes... kvm::util::read_common_options ${*} # Set specific options for Debian... kvm::util::read_distro_options --distribution debian ${*} # Check that virtualization is supported on the host dependency::virtualization # Lookup specific binaries: use full path since they may just have been added and not in $PATH yet, and set variables. : "${bin_virt_install:=$(type -p virt-install)}" local _existing_vm_name="${vm_name:-}" local vm_name="" local -a _vm_name_parts=() # Ensure Debian installer defaults are populated even if _locations.info was not sourced : "${_DEBIAN_SOURCE_URL:="https://deb.debian.org/debian/dists/stable/main/installer-amd64"}" : "${_DEBIAN_FRONTEND_GTK_KERNEL:="${_DEBIAN_SOURCE_URL}/current/images/netboot/gtk/debian-installer/amd64/linux"}" : "${_DEBIAN_FRONTEND_GTK_INITRD:="${_DEBIAN_SOURCE_URL}/current/images/netboot/gtk/debian-installer/amd64/initrd.gz"}" : "${_DEBIAN_FRONTEND_DEFAULT_KERNEL:="${_DEBIAN_SOURCE_URL}/current/images/netboot/debian-installer/amd64/linux"}" : "${_DEBIAN_FRONTEND_DEFAULT_INITRD:="${_DEBIAN_SOURCE_URL}/current/images/netboot/debian-installer/amd64/initrd.gz"}" # Set the default source URL for the Debian version if not already set... and add some options depending on the role. case "${_role}" in desktop | Desktop | VDI | vdi | workstation | Workstation) write_status "🖥️ - Using a desktop environment: ${_UserDesktopEnvironmentSelection}" : "${vm_kernel="${_DEBIAN_FRONTEND_GTK_KERNEL}"}" : "${vm_initrd="${_DEBIAN_FRONTEND_GTK_INITRD}"}" : "${ui_diplay_style:="theme=dark"}" : "${vm_usecase="VDI"}" local de_label de_label=$(kvm::util::config_label "${_UserDesktopEnvironmentSelection}" "${_role}") _vm_name_parts=("Template_Debian" "${vm_usecase}" "${de_label:-${_UserDesktopEnvironmentSelection}}") write_status "🖥️ - Using Variables: \n ${vm_kernel}, \n ${vm_initrd}" system::make_preseed_cfg --role "${_role}" --desktop_environment "${_UserDesktopEnvironmentSelection}" --saveto "$(mktemp -d)" ;; *) write_status "🖥️ - Using a environment: ${_role}" : "${vm_kernel="${_DEBIAN_FRONTEND_DEFAULT_KERNEL}"}" : "${vm_initrd="${_DEBIAN_FRONTEND_DEFAULT_INITRD}"}" : "${ui_diplay_style:="theme=light"}" : "${vm_usecase="Server"}" local task_label task_label=$(kvm::util::config_label "${_task}" "server") _vm_name_parts=("Template_Debian" "${task_label:-${_task}}") write_status "🖥️ - Using Variables: \n ${vm_kernel}, \n ${vm_initrd}" system::make_preseed_cfg --role "${_role}" --server_role "${_UserServerEnvironemtSelection}" --saveto "$(mktemp -d)" ;; esac if [[ -z "${_vm_name_parts[*]}" && -z "$_existing_vm_name" ]]; then write_error "⛔ Unable to determine VM name components for Debian build." return 1 fi if [[ -n "$_existing_vm_name" ]]; then kvm::util::set_vm_name --var vm_name --name "$_existing_vm_name" || return 1 else kvm::util::set_vm_name --var vm_name "${_vm_name_parts[@]}" || return 1 fi write_status "🔎 - Discovering what the appropriate VM net interface is (default or br'0 - n')" : ${virt_net="$(system::find_vm_bridge)"} if [[ "$virt_net" == "NONE" ]]; then write_error "No bridge interface detected. Supply --network explicitly (e.g., bridge=br0 or network=default) before proceeding." return 1 fi write_status "🔎 - Using network (${virt_net})" : ${source_url:="${_DEBIAN_SOURCE_URL}"} system::check_required_variables "vm_name" "vm_kernel" "vm_initrd" "source_url" "virt_net" "_mem" "_cpu" "_dsk" "bin_virt_install" "PRESEED_FILE" || (read -p "💥 - An ERROR has occurred. Please press [ENTER] to continue..." && return 1) local disk_path disk_path=$(kvm::create_expanding_vm_hardisk "$vm_name" "$_dsk") || { write_error "❌ Failed to create VM disk image. Aborting VM creation." return 1 } _summary_message=" The virtual machine (${vm_name}) \n 📋 - Using the instructions in: ${_preseed_file} \n 🔧 - Using this source for the packages and files to download: \n 🌎 - ${source_url%%/*pub*} \n 🖥️ - Using the network: ${virt_net:=default} \n 🖥️ - Memory: ${_mem} \n 🖥️ - CPU's: ${_cpu} \n 🖥️ - Disk Size: ${_dsk} \n NOTE: You may connecct to the VM and see the progress at IP: $(hostname -I) \n After the build is complete you may reseal the VM and use it as a template for future VMs. \n" write_status "🔧 - Creating the VM... Please Wait..." { "${bin_virt_install}" --name "${vm_name}" \ --vcpus "${_cpu}" \ --memory "${_mem}" \ --network "${virt_net}" \ --disk path="${disk_path},format=qcow2" \ --location="${source_url}" \ --os-variant="debian10" \ --video "virtio" --channel "spicevmc" \ --initrd-inject="${PRESEED_FILE}" \ --install kernel="${vm_kernel}",initrd="${vm_initrd}" \ --extra-args="auto=true priority=critical preseed/file=/$(basename ${PRESEED_FILE}) ${ui_diplay_style}" \ --wait --noautoconsole \ || (read -p "💥 - An ERROR has occurred. Please press [ENTER] to continue..." && return 1) } >>"${_LOGFILE}" 2>&1 & virt_pid=$! disown "${virt_pid}" kvm::wait_for_domain_definition "${vm_name}" 60 20 || true virsh set-lifecycle-action "${vm_name}" on_reboot restart virsh set-lifecycle-action "${vm_name}" on_poweroff restart # Debian installer now powers off after build; restart instead if ! kvm::wait_for_domain_start --domain "${vm_name}"; then write_warning "Domain '${vm_name}' did not emit a start event within the expected window." fi dialog::display_summary_message "NOTICE!" for i in PRESEED_FILE vm_kernel vm_initrd source_url ui_diplay_style vm_name vm_usecase distro disk_path; do unset $i; done } kvm::make_vm_now_from_kali_org() { # Description: Function to create a Debian KVM virtual michine disk and define a VM. This function should be # used on a Qemu/KVM virtual host that the virtual guest is to be used on. # NOTE: QCOW2 is a storage format for virtual . # machine disk images QCOW stands for QEMU copy on write. The QCOW2 format decouples the physical storage # layer from the virtual layer by adding a mapping between logical and physical blocks. # NOTE2: Kernel-based Virtual Machine (KVM) is an open source virtualization technology built into Linux®. # Specifically, KVM lets you turn Linux into a hypervisor that allows a host machine to run multiple, # isolated virtual environments called guests or virtual machines (VMs). KVM is part of Linux. # For complete info: https://www.redhat.com/en/topics/virtualization/what-is-KVM # # Use the software::check_native_package_dependency function to make sure the needed software is available. # Usage: Either simply call this function to use the defaults or: # Call this function with "function" "RAM_SIZE" "DISK_SIZE" "DISK_FORMAT" "DISK_FILE" # Omitting any of the arguments will us the default instead. # # Globals: # Arguments: None # Outputs: # Returns: # Usage: # End of Documentation # Set default values for VM T-Shirts sizes... : ${_cpu="${_cpu:-2}"} : ${_mem="${_mem:-2048}"} : ${_dsk="${_dsk:-100}"} : ${_role:="desktop"} : ${_task:="ssh-server"} # Set default values for VM T-Shirts sizes... kvm::util::read_common_options ${*} # Set specific options for Debian... kvm::util::read_distro_options --distribution debian ${*} # Check that virtualization is supported on the host dependency::virtualization # Lookup specific binaries: use full path since they may just have been added and not in $PATH yet, and set variables. : "${bin_virt_install:=$(type -p virt-install)}" # Set the default source URL for the Debian version if not already set... and add some options depending on the role. write_status "🖥️ - Using a desktop environment: ${_UserDesktopEnvironmentSelection}" : "${ui_diplay_style="theme=dark"}" : "${vm_usecase="VDI"}" write_status "🔎 - Discovering the install media to use and download if not present..." # Ensure that the Kali ISO is available... the path will be stored in the variable "iso". if kvm::get_kali_iso; then write_status "Kali ISO is ready at ${iso}" else write_error "Failed to get the Kali ISO" fi wrtite_status "🔎 - Using the Kali ISO: ${iso}" local _existing_vm_name="${vm_name:-}" local vm_name="" if [[ -n "$_existing_vm_name" ]]; then kvm::util::set_vm_name --var vm_name --name "$_existing_vm_name" || return 1 else kvm::util::set_vm_name --var vm_name "Template_kali" "${vm_usecase}" || return 1 fi system::make_preseed_cfg --role "${_role}" --desktop_environment "standard" --saveto "$(mktemp -d)" --package-list "curl git kali-linux-default kali-desktop-xfce" --ask YES write_status "🔎 - Discovering what the appropriate VM net interface is (default or br'0 - n')" : ${virt_net="$(system::find_vm_bridge)"} if [[ "$virt_net" == "NONE" ]]; then write_error "No bridge interface detected. Supply --network explicitly (e.g., bridge=br0 or network=default) before proceeding." return 1 fi write_status "🔎 - Using network (${virt_net})" local disk_path disk_path=$(kvm::create_expanding_vm_hardisk "$vm_name" "$_dsk") || { write_error "❌ Failed to create VM disk image. Aborting VM creation." return 1 } _summary_message=" The virtual machine (${vm_name}) \n 📋 - Using the instructions in: ${_preseed_file} \n 🔧 - Using this source for the packages and files to download: \n 🌎 - ${iso} \n 🖥️ - Using the network: ${virt_net:=default} \n 🖥️ - Memory: ${_mem} \n 🖥️ - CPU's: ${_cpu} \n 🖥️ - Disk Size: ${_dsk} \n NOTE: You may connecct to the VM and see the progress at IP: $(hostname -I) \n After the build is complete you may reseal the VM and use it as a template for future VMs. \n" write_status "🔧 - Creating the VM..." { "${bin_virt_install}" --name "${vm_name}" \ --vcpus "${_cpu}" \ --memory "${_mem}" \ --network "${virt_net}" \ --disk path="${disk_path},format=qcow2" \ --location="${iso},kernel=install.amd/gtk/vmlinuz,initrd=install.amd/gtk/initrd.gz" \ --os-variant="debian12" \ --video "virtio" --channel "spicevmc" \ --initrd-inject="${PRESEED_FILE}" \ --extra-args "ks=file:/$(basename $PRESEED_FILE) " \ --wait --noautoconsole \ || (read -p "💥 - An ERROR has occurred. Please press [ENTER] to continue..." && return 1) } >>"${_LOGFILE}" 2>&1 & disown kvm::wait_for_domain_definition "${vm_name}" 60 3 || true if ! kvm::wait_for_domain_start --domain "${vm_name}"; then write_warning "Domain '${vm_name}' did not emit a start event within the expected window." fi dialog::display_summary_message "NOTICE!" unset PRESEED_FILE vm_kernel vm_initrd source_url ui_diplay_style vm_name vm_usecase distro pkgsel_include_string iso _role _task _cpu _mem _dsk _UserDesktopEnvironmentSelection _UserServerEnvironemtSelection _service disk_path } kvm::make_vm_now_from_zorin() { # Description: Creates a KVM virtual machine template using a Zorin OS ISO. # # This function automates the creation of a KVM virtual machine template using a Zorin OS ISO file. # It sets default values for various VM parameters, checks for virtualization support, and uses # virt-install to create the VM with specified configurations. # # Globals: # - ubuntu_flavor: Default flavor of Ubuntu to use (default: "ubuntu"). # - _role: Default role of the VM (default: "workstation"). # - _cpu: Default number of CPUs to use (default: 2). # - _mem: Default amount of memory to use in MB (default: 2048). # - _dsk: Default size of the disk in GB (default: 100). # - permanent_download_dir: Location for KVM ISO files (default: "/var/lib/libvirt/boot/"). # - _saveto: Location for preseed files (default: temporary directory). # - CloudConfigDir: Location for cidata files (default: _saveto). # - bin_virt_install: Path to the virt-install binary. # - virt_net: Network to use for the VM. # Arguments: # - Accepts the same generic VM flags handled by kvm::util::read_common_options plus: # --edition|--product (defaults to Core) # --version or --latest (defaults to latest) # Outputs: # - Creates a KVM virtual machine template. # Returns: # - 0 on success. # - 1 on error. # # Usage: # kvm::make_vm_now_from_zorin [--edition Core] [--version latest] [generic options] # # Example: # kvm::make_vm_now_from_zorin --edition Core --version latest --tshirt-size small # # Notes: # - This function relies on other utility functions and dependencies such as dependency::virtualization and kvm::get_zorin_iso. # - The function prompts the user for input if required (e.g., for preseed configuration). # - Ensure that the necessary dependencies and utilities are installed and configured. # # End of Documentation # Check that virtualization is supported on the host dependency::virtualization local _existing_vm_name="${vm_name:-}" local ubuntu_flavor _role _cpu _mem _dsk permanent_download_dir _saveto CloudConfigDir bin_virt_install virt_net edition version _zorin_current_version vm_name : ${ubuntu_flavor="ubuntu"} # This is the default flavor of Ubuntu to use. : ${_role:="VDI"} # This is the default role of the Ubuntu VM to create. : ${_cpu:="${_cpu:=2}"} # This is the default number of CPUs to use. : ${_mem:="${_mem:=2048}"} # This is the default amount of memory to use. : ${_dsk:="${_dsk:=100}"} # This is the default size of the disk to use. : ${permanent_download_dir:="/var/lib/libvirt/boot/"} # This is the default location for KVM ISO files. : ${_saveto="$(mktemp -d /tmp/CloudConfig.XXXXXX)"} # This is the default location preseed files. : ${CloudConfigDir:="$_saveto"} # This is the default location cidata files. : ${bin_virt_install:=$(type -p virt-install)} # This is the default location for the virt-install binary. : ${virt_net="$(system::find_vm_bridge)"} # This is the default network to use for the VM. if [[ "$virt_net" == "NONE" ]]; then write_error "No bridge interface detected. Supply --network explicitly (e.g., bridge=br0 or network=default) before proceeding." return 1 fi : ${edition:="Core"} # This is the default edition of Zorin OS to use. : ${version:="latest"} # Default to the most recent Zorin OS release. local -a passthrough_args=() local version_pattern='^[0-9]+([.][0-9]+)?$' if [ $# -gt 0 ]; then while [[ $# -gt 0 ]]; do case "$1" in --edition|--product) if [[ -z "${2:-}" ]]; then write_error "⛔ Option $1 requires a value (e.g., --edition Core)." >&2 return 1 fi edition="$2" shift 2 continue ;; --version) if [[ -z "${2:-}" ]]; then write_error "⛔ Option --version requires a value (e.g., --version 18 or --version latest)." >&2 return 1 fi local version_value="$2" local version_value_lc version_value_lc=$(printf '%s' "$version_value" | tr '[:upper:]' '[:lower:]') if [[ "$version_value_lc" == "latest" ]]; then version="latest" elif [[ "$version_value" =~ $version_pattern ]]; then version="$version_value" else write_error "⛔ Invalid version '${version_value}'. Use a number (e.g., 17) or 'latest'." >&2 return 1 fi shift 2 continue ;; --latest) version="latest" shift continue ;; *) passthrough_args+=("$1") shift ;; esac done fi _ask=YES # Set default values for VM T-Shirts sizes... if [ ${#passthrough_args[@]} -gt 0 ]; then kvm::util::read_common_options "${passthrough_args[@]}" kvm::util::read_distro_options --distribution ubuntu "${passthrough_args[@]}" else kvm::util::read_common_options kvm::util::read_distro_options --distribution ubuntu fi kvm::get_zorin_iso --version "$version" --product "$edition" || (dialog::display_error "💥 - An ERROR has occurred. Unable to get Zorin ISO file. Please press [OK] to continue..." && return 1) if [[ -z "${_zorin_current_version}" ]]; then if [[ -n "${iso:-}" ]]; then local iso_basename iso_basename=$(basename -- "$iso") if [[ "$iso_basename" =~ Zorin-OS-([0-9.]+)- ]]; then _zorin_current_version="${BASH_REMATCH[1]}" else _zorin_current_version="${version}" fi else _zorin_current_version="${version}" fi fi if [[ -n "$_existing_vm_name" ]]; then kvm::util::set_vm_name --var vm_name --name "$_existing_vm_name" || return 1 else kvm::util::set_vm_name --var vm_name "Template_Zorin_OS" "${_zorin_current_version}" "${edition}" "${_role}" --timestamp-format "%Y-%m-%d-%s" || return 1 fi local guest_hostname="${vm_hostname:-$vm_name}" [[ -z "${guest_hostname}" ]] && guest_hostname="${vm_name}" local disk_path disk_path=$(kvm::create_expanding_vm_hardisk "$vm_name" "$_dsk") || { dialog::display_error "💥 - Failed to create VM disk image. Please press [OK] to continue..." return 1 } system::make_preseed_cfg --role ${_role} --saveto ${_saveto} --ask ${_ask:-"NO"} || (dialog::display_error "💥 - An ERROR has occurred. Unable to create preseed file. Please press [OK] to continue..." && return 1) _summary_message=" The virtual machine (${vm_name}) \n 📋 - Using the instructions in: ${_preseed_file} \n 🔧 - Using this source: \n 🌎 - ${iso} \n 🖥️ - Using the network: ${virt_net:=default} \n 🖥️ - Memory: ${_mem} \n 🖥️ - CPU's: ${_cpu} \n 🖥️ - Disk Size: ${_dsk} \n NOTE: You may connecct to the VM and see the progress at IP: \n $(hostname -I) \n After the build is complete you may reseal the VM and use it as a template for future VMs. \n" { "${bin_virt_install}" --name ${vm_name} \ --vcpus "${_cpu}" \ --memory "${_mem}" \ --network "${virt_net}" \ --disk path="${disk_path},format=qcow2" \ --os-variant="ubuntu24.04" \ --video "virtio" --channel "spicevmc" \ --location="${iso},kernel=casper/vmlinuz,initrd=casper/initrd.zstd" \ --extra-args="ks=file:/preseed.cfg auto=true priority=critical debian-installer/locale=en_US keyboard-configuration/layoutcode=${Preference_InitialKeyboardLayout} \ netcfg/get_hostname=${guest_hostname} console-setup/ask_detect=false ubiquity/reboot=true languagechooser/language-name=English countrychooser/shortlist=US \ localechooser/supported-locales=en_US.UTF-8 ubiquity/use_nonfree=true boot=casper automatic-ubiquity splash noprompt " \ --initrd-inject="${_saveto}/preseed.cfg" \ --initrd-inject="${_saveto}/ks.cfg" \ --wait --noautoconsole \ || (read -p "💥 - An ERROR has occurred. Please press [ENTER] to continue..." && return 1) }>> ${_LOGFILE} 2>&1 & disown kvm::wait_for_domain_definition "${vm_name}" 60 3 || true if ! kvm::wait_for_domain_start --domain "${vm_name}"; then write_warning "Domain '${vm_name}' did not emit a start event within the expected window." fi dialog::display_summary_message "NOTICE!" unset disk_path } kvm::make_vm_now_from_ubuntu_com() { # Description: # Automates the creation of a KVM-based Ubuntu VM, ideal for quickly setting up virtual # environments with customizable configurations. This function should be run on the Qemu/KVM host # intended to run the VM. # # About QCOW2 (QEMU Copy-On-Write v2): # - A disk image format that provides snapshot support and dynamic allocation. # - Decouples physical storage from the virtual storage layer, using logical-to-physical block mapping. # # About KVM (Kernel-based Virtual Machine): # - An open-source virtualization technology built into the Linux kernel. # - Turns the Linux host into a hypervisor to run isolated guest VMs. # - Further reference: https://www.redhat.com/en/topics/virtualization/what-is-KVM # # Prerequisites: # - Must run on a Linux host with QEMU + KVM installed and working. # - Requires internet access to download the Ubuntu ISO, unless manually placed in # /var/lib/libvirt/boot/ or a user-specified path. # - The 'virt-install' tool must be installed. # - Run as root or via sudo. # # Parameters: # --ram : VM RAM size in MB (default: 2048). # --cpus : Number of CPU cores (default: 2). # --disk : Disk size in GB (default: 100). # --flavor : Ubuntu flavor (default: 'ubuntu'). # --role : VM role, e.g. 'desktop' or 'server' (default: 'server'). # --version : Ubuntu version (defaults to latest LTS if unspecified). # --location : Alternate location for ISO files (default: /var/lib/libvirt/boot/). # --ask : Interactive mode for manual VM settings (default: 'NO'). # --desktop_environment : Desktop environment package(s) for a desktop VM. # --server_role : Server role package(s) for a server VM. # --pre-config : Pre-configured VM configured for a specific role like "minecraft". # # Usage Examples: # 1) Default VM creation with 2 CPU, 2GB RAM, 100GB disk: # kvm::make_vm_now_from_ubuntu_com # # 2) Customized VM (e.g., 8GB RAM, 4 CPUs, 250GB disk, Ubuntu 20.04 Desktop): # kvm::make_vm_now_from_ubuntu_com --ram 8192 --cpus 4 --disk 250 \ # --flavor ubuntu --version 20.04 --role desktop # # 3) Interactive VM setup: # kvm::make_vm_now_from_ubuntu_com --ask YES # # Process Overview: # 1) Parameter Parsing : Reads CLI arguments, overriding defaults where applicable. # 2) Dependency Check : Ensures required virtualization tools are present. # 3) ISO Retrieval : Downloads or locates the Ubuntu ISO. # 4) VM Configuration : Sets up CPU, RAM, disk, network, and OS variant settings. # 5) Installation : Invokes 'virt-install' to install Ubuntu automatically (via preseed.cfg or autoinstall.yaml). # 6) Logging & Cleanup : Logs the process and removes temporary working files. # # Error Handling: # - Performs checks at key steps (e.g., missing tools, download failure). # - Logs and notifies the user of any issues. # # Additional Notes: # - This function is part of a broader VM management system, relying on certain utilities and patterns # (e.g. kvm::util::read_common_options, ubuntu::download_iso, etc.). # - Different Ubuntu releases require different installation methods: # < 16.x → Preseed with Debian Installer # 16.x–22.x → Preseed (debian-installer) for older releases # ≥ 22.x → Autoinstall (cloud-config) # - Canonical frequently modifies installation methods, so results may vary on less tested versions. # # Globals: # ${ubuntu_flavor}, ${_role}, ${_task}, ${_cpu}, ${_mem}, ${_dsk}, ${permanent_download_dir}, # ${_saveto}, ${CloudConfigDir}, ${bin_virt_install}, ${virt_net}, and others set dynamically. # # # Notes # # The function is part of a larger system presumably dealing with VM management, and it # expects certain utilities and conventions to be present in the environment. # It is designed to be flexible with various Ubuntu versions and handles different # installation (preseed.cfg or autoinstall.yaml) methods based on the version. # Error handling is incorporated to manage issues during the VM creation process. # The function includes logging capabilities for tracking the VM creation process. # # Please note that the various Ubuntu versions may or may not work with the preseed.cfg file # or the autoinstall.yaml correctly as Canonical changes the way the installer works constantly. # For stability and predictability please refer to Debian instead. Also please note that the # automatic installations are created per the Ubuntu documentation, and you may experience issues # with the preseed.cfg file or the autoinstall.yaml file anyway as the documentation appears incomplete. # # End of Documentation # Set default values for VM T-Shirts sizes (may vary by OS)... : ${all_params=${*}} # Save all parameters for later use. : ${_flavor="ubuntu"} # This is the default flavor of Ubuntu to use. : ${_role:="server"} # This is the default role of the Ubuntu VM to create. #: ${_UserDesktopEnvironmentSelection:="ubuntu-desktop"} # This is the default task to perform on the Ubuntu VM. #: ${_UserServerEnvironemtSelection:="ssh-server"} # This is the default server role for the Ubuntu VM. : ${_cpu:="${_cpu:=2}"} # This is the default number of CPUs to use. : ${_mem:="${_mem:=2048}"} # This is the default amount of memory to use. : ${_dsk:="${_dsk:=100}"} # This is the default size of the disk to use. : ${permanent_download_dir:="/var/lib/libvirt/boot/"} # This is the default location for KVM ISO files. : ${_saveto:="$(mktemp -d /tmp/CloudConfig.XXXXXX)"} # This is the default location preseed files. : ${CloudConfigDir:="$_saveto"} # This is the default location cidata files. : ${bin_virt_install:=$(type -p virt-install)} # This is the default location for the virt-install binary. : ${virt_net="$(system::find_vm_bridge)"} # This is the default network to use for the VM. if [[ "$virt_net" == "NONE" ]]; then write_error "No bridge interface detected. Supply --network explicitly (e.g., bridge=br0 or network=default) before proceeding." return 1 fi : ${_ask="NO"} # This is the default for interactive mode for manual input during setup. system::log_item "Read/Set default values for VM T-Shirts sizes..." kvm::util::read_common_options ${all_params} system::log_item "Set specific options for Ubuntu..." kvm::util::read_distro_options --distro ubuntu ${all_params} system::log_item "Check that virtualization is supported on the host..."DS dependency::virtualization system::log_item "Get the version of Ubuntu to use; sets variable _tgt_ubuntu_ver..." if [[ -z "${_tgt_ubuntu_ver}" ]]; then if ubuntu::get_target_version ; then write_status "🔎 - Using the Ubuntu version as requested: ${_tgt_ubuntu_ver}" else write_status "💥 - User pressed [ESC] or [Cancel] not selecting a version..." write_status "💥 - Press [ENTER] to select default version of Ubuntu (Latest LTS release)" return 1 fi fi system::log_item "Get VM configuration preferences..." if [[ "${_ask}" == "YES" ]]; then kvm::get_vm_config_preferences --ask else kvm::get_vm_config_preferences fi system::log_item "Format the name of the VM to be created..." local _ubuntu_version_component="${_tgt_ubuntu_ver}" local _existing_vm_name="${vm_name:-}" local vm_name="" local config_label="" if [[ "${_role,,}" == "server" ]]; then config_label=$(kvm::util::config_label "${server_app}" "${_role}") else config_label=$(kvm::util::config_label "${_UserDesktopEnvironmentSelection:-${_role}}" "${_role}") fi [[ -z "$config_label" ]] && config_label="${server_app:-${_role}}" if [[ -n "$_existing_vm_name" ]]; then kvm::util::set_vm_name --var vm_name --name "$_existing_vm_name" || return 1 else local _name_parts=("Template" "${config_label}") # Avoid repeating the role if it's already part of the label (e.g., Minecraft-Server). if [[ -n "${_role}" && "${config_label,,}" != *"${_role,,}"* ]]; then _name_parts+=("${_role}") fi if [[ -n "${_ubuntu_version_component}" ]]; then _name_parts+=("${_ubuntu_version_component}") fi kvm::util::set_vm_name --var vm_name "${_name_parts[@]}" --timestamp-format "%Y-%m-%d-%s" || return 1 fi system::log_item "Ensure that the QCOW2 disk image is created for the VM in expandable format..." disk_path=$(kvm::create_expanding_vm_hardisk "$vm_name" "$_dsk") || { write_error "❌ Failed to create VM disk image. Aborting VM creation." return 1 } # Prepare the summary message for the VM creation... _summary_message=" The virtual machine ($vm_name) is being built:\n 📋 - Using the instructions in: $_preseed_file $CloudConfigDir \n 💿 - ISO from $permanent_download_dir/$iso_filename \n 🖥️ - Using the network: $virt_net \n 🖥️ - Memory: ${_mem} \n 🖥️ - CPU's: ${_cpu} \n 🖥️ - Disk Size: ${_dsk} \n NOTE: You may connect to the VM and see the progress at IP: \n $(hostname -I) \n After the build is complete you may reseal the VM and use it as a template for future VMs. \n" write_status "🔧 - Creating the VM... Please Wait..." case "${_role}" in workstation | desktop ) write_status "🖥️ - Using a desktop environment: ${_UserDesktopEnvironmentSelection}" if [[ ${_tgt_ubuntu_ver::2} -lt 16 ]]; then system::log_item "For really old Ubuntu builds (less than 16) use the debian installer (d-i) and using DVD ISO but still PRESEED.cfg... # Ubuntu 14* look for the kernel called vmlinuz.efi and initrd called initrd instead of vmlinuz and initrd.gz, so # we use a different option to install the VM." system::log_item "Downloading the Ubuntu ISO if not already present to ${permanent_download_dir} for media-type desktop version ${_tgt_ubuntu_ver}" ubuntu::download_iso --dir ${permanent_download_dir} --media-type desktop --version ${_tgt_ubuntu_ver} : ${iso:="$permanent_download_dir/$iso_filename"} system::make_preseed_cfg ${all_params} system::log_item "🐧 Ubuntu version ${_tgt_ubuntu_ver}: is less than 16, so using debian installer (d-i) and desktop media..." system::log_item "🐧 Ubuntu version ${_tgt_ubuntu_ver}: Running install options for debian installer (d-i)..." { "${bin_virt_install}" --name ${vm_name} \ --vcpus "${_cpu}" \ --memory "${_mem}" \ --network "${virt_net}" \ --disk path="${disk_path},format=qcow2" \ --os-variant="ubuntu${_tgt_ubuntu_ver::5}" \ --video "qxl" --channel "spicevmc" \ --location="${iso},kernel=casper/vmlinuz.efi,initrd=casper/initrd" \ --extra-args="ks=file:/preseed.cfg auto=true priority=critical debian-installer/locale=en_US keyboard-configuration/layoutcode=${Preference_InitialKeyboardLayout} \ netcfg/get_hostname=${guest_hostname} console-setup/ask_detect=false ubiquity/reboot=true languagechooser/language-name=English countrychooser/shortlist=US \ localechooser/supported-locales=en_US.UTF-8 ubiquity/use_nonfree=true boot=casper automatic-ubiquity splash noprompt " \ --initrd-inject="${_saveto}/preseed.cfg" \ --initrd-inject="${_saveto}/ks.cfg" \ --wait --noautoconsole \ || (read -p "💥 - An ERROR has occurred. Please press [ENTER] to continue..." && return 1) } >>"${_LOGFILE}" 2>&1 & disown kvm::wait_for_domain_definition "${vm_name}" 60 3 || true if ! kvm::wait_for_domain_start --domain "${vm_name}"; then write_warning "Domain '${vm_name}' did not emit a start event within the expected window." fi dialog::display_summary_message "NOTICE!" elif [[ ${_tgt_ubuntu_ver::2} -lt 23 ]]; then system::log_item "For interim Ubuntu builds (less than 23) use the debian installer (d-i) using DVD ISO but still PRESEED.cfg..." system::log_item "Downloading the Ubuntu ISO if not already present to ${permanent_download_dir} for media-type desktop version ${_tgt_ubuntu_ver}" ubuntu::download_iso --dir ${permanent_download_dir} --media-type desktop --version ${_tgt_ubuntu_ver} : ${iso:="$permanent_download_dir/$iso_filename"} system::make_preseed_cfg ${all_params} system::log_item "🐧 Ubuntu version ${_tgt_ubuntu_ver}: is less than 23, so using debian installer (d-i) and desktop media..." system::log_item "🐧 Ubuntu version ${_tgt_ubuntu_ver}: Running install options for debian installer (d-i)..." { "${bin_virt_install}" --name ${vm_name} \ --vcpus "${_cpu}" \ --memory "${_mem}" \ --network "${virt_net}" \ --disk path="${disk_path},format=qcow2" \ --os-variant="ubuntu${_tgt_ubuntu_ver::5}" \ --video "virtio" --channel "spicevmc" \ --location="${iso},kernel=casper/vmlinuz,initrd=casper/initrd" \ --extra-args="ks=file:/preseed.cfg auto=true priority=critical debconf/frontend=noninteractive debian-installer/locale=en_US keyboard-configuration/layoutcode=${Preference_InitialKeyboardLayout} \ netcfg/get_hostname=RTD-VDI console-setup/ask_detect=false ubiquity/reboot=true languagechooser/language-name=English countrychooser/shortlist=US \ localechooser/supported-locales=en_US.UTF-8 ubiquity/use_nonfree=true boot=casper automatic-ubiquity splash noprompt " \ --initrd-inject="${_saveto}/preseed.cfg" \ --initrd-inject="${_saveto}/ks.cfg" \ --wait --noautoconsole \ || (read -p "💥 - An ERROR has occurred. Please press [ENTER] to continue..." && return 1) } >>"${_LOGFILE}" 2>&1 & disown kvm::wait_for_domain_definition "${vm_name}" 60 3 || true if ! kvm::wait_for_domain_start --domain "${vm_name}"; then write_warning "Domain '${vm_name}' did not emit a start event within the expected window." fi dialog::display_summary_message "NOTICE!" elif [[ ${_tgt_ubuntu_ver::2} -ge 23 && ${_tgt_ubuntu_ver::2} -lt 24 ]]; then system::log_item "For interim Ubuntu builds (greater than 23 but less than 24) there is no way to automate the install. Only the server media supports automation... This is not a supported version of Ubuntu." dialog::display_notice "🐧 Ubuntu version ${_tgt_ubuntu_ver} no longer supports Debian preseed and not yet CloudCOnfig thus it is an unsupported version of Ubuntu. \n\n \n Please consider using a supported version instead: ${all_lts_versions[*]} \n\n" return 1 elif [[ ${_tgt_ubuntu_ver::2} -ge 24 ]]; then system::log_item "For modern Ubuntu builds (greater than 24) use the cloud config and desktop media to build desktop... However, this seems to be half baked... Hope it actually works for you..." system::log_item "Downloading the Ubuntu ISO if not already present to ${permanent_download_dir} for media-type desktop version ${_tgt_ubuntu_ver}" ubuntu::download_iso --dir ${permanent_download_dir} --media-type desktop --version ${_tgt_ubuntu_ver} : ${iso:="$permanent_download_dir/$iso_filename"} system::log_item "🐧 Ubuntu version ${_tgt_ubuntu_ver}: Using MODERN installer..." system::generate_cloudconfig --CloudConfigDir "${CloudConfigDir}" --vmname "${vm_name}" ${all_params} system::log_item "Ubuntu version ${_tgt_ubuntu_ver}: is greated than 24, so using cloud config and desktop media to build desktop..." { "${bin_virt_install}" --name ${vm_name} \ --vcpus "${_cpu}" \ --memory "${_mem}" \ --network "${virt_net}" \ --disk path="${disk_path},format=qcow2" \ --os-variant="ubuntu24.04" \ --video "virtio" --channel "spicevmc" \ --disk path="${CloudConfigDir}/CIDATA.iso,device=cdrom" \ --location="${iso},kernel=casper/vmlinuz,initrd=casper/initrd" \ --extra-args "autoinstall" \ --wait --noautoconsole \ || (read -p "💥 - An ERROR has occurred. Please press [ENTER] to continue..." && return 1) } >>"${_LOGFILE}" 2>&1 & disown kvm::wait_for_domain_definition "${vm_name}" 60 3 || true if ! kvm::wait_for_domain_start --domain "${vm_name}"; then write_warning "Domain '${vm_name}' did not emit a start event within the expected window." fi dialog::display_summary_message "NOTICE!" fi ;; VDI ) if [[ ${_tgt_ubuntu_ver::2} -lt 22 ]]; then system::log_item "Ubuntu version ${_tgt_ubuntu_ver}: is greater than less than 23, VDI build is deprecated. " _summary_message=" NOTE: Building a VDI for Ubuntu versions less than 23 is deprecated \n Press OK to return to the previous menu. \n" dialog::display_summary_message "NOTICE!" unset _tgt_ubuntu_ver _summary_message iso_filename CloudConfigDir iso vm_name return else system::log_item "Downloading the Ubuntu ISO if not already present to ${permanent_download_dir} for media-type server version ${_tgt_ubuntu_ver}" ubuntu::download_iso --dir ${permanent_download_dir} --media-type server --version ${_tgt_ubuntu_ver} : ${iso:="$permanent_download_dir/$iso_filename"} system::log_item "Ubuntu version ${_tgt_ubuntu_ver}: is greater than 23, so using cloud config to build server..." system::log_item "🐧 Ubuntu version ${_tgt_ubuntu_ver}: Using SERVER installer as requested..." system::generate_cloudconfig --CloudConfigDir "${CloudConfigDir}" --vmname "${vm_name}" ${all_params} { "${bin_virt_install}" --name ${vm_name:-"Ubuntu-Server-${_tgt_ubuntu_ver}-$(date +%Y-%m-%d-%s)"} \ --vcpus "${_cpu}" \ --memory "${_mem}" \ --network "${virt_net}" \ --disk path="${disk_path},format=qcow2" \ --os-variant="ubuntu24.04" \ --video "virtio" --channel "spicevmc" \ --disk path="${CloudConfigDir}/CIDATA.iso,device=cdrom" \ --location="${iso},kernel=casper/vmlinuz,initrd=casper/initrd" \ --extra-args "autoinstall reboot=acpi" \ --events on_reboot=restart,on_poweroff=destroy,on_crash=destroy \ --wait --noautoconsole \ || (read -p "💥 - An ERROR has occurred. Please press [ENTER] to continue..." && return 1) } >>"${_LOGFILE}" 2>&1 & disown kvm::wait_for_domain_definition "${vm_name}" 60 3 || true if ! kvm::wait_for_domain_start --domain "${vm_name}"; then write_warning "Domain '${vm_name}' did not emit a start event within the expected window." fi dialog::display_summary_message "NOTICE!" fi ;; server | Server | SERVER) if [[ ${_tgt_ubuntu_ver::2} -lt 22 ]]; then system::log_item "Ubuntu version ${_tgt_ubuntu_ver}: is greater than less than 22, build is deprecated. " _summary_message=" NOTE: Building a server for Ubuntu versions less than 22 is deprecated \n they are just too old and un modern to be useful. \n Press OK to return to the previous menu. \n" dialog::display_summary_message "NOTICE!" unset _UserDesktopEnvironmentSelection _UserServerEnvironemtSelection PRESEED_FILE vm_kernel vm_initrd source_url iso _task _role _cpu _dsk _mem _tgt_ubuntu_ver ubuntu_iso_url iso_filename CloudConfig CloudConfigDir _summary_message _saveto _ask _flavor _role _cpu _mem _dsk permanent_download_dir bin_virt_install virt_net vm_name all_params server_app pre_config return 1 else system::log_item "Ubuntu version ${_tgt_ubuntu_ver}: is greater than 23, so using cloud config to build server..." system::log_item "🐧 Ubuntu version ${_tgt_ubuntu_ver}: Using SERVER installer as requested..." system::log_item "Downloading the Ubuntu ISO if not already present to ${permanent_download_dir} for media-type server version ${_tgt_ubuntu_ver}" ubuntu::download_iso --dir ${permanent_download_dir} --media-type server --version ${_tgt_ubuntu_ver} : ${iso:="$permanent_download_dir/$iso_filename"} system::generate_cloudconfig --CloudConfigDir "${CloudConfigDir}" --vmname "${vm_name}" ${all_params} { "${bin_virt_install}" --name ${vm_name:-"Ubuntu-Server-${_tgt_ubuntu_ver}-$(date +%Y-%m-%d-%s)"} \ --vcpus "${_cpu}" \ --memory "${_mem}" \ --network "${virt_net}" \ --disk path="${disk_path},format=qcow2" \ --os-variant="ubuntu24.04" \ --video "virtio" --channel "spicevmc" \ --disk path="${CloudConfigDir}/CIDATA.iso,device=cdrom" \ --location="${iso},kernel=casper/vmlinuz,initrd=casper/initrd" \ --extra-args "autoinstall reboot=acpi" \ --events on_reboot=restart,on_poweroff=destroy,on_crash=destroy \ --wait --noautoconsole \ || (read -p "💥 - An ERROR has occurred. Please press [ENTER] to continue..." && return 1) } >>"${_LOGFILE}" 2>&1 & disown kvm::wait_for_domain_definition "${vm_name}" 60 3 || true if ! kvm::wait_for_domain_start --domain "${vm_name}"; then write_warning "Domain '${vm_name}' did not emit a start event within the expected window." fi dialog::display_summary_message "NOTICE!" fi ;; esac # Clean up residual variables... unset disk_path _UserDesktopEnvironmentSelection _UserServerEnvironemtSelection PRESEED_FILE vm_kernel vm_initrd source_url iso _task _role _cpu _dsk _mem _tgt_ubuntu_ver ubuntu_iso_url iso_filename CloudConfig CloudConfigDir _summary_message _saveto _ask _flavor _role _cpu _mem _dsk permanent_download_dir bin_virt_install virt_net vm_name all_params server_app pre_config } kvm::make_vm_now_from_microsoft() { # Description: # Function to create a KVM virtual michine disk and define a VM. This function should be # used on a Qemu/KVM virtual host that the virtual guest is to be used on. In the future # scripting will be added to allow for acting on remote machines directly... # At this time this functio takes no arguments but will prompt for what it needs if it is not available. # # Globals: # Arguments: [ win11 | win10 | win8 | win7 ] # Outputs: One VM running windows # Returns: # Usage: "kvm::make_vm_now_from_microsoft --winver target_winver" # # # NOTE: QCOW2 is a storage format for virtual . # machine disk images QCOW stands for QEMU copy on write. The QCOW2 format decouples the physical storage # layer from the virtual layer by adding a mapping between logical and physical blocks. # NOTE2: Kernel-based Virtual Machine (KVM) is an open source virtualization technology built into Linux®. # Specifically, KVM lets you turn Linux into a hypervisor that allows a host machine to run multiple, # isolated virtual environments called guests or virtual machines (VMs). KVM is part of Linux. # For complete info: https://www.redhat.com/en/topics/virtualization/what-is-KVM # set -e # Use the software::check_native_package_dependency function to make sure the needed software is available. # Usage: Either simply call this function to use the defaults or: # Call this function with "function" "RAM_SIZE" "DISK_SIZE" "DISK_FORMAT" "DISK_FILE" # Omitting any of the arguments will us the default instead. # # End of Documentation # Check for optional default T-Shirts size overrides... local _cpu _mem _dsk _saveto AutoUnattend PostTasks ConfigMenu WindowsMedia boot_options _virt_net # Set default values for VM T-Shirts sizes... : ${_cpu="${_cpu:-2}"} : ${_mem="${_mem:-4098}"} : ${_dsk="${_dsk:-100}"} : ${target_winver="${target_winver:-"win10"}"} : ${BIN_VIRT_INSTALL:="$(type -p virt-install)"} : ${CONFIG:="VDI"} : ${WindowsInstructions:="/var/lib/libvirt/boot/win$(date +%Y-%m-%d-%H-%M-%S-%s).img"} : ${virt_net="$(system::find_vm_bridge)"} if [[ "$virt_net" == "NONE" ]]; then write_error "No bridge interface detected. Supply --network explicitly (e.g., bridge=br0 or network=default) before proceeding." return 1 fi : ${_saveto="$(mktemp -d /tmp/WinConfig.XXXXXX)"} : ${AutoUnattend="${_saveto}/autounattend.xml"} : ${PostTasks="${_saveto}/rtd-me-sh.cmd"} : ${ConfigMenu="${_saveto}/_config_menu.cmd"} : ${preferred_video="${5:-"qxl"}"} : ${RTD_GUI:="dialog"} : ${_TLA:="$(basename "$0" | cut -c1-3)"} boot_options="cdrom,hd,menu=off" # Set default values for VM T-Shirts sizes... kvm::util::read_common_options ${*} # Set specific options for Debian... kvm::util::read_distro_options --distribution microsoft ${*} # Check that virtualization is supported on the host dependency::virtualization # Select the appropriate Windows ISO and UEFI boot option based on requested Windows version... case ${target_winver} in win12) : ${WindowsMedia="/var/lib/libvirt/boot/Win11_English_x64.iso"} boot_options="uefi,cdrom,hd,menu=off" ;; win11) : ${WindowsMedia="/var/lib/libvirt/boot/Win11_English_x64.iso"} boot_options="uefi,cdrom,hd,menu=off" ;; win10) : ${WindowsMedia="/var/lib/libvirt/boot/Win10_English_x64.iso"} ;; win8) : ${WindowsMedia="/var/lib/libvirt/boot/Win8_English_x64.iso"} ;; win7) : ${WindowsMedia="/var/lib/libvirt/boot/Win7_English_x64v1.iso"} ;; esac # Check for the needed ISO file and request it if not found... if [[ ! -e "${WindowsMedia}" ]]; then if ($RTD_GUI --backtitle "..$BRANDING" --title "💿 Windows Install Media Needed" \ --no-button "NO: Just Cancel" \ --yes-button "OK: I am done upploading" \ --yesno "Windows install media was not found where expected ${WindowsMedia} \n It is not possible to automatically download the media due to restrictions from Microsoft. Please place a copy of it in 📂 /home/$SUDO_USER/ and press [ OK ] \n Please name it Win**something**.iso NOTE: you can download Windows from Microsoft for free" 20 90); then if ls /home/$SUDO_USER/Win*.iso; then mv -v /home/$SUDO_USER/Win*.iso ${WindowsMedia} || pause::for_input 1S else write_error "Could not find the file: /home/$SUDO_USER/Downloads/Win*.iso " pause::for_input 1 return fi else return fi fi if [[ ! -f "${WindowsInstructions}" ]]; then write_information "Generate Windows Instruction media requested..." # Ensure mkfs.msdos is available if ! hash mkfs.msdos && ! software::check_native_package_dependency dosfstools; then write_error "Failed to install or find dosfstools. mkfs.msdos required to create the floppy disk." pause::for_input 1 "Install will not be fully automatic: Please press [ENTER] to continue..." fi command -v mkfs.msdos >/dev/null && { # Create virtual floppy disk, skipp if some optional tasks above failed... if ! mkfs.msdos -C "${WindowsInstructions}" 1440; then write_error "Failed to create the virtual floppy disk..." pause::for_input 1 "Install will not be fully automatic: Please press [ENTER] to continue..." fi } # Generate autounattend.xml write_information "Generate installations instructions for Windows ${AutoUnattend}..." if ! template::autounattend_xml --write "${AutoUnattend}" --winver ${target_winver}; then write_error "Failed to create the autounattend.xml file..." pause::for_input 1 "Install will not be fully automatic: Please press [ENTER] to continue..." fi # Generate rtd-me-sh.cmd write_information "Generate post install bootstrap script ${PostTasks}..." if ! template::rtd_me_sh_cmd --write "${PostTasks}"; then write_error "Failed to create the rtd-me-sh.cmd file..." pause::for_input 1 "Install will not be fully automatic: Please press [ENTER] to continue..." fi # Generate _config_menu.cmd write_information "Generate Windows config menu script ${ConfigMenu}..." if ! template::config_menu_cmd --write "${ConfigMenu}"; then write_error "Failed to create the _config_menu.cmd file..." pause::for_input 1 "Install will not be fully automatic: Please press [ENTER] to continue..." fi # Ensure mcopy is available if ! hash mcopy && ! software::check_native_package_dependency mtools; then write_warning "No mcopy found in mtools (unable to install?) to copy the post-install script to the floppy disk..." pause::for_input 1 "Install will not be fully automatic: Please press [ENTER] to continue..." fi # Copy files to the floppy disk but skipp if some optional tasks above failed... command -v mcopy >/dev/null && { mcopy -o -i "${WindowsInstructions}" "${PostTasks}" ::/ || pause::for_input 1 mcopy -o -i "${WindowsInstructions}" "${ConfigMenu}" ::/ || pause::for_input 1 mcopy -o -i "${WindowsInstructions}" "${AutoUnattend}" ::/ || pause::for_input 1 write_information "Copy the Windows.mod files files to the floppy disk..." for i in $(find /opt/${_TLA,,} -type d -name "windows.mod")/_*; do if [[ -e "${i}" ]]; then command -v mcopy >/dev/null && mcopy -o -i "${WindowsInstructions}" "${i}" ::/ || pause::for_input 1 write_status "Copied ${i} to the floppy disk..." else write_warning "No files found in ${i} to copy to the floppy disk..." fi done } fi local _win_suffix="${target_winver:(-2)}" local _random_suffix="${RANDOM}" local _existing_vm_name="${vm_name:-}" local vm_name="" if [[ -n "$_existing_vm_name" ]]; then kvm::util::set_vm_name --var vm_name --name "$_existing_vm_name" || return 1 else kvm::util::set_vm_name --var vm_name "Template_Windows${_win_suffix}" "${CONFIG}" "${_random_suffix}" --no-timestamp || return 1 fi local disk_path disk_path=$(kvm::create_expanding_vm_hardisk "$vm_name" "$_dsk") || { write_error "❌ Failed to create VM disk image. Aborting VM creation." return 1 } _summary_message="The virtual machine (${vm_name}) \n 📋 - Using the instructions in: ${AutoUnattend} \n 🔧 - And Using this source for the packages and files to download: \n 🌎 - ${WindowsMedia} \n 🌎 - ${WindowsInstructions} \n 🌎 - ${PostTasks} \n 🌎 - ${ConfigMenu} \n 🖥️ - Using the network: ${virt_net:=default} \n 🖥️ - Memory: ${_mem} \n 🖥️ - CPU's: ${_cpu} \n 🖥️ - Disk Size: ${_dsk} \n NOTE: You may connecct to the VM and see the progress at IP: $(hostname -I) \n After the build is complete you may reseal the VM and use it as a template for future VMs. \n" write_status "Creating and starting Virtual machine... " { "${BIN_VIRT_INSTALL}" --connect qemu:///system --name "${vm_name}" \ --vcpus "${_cpu}" \ --memory "${_mem}" \ --network "${virt_net}" \ --disk path="${disk_path},format=qcow2" \ --video ${preferred_video} \ --os-variant="$(if osinfo-query -s -f os | grep ${target_winver} &>/dev/null; then echo ${target_winver}; else echo win10; fi)" \ --cdrom "${WindowsMedia}" \ --disk "${WindowsInstructions}",device=floppy \ --livecd \ --boot ${boot_options} \ --features kvm_hidden=on,smm=on \ --tpm backend.type=emulator,backend.version=2.0,model=tpm-tis \ --wait --noautoconsole \ || (read -p "💥 - An ERROR has occurred. Please press [ENTER] to continue..." && return 1) } >>"${_LOGFILE}" 2>&1 & disown kvm::wait_for_domain_definition "${vm_name}" 60 3 || true if ! kvm::wait_for_domain_start --domain "${vm_name}"; then write_warning "Domain '${vm_name}' did not emit a start event within the expected window." fi system::log_item "Attempt to automatically satisfy the Windows "Press any key to boot from CD or DVD" prompt." if command -v virsh >/dev/null 2>&1; then ( sleep 8 if virsh --connect qemu:///system list --name | grep -Fxq "${vm_name}"; then virsh --connect qemu:///system send-key "${vm_name}" KEY_ENTER KEY_ENTER KEY_ENTER >/dev/null 2>&1 \ || system::log_item "Failed to auto-send boot key for ${vm_name}" fi ) & else system::log_item "virsh not available; unable to auto-send boot key for ${vm_name}" fi dialog::display_summary_message "NOTICE!" rm -f "${PostTasks}" || system::log_item "Failed to remove ${PostTasks}..." rm -f "${ConfigMenu}" || system::log_item "Failed to remove ${ConfigMenu}..." for i in WindowsInstructions CONFIG WindowsMedia AutoUnattend PostTasks ConfigMenu target_winver mem_size boot_options cpu_count disk_size preferred_video disk_path; do unset $i; done } kvm::backup_all_running_vm() { # Description: # This function backs up all running virtual machines on the host system. It checks for necessary components # (KVM, QEMU) and verifies if the VMs are running before performing the backup. If the VM's name contains "nobackup", # it will be excluded from the backup process. Each VM's disk is backed up using snapshots, and backup files are # stored in the specified target directory. # # Globals: # - VM_BACKUP_TARGET: The directory where VM backups will be stored (default: /mnt/vmdsk/VM_BACKUP). # Arguments: # - None # Outputs: # - Backups of running VMs are created in the backup directory. # Returns: # - 0 if all backups complete successfully. # - 1 if any errors occur during the backup process. # # Usage: # kvm::backup_all_running_vm # # Example: # kvm::backup_all_running_vm # # End of Documentation kvm::backup_all_running_vm::do_vm_backup() { # Display list of VMs to be backed up dialog::display_result "Attempting backup of the following VMs:" "$(virsh list)" DATE=$(date +%Y-%m-%d.%H:%M:%S) LOG="/var/log/kvm-backup.$DATE.LOG" BACKUPROOT=${VM_BACKUP_TARGET:=/mnt/vmdsk/VM_BACKUP} # Get list of all running virtual machines DOMAINS=$(virsh list --all | tail -n +3 | awk '{print $2}') for DOMAIN in $DOMAINS; do dialog::display_notice "Starting backup for $DOMAIN..." if [[ $DOMAIN == *"nobackup"* ]]; then dialog::display_notice "Skipping $DOMAIN as it is marked for exclusion." continue fi VMSTATE=$(virsh list --all | grep $DOMAIN | awk '{print $3}') if [[ $VMSTATE != "running" ]]; then dialog::display_notice "Skipping $DOMAIN as it is not running." continue fi BACKUPFOLDER=$BACKUPROOT/KVM-BACKUPS/$DOMAIN mkdir -p "$BACKUPFOLDER" TARGETS="$(virsh domblklist "$DOMAIN" --details | grep disk | awk '{print $3}')" IMAGES="$(virsh domblklist "$DOMAIN" --details | grep disk | awk '{print $4}')" DISKSPEC="" for TARGET in $TARGETS; do DISKSPEC="$DISKSPEC --diskspec $TARGET,snapshot=external" done virsh snapshot-create-as --domain "$DOMAIN" --name "backup-$DOMAIN" --no-metadata --atomic --disk-only "$DISKSPEC" >>"$LOG" if [ $? -ne 0 ]; then dialog::display_error "Failed to create snapshot for $DOMAIN." continue fi for IMAGE in $IMAGES; do NAME=$(basename $IMAGE) if test -f "$BACKUPFOLDER/$NAME"; then dialog::display_summary_message "Backup exists, merging changes to image for $DOMAIN." rsync -apvz --inplace "$IMAGE" "$BACKUPFOLDER"/"$NAME" >>"$LOG" else dialog::display_summary_message "Creating a full sparse copy for $DOMAIN." rsync -apvz --sparse "$IMAGE" "$BACKUPFOLDER"/"$NAME" >>"$LOG" fi done BACKUPIMAGES="$(virsh domblklist "$DOMAIN" --details | grep disk | awk '{print $4}')" for TARGET in $TARGETS; do if ! virsh blockcommit "$DOMAIN" "$TARGET" --active --pivot >>"$LOG"; then dialog::display_error "Could not merge changes for disk of $TARGET for $DOMAIN. VM may be in an invalid state." continue fi done for BACKUP in $BACKUPIMAGES; do if [[ $BACKUP == *"backup-"* ]]; then rm -f "$BACKUP" dialog::display_summary_message "Deleted temporary image $BACKUP." fi done virsh dumpxml "$DOMAIN" >"$BACKUPFOLDER"/"$DOMAIN".xml dialog::display_summary_message "Finished backup of $DOMAIN." done } if command -v virsh &>/dev/null; then if dialog::display_notice "Do you want to back up the following VMs? $(virsh list --all)"; then kvm::backup_all_running_vm::do_vm_backup else return fi else if dialog::prompt_yes_no "KVM Virtualization is not installed on this machine. Would you like to attempt installation?"; then dependency::virtualization || { dialog::display_error "KVM installation failed." return } && dialog::display_notice "KVM installation completed." kvm::backup_all_running_vm::do_vm_backup else return fi fi } kvm::clone_template_vm::_build_customize_args() { # Description: # Helper function to split a virt-customize option string into an array. # When the provided spec is a file path it is treated as a script passed via --run. # # Globals: None # # Arguments: # $1 - The user supplied specification string or script path. # $2 - Name reference to the array that will receive the parsed arguments. # # Outputs: # - Populates the referenced array with parsed arguments. # # Returns: # 0 on success, 1 on parsing failure. # # Usage: # local -a args # kvm::clone_template_vm::_build_customize_args "--install htop --run /tmp/script.sh" args # End of documentation local spec="$1" local -n _result_ref="$2" _result_ref=() if [[ -z "$spec" ]]; then return 0 fi if [[ -f "$spec" ]]; then _result_ref=(--run "$spec") return 0 fi if command -v python3 >/dev/null 2>&1; then local parsed_output if ! parsed_output=$(python3 - "$spec" <<'PY' import shlex import sys try: parts = shlex.split(sys.argv[1]) except ValueError as exc: sys.stderr.write(str(exc)) sys.exit(1) for token in parts: print(token) PY ); then return 1 fi mapfile -t _result_ref <<<"$parsed_output" else write_warning "python3 not found; basic shell parsing will be used for --customize arguments." eval "set -- $spec" _result_ref=("$@") fi return 0 } kvm::clone_template_vm::_format_name_component() { # Description: # Convert a raw string into Title_Case components joined by a delimiter (underscore by default). # Globals: None # Arguments: # $1 - Raw string to normalize (underscores/spaces/dashes supported). # $2 - Optional joiner (default: "_"; use " " for spaced labels). # Outputs: # Echoes the normalized string. # Returns: # 0 # Usage: # label=$(kvm::clone_template_vm::_format_name_component "ubuntu_server" " ") # End of documentation local raw="$1" local joiner="${2:-_}" raw="${raw//-/_}" raw="${raw// /_}" local out="" IFS='_' read -ra parts <<<"$raw" for part in "${parts[@]}"; do [[ -z "$part" ]] && continue out+="${part^}${joiner}" done out=${out%"${joiner}"} echo "$out" } kvm::clone_template_vm::_display_template_label() { # Description: # Build a human-friendly label for a template entry (e.g., "Source: Ubuntu Server"). # Globals: None # Arguments: # $1 - Template VM name (expects prefix Template_ and optional suffixes). # Outputs: # Echoes a formatted label string. # Returns: # 0 # Usage: # kvm::clone_template_vm::_display_template_label "Template_ubuntu_server__base" # End of documentation local template_name="$1" local core="${template_name#Template_}" core="${core%%__*}" core=$(kvm::clone_template_vm::_format_name_component "$core" " ") echo "Source: ${core}" } kvm::clone_template_vm::_clone_from_category() { # Description: # Internal helper that drives the template clone dialog for a VM category. # # Globals: # RTD_GUI, BACKTITLE, HEIGHT, WIDTH, LIST_HEIGHT, DIALOG_CANCEL, DIALOG_ESC # (dialog appearance) plus RTD_VM_SERVICE_LABEL (optional). # # Arguments: # $1 - Case-insensitive pattern to identify template VMs (matched after the Template prefix). # $2 - Dialog title string. # $3 - Dialog menu prompt string. # $4 - Error message shown when no templates are found. # $5 - Success label ("VM", "VDI VM", etc.). # $@ - Optional: --sysprep flag and/or --customize "". # --post-clone-handler : function called with new VM name after successful clone. # # Outputs: # Dialog UI and status/error messages. # # Returns: # 0 when a VM is cloned successfully, 1 otherwise. # # Usage: # kvm::clone_template_vm::_clone_from_category "server" "Clone Template" "Pick one:" "No templates" "VM" --sysprep --customize "--install htop" # End of documentation local pattern="$1" local menu_title="$2" local menu_prompt="$3" local empty_message="$4" local success_label="$5" shift 5 # Soften errexit while we drive interactive dialog flows. local enable_sysprep=0 local customize_spec="" local post_clone_handler="" while [[ $# -gt 0 ]]; do case "$1" in --sysprep) enable_sysprep=1 system::log_item "--sysprep requested by ${FUNCNAME[1]}" shift ;; --customize) if [[ -z "${2:-}" ]]; then write_error "--customize requires an additional argument specifying virt-customize options or a script path." return 1 fi customize_spec="$2" system::log_item "customize_spec requested by ${FUNCNAME[1]} ='${customize_spec}'" shift 2 ;; --post-clone-handler) if [[ -z "${2:-}" ]]; then write_warning "--post-clone-handler provided without a function name." shift continue fi post_clone_handler="$2" system::log_item "post_clone_handler requested by ${FUNCNAME[1]} ='${post_clone_handler}'" shift 2 ;; *) write_warning "Ignoring unrecognized option '$1' for template cloning." shift ;; esac done local vm_list local -a menu_entries=() local template_name new_name selected_tag exit_status write_status "clone_from_category: pattern=${pattern} title='${menu_title}' prompt='${menu_prompt}'" if ! command -v virsh >/dev/null || ! command -v "$RTD_GUI" >/dev/null; then write_error "❌ Required software not found: may I install it for you?" read -rp "Press Enter to continue..." software::check_native_package_dependency virsh dialog if ! command -v virsh >/dev/null || ! command -v "$RTD_GUI" >/dev/null; then write_error "❌ Required commands not found: virsh or dialog wrapper ($RTD_GUI), please install them manually." return 1 fi write_information "✅ Required software installed successfully." return 1 fi if ! command -v virt-clone >/dev/null 2>&1; then write_error "❌ virt-clone is required but not installed. Install libvirt-clients or libvirt-tools." return 1 fi if (( enable_sysprep )) && ! command -v virt-sysprep >/dev/null 2>&1; then write_error "❌ virt-sysprep requested via --sysprep but not found. Install libguestfs-tools." return 1 fi if [[ -n "$customize_spec" ]] && ! command -v virt-customize >/dev/null 2>&1; then write_error "❌ virt-customize requested via --customize but not found. Install libguestfs-tools." return 1 fi if ! vm_list=$(virsh list --all --name | grep -i '^Template' | grep -i -- "$pattern"); then dialog::display_error "$empty_message" write_warning "clone_from_category: no matching templates for pattern '${pattern}'" return 1 fi write_information "clone_from_category: available templates:\n${vm_list}" while IFS= read -r template_name; do [[ -z "$template_name" ]] && continue menu_entries+=("$template_name" "$(kvm::clone_template_vm::_display_template_label "$template_name")") done <<< "$vm_list" if (( ${#menu_entries[@]} == 0 )); then write_error "❌ No Template VMs found to clone" return 1 fi while true; do write_status "clone_from_category: launching selection dialog (entries=${#menu_entries[@]})" exec 3>&1 selected_tag=$("$RTD_GUI" \ --backtitle "$BACKTITLE" --cancel-button "Back" \ --title "$menu_title" \ --menu "$menu_prompt" "$HEIGHT" "$WIDTH" "$LIST_HEIGHT" \ "${menu_entries[@]}" \ 2>&1 1>&3) exit_status=$? exec 3>&- clear write_status "clone_from_category: dialog exit_status=${exit_status} selected_tag='${selected_tag}'" case "$exit_status" in "$DIALOG_CANCEL"|"$DIALOG_ESC") write_warning "clone_from_category: user cancelled from template menu." return 0 ;; esac if [[ -z "${selected_tag}" ]]; then write_warning "clone_from_category: no selection returned; aborting." return 1 fi template_name="${selected_tag}" local template_core="${template_name#Template_}" template_core="${template_core%%__*}" local template_os="${template_core%%_*}" if [[ -z "$template_os" || "$template_os" == "$template_core" ]]; then template_os="$template_core" fi template_os=$(kvm::clone_template_vm::_format_name_component "$template_os") local service_label="${RTD_VM_SERVICE_LABEL:-Server}" service_label=$(kvm::clone_template_vm::_format_name_component "$service_label") new_name="${template_os}_${service_label}_$(date +%Y%m%d_%H%M%S)" if [[ -z "$template_name" ]]; then write_warning "clone_from_category: template_name empty after selection_index='${selected_index}'." return 1 fi write_status "clone_from_category: user selected ${template_name}, new VM will be ${new_name}" write_status "📀 Cloning ${success_label} '$template_name' to '$new_name'..." if virsh dominfo "$new_name" &>/dev/null; then write_error "⚠️ VM '$new_name' already exists. Choose a different name or remove the existing VM." sleep 2 continue fi if ! virt-clone --original "$template_name" --name "$new_name" --auto-clone; then write_error "❌ Failed to clone VM '$template_name'" return 1 fi write_information "clone_from_category: virt-clone completed for ${new_name}" if (( enable_sysprep )); then write_status "🧼 Running virt-sysprep on '$new_name'..." if ! virt-sysprep -d "$new_name"; then write_error "❌ virt-sysprep failed for '$new_name'" return 1 fi write_information "✅ virt-sysprep completed for '$new_name'" fi if [[ -n "$customize_spec" ]]; then local spec_for_vm="${customize_spec//\{\{VM_NAME\}\}/$new_name}" local -a customize_args=() if ! kvm::clone_template_vm::_build_customize_args "$spec_for_vm" customize_args || (( ${#customize_args[@]} == 0 )); then write_error "❌ Unable to parse --customize arguments for '$new_name'." return 1 fi write_status "🛠️ Running virt-customize on '$new_name'..." if ! virt-customize -d "$new_name" "${customize_args[@]}"; then write_error "❌ virt-customize failed for '$new_name'" return 1 fi write_information "✅ virt-customize completed for '$new_name'" fi # Expose the last cloned VM name for callers and allow optional handler execution. export RTD_LAST_CLONED_VM="$new_name" if [[ -n "$post_clone_handler" ]]; then write_status "clone_from_category: invoking post-clone handler '${post_clone_handler}' for ${new_name}" if ! "$post_clone_handler" "$new_name"; then write_warning "Post-clone handler '${post_clone_handler}' reported a problem for '$new_name'." fi fi write_information "✅ ${success_label} '$new_name' created successfully from template '$template_name'" sleep 2 return 0 done } kvm::clone_server_template_vm() { # Description: # Display available server template VMs (Template*server*) and clone the selected entry. # Optional flags enable virt-sysprep (--sysprep) and virt-customize (--customize ""). # Use {{VM_NAME}} inside the --customize string to expand to the generated VM name. # # Globals: # RTD_GUI, BACKTITLE, HEIGHT, WIDTH, LIST_HEIGHT, DIALOG_CANCEL, DIALOG_ESC # Arguments: # --sysprep : Run virt-sysprep against the cloned VM before returning. # --customize "..." : Pass additional virt-customize options or a script path executed with --run. # Outputs: # Dialog UI and informational status lines. # Returns: # 0 on success, 1 on failure. # Usage: # kvm::clone_server_template_vm --sysprep --customize "--install htop" # End of documentation system::log_item "Called with args: $*" kvm::clone_template_vm::_clone_from_category \ "server" \ "Clone Template Server VM" \ "Select a Template VM to clone:" \ "❌ No Server Virtual Machine Templates found on this system. Please create a template VM first." \ "VM" \ "$@" } kvm::clone_vdi_template_vm() { # Description: # Display available VDI template VMs (Template*VDI*) and clone the selected entry. # Optional flags enable virt-sysprep (--sysprep) and virt-customize (--customize ""). # Use {{VM_NAME}} inside the --customize string to expand to the generated VM name. # # Globals: # RTD_GUI, BACKTITLE, HEIGHT, WIDTH, LIST_HEIGHT, DIALOG_CANCEL, DIALOG_ESC # Arguments: # --sysprep : Run virt-sysprep against the cloned VM before returning. # --customize "..." : Pass additional virt-customize options or a script path executed with --run. # Outputs: # Dialog UI and informational status lines. # Returns: # 0 on success, 1 on failure. # Usage: # kvm::clone_vdi_template_vm --customize "/opt/customize.sh" # End of documentation system::log_item "Called with args: $*" kvm::clone_template_vm::_clone_from_category \ "VDI" \ "Clone Template VDI VM" \ "Select a Template VDI VM to clone:" \ "❌ No VDI Virtual Machine Templates found on this system. Please create a template VM first." \ "VDI VM" \ "$@" } kvm::cicd::_render_extra_vars() { # Description: # Render an extra-vars YAML file for ansible-pull based on provided inputs. # Arguments: # $1 - output file path # $2 - CSV of server tasks # $3 - llm_variant # $4 - CSV of llm models (for ollama) # Returns: # 0 on success # End of documentation local file="$1" ; shift local server_tasks_csv="$1" ; shift local llm_variant="$1" ; shift local llm_models_csv="$1" ; shift local llm_enable=false [[ "$llm_variant" != "none" && -n "$llm_variant" ]] && llm_enable=true IFS=',' read -r -a tasks_arr <<<"${server_tasks_csv:-openssh-server}" IFS=',' read -r -a models_arr <<<"${llm_models_csv:-}" { echo "server_tasks:" for t in "${tasks_arr[@]}"; do [[ -z "$t" ]] && continue echo " - ${t}" done echo "llm_enable: ${llm_enable}" echo "llm_variant: \"${llm_variant:-none}\"" if [[ "$llm_variant" == "ollama" ]]; then echo "ollama_models:" for m in "${models_arr[@]}"; do [[ -z "$m" ]] && continue echo " - ${m}" done fi } >"$file" } kvm::cicd::_render_user_data() { # Description: # Build cloud-init user-data that installs ansible and runs ansible-pull with prepared extra vars. # Arguments: # $1 - output user-data path # $2 - hostname for the guest # $3 - repo URL for ansible-pull # $4 - playbook path inside the repo # $5 - target path for the extra-vars file inside the guest # $6 - source path to the rendered extra-vars file on the host # Returns: # 0 on success # End of documentation local user_data="$1" local hostname="$2" local repo_url="$3" local playbook_path="$4" local extra_vars_path="$5" local extra_vars_source="$6" local extra_vars_dir extra_vars_dir=$(dirname "$extra_vars_path") cat >"$user_data" <"$meta_file" </dev/null 2>&1 || true if command -v cloud-localds >/dev/null 2>&1; then cloud-localds "$iso_path" "$user_data" "$meta_data" return $? fi if command -v genisoimage >/dev/null 2>&1; then genisoimage -output "$iso_path" -volid cidata -joliet -rock "$user_data" "$meta_data" return $? fi write_error "Neither cloud-localds nor genisoimage is available to create NoCloud ISO." return 1 } kvm::cicd::configure_vm_with_ansible_pull() { # Description: # Create a NoCloud ISO containing ansible-pull instructions and attach it to a VM, then start the VM. # # Arguments: # --vm : Target VM name (required). # --repo : ansible-pull repository URL (default: https://github.com/vonschutter/RTD-Ansible.git). # --playbook : Playbook path inside repo (default: playbooks/server-auto-config.yml). # --server-tasks : Comma-separated server tasks (default: openssh-server). # --llm-variant : LLM variant (none|ollama|text-generation-webui) default none. # --llm-models : Comma-separated ollama models to pre-pull. # --iso-path : Where to write the ISO (default: temp file in /tmp). # # Returns: # 0 on success, non-zero on error. # End of documentation local vm_name="" local repo_url="https://github.com/vonschutter/RTD-Ansible.git" local playbook_path="playbooks/server-auto-config.yml" local server_tasks_csv="openssh-server" local llm_variant="none" local llm_models_csv="" local iso_path="" local extra_vars_target="/etc/ansible-pull/extra-vars.yml" while [[ $# -gt 0 ]]; do case "$1" in --vm) vm_name="$2"; shift 2 ;; --repo) repo_url="$2"; shift 2 ;; --playbook) playbook_path="$2"; shift 2 ;; --server-tasks) server_tasks_csv="$2"; shift 2 ;; --llm-variant) llm_variant="$2"; shift 2 ;; --llm-models) llm_models_csv="$2"; shift 2 ;; --iso-path) iso_path="$2"; shift 2 ;; *) write_warning "kvm::cicd::configure_vm_with_ansible_pull: ignoring unknown arg '$1'"; shift ;; esac done if [[ -z "$vm_name" ]]; then write_error "kvm::cicd::configure_vm_with_ansible_pull requires --vm " return 1 fi # Place the seed ISO in the libvirt images path for correct labeling; still use a unique temp name. local iso_dir="/var/lib/libvirt/images" mkdir -p "$iso_dir" iso_path="${iso_path:-$(mktemp -p "$iso_dir" -t "${vm_name}_cicd_XXXXXX.iso")}" local tmpdir if ! tmpdir=$(mktemp -d); then write_error "Failed to allocate temp dir for NoCloud data." return 1 fi local extra_vars="${tmpdir}/extra-vars.yml" local user_data="${tmpdir}/user-data" local meta_data="${tmpdir}/meta-data" kvm::cicd::_render_extra_vars "$extra_vars" "$server_tasks_csv" "$llm_variant" "$llm_models_csv" kvm::cicd::_render_user_data "$user_data" "$vm_name" "$repo_url" "$playbook_path" "${extra_vars_target}" "${extra_vars}" kvm::cicd::_render_meta_data "$meta_data" "$vm_name" write_status "Creating NoCloud ISO for $vm_name at $iso_path" if ! kvm::cicd::_create_nocloud_iso "$iso_path" "$user_data" "$meta_data"; then rm -rf "$tmpdir" return 1 fi rm -rf "$tmpdir" if ! virsh dominfo "$vm_name" &>/dev/null; then write_error "VM '$vm_name' does not exist; cannot attach ISO." return 1 fi # Ensure cloud-init is installed in the guest so the seed is acted upon. # virt-customize lives in libguestfs-tools on Ubuntu/Debian. software::check_native_package_dependency libguestfs-tools >/dev/null 2>&1 || true if command -v virt-customize >/dev/null 2>&1; then write_status "Ensuring cloud-init is present on ${vm_name}..." if ! virt-customize -d "$vm_name" --install cloud-init >/dev/null 2>&1; then write_warning "Unable to verify/install cloud-init inside ${vm_name}. Continuing." fi # Re-enable cloud-init in case the template had it disabled. write_status "Re-enabling cloud-init in ${vm_name} if it was disabled..." if ! virt-customize -d "$vm_name" \ --run-command "rm -f /etc/cloud/cloud-init.disabled" \ --run-command "rm -f /etc/cloud/cloud.cfg.d/99-installer.cfg /etc/cloud/cloud.cfg.d/99-disable-network-config.cfg || true" \ --run-command "cloud-init clean || true"; then write_warning "Could not re-enable/clean cloud-init inside ${vm_name}; seed may be ignored." fi else write_warning "virt-customize not available; cannot pre-install cloud-init if missing." fi write_status "Prepared NoCloud seed at ${iso_path} (size: $(stat -c%s "$iso_path" 2>/dev/null || echo '?') bytes)" # Attach the ISO as a cdrom device (config only; live if running). local cd_targets=() while IFS= read -r t; do [[ -n "$t" ]] && cd_targets+=("$t") done < <(virsh dumpxml "$vm_name" 2>/dev/null | awk ' // {in_cd=0} ') # Keep one CD target, remove extras to avoid accumulation. if (( ${#cd_targets[@]} > 1 )); then for ((i=1; i<${#cd_targets[@]}; i++)); do write_status "Detaching extra cdrom target ${cd_targets[$i]} from ${vm_name}" virsh detach-disk "$vm_name" "${cd_targets[$i]}" --config --live >/dev/null 2>&1 || true done fi if (( ${#cd_targets[@]} >= 1 )); then local cd_target="${cd_targets[0]}" write_status "Using existing cdrom target ${cd_target} for ${vm_name}" if ! virsh change-media "$vm_name" "$cd_target" --insert "$iso_path" --config --force --live >/dev/null 2>&1; then write_error "Unable to attach ISO $iso_path to $vm_name via change-media on ${cd_target}" return 1 fi else local attach_target="sdz" local attach_args=(attach-disk "$vm_name" "$iso_path" "$attach_target" --type cdrom --targetbus sata --mode readonly --config) virsh domstate "$vm_name" | grep -qi running && attach_args+=(--live) if ! virsh "${attach_args[@]}" >/dev/null 2>&1; then write_error "Unable to attach ISO $iso_path to $vm_name (attach-disk ${attach_target})" return 1 fi fi if ! virsh domstate "$vm_name" | grep -qi running; then write_status "Starting VM $vm_name with CI/CD seed attached..." if ! virsh start "$vm_name" >/dev/null 2>&1; then write_error "Failed to start VM $vm_name" return 1 fi else write_information "VM $vm_name already running; ISO attached for next boot." fi write_information "CI/CD seed ISO attached to $vm_name. Ansible pull will run at first boot via cloud-init." return 0 } #:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: #:::::::::::::: :::::::::::::::::::::: #:::::::::::::: Whonix helper functions :::::::::::::::::::::: #:::::::::::::: :::::::::::::::::::::: #:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: whonix::init_context() { # Description: # Initialize Whonix-related globals with sane defaults so downstream functions # can safely run under `set -u`. Intended to be called before any Whonix # download/extract/define helpers. # # Globals: # WORK_DIR - Working directory for Whonix assets (default: ~/.local/share/whonix-kvm). # IMAGE_DIR - Destination for libvirt images (default: /var/lib/libvirt/images). # WHONIX_BASE - Base URL to fetch Whonix bundles. # WHONIX_URL - Resolved bundle URL (set later by helper functions). # WHONIX_SHA - Expected SHA512 checksum URL/content (set later). # WHONIX_VERSION - Target Whonix version string. # BUNDLE_NAME - Bundle filename discovered/downloaded. # BUNDLE_PATH - Full path to the downloaded bundle. # IMAGE_OWNER - Desired owner:group for placed images. # REFRESH - Flag (0/1) to force refresh of existing bundle/images. # ADD_WORKSTATION - Flag (0/1) to add an extra workstation VM. # # Arguments: None # # Outputs: None # # Returns: # 0 always; the function only sets defaults. # # Usage: # whonix::init_context # whonix::discover_bundle # downstream functions rely on initialized globals # # End of documentation : "${WORK_DIR:=${HOME:-/tmp}/.local/share/whonix-kvm}" : "${IMAGE_DIR:=/var/lib/libvirt/images}" : "${WHONIX_BASE:=https://download.whonix.org/libvirt}" : "${WHONIX_URL:=}" : "${WHONIX_SHA:=}" : "${WHONIX_VERSION:=}" : "${BUNDLE_NAME:=}" : "${BUNDLE_PATH:=}" : "${IMAGE_OWNER:=}" : "${REFRESH:=0}" : "${ADD_WORKSTATION:=0}" } whonix::version_ge() { # Description: # Compare two dotted version strings and succeed if the first is >= the second. # Globals: # None # Arguments: # - $1: Version A # - $2: Version B # Outputs: # None # Returns: # 0 when A >= B, 1 otherwise # Usage: # if whonix::version_ge "17.0.1.7" "17.0"; then ... # End of documentation [[ -z ${1:-} || -z ${2:-} ]] && return 1 [[ $(printf '%s\n%s\n' "$1" "$2" | sort -V | tail -1) == "$1" ]] } whonix::current_vm_version() { # Description: # Extract the Whonix version from an existing VM disk path. # Globals: # - IMAGE_DIR: Directory containing libvirt images. # Arguments: # - $1: VM name (Whonix-Gateway or Whonix-Workstation) # Outputs: # - Echoes the detected version (if any) # Returns: # 0 (always); empty output when the VM or disk is not found. # Usage: # version=$(whonix::current_vm_version Whonix-Gateway) # End of documentation whonix::init_context local vm=$1 disk="" if virsh -c qemu:///system list --all --name | grep -qxF "$vm"; then disk=$(virsh -c qemu:///system dumpxml "$vm" 2>/dev/null | sed -nE "s@.* $BUNDLE_NAME)" rm -rf "$WORK_DIR" fi fi mkdir -p "$WORK_DIR" } whonix::download_with_verify() { # Description: Download a file with retries and optional SHA-512 verification. # Globals: None # Arguments: # - $1: URL # - $2: Destination path # - $3: Checksum URL (optional) # Outputs: Log messages. # Returns: 0 on success, 1 on failure # Usage: # whonix::download_with_verify "$url" "/tmp/file" "$url.sha512sums" # End of documentation local url=$1 dest=$2 sha=${3:-} tmp="${dest}.part" retry=2 while ((retry--)); do write_information "downloading $(basename "$dest") ..." if wget --progress=bar:force:noscroll -O "$tmp" "$url"; then mv "$tmp" "$dest" break fi write_warning "download failed, retrying ..." sleep 1 done rm -f "$tmp" [[ -s $dest ]] || return 1 if [[ -n $sha ]]; then write_information "fetching checksum" local expected expected=$(wget -qO- "$sha" | awk '{print $1}') [[ ${#expected} -eq 128 ]] || return 1 write_information "verifying SHA-512" echo "$expected $dest" | sha512sum -c >/dev/null || return 1 fi } whonix::download_bundle() { # Description: Fetch the Whonix bundle if not already present locally. # Globals: # - BUNDLE_PATH, BUNDLE_NAME, WHONIX_URL, WHONIX_SHA # Arguments: None # Outputs: Log messages. # Returns: 0 # Usage: # whonix::discover_bundle # whonix::prepare_workdir # whonix::download_bundle # End of documentation whonix::init_context if [[ -s $BUNDLE_PATH ]]; then write_status "re-using existing $BUNDLE_NAME" return 0 fi whonix::download_with_verify "$WHONIX_URL" "$BUNDLE_PATH" "$WHONIX_SHA" || return 1 } whonix::extract_archive() { # Description: Extract the Whonix libvirt archive only when images/XMLs are missing. # Globals: # - WORK_DIR, IMAGE_DIR, BUNDLE_NAME, BUNDLE_PATH, WHONIX_VERSION # Arguments: None # Outputs: Log messages. # Returns: 0 on success, 1 on failure # Usage: # whonix::extract_archive # End of documentation whonix::init_context local marker="${WORK_DIR}/.bundle.name" have_marker=0 have_images=0 [[ -s $marker && $(<"$marker") == "$BUNDLE_NAME" ]] && have_marker=1 if compgen -G "$WORK_DIR"/Whonix*-Gateway*.qcow2 >/dev/null \ && compgen -G "$WORK_DIR"/Whonix*-Workstation*.qcow2 >/dev/null; then have_images=1 elif [[ -d $IMAGE_DIR ]]; then if compgen -G "$IMAGE_DIR"/Whonix*-Gateway*"${WHONIX_VERSION}"*.qcow2 >/dev/null \ && compgen -G "$IMAGE_DIR"/Whonix*-Workstation*"${WHONIX_VERSION}"*.qcow2 >/dev/null; then have_images=1 elif compgen -G "$IMAGE_DIR"/Whonix*-Gateway*.qcow2 >/dev/null \ && compgen -G "$IMAGE_DIR"/Whonix*-Workstation*.qcow2 >/dev/null; then have_images=1 fi fi if (( have_marker )) \ && ls "$WORK_DIR"/Whonix*-Gateway*.xml "$WORK_DIR"/Whonix*-Workstation*.xml &>/dev/null \ && (( have_images )); then write_status "Using existing extraction for $BUNDLE_NAME" return 0 fi write_status "Extracting archive (this can take a while)" find "$WORK_DIR" -mindepth 1 -maxdepth 1 ! -name "$(basename "$BUNDLE_PATH")" ! -name ".bundle.name" -exec rm -rf -- {} + tar -xf "$BUNDLE_PATH" -C "$WORK_DIR" echo "$BUNDLE_NAME" > "$marker" ls "$WORK_DIR"/Whonix*-Gateway*.xml &>/dev/null || return 1 ls "$WORK_DIR"/Whonix*-Workstation*.xml &>/dev/null || return 1 ls "$WORK_DIR"/Whonix*-Gateway*.qcow2 &>/dev/null || return 1 ls "$WORK_DIR"/Whonix*-Workstation*.qcow2 &>/dev/null || return 1 } whonix::detect_image_owner() { # Description: Determine the appropriate owner:group for libvirt images. # Globals: # - IMAGE_DIR, IMAGE_OWNER # Arguments: None # Outputs: Sets IMAGE_OWNER. # Returns: 0 # Usage: # whonix::detect_image_owner # End of documentation whonix::init_context if [[ -z $IMAGE_OWNER && -d $IMAGE_DIR ]]; then IMAGE_OWNER=$(stat -c "%U:%G" "$IMAGE_DIR" 2>/dev/null || true) fi if [[ -z $IMAGE_OWNER ]]; then if getent group libvirt-qemu &>/dev/null; then IMAGE_OWNER="libvirt-qemu:libvirt-qemu" elif getent group qemu &>/dev/null; then IMAGE_OWNER="qemu:qemu" else IMAGE_OWNER="root:root" fi fi } whonix::stage_images() { # Description: Move or reuse Whonix qcow2/raw images in the libvirt images directory. # Globals: # - WORK_DIR, IMAGE_DIR, IMAGE_OWNER, REFRESH # Arguments: None # Outputs: Log messages. # Returns: 0 # Usage: # whonix::stage_images # End of documentation whonix::init_context whonix::detect_image_owner mkdir -p "$IMAGE_DIR" for img in "$WORK_DIR"/*.raw "$WORK_DIR"/*.qcow2; do [[ -e $img ]] || continue local target="$IMAGE_DIR/$(basename "$img")" if [[ -e $target ]]; then if (( REFRESH )); then write_status "$(basename "$img") (refresh) -> replacing existing copy" mv -f "$img" "$target" else write_status "$(basename "$img") already present -> re-using existing copy (no refresh)" continue fi else write_status "$(basename "$img") -> $IMAGE_DIR" mv -f "$img" "$target" fi chown "$IMAGE_OWNER" "$target" 2>/dev/null || true chmod 0644 "$target" done } whonix::ensure_networks_defined() { # Description: Define Whonix virtual networks idempotently. # Globals: # - WORK_DIR # Arguments: None # Outputs: Log messages. # Returns: 0 # Usage: # whonix::ensure_networks_defined # End of documentation whonix::init_context ( cd "$WORK_DIR" || exit 1 for xml in *{e,E}xternal*.xml *{i,I}nternal*.xml; do [[ -e $xml ]] || continue local net net=$(perl -ne 'print $1 if /([^<]+)<\/name>/' "$xml" | head -1) net=${net:-${xml%.xml}} [[ -z $net ]] && { write_warning "cannot determine network name from $xml – skipping"; continue; } if virsh -c qemu:///system net-list --all --name | grep -qxF "$net"; then write_status "network '$net' already defined" else write_status "defining network '$net'" virsh -c qemu:///system net-define "$xml" || write_warning "could not define '$net' (likely UUID clash)" fi if virsh -c qemu:///system net-list --name | grep -qxF "$net"; then write_status "network '$net' already active" else write_status "starting & autostarting '$net'" virsh -c qemu:///system net-start "$net" virsh -c qemu:///system net-autostart "$net" fi done ) } whonix::ensure_networks_running() { # Description: Start Whonix virtual networks if they are not active. # Globals: None # Arguments: None # Outputs: Log messages. # Returns: 0 # Usage: # whonix::ensure_networks_running # End of documentation whonix::init_context local nets nets=$(virsh -c qemu:///system net-list --all --name | grep -i whonix || true) while read -r net; do [[ -n $net ]] || continue if ! virsh -c qemu:///system net-list --name | grep -qxF "$net"; then write_status "starting $net" virsh -c qemu:///system net-start "$net" virsh -c qemu:///system net-autostart "$net" else write_status "$net already active" fi done <<< "$nets" } whonix::add_extra_workstation() { # Description: Create an additional workstation VM overlaying an existing base disk. # Globals: # - WHONIX_VERSION, IMAGE_OWNER, IMAGE_DIR, WORK_DIR # Arguments: # - $1: Base disk path to overlay. # Outputs: Log messages. # Returns: 0 on success, 1 on failure # Usage: # whonix::add_extra_workstation "/var/lib/libvirt/images/Whonix-Workstation.qcow2" # End of documentation whonix::init_context local base_disk=$1 [[ -f $base_disk ]] || { write_warning "cannot add workstation – base disk missing ($base_disk)"; return 1; } local ts new_vm new_disk template ts=$(date +%s) new_vm="Whonix-Workstation-extra-${WHONIX_VERSION}-${ts}" new_disk="${IMAGE_DIR}/Whonix-Workstation-extra-${WHONIX_VERSION}-${ts}.qcow2" write_status "adding extra workstation '${new_vm}'" qemu-img create -f qcow2 -b "$base_disk" -F qcow2 "$new_disk" >/dev/null chown "$IMAGE_OWNER" "$new_disk" 2>/dev/null || true chmod 0644 "$new_disk" template="${WORK_DIR}/Whonix-Workstation-extra-${ts}.xml" perl -0pe "s@[^<]+@${new_vm}@; s@ "$template" virsh -c qemu:///system define "$template" } whonix::define_vms() { # Description: Define or refresh Whonix Gateway/Workstation VMs against target images. # Globals: # - WORK_DIR, IMAGE_DIR, WHONIX_VERSION, REFRESH, ADD_WORKSTATION # Arguments: None # Outputs: Log messages. # Returns: 0 on success, 1 on failure # Usage: # whonix::define_vms # End of documentation whonix::init_context local vm xml target_path fixed candidates vm_exists existing_disk up_to_date for vm in Whonix-Gateway Whonix-Workstation; do # Prefer the upstream bundle XML (avoid reusing prior -fixed files) xml="" if [[ -f "${WORK_DIR}/${vm}.xml" ]]; then xml="${WORK_DIR}/${vm}.xml" else # pick the first matching XML that is not a prior fixed file xml=$(compgen -G "${WORK_DIR}/${vm}"*.xml 2>/dev/null | grep -v -- '-fixed.xml$' | head -1 || true) fi [[ -n $xml ]] || { write_warning "no XML for $vm – skipping"; continue; } vm_exists=0 existing_disk="" up_to_date=0 if virsh -c qemu:///system list --all --name | grep -qxF "$vm"; then vm_exists=1 existing_disk=$(virsh -c qemu:///system dumpxml "$vm" 2>/dev/null | sed -nE "s@.*/dev/null || true) if [[ -z $candidates ]]; then candidates=$(whonix::_need_root sh -c "ls -1 $IMAGE_DIR/${vm}*.qcow2 2>/dev/null" || true) fi if [[ -n $candidates ]]; then target_path=$(head -1 <<<"$candidates") elif compgen -G "$WORK_DIR/${vm}"*.qcow2 >/dev/null; then target_path=$(compgen -G "$WORK_DIR/${vm}"*.qcow2 | head -1) fi [[ -n $target_path ]] || { write_warning "disk image for $vm missing in $IMAGE_DIR (or $WORK_DIR)"; return 1; } test -f "$target_path" || { write_warning "disk image $target_path missing; extraction may have failed"; return 1; } if (( vm_exists )) && (( up_to_date )) && (( REFRESH == 0 )); then write_status "VM '$vm' already on version $WHONIX_VERSION – keeping existing definition" if [[ $vm == "Whonix-Workstation" ]] && (( ADD_WORKSTATION )); then whonix::add_extra_workstation "$target_path" fi continue fi fixed="${WORK_DIR}/${vm}-fixed.xml" perl -0pe "s@ "$fixed" # Ensure remote graphical console works over SSH by binding SPICE to localhost instead of using FD passing. perl -0pi -e "s@]*)listen=['\"]none['\"]([^>]*)>@@gi; s@@@gi" "$fixed" if (( vm_exists )); then write_status "updating existing VM '$vm'" else write_status "defining VM '$vm'" fi virsh -c qemu:///system define "$fixed" done } whonix::maybe_skip_install() { # Description: Skip all work when existing VMs are already at/above the target version unless refresh/add is requested. # Globals: # - REFRESH, ADD_WORKSTATION, WHONIX_VERSION, IMAGE_DIR # Outputs: Log message when skipping. # Returns: Exits 0 when skipping; 0 otherwise. # Usage: # whonix::maybe_skip_install || true # End of documentation whonix::init_context local gw ws gw=$(whonix::current_vm_version Whonix-Gateway || true) ws=$(whonix::current_vm_version Whonix-Workstation || true) if (( REFRESH == 0 )) && (( ADD_WORKSTATION == 0 )) \ && whonix::version_ge "$gw" "$WHONIX_VERSION" \ && whonix::version_ge "$ws" "$WHONIX_VERSION"; then write_status "Existing Whonix VMs are up-to-date (Gateway ${gw:-unknown}, Workstation ${ws:-unknown}) – nothing to do" exit 0 fi } # --- # - -- # --( / \ )XXXXXXXXXXXXX # --XXX( O O )XXXXXXXXXXXXXXX- # /XXX( U ) XXXXXXX\ # /XXXXX( )-- XXXXXXXXXXX\ # /XXXXX/ ( O ) XXXXXX \XXXXX\ # XXXXX/ / XXXXXX \ \XXXXX---- # XXXXXX / XXXXXX \ ---- - # --- XXX / XXXXXX \ --- # -- -- / /\ XXXXXX / ---= # - / XXXXXX '--- XXXXXX # --\/XXX\ XXXXXX /XXXXX # \XXXXXXXXX /XXXXX/ # \XXXXXX /XXXXX/ # \XXXXX-- / -- XXXX/ # --XXXXXXX--------------- XXXXX-- # \XXXXXXXXXXXXXXXXXXXXXXXX- # --XXXXXXXXXXXXXXXXXX- # ########################################################################################### # # 8888888 888 888 # 888 888 888 # 888 888 888 # 888 88888b. 888888 .d88b. 888d888 88888b. 8888b. 888 # 888 888 "88b 888 d8P Y8b 888P" 888 "88b "88b 888 # 888 888 888 888 88888888 888 888 888 .d888888 888 # 888 888 888 Y88b. Y8b. 888 888 888 888 888 888 # 8888888 888 888 "Y888 "Y8888 888 888 888 "Y888888 888 # # # # .d8888b. .d888 d8b 888 d8b # d88P Y88b d88P" Y8P 888 Y8P # 888 888 888 888 # 888 .d88b. 88888b. 888888 888 .d88b. 888 888 888d888 8888b. 888888 888 .d88b. 88888b. # 888 d88""88b 888 "88b 888 888 d88P"88b 888 888 888P" "88b 888 888 d88""88b 888 "88b # 888 888 888 888 888 888 888 888 888 888 888 888 888 .d888888 888 888 888 888 888 888 # Y88b d88P Y88..88P 888 888 888 888 Y88b 888 Y88b 888 888 888 888 Y88b. 888 Y88..88P 888 888 # "Y8888P" "Y88P" 888 888 888 888 "Y88888 "Y88888 888 "Y888888 "Y888 888 "Y88P" 888 888 # 888 # Y8b d88P # "Y88P" # 8888888b. d8b 888 # 888 Y88b Y8P 888 # 888 888 888 # 888 d88P .d88b. 88888b. .d88b. .d8888b 888 888888 .d88b. 888d888 888 888 # 8888888P" d8P Y8b 888 "88b d88""88b 88K 888 888 d88""88b 888P" 888 888 # 888 T88b 88888888 888 888 888 888 "Y8888b. 888 888 888 888 888 888 888 # 888 T88b Y8b. 888 d88P Y88..88P X88 888 Y88b. Y88..88P 888 Y88b 888 # 888 T88b "Y8888 88888P" "Y88P" 88888P' 888 "Y888 "Y88P" 888 "Y88888 # 888 888 # 888 Y8b d88P # 888 "Y88P" # ########################################################################################### # # Below please find the internally stored and maintained configuration cards. # When creating Linux installation configurations for use in a virtual envoironment # or for transferring to physical media to install on a laptop or PC; require # the creation of preseed and kick start configuration files that "answer" the # installer's questions. These pieces of configuration files are stored below # such that they may be applied to the actual configuration file as needed and # wiht variable instructions, for example: what software to install. Any configuration # item that needs to be written out to a separate file should be stored in this section. template::preseed_cfg::early_command() { # Description: # Generates the early command section of a preseed configuration file for Debian-based installations. # This function prevents the installer from mistakenly installing the operating system on the USB stick # used for booting by detecting and setting the correct target disk for installation. # # Globals: # None # # Arguments: # $1 - The path to the output file where the early command configuration will be written. # # Outputs: # - Appends the early command section to the specified preseed configuration file. # # Returns: # 0 - If the early command section was successfully generated. # 1 - If no output file is provided or if an error occurs during the generation process. # # Usage: # template::preseed_cfg::early_command /path/to/output_file # # Example: # template::preseed_cfg::early_command /path/to/preseed.cfg # # End of Documentation if [[ -z ${1} ]]; then { write_error "No output file provided, use: ${FUNCNAME[0]} " return 1 }; fi # Write out the preseed file header with 'EOF' to prevent variable expansion: cat >>${1} <<-'EOF' # --------------------------------------------------- # # C.4.1. Do NOT install on the USB stick(!) # --------------------------------------------------- # # # The Debian installer will install on the first disk it finds which can # sometimes be the USB stick itself. Work around this by rolling our own auto # detect logic which disallows installing on USB devices. # # d-i partman/early_command string \ # USBDEV=$(mount | grep hd-media | cut -d" " -f1 | sed "s/\(.*\)./\1/");\ # BOOTDEV=$(list-devices disk | grep -v \$USBDEV | head -1);\ # debconf-set partman-auto/disk $BOOTDEV;\ # debconf-set grub-installer/bootdev $BOOTDEV; d-i partman/early_command string \ USBDEV=$(mount | grep hd-media | cut -d" " -f1 | sed "s/\(.*\)./\1/"); \ BOOTDEV=$(list-devices disk | grep -v \$USBDEV | while read -r DEV; do \ if [ $(cat /sys/block/${DEV##*/}/queue/rotational) -eq 0 ]; then \ echo $DEV; break; \ fi; \ done); \ if [ -z "$BOOTDEV" ]; then \ BOOTDEV=$(list-devices disk | grep -v \$USBDEV | head -1); \ fi; \ debconf-set partman-auto/disk $BOOTDEV; \ debconf-set grub-installer/bootdev $BOOTDEV; # --------------------------------------------------- # EOF } template::preseed_cfg::main() { # Description: # Generates the main configuration section of a preseed file for automated Debian-based installations. # This function configures unattended installation, network settings, user account setup, localization, # keyboard layout, timezone, bootloader installation, EFI support, and package selection. # # Globals: # Preference_Wireless_ID - The SSID of the wireless network for installations with wireless cards. # Preference_Wireless_Password - The WPA password for the wireless network. # Preference_InitialUser - The initial username for the system. # Preference_InitialUserPassword - The initial password for the user account, typically encrypted. # Preference_InitialLanguage - The language preference for localization. # Preference_InitialKeyboardLayout - The keyboard layout preference (e.g., "us"). # Preference_InitialTimeZone - The timezone to be set during installation. # _task - The tasks or packages to be installed during the installation process. # # Arguments: # $1 - The path to the output file where the main preseed configuration will be written. # # Outputs: # - Appends the main configuration settings to the specified preseed file. # # Returns: # 0 - If the main configuration was successfully generated. # 1 - If no output file is provided or if an error occurs during the generation process. # # Usage: # template::preseed_cfg::main /path/to/output_file # # Example: # template::preseed_cfg::main /path/to/preseed.cfg # # End of Documentation if [[ -z ${1} ]]; then { write_error "No output file provided, use: ${FUNCNAME[0]} " return 1 }; fi cat >>${1} <<-EOF # --------------------------------------------------- # # C.4.2. Unattended Installation $(date) # --------------------------------------------------- # d-i auto-install/enable boolean true d-i debconf/priority select critical # --------------------------------------------------- # # --------------------------------------------------- # # C.4.3. Network configuration # --------------------------------------------------- # # Network setup. This can be a static setup or DHCP. # The RTD default preferred config is to rely on DHCP, # and for wireless networks (systems with only wireless cards) # default to an SSID named "loader" and the guest wpa string # "letmein1234". This allows supported systems to be # installed directly over WiFi with no user input. # Configure network automatically if a wired connection is available d-i netcfg/enable boolean true d-i netcfg/choose_interface select auto d-i netcfg/dhcp_options select Configure network automatically # Set DHCP timeout to handle slow networks d-i netcfg/dhcp_timeout string 30 d-i netcfg/link_wait_timeout string 30 # Prompt for wireless setup if no wired connection is found d-i netcfg/wireless_show_essids select manual # d-i netcfg/wireless_essid string ${Preference_Wireless_ID} # d-i netcfg/wireless_essid_again string ${Preference_Wireless_ID} d-i netcfg/wireless_essid string loader d-i netcfg/wireless_essid_again string loader d-i netcfg/wireless_security_type select wpa2 d-i netcfg/wireless_wpa string ${Preference_Wireless_Password} # Fallback to manual configuration if DHCP fails d-i netcfg/dhcp_failed note d-i netcfg/dhcp_options select Configure network manually # Ensure firmware is loaded for wireless cards d-i hw-detect/load_firmware boolean true # Set hostname and domain d-i netcfg/get_hostname string RTD-Client d-i netcfg/get_domain string unassigned-domain # --------------------------------------------------- # # --------------------------------------------------- # # C.4.4. Account setup (temporary user account) # --------------------------------------------------- # # Setup an initial user and disable root login by default. # root login may be re-enabled later by setting a root password. # An encrypted password is set here, and should be changed # ASAP after the system is built. Preferebly connect to LDAP/AD # in a managed environment. d-i passwd/root-login boolean false d-i passwd/user-fullname string RTD User d-i passwd/username string ${Preference_InitialUser} d-i passwd/user-password seen true d-i user-setup/allow-password-weak boolean true d-i passwd/user-password-crypted password ${Preference_InitialUserPassword} d-i passwd/auto-login boolean true # --------------------------------------------------- # # --------------------------------------------------- # # C.4.5. Localization # --------------------------------------------------- # # Provide localizaton preferences so that the prefferred # language is used for display and formats. Comparable to # the MUI in Microsoft environments. d-i debian-installer/locale string en_US.UTF-8 d-i localechooser/supported-locales multiselect en_US.UTF-8, ${Preference_InitialLanguage}.UTF-8 d-i console-setup/ask_detect boolean false # --------------------------------------------------- # # --------------------------------------------------- # # C.4.6. Set Keyboard layout # --------------------------------------------------- # # Set the prefferred keyboard layout. Keyboards will work # regardless, but the letters and symbols may not actually # be the ones drawn on the keys. Default is us. d-i keyboard-configuration/xkb-keymap select ${Preference_InitialKeyboardLayout} d-i keyboard-configuration/layoutcode string ${Preference_InitialKeyboardLayout} d-i debian-installer/keymap select ${Preference_InitialKeyboardLayout} d-i keymap select ${Preference_InitialKeyboardLayout} d-i console-setup/layoutcode string ${Preference_InitialKeyboardLayout} d-i console-setup/ask_detect boolean false # --------------------------------------------------- # # --------------------------------------------------- # # C.4.7. Clock and time zone setup # --------------------------------------------------- # d-i clock-setup/utc boolean true d-i time/zone string ${Preference_InitialTimeZone} d-i clock-setup/ntp boolean true d-i clock-setup/ntp-server string ntp.ubuntu.com # --------------------------------------------------- # # --------------------------------------------------- # # C.4.8. GRUB bootloader installation # --------------------------------------------------- # # # Tell the grub-installer to install to the MBR even if it # also finds some other OS, which is more likely to allow # the newly installed Linux OS to boot without issue. d-i grub-installer/only_debian boolean true d-i grub-installer/with_other_os boolean true # --------------------------------------------------- # # --------------------------------------------------- # # C.4.9. EFI # --------------------------------------------------- # # # The EFI (Extensible Firmware Interface) system partition # is a partition on a data storage device. UEFI provides # backward compatibility with legacy systems by reserving # the first block (sector) of the partition for compatibility # code, effectively creating a legacy boot sector. On # legacy BIOS-based systems, the first sector of a partition # is loaded into memory and execution is transferred to this # code. Here we tell setup to install EFI boot setup if # possible so that both NEW and old systems may be handled # and the installed system can be started. d-i partman-efi/non_efi_system boolean true # --------------------------------------------------- # # --------------------------------------------------- # # C.4.10. Package selection # --------------------------------------------------- # # Packages may be selected as groups (meta packages) like # kde-desktop or as individual packages. Only one "pkgsel/include" # string will be used though! remembder to fit all the # packages you want on one line. # # tasksel tasksel/first multiselect standard options: # ubuntu-desktop-minimal kubuntu-desktop, ubuntu-gnome-desktop, # lubuntu-desktop, ubuntu-mate-desktop, gnome-desktop, # kde-desktop, cinnamon-desktop, mate-desktop, lxde-desktop, # web-server, ssh-server, print-server. # The "OEM" line below ist to make it simple to replace the # line with a relevant chice of debian role using "sed" or similar. tasksel tasksel/first multiselect ${_task} # d-i pkgsel/include string ${_task} ${pkgsel_include_string} # Valid choices for pkgsel/upgrade are: safe-upgrade full-upgrade none d-i pkgsel/upgrade select none d-i pkgsel/update-policy select unattended-upgrades # Random other questions that may need to be answered during install console-setup console-setup/charmap47 select UTF-8 samba-common samba-common/dhcp boolean false macchanger macchanger/automatically_run boolean false kismet-capture-common kismet-capture-common/install-users string kismet-capture-common kismet-capture-common/install-setuid boolean true wireshark-common wireshark-common/install-setuid boolean true sslh sslh/inetd_or_standalone select standalone atftpd atftpd/use_inetd boolean false # By default, the system’s locate database will be updated after the # installer has finished installing most packages. This may take a while, so # if you don’t want it, you can set this to "false" to turn it off. # d-i pkgsel/updatedb boolean false # --------------------------------------------------- # EOF } template::preseed_cfg::auto_disk_layout() { # Description: # Generates the disk layout section of a preseed configuration file for Debian-based installations. # The function automatically configures the disk layout based on user preferences, with options for encrypted or non-encrypted disks. # It supports different predefined partitioning recipes, such as "atomic", "home", and "multi". # # Globals: # Preference_DiskEncryption - User preference for disk encryption (e.g., "YES" or "no"). # Preference_Disk_Password - The password used for disk encryption, if enabled. # # Arguments: # $1 - The path to the output file where the disk layout configuration will be written. # # Outputs: # - Appends the disk layout configuration to the specified preseed configuration file. # # Returns: # 0 - If the disk layout configuration was successfully generated. # 1 - If no output file is provided or if an error occurs during the generation process. # # Usage: # template::preseed_cfg::auto_disk_layout /path/to/output_file # # Example: # template::preseed_cfg::auto_disk_layout /path/to/preseed.cfg # # Note: # The disk layout is controlled by the "Preference_DiskEncryption" variable. If disk encryption is enabled, # the disk will be encrypted and require a password to unlock on boot. # # End of Documentation if [[ -z ${1} ]]; then { write_error "No output file provided, use: ${FUNCNAME[0]} " return 1 }; fi cat >>${1} <<-EOF # --------------------------------------------------- # # C.4.11. Disk layout (default plain non crypt disk) # --------------------------------------------------- # # # Set option to encrypt the hard disk: # By default the harddisk will not be encrypted # To protect data the disk encrypted and need to be # unlocked with a password when the system is booted. # This configuration is controlled by the "Preference_DiskEncryption" # variable set in a configuration file for user preferences # when building VM's. See _rtd_library documentation for more. # For Disk layout: # You can choose one of the three predefined partitioning recipes: # - atomic: all files in one partition # - home: separate /home partition # - multi: separate /home, /var, and /tmp partitions $( if [ "${Preference_DiskEncryption}" = "YES" ] || [ "${Preference_DiskEncryption}" = "yes" ]; then echo " d-i partman-auto/method string crypto d-i partman-crypto/passphrase password ${Preference_Disk_Password} d-i partman-crypto/passphrase-again password ${Preference_Disk_Password} d-i partman-auto-crypto/erase_disks boolean false d-i partman-lvm/confirm boolean true d-i partman-auto-lvm/guided_size string max d-i partman-auto-lvm/new_vg_name string rtd-crypt d-i partman-lvm/device_remove_lvm boolean true d-i partman-lvm/device_remove_lvm_span boolean true d-i partman-auto/purge_lvm_from_device boolean true d-i partman-md/device_remove_md boolean true d-i partman-md/confirm boolean true d-i partman-basicfilesystems/no_mount_point boolean false d-i partman-partitioning/confirm_write_new_label boolean true d-i partman/choose_partition select finish d-i partman/confirm boolean true d-i partman-auto/choose_recipe select atomic d-i partman/confirm_nooverwrite boolean true " else echo " # Setup a simple disk layout with all files in one partition: d-i partman-auto/method string regular d-i partman-auto/choose_recipe select atomic d-i partman-partitioning/confirm_write_new_label boolean true d-i partman/choose_partition select finish d-i partman/confirm boolean true d-i partman/confirm_nooverwrite boolean true # For Ubuntu: ubiquity partman-auto/method string regular ubiquity partman-lvm/device_remove_lvm boolean true ubiquity partman-md/device_remove_md boolean true ubiquity partman-auto/choose_recipe select atomic " fi ) EOF } template::preseed_cfg::expert_recipe() { # Description: # Generates an expert partitioning recipe for a Debian-based system installation, to be included in a preseed configuration file. # This expert recipe defines the partition layout, including EFI, boot, swap, and root partitions, with support for LVM and encryption. # # Globals: # None # # Arguments: # $1 - The path to the output file where the expert partitioning recipe will be written. # # Outputs: # - Appends the expert partitioning recipe to the specified preseed configuration file. # # Returns: # 0 - If the expert partitioning recipe was successfully generated. # 1 - If no output file is provided or if an error occurs during the generation process. # # Usage: # template::preseed_cfg::expert_recipe /path/to/output_file # # Example: # template::preseed_cfg::expert_recipe /path/to/preseed.cfg # # End of Documentation if [[ -z ${1} ]]; then { write_error "No output file provided, use: ${FUNCNAME[0]} " return 1 }; fi cat >>${1} <<-'EOF' d-i partman-auto/expert_recipe string \ multi-cnx :: \ 538 538 1075 free \ $primary \ $iflabel{ gpt } \ $reusemethod{ } \ method{ efi } format{ } \ . \ 3500 3500 3500 ext3 \ $primary{ } $bootable{ } \ method{ format } format{ } \ use_filesystem{ } filesystem{ ext4 } \ mountpoint{ /boot } \ . \ 200% 25000 200% linux-swap \ $lvmok{ } lv_name{ swap } \ in_vg { crypt } \ $primary{ } \ method{ swap } format{ } \ . \ 500 1000 1000000000 ext4 \ $lvmok{ } lv_name{ root } \ in_vg { crypt } \ $primary{ } \ method{ format } format{ } \ use_filesystem{ } filesystem{ ext4 } \ mountpoint{ / } \ . # --------------------------------------------------- # EOF } template::preseed_cfg::late_command() { # Description: # Generates the late command section of a preseed configuration file for Debian-based installations. # The function tailors the preseed file to include additional tasks and configurations specific to the role of the system, # such as setting up a Minecraft server, KVM server, or applying a default configuration. # # Globals: # _role - The role of the system being configured (e.g., "minecraft-server", "kvm_server"). # _d_i_post_statment_ubunto_or_deb - Post-install command specific to Ubuntu or Debian systems. # Preference_InitialUser - The initial username for the system. # Preference_ssh_pub_key - The SSH public key to be added to the authorized keys for the root and user accounts. # Preference_InitialLanguage - The initial language setting for the system. # _task - Additional tasks to be installed on the system. # _saveto - Directory where scripts and tasks are saved. # # Arguments: # $1 - The path to the output file where the preseed configuration will be written. # # Outputs: # - Writes the late command section of the preseed configuration to the specified output file. # # Returns: # 0 - If the late command section was successfully generated. # 1 - If no output file is provided or if an error occurs during the generation process. # # Usage: # template::preseed_cfg::late_command /path/to/output_file # # Example: # template::preseed_cfg::late_command /path/to/preseed.cfg # # End of Documentation if [[ -z ${1} ]]; then { write_error "No output file provided, use: ${FUNCNAME[0]} " return 1 }; fi # write out conditional elements of the preseed file: case $_role in minecraft-server | minecraft | Minecraft | MINECRAFT | minecraft_server | Minecraft-server) # Generate the minecraft server launcher script: template::minecraft_server_launcher --write "${_saveto}"/task.sh # Write out the preseed file: cat >>${1} <<-EOF # --------------------------------------------------- # # C.4.12. Addon Tasks for Minecraft Server # --------------------------------------------------- # ${_d_i_post_statment_ubunto_or_deb} \ in-target apt-get -y install ansible ; \ in-target /bin/bash /${_OEM_DIR:-"/opt/rtd"}/core/rtd-oem-enable-config.sh ; \ in-target mkdir -p /root/.ssh ; \ in-target mkdir -p /home/${Preference_InitialUser}/.ssh ; \ in-target /bin/sh -c "echo ${Preference_ssh_pub_key} >> /root/.ssh/authorized_keys" ; \ in-target /bin/sh -c "echo ${Preference_ssh_pub_key} >> /home/${Preference_InitialUser}/.ssh/authorized_keys" ; \ cp /*.cfg /target/${_OEM_DIR:-"/opt/rtd"}/ ; cp /task.sh /home/${Preference_InitialUser}/minecraft.server ;\ in-target echo "bash /home/${Preference_InitialUser}/minecraft.server" >> /home/${Preference_InitialUser}/.bashrc ; \ umount -l /media || echo "Empty"; \ eject || true; \ reboot; \ echo "------ initial Setup Complete! ------" # Answer the last question d-i debian-installer/splash boolean true # Option to try to greacefully eject the installation media: d-i cdrom-detect/eject boolean true # Shutdown machine d-i finish-install/reboot_in_progress note d-i debian-installer/exit/halt boolean false d-i debian-installer/exit/reboot boolean true # --------------------------------------------------- # EOF ;; kvm_server | Kvm | KVM | kvm | kvm-server) cat >>${1} <<-EOF # --------------------------------------------------- # # C.4.12. Addon Tasks for KVM Server # --------------------------------------------------- # ${_d_i_post_statment_ubunto_or_deb} \ in-target apt-get -y install qemu-system libvirt-daemon-system ; \ in-target apt-get -y install git ; \ in-target apt-get -y install ansible ; \ in-target git clone --depth=1 ${GIT_RTD_SRC_URL} ${_OEM_DIR:-"/opt/rtd"} ; \ in-target /usr/bin/chmod 755 ${_OEM_DIR:-"/opt/rtd"}/${_OEM_DIR:-"/opt/rtd"}/* ; \ in-target /bin/bash ${_OEM_DIR:-"/opt/rtd"}/core/rtd-oem-enable-config.sh ; \ in-target mkdir -p /root/.ssh ; \ in-target mkdir -p /home/${Preference_InitialUser}/.ssh ; \ in-target /bin/sh -c "echo ${Preference_ssh_pub_key} >> /root/.ssh/authorized_keys" ; \ in-target /bin/sh -c "echo ${Preference_ssh_pub_key} >> /home/${Preference_InitialUser}/.ssh/authorized_keys" ; \ cp /*.cfg /target/${_OEM_DIR:-"/opt/rtd"}/ ; \ # umount -l /media || echo "Empty" ; \ # eject || true; \ echo "------ initial Setup Complete! ------" # Answer the last question d-i debian-installer/splash boolean true # Option to try to gracefully eject the installation media: d-i cdrom-detect/eject boolean true # Shutdown machine d-i finish-install/reboot_in_progress note d-i debian-installer/exit/halt boolean false d-i debian-installer/exit/reboot boolean true # --------------------------------------------------- # EOF ;; *) cat >>${1} <<-EOF # --------------------------------------------------- # # C.4.12. Default preseed Addon Tasks # --------------------------------------------------- # ${_d_i_post_statment_ubunto_or_deb} \ in-target apt-get -y install git ; \ in-target apt-get -y install ansible ; \ in-target apt-get -y install dialog p7zip-full virt-what curl wget ; \ in-target apt-get -y install spice-vdagent qemu-guest-agent ; \ in-target apt-get -y install neofetch ; \ in-target /bin/sh -c echo "LC_ALL=${Preference_InitialLanguage}.UTF-8" >> /etc/default/locale ; \ in-target locale-gen en_US.UTF-8; in-target update-locale LANG=en_US.UTF-8; \ in-target /usr/bin/git clone --depth=1 ${GIT_RTD_SRC_URL} ${_OEM_DIR:-"/opt/rtd"} ; \ in-target /usr/bin/chmod 755 ${_OEM_DIR:-"/opt/rtd"}/core/rtd* ; \ in-target /bin/bash ${_OEM_DIR:-"/opt/rtd"}/core/rtd-oem-enable-config.sh ; \ in-target mkdir -p /root/.ssh ; \ in-target mkdir -p /home/${Preference_InitialUser}/.ssh ; \ in-target /bin/sh -c "echo ${Preference_ssh_pub_key} >> /root/.ssh/authorized_keys" ; \ in-target /bin/sh -c "echo ${Preference_ssh_pub_key} >> /home/${Preference_InitialUser}/.ssh/authorized_keys" ; \ in-target /bin/sh -c "chown -R ${Preference_InitialUser} /home/${Preference_InitialUser}/.ssh" ; \ cp /*.cfg /target/${_OEM_DIR:-"/opt/rtd"}/ ; \ # /bin/bash reboot ; \ # umount -l /media || echo "Empty" ; \ # eject || true; \ echo "------ initial Setup Complete! ------" # Answer the last question d-i debian-installer/splash boolean true # Option to try to gracefully eject the installation media: # d-i cdrom-detect/eject boolean true # Shutdown machine d-i finish-install/reboot_in_progress note d-i debian-installer/exit/halt boolean false d-i debian-installer/exit/reboot boolean true # --------------------------------------------------- # EOF ;; esac } template::kickstart_cfg() { # Description: # Generates a Kickstart configuration file for automated Red Hat-based Linux installations. # The function supports different configurations based on the role of the system, such as server or workstation. # The generated file includes settings for partitioning, package selection, and post-installation scripts. # # Globals: # _role - The role of the system being configured, e.g., "server" or "workstation". # _repo_url - The URL of the installation repository. # _ks_file - The path to the output Kickstart file. # _UserServerEnvironemtSelection - The server environment to be installed (e.g., specific server packages). # _UserDesktopEnvironmentSelection - The desktop environment to be installed (e.g., GNOME, KDE). # Preference_InitialKeyboardLayout - The keyboard layout to be used during installation. # Preference_InitialTimeZone - The time zone to be set during installation. # Preference_InitialUserPassword - The initial password for the root and user accounts. # Preference_DiskEncryption_RH - Disk encryption settings for Red Hat-based installations. # # Arguments: # $1 - The path to the output file where the Kickstart configuration will be written. # # Outputs: # - Writes the Kickstart configuration file to the specified output path. # # Returns: # 0 - If the Kickstart configuration file was successfully generated. # 1 - If no output file is provided or if an invalid role is specified. # # Usage: # template::kickstart_cfg /path/to/output_file # # Example: # template::kickstart_cfg /path/to/kickstart.cfg # # End of Documentation # make sure the output file is provided if [[ -z ${1} ]]; then { write_error "No output file provided, use: ${FUNCNAME[0]} " return 1 }; fi local _kickstart_epel_repo="" local _kickstart_baseos_repo="" read -r -d '' _kickstart_baseos_repo <<-'KS_BASEOS' repo --name=baseos --baseurl=https://repo.almalinux.org/almalinux/$releasever/BaseOS/$basearch/os/ KS_BASEOS local _kickstart_appstream_repo="" read -r -d '' _kickstart_appstream_repo <<-'KS_APPSTREAM' repo --name=appstream --baseurl=https://repo.almalinux.org/almalinux/$releasever/AppStream/$basearch/os/ KS_APPSTREAM local _kickstart_epel_repo="" if [[ "${_kickstart_enable_epel:-}" == "true" ]]; then read -r -d '' _kickstart_epel_repo <<-'KS_EPEL' repo --name=epel --baseurl=https://dl.fedoraproject.org/pub/epel/$releasever/Everything/$basearch/ KS_EPEL fi # Generate the kickstart file case ${_role} in server) write_information "Selected configuration is ${_role}:${_UserServerEnvironemtSelection}" cat >>"${_ks_file}" <<-KS_EOF # Generated by: system::generate_ks_cfg_file by RTD Power Tools # Choosing mode (graphical|text|cmdline [--non-interactive]) text ${_kickstart_install_source:-} firstboot --enable keyboard --vckeymap=${Preference_InitialKeyboardLayout:-"us"} --xlayouts="${Preference_InitialKeyboardLayout:-"us"}" lang en_US.UTF-8 timezone ${Preference_InitialTimeZone:-"Etc/UTC"} --utc network --onboot=yes --bootproto=dhcp rootpw ${Preference_InitialUserPassword} --iscrypted user --groups=wheel --name=tangarora --password=${Preference_InitialUserPassword} --iscrypted zerombr clearpart --all autopart --nohome ${Preference_DiskEncryption_RH} ${_kickstart_appstream_repo} ${_kickstart_epel_repo} ${_kickstart_baseos_repo} reboot --eject %packages --retries 5 --timeout 20 @server-product-environment @guest-agents openssh-server spice-vdagent git curl dialog ${_UserServerEnvironemtSelection} %end # Post-installation Script %post --interpreter=/bin/bash # Ensure EPEL repository and Ansible are present for automation tooling. if command -v dnf >/dev/null 2>&1; then dnf -y install epel-release dnf -y install ansible fi git clone --depth 1 ${GIT_RTD_SRC_URL} ${_OEM_DIR:-"/opt/rtd"} chmod 755 ${_OEM_DIR:-"/opt/rtd"}/core/rtd-oem-enable-config.sh bash ${_OEM_DIR:-"/opt/rtd"}/core/rtd-oem-enable-config.sh systemctl enable sshd %end KS_EOF return ;; workstation) write_information "Selected configuration is ${_role}:${_UserDesktopEnvironmentSelection}" cat >>"${_ks_file}" <<-KS_EOF # Generated by RTD Power Tools ( ${FUNCNAME[0]} ) # Choosing mode (graphical|text|cmdline [--non-interactive]) graphical ${_kickstart_install_source:-} firstboot --disable keyboard --vckeymap=${Preference_InitialKeyboardLayout} --xlayouts="${Preference_InitialKeyboardLayout}" lang en_US.UTF-8 timezone ${Preference_InitialTimeZone:-"Etc/UTC"} --utc network --onboot=yes --bootproto=dhcp rootpw ${Preference_InitialUserPassword} --iscrypted user --groups=wheel --name=tangarora --password=${Preference_InitialUserPassword} --iscrypted zerombr clearpart --all autopart --nohome ${Preference_DiskEncryption_RH} ${_kickstart_appstream_repo} ${_kickstart_epel_repo} ${_kickstart_baseos_repo} reboot --eject %packages --retries 5 --timeout 20 ${_UserDesktopEnvironmentSelection:-"@^workstation-product-environment"} @graphical-admin-tools @fonts openssh-server spice-vdagent mesa-dri-drivers mesa-vulkan-drivers xorg-x11-server-Xwayland git curl dialog %end %post --interpreter=/bin/bash if command -v dnf >/dev/null 2>&1; then dnf -y install epel-release dnf -y install ansible fi git clone --depth 1 ${GIT_RTD_SRC_URL} ${_OEM_DIR:-"/opt/rtd"} chmod 755 ${_OEM_DIR:-"/opt/rtd"}/core/rtd-oem-enable-config.sh bash ${_OEM_DIR:-"/opt/rtd"}/core/rtd-oem-enable-config.sh # systemctl enable sshd # restorecon -Rv /usr/share/wayland-sessions /usr/share/xsessions systemctl enable gdm systemctl set-default graphical.target %end KS_EOF return ;; *) write_information ": No valid OS configuration requested: Valid requests are: workstation, ssh-server, ansible-server skipping..." return 1 ;; esac } template::rtd_me_sh_cmd() { # Description: # Generates a Windows batch script (rtd_me.sh.cmd) to perform initial setup and configuration # tasks on a Windows system. The script includes sections for initializing system variables, # downloading and installing necessary tools,and running additional configuration scripts # based on the detected version of Windows. # # Globals: # None # # Arguments: # $2 - The path to the output file where the rtd_me.sh.cmd script will be written. # # Outputs: # - Writes the rtd_me.sh.cmd script to the specified output file. # # Returns: # 0 - If the rtd_me.sh.cmd script was successfully generated. # 1 - If no output file is provided or if an error occurs during the generation process. # # Usage: # template::rtd_me_sh_cmd /path/to/output_file # # Example: # template::rtd_me_sh_cmd /path/to/rtd_me.sh.cmd # # End of Documentation # Template for the rtd_me.sh.cmd script local outfile=${2} write_information "Writing out the rtd_me.sh.cmd script to ${outfile}" cat >>${outfile} <<'EOF' :INIT ::::::::::::::::::::::::::::::::::::::::::::::::::: :: Script startup components; tasks that always :: need to be done when the initializes. :: @echo off echo Welcome to %COMSPEC% echo This is a windows script! setlocal & pushd %~dp0 :: %debug% :SETINGS :::::::::::::::::::::::::::::::::::::::::::::::::::::: :: *** Settings *** :: :::::::::::::::::::::::::::::::::::::::::::::::::::::: :: :: gather some info... setlocal EnableDelayedExpansion set "ScriptName=%~nx0" set "ScriptPath=%~dp0" set "_tla=%ScriptName:~0,3%" set "lowercase=abcdefghijklmnopqrstuvwxyz" set "uppercase=ABCDEFGHIJKLMNOPQRSTUVWXYZ" set "Result=" for /L %%i in (0,1,2) do ( set "char=!_tla:~%%i,1!" for /L %%j in (0,1,25) do ( if "!char!"=="!lowercase:~%%j,1!" set "char=!uppercase:~%%j,1!" ) set "Result=!Result!!char!" ) set _TLA=%Result% endlocal ::set _TLA=%Result% set _TLA=RTD set TEMP=C:\%_TLA%\temp set LOG_DIR=C:\%_TLA%\log set WALLPAPER_DIR=C:\%_TLA%\wallpaper set CACHE_DIR=C:\%_TLA%\cache set CORE_DIR=C:\%_TLA%\core set WALLPAPER_URL=https://raw.githubusercontent.com/vonschutter/RTD-Setup/main/wallpaper/Wayland.jpg set VIRTIO_URL=https://fedorapeople.org/groups/virt/virtio-win/direct-downloads/archive-virtio/virtio-win-0.1.240-1/virtio-win-guest-tools.exe set _STAGE2LOC=https://raw.githubusercontent.com/vonschutter/RTD-Setup/main/core/ set _STAGE2FILE=rtd-oem-win10-config.ps1 md %TEMP% md %LOG_DIR% md %WALLPAPER_DIR% md %CACHE_DIR% md %CORE_DIR% @title "Stage 2 file is located at: %_STAGE2LOC%\%_STAGE2FILE%" set >>%LOG_DIR%\%_TLA%.log ver >>%LOG_DIR%\%_TLA%.log :GetInterestingThigsToDoOnThisSystem :: Given that Microsoft Windows has been detected and the CMD shell portion of this script is executed, :: the second stage script must be downloaded from an online location. Depending on the version of windows :: there are different methods available to get and run remote files. All versions of windows do not necessarily :: support power-shell scripting. Therefore the base of this activity is coded in simple command CMD.EXE shell scripting :: :: Table of evaluating version of windows and calling the appropriate action given the version of windows found. :: In this case it is easier to manage a straight table than a for loop or array: :: DOS Based versions of Windows: :: ver | find "4.0" > nul && goto CMD1 rem Windows 95 :: ver | find "4.10" > nul && goto CMD1 rem Windows 98 :: ver | find "4.90" > nul && goto CMD1 rem Windows ME :: Windows 32 and 64 Bit versions: ver | find "NT 4.0" > nul && call :CMD1 Windows NT 4.0 ver | find "5.0" > nul && call :CMD1 Windows 2000 ver | find "5.1" > nul && call :CMD1 Windows XP ver | find "5.2" > nul && call :CMD1 Windows XP 64 Bit ver | find "6.0" > nul && call :DispErr Vista is not supported!!! ver | find "6.1" > nul && call :PS1 Windows 7 ver | find "6.2" > nul && call :PS2 Windows 8 ver | find "6.3" > nul && call :PS2 Windows 8 ver | find "6.3" > nul && call :PS2 Windows 8 ver | find "10.0" > nul && call :PS2 Windows 10 :: Windows Server OS Versions: ver | find "NT 6.2" > nul && call :PS2 Windows Server 2012 ver | find "NT 6.3" > nul && call :PS2 Windows Server 2012 R2 ver | find "NT 10.0" > nul && call :PS2 Windows Server 2016 and up... goto end :PS1 :: Procedure to get the second stage in Windows 7. Windows 7, by default has a different version of :: PowerShell installed. Therefore a slightly different syntax must be used. :: get stage 2 and run it... @title Found %* >>%LOG_DIR%\rtd.log echo Please wait... if exist A:\autounattend.xml copy /y A:\*.* %CORE_DIR%\ @title: "Download and install virtio-drivers" powershell -Command "(New-Object Net.WebClient).DownloadFile('%VIRTIO_URL%', '%CACHE_DIR%\virtio-win-gt-x64.msi')" msiexec /i %CACHE_DIR%\virtio-win-gt-x64.msii /passive /norestart /l*v %LOG_DIR%\virtio_log.txt @title "Fetch Wallpaper for default background" powershell -Command "(New-Object Net.WebClient).DownloadFile('%WALLPAPER_URL%', '%WALLPAPER_DIR%\Default.jpg')" @title "Set network profiles to Private" powershell -Command "& {Get-ChildItem -Path 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\NetworkList\Profiles' | ForEach-Object {Set-ItemProperty -Path $_.PSParentPath -Name 'Category' -Value 1}}" if exist %CORE_DIR%\%_STAGE2FILE% ( echo File found locally... powershell -ExecutionPolicy UnRestricted -File %CORE_DIR%\%_STAGE2FILE% ) else ( echo Fetching %_STAGE2FILE% from the internet... powershell -Command "(New-Object Net.WebClient).DownloadFile('%_STAGE2LOC%/%_STAGE2FILE%', '%CACHE_DIR%\%_STAGE2FILE%')" powershell -ExecutionPolicy UnRestricted -File %CACHE_DIR%\%_STAGE2FILE% ) powershell -Command "iwr -useb https://raw.githubusercontent.com/ChrisTitusTech/winutil/main/winutil.ps1 | iex" :: if exist %CORE_DIR%\_Chris-Titus-Post-Windows-Install-App.ps1 ( :: @title "CMD: _Chris-Titus-Post-Windows-Install-App.ps1 File found locally..." :: powershell -ExecutionPolicy UnRestricted -File %CORE_DIR%\_Chris-Titus-Post-Windows-Install-App.ps1 :: ) else ( :: @title "CMD: Fetching _Chris-Titus-Post-Windows-Install-App.ps1 from the internet..." :: powershell -Command "iwr -useb https://raw.githubusercontent.com/ChrisTitusTech/winutil/main/winutil.ps1 | iex" :: ) goto end :PS2 :: Procedure to get the second stage configuration script in all version of windows after 7. :: These version of windows have a more modern version of PowerShell. :: get stage 2 and run it... echo Found %* if exist A:\autounattend.xml copy /y A:\*.* %CORE_DIR%\ @title "POWERSHELL: seting NETWORK Config" powershell -Command "& {Get-ChildItem -Path 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\NetworkList\Profiles' | ForEach-Object {Set-ItemProperty -Path $_.PSParentPath -Name 'Category' -Value 1}}" @title "POWERSHELL: Fetch Wallpaper for default background" powershell -Command "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12;Invoke-WebRequest %WALLPAPER_URL% -OutFile %WALLPAPER_DIR%\Default.jpg" @title "POWERSHELL: Download and install virtio-drivers" powershell -Command "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12;Invoke-WebRequest %VIRTIO_URL% -OutFile %CACHE_DIR%\virtio-win-guest-tools.exe" %CACHE_DIR%\virtio-win-guest-tools.exe /passive /norestart /log %LOG_DIR%\virtio_log.txt if exist %CORE_DIR%\%_STAGE2FILE% ( @title "CMD: %_STAGE2FILE%File found locally..." powershell -ExecutionPolicy UnRestricted -File %CORE_DIR%\%_STAGE2FILE% ) else ( @title "CMD: Fetching %_STAGE2FILE% from the internet..." powershell -Command "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12;Invoke-WebRequest %_STAGE2LOC%/%_STAGE2FILE% -OutFile %CACHE_DIR%\%_STAGE2FILE%" powershell -ExecutionPolicy UnRestricted -File %CACHE_DIR%\%_STAGE2FILE% ) powershell -Command "iwr -useb https://raw.githubusercontent.com/ChrisTitusTech/winutil/main/winutil.ps1 | iex" :: if exist %CORE_DIR%\_Chris-Titus-Post-Windows-Install-App.ps1 ( :: @title "CMD: _Chris-Titus-Post-Windows-Install-App.ps1 File found locally..." :: powershell -ExecutionPolicy UnRestricted -File %CORE_DIR%\_Chris-Titus-Post-Windows-Install-App.ps1 :: ) else ( :: @title "CMD: Fetching _Chris-Titus-Post-Windows-Install-App.ps1 from the internet..." :: powershell -Command "iwr -useb https://raw.githubusercontent.com/ChrisTitusTech/winutil/main/winutil.ps1 | iex" :: ) goto end :CMD1 :: Pre windows 7 instruction go here (except vista)... :: Windows NT, XP, and 2000 etc. do not have powershell and must find a different way to :: fetch a script over the internet and execute it. echo Detected %* ... echo executing PRE Windows 7 instructions... :: Assuming wget is in teh path... wget -O %TEMP%\%_STAGE2FILE% %_STAGE2LOC%/%_STAGE2FILE% powershell -ExecutionPolicy UnRestricted -File %TEMP%\%_STAGE2FILE% goto end :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: :::::::::::::: :::::::::::::::::::::: :::::::::::::: ERROR handling Below :::::::::::::::::::::: :::::::::::::: :::::::::::::::::::::: :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: :DispErr set _ERRMSG=%* @title %0 -- !!%_ERRMSG%!! echo ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: echo ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: echo :: Message :: echo ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: echo. echo. echo %_ERRMSG% echo Presently I know what to do for Linux, and Windows 7 and beyond... echo. echo :: :: echo ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: echo ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: pause goto end :end EOF } template::config_menu_cmd() { # Description: # Writes the Windows configuration and activation menu script (_config_menu.cmd) to the specified # destination so it can be packaged with Windows unattended installation assets. # # Globals: # None # # Arguments: # --write : Destination file path for the generated _config_menu.cmd script. # # Outputs: # - Appends the configuration menu script content to the provided file. # # Returns: # 0 - Script written successfully. # 1 - Destination path missing or invalid. # # Usage: # template::config_menu_cmd --write /tmp/_config_menu.cmd # # End of Documentation local outfile= while [[ $# -gt 0 ]]; do case "$1" in --write) outfile="$2" shift 2 ;; *) write_warning "template::config_menu_cmd: Unknown parameter '$1'" shift ;; esac done if [[ -z ${outfile} ]]; then write_error "template::config_menu_cmd: No output file specified. Use --write ." return 1 fi write_information "Writing Windows configuration menu script to ${outfile}" cat >>"${outfile}" <<'EOF' :: -- -- :: Windows CMD Script :: :: A D M I N S C R I P T :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: ::::::::::::::::::::::::::::::::::::::// OEM System Configuration Script //:::::::::::::::::::::::::// Windows //::::::: :: :: Author: Vonschutter :: Version: 1.0 :: :: :: Purpose: The purpose of the script is to: :: - Download KMS activation script from 3rd party :: - Run KMS activation for Windows and Office 180 day trial :: :: :: Background: This script is shared in the hopes that someone will find it usefull. To encourage sharing changes :: back to the source this script is released under the GPL v3. (see source location for details) :: https://github.com/vonschutter/RTD-Setup/raw/master/LICENSE.md :: :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: :INIT @echo off @title "RTD Windows Configuration/Activation Menu" set CTT_CONFIG_SCRIPT=_Chris-Titus-Post-Windows-Install-App.ps1 set CTT_URL=https://raw.githubusercontent.com/ChrisTitusTech/winutil/main/winutil.ps1 set MAS_URL=https://get.activated.win set MAS_ACTIVATION_SCRIPT=_MAS_AIO.cmd set RUN_DIR=%~dp0 pushd %~dp0 :MENU color 1F cls echo :::::::::::::::::::// Windows Configuration Options //:::::::::::::::::::::::::: echo :::::::::::::::::::::::::::::// Menu //::::::::::::::::::::::::::::::::::::::::: echo . echo . echo . 1. CTT Menu (Windows Configuration) echo . 2. MAS Menu (Windows Activation) echo . 3. Exit echo . echo . NOTE: Local execution will be tried before fetching from the internet echo . echo :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: echo :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: set /p choice=" Select an option (1-3): " cls if "%choice%"=="1" goto CTT if "%choice%"=="2" goto MAS if "%choice%"=="3" exit /b goto MENU :CTT if exist %CTT_CONFIG_SCRIPT% ( @title "CMD: Running %CTT_CONFIG_SCRIPT% locally" powershell -ExecutionPolicy UnRestricted -File %RUN_DIR%\%CTT_CONFIG_SCRIPT% ) else ( @title "POWERSHELL: Running %CTT_URL% from the internet..." powershell -Command "iwr -useb %CTT_URL% | iex" ) goto MENU :MAS if exist %RUN_DIR%\%MAS_ACTIVATION_SCRIPT% ( @title "CMD: %MAS_ACTIVATION_SCRIPT% File found locally..." powershell -ExecutionPolicy UnRestricted -File %RUN_DIR%\%MAS_ACTIVATION_SCRIPT% if errorlevel 1 ( echo. echo ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: echo . There was an error running the local %MAS_ACTIVATION_SCRIPT% file. echo . Trying to run from the internet... echo ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: echo. powershell -Command "irm %MAS_URL% | iex" ) ) else ( @title "CMD: Running %MAS_URL% from the internet..." powershell -Command "irm %MAS_URL% | iex" ) goto MENU EOF } template::autounattend_xml() { # Description: # Generates an unattended installation XML file (autounattend.xml) for automated Windows setup. # The function can generate configuration files tailored for different Windows versions, # including Windows 7, Windows 10, and Windows 11. # # Globals: # _TLA - The top-level abbreviation, defaults to "RTD". # Preference_InitialKeyboardLayout - The keyboard layout to be used during installation. # Preference_Wireless_Password - The password for the wireless network and user accounts. # # Arguments: # --write - Specifies the path to the output file where the autounattend.xml will be written. # --winver - Specifies the Windows version for which the XML file is being generated. Valid values are "win7", "win10", "win11". # # Outputs: # - Writes the autounattend.xml configuration to the specified output file. # # Returns: # 0 - If the autounattend.xml file was successfully generated. # 1 - If no output file is provided, an invalid Windows version is specified, or if an error occurs during the generation process. # # Usage: # template::autounattend_xml --write /path/to/autounattend.xml --winver win10 # # Example: # template::autounattend_xml --write /path/to/autounattend.xml --winver win11 # # End of Documentation # Generate the autounattend file kvm::get_vm_config_preferences : ${_TLA:="RTD"} local target_file target_winver while [[ $# -gt 0 ]]; do case "$1" in --write) target_file="$2" write_information "Requested to write out instructions to: ${target_file}" shift 2 ;; --winver) target_winver="$2" write_information "Requested to write out instructions for ${target_winver}" shift 2 ;; *) write_error "Incorrect sytax provided, use: ${FUNCNAME[0]} --write --winver " return 1 ;; esac done system::check_required_variables target_winver target_file # Write out the autounattend file case ${target_winver} in win11 | Win11) cat >>${target_file} <<-EOF en-US 0409:00000409 en-US en-US en-US 0 3 VK7JG-NPHTM-C97JM-9MPGT-3V66T true 1 cmd.exe /c echo SELECT DISK=0 >> X:\diskpart.txt 2 cmd.exe /c echo CLEAN >> X:\diskpart.txt 3 cmd.exe /c echo CONVERT GPT >> X:\diskpart.txt 4 cmd.exe /c echo CREATE PARTITION EFI SIZE=100 >> X:\diskpart.txt 5 cmd.exe /c echo FORMAT QUICK FS=FAT32 LABEL="System" >> X:\diskpart.txt 6 cmd.exe /c echo CREATE PARTITION MSR SIZE=16 >> X:\diskpart.txt 7 cmd.exe /c echo CREATE PARTITION PRIMARY >> X:\diskpart.txt 8 cmd.exe /c echo FORMAT QUICK FS=NTFS LABEL="Windows" >> X:\diskpart.txt 9 cmd.exe /c diskpart /s X:\diskpart.txt >> X:\diskpart.log ${Preference_InitialKeyboardLayout//_/-} ${Preference_InitialKeyboardLayout//_/-} en-US ${Preference_InitialKeyboardLayout//_/-} false tangarora Administrators ${Preference_Wireless_Password} true</PlainText> </Password> </LocalAccount> <LocalAccount wcm:action="add"> <Name>RTDUser</Name> <Group>Users</Group> <Password> <Value>${Preference_Wireless_Password}</Value> <PlainText>true</PlainText> </Password> </LocalAccount> </LocalAccounts> </UserAccounts> <OOBE> <ProtectYourPC>3</ProtectYourPC> <HideEULAPage>true</HideEULAPage> <HideWirelessSetupInOOBE>true</HideWirelessSetupInOOBE> </OOBE> <AutoLogon> <Enabled>true</Enabled> <LogonCount>1</LogonCount> <Username>tangarora</Username> <Password> <Value>${Preference_Wireless_Password}</Value> <PlainText>true</PlainText> </Password> </AutoLogon> <FirstLogonCommands> <SynchronousCommand wcm:action="add"> <Order>1</Order> <CommandLine>reg.exe add "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon" /v AutoLogonCount /t REG_DWORD /d 0 /f</CommandLine> </SynchronousCommand> <SynchronousCommand wcm:action="add"> <Order>2</Order> <CommandLine>powershell.exe -NoProfile -Command "Disable-ComputerRestore -Drive 'C:';"</CommandLine> </SynchronousCommand> <SynchronousCommand wcm:action="add"> <Order>3</Order> <CommandLine>powershell.exe -NoProfile -Command "Set-ItemProperty -Path 'HKLM:\Software\Policies\Microsoft\Windows NT\Driver Signing' -Name 'BehaviorOnFailedVerify' -Value 0"</CommandLine> </SynchronousCommand> <SynchronousCommand wcm:action="add"> <Order>4</Order> <CommandLine>cmd.exe /c "gpupdate /target:computer /force"</CommandLine> </SynchronousCommand> <SynchronousCommand wcm:action="add"> <Order>5</Order> <CommandLine>cmd.exe /c A:\rtd-me-sh.cmd</CommandLine> </SynchronousCommand> <SynchronousCommand wcm:action="add"> <Order>6</Order> <Description>Set password age policy</Description> <CommandLine>cmd.exe /c net accounts /maxpwage:1</CommandLine> </SynchronousCommand> <SynchronousCommand wcm:action="add"> <Order>98</Order> <Description>Force reboot to finalize setup</Description> <CommandLine>shutdown.exe /r /t 5 /f</CommandLine> </SynchronousCommand> </FirstLogonCommands> </component> </settings> </unattend> EOF ;; win10 | Win10 | win7 | Win7) cat >${target_file} <<EOF <?xml version="1.0" encoding="utf-8"?> <unattend xmlns="urn:schemas-microsoft-com:unattend" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State"> <settings pass="offlineServicing"></settings> <settings pass="windowsPE"> <component name="Microsoft-Windows-International-Core-WinPE" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS"> <SetupUILanguage> <UILanguage>en-US</UILanguage> </SetupUILanguage> <InputLocale>0409:00000409</InputLocale> <SystemLocale>en-US</SystemLocale> <UILanguage>en-US</UILanguage> <UserLocale>en-US</UserLocale> </component> <component name="Microsoft-Windows-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS"> <ImageInstall> <OSImage> <InstallTo> <DiskID>0</DiskID> <PartitionID>2</PartitionID> </InstallTo> </OSImage> </ImageInstall> <UserData> <ProductKey> <Key>DXG7C-N36C4-C4HTG-X4T3X-2YV77</Key> </ProductKey> <AcceptEula>true</AcceptEula> </UserData> <RunSynchronous> <RunSynchronousCommand> <Order>1</Order> <Path>cmd.exe /c echo SELECT DISK=0 &gt;&gt; X:\diskpart.txt</Path> </RunSynchronousCommand> <RunSynchronousCommand> <Order>2</Order> <Path>cmd.exe /c echo CLEAN &gt;&gt; X:\diskpart.txt</Path> </RunSynchronousCommand> <RunSynchronousCommand> <Order>3</Order> <Path>cmd.exe /c echo CREATE PARTITION PRIMARY SIZE=100 &gt;&gt; X:\diskpart.txt</Path> </RunSynchronousCommand> <RunSynchronousCommand> <Order>4</Order> <Path>cmd.exe /c echo FORMAT QUICK FS=NTFS LABEL="System Reserved" &gt;&gt; X:\diskpart.txt</Path> </RunSynchronousCommand> <RunSynchronousCommand> <Order>5</Order> <Path>cmd.exe /c echo ACTIVE &gt;&gt; X:\diskpart.txt</Path> </RunSynchronousCommand> <RunSynchronousCommand> <Order>6</Order> <Path>cmd.exe /c echo CREATE PARTITION PRIMARY &gt;&gt; X:\diskpart.txt</Path> </RunSynchronousCommand> <RunSynchronousCommand> <Order>7</Order> <Path>cmd.exe /c echo FORMAT QUICK FS=NTFS LABEL="Windows" &gt;&gt; X:\diskpart.txt</Path> </RunSynchronousCommand> <RunSynchronousCommand> <Order>8</Order> <Path>cmd.exe /c diskpart /s X:\diskpart.txt &gt;&gt; X:\diskpart.log</Path> </RunSynchronousCommand> </RunSynchronous> </component> </settings> <settings pass="generalize"></settings> <settings pass="specialize"></settings> <settings pass="auditSystem"></settings> <settings pass="auditUser"></settings> <settings pass="oobeSystem"> <component name="Microsoft-Windows-International-Core" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS"> <InputLocale>${Preference_InitialKeyboardLayout//_/-}</InputLocale> <SystemLocale>${Preference_InitialKeyboardLayout//_/-}</SystemLocale> <UILanguage>en-US</UILanguage> <UserLocale>${Preference_InitialKeyboardLayout//_/-}</UserLocale> </component> <component name="Microsoft-Windows-Shell-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS"> <UserAccounts> <LocalAccounts> <LocalAccount wcm:action="add"> <Name>tangarora</Name> <Group>Administrators</Group> <Password> <Value>${Preference_Wireless_Password}</Value> <PlainText>true</PlainText> </Password> </LocalAccount> <LocalAccount wcm:action="add"> <Name>RTD-User</Name> <Group>Users</Group> <Password> <Value>${Preference_Wireless_Password}</Value> <PlainText>true</PlainText> </Password> </LocalAccount> </LocalAccounts> </UserAccounts> <AutoLogon> <Username>tangarora</Username> <Enabled>true</Enabled> <LogonCount>1</LogonCount> <Password> <Value>${Preference_Wireless_Password}</Value> <PlainText>true</PlainText> </Password> </AutoLogon> <OOBE> <HideEULAPage>true</HideEULAPage> <HideWirelessSetupInOOBE>true</HideWirelessSetupInOOBE> <NetworkLocation>Work</NetworkLocation> <ProtectYourPC>1</ProtectYourPC> <SkipMachineOOBE>true</SkipMachineOOBE> <SkipUserOOBE>true</SkipUserOOBE> </OOBE> <FirstLogonCommands> <SynchronousCommand wcm:action="add"> <Order>1</Order> <CommandLine>reg.exe add "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon" /v AutoLogonCount /t REG_DWORD /d 0 /f</CommandLine> </SynchronousCommand> <SynchronousCommand wcm:action="add"> <Order>2</Order> <CommandLine>powershell.exe -NoProfile -Command "Disable-ComputerRestore -Drive 'C:';"</CommandLine> </SynchronousCommand> <SynchronousCommand wcm:action="add"> <Order>3</Order> <CommandLine>powershell.exe -NoProfile -Command "Set-ItemProperty -Path 'HKLM:\Software\Policies\Microsoft\Windows NT\Driver Signing' -Name 'BehaviorOnFailedVerify' -Value 0"</CommandLine> </SynchronousCommand> <SynchronousCommand wcm:action="add"> <Order>4</Order> <CommandLine>cmd.exe /c "gpupdate /target:computer /force"</CommandLine> </SynchronousCommand> <SynchronousCommand wcm:action="add"> <Order>7</Order> <CommandLine>cmd.exe /c "A:\rtd-me-sh.cmd" </CommandLine> </SynchronousCommand> <SynchronousCommand wcm:action="add"> <Order>98</Order> <Description>Force reboot to finalize setup</Description> <CommandLine>shutdown.exe /r /t 5 /f</CommandLine> </SynchronousCommand> </FirstLogonCommands> </component> </settings> </unattend> EOF ;; *) write_error "No valid Windows version provided, use: win7, win10, win11" ;; esac } template::AutoYast_xml() { # Description: # Generates an AutoYaST XML file for automated SUSE Linux installation. # The generated XML file contains configuration settings such as storage, networking, software packages, # user accounts, security settings, and other system configurations. # # Globals: # Preference_DiskEncryption_SUSE - Disk encryption settings for SUSE installation. # Preference_InitialUserPassword - The initial password for the root and user accounts. # Preference_InitialUser - The initial username for the system. # Preference_InitialKeyboardLayout - The keyboard layout for the system. # _UserDesktopEnvironmentSelection - The desktop environment to be installed (e.g., KDE). # _UserServerEnvironemtSelection - The server environment to be installed (e.g., file_server). # _UserProductSelection - The SUSE product to be installed (e.g., Leap). # _SUSE_PACKMAN_LEAP/_SUSE_PACKMAN_TUMBLEWEED - Packman repository URLs defined in _locations. # _SUSE_NON_OSS_LEAP/_SUSE_NON_OSS_TUMBLEWEED - Non-OSS repository URLs defined in _locations. # _autoyast_filename - The filename where the AutoYaST XML content will be saved. # # Arguments: # $1 - The path to the output file where the AutoYaST XML will be saved. # # Outputs: # - Writes the AutoYaST XML configuration to the specified output file. # # Returns: # 0 - If the AutoYaST XML file was successfully generated. # 1 - If no output file is provided, or if an error occurs during the generation process. # # Usage: # template::AutoYast_xml </path/to/filename> # # Example: # template::AutoYast_xml /path/to/autoyast.xml # # Note: # Ensure that the output file path is correctly specified and that the function's global variables are properly set. # Most valuse are set in the kvm::get_vm_config_preferences function, but defould valuse are provided in the function. # # End of Documentation # make sure the output file is provided if [[ -z ${1} ]]; then { write_error "No output file provided, use: ${FUNCNAME[0]} </path/to/filename>" return 1 }; fi local suse_product="${_UserProductSelection:-${_Product:-Leap}}" local packman_repo="" local non_oss_repo="" case "${suse_product,,}" in tumbleweed) packman_repo="${_SUSE_PACKMAN_TUMBLEWEED:-${_SUSE_PACKMAN_LEAP:-}}" non_oss_repo="${_SUSE_NON_OSS_TUMBLEWEED:-${_SUSE_NON_OSS_LEAP:-}}" ;; leap|opensuse|opensuse-leap|"") packman_repo="${_SUSE_PACKMAN_LEAP:-}" non_oss_repo="${_SUSE_NON_OSS_LEAP:-}" ;; *) packman_repo="${_SUSE_PACKMAN_LEAP:-}" non_oss_repo="" ;; esac local repo_lines="" if [[ -n ${non_oss_repo} ]]; then repo_lines+=$'\t\tsudo zypper ar -cfp 85 '"${non_oss_repo}"$' non-oss\n' fi if [[ -n ${packman_repo} ]]; then repo_lines+=$'\t\tsudo zypper ar -cfp 90 '"${packman_repo}"$' packman\n' fi cat >>${_autoyast_filename} <<-AUTOYAST_EOF <?xml version="1.0"?> <!DOCTYPE profile> <profile xmlns="http://www.suse.com/1.0/yast2ns" xmlns:config="http://www.suse.com/1.0/configns" > <general> <mode> <confirm config:type="boolean">false</confirm> <final_reboot config:type="boolean">true</final_reboot> <final_halt config:type="boolean">false</final_halt> <halt config:type="boolean">false</halt> <second_stage config:type="boolean">false</second_stage> <forceboot config:type="boolean">true</forceboot> </mode> </general> <storage> <proposal> <confirm config:type="boolean">false</confirm> <lvm config:type="boolean">true</lvm> <windows_delete_mode config:type="symbol">all</windows_delete_mode> <linux_delete_mode config:type="symbol">all</linux_delete_mode> <other_delete_mode config:type="symbol">all</other_delete_mode> ${Preference_DiskEncryption_SUSE} </proposal> </storage> <networking> <keep_install_network config:type="boolean">true</keep_install_network> </networking> <deploy_image> <image_installation config:type="boolean">true</image_installation> </deploy_image> <software> <install_recommended config:type="boolean">true</install_recommended> <patterns config:type="list"> <pattern>${_UserDesktopEnvironmentSelection}</pattern> </patterns> <packages config:type="list"> <package>wget</package> <package>curl</package> <package>dialog</package> <package>vim</package> <package>spice-vdagent</package> <package>git</package> <package>ansible</package> <package>pkexec</package> <package>zip</package> <package>zenity</package> <package>acpi</package> <package>pciutils</package> <package>jq</package> <package>xhost</package> <package>rsync</package> </packages> <products config:type="list"> <product>${_Product:-"Leap"}</product> </products> </software> <firstboot> <firstboot_enabled config:type="boolean">false</firstboot_enabled> </firstboot> <users config:type="list"> <user> <encrypted config:type="boolean">true</encrypted> <fullname>root</fullname> <gid>0</gid> <home>/root</home> <shell>/bin/bash</shell> <uid>0</uid> <user_password>${Preference_InitialUserPassword:-"letmein"}</user_password> <username>root</username> </user> <user> <encrypted config:type="boolean">true</encrypted> <fullname>RTD User</fullname> <shell>/bin/bash</shell> <user_password>${Preference_InitialUserPassword:-"letmein"}</user_password> <username>${Preference_InitialUser:-"tangarora"}</username> </user> </users> <keyboard> <keymap>${Preference_InitialKeyboardLayout:-"english-us"}</keymap> </keyboard> <timezone> <hwclock>UTC</hwclock> <timezone>Europe/Berlin</timezone> </timezone> <login_settings> <autologin_user>${Preference_InitialUser:-"tangarora"}</autologin_user> <password_less_login config:type="boolean">true</password_less_login> </login_settings> <services-manager> <default_target>graphical</default_target> <services> <enable config:type="list"> <service>sshd</service> </enable> </services> </services-manager> <scripts> <init-scripts config:type="list"> <script> <source><![CDATA[ #!/bin/bash git clone ${GIT_RTD_SRC_URL} ${_OEM_DIR:-"/opt/rtd"} chmod 755 ${_OEM_DIR:-"/opt/rtd"}/core/rtd-oem-enable-config.sh ${_OEM_DIR:-"/opt/rtd"}/core/rtd-oem-enable-config.sh cp *.xml ${_OEM_DIR:-"/opt/rtd"}/ $(printf "%s" "$repo_lines") zypper --gpg-auto-import-keys refresh # zypper dup --from packman -y --force-resolution zypper in -y ansible zypper in -y yad reboot ]]> </source> </script> </init-scripts> </scripts> <security> <displaymanager_remote_access>no</displaymanager_remote_access> <fail_delay>3</fail_delay> <faillog_enab>yes</faillog_enab> <gid_max>60000</gid_max> <gid_min>101</gid_min> <lastlog_enab>yes</lastlog_enab> <obscure_checks_enab>no</obscure_checks_enab> <permission_security>secure</permission_security> <run_updatedb_as>nobody</run_updatedb_as> <uid_max>60000</uid_max> <uid_min>500</uid_min> <selinux_mode>permissive</selinux_mode> <lsm_select>selinux</lsm_select> </security> </profile> AUTOYAST_EOF } template::Agama_json() { # Description: # Generates an Agama JSON profile for automated SUSE Linux installations using the new # Agama installer format. The generated JSON mirrors the intent of template::AutoYast_xml # but in the modern schema expected by Agama. # # Globals: # Preference_DiskEncryption_SUSE - Disk encryption XML snippet used to extract password. # Preference_InitialUserPassword - Initial password for root and user accounts. # Preference_InitialUser - Primary username to create. # Preference_InitialKeyboardLayout - Keyboard layout to apply. # _UserDesktopEnvironmentSelection - Desktop environment pattern to install. # _UserProductSelection/_Product - SUSE product channel to enable. # _SUSE_PACKMAN_LEAP/_SUSE_PACKMAN_TUMBLEWEED - Packman repo URLs (optional). # _SUSE_NON_OSS_LEAP/_SUSE_NON_OSS_TUMBLEWEED - Non-OSS repo URLs (optional). # GIT_RTD_SRC_URL - Git source for RTD OEM scripts. # _OEM_DIR - Target directory for RTD OEM scripts. # _agama_filename - Optional pre-defined output file path. # # Arguments: # $1 - Path to the output JSON file (optional if _agama_filename is already set). # # Outputs: # - Writes an Agama-compatible JSON profile to the specified file. # # Returns: # 0 - Profile successfully generated. # 1 - Missing output path or Python error while generating JSON. # # Usage: # template::Agama_json </path/to/profile.json> # # Notes: # - Ensures python3 is present using software::check_native_package_dependency before generating JSON. # - Assumes Preference_DiskEncryption_SUSE holds an <encryption_password> snippet when set. # # End of documentation local output_file="${1:-${_agama_filename:-}}" if [[ -z ${output_file} ]]; then write_error "No output file provided, use: ${FUNCNAME[0]} </path/to/filename>" return 1 fi _agama_filename="${output_file}" # Ensure python3 is available (installs if necessary on supported distros) if ! command -v python3 >/dev/null 2>&1; then write_information "Ensuring python3 is available using software::check_native_package_dependency..." if ! software::check_native_package_dependency python3; then write_error "Failed to ensure python3 is installed." return 1 fi fi local python_bin="python3" if ! command -v "${python_bin}" >/dev/null 2>&1; then write_error "python3 is required but not available." return 1 fi if ! "${python_bin}" -c 'import json, re, textwrap' >/dev/null 2>&1; then write_error "python3 is missing required standard modules (json, re, textwrap)." return 1 fi local pref_enc="${Preference_DiskEncryption_SUSE:-}" local init_pass="${Preference_InitialUserPassword:-letmein}" local init_user="${Preference_InitialUser:-tangarora}" local keyboard_layout="${Preference_InitialKeyboardLayout:-english-us}" local desktop_pattern="${_UserDesktopEnvironmentSelection:-gnome_x11}" local product="${_Product:-${_UserProductSelection:-Leap}}" local suse_product="${_UserProductSelection:-${_Product:-Leap}}" local packman_repo="" local non_oss_repo="" case "${suse_product,,}" in tumbleweed) packman_repo="${_SUSE_PACKMAN_TUMBLEWEED:-${_SUSE_PACKMAN_LEAP:-}}" non_oss_repo="${_SUSE_NON_OSS_TUMBLEWEED:-${_SUSE_NON_OSS_LEAP:-}}" ;; leap|opensuse|opensuse-leap|"") packman_repo="${_SUSE_PACKMAN_LEAP:-}" non_oss_repo="${_SUSE_NON_OSS_LEAP:-}" ;; *) packman_repo="${_SUSE_PACKMAN_LEAP:-}" non_oss_repo="" ;; esac local oem_dir="${_OEM_DIR:-/opt/rtd}" local git_src="${GIT_RTD_SRC_URL:-https://github.com/rivertech-division/RTD-Setup.git}" PREF_ENC="${pref_enc}" \ PREF_INIT_PASS="${init_pass}" \ PREF_INIT_USER="${init_user}" \ PREF_KEYBOARD="${keyboard_layout}" \ PREF_DESKTOP="${desktop_pattern}" \ PREF_PRODUCT="${product}" \ PREF_PACKMAN="${packman_repo}" \ PREF_NONOSS="${non_oss_repo}" \ PREF_OEM_DIR="${oem_dir}" \ PREF_GIT_SRC="${git_src}" \ PREF_OUTPUT="${output_file}" \ "${python_bin}" - <<'PY' || return 1 import json import os import re import textwrap output_path = os.environ["PREF_OUTPUT"] enc_xml = os.environ.get("PREF_ENC", "") init_pass = os.environ.get("PREF_INIT_PASS", "letmein") init_user = os.environ.get("PREF_INIT_USER", "tangarora") keyboard_layout = os.environ.get("PREF_KEYBOARD", "english-us") desktop_pattern = os.environ.get("PREF_DESKTOP", "gnome_x11") product = os.environ.get("PREF_PRODUCT", "Leap") packman_repo = os.environ.get("PREF_PACKMAN", "") non_oss_repo = os.environ.get("PREF_NONOSS", "") oem_dir = os.environ.get("PREF_OEM_DIR", "/opt/rtd") git_src = os.environ.get("PREF_GIT_SRC", "https://github.com/vonschutter/RTD-Setup.git") enc_password = None if enc_xml: match = re.search(r"<encryption_password>(.*?)</encryption_password>", enc_xml) if match: enc_password = match.group(1) script_lines = [ "#!/bin/bash", f"git clone {git_src} {oem_dir}", f"chmod 755 {oem_dir}/core/rtd-oem-enable-config.sh", f"{oem_dir}/core/rtd-oem-enable-config.sh", f"cp *.json {oem_dir}/", ] if non_oss_repo: script_lines.append(f"zypper ar -cfp 85 {non_oss_repo} non-oss") if packman_repo: script_lines.append(f"zypper ar -cfp 90 {packman_repo} packman") if non_oss_repo or packman_repo: script_lines.append("zypper --gpg-auto-import-keys refresh") script_lines.extend([ "zypper in -y ansible", "zypper in -y yad", "reboot" ]) script_content = "\n".join(script_lines) storage_proposal = { "confirm": False, "lvm": True, "windows_delete_mode": "all", "linux_delete_mode": "all", "other_delete_mode": "all" } if enc_password: storage_proposal["encryption_password"] = enc_password data = { "general": { "mode": { "confirm": False, "final_reboot": True, "final_halt": False, "halt": False, "second_stage": False, "forceboot": True } }, "storage": { "proposal": storage_proposal }, "networking": { "keep_install_network": True }, "deploy_image": { "image_installation": True }, "software": { "install_recommended": True, "patterns": [desktop_pattern], "packages": [ "wget", "curl", "dialog", "vim", "spice-vdagent", "git", "ansible", "pkexec", "zip", "zenity", "acpi", "pciutils", "jq", "xhost", "rsync" ], "products": [product] }, "firstboot": { "firstboot_enabled": False }, "users": [ { "encrypted": True, "fullname": "root", "gid": 0, "home": "/root", "shell": "/bin/bash", "uid": 0, "user_password": init_pass, "username": "root" }, { "encrypted": True, "fullname": "RTD User", "shell": "/bin/bash", "user_password": init_pass, "username": init_user } ], "keyboard": { "keymap": keyboard_layout }, "timezone": { "hwclock": "UTC", "timezone": "Europe/Berlin" }, "login_settings": { "autologin_user": init_user, "password_less_login": True }, "services": { "default_target": "graphical", "enable": ["sshd"] }, "scripts": [ { "stage": "init", "interpreter": "bash", "content": script_content } ], "security": { "displaymanager_remote_access": "no", "fail_delay": 3, "faillog_enab": "yes", "gid_max": 60000, "gid_min": 101, "lastlog_enab": "yes", "obscure_checks_enab": "no", "permission_security": "secure", "run_updatedb_as": "nobody", "uid_max": 60000, "uid_min": 500, "selinux_mode": "permissive", "lsm_select": "selinux" } } with open(output_path, "w", encoding="utf-8") as f: json.dump(data, f, indent=4) f.write("\n") PY system::log_item "Agama JSON profile generated at: ${_agama_filename}" system::log_item "Contents of ${_agama_filename}:" cat "${_agama_filename}" return 0 } template::cloud_config() { # Description: # Appends specialized cloud-config YAML to an existing user-data file for an Ubuntu VM. # The content is dynamically generated based on the VM's intended role (desktop flavor, # server variant, or custom deployments such as Minecraft). # # Key Features: # - Uses autoinstall directives (via curtin late-commands) to install additional packages. # - Embeds user credentials, SSH key data, and initial configurations. # - References external resources (e.g., GIT_RTD_SRC_URL) for OEM or post-install scripts. # - Highly sensitive to YAML indentation and spacing (be cautious with modifications). # # Globals (set elsewhere, e.g., kvm::get_vm_config_preferences): # Preference_ssh_pub_key : SSH public key for initial user. # Preference_InitialKeyboardLayout: Default keyboard layout. # Preference_InitialUser : Username for the initial account. # Preference_InitialUserPassword : Password for the initial account. # GIT_RTD_SRC_URL : Git repository URL for RTD setup scripts. # # Arguments (provided as CLI flags): # --vmname <string> : Hostname/VM name (validated for standard DNS rules). # --CloudConfigDir <path> : Directory used by cloud-config generation. # --write <output_file> : Where the YAML content is appended (required). # --role <role_string> : VM role (e.g., 'VDI', 'server', 'minecraft'). # --server-app <app_name> : Specific server package/flavor to install (e.g., 'kubuntu-desktop'). # --pre-config <string> : Overrides the role with a pre-config option if provided. # # Outputs: # - Appends the generated YAML directives to the specified user-data file. # # Returns: # - 0 : Success (cloud-config directives appended successfully). # - 1 : Invalid or missing output file, or an error during content generation. # # Usage: # template::cloud_config --vmname myVM --write /path/to/user-data --role VDI \ # --server-app ubuntu-desktop # # Example: # template::cloud_config \ # --vmname my-server \ # --write /tmp/cloud-init/user-data \ # --role server \ # --server-app apache2 # # Notes: # - Typically invoked by higher-level functions (e.g., system::generate_cloudconfig). # - YAML indentation is crucial. Ensure changes maintain valid YAML structure. # - The function appends different sections of late-commands and package lists depending # on the requested role/server-app combination. # - Cloud-init autoinstall runs these commands during or immediately after installation. # # End of Documentation system::log_item "Called by: ${FUNCNAME[1]}" system::log_item "Processing parameters: $*" local vm_name user_data_file role server_app ask cloud_config_dir pre_config common_vdi_config common_server_config common_cleanup_config while [[ $# -gt 0 ]]; do case "$1" in --CloudConfigDir) cloud_config_dir="$2" shift 2 ;; --write) user_data_file="$2" shift 2 ;; --role) role="$2" shift 2 ;; --server-app) server_app="$2" shift 2 ;; --pre-config) pre_config="$2" shift 2 ;; --vmname | --VMName | --vm_name | --VmName | --vm-name | --VM-Name | --hostname | --HostName | --host-name | --Host-Name) if ! kvm::util::set_vm_name --var vm_name --name "$2"; then return 1 fi shift 2 # Remove argument name from processing ;; *) system::log_item "Warnig: Unknown parameter '$1'" shift 2 ;; esac done # Display parameter interpretation (or proceed with processing). system::log_item "Cloud Configuration Directory: $cloud_config_dir" system::log_item "Role: $role" system::log_item "Server App: ${server_app:-"None"}" if [[ -n $pre_config ]] ; then { role=$pre_config ; system::log_item "Pre-Config: ${pre_config}" ; } ; fi system::log_item "VM Name: $vm_name" local cloud_hostname="${vm_hostname:-$vm_name}" [[ -z "$cloud_hostname" ]] && cloud_hostname="${vm_name:-$(hostname)}" # Define common installation tasks for manageable code common_vdi_pre_config=$(cat <<EOF - echo '******************* Common VDI Pre-Config ********************' # Set up the target for installation # - mount --bind /dev /target/dev # - mount --bind /proc /target/proc # - mount --bind /sys /target/sys # - mount --bind /run /target/run # Update software sources and upgrade packages - curtin in-target --target=/target -- bash -c 'apt-get update -y && apt-get upgrade -y ; exit 0' EOF ) # Define common cleanup tasks for manageable code common_vdi_post_config=$(cat <<EOF - echo '******************* Common VDI Post-Config ********************' # Final setup and installation of GUI software: # Enable and start spice-vdagent onlogin - curtin in-target --target=/target -- /bin/bash -c 'apt-get -y install spice-vdagent qemu-guest-agent ; exit 0' - curtin in-target --target=/target -- /bin/bash -c 'apt-get -y install ansible ; exit 0' - curtin in-target --target=/target -- /bin/bash -c 'systemctl enable spice-vdagentd ; exit 0' # Ensure spice-vdagent autostarts in user sessions - curtin in-target --target=/target -- mkdir -p /etc/xdg/autostart - curtin in-target --target=/target -- bash -c 'echo -e "[Desktop Entry]\nType=Application\nExec=/usr/bin/spice-vdagent\nHidden=false\nNoDisplay=false\nX-GNOME-Autostart-enabled=true\nName=Spice Agent" > /etc/xdg/autostart/spice-vdagent.desktop' # Clone RTD Setup & Execute OEM Config - curtin in-target --target=/target -- /bin/bash -c 'mkdir -p ${_OEM_DIR:-"/opt/rtd"}' - curtin in-target --target=/target -- /bin/bash -c 'for i in {1..5}; do git clone --depth=1 ${GIT_RTD_SRC_URL} ${_OEM_DIR:-"/opt/rtd"} && break || { echo "Clone failed, trying again..." ; rm -r ${_OEM_DIR:-"/opt/rtd"}/* ; sleep 10; }; done' - curtin in-target --target=/target -- /bin/bash -c '${_OEM_DIR:-"/opt/rtd"}/core/rtd-oem-enable-config.sh' EOF ) common_cleanup_config=$(cat <<EOF - echo '******************* Common Cleanup Post-Config ********************' # Ensure installer media and loop devices are detached before reboot # - /bin/bash -c 'umount -lf /cdrom 2>/dev/null || true' # - /bin/bash -c 'umount -lf /media/cdrom 2>/dev/null || true' # - /bin/bash -c 'umount -lf /run/install/media 2>/dev/null || true' # - /bin/bash -c 'for d in /run/shutdown/mounts/*; do umount -lf "$d" 2>/dev/null || true; done' - /bin/bash -c 'losetup -D 2>/dev/null || true' # - /bin/bash -c 'eject -m 2>/dev/null || true' EOF ) # make the main section of the cloud config file cat >>${user_data_file} <<EOF #cloud-config power_state: mode: reboot message: "RTD: Installation complete, rebooting..." condition: true autoinstall: version: 1 timezone: geoip drivers: install: true ssh: allow-pw: true install-server: true $([[ -n ${Preference_ssh_pub_key} ]] && echo authorized-keys:) $([[ -n ${Preference_ssh_pub_key} ]] && echo -) ${Preference_ssh_pub_key} keyboard: layout: ${Preference_InitialKeyboardLayout} identity: hostname: "${cloud_hostname}" username: "${Preference_InitialUser}" password: "${Preference_InitialUserPassword}" storage: layout: name: lvm sizing-policy: all $( if [[ "${Preference_DiskEncryption}" == "YES" ]] ; then echo "password: ${Preference_Disk_Password}" ; fi ) packages: - dialog - git - ansible - curl - wget - zip - p7zip-full - byobu - vim - jq - spice-vdagent EOF # ----------------------------------------------------------------------------------------- # End COMMON section to the cloud config file # ----------------------------------------------------------------------------------------- if [[ $role == "VDI" ]]; then case ${server_app} in kubuntu-desktop) system::log_item "Generating cloud-config for the role: kubuntu-desktop" # ----------------------------------------------------------------------------------------- cat >>${user_data_file} <<EOF late-commands: ${common_vdi_pre_config} # Install Kubuntu Desktop Environment - curtin in-target --target=/target -- /bin/bash -c 'for i in {1..3}; do apt-get -y install kubuntu-desktop || apt-get upgrade -y || true ; done' - curtin in-target --target=/target -- /bin/bash -c 'apt-get -y install plymouth-theme-kubuntu-logo plymouth-theme-kubuntu-text plymouth-theme-spinner && update-initramfs -u ; exit 0' - curtin in-target --target=/target -- /bin/bash -c 'systemctl set-default graphical.target ; exit 0' ${common_vdi_post_config} ${common_cleanup_config} EOF # ----------------------------------------------------------------------------------------- ;; ubuntu-desktop) system::log_item "Generating cloud-config for the role: ubuntu-desktop" # ----------------------------------------------------------------------------------------- cat >>${user_data_file} <<EOF - gdm3 - xinit late-commands: ${common_vdi_pre_config} # Install Ubuntu Desktop Environment - curtin in-target --target=/target -- /bin/bash -c 'for i in {1..3}; do apt-get -y install ubuntu-desktop-minimal || true ; done' - curtin in-target --target=/target -- /bin/bash -c 'apt-get -y install plymouth-theme-ubuntu-text plymouth-theme-spinner plymouth || true' - curtin in-target --target=/target -- systemctl disable systemd-networkd-wait-online.service || true ${common_vdi_post_config} ${common_cleanup_config} EOF # ----------------------------------------------------------------------------------------- ;; xubuntu-desktop) system::log_item "Generating cloud-config for the role: xubuntu-desktop" # ----------------------------------------------------------------------------------------- cat >>${user_data_file} <<EOF - lightdm - wine late-commands: ${common_vdi_pre_config} # Install xubuntu-desktop - curtin in-target --target=/target -- /bin/bash -c 'apt-get update -y' - curtin in-target --target=/target -- /bin/bash -c 'apt-get upgrade -y' - curtin in-target --target=/target -- /bin/bash -c 'for i in {1..3}; do apt-get -y install xubuntu-desktop && break || { printf "Attempt %d failed\n" "$i" >&2; sleep 5; }; done' - curtin in-target --target=/target -- apt install -y plymouth-theme-xubuntu-logo plymouth-theme-xubuntu-logo plymouth-theme-spinner plymouth-x11 ; update-initramfs -u ; exit 0 ${common_vdi_post_config} EOF # ----------------------------------------------------------------------------------------- ;; ubuntu-mate-desktop) system::log_item "Generating cloud-config for ${2}" # ----------------------------------------------------------------------------------------- cat >>${user_data_file} <<EOF - lightdm - wine late-commands: ${common_vdi_pre_config} # Install Ubuntu MATE Desktop - curtin in-target --target=/target -- /bin/bash -c 'apt-get update -y' - curtin in-target --target=/target -- /bin/bash -c 'apt-get upgrade -y' - curtin in-target --target=/target -- /bin/bash -c "echo lightdm shared/default-x-display-manager select lightdm | debconf-set-selections" - curtin in-target --target=/target -- /bin/bash -c 'for i in {1..3}; do apt-get -y install ubuntu-mate-desktop && break || { printf "Attempt %d failed\n" "$i" >&2; sleep 5; }; done' - curtin in-target --target=/target -- apt-get -y install plymouth-theme-ubuntu-mate-logo plymouth-theme-ubuntu-mate-text plymouth-theme-spinner plymouth-x11 ; update-initramfs -u ; exit 0 - curtin in-target --target=/target -- systemctl set-default graphical.target ; exit 0 - curtin in-target --target=/target -- systemctl disable systemd-networkd-wait-online.service ; exit 0 ${common_vdi_post_config} EOF # ----------------------------------------------------------------------------------------- ;; lubuntu-desktop) system::log_item "Generating cloud-config for ${2}" # ----------------------------------------------------------------------------------------- cat >>${user_data_file} <<EOF - sddm - wine late-commands: ${common_vdi_pre_config} # Install Lubuntu Desktop - curtin in-target --target=/target -- /bin/bash -c 'apt-get update -y' - curtin in-target --target=/target -- /bin/bash -c 'apt-get upgrade -y' - curtin in-target --target=/target -- /bin/bash -c 'for i in {1..3}; do apt-get -y install lubuntu-desktop && break || { printf "Attempt %d failed\n" "$i" >&2; sleep 5; }; done' - curtin in-target --target=/target -- /bin/bash -c 'apt-get -y install plymouth-theme-lubuntu-logo plymouth-theme-lubuntu-text ; update-initramfs -u ; || true' ${common_vdi_post_config} EOF # ----------------------------------------------------------------------------------------- ;; cinnamon-desktop-environment) system::log_item "Generating cloud-config for ${2}" # ----------------------------------------------------------------------------------------- cat >>${user_data_file} <<EOF - sddm - wine late-commands: ${common_vdi_pre_config} # Install Cinnamon Desktop - curtin in-target --target=/target -- /bin/bash -c 'apt-get update -y' - curtin in-target --target=/target -- /bin/bash -c 'apt-get upgrade -y' - curtin in-target --target=/target -- /bin/bash -c 'for i in {1..3}; do apt-get -y install cinnamon-desktop-environment && break || { printf "Attempt %d failed\n" "$i" >&2; sleep 5; }; done' - curtin in-target --target=/target -- /bin/bash -c 'apt-get -y install plymouth-theme-ubuntucinnamon-spinner ; update-initramfs -u ; exit 0' ${common_vdi_post_config} EOF # ----------------------------------------------------------------------------------------- ;; esac elif [[ $role == "server" ]]; then # ----------------------------------------------------------------------------------------- # Add SERVER section to the cloud config file based on preferences # This will install a server application if selected along with standard ubuntu-server # ----------------------------------------------------------------------------------------- if [[ -n "${server_app}" ]]; then system::log_item "📱 Got: ${server_app} Attempting to install $server_app..." system::log_item "📱 Generating cloud-config for $server_app" # ----------------------------------------------------------------------------------------- cat >>${user_data_file} <<EOF - ubuntu-server - ${server_app} late-commands: - echo "******************* ${server_app} Config ********************" - curtin in-target --target=/target -- /bin/bash -c 'mkdir -p ${_OEM_DIR:-"/opt/rtd"} ; exit 0' - curtin in-target --target=/target -- /bin/bash -c 'git clone --depth=1 ${GIT_RTD_SRC_URL} ${_OEM_DIR:-"/opt/rtd"} ; exit 0' - curtin in-target --target=/target -- /bin/bash -c 'apt-get -y install ${server_app} ; exit 0' - curtin in-target --target=/target -- /bin/bash -c '${_OEM_DIR:-"/opt/rtd"}/core/rtd-oem-enable-config.sh ; exit 0' ${common_cleanup_config} EOF # ----------------------------------------------------------------------------------------- else server_app="openssh-server" system::log_item "〇 No server application selected; defaulting to ${server_app}" system::log_item "🖧 Generating cloud-config for ubuntu-server" # ----------------------------------------------------------------------------------------- cat >>${user_data_file} <<EOF - ubuntu-server - ${server_app} late-commands: - echo "******************* ${server_app} Default Config ********************" - curtin in-target --target=/target -- /bin/bash -c 'apt-get -y install ${server_app}' - curtin in-target --target=/target -- /bin/bash -c 'mkdir -p ${_OEM_DIR:-"/opt/rtd"} ; exit 0' - curtin in-target --target=/target -- /bin/bash -c 'git clone --depth=1 ${GIT_RTD_SRC_URL} ${_OEM_DIR:-"/opt/rtd"} ; exit 0' - curtin in-target --target=/target -- /bin/bash -c '${_OEM_DIR:-"/opt/rtd"}/core/rtd-oem-enable-config.sh ; exit 0' ${common_cleanup_config} EOF # ----------------------------------------------------------------------------------------- fi # ----------------------------------------------------------------------------------------- else case ${role} in minecraft) system::log_item "⛏️ Generating cloud-config for minecraft-server" # ----------------------------------------------------------------------------------------- cat >>${user_data_file} <<EOF - ubuntu-server - byobu - default-jre late-commands: - echo "******************* ${role} Config ********************" # Clone RTD Setup & Execute OEM Config - curtin in-target --target=/target -- /bin/bash -c 'mkdir -p ${_OEM_DIR:-"/opt/rtd"} ; exit 0' - curtin in-target --target=/target -- /bin/bash -c 'git clone --depth=1 ${GIT_RTD_SRC_URL} ${_OEM_DIR:-"/opt/rtd"} ; exit 0' - curtin in-target --target=/target -- /bin/bash -c '${_OEM_DIR:-"/opt/rtd"}/core/rtd-oem-enable-config.sh ; exit 0' # Setup Minecraft Server and autologin to the console and launch the server - mkdir -p /target/etc/systemd/system/getty@tty1.service.d - touch /target/etc/systemd/system/getty@tty1.service.d/override.conf - echo [Service] > /target/etc/systemd/system/getty@tty1.service.d/override.conf - echo ExecStart= >> /target/etc/systemd/system/getty@tty1.service.d/override.conf - echo 'ExecStart=-/sbin/agetty --noissue --autologin tangarora %I \$TERM' >> /target/etc/systemd/system/getty@tty1.service.d/override.conf - echo Type=idle >> /target/etc/systemd/system/getty@tty1.service.d/override.conf - echo NAutoVTs=1 >> /etc/systemd/logind.conf - echo "Please run rtd-minecraft-server to start the Minecraft Server!" >> /target/etc/update-motd.d/00-header - echo rtd-minecraft-server >> /target/etc/profile EOF # ----------------------------------------------------------------------------------------- ;; *) system::log_item "〇 No CUSTOM server application selected, installing ubuntu-server" system::log_item "🖧 Generating cloud-config for generic ubuntu-server" system::log_item "🖧 Please install the desired application MANUALLY" # ----------------------------------------------------------------------------------------- cat >>${user_data_file} <<EOF ${common_vdi_pre_config} late-commands: - echo "******************* ${server_app} Config ********************" - curtin in-target --target=/target -- apt-get install ${server_app:-"openssh-server"} - curtin in-target --target=/target -- /bin/bash -c 'mkdir -p ${_OEM_DIR:-"/opt/rtd"} ; exit 0' - curtin in-target --target=/target -- /bin/bash -c 'git clone --depth=1 ${GIT_RTD_SRC_URL} ${_OEM_DIR:-"/opt/rtd"} ; exit 0' - curtin in-target --target=/target -- /bin/bash -c '${_OEM_DIR:-"/opt/rtd"}/core/rtd-oem-enable-config.sh ; exit 0' ${common_cleanup_config} EOF # ----------------------------------------------------------------------------------------- ;; esac fi return 0 } template::minecraft_server_launcher() { case ${1} in --write) cat >>${2} <<'EOF' #!/bin/bash #:: RTD System System Managment Script #:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: #:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: #:: Author: RTD Library #:: Version: 1.03 #:: #:: #:: Purpose: The purpose of the script is to perform managment tasks on Linux systems #:: #:: This is a script that will start Minecraft Server on an Ubuntu or Debian (Ubuntu) based server. It will start #:: multiple panes using "byobu", and start the server etc there... #:: #:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: #:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: #:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: #:::::::::::::: :::::::::::::::::::::: #:::::::::::::: Script Settings :::::::::::::::::::::: #:::::::::::::: :::::::::::::::::::::: #:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: # Variables that govern the behavior or the script and location of files are # set here. There should be no reason to change any of this to setup and # get a working Minecraft server on Ubuntu. However, if you have updated scripts # and need to download yours from someplace else you only need to change these # setings. # Your Minecraft version (this is a preference only), script will always # get the latest Minecraft if it does not find a local server. MINECRAFT_VERSION=Latest # Your local Minecraft directory. MINECRAFT_HOME=~/bin/minecraft.server/Minecraft.$MINECRAFT_VERSION MINECRAFT_JAR=server.jar #:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: #:::::::::::::: :::::::::::::::::::::: #:::::::::::::: Script Functions :::::::::::::::::::::: #:::::::::::::: :::::::::::::::::::::: #:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: # function software::check_native_package_dependency (){ # Simple function to check if software is available and take action # if it is not. Software name must match command to envoke it. #--------------------------------------------------------------- echo "Checking for script dependencies and install if not there..." #--------------------------------------------------------------- if hash "$1" 2>/dev/null; then echo "I found that $1 is present on this system... thankyou for that! " else echo "You seem to have no $1... I will try to get it... " install_software "$1" if [ $? != 0 ]; then echo "That install didn't work out so well." echo "Please manually try to add the software since I couldn't do it." exit fi echo "OK Done! Continuing..." fi } function install_software (){ # Simple function to help installing software on several linux distributions # Should work on Fedora, SUSE, RedHat, Ubuntu, Debian etc. unless the naming # convention of the software package is different between distributions. if hash pkcon 2>/dev/null; then sudo pkcon -y install "$@" elif hash yum 2>/dev/null; then sudo yum -y install "$@" elif hash zypper 2>/dev/null; then sudo zypper install -y "$@" elif hash apt-get 2>/dev/null; then export DEBIAN_FRONTEND=noninteractive sudo apt-get -y -qq --allow-change-held-packages --ignore-missing install "$@" else echo -e $YELLOW "This system does not seem to have a software managment system" $ENDCOLOR _cleanup exit 1 fi } function setup_minecraft_root (){ # Setup and initiate the location of the mincraft server. # For us i likes to live in /home/$USER/bin/minecraft.server/Minecraft.$VERSION # This tests if the script files are available and downloades them if not. mkdir -p $MINECRAFT_HOME for i in start.sh announce.py server.properties eula.txt do if [ ! -f "$MINECRAFT_HOME/$i" ]; then $i $MINECRAFT_HOME fi done if [ ! -f "$MINECRAFT_HOME/$MINECRAFT_JAR" ]; then minecraft_update fi } function run_minecraft_server_manager (){ # Start byobu multi screen app... byobu new-session -d -s $USER # status screen byobu rename-window -t $USER:0 'Minecraft Server Manager' byobu send-keys "bash $MINECRAFT_HOME/start.sh" C-m # Create new pane vertically and display htop byobu split-window -v byobu send-keys "htop " C-m # Split the "htop" window in 2, and start spedometer there. byobu split-window -h byobu send-keys "speedometer -r $NETINT -t $NETINT" C-m # Create new window... byobu new-window -t $USER:1 -n 'Anouncing on Network (Press F4 to switch between windows)' byobu send-keys "python3 $MINECRAFT_HOME/announce.py" C-m # Set default window as the dev split plane byobu select-window -t $USER:0 byobu-tmux select-pane -t 0 # Attach to the session you just created byobu attach-session -t $USER } function check_java (){ ~/bin/java/bin/java --version >/dev/null if [ $? -eq 0 ]; then echo "java of some version is present. I respect your choice... and will try to run! --- OK!" else echo Java is not present where expected... will now download: wget https://download.oracle.com/java/20/latest/jdk-20_linux-x64_bin.tar.gz mkdir ~/bin/java && tar xzvf jdk-20_linux-x64_bin.tar.gz --directory ~/bin/java/ --strip 1 #software::check_native_package_dependency default-jre fi } function find_active_network_interface (){ # Description: Detect the active network interface and export it for later use. # Globals: Sets NETINT # Arguments: None. # Outputs: Logs the detected interface to STDOUT. # Returns: 0 on success, 1 if no interface is found. # End of documentation NETINT=$(ip addr | awk '/state UP/ {print $2}' | grep -v "br*" | grep -v "wlp*" | head --bytes -2 ) if [[ -z "$NETINT" ]]; then echo "Active network interface not found." return 1 fi echo "Active network interface is: $NETINT" export NETINT return 0 } function minecraft_update() { # Ensure the Minecraft home directory exists if [ ! -d "$MINECRAFT_HOME" ]; then echo "Minecraft directory not found: $MINECRAFT_HOME" exit 1 fi # Download the version manifest VERSION_MANIFEST_URL="https://launchermeta.mojang.com/mc/game/version_manifest.json" VERSION_MANIFEST_FILE="$MINECRAFT_HOME/version_manifest.json" if ! wget -q -O "$VERSION_MANIFEST_FILE" --no-check-certificate "$VERSION_MANIFEST_URL"; then echo "Failed to download version manifest." exit 1 fi # Use Python to extract the server URL from the manifest MC_SERVER_URL=$(python3 -c " import json with open('$VERSION_MANIFEST_FILE') as file: data = json.load(file) version = data['latest']['release'] version_info = next(item for item in data['versions'] if item['id'] == version) print(version_info['url']) " | xargs wget -q -O - --no-check-certificate | python3 -c " import json, sys data = json.load(sys.stdin) print(data['downloads']['server']['url']) ") # Clean up the version manifest file rm "$VERSION_MANIFEST_FILE" # Download the latest Minecraft server jar UPDATED_JAR="$MINECRAFT_HOME/minecraft_server.jar.update" if ! wget -q -O "$UPDATED_JAR" --no-check-certificate "$MC_SERVER_URL"; then dialog --title 'Minecraft Update' --msgbox "Minecraft update could not be downloaded." 0 0 exit 1 fi # Compare with the existing server jar if [ -f "$MINECRAFT_HOME/$MINECRAFT_JAR" ] && diff "$MINECRAFT_HOME/$MINECRAFT_JAR" "$UPDATED_JAR" > /dev/null; then dialog --title 'Minecraft Update' --msgbox "You are already running the latest version of $MINECRAFT_JAR." 0 0 rm "$UPDATED_JAR" else # Backup the old jar and update if [ -f "$MINECRAFT_HOME/$MINECRAFT_JAR" ]; then mv "$MINECRAFT_HOME/$MINECRAFT_JAR" "$MINECRAFT_HOME/$MINECRAFT_JAR.previous" fi mv "$UPDATED_JAR" "$MINECRAFT_HOME/$MINECRAFT_JAR" dialog --title 'Minecraft Update' --msgbox "Minecraft successfully updated." 0 0 fi } #:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: #:::::::::::::: :::::::::::::::::::::: #:::::::::::::: Script internal config repository :::::::::::::::::::::: #:::::::::::::: :::::::::::::::::::::: #:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: # This section contains content to written out to several configuration # files and external scripts. announce.py () { cat >> $1/announce.py <<-'EOF_PY' #!/bin/python3 # RTD System System Managment Script # ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: # ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: # :: Author: generated by minecraft.server # :: Version 1.02 # :: # :: # :: Purpose: The purpose of the script is to: # :: 1 - Broadcast service availability to the local network. # :: This script is used to broadcast the minecraft server on the # :: same machine (see "servers"). You may have as many minecraft # :: servers running and broadcast as you like. # ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: # ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: import socket import time import sys import os import urllib.request servers = [ ["Local Network - Minecraft Server", 25565], ] BROADCAST_IP = "255.255.255.255" BROADCAST_PORT = 4445 sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) print("Broadcasting Minecraft servers to LAN on: " + os.environ['NETINT']) print("Local IP adress: ") print((([ip for ip in socket.gethostbyname_ex(socket.gethostname())[2] if not ip.startswith("127.")] or [[(s.connect(("8.8.8.8", 53)), s.getsockname()[0], s.close()) for s in [socket.socket(socket.AF_INET, socket.SOCK_DGRAM)]][0][1]]) + ["no IP found"])[0]) print("External IP address visible on the internet: ") external_ip = urllib.request.urlopen('https://ident.me').read().decode('utf8') print(external_ip) while True: for server in servers: msg = "[MOTD]%s[/MOTD][AD]%d[/AD]" % (server[0], server[1]) encode = str.encode(msg) sock.sendto(encode, (BROADCAST_IP, BROADCAST_PORT)) time.sleep(3) EOF_PY } start.sh () { cat >> $1/start.sh <<-'EOF_SH' #!/bin/bash #:: RTD System System Managment Script #:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: #:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: #:: Author: generated by minecraft.server #:: Version 1.00 #:: #:: #:: Purpose: The purpose of the script is to: #:: 1 - Launch Minecraft #:: 2 - Check that Minecraft runs for at least one minute #:: 3 - If Minecraft crashes or quits after one minute, restart Minecraft #:: #:: #:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: #:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: pushd $(dirname "$0") MC_SERVER_NAME=`cat server.properties |grep "motd=.*" ` export $MC_SERVER_NAME while true; do start_epoch=$(date +%s) echo Starting Minecraft server $MC_SERVER_NAME .... ~/bin/java/bin/java -Xmx2048M -Xms2048M -jar $(dirname "$0")/server.jar noggui broadcast # Abort if the application exited too quickly duration=$(( $(date +%s) - $start_epoch )) if [[ "$duration" < 60 ]]; then echo "Program exited too quickly! Aborting Minecraft Launcher...." exit fi done exit EOF_SH } server.properties () { cat >> $1/server.properties <<-'EOF_CFG' # Minecraft server properties # generated by minecraft.server broadcast-rcon-to-ops=true view-distance=6 max-build-height=256 server-ip= rcon.port=25575 level-seed= allow-nether=true gamemode=survival enable-command-block=true server-port=25565 enable-rcon=false enable-query=true op-permission-level=3 prevent-proxy-connections=false generator-settings= resource-pack= player-idle-timeout=0 level-name=world rcon.password= motd=\u00A7d CuteWorld query.port=25565 force-gamemode=true debug=false hardcore=false white-list=false broadcast-console-to-ops=true pvp=true spawn-npcs=true spawn-animals=true generate-structures=true snooper-enabled=false difficulty=normal network-compression-threshold=256 level-type=default max-tick-time=-1 spawn-monsters=true enforce-whitelist=false max-players=10 use-native-transport=true spawn-protection=16 resource-pack-sha1= online-mode=true allow-flight=false max-world-size=29999984 EOF_CFG } eula.txt () { cat >> $1/eula.txt <<-'EOF_ULA' #By changing the setting below to TRUE you are indicating your agreement to our EULA (https://account.mojang.com/documents/minecraft_eula). #Sun Feb 26 16:57:58 CET 2017 eula=true EOF_ULA } } #:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: #:::::::::::::: :::::::::::::::::::::: #:::::::::::::: Script Executive :::::::::::::::::::::: #:::::::::::::: :::::::::::::::::::::: #:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: # Check that the software components of this script are available. # iff they are not, attempt to install them. for i in byobu speedometer htop python3 dialog do software::check_native_package_dependency $i done if echo "$@" | grep "update" ;then dialog --title "Update to latest Minecraft?" --yesno "You have requested that I try to get the latest verion of Minecraft from Mojang. This means that the people playing in the server also will need the latest version. Is this OK?" 0 0 case $? in 0) minecraft_update ;; 1) echo "Proceeding without update...";; 255) echo "ESC pressed.";; esac fi # Basic check to see that the Minecraft folder is present and # if not crate it and download the scripts and the server jar file # to enable the server. setup_minecraft_root check_java # Start the server and monitor the server performance find_active_newtork_interface run_minecraft_server_manager EOF ;; *) { write_error "No output file provided, use: ${FUNCNAME[0]} --write </path/to/filename>" return 1 } ;; esac } ########################################################################################## # __ # .' '. # : : # | _ _ | # .-.|(o)(o)|.-. _._ _._ # ( ( | .--. | ) ) .',_ '. .' _,'. # '-/ ( ) \-' / /' `\ \ __ / /' `\ \ # / '--' \ / / \.' './ \ \ # \ `"===="` / `-` : _ _ : `-` # `\ /' |(o)(o)| # `\ /' | | # /`-.-`\_ / \ # _..:;\._/V\_./:;.._ / .--. \ # .'/;:;:;\ /^\ /:;:;:\'. | ( ) | # / /;:;:;:;\| |/:;:;:;:\ \ _\ '--' /__ # / /;:;:;:;:;\_/:;:;:;:;:\ \ .' '-.__.-' `-. ########################################################################################## # .d8888b. d8b 888 # d88P Y88b Y8P 888 # Y88b. 888 # "Y888b. .d8888b 888d888 888 88888b. 888888 # "Y88b. d88P" 888P" 888 888 "88b 888 # "888 888 888 888 888 888 888 # Y88b d88P Y88b. 888 888 888 d88P Y88b. # "Y8888P" "Y8888P 888 888 88888P" "Y888 # 888 # 888 # 888 # 8888888 888 888 # 888 888 888 # 888 888 888 # 888 88888b. 888888 .d88b. 888d888 88888b. 8888b. 888 # 888 888 "88b 888 d8P Y8b 888P" 888 "88b "88b 888 # 888 888 888 888 88888888 888 888 888 .d888888 888 # 888 888 888 Y88b. Y8b. 888 888 888 888 888 888 # 8888888 888 888 "Y888 "Y8888 888 888 888 "Y888888 888 # # # # 8888888888 888 d8b # 888 888 Y8P # 888 888 # 8888888 888 888 88888b. .d8888b 888888 888 .d88b. 88888b. .d8888b # 888 888 888 888 "88b d88P" 888 888 d88""88b 888 "88b 88K # 888 888 888 888 888 888 888 888 888 888 888 888 "Y8888b. # 888 Y88b 888 888 888 Y88b. Y88b. 888 Y88..88P 888 888 X88 # 888 "Y88888 888 888 "Y8888P "Y888 888 "Y88P" 888 888 88888P' # ########################################################################################## term::set_colors() { # Description: Set colors for prompting on screen in human readable variables. These will be set globally # and can be can be used by in echo statements to modify the color of the message. # Examples: $YELLOW, $RED, $ENDCOLOR (reset), $GREEN, $BLUE # Usage: # Simply call this function by stating its name: # term::set_colors # # Globals: # Arguments: None # Outputs: Sets usable variables: $picachu $music $chess $YELLOW, $RED, $ENDCOLOR (reset), $GREEN, $BLUE # as well as: $[yellow, darkyellow, red, darkred, endcolor, green, darkgreen, blue, darkblue, cyan, darkcyan, gray, purple, darkpurple] # Returns: # Usage: echo -e $yellow "text to display" or echo -e $picachu # # "🔴 🔵 " # The function will not return any sucess or failure codes. It will do its best and exit. # # Num Colour #define R G B # 0 black COLOR_BLACK 0,0,0 # 1 red COLOR_RED 1,0,0 # 2 green COLOR_GREEN 0,1,0 # 3 yellow COLOR_YELLOW 1,1,0 # 4 blue COLOR_BLUE 0,0,1 # 5 magenta COLOR_MAGENTA 1,0,1 # 6 cyan COLOR_CYAN 0,1,1 # 7 white COLOR_WHITE 1,1,1 # End of documentation # Foreground & background colour commands: (uncomment to use) # # tput setab [1-7] # Set the background colour using ANSI escape # tput setaf [1-7] # Set the foreground colour using ANSI escape # # # Black 0;30 Dark Gray 1;30 # Red 0;31 Light Red 1;31 # Green 0;32 Light Green 1;32 # Brown/Orange 0;33 Yellow 1;33 # Blue 0;34 Light Blue 1;34 # Purple 0;35 Light Purple 1;35 # Cyan 0;36 Light Cyan 1;36 # Light Gray 0;37 White 1;37 # # # Text mode commands # # tput bold # Select bold mode # tput dim # Select dim (half-bright) mode # tput smul # Enable underline mode # tput rmul # Disable underline mode # tput rev # Turn on reverse video mode # tput smso # Enter standout (bold) mode # tput rmso # Exit standout mode # # Cursor movement commands # # tput cup Y X # Move cursor to screen postion X,Y (top left is 0,0) # tput cuf N # Move N characters forward (right) # tput cub N # Move N characters back (left) # tput cuu N # Move N lines up # tput ll # Move to last line, first column (if no cup) # tput sc # Save the cursor position # tput rc # Restore the cursor position # tput lines # Output the number of lines of the terminal # tput cols # Output the number of columns of the terminal # # Clear and insert commands # # tput ech N # Erase N characters # tput clear # Clear screen and move the cursor to 0,0 # tput el 1 # Clear to beginning of line # tput el # Clear to end of line # tput ed # Clear to end of screen # tput ich N # Insert N characters (moves rest of line forward!) # tput il N # Insert N lines # # Other commands # # tput sgr0 # Reset text format to the terminal's default # tput bel # Play a bell # # Reset Color_Off='\033[0m' # Text Reset # Regular Colors Black='\033[0;30m' # Black Red='\033[0;31m' # Red Green='\033[0;32m' # Green Yellow='\033[0;33m' # Yellow Blue='\033[0;34m' # Blue Purple='\033[0;35m' # Purple Cyan='\033[0;36m' # Cyan White='\033[0;37m' # White # Bold BBlack='\033[1;30m' # Black BRed='\033[1;31m' # Red BGreen='\033[1;32m' # Green BYellow='\033[1;33m' # Yellow BBlue='\033[1;34m' # Blue BPurple='\033[1;35m' # Purple BCyan='\033[1;36m' # Cyan BWhite='\033[1;37m' # White # Underline UBlack='\033[4;30m' # Black URed='\033[4;31m' # Red UGreen='\033[4;32m' # Green UYellow='\033[4;33m' # Yellow UBlue='\033[4;34m' # Blue UPurple='\033[4;35m' # Purple UCyan='\033[4;36m' # Cyan UWhite='\033[4;37m' # White # Background On_Black='\033[40m' # Black On_Red='\033[41m' # Red On_Green='\033[42m' # Green On_Yellow='\033[43m' # Yellow On_Blue='\033[44m' # Blue On_Purple='\033[45m' # Purple On_Cyan='\033[46m' # Cyan On_White='\033[47m' # White # High Intensity IBlack='\033[0;90m' # Black IRed='\033[0;91m' # Red IGreen='\033[0;92m' # Green IYellow='\033[0;93m' # Yellow IBlue='\033[0;94m' # Blue IPurple='\033[0;95m' # Purple ICyan='\033[0;96m' # Cyan IWhite='\033[0;97m' # White # Bold High Intensity BIBlack='\033[1;90m' # Black BIRed='\033[1;91m' # Red BIGreen='\033[1;92m' # Green BIYellow='\033[1;93m' # Yellow BIBlue='\033[1;94m' # Blue BIPurple='\033[1;95m' # Purple BICyan='\033[1;96m' # Cyan BIWhite='\033[1;97m' # White # High Intensity backgrounds On_IBlack='\033[0;100m' # Black On_IRed='\033[0;101m' # Red On_IGreen='\033[0;102m' # Green On_IYellow='\033[0;103m' # Yellow On_IBlue='\033[0;104m' # Blue On_IPurple='\033[0;105m' # Purple On_ICyan='\033[0;106m' # Cyan On_IWhite='\033[0;107m' # White local ecode="\033[" yellow="${ecode}1;33m" darkyellow="${ecode}0;33m" red="${ecode}1;31m" darkred="${ecode}0;31m" endcolor="${ecode}0m" green="${ecode}1;32m" darkgreen="${ecode}1;32m" blue="${ecode}1;34m" darkblue="${ecode}0;34m" cyan="${ecode}0;36" darkcyan="${ecode}0;36" gray="${ecode}0;37" purple="${ecode}1;35" darkpurple="${ecode}0;35" magenta="${ecode}1;35m" # Back compatability w. old scripts export YELLOW="$yellow" export RED="$red" export ENDCOLOR="$endcolor" export GREEN="$green" export BLUE="$blue" anim=( "${blue}•${green}•${red}•${magenta}• " " ${green}•${red}•${magenta}•${blue}• " " ${red}•${magenta}•${blue}•${green}• " " ${magenta}•${blue}•${green}•${red}• " " ${blue}•${green}•${red}•${magenta}•" ) echo -e "🎨 $yellow * $darkyellow * $red * $darkred * $green * $darkgreen * $blue * $darkblue * $cyan * $darkcyan * $gray * $darkgray * $purple * $darkpurple * $endcolor" } pause::for_input() { # Description: # A simple function to pause and wait for end user input. # The function expects a non zero (0) argument to execute the pause # and wait for the end user to press ENTER to continue execution. # # Globals: # Arguments: 0 - !0 "[error message]" # Outputs: Standard out # Returns: # Usage: suggestion stop on execution error: # EXPRESSION || pause::for_input 1 # or # EXPRESSION ; pause::for_input $? # # In both of these cases script execution will halt and wait for confirmation # before continuing. # # Calling the function with no parameters will cause the function to simply continue. # This is useful in scripts when using with a captured return variable: $? # # EXPRESSION ; pause::for_input $? # # in this case the script will continue if the EXPRESSION was successful, but will # pause if the EXPRESSION failed. # # End of documentation ERRMSG=$@ if [ ! $1 -eq 0 ]; then read -p "$ERRMSG: Press [ ENTER ] to continue:" fi } pause::a_given_time() { # Description: # Simple function to pause for a given number of seconds whle showing a count down # in seconds. If only a pause and no output is needed then it would be simpler to # use the 'pause' built in command. # # Globals: OPTIND # Arguments: -t -m -e where -t 'seconds' is mandatory. # Outputs: STDOUT # Returns: STDERR # Usage: [-t <seconds> ] [-e <word> *completion message* ] [-m <word> *start message* ] # # EXAMPLE: # # pause::a_given_time -t 10 -m Waiting... -e Done! # # End of documentation local OPTIND o a while getopts ':t:m:e:' OPTION; do case "$OPTION" in t) local _time="${OPTARG}" ;; e) local _end_message="${OPTARG}" ;; m) local _message="${OPTARG}" ;; ?) write_host --cyan "Usage: ${FUNCNAME[0]} [-t <seconds> ] [-e <word> *completion message* ] [-m <word> *start message* ]" return 1 ;; esac done unset OPTIND if [[ -z $_time ]]; then write_error "Specifying time is required! [-t <seconds> ] Optional arguments are: [-e <word> *completion message* ] [-m <word> *start message* ]" return 1 else write_status "$_message" while [ $_time -ge 1 ]; do echo -ne "One Moment please $_time ... \r" sleep 1 _time=$(($_time - 1)) done echo write_status "$_end_message" fi } write_host() { # Description: # This is a simplified and consistent way to write output to the screen. # This function will print a message to the standard out in a color specified. # Specified colors can be colord defined in the function "term::set_colors" # If no color option is specified this cunctio simply works like the echo command. # # Globals: # Arguments: [color option] # Outputs: # Returns: # Usage: write_host [option] [String] # # where [option] is a supported color: # --yellow Prints message in YELLOW # --darkyellow Prints message in DARK YELLOW # --red Prints message in RED # --darkred Prints message in DARK RED # --endcolor DEFAULT color as in terminal. # --green Prints message in GREEN # --darkgreen Prints message in DARK GREEN # --blue Prints message in BLUE # --darkblue Prints message in BLUE # --cyan Prints message in CYAN # --darkcyan Prints message in DARK CYAN # --gray Prints message in GRAY # --purple Prints message in PURPLE # --darkpurple Prints message in DARK PURPLE # # NOTE: this function uses the "tput" mechanisms to set colors, and gice a convenient # and simple uman way to access them. The target system must have "tput" aavailable. # # # End of documentation _option=$1 case ${_option} in --endcolor|--reset) color="$(tput sgr0)" ;; # Yellow --yellow|--Y|--Yellow) color="$(tput setaf 3)" ;; --Byellow|--BY|--BYellow|--yellow-bold) color="$(tput bold; tput setaf 3)" ;; --Dyellow|--DY|--DYellow|--yellow-dim) color="$(tput dim; tput setaf 3)" ;; # Red --red|--R|--Red) color="$(tput setaf 1)" ;; --Bred|--BR|--BRed|--red-bold) color="$(tput bold; tput setaf 1)" ;; --Dred|--DR|--DRed|--red-dim) color="$(tput dim; tput setaf 1)" ;; # Green --green|--G|--Green) color="$(tput setaf 2)" ;; --Bgreen|--BG|--BGreen|--green-bold) color="$(tput bold; tput setaf 2)" ;; --Dgreen|--DG|--DGreen|--green-dim) color="$(tput dim; tput setaf 2)" ;; # Blue --blue|--B|--Blue) color="$(tput setaf 4)" ;; --Bblue|--BB|--BBlue|--blue-bold) color="$(tput bold; tput setaf 4)" ;; --Dblue|--DB|--DBlue|--blue-dim) color="$(tput dim; tput setaf 4)" ;; # Cyan --cyan|--C|--Cyan) color="$(tput setaf 6)" ;; --Bcyan|--BC|--BCyan|--cyan-bold) color="$(tput bold; tput setaf 6)" ;; --Dcyan|--DC|--DCyan|--cyan-dim) color="$(tput dim; tput setaf 6)" ;; # Gray --gray|--grey|--Gr|--Gray|--Grey) color="$(tput setaf 7)" ;; --Bgray|--Bgrey|--BGy|--gray-bold) color="$(tput bold; tput setaf 7)" ;; --Dgray|--Dgrey|--DGy|--gray-dim) color="$(tput dim; tput setaf 7)" ;; # Purple --purple|--P|--Purple) color="$(tput setaf 5)" ;; --Bpurple|--BP|--BPurple|--purple-bold) color="$(tput bold; tput setaf 5)" ;; --Dpurple|--DP|--DPurple|--purple-dim) color="$(tput dim; tput setaf 5)" ;; *) local _text="$1" ;; esac [[ -z "${_text}" ]] && local _text="$(tput sgr0)${color} $2 $(tput sgr0)" echo -e "${_text} " } write_error() { # Description: # is a simplified and consistent way to write output to the screen. # write_error will print a message to the standard out in RED. # Globals: # Arguments: "text to be printed to standard out" # Outputs: standard out in color # Returns: # Usage: write_error "[String]" # # End of documentation local text=$1 if [[ -z "${text}" ]]; then return 0 fi if [[ "${TERMUITXT}" == "nocolor" ]]; then printf '💥 %s: %s\n' "${FUNCNAME[1]}" "${text}" else write_host --red "💥 ${FUNCNAME[1]}: ${text}" fi # Tell the loging function to log the message requested... [ -n "${text}" ] && system::log_item " 💥 ${FUNCNAME[1]}: ${text}" } write_warning() { # Description: # This is a simplified and consistent way to write output to the screen. # write_warning will print a message to the standard out in Yellow. # Globals: # Arguments: "text to be printed to standard out" # Outputs: standard out in color # Returns: # Usage: write_warning "[String]" # # End of documentation local text=$1 if [[ -z "${text}" ]]; then return 0 fi if [[ "${TERMUITXT}" == "nocolor" ]]; then printf '⚠ %s: %s\n' "${FUNCNAME[1]}" "${text}" else write_host --yellow "⚠ ${FUNCNAME[1]}: ${text}" fi # Tell the loging function to log the message requested... [ -n "${text}" ] && system::log_item " ⚠ ${FUNCNAME[1]}: ${text}" } write_status() { # Description: # This is a simplified and consistent way to write output to the screen. # write_status will print a message to the standard out in GREEN. # Globals: # Arguments: "text to be printed to standard out" # Outputs: standard out in color # Returns: # Usage: write_status "[String]" # # End of documentation local text=$1 if [[ -z "${text}" ]]; then return 0 fi if [[ "${TERMUITXT}" == "nocolor" ]]; then printf '✓ %s: %s\n' " ${FUNCNAME[1]}" "${text}" else write_host --green "✓ ${FUNCNAME[1]}: ${text}" fi # Tell the loging function to log the message requested... [ -n "${text}" ] && system::log_item " ✓ ${FUNCNAME[1]}: ${text}" } write_information() { # Description: # This is a simplified and consistent way to write output to the screen. # write_information will print a message to the standard out in BLUE. # Globals: # Arguments: "text to be printed to standard out" # Outputs: standard out in color # Returns: # Usage: write_information "[String]" # # End of documentation local text=$1 if [[ -z "${text}" ]]; then return 0 fi if [[ "${TERMUITXT}" == "nocolor" ]]; then printf '🛈 %s: %s\n' "${FUNCNAME[1]}" "${text}" else write_host --blue "🛈 ${FUNCNAME[1]}: ${text}" fi # Tell the loging function to log the message requested... system::log_item " 🛈 ${FUNCNAME[1]}: ${text}" } library::__cache_dir() { # Description: Resolve and create the cache directory used by RTD helper caches. # Globals: XDG_CACHE_HOME (r), HOME (r), TMPDIR (r) # Arguments: None # Outputs: Absolute path to the cache directory on STDOUT. # Returns: 0 on success; falls back to TMPDIR (/tmp) if mkdir fails. # Usage: cache_dir=$(library::__cache_dir) # End of documentation local cache_dir="${XDG_CACHE_HOME:-${HOME:-/tmp}/.cache}/rtd" mkdir -p "$cache_dir" 2>/dev/null || cache_dir="${TMPDIR:-/tmp}" echo "$cache_dir" } library::__desc_cache_file() { # Description: Return the cache file path used to persist function descriptions. # Globals: XDG_CACHE_HOME (r), HOME (r), TMPDIR (r) # Arguments: None # Outputs: Cache file path on STDOUT. # Returns: 0 # Usage: cache_file=$(library::__desc_cache_file) # End of documentation local cache_dir cache_dir=$(library::__cache_dir) local key key=$(printf '%s' "$0" | md5sum | awk '{print $1}') echo "${cache_dir}/desc-${key}.cache" } library::__load_desc_cache() { # Description: Load cached header/description maps when the cache is newer than this script. # Globals: __FUNC_HEADER_DESC (w), __FUNC_DESC_CACHE (w), __FUNC_HEADER_DESC_BUILT (w), __LIB_SCRIPT_MTIME (r/w) # Arguments: None # Outputs: None # Returns: 0 if cache loaded; 1 when cache is missing or stale. # Usage: library::__load_desc_cache # End of documentation local cache_file cache_file=$(library::__desc_cache_file) [[ -f "$cache_file" ]] || return 1 local script_mtime cache_mtime script_mtime=${__LIB_SCRIPT_MTIME:-$(stat -c %Y "$0" 2>/dev/null || date +%s)} cache_mtime=$(head -n1 "$cache_file" | sed 's/^#mtime://') [[ -n "$cache_mtime" && "$cache_mtime" -ge "$script_mtime" ]] || return 1 declare -gA __FUNC_HEADER_DESC declare -gA __FUNC_DESC_CACHE while IFS= read -r line; do case "$line" in \#header:*) line=${line#\#header:} if [[ "$line" =~ ^([^:]+):(.*)$ ]]; then __FUNC_HEADER_DESC["${BASH_REMATCH[1]}"]="${BASH_REMATCH[2]}" fi ;; \#desc:*) line=${line#\#desc:} if [[ "$line" =~ ^([^:]+):(.*)$ ]]; then __FUNC_DESC_CACHE["${BASH_REMATCH[1]}"]="${BASH_REMATCH[2]}" fi ;; esac done < <(tail -n +2 "$cache_file") __FUNC_HEADER_DESC_BUILT=1 return 0 } library::__save_desc_cache() { # Description: Persist header/description maps to disk for faster subsequent runs. # Globals: __FUNC_HEADER_DESC (r), __FUNC_DESC_CACHE (r), __LIB_SCRIPT_MTIME (r/w) # Arguments: None # Outputs: None (writes cache file on disk). # Returns: 0 on success; ignores write errors. # Usage: library::__save_desc_cache # End of documentation local cache_file script_mtime cache_file=$(library::__desc_cache_file) script_mtime=${__LIB_SCRIPT_MTIME:-$(stat -c %Y "$0" 2>/dev/null || date +%s)} { echo "#mtime:${script_mtime}" local key for key in "${!__FUNC_HEADER_DESC[@]}"; do printf '#header:%s:%s\n' "$key" "${__FUNC_HEADER_DESC[$key]}" done for key in "${!__FUNC_DESC_CACHE[@]}"; do printf '#desc:%s:%s\n' "$key" "${__FUNC_DESC_CACHE[$key]}" done } >"$cache_file" 2>/dev/null || true } library::__build_header_desc_map() { # Description: # Populate an associative array mapping function names -> header descriptions # from the "Functions defined in this library" table at the top of this file. # Globals: # __FUNC_HEADER_DESC (assoc w), __FUNC_HEADER_DESC_BUILT (w), __LIB_SCRIPT_MTIME (r/w) # Arguments: None # Outputs: None # Returns: 0 # Usage: # library::__build_header_desc_map # End of documentation if library::__load_desc_cache; then [[ "${__FUNC_HEADER_DESC_BUILT:-0}" -eq 1 ]] && return 0 fi declare -gA __FUNC_HEADER_DESC local header_re='^[[:space:]]*[0-9]+[[:space:]]+([^;]+);[[:space:]]*(.*)$' while IFS= read -r line; do if [[ "$line" =~ $header_re ]]; then local name="${BASH_REMATCH[1]}" local desc="${BASH_REMATCH[2]}" name="${name#"${name%%[![:space:]]*}"}" name="${name%"${name##*[![:space:]]}"}" __FUNC_HEADER_DESC["$name"]="$desc" fi done < <(awk '/^Functions defined/{flag=1;next} flag && /^[[:space:]]*[0-9]+[[:space:]]+[^;]+;/{print}' "$0") __FUNC_HEADER_DESC_BUILT=1 } library::__get_function_description() { # Description: # Resolve a one-line description for a function by first reading its inline # "Description:" comment, then falling back to the header function table. # Globals: # __FUNC_DESC_CACHE (assoc r/w), __FUNC_HEADER_DESC (assoc r/w), __LIB_SCRIPT_MTIME (r/w) # Arguments: # $1 - Function name (without parentheses). # Outputs: # Echoes the description string or a placeholder when missing. # Returns: # 0 (always). # Usage: # library::__get_function_description "software::add_native_package" # End of documentation declare -gA __FUNC_DESC_CACHE declare -gA __FUNC_HEADER_DESC library::__build_header_desc_map local func="$1" desc="" if [[ -n "${__FUNC_DESC_CACHE[$func]:-}" ]]; then echo "${__FUNC_DESC_CACHE[$func]}" return 0 fi # Prefer inline '# Description:' following the function definition. desc=$( awk -v fn="${func}()" ' $0 ~ "^"fn"[[:space:]]*{" {found=1; next} found && $0 ~ /^[[:space:]]*#/ { if (match($0, /[Dd]escription:[[:space:]]*(.*)/, m)) { print m[1]; exit } } found && $0 !~ /^[[:space:]]*#/ { exit } ' "$0" ) # Fallback: look in the header function table (lines with index ; description). if [[ -z "$desc" ]]; then desc=$( awk -v fn="$func" ' /^[[:space:]]*[0-9]+[[:space:]]+[^;]+;/{ s=$0 if (match(s, /^[[:space:]]*[0-9]+[[:space:]]+([^;]+);[[:space:]]*(.*)$/, m)) { name=m[1]; gsub(/^[[:space:]]+|[[:space:]]+$/, "", name) if (name == fn) { print m[2]; exit } } } ' "$0" ) fi __FUNC_DESC_CACHE["$func"]="${desc:-<no description found>}" echo "${__FUNC_DESC_CACHE[$func]}" library::__save_desc_cache } library::__zformat_cache_file() { # Description: Return the cache file path for cached --zformat output. # Globals: XDG_CACHE_HOME (r), HOME (r), TMPDIR (r) # Arguments: None # Outputs: Cache file path on STDOUT. # Returns: 0 # Usage: cache_file=$(library::__zformat_cache_file) # End of documentation local cache_dir cache_dir=$(library::__cache_dir) local key key=$(printf '%s' "$0" | md5sum | awk '{print $1}') echo "${cache_dir}/zformat-${key}.cache" } library::__load_zformat_cache() { # Description: Emit cached --zformat lines when the cache is fresh. # Globals: __LIB_SCRIPT_MTIME (r/w) # Arguments: None # Outputs: Cached zformat lines to STDOUT. # Returns: 0 if cache loaded; 1 if cache missing/stale. # Usage: library::__load_zformat_cache # End of documentation local cache_file script_mtime cache_mtime cache_file=$(library::__zformat_cache_file) [[ -f "$cache_file" ]] || return 1 script_mtime=${__LIB_SCRIPT_MTIME:-$(stat -c %Y "$0" 2>/dev/null || date +%s)} cache_mtime=$(head -n1 "$cache_file" | sed 's/^#mtime://') [[ -n "$cache_mtime" && "$cache_mtime" -ge "$script_mtime" ]] || return 1 tail -n +2 "$cache_file" return 0 } library::__save_zformat_cache() { # Description: Persist the most recent --zformat lines to disk for reuse. # Globals: __RTD_DEVHELP_ZFORMAT (r), __LIB_SCRIPT_MTIME (r/w) # Arguments: None # Outputs: None (writes cache file). # Returns: 0 on success; ignores write errors. # Usage: library::__save_zformat_cache # End of documentation local cache_file script_mtime cache_file=$(library::__zformat_cache_file) script_mtime=${__LIB_SCRIPT_MTIME:-$(stat -c %Y "$0" 2>/dev/null || date +%s)} { echo "#mtime:${script_mtime}" printf "%s\n" "${__RTD_DEVHELP_ZFORMAT[@]}" } >"$cache_file" 2>/dev/null || true } library::list_loaded_internal_functions() { # Description: # Function to list all internal library functions currently loaded. This function can be used # for debugging, support, and documentation purposes. It allows for different output formats # based on the input argument. This function is used to facilitate managing this library by # helping to output documentation or providinig a list of loaded functions for information. # # Globals: # __RTD_DEVHELP_LIST (array scratch, w) # __RTD_DEVHELP_ZFORMAT (array scratch, w) # __RTD_DEVHELP_LIST_BUILT (flag, w) # __LIB_SCRIPT_MTIME (r/w) # # Arguments: # $1 - Optional format flag. Supported flags are: # --zformat : Outputs the list formatted for use with Zenity dialogs. # --yformat : Outputs the list formatted for use with Yad dialogs stored in the functions_list array. # --make-doc : Outputs the list as a plain-text formatted documentation. # --hformat : Outputs the list with a header format suitable for inline documentation. # (No flag) : Outputs the list in a basic numbered format. # # Outputs: # Standard output is used to return the list of functions. The format of the output depends # on the provided format flag. # # Returns: # None (function outputs data to standard output) # # Usage: # library::list_loaded_internal_functions # Outputs the list in a basic numbered format. # library::list_loaded_internal_functions --zformat # Outputs the list in Zenity format. # library::list_loaded_internal_functions --yformat # Outputs the list in Yad format stored in the functions_list array. # library::list_loaded_internal_functions --make-doc # Outputs the list in plain-text documentation format. # library::list_loaded_internal_functions --hformat # Outputs the list in header format. # # End of documentation # For zformat in a fresh subshell, try on-disk cache first (fastest). if [[ "$1" == "--zformat" && -z "${__RTD_DEVHELP_LIST_BUILT:-}" ]]; then if library::__load_zformat_cache; then return 0 fi fi # Cache script mtime once for all cache helpers. : "${__LIB_SCRIPT_MTIME:=$(stat -c %Y "$0" 2>/dev/null || date +%s)}" # Fast-path: reuse previously built menu entries within this shell session. if [[ -n "${__RTD_DEVHELP_LIST_BUILT:-}" && ${#__RTD_DEVHELP_LIST[@]:-0} -gt 0 ]]; then case "$1" in --zformat) printf '%s\n' "${__RTD_DEVHELP_ZFORMAT[@]}" return 0 ;; --yformat) functions_list=("${__RTD_DEVHELP_LIST[@]}") return 0 ;; --make-doc) for ((i=0; i<${#__RTD_DEVHELP_LIST[@]}; i+=3)); do printf "%-5s %-50s ; %s\n" "${__RTD_DEVHELP_LIST[i]}" "${__RTD_DEVHELP_LIST[i+1]}" "${__RTD_DEVHELP_LIST[i+2]}" done return 0 ;; --hformat) for ((i=0; i<${#__RTD_DEVHELP_LIST[@]}; i+=3)); do echo "#${__RTD_DEVHELP_LIST[i]}: ${__RTD_DEVHELP_LIST[i+1]} " done return 0 ;; esac fi __RTD_DEVHELP_ZFORMAT=() if [[ "$1" == "--zformat" ]]; then i=1 for index in $(declare -F | awk '{print $NF}' | sort | egrep -v "^_" | grep -v "^library::" | grep -v "recipe:_" | grep -v "Single_Install"); do Description=$(library::__get_function_description "${index}") __RTD_DEVHELP_LIST+=("$i" "${index}" "${Description}") __RTD_DEVHELP_ZFORMAT+=("$i" "${index}" "${Description}") echo -e "$i \n ${index} \n ${Description} \n" ((i = i + 1)) done elif [[ "$1" == "--yformat" ]]; then functions_list=() i=1 for index in $(declare -F | awk '{print $NF}' | sort | grep -E -v "^_" | grep -v "^library::" | grep -v "recipe:_" | grep -v "Single_Install"); do Description=$(library::__get_function_description "${index}" | sed 's/^ //g') # Trim leading spaces # Add the elements to the array functions_list+=("$i" "${index}" "${Description}") __RTD_DEVHELP_LIST+=("$i" "${index}" "${Description}") ((i = i + 1)) done elif [[ "${1}" == "--make-doc" ]]; then i=1 for index in $(declare -F | awk '{print $NF}' | sort | egrep -v "^_" | grep -v "^library::" | grep -v "recipe:_" | grep -v "Single_Install"); do Description=$(library::__get_function_description "${index}") printf "%-5s %-50s ; %s\n" "${i}" "${index}" "${Description}" __RTD_DEVHELP_LIST+=("$i" "${index}" "${Description}") ((i = i + 1)) done elif [[ "$1" == "--hformat" ]]; then i=1 for index in $(declare -F | awk '{print $NF}' | sort | egrep -v "^_" | grep -v "^library::" | grep -v "recipe:_" | grep -v "Single_Install"); do echo "#$i: $index " __RTD_DEVHELP_LIST+=("$i" "${index}" "$(library::__get_function_description "${index}")") ((i = i + 1)) done else i=1 for index in $(declare -F | awk '{print $NF}' | sort | egrep -v "^_" | grep -v "^library::" | grep -v "recipe_" | grep -v "Single_Install"); do echo "$index #$i " __RTD_DEVHELP_LIST+=("$i" "${index}" "$(library::__get_function_description "${index}")") ((i = i + 1)) done fi __RTD_DEVHELP_LIST_BUILT=1 [[ ${#__RTD_DEVHELP_ZFORMAT[@]} -gt 0 ]] && library::__save_zformat_cache } library::check_status() { # Description: # Function to check how the library was loaded (sourced or executed directly) # Globals: # $sourced - Indicates if the script was sourced (0) or executed directly (1). # Arguments: # None # Outputs: # Standard output indicating the load status of the library. # Returns: # 0 if sourced, 1 if executed directly. # Usage: # # if (return 0 2>/dev/null); then # sourced=0 # else # sourced=1 # echo clear >/dev/null # fi # library::check_status # # End of documentation if [[ $sourced -eq 0 ]]; then if [[ "$(basename $0)" == "bash" ]]; then write_host --cyan "Script is sourced from bash in a terminal: $0 $*" else write_host --cyan "Script is being sourced from a script: $(basename $0) : |${FUNCNAME[3]}: $*" fi else write_host --yellow "Library is run directly as an executable! This does nothing by itself." write_host --cyan "Use \"bash $0 --help\" to see how to use this library." fi write_status "📚 RTD Power Library $RTD_VERSION loaded OK" export RTDFUNCTIONS="${RTD_VERSION}" # log variables for support reasons... system::log_item "Library Loaded Variables: ......................................... RTD Functions Version: ${RTDFUNCTIONS} ......................................... $(library::envcheck_report) ......................................... " system::log_item " *** : $(basename ${BASH_SOURCE[0]}) loaded by $(basename "${0}") on $HOSTNAME : ***" system::log_item " *** : executed by $SUDO_USER as $USER : ***" system::log_item "💻 Basic System Information: $(hostnamectl)" } library::apply_rtd_defaults() { # Description: Apply sane default values to core RTD paths and URLs using precedence. # Existing environment or sourced *.info values win; hardcoded defaults fill gaps. # Arguments: None. # Globals: Mutates *_DIR, *_tla, GIT_* vars when unset. # Returns: 0 # End of documentation local _dep="" for _dep in "${_OEM_dependencies[@]}"; do dependency::file "${_dep}" && system::log_item "✅ ${_dep} Loaded without errors..." || system::log_item "👾 ${_dep} Loaded with errors..." done : "${_TLA:="RTD"}" : "${_tla:="${_TLA,,}"}" local user_home_default="/home/${SUDO_USER:-$USER}" : "${_USER_HOME_DIR:="${user_home_default}"}" : "${_USER_CONFIG_DIR:="${_USER_HOME_DIR}/.config/${_tla}"}" local _git_profile_default="${_GIT_PROFILE:-${GIT_Profile:-vonschutter}}" : "${_GIT_PROFILE:="${_git_profile_default}"}" : "${GIT_Profile:="${_git_profile_default}"}" : "${GIT_Theme_URL:="https://github.com/${_git_profile_default}/RTD-Themes.git"}" : "${GIT_RTD_SETUP_BASE_URL:="https://github.com/${_git_profile_default}/RTD-Setup/"}" : "${GIT_RTD_SRC_URL:="https://github.com/${_git_profile_default}/RTD-Setup.git"}" : "${_OEM_DIR:="/opt/${_tla}"}" : "${_CACHE_DIR:="${_OEM_DIR}/cache"}" : "${_WALLPAPER_DIR:="${_OEM_DIR}/wallpaper"}" : "${_CUSTOM_SOUND_DIR:="${_OEM_DIR}/sound"}" : "${_THEME_DIR:="${_OEM_DIR}/themes"}" : "${_APP_DIR:="${_OEM_DIR}/apps"}" : "${_MODS_DIR:="${_OEM_DIR}/modules"}" : "${_CORE_DIR:="${_OEM_DIR}/core"}" : "${_APPS_DIR:="${_MODS_DIR}/oem-app-runner.mod/apps"}" : "${_LOG_DIR:="/var/log/${_tla}"}" : "${_CONFIG_DIR:="/etc/${_tla}"}" : "${_OEM_TEST_IPS:="8.8.8.8 1.1.1.1 9.9.9.9"}" : "${_XDG_WALLPAPER_DIR:="/usr/share/wallpapers"}" : "${VM_BUILD_TARGET:="/mnt/vmdsk/VM_IMAGES"}" : "${VM_BACKUP_TARGET:="/mnt/vmdsk/VM_BACKUP"}" : "${INITIAL_VM_DISK_ALLOC_GB:=32}" : "${DEFAULT_VM_MEMORY_MB:=2048}" : "${DEFAULT_VM_CPU_COUNT:=2}" : "${DEFAULT_VM_DISK_SIZE_GB:=100}" : "${_TSHIRT_CPU_SMALL:=2}" : "${_TSHIRT_MEM_SMALL:=2048}" : "${_TSHIRT_DSK_SMALL:=100}" : "${_TSHIRT_CPU_MEDIUM:=4}" : "${_TSHIRT_MEM_MEDIUM:=4096}" : "${_TSHIRT_DSK_MEDIUM:=100}" : "${_TSHIRT_CPU_LARGE:=8}" : "${_TSHIRT_MEM_LARGE:=8192}" : "${_TSHIRT_DSK_LARGE:=100}" : "${_TSHIRT_CPU_XL:=16}" : "${_TSHIRT_MEM_XL:=16384}" : "${_TSHIRT_DSK_XL:=100}" : "${_TSHIRT_CPU_XXL:=32}" : "${_TSHIRT_MEM_XXL:=32768}" : "${_TSHIRT_DSK_XXL:=100}" : "${NON_WESTERN_FONTS_LIST:="fonts-kacst fonts-kacst-one fonts-khmeros-core fonts-lklug-sinhala fonts-lohit-guru fonts-guru \ fonts-nanum fonts-noto-cjk fonts-takao-pgothic fonts-tibetan-machine fonts-guru-extra fonts-lao fonts-sil-padauk \ fonts-sil-abyssinica fonts-tlwg-* fonts-beng fonts-beng-extra fonts-deva fonts-deva-extra fonts-gubbi fonts-gujr \ fonts-gujr-extra fonts-kalapi fonts-knda fonts-lohit-* fonts-orya* fonts-pagul fonts-sahadeva fonts-samyak-* \ fonts-sarai fonts-smc fonts-smc-* fonts-taml fonts-telu fonts-telu-extra"}" export _TLA _tla _USER_HOME_DIR _USER_CONFIG_DIR _OEM_DIR _CACHE_DIR _WALLPAPER_DIR _CUSTOM_SOUND_DIR \ _THEME_DIR _APP_DIR _MODS_DIR _CORE_DIR _APPS_DIR _LOG_DIR _CONFIG_DIR _OEM_TEST_IPS _XDG_WALLPAPER_DIR \ GIT_Profile _GIT_PROFILE GIT_Theme_URL GIT_RTD_SETUP_BASE_URL GIT_RTD_SRC_URL VM_BUILD_TARGET VM_BACKUP_TARGET \ INITIAL_VM_DISK_ALLOC_GB DEFAULT_VM_MEMORY_MB DEFAULT_VM_CPU_COUNT DEFAULT_VM_DISK_SIZE_GB \ _TSHIRT_CPU_SMALL _TSHIRT_MEM_SMALL _TSHIRT_DSK_SMALL _TSHIRT_CPU_MEDIUM _TSHIRT_MEM_MEDIUM _TSHIRT_DSK_MEDIUM \ _TSHIRT_CPU_LARGE _TSHIRT_MEM_LARGE _TSHIRT_DSK_LARGE _TSHIRT_CPU_XL _TSHIRT_MEM_XL _TSHIRT_DSK_XL \ _TSHIRT_CPU_XXL _TSHIRT_MEM_XXL _TSHIRT_DSK_XXL NON_WESTERN_FONTS_LIST return 0 } library::envcheck_report() { # Display how and where the library was loaded for support/debugging. local load_mode loader source_path working_dir log_location caller_context source_path="${BASH_SOURCE[0]:-$0}" working_dir="${PWD}" log_location="${_LOGFILE:-unset}" if [[ $sourced -eq 0 ]]; then load_mode="sourced" if [[ "$(basename "$0")" == "bash" ]]; then loader="bash (interactive terminal)" else loader="script ${0##*/}" fi caller_context="${FUNCNAME[3]:-}" else load_mode="executed" loader="${0##*/}" fi if [[ -n "${caller_context}" && "${caller_context}" != "main" ]]; then loader+=" (caller: ${caller_context})" fi write_information "Library file : ${source_path}" write_information "Load mode : ${load_mode}" write_information "Invoker : ${loader}" write_information "Working directory : ${working_dir}" write_information "Log file : ${log_location}" system::log_item "envcheck: mode=${load_mode} invoker=${loader} source=${source_path} pwd=${working_dir} log=${log_location}" } library::list_loaded_software_functions() { # Description: Function to list all software (_rtd_recipes) functions loaded... for debugging/support purposes. # Globals: # Arguments: none or [--nonum] # Outputs: Standard out, list of functions either numerated or not. # Returns: # Usage: library::list_loaded_software_functions [] [--nonum] # End of documentation if [[ "$1" == "--nonum" ]]; then for index in $(declare -F | awk '{print $NF}' | sort | egrep -v "^_" | grep "recipe:_"); do echo false ${index/recipe:_/} done else i=1 for index in $(declare -F | awk '{print $NF}' | sort | egrep -v "^_" | grep "recipe:_"); do echo "$i ${index/recipe:_/}" ((i = i + 1)) done fi } #:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: #:::::::::::::: :::::::::::::::::::::: #:::::::::::::: Script Executive :::::::::::::::::::::: #:::::::::::::: :::::::::::::::::::::: #:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: # The following section launches the internal functions if given arguments # request this. If no arguments are provided, then all the functions in this # library will simply be loaded in to memory. # Load base color scheme for use in the library... term::set_colors # Find and load all dependent files for this library... library::apply_rtd_defaults # Determine logfile location... _LOGFILE="$(system::determine_logfile)" # Check if library is loaded from a terminal or another script (behavior may differ)... if (return 0 2>/dev/null); then sourced=0 else sourced=1 echo clear >/dev/null fi library::check_status main() { case $1 in --help | -?) clear echo -e " 📚 ${UWhite}${0##*/} ${red}::${BICyan} RunTime Data Library HELP ${endcolor}${red}${UWhite} ::${endcolor} ${blue}Usage${red}:${endcolor} ${IYellow}source ${IGreen}${0##*/} ${endcolor}(load the library into the current shell) ${blue}Alternate usage${red}:${endcolor} ${green}"${0##*/}"${endcolor} [OPTIONS] ${blue}Valid options are${red}:${endcolor} --help ${red}: ${endcolor}Show this help text --list [software|internal|all] ${blue}Where${red}:${endcolor} --list software ${red}: ${endcolor}List available software bundles --list internal ${red}: ${endcolor}List internal utility functions --list internal package ${red}: ${endcolor}Filter internal functions by package name (use with --list internal|all) --list internal make-doc${red}: ${endcolor}Print inline documentation for every function --list all ${red}: ${endcolor}List software bundles together with internal functions --devhelp ${red}: ${endcolor}Display developer help in the terminal --devhelp-gtk ${red}: ${endcolor}Display developer help in a GTK window --clean ${red}: ${endcolor}Remove library variables from the current environment --version ${red}: ${endcolor}Show detailed version information --V ${red}: ${endcolor}Show only the version number --envcheck ${red}: ${endcolor}Report how and where the library was loaded --selfcheck ${red}: ${endcolor}Run shellcheck to look for syntax issues ${blue}EXAMPLES${red}:${endcolor} ${0##*/} --list internal ${0##*/} --list internal kvm:: ${0##*/} --list all ${0##*/} --devhelp ${0##*/} --devhelp-gtk" return ;; --list) case $2 in software) library::list_loaded_software_functions ;; internal) case $3 in make-doc) library::list_loaded_internal_functions --make-doc ;; *) echo -e $YELLOW "INTERNAL Functions: $ENDCOLOR" echo ".........................................." echo "Number Function" echo ".........................................." library::list_loaded_internal_functions --hformat | grep "${3:-""}" ;; esac ;; all) (echo -e $YELLOW "Software Recipes: $ENDCOLOR" && library::list_loaded_software_functions && echo -e $YELLOW "INTERNAL Functions: $ENDCOLOR" && library::list_loaded_internal_functions) ;; *) echo "USAGE: The --list option requires a (what option). Valid option are: software, internal, all." ;; esac return ;; --devhelp) write_status "Reading library to build the menu list once (reused on return from child dialogs)" write_information "Please be patient, since this could take some time..." DisplayList=$(library::list_loaded_internal_functions) write_status "Library read OK, launching dialog now..." while true; do exec 3>&1 selection=$(dialog --clear --no-collapse --backtitle "${BRANDING:-" RunTime Data : Function library : Script Development Support"}" --title "RTD Library Loaded Functions:" --menu "\n Chose a function below to see more details about the function." "${HEIGHT:-0}" "${WIDTH:-90}" "${LIST_HEIGHT:-0}" ${DisplayList} 2>&1 1>&3) exit_status=$? exec 3>&- case $exit_status in 1) clear && echo [Cancel] && exit ;; 255) clear && echo [ESC] && exit ;; esac case $selection in $selection) result=$(while IFS= read -r 'line'; do [[ "$line" == "$selection() {" ]] && printline="yes" [[ "$line" == "$selection() { " ]] && printline="yes" [[ "$line" == "# End of documentation"* ]] && printline="no" [[ "$line" == *"# End of documentation"* ]] && printline="no" [[ "$line" == *End*of*Documentation* ]] && printline="no" [[ "$printline" == "yes" ]] && echo "$line" | grep -E '(^|\s*)#\s*' | sed 's/#//' done <$0) dialog --clear --no-collapse --backtitle "${0##*/} :: RunTime Data Library HELP ::" --title "RTD Functon: $selection" --msgbox "$result" "${HEIGHT:-25}" "${WIDTH:-110}" ;; *) exit ;; esac done return ;; --devhelp-gui) # Build once for reuse; functions_list is consumed by yad. library::list_loaded_internal_functions --yformat touch /tmp/yad_doc.txt : >/tmp/yad_doc.txt YAD_KEY=$$ function display_documentation() { local selected_function="$1" local script_source="${BASH_SOURCE[0]:-$0}" local printline="" local result="" selected_function="${selected_function%%|*}" result=$( while read -r line; do if [[ "$line" == "${selected_function}() {"* || "$line" == "${selected_function}()"* ]]; then printline="yes" fi if [[ "$printline" == "yes" ]]; then local _req_success=true if [[ "$line" == "# End of documentation"* || "$line" == *"# End of documentation"* || "$line" == *End*of*Documentation* ]]; then printline="no" else echo "$line" | grep -E '(^|\s*)#\s*' | sed 's/#//' fi fi done <"${script_source}" ) if [ -z "$result" ]; then result="No documentation found for ${selected_function}." fi echo "$result" >/tmp/yad_doc.txt } while true; do library::list_loaded_internal_functions --yformat yad --paned --title="Select a function below to see the function's documentation and usage:" \ --key=$YAD_KEY --orient=hor --width="${ZWIDTH:-1200}" --height="${ZHEIGHT:-600}" --splitter=300 --listen --button="Quit:1" --button="Refresh:0" & YAD_PID=$! yad --text-info --text="Documentation:" --no-markup --tail --filename=/tmp/yad_doc.txt --plug=$YAD_KEY --tabnum=2 --margins=10 --wrap --no-buttons & yad --list --no-markup --title="Select Function" --text="Functions:" --column="Number" --column="Function" --column="Description" \ --center --print-column=2 --separator='|' --listen --plug=$YAD_KEY --tabnum=1 \ "${functions_list[@]}" | while IFS='|' read -r selected_function _; do if [ -z "$selected_function" ]; then echo "No function selected, continuing..." continue fi display_documentation "$selected_function" done & wait $YAD_PID exit_status=$? if [ $exit_status -eq 1 ] || [ $exit_status -eq 252 ]; then pkill yad break fi done return ;; --devhelp-gtk) write_status "Reading library to build the menu list once (reused on return from child dialogs)" write_information "Please be patient, since this could take some time..." DisplayList=$(library::list_loaded_internal_functions --zformat) write_status "Library read OK, launching dialog now..." while true; do exec 3>&1 IFS_SAV=$IFS IFS=$(echo -en "\n\b") local text="This library contains many functions that may be used in any script that sources it. This list is searchable, simply start typing to filter. \n Select a function below to see the function's documentation and usage:" local title="RunTime Data Library Fuction Documentation Viewer" selection=$(zenity --list --modal --title=$title --text=$text --height="${ZHEIGHT:-600}" --width="${ZWIDTH:-1000}" --print-column=2 --column="Number" --column="Function" --column="Description" ${DisplayList} 2>/dev/null) exit_status=$? IFS=$IFS_SAV exec 3>&- case $exit_status in 1) clear && echo [Cancel] && exit ;; 255) clear && echo [ESC] && exit ;; esac export selection case "$selection" in "$selection") system::log_item "Selection = [${selection:1:-1}]" result=$(while read -r line; do # Flip the printline flag if the function is found... if [[ "$line" == "${selection:1:-1}() {"* || "$line" == "${selection:1:-1}()"* || "$line" == "${selection}()"* ]]; then system::log_item "Found function definition: $line" printline="yes" fi # If the printline flag is set, print the line... if [[ "$printline" == "yes" ]]; then # Flip the sucess flag if the function was found... local _req_sucess=true if [[ "$line" == "# End of documentation"* || "$line" == *"# End of documentation"* || "$line" == *End*of*Documentation* ]]; then # If the end of documentation is found, stop printing... system::log_item "End of Documentation found: $line" printline="no" else # Print the line... echo "$line" | grep -E '(^|\s*)#\s*' | sed 's/#//' fi fi done <"${0}") if [[ ! "$_req_sucess" == "true" ]]; then # If the function is not found, log it... system::log_item "Selection not found: [${selection}]: $line" unset _req_sucess fi system::log_item "Displaying [${selection}] in a [${ZHEIGHT:-600}]x[${ZWIDTH:-1024}]: \n $result" echo -e "$result" | zenity --text-info --font=monospace --modal --ok-label=BACK --title "Function Documentation: [${selection}]" --height="${ZHEIGHT:-600}" --width="${ZWIDTH:-1024}" 2>/dev/null unset result ;; *) # Trap unknown exit code... exit ;; esac done return ;; --clean) # Clean up the environment just in case... for var in $(compgen -v | grep '^_'); do unset "$var" done write_host --cyan "Environment cleaned up!" return 0 ;; --version) write_host --cyan "RTD Library Version: $RTD_VERSION" return 0 ;; --envcheck) library::envcheck_report "$@" return 0 ;; --selfcheck) write_host --cyan "Checking library syntax..." if dependency::command_exists "shellcheck"; then if shellcheck -S error $0; then write_host --cyan "Library sytax OK!" else write_host --red "Library has a syntax error!" fi else write_host --red "shellcheck command not found! Please install it to use this function." fi ;; --V | --v | --version) echo "$RTD_VERSION" return 0 ;; *) write_host --cyan "Unknown option! Please use --help for valid options." write_host --cyan "CALL: ${0##*/} $*" return 0 ;; esac } # If the library is asked to do something, evaluate the parameters... if [[ -n $1 ]]; then main $* fi