Add initial install_station Python package structure
- Add install_station package with __init__.py and core modules - Include boot_manager.py for boot loader configuration (BootManager class with singleton pattern) - Add common.py with password strength validation utilities and ZFS dataset definitions - Add custom.py with Partitions class for disk partitioning management (1010 lines) - Establish foundation for GTK+ based GhostBSD installer application Modules added: - boot_manager: UEFI/BIOS boot manager selection with rEFInd and FreeBSD options - common: Password validation functions and deprecated decorator utility - custom: Comprehensive partition management with GTK+ interface
This commit is contained in:
@@ -0,0 +1,218 @@
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gtk, Gdk
|
||||
from install_station.partition import bios_or_uefi
|
||||
from install_station.data import InstallationData
|
||||
import gettext
|
||||
|
||||
gettext.bindtextdomain('install-station', '/usr/local/share/locale')
|
||||
gettext.textdomain('install-station')
|
||||
_ = gettext.gettext
|
||||
|
||||
cssProvider = Gtk.CssProvider()
|
||||
cssProvider.load_from_path('/usr/local/lib/install-station/ghostbsd-style.css')
|
||||
screen = Gdk.Screen.get_default()
|
||||
styleContext = Gtk.StyleContext()
|
||||
styleContext.add_provider_for_screen(
|
||||
screen,
|
||||
cssProvider,
|
||||
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION
|
||||
)
|
||||
|
||||
|
||||
class BootManager:
|
||||
"""
|
||||
Utility class for managing boot manager selection in GhostBSD installation following the utility class pattern.
|
||||
|
||||
This class provides a GTK+ interface for selecting and configuring boot managers
|
||||
including rEFInd, FreeBSD boot manager, and native UEFI/BIOS loaders. The class
|
||||
automatically determines available options based on the partition scheme (GPT/MBR)
|
||||
and firmware type (UEFI/BIOS).
|
||||
|
||||
Available boot manager options:
|
||||
- rEFInd: Available only for GPT + UEFI configurations
|
||||
- FreeBSD boot manager: Available only for MBR partition schemes
|
||||
- Native loader: Default option, always available (UEFI or BIOS loader)
|
||||
|
||||
The class follows a utility pattern with class methods and variables for state management,
|
||||
designed to integrate with the InstallationData system for configuration persistence.
|
||||
"""
|
||||
# Class variables for state management
|
||||
boot = None
|
||||
vbox1 = None
|
||||
|
||||
# UI elements as class variables
|
||||
refind = None
|
||||
bsd = None
|
||||
none = None
|
||||
box3 = None
|
||||
|
||||
@classmethod
|
||||
def get_model(cls):
|
||||
"""
|
||||
Return the GTK widget model for the boot manager interface.
|
||||
|
||||
Creates and initializes the UI if it doesn't exist yet.
|
||||
|
||||
Returns:
|
||||
Gtk.Box: The main container widget for the boot manager interface
|
||||
"""
|
||||
if cls.vbox1 is None:
|
||||
cls.initialize()
|
||||
return cls.vbox1
|
||||
|
||||
@classmethod
|
||||
def boot_manager_selection(cls, _radiobutton, val: str):
|
||||
"""
|
||||
Handle boot manager selection from radio buttons.
|
||||
|
||||
Called when a radio button is toggled to update the selected boot manager
|
||||
option and store it in both the class variable and InstallationData.
|
||||
|
||||
Args:
|
||||
_radiobutton: The radio button widget that was toggled (unused)
|
||||
val: The boot manager value ('refind', 'bsd', or 'none')
|
||||
"""
|
||||
cls.boot = val
|
||||
InstallationData.boot = cls.boot
|
||||
|
||||
@classmethod
|
||||
def get_boot_manager_option(cls):
|
||||
"""
|
||||
Get the currently selected boot manager option.
|
||||
|
||||
Returns the boot manager value from InstallationData if available,
|
||||
otherwise falls back to the class variable.
|
||||
|
||||
Returns:
|
||||
str: The selected boot manager ('refind', 'bsd', or 'none')
|
||||
"""
|
||||
return InstallationData.boot or cls.boot
|
||||
|
||||
@classmethod
|
||||
def initialize(cls):
|
||||
"""
|
||||
Initialize the boot manager user interface following the utility class pattern.
|
||||
|
||||
Creates the GTK+ interface for boot manager selection including:
|
||||
- Title header ("Boot Option")
|
||||
- Radio button group for boot manager options
|
||||
- Automatic option availability based on partition scheme and firmware type
|
||||
- Default selection of native loader option
|
||||
|
||||
The interface adapts based on:
|
||||
- Firmware type (UEFI/BIOS) detected from system
|
||||
- Partition scheme (GPT/MBR) from installation configuration
|
||||
- rEFInd: Only available for GPT + UEFI
|
||||
- FreeBSD boot manager: Only available for MBR
|
||||
- Native loader: Always available as default
|
||||
|
||||
This method is called automatically by get_model() when the interface is first accessed.
|
||||
"""
|
||||
cls.vbox1 = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, homogeneous=False, spacing=0)
|
||||
cls.vbox1.show()
|
||||
|
||||
# Determine firmware type
|
||||
if bios_or_uefi() == "UEFI":
|
||||
loader = "UEFI"
|
||||
else:
|
||||
loader = "BIOS"
|
||||
|
||||
# Get partition scheme from InstallationData
|
||||
scheme = cls._get_partition_scheme()
|
||||
|
||||
# Create title header
|
||||
title = Gtk.Label(label=_('Boot Option'), name="Header")
|
||||
title.set_property("height-request", 50)
|
||||
cls.vbox1.pack_start(title, False, False, 0)
|
||||
|
||||
# Create main horizontal container
|
||||
hbox1 = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, homogeneous=False, spacing=0)
|
||||
hbox1.show()
|
||||
cls.vbox1.pack_start(hbox1, True, True, 10)
|
||||
|
||||
# Create vertical box for radio buttons
|
||||
bbox1 = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, homogeneous=False, spacing=0)
|
||||
bbox1.show()
|
||||
|
||||
# rEFInd boot manager option
|
||||
cls.refind = Gtk.RadioButton(label=_("Setup rEFInd boot manager"))
|
||||
bbox1.pack_start(cls.refind, False, True, 10)
|
||||
cls.refind.connect("toggled", cls.boot_manager_selection, "refind")
|
||||
cls.refind.show()
|
||||
|
||||
# Enable rEFInd only for GPT + UEFI
|
||||
if scheme == 'GPT' and loader == "UEFI":
|
||||
cls.refind.set_sensitive(True)
|
||||
else:
|
||||
cls.refind.set_sensitive(False)
|
||||
|
||||
# FreeBSD boot manager option
|
||||
cls.bsd = Gtk.RadioButton.new_with_label_from_widget(
|
||||
cls.refind,
|
||||
_("Setup FreeBSD boot manager")
|
||||
)
|
||||
bbox1.pack_start(cls.bsd, False, True, 10)
|
||||
cls.bsd.connect("toggled", cls.boot_manager_selection, "bsd")
|
||||
cls.bsd.show()
|
||||
|
||||
# Enable FreeBSD boot manager only for MBR
|
||||
if scheme == 'MBR':
|
||||
cls.bsd.set_sensitive(True)
|
||||
else:
|
||||
cls.bsd.set_sensitive(False)
|
||||
|
||||
# Native loader option (always available)
|
||||
cls.none = Gtk.RadioButton.new_with_label_from_widget(
|
||||
cls.bsd,
|
||||
_("FreeBSD {loader} loader only").format(loader=loader)
|
||||
)
|
||||
bbox1.pack_start(cls.none, False, True, 10)
|
||||
cls.none.connect("toggled", cls.boot_manager_selection, "none")
|
||||
cls.none.show()
|
||||
|
||||
# Add radio button container to main layout
|
||||
hbox1.pack_start(bbox1, False, False, 50)
|
||||
|
||||
# Set default selection
|
||||
cls.none.set_active(True)
|
||||
cls.boot = "none"
|
||||
InstallationData.boot = cls.boot
|
||||
|
||||
# Create additional container for future expansion
|
||||
cls.box3 = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, homogeneous=False, spacing=0)
|
||||
cls.box3.set_border_width(0)
|
||||
cls.vbox1.pack_start(cls.box3, True, True, 0)
|
||||
|
||||
@classmethod
|
||||
def _get_partition_scheme(cls):
|
||||
"""
|
||||
Determine the partition scheme from installation configuration data.
|
||||
|
||||
Checks ZFS and UFS configuration data, then falls back to InstallationData.scheme
|
||||
or defaults to GPT if no scheme is found.
|
||||
|
||||
Returns:
|
||||
str: The partition scheme ('GPT' or 'MBR')
|
||||
"""
|
||||
# Check ZFS config data for scheme
|
||||
if InstallationData.zfs_config_data:
|
||||
scheme_line = next((line for line in InstallationData.zfs_config_data if 'partscheme=' in line), '')
|
||||
if scheme_line:
|
||||
return scheme_line.split('=')[1].strip()
|
||||
|
||||
# Check UFS config data for scheme
|
||||
if InstallationData.ufs_config_data:
|
||||
scheme_line = next((line for line in InstallationData.ufs_config_data if 'partscheme=' in line), '')
|
||||
if scheme_line:
|
||||
return scheme_line.split('=')[1].strip()
|
||||
|
||||
# Use scheme from InstallationData or default to GPT
|
||||
if InstallationData.scheme:
|
||||
# Handle both 'partscheme=GPT' and 'GPT' formats
|
||||
if 'partscheme=' in InstallationData.scheme:
|
||||
return InstallationData.scheme.split('=')[1].strip()
|
||||
else:
|
||||
return InstallationData.scheme.strip()
|
||||
|
||||
return 'GPT' # Default fallback
|
||||
@@ -0,0 +1,168 @@
|
||||
import re
|
||||
import warnings
|
||||
|
||||
|
||||
def lower_case(text: str) -> bool:
|
||||
"""
|
||||
Find if password contain only lower case.
|
||||
:param text: password
|
||||
|
||||
:return: True if password contain only lower case
|
||||
"""
|
||||
search = re.compile(r'[^a-z]').search
|
||||
return not bool(search(text))
|
||||
|
||||
|
||||
# Find if password contain only upper case
|
||||
def upper_case(text: str) -> bool:
|
||||
"""
|
||||
Find if password contain only upper case.
|
||||
:param text: password
|
||||
|
||||
:return: True if password contain only upper case
|
||||
"""
|
||||
search = re.compile(r'[^A-Z]').search
|
||||
return not bool(search(text))
|
||||
|
||||
|
||||
# Find if password contain only lower case and number
|
||||
def lower_and_number(text: str) -> bool:
|
||||
"""
|
||||
Find if password contain only lower case and number.
|
||||
:param text: password
|
||||
|
||||
:return: True if password contain only lower case and number
|
||||
"""
|
||||
search = re.compile(r'[^a-z0-9]').search
|
||||
return not bool(search(text))
|
||||
|
||||
|
||||
# Find if password contain only upper case and number
|
||||
def upper_and_number(text: str) -> bool:
|
||||
"""
|
||||
Find if password contain only upper case and number.
|
||||
:param text: password
|
||||
|
||||
:return: True if password contain only upper case and number
|
||||
"""
|
||||
search = re.compile(r'[^A-Z0-9]').search
|
||||
return not bool(search(text))
|
||||
|
||||
|
||||
# Find if password contain only lower and upper case and
|
||||
def lower_upper(text: str) -> bool:
|
||||
"""
|
||||
Find if password contain only lower and upper case and
|
||||
:param text: password
|
||||
|
||||
:return: True if password contain only lower and upper case and
|
||||
"""
|
||||
search = re.compile(r'[^a-zA-Z]').search
|
||||
return not bool(search(text))
|
||||
|
||||
|
||||
# Find if password contain only lower and upper case and
|
||||
def lower_upper_number(text) -> bool:
|
||||
"""
|
||||
Find if password contain only lower and upper case and
|
||||
:param text: password
|
||||
|
||||
:return: True if password contain only lower and upper case and
|
||||
"""
|
||||
search = re.compile(r'[^a-zA-Z0-9]').search
|
||||
return not bool(search(text))
|
||||
|
||||
|
||||
# Find if password contain only lowercase, uppercase numbers
|
||||
# and some special character.
|
||||
def all_character(text):
|
||||
"""
|
||||
Find if password contain only lowercase, uppercase numbers
|
||||
and some special character.
|
||||
:param text: password
|
||||
|
||||
:return: True if password contain only lowercase, uppercase numbers
|
||||
and some special character.
|
||||
"""
|
||||
search = re.compile(r'[^a-zA-Z0-9~!@#$%^&*_+":;\'-]').search
|
||||
return not bool(search(text))
|
||||
|
||||
|
||||
def password_strength(password, label3):
|
||||
same_character_type = any(
|
||||
[
|
||||
lower_case(password),
|
||||
upper_case(password),
|
||||
password.isdigit()
|
||||
]
|
||||
)
|
||||
mix_character = any(
|
||||
[
|
||||
lower_and_number(password),
|
||||
upper_and_number(password),
|
||||
lower_upper(password)
|
||||
]
|
||||
)
|
||||
if ' ' in password or '\t' in password:
|
||||
label3.set_text("Space not allowed")
|
||||
elif len(password) <= 4:
|
||||
label3.set_text("Super Weak")
|
||||
elif len(password) <= 8 and same_character_type:
|
||||
label3.set_text("Super Weak")
|
||||
elif len(password) <= 8 and mix_character:
|
||||
label3.set_text("Very Weak")
|
||||
elif len(password) <= 8 and lower_upper_number(password):
|
||||
label3.set_text("Fairly Weak")
|
||||
elif len(password) <= 8 and all_character(password):
|
||||
label3.set_text("Weak")
|
||||
elif len(password) <= 12 and same_character_type:
|
||||
label3.set_text("Very Weak")
|
||||
elif len(password) <= 12 and mix_character:
|
||||
label3.set_text("Fairly Weak")
|
||||
elif len(password) <= 12 and lower_upper_number(password):
|
||||
label3.set_text("Weak")
|
||||
elif len(password) <= 12 and all_character(password):
|
||||
label3.set_text("Strong")
|
||||
elif len(password) <= 16 and same_character_type:
|
||||
label3.set_text("Fairly Weak")
|
||||
elif len(password) <= 16 and mix_character:
|
||||
label3.set_text("Weak")
|
||||
elif len(password) <= 16 and lower_upper_number(password):
|
||||
label3.set_text("Strong")
|
||||
elif len(password) <= 16 and all_character(password):
|
||||
label3.set_text("Fairly Strong")
|
||||
elif len(password) <= 20 and same_character_type:
|
||||
label3.set_text("Weak")
|
||||
elif len(password) <= 20 and mix_character:
|
||||
label3.set_text("Strong")
|
||||
elif len(password) <= 20 and lower_upper_number(password):
|
||||
label3.set_text("Fairly Strong")
|
||||
elif len(password) <= 20 and all_character(password):
|
||||
label3.set_text("Very Strong")
|
||||
elif len(password) <= 24 and same_character_type:
|
||||
label3.set_text("Strong")
|
||||
elif len(password) <= 24 and mix_character:
|
||||
label3.set_text("Fairly Strong")
|
||||
elif len(password) <= 24 and lower_upper_number(password):
|
||||
label3.set_text("Very Strong")
|
||||
elif len(password) <= 24 and all_character(password):
|
||||
label3.set_text("Super Strong")
|
||||
elif same_character_type:
|
||||
label3.set_text("Fairly Strong")
|
||||
else:
|
||||
label3.set_text("Super Strong")
|
||||
|
||||
|
||||
def deprecated(*, version: str, reason: str):
|
||||
def decorator(func):
|
||||
def wrapper(*args, **kwargs):
|
||||
warnings.warn(
|
||||
f"{func.__name__} is deprecated (version {version}): {reason}",
|
||||
category=DeprecationWarning,
|
||||
stacklevel=2
|
||||
)
|
||||
return func(*args, **kwargs)
|
||||
wrapper.__name__ = func.__name__
|
||||
wrapper.__doc__ = func.__doc__
|
||||
return wrapper
|
||||
return decorator
|
||||
@@ -0,0 +1,167 @@
|
||||
from install_station.data import InstallationData, installation_config
|
||||
|
||||
|
||||
class Configuration:
|
||||
"""
|
||||
Utility class for creating and validating GhostBSD installation configuration files.
|
||||
|
||||
This class provides methods to generate the ghostbsd_installation.cfg file
|
||||
used by the GhostBSD installer and validate all required installation data
|
||||
before configuration file creation.
|
||||
|
||||
The class follows a utility pattern with class methods for stateless operations,
|
||||
designed to integrate with the InstallationData system for configuration management.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def sanity_check(cls):
|
||||
"""
|
||||
Perform sanity checks on all installation data used by create_cfg.
|
||||
|
||||
Validates that all required installation parameters are present and valid
|
||||
before attempting to create the configuration file.
|
||||
|
||||
Returns:
|
||||
tuple: (bool, list) - (is_valid, list_of_errors)
|
||||
is_valid: True if all checks pass, False otherwise
|
||||
list_of_errors: List of error messages describing validation failures
|
||||
"""
|
||||
errors = []
|
||||
|
||||
# Check basic installation data
|
||||
if not hasattr(InstallationData, 'boot') or not InstallationData.boot:
|
||||
errors.append("Boot manager not specified")
|
||||
elif InstallationData.boot not in ['refind', 'grub', 'none']:
|
||||
errors.append(f"Invalid boot manager: {InstallationData.boot}")
|
||||
|
||||
# Check ZFS configuration path
|
||||
if InstallationData.zfs_config_data:
|
||||
if not isinstance(InstallationData.zfs_config_data, list):
|
||||
errors.append("ZFS config data is not a list")
|
||||
else:
|
||||
# Check for required ZFS configuration elements
|
||||
has_partscheme = any('partscheme' in str(line) for line in InstallationData.zfs_config_data)
|
||||
if not has_partscheme:
|
||||
errors.append("ZFS config missing partition scheme")
|
||||
|
||||
has_disk = any('disk0=' in str(line) for line in InstallationData.zfs_config_data)
|
||||
if not has_disk:
|
||||
errors.append("ZFS config missing disk specification")
|
||||
else:
|
||||
# Check custom partition configuration path
|
||||
if not hasattr(InstallationData, 'disk') or not InstallationData.disk:
|
||||
errors.append("Disk not specified for custom partitioning")
|
||||
|
||||
if not hasattr(InstallationData, 'slice') or not InstallationData.slice:
|
||||
errors.append("Partition slice not specified")
|
||||
|
||||
if not hasattr(InstallationData, 'scheme') or not InstallationData.scheme:
|
||||
errors.append("Partition scheme not specified")
|
||||
elif InstallationData.scheme not in ['partscheme=GPT', 'partscheme=MBR']:
|
||||
errors.append(f"Invalid partition scheme: {InstallationData.scheme}")
|
||||
|
||||
if not hasattr(InstallationData, 'new_partition') or not InstallationData.new_partition:
|
||||
errors.append("No partitions defined for custom partitioning")
|
||||
elif not isinstance(InstallationData.new_partition, list):
|
||||
errors.append("Partition data is not a list")
|
||||
|
||||
# Check installation config file path
|
||||
if not installation_config:
|
||||
errors.append("Installation config file path not defined")
|
||||
|
||||
return len(errors) == 0, errors
|
||||
|
||||
@classmethod
|
||||
def create_cfg(cls):
|
||||
"""
|
||||
Create the ghostbsd_installation.cfg file to install GhostBSD.
|
||||
|
||||
Generates the configuration file used by the GhostBSD installer based on
|
||||
data stored in InstallationData. Supports both ZFS and custom partitioning
|
||||
configurations.
|
||||
|
||||
The configuration includes:
|
||||
- Installation mode and type settings
|
||||
- Disk and partition configuration
|
||||
- Boot manager setup
|
||||
- Network configuration
|
||||
- First boot preparation commands
|
||||
|
||||
Raises:
|
||||
ValueError: If sanity check fails, indicating invalid configuration data
|
||||
IOError: If unable to write to the configuration file
|
||||
"""
|
||||
# Perform sanity check before creating configuration
|
||||
is_valid, errors = cls.sanity_check()
|
||||
if not is_valid:
|
||||
error_msg = "Configuration validation failed:\n" + "\n".join(f"- {error}" for error in errors)
|
||||
raise ValueError(error_msg)
|
||||
|
||||
try:
|
||||
with open(installation_config, 'w') as f:
|
||||
# Installation Mode
|
||||
f.write('# Installation Mode\n')
|
||||
f.write('installMode=fresh\n')
|
||||
f.write('installInteractive=no\n')
|
||||
f.write('installType=GhostBSD\n')
|
||||
f.write('installMedium=livezfs\n')
|
||||
f.write('packageType=livezfs\n')
|
||||
|
||||
if InstallationData.zfs_config_data:
|
||||
# ZFS Configuration Path
|
||||
for line in InstallationData.zfs_config_data:
|
||||
if 'partscheme' in line:
|
||||
f.write(line)
|
||||
boot = InstallationData.boot
|
||||
if boot == 'refind':
|
||||
f.write('bootManager=none\n')
|
||||
f.write(f'efiLoader={boot}\n')
|
||||
else:
|
||||
f.write(f'bootManager={boot}\n')
|
||||
f.write('efiLoader=none\n')
|
||||
else:
|
||||
f.write(line)
|
||||
else:
|
||||
# Custom Partition Configuration Path
|
||||
d_output = InstallationData.disk
|
||||
f.write('\n# Disk Setup\n')
|
||||
f.write('ashift=12\n')
|
||||
f.write(f'disk0={d_output}\n')
|
||||
# Partition Slice.
|
||||
f.write(f'partition={InstallationData.slice}\n')
|
||||
# Boot Menu
|
||||
boot = InstallationData.boot
|
||||
if boot == 'refind':
|
||||
f.write('bootManager=none\n')
|
||||
f.write(f'efiLoader={boot}\n')
|
||||
else:
|
||||
f.write(f'bootManager={boot}\n')
|
||||
f.write('efiLoader=none\n')
|
||||
f.write(f'{InstallationData.scheme}\n')
|
||||
f.write('commitDiskPart\n')
|
||||
# Partition Setup
|
||||
f.write('\n# Partition Setup\n')
|
||||
for line in InstallationData.new_partition:
|
||||
if 'BOOT' in line or 'BIOS' in line or 'UEFI' in line:
|
||||
pass
|
||||
else:
|
||||
f.write(f'disk0-part={line.strip()}\n')
|
||||
f.write('commitDiskLabel\n')
|
||||
|
||||
# Network Configuration
|
||||
f.write('\n# Network Configuration\n')
|
||||
f.write('hostname=installed\n')
|
||||
|
||||
# First Boot Preparation Commands
|
||||
f.write('\n# command to prepare first boot\n')
|
||||
f.write("runCommand=sysrc hostname='installed'\n")
|
||||
f.write("runCommand=pw userdel -n ghostbsd -r\n")
|
||||
f.write("runCommand=sed -i '' 's/ghostbsd/root/g' /etc/gettytab\n")
|
||||
f.write("runCommand=sed -i '' 's/ghostbsd/root/g' /etc/ttys\n")
|
||||
f.write("runCommand=mv /usr/local/etc/devd/automount_devd"
|
||||
".conf.skip /usr/local/etc/devd/automount_devd.conf\n")
|
||||
f.write("runCommand=mv /usr/local/etc/devd/automount_devd"
|
||||
"_localdisks.conf.skip /usr/local/etc/devd/"
|
||||
"automount_devd_localdisks.conf\n")
|
||||
except IOError as e:
|
||||
raise IOError(f"Failed to write configuration file: {e}")
|
||||
@@ -0,0 +1,887 @@
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
gi.require_version('Gdk', '3.0')
|
||||
from gi.repository import Gtk, Gdk
|
||||
from install_station.partition import (
|
||||
DiskPartition,
|
||||
DeletePartition,
|
||||
bios_or_uefi,
|
||||
CreateSlice,
|
||||
AutoFreeSpace,
|
||||
CreatePartition,
|
||||
CreateLabel
|
||||
)
|
||||
from install_station.data import InstallationData, logo
|
||||
from install_station.interface_controller import Button
|
||||
import gettext
|
||||
|
||||
gettext.bindtextdomain('install-station', '/usr/local/share/locale')
|
||||
gettext.textdomain('install-station')
|
||||
_ = gettext.gettext
|
||||
|
||||
bios_type = bios_or_uefi()
|
||||
|
||||
cssProvider = Gtk.CssProvider()
|
||||
cssProvider.load_from_path('/usr/local/lib/install-station/ghostbsd-style.css')
|
||||
screen = Gdk.Screen.get_default()
|
||||
styleContext = Gtk.StyleContext()
|
||||
styleContext.add_provider_for_screen(
|
||||
screen,
|
||||
cssProvider,
|
||||
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION
|
||||
)
|
||||
|
||||
|
||||
class PartitionManager:
|
||||
"""
|
||||
Utility class for partition management operations following the pattern of InstallTypes and ZFS.
|
||||
|
||||
This class provides a GTK+ interface for managing disk partitions including creating,
|
||||
deleting, and configuring partitions with support for both GPT and MBR schemes.
|
||||
Supports ZFS, UFS, SWAP, BOOT, and UEFI filesystem types.
|
||||
|
||||
The class follows a utility pattern with class methods and variables for state management,
|
||||
designed to integrate with the InstallationData system for configuration persistence.
|
||||
"""
|
||||
# Class variables for state management
|
||||
fs = None
|
||||
mount_point = None
|
||||
window = None
|
||||
efi_exist = True
|
||||
fs_behind = None
|
||||
disk_index = None
|
||||
path = None
|
||||
disk = None
|
||||
vbox1 = None
|
||||
scheme = 'GPT'
|
||||
size = None
|
||||
slice = None
|
||||
label = None
|
||||
mount_point_behind = None
|
||||
change_schemes = False
|
||||
iter = None
|
||||
store = None
|
||||
treeview = None
|
||||
tree_selection = None
|
||||
|
||||
# UI elements as class variables
|
||||
create_bt = None
|
||||
delete_bt = None
|
||||
revert_bt = None
|
||||
auto_bt = None
|
||||
fs_type = None
|
||||
entry = None
|
||||
mount_point_box = None
|
||||
|
||||
@classmethod
|
||||
def set_fs(cls, widget):
|
||||
"""
|
||||
Set the filesystem type from a ComboBoxText widget selection.
|
||||
|
||||
Args:
|
||||
widget: GTK ComboBoxText widget containing filesystem type options
|
||||
"""
|
||||
cls.fs = widget.get_active_text()
|
||||
|
||||
@classmethod
|
||||
def get_mount_point(cls, widget):
|
||||
"""
|
||||
Get the mount point from a ComboBoxText widget selection.
|
||||
|
||||
Args:
|
||||
widget: GTK ComboBoxText widget containing mount point options
|
||||
"""
|
||||
cls.mount_point = widget.get_active_text()
|
||||
|
||||
@classmethod
|
||||
def get_model(cls):
|
||||
"""
|
||||
Return the GTK widget model for the partition manager.
|
||||
|
||||
Creates and initializes the UI if it doesn't exist yet.
|
||||
|
||||
Returns:
|
||||
Gtk.Box: The main container widget for the partition manager interface
|
||||
"""
|
||||
if cls.vbox1 is None:
|
||||
cls.initialize()
|
||||
return cls.vbox1
|
||||
|
||||
@classmethod
|
||||
def initialize(cls):
|
||||
"""
|
||||
Initialize the partition manager UI following the utility class pattern.
|
||||
|
||||
Creates the main interface including the partition tree view, control buttons,
|
||||
and sets up the partition database. This method is called automatically
|
||||
by get_model() when the interface is first accessed.
|
||||
"""
|
||||
DiskPartition.create_partition_database()
|
||||
cls.vbox1 = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, homogeneous=False, spacing=0)
|
||||
cls.vbox1.show()
|
||||
|
||||
# Scrolled window for partition tree
|
||||
sw = Gtk.ScrolledWindow(hexpand=True, vexpand=True)
|
||||
sw.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
|
||||
cls.store = Gtk.TreeStore(str, str, str, str, bool)
|
||||
cls.tree_store()
|
||||
cls.treeview = Gtk.TreeView()
|
||||
cls.treeview.set_model(cls.store)
|
||||
cls.treeview.set_rules_hint(True)
|
||||
|
||||
# Setup columns
|
||||
cls._setup_columns()
|
||||
|
||||
cls.treeview.set_reorderable(True)
|
||||
cls.treeview.expand_all()
|
||||
cls.tree_selection = cls.treeview.get_selection()
|
||||
cls.tree_selection.set_mode(Gtk.SelectionMode.SINGLE)
|
||||
cls.tree_selection.connect("changed", cls.partition_selection)
|
||||
sw.add(cls.treeview)
|
||||
sw.show()
|
||||
cls.vbox1.pack_start(sw, True, True, 0)
|
||||
|
||||
# Button box
|
||||
hbox1 = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, homogeneous=False, spacing=0)
|
||||
hbox1.set_border_width(10)
|
||||
cls.vbox1.pack_start(hbox1, False, False, 0)
|
||||
hbox1.show()
|
||||
cls.scheme = 'GPT'
|
||||
hbox1.pack_start(cls.delete_create_button(), False, False, 10)
|
||||
|
||||
@classmethod
|
||||
def _setup_columns(cls):
|
||||
"""
|
||||
Setup treeview columns for the partition display.
|
||||
|
||||
Creates columns for Partition, Size(MB), Mount Point, and System/Type
|
||||
with appropriate widths and header labels.
|
||||
"""
|
||||
columns_config = [
|
||||
('Partition', 0, 150),
|
||||
('Size(MB)', 1, 150),
|
||||
('Mount Point', 2, 150),
|
||||
('System/Type', 3, 150)
|
||||
]
|
||||
|
||||
for i, (title, text_col, width) in enumerate(columns_config):
|
||||
cell = Gtk.CellRendererText()
|
||||
column = Gtk.TreeViewColumn(None, cell, text=text_col)
|
||||
column_header = Gtk.Label(label=title)
|
||||
column_header.set_use_markup(True)
|
||||
column_header.show()
|
||||
column.set_widget(column_header)
|
||||
column.set_resizable(True)
|
||||
column.set_fixed_width(width)
|
||||
if i == 0:
|
||||
column.set_sort_column_id(0)
|
||||
cls.treeview.append_column(column)
|
||||
|
||||
@classmethod
|
||||
def tree_store(cls):
|
||||
"""
|
||||
Populate the tree store with disk and partition information from the disk database.
|
||||
|
||||
Creates a hierarchical tree structure showing:
|
||||
- Disks (top level)
|
||||
- Partitions/slices (second level)
|
||||
- Labels/partitions (third level)
|
||||
|
||||
Each level displays relevant information like size, mount points, and filesystem types.
|
||||
|
||||
Returns:
|
||||
Gtk.TreeStore: The populated tree store model
|
||||
"""
|
||||
cls.store.clear()
|
||||
disk_db = DiskPartition.get_disk_database()
|
||||
cls.disk_index = list(disk_db.keys())
|
||||
for disk in disk_db:
|
||||
disk_info = disk_db[disk]
|
||||
disk_scheme = disk_info['scheme']
|
||||
mount_point = ''
|
||||
disk_size = str(disk_info['size'])
|
||||
disk_partitions = disk_info['partitions']
|
||||
partition_list = disk_info['partition-list']
|
||||
pinter1 = cls.store.append(None, [disk, disk_size, mount_point,
|
||||
disk_scheme, True])
|
||||
for partition in partition_list:
|
||||
partition_info = disk_partitions[partition]
|
||||
file_system = partition_info['file-system']
|
||||
mount_point = partition_info['mount-point']
|
||||
partition_size = str(partition_info['size'])
|
||||
partition_partitions = partition_info['partitions']
|
||||
label_list = partition_info['partition-list']
|
||||
pinter2 = cls.store.append(pinter1, [partition, partition_size, mount_point, file_system, True])
|
||||
for label in label_list:
|
||||
label_info = partition_partitions[label]
|
||||
file_system = label_info['file-system']
|
||||
label_mount_point = label_info['mount-point']
|
||||
label_size = str(label_info['size'])
|
||||
cls.store.append(pinter2, [label, label_size, label_mount_point, file_system, True])
|
||||
return cls.store
|
||||
|
||||
@classmethod
|
||||
def update(cls):
|
||||
"""
|
||||
Update the treeview after partition operations.
|
||||
|
||||
Refreshes the partition tree display, expands all rows, and attempts
|
||||
to restore the previously selected row if it still exists.
|
||||
"""
|
||||
old_path = cls.path
|
||||
cls.tree_store()
|
||||
cls.treeview.expand_all()
|
||||
if old_path:
|
||||
cls.treeview.row_activated(old_path, cls.treeview.get_columns()[0])
|
||||
cls.treeview.set_cursor(old_path)
|
||||
|
||||
@classmethod
|
||||
def delete_create_button(cls):
|
||||
"""
|
||||
Create the button toolbar for partition operations.
|
||||
|
||||
Creates a horizontal box containing Create, Delete, Revert, and Auto buttons
|
||||
for partition management operations.
|
||||
|
||||
Returns:
|
||||
Gtk.Box: Container with partition operation buttons
|
||||
"""
|
||||
bbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, homogeneous=True, spacing=10)
|
||||
bbox.set_border_width(5)
|
||||
bbox.set_spacing(10)
|
||||
cls.create_bt = Gtk.Button(label="Create")
|
||||
cls.create_bt.connect("clicked", cls.create_partition)
|
||||
cls.create_bt.set_sensitive(False)
|
||||
bbox.pack_start(cls.create_bt, True, True, 0)
|
||||
cls.delete_bt = Gtk.Button(label="Delete")
|
||||
cls.delete_bt.connect("clicked", cls.delete_partition)
|
||||
cls.delete_bt.set_sensitive(False)
|
||||
bbox.pack_start(cls.delete_bt, True, True, 0)
|
||||
cls.revert_bt = Gtk.Button(label="Revert")
|
||||
cls.revert_bt.connect("clicked", cls.revert_change)
|
||||
cls.revert_bt.set_sensitive(False)
|
||||
bbox.pack_start(cls.revert_bt, True, True, 0)
|
||||
cls.auto_bt = Gtk.Button(label="Auto")
|
||||
cls.auto_bt.connect("clicked", cls.auto_partition)
|
||||
cls.auto_bt.set_sensitive(False)
|
||||
bbox.pack_start(cls.auto_bt, True, True, 0)
|
||||
return bbox
|
||||
|
||||
@classmethod
|
||||
def on_add_label(cls, _widget, entry, free_space, path):
|
||||
"""
|
||||
Handle adding a new label/partition in MBR scheme.
|
||||
|
||||
Args:
|
||||
_widget: The button widget (unused)
|
||||
entry: SpinButton containing the partition size
|
||||
free_space: Available space in MB
|
||||
path: TreePath indicating the parent location
|
||||
"""
|
||||
create_size = entry.get_value_as_int()
|
||||
left_size = free_space - create_size
|
||||
CreateLabel(path, cls.disk, cls.slice, left_size, create_size,
|
||||
cls.mount_point, cls.fs)
|
||||
cls.window.hide()
|
||||
cls.update()
|
||||
|
||||
@classmethod
|
||||
def on_add_partition(cls, _widget, entry, free_space, path):
|
||||
"""
|
||||
Handle adding a new partition in GPT scheme.
|
||||
|
||||
Args:
|
||||
_widget: The button widget (unused)
|
||||
entry: SpinButton containing the partition size
|
||||
free_space: Available space in MB
|
||||
path: TreePath indicating the parent location
|
||||
"""
|
||||
create_size = entry.get_value_as_int()
|
||||
left_size = int(free_space - create_size)
|
||||
CreatePartition(path, cls.disk, left_size, create_size,
|
||||
cls.mount_point, cls.fs)
|
||||
cls.window.hide()
|
||||
cls.update()
|
||||
|
||||
@classmethod
|
||||
def cancel(cls, _widget):
|
||||
"""
|
||||
Cancel the current partition operation and close the dialog.
|
||||
|
||||
Args:
|
||||
_widget: The cancel button widget (unused)
|
||||
"""
|
||||
cls.window.hide()
|
||||
cls.update()
|
||||
|
||||
@classmethod
|
||||
def label_editor(cls, path, size, scheme):
|
||||
"""
|
||||
Open the partition/label editor dialog.
|
||||
|
||||
Creates a dialog window for configuring a new partition with options for
|
||||
filesystem type, size, and mount point based on the partitioning scheme.
|
||||
|
||||
Args:
|
||||
path: TreePath indicating where to create the partition
|
||||
size: Available free space in MB
|
||||
scheme: Partitioning scheme ('GPT' or 'MBR')
|
||||
"""
|
||||
free_space = int(size)
|
||||
cls.window = Gtk.Window()
|
||||
cls.window.set_title(title="Add Partition")
|
||||
cls.window.set_border_width(0)
|
||||
cls.window.set_size_request(480, 200)
|
||||
cls.window.set_icon_from_file(logo)
|
||||
box1 = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, homogeneous=False, spacing=0)
|
||||
cls.window.add(box1)
|
||||
box1.show()
|
||||
box2 = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, homogeneous=False, spacing=10)
|
||||
box2.set_border_width(10)
|
||||
box1.pack_start(box2, True, True, 0)
|
||||
box2.show()
|
||||
|
||||
# Create partition configuration table
|
||||
table = Gtk.Table(1, 2, True)
|
||||
label1 = Gtk.Label(label="Type:")
|
||||
label2 = Gtk.Label(label="Size(MB):")
|
||||
label3 = Gtk.Label(label="Mount point:")
|
||||
cls.fs_type = Gtk.ComboBoxText()
|
||||
cls.fs_type.append_text('ZFS')
|
||||
cls.fs_type.append_text('SWAP')
|
||||
cls.fs_type.append_text('UFS') # Add UFS option
|
||||
|
||||
# Set filesystem options based on scheme and existing partitions
|
||||
if scheme == 'GPT':
|
||||
if bios_type == "UEFI":
|
||||
cls.fs_type.append_text("UEFI")
|
||||
if cls.efi_exist is False:
|
||||
cls.fs_type.set_active(3) # UEFI
|
||||
cls.fs = "UEFI"
|
||||
elif cls.mount_point_behind == "/" or cls.fs_behind == "ZFS":
|
||||
cls.fs_type.set_active(1) # SWAP
|
||||
cls.fs = "SWAP"
|
||||
else:
|
||||
cls.fs_type.set_active(0) # ZFS
|
||||
cls.fs = "ZFS"
|
||||
else:
|
||||
cls.fs_type.append_text("BOOT")
|
||||
if not InstallationData.new_partition:
|
||||
cls.fs_type.set_active(4) # BOOT
|
||||
cls.fs = "BOOT"
|
||||
elif len(InstallationData.new_partition) == 0:
|
||||
cls.fs_type.set_active(4) # BOOT
|
||||
cls.fs = "BOOT"
|
||||
elif cls.mount_point_behind == "/" or cls.fs_behind == "ZFS":
|
||||
cls.fs_type.set_active(1) # SWAP
|
||||
cls.fs = "SWAP"
|
||||
else:
|
||||
cls.fs_type.set_active(0) # ZFS
|
||||
cls.fs = "ZFS"
|
||||
elif cls.mount_point_behind == "/" or cls.fs_behind == "ZFS":
|
||||
cls.fs_type.set_active(1) # SWAP
|
||||
cls.fs = "SWAP"
|
||||
else:
|
||||
cls.fs_type.set_active(0) # ZFS
|
||||
cls.fs = "ZFS"
|
||||
|
||||
cls.fs_type.connect("changed", cls.set_fs)
|
||||
|
||||
# Size spinner
|
||||
adj = Gtk.Adjustment(free_space, 0, free_space, 1, 100, 0)
|
||||
cls.entry = Gtk.SpinButton(adjustment=adj, numeric=True)
|
||||
cls.entry.set_editable(True)
|
||||
|
||||
# Mount point selection
|
||||
cls.mount_point_box = Gtk.ComboBoxText()
|
||||
cls.mount_point = "none"
|
||||
cls.mount_point_box.append_text('none')
|
||||
cls.mount_point_box.append_text('/')
|
||||
if InstallationData.new_partition:
|
||||
if scheme == 'GPT' and len(InstallationData.new_partition) == 1:
|
||||
cls.mount_point_box.append_text('/boot')
|
||||
elif scheme == 'MBR' and len(InstallationData.new_partition) == 0:
|
||||
cls.mount_point_box.append_text('/boot')
|
||||
elif scheme == 'MBR' and not InstallationData.new_partition:
|
||||
cls.mount_point_box.append_text('/boot')
|
||||
cls.mount_point_box.append_text('/etc')
|
||||
cls.mount_point_box.append_text('/root')
|
||||
cls.mount_point_box.append_text('/tmp')
|
||||
cls.mount_point_box.append_text('/usr')
|
||||
cls.mount_point_box.append_text('/home')
|
||||
cls.mount_point_box.append_text('/var')
|
||||
cls.mount_point_box.set_active(0)
|
||||
|
||||
# Enable mount point selection for UFS
|
||||
if 'UFS' in cls.fs:
|
||||
cls.mount_point_box.set_sensitive(True)
|
||||
else:
|
||||
cls.mount_point_box.set_sensitive(False)
|
||||
cls.mount_point_box.connect("changed", cls.get_mount_point)
|
||||
|
||||
# Add to table
|
||||
table.attach(label1, 0, 1, 1, 2)
|
||||
table.attach(cls.fs_type, 1, 2, 1, 2)
|
||||
table.attach(label2, 0, 1, 2, 3)
|
||||
table.attach(cls.entry, 1, 2, 2, 3)
|
||||
table.attach(label3, 0, 1, 3, 4)
|
||||
table.attach(cls.mount_point_box, 1, 2, 3, 4)
|
||||
box2.pack_start(table, False, False, 0)
|
||||
|
||||
# Buttons
|
||||
box2 = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, homogeneous=False, spacing=10)
|
||||
box2.set_border_width(5)
|
||||
box1.pack_start(box2, False, True, 0)
|
||||
box2.show()
|
||||
bbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, homogeneous=True, spacing=10)
|
||||
bbox.set_border_width(5)
|
||||
bbox.set_spacing(10)
|
||||
|
||||
button = Gtk.Button(stock=Gtk.STOCK_CANCEL)
|
||||
button.connect("clicked", cls.cancel)
|
||||
bbox.pack_start(button, True, True, 0)
|
||||
|
||||
button = Gtk.Button(stock=Gtk.STOCK_ADD)
|
||||
if scheme == 'MBR':
|
||||
button.connect(
|
||||
"clicked", cls.on_add_label, cls.entry, free_space, path
|
||||
)
|
||||
elif scheme == 'GPT' and cls.fs == 'BOOT':
|
||||
button.connect(
|
||||
"clicked", cls.on_add_partition, cls.entry, free_space, path
|
||||
)
|
||||
elif scheme == 'GPT' and cls.fs == 'UEFI' and cls.efi_exist is False:
|
||||
button.connect(
|
||||
"clicked", cls.on_add_partition, cls.entry, free_space, path
|
||||
)
|
||||
else:
|
||||
button.connect(
|
||||
"clicked", cls.on_add_partition, cls.entry, free_space, path
|
||||
)
|
||||
bbox.pack_start(button, True, True, 0)
|
||||
box2.pack_end(bbox, True, True, 5)
|
||||
cls.window.show_all()
|
||||
|
||||
@classmethod
|
||||
def scheme_selection(cls, combobox):
|
||||
"""
|
||||
Handle partition scheme selection from combo box.
|
||||
|
||||
Args:
|
||||
combobox: ComboBox widget containing scheme options
|
||||
"""
|
||||
model = combobox.get_model()
|
||||
index = combobox.get_active()
|
||||
data = model[index][0]
|
||||
value = data.partition(':')[0]
|
||||
cls.scheme = value
|
||||
|
||||
@classmethod
|
||||
def add_gpt_mbr(cls, _widget):
|
||||
"""
|
||||
Apply the selected partition scheme to the disk.
|
||||
|
||||
Args:
|
||||
_widget: The add button widget (unused)
|
||||
"""
|
||||
DiskPartition.set_disk_scheme(cls.scheme, cls.disk, cls.size)
|
||||
cls.update()
|
||||
cls.window.hide()
|
||||
|
||||
@classmethod
|
||||
def scheme_editor(cls):
|
||||
"""
|
||||
Create a partition scheme editor window.
|
||||
|
||||
Opens a dialog allowing the user to select between GPT and MBR
|
||||
partition schemes for the selected disk.
|
||||
"""
|
||||
cls.window = Gtk.Window()
|
||||
cls.window.set_title("Partition Scheme")
|
||||
cls.window.set_border_width(0)
|
||||
cls.window.set_size_request(400, 150)
|
||||
cls.window.set_icon_from_file(logo)
|
||||
box1 = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, homogeneous=False, spacing=0)
|
||||
cls.window.add(box1)
|
||||
box1.show()
|
||||
box2 = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, homogeneous=False, spacing=10)
|
||||
box2.set_border_width(10)
|
||||
box1.pack_start(box2, True, True, 0)
|
||||
box2.show()
|
||||
|
||||
# Creating MBR or GPT drive
|
||||
label = Gtk.Label(label='<b>Select a partition scheme for this drive:</b>')
|
||||
label.set_use_markup(True)
|
||||
|
||||
# Adding a combo box to selecting MBR or GPT scheme.
|
||||
cls.scheme = 'GPT'
|
||||
scheme_box = Gtk.ComboBoxText()
|
||||
scheme_box.append_text("GPT: GUID Partition Table")
|
||||
scheme_box.append_text("MBR: DOS Partition")
|
||||
scheme_box.connect('changed', cls.scheme_selection)
|
||||
scheme_box.set_active(0)
|
||||
table = Gtk.Table(1, 2, True)
|
||||
table.attach(label, 0, 2, 0, 1)
|
||||
table.attach(scheme_box, 0, 2, 1, 2)
|
||||
box2.pack_start(table, False, False, 0)
|
||||
box2 = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, homogeneous=False, spacing=10)
|
||||
box2.set_border_width(5)
|
||||
box1.pack_start(box2, False, True, 0)
|
||||
box2.show()
|
||||
|
||||
# Add create_scheme button
|
||||
bbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, homogeneous=True, spacing=10)
|
||||
bbox.set_border_width(5)
|
||||
bbox.set_spacing(10)
|
||||
button = Gtk.Button(stock=Gtk.STOCK_ADD)
|
||||
button.connect("clicked", cls.add_gpt_mbr)
|
||||
bbox.pack_start(button, True, True, 0)
|
||||
box2.pack_end(bbox, True, True, 5)
|
||||
cls.window.show_all()
|
||||
|
||||
@classmethod
|
||||
def get_value(cls, _widget, entry):
|
||||
"""
|
||||
Handle slice creation from the slice editor dialog.
|
||||
|
||||
Gets the partition size from the entry widget and creates a new slice
|
||||
in the MBR partition table.
|
||||
|
||||
Args:
|
||||
_widget: The add button widget (unused)
|
||||
entry: SpinButton containing the partition size
|
||||
"""
|
||||
partition_size = int(entry.get_value_as_int())
|
||||
rs = int(cls.size) - partition_size
|
||||
CreateSlice(partition_size, rs, cls.path, cls.disk)
|
||||
cls.update()
|
||||
cls.window.hide()
|
||||
|
||||
@classmethod
|
||||
def slice_editor(cls):
|
||||
"""
|
||||
Create a window for editing partition slices in MBR scheme.
|
||||
|
||||
Opens a dialog for creating a new slice partition with size configuration.
|
||||
Used specifically for MBR partitioning scheme.
|
||||
"""
|
||||
free_space = int(cls.size)
|
||||
cls.window = Gtk.Window()
|
||||
cls.window.set_title("Add Partition")
|
||||
cls.window.set_border_width(0)
|
||||
cls.window.set_size_request(400, 150)
|
||||
cls.window.set_icon_from_file(logo)
|
||||
box1 = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, homogeneous=False, spacing=0)
|
||||
cls.window.add(box1)
|
||||
box1.show()
|
||||
box2 = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, homogeneous=False, spacing=10)
|
||||
box2.set_border_width(10)
|
||||
box1.pack_start(box2, True, True, 0)
|
||||
box2.show()
|
||||
|
||||
# Create Partition slice
|
||||
table = Gtk.Table(1, 2, True)
|
||||
label1 = Gtk.Label(label="Size(MB):")
|
||||
adj = Gtk.Adjustment(free_space, 0, free_space, 1, 100, 0)
|
||||
cls.entry = Gtk.SpinButton(adjustment=adj, numeric=True)
|
||||
cls.entry.set_numeric(True)
|
||||
table.attach(label1, 0, 1, 1, 2)
|
||||
table.attach(cls.entry, 1, 2, 1, 2)
|
||||
box2.pack_start(table, False, False, 0)
|
||||
box2 = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, homogeneous=False, spacing=10)
|
||||
box2.set_border_width(5)
|
||||
box1.pack_start(box2, False, True, 0)
|
||||
box2.show()
|
||||
|
||||
# Add button
|
||||
bbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, homogeneous=True, spacing=10)
|
||||
bbox.set_border_width(5)
|
||||
bbox.set_spacing(10)
|
||||
button = Gtk.Button(stock=Gtk.STOCK_CANCEL)
|
||||
button.connect("clicked", cls.cancel)
|
||||
bbox.pack_start(button, True, True, 0)
|
||||
button = Gtk.Button(stock=Gtk.STOCK_ADD)
|
||||
button.connect("clicked", cls.get_value, cls.entry)
|
||||
bbox.pack_start(button, True, True, 0)
|
||||
box2.pack_end(bbox, True, True, 5)
|
||||
cls.window.show_all()
|
||||
|
||||
@classmethod
|
||||
def delete_partition(cls, _widget):
|
||||
"""
|
||||
Delete the currently selected partition.
|
||||
|
||||
Removes the selected partition or slice from the disk and updates
|
||||
the partition display.
|
||||
|
||||
Args:
|
||||
_widget: The delete button widget (unused)
|
||||
"""
|
||||
part = cls.slice if cls.label == "Not selected" else cls.label
|
||||
DeletePartition(part, cls.path)
|
||||
cls.update()
|
||||
|
||||
@classmethod
|
||||
def auto_partition(cls, _widget):
|
||||
"""
|
||||
Automatically partition the disk with default ZFS configuration.
|
||||
|
||||
Creates automatic partitions suitable for ZFS installation including
|
||||
boot partitions (if needed) and ZFS root partition.
|
||||
|
||||
Args:
|
||||
_widget: The auto button widget (unused)
|
||||
"""
|
||||
cls.create_bt.set_sensitive(False)
|
||||
cls.delete_bt.set_sensitive(False)
|
||||
cls.auto_bt.set_sensitive(False)
|
||||
cls.revert_bt.set_sensitive(False)
|
||||
if 'freespace' in cls.slice:
|
||||
AutoFreeSpace(cls.path, cls.size, 'ZFS', cls.efi_exist,
|
||||
cls.disk, cls.scheme)
|
||||
cls.update()
|
||||
else:
|
||||
print('wrong utilization')
|
||||
|
||||
@classmethod
|
||||
def revert_change(cls, _widget):
|
||||
"""
|
||||
Revert all partition changes and restore original state.
|
||||
|
||||
Clears all partition configuration data from InstallationData and
|
||||
recreates the original partition database, effectively undoing
|
||||
all partition modifications.
|
||||
|
||||
Args:
|
||||
_widget: The revert button widget (unused)
|
||||
"""
|
||||
# Reset all partition configuration data in InstallationData
|
||||
InstallationData.create = []
|
||||
InstallationData.scheme = ""
|
||||
InstallationData.disk = ""
|
||||
InstallationData.slice = ""
|
||||
InstallationData.delete = []
|
||||
InstallationData.destroy = {}
|
||||
InstallationData.new_partition = []
|
||||
DiskPartition.create_partition_database()
|
||||
cls.tree_store()
|
||||
cls.treeview.expand_all()
|
||||
|
||||
@classmethod
|
||||
def create_partition(cls, _widget):
|
||||
"""
|
||||
Create a new partition based on the current selection.
|
||||
|
||||
Opens the appropriate editor dialog based on the selected item:
|
||||
- Scheme editor for un-partitioned disks
|
||||
- Label editor for free space in MBR or GPT
|
||||
- Slice editor for MBR primary partition creation
|
||||
|
||||
Args:
|
||||
_widget: The create button widget (unused)
|
||||
"""
|
||||
cls.create_bt.set_sensitive(False)
|
||||
cls.delete_bt.set_sensitive(False)
|
||||
cls.auto_bt.set_sensitive(False)
|
||||
cls.revert_bt.set_sensitive(False)
|
||||
if cls.change_schemes is True:
|
||||
cls.scheme_editor()
|
||||
elif 'freespace' in cls.label:
|
||||
cls.label_editor(cls.path, cls.size, 'MBR')
|
||||
elif 'freespace' in cls.slice:
|
||||
if cls.scheme == "MBR" and cls.path[1] < 4:
|
||||
cls.slice_editor()
|
||||
elif cls.scheme == "GPT":
|
||||
cls.label_editor(cls.path, cls.size, 'GPT')
|
||||
else:
|
||||
print('This method of creating partition is not implemented')
|
||||
|
||||
@classmethod
|
||||
def partition_selection(cls, widget):
|
||||
"""
|
||||
Handle partition selection events and update UI button states.
|
||||
|
||||
This method is called when a user selects a different item in the partition
|
||||
tree view. It analyzes the selection and enables/disables appropriate buttons
|
||||
based on what operations are valid for the selected item.
|
||||
|
||||
The method handles complex logic for:
|
||||
- Determining partition hierarchy (disk/slice/label)
|
||||
- Checking partition scheme compatibility
|
||||
- Validating boot partition requirements
|
||||
- Managing button sensitivity states
|
||||
|
||||
Args:
|
||||
widget: TreeSelection widget that triggered the selection change
|
||||
"""
|
||||
efi_already_exist = False
|
||||
model, cls.iter, = widget.get_selected()
|
||||
if cls.iter is None:
|
||||
Button.next_button.set_sensitive(False)
|
||||
return None
|
||||
cls.path = model.get_path(cls.iter)
|
||||
main_tree_iter = model.get_iter(cls.path)
|
||||
cls.size = model.get_value(main_tree_iter, 1)
|
||||
tree_iter1 = model.get_iter(cls.path[0])
|
||||
cls.scheme = model.get_value(tree_iter1, 3)
|
||||
cls.disk = model.get_value(tree_iter1, 0)
|
||||
|
||||
if len(cls.path) >= 2:
|
||||
tree_iter2 = model.get_iter(cls.path[:2])
|
||||
cls.slice = model.get_value(tree_iter2, 0)
|
||||
cls.change_schemes = False
|
||||
else:
|
||||
if len(cls.path) == 1:
|
||||
if DiskPartition.how_partition(cls.disk) == 0:
|
||||
cls.change_schemes = True
|
||||
elif DiskPartition.how_partition(cls.disk) == 1:
|
||||
slice_path = f'{cls.path[0]}:0'
|
||||
try:
|
||||
tree_iter2 = model.get_iter(slice_path)
|
||||
if 'freespace' in model.get_value(tree_iter2, 0):
|
||||
cls.change_schemes = True
|
||||
else:
|
||||
cls.change_schemes = False
|
||||
except ValueError:
|
||||
cls.change_schemes = True
|
||||
else:
|
||||
cls.change_schemes = False
|
||||
cls.slice = 'Not selected'
|
||||
else:
|
||||
cls.slice = 'Not selected'
|
||||
cls.change_schemes = False
|
||||
|
||||
if len(cls.path) == 3:
|
||||
tree_iter3 = model.get_iter(cls.path[:3])
|
||||
cls.label = model.get_value(tree_iter3, 0)
|
||||
else:
|
||||
cls.label = 'Not selected'
|
||||
|
||||
# Get previous partition info for context
|
||||
if len(cls.path) == 2 and cls.path[1] > 0 and cls.scheme == "GPT":
|
||||
path_behind = f'{cls.path[0]}:{str(int(cls.path[1] - 1))}'
|
||||
tree_iter4 = model.get_iter(path_behind)
|
||||
cls.mount_point_behind = model.get_value(tree_iter4, 2)
|
||||
cls.fs_behind = model.get_value(tree_iter4, 3)
|
||||
elif len(cls.path) == 3 and cls.path[2] > 0 and cls.scheme == "MBR":
|
||||
path1 = cls.path[0]
|
||||
path2 = str(cls.path[1])
|
||||
path3 = str(int(cls.path[2] - 1))
|
||||
path_behind2 = f'{path1}:{path2}:{path3}'
|
||||
tree_iter1 = model.get_iter(path_behind2)
|
||||
cls.mount_point_behind = model.get_value(tree_iter1, 2)
|
||||
cls.fs_behind = model.get_value(tree_iter1, 3)
|
||||
else:
|
||||
cls.mount_point_behind = None
|
||||
cls.fs_behind = None
|
||||
|
||||
# Set button states based on selection
|
||||
if 'freespace' in cls.slice:
|
||||
cls.create_bt.set_sensitive(True)
|
||||
cls.delete_bt.set_sensitive(False)
|
||||
cls.auto_bt.set_sensitive(True)
|
||||
# Scan for efi partition
|
||||
for num in range(cls.path[1]):
|
||||
partition_path = f"{cls.path[0]}:{num}"
|
||||
tree_iter_1 = model.get_iter(partition_path)
|
||||
first_fs = model.get_value(tree_iter_1, 3)
|
||||
if first_fs == "UEFI" or 'efi' in first_fs:
|
||||
cls.efi_exist = True
|
||||
break
|
||||
else:
|
||||
cls.efi_exist = False
|
||||
elif 'freespace' in cls.label:
|
||||
if cls.path[1] > 3:
|
||||
cls.create_bt.set_sensitive(False)
|
||||
else:
|
||||
cls.create_bt.set_sensitive(True)
|
||||
cls.auto_bt.set_sensitive(True)
|
||||
cls.delete_bt.set_sensitive(False)
|
||||
elif 's' in cls.slice and len(cls.path) > 1:
|
||||
cls.create_bt.set_sensitive(False)
|
||||
cls.delete_bt.set_sensitive(True)
|
||||
cls.auto_bt.set_sensitive(False)
|
||||
elif 'p' in cls.slice and len(cls.path) > 1:
|
||||
cls.create_bt.set_sensitive(False)
|
||||
cls.delete_bt.set_sensitive(True)
|
||||
cls.auto_bt.set_sensitive(False)
|
||||
else:
|
||||
cls.delete_bt.set_sensitive(False)
|
||||
cls.auto_bt.set_sensitive(False)
|
||||
if DiskPartition.how_partition(cls.disk) == 0:
|
||||
cls.create_bt.set_sensitive(True)
|
||||
elif cls.change_schemes is True:
|
||||
cls.create_bt.set_sensitive(True)
|
||||
else:
|
||||
cls.create_bt.set_sensitive(False)
|
||||
|
||||
# Handle partition validation
|
||||
if InstallationData.new_partition:
|
||||
cls.partitions = InstallationData.new_partition
|
||||
if not cls.partitions:
|
||||
Button.next_button.set_sensitive(False)
|
||||
return None
|
||||
if 'GPT' in InstallationData.scheme:
|
||||
if InstallationData.disk:
|
||||
disk = InstallationData.disk
|
||||
disk_id = cls.disk_index.index(disk)
|
||||
num = 0
|
||||
while True:
|
||||
partition_path = f"{disk_id}:{num}"
|
||||
try:
|
||||
tree_iter_1 = model.get_iter(partition_path)
|
||||
first_fs = model.get_value(tree_iter_1, 3)
|
||||
if 'efi' in first_fs:
|
||||
efi_already_exist = True
|
||||
break
|
||||
except ValueError:
|
||||
efi_already_exist = False
|
||||
break
|
||||
num += 1
|
||||
if 'BOOT' in cls.partitions[0] and bios_type == 'BIOS':
|
||||
if len(cls.partitions) >= 2 and 'ZFS' in cls.partitions[1]:
|
||||
Button.next_button.set_sensitive(True)
|
||||
else:
|
||||
Button.next_button.set_sensitive(False)
|
||||
elif efi_already_exist is True and bios_type == 'UEFI':
|
||||
if 'ZFS' in cls.partitions[0]:
|
||||
Button.next_button.set_sensitive(True)
|
||||
else:
|
||||
Button.next_button.set_sensitive(False)
|
||||
elif len(cls.partitions) >= 2 and 'UEFI' in cls.partitions[0] and 'ZFS' in cls.partitions[1]:
|
||||
Button.next_button.set_sensitive(True)
|
||||
else:
|
||||
Button.next_button.set_sensitive(False)
|
||||
elif 'MBR' in InstallationData.scheme:
|
||||
cls.efi_exist = False
|
||||
if len(cls.partitions) >= 1:
|
||||
if "/boot\n" in cls.partitions[0]:
|
||||
if len(cls.partitions) >= 2 and 'ZFS' in cls.partitions[1]:
|
||||
Button.next_button.set_sensitive(True)
|
||||
else:
|
||||
Button.next_button.set_sensitive(False)
|
||||
elif 'ZFS' in cls.partitions[0]:
|
||||
Button.next_button.set_sensitive(True)
|
||||
else:
|
||||
Button.next_button.set_sensitive(False)
|
||||
else:
|
||||
Button.next_button.set_sensitive(False)
|
||||
else:
|
||||
Button.next_button.set_sensitive(False)
|
||||
else:
|
||||
Button.next_button.set_sensitive(False)
|
||||
|
||||
# Check if any configuration exists to enable revert button
|
||||
path_exist = [
|
||||
bool(InstallationData.create),
|
||||
bool(InstallationData.scheme),
|
||||
bool(InstallationData.disk),
|
||||
bool(InstallationData.slice),
|
||||
bool(InstallationData.delete),
|
||||
bool(InstallationData.destroy),
|
||||
bool(InstallationData.new_partition)
|
||||
]
|
||||
if any(path_exist):
|
||||
cls.revert_bt.set_sensitive(True)
|
||||
else:
|
||||
cls.revert_bt.set_sensitive(False)
|
||||
@@ -0,0 +1,77 @@
|
||||
"""
|
||||
Contains the data class and some commonly use variables
|
||||
"""
|
||||
|
||||
be_name = "default"
|
||||
logo = "/usr/local/lib/install-station/image/logo.png"
|
||||
pc_sysinstall = "/usr/local/sbin/pc-sysinstall"
|
||||
query = "sh /usr/local/lib/install-station/backend-query"
|
||||
tmp = "/tmp"
|
||||
installation_config = f'{tmp}/ghostbsd_installation.cfg'
|
||||
zfs_datasets = "/," \
|
||||
"/home(mountpoint=/home)," \
|
||||
"/tmp(mountpoint=/tmp|exec=on|setuid=off)," \
|
||||
"/usr(mountpoint=/usr|canmount=off)," \
|
||||
"/usr/ports(setuid=off)," \
|
||||
"/usr/src," \
|
||||
"/var(mountpoint=/var|canmount=off)," \
|
||||
"/var/audit(exec=off|setuid=off)," \
|
||||
"/var/crash(exec=off|setuid=off)," \
|
||||
"/var/log(exec=off|setuid=off)," \
|
||||
"/var/mail(atime=on)," \
|
||||
"/var/tmp(setuid=off)"
|
||||
|
||||
|
||||
class InstallationData:
|
||||
"""
|
||||
Centralized data storage for installation configuration
|
||||
"""
|
||||
# Partition configuration
|
||||
destroy: dict = {}
|
||||
delete: list = []
|
||||
new_partition: list = []
|
||||
create: list = []
|
||||
scheme: str = ""
|
||||
disk: str = ""
|
||||
slice: str = ""
|
||||
boot: str = ""
|
||||
|
||||
# ZFS configuration data (instead of zfs_config file)
|
||||
zfs_config_data: list = []
|
||||
|
||||
# UFS configuration data (instead of ufs_config file)
|
||||
ufs_config_data: list = []
|
||||
|
||||
# Installation type and mode
|
||||
install_mode: str = "" # "install" or "try"
|
||||
filesystem_type: str = "" # "zfs", "ufs", or "custom"
|
||||
|
||||
# Language and localization
|
||||
language: str = ""
|
||||
language_code: str = ""
|
||||
|
||||
# Boot manager configuration
|
||||
boot_manager: str = ""
|
||||
|
||||
# Network configuration (for live mode)
|
||||
network_config: dict = {}
|
||||
|
||||
@classmethod
|
||||
def reset(cls):
|
||||
"""Reset all installation data"""
|
||||
cls.destroy = {}
|
||||
cls.delete = []
|
||||
cls.new_partition = []
|
||||
cls.create = []
|
||||
cls.scheme = ""
|
||||
cls.disk = ""
|
||||
cls.slice = ""
|
||||
cls.boot = ""
|
||||
cls.zfs_config_data = []
|
||||
cls.ufs_config_data = []
|
||||
cls.install_mode = ""
|
||||
cls.filesystem_type = ""
|
||||
cls.language = ""
|
||||
cls.language_code = ""
|
||||
cls.boot_manager = ""
|
||||
cls.network_config = {}
|
||||
@@ -0,0 +1,61 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gtk
|
||||
from subprocess import Popen
|
||||
import gettext
|
||||
|
||||
gettext.bindtextdomain('install-station', '/usr/local/share/locale')
|
||||
gettext.textdomain('install-station')
|
||||
_ = gettext.gettext
|
||||
|
||||
lyrics = _("""Installation is complete. You need to restart the
|
||||
computer in order to use the new installation.
|
||||
You can continue to use this live media, although
|
||||
any changes you make or documents you save will
|
||||
not be preserved on reboot.""")
|
||||
|
||||
|
||||
class PyApp:
|
||||
@classmethod
|
||||
def on_reboot(cls, _widget):
|
||||
Popen('shutdown -r now', shell=True)
|
||||
Gtk.main_quit()
|
||||
|
||||
@classmethod
|
||||
def on_close(cls, _widget):
|
||||
Gtk.main_quit()
|
||||
|
||||
def __init__(self):
|
||||
window = Gtk.Window()
|
||||
window.set_border_width(8)
|
||||
window.connect("destroy", Gtk.main_quit)
|
||||
window.set_title(_("Installation Completed"))
|
||||
window.set_icon_from_file("/usr/local/lib/install-station/image/logo.png")
|
||||
box1 = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, homogeneous=False, spacing=0)
|
||||
window.add(box1)
|
||||
box1.show()
|
||||
box2 = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, homogeneous=False, spacing=10)
|
||||
box2.set_border_width(10)
|
||||
box1.pack_start(box2, True, True, 0)
|
||||
box2.show()
|
||||
label = Gtk.Label(label=lyrics)
|
||||
box2.pack_start(label, True, True, 0)
|
||||
box2 = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, homogeneous=False, spacing=10)
|
||||
box2.set_border_width(5)
|
||||
box1.pack_start(box2, False, True, 0)
|
||||
box2.show()
|
||||
table = Gtk.Table(1, 2, True)
|
||||
restart = Gtk.Button(label=_("Restart"))
|
||||
restart.connect("clicked", self.on_reboot)
|
||||
continue_button = Gtk.Button(label=_("Continue"))
|
||||
continue_button.connect("clicked", self.on_close)
|
||||
table.attach(continue_button, 0, 1, 0, 1)
|
||||
table.attach(restart, 1, 2, 0, 1)
|
||||
box2.pack_start(table, True, True, 0)
|
||||
window.show_all()
|
||||
|
||||
|
||||
PyApp()
|
||||
Gtk.main()
|
||||
@@ -0,0 +1,55 @@
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gtk
|
||||
import gettext
|
||||
|
||||
gettext.bindtextdomain('install-station', '/usr/local/share/locale')
|
||||
gettext.textdomain('install-station')
|
||||
_ = gettext.gettext
|
||||
|
||||
|
||||
class ErrorWindow:
|
||||
|
||||
@classmethod
|
||||
def on_close(cls, _widget):
|
||||
Gtk.main_quit()
|
||||
|
||||
def __init__(self):
|
||||
window = Gtk.Window()
|
||||
window.set_border_width(8)
|
||||
window.connect("destroy", Gtk.main_quit)
|
||||
window.set_title(_("Installation Error"))
|
||||
# window.set_icon_from_file("/usr/local/lib/install-station/image/logo.png")
|
||||
box1 = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, homogeneous=False, spacing=0)
|
||||
window.add(box1)
|
||||
box1.show()
|
||||
box2 = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, homogeneous=False, spacing=10)
|
||||
box2.set_border_width(10)
|
||||
box1.pack_start(box2, True, True, 0)
|
||||
box2.show()
|
||||
title = Gtk.Label()
|
||||
title.set_use_markup(True)
|
||||
title_text = _("Installation has failed!")
|
||||
title.set_markup(f'<b><span size="larger">{title_text}</span></b>')
|
||||
label = Gtk.Label()
|
||||
label.set_use_markup(True)
|
||||
url = 'https://github.com/ghostbsd/ghostbsd-src/issues/new/choose'
|
||||
anchor = f"<a href='{url}'>{_('GhostBSD issue system')}</a>"
|
||||
message = _("Please report the issue to {anchor}, and \nbe sure to provide /tmp/.pc-sysinstall/pc-sysinstall.log.").format(anchor=anchor)
|
||||
label.set_markup(message)
|
||||
box2.pack_start(title, True, True, 0)
|
||||
box2.pack_start(label, True, True, 0)
|
||||
box2 = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, homogeneous=False, spacing=10)
|
||||
box2.set_border_width(5)
|
||||
box1.pack_start(box2, False, True, 0)
|
||||
box2.show()
|
||||
table = Gtk.Table(n_rows=1, n_columns=2, homogeneous=True)
|
||||
ok = Gtk.Button(label=_("Ok"))
|
||||
ok.connect("clicked", self.on_close)
|
||||
table.attach(ok, 0, 2, 0, 1)
|
||||
box2.pack_start(table, True, True, 0)
|
||||
window.show_all()
|
||||
|
||||
|
||||
ErrorWindow()
|
||||
Gtk.main()
|
||||
@@ -0,0 +1,140 @@
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gtk, GLib, Gdk
|
||||
import threading
|
||||
from subprocess import Popen, PIPE, STDOUT
|
||||
from time import sleep
|
||||
from install_station.partition import delete_partition, destroy_partition, add_partition
|
||||
from install_station.create_cfg import Configuration
|
||||
from install_station.window import Window
|
||||
from install_station.data import InstallationData, installation_config, pc_sysinstall
|
||||
import sys
|
||||
import gettext
|
||||
|
||||
gettext.bindtextdomain('install-station', '/usr/local/share/locale')
|
||||
gettext.textdomain('install-station')
|
||||
_ = gettext.gettext
|
||||
|
||||
cssProvider = Gtk.CssProvider()
|
||||
cssProvider.load_from_path('/usr/local/lib/install-station/ghostbsd-style.css')
|
||||
screen = Gdk.Screen.get_default()
|
||||
styleContext = Gtk.StyleContext()
|
||||
styleContext.add_provider_for_screen(
|
||||
screen,
|
||||
cssProvider,
|
||||
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION
|
||||
)
|
||||
|
||||
|
||||
def update_progress(progressbar, text):
|
||||
"""
|
||||
This method
|
||||
"""
|
||||
new_val = progressbar.get_fraction() + 0.000003
|
||||
progressbar.set_fraction(new_val)
|
||||
progressbar.set_text(text[0:80])
|
||||
|
||||
|
||||
def read_output(command, progressbar):
|
||||
GLib.idle_add(update_progress, progressbar, _("Creating ghostbsd_installation.cfg"))
|
||||
Configuration.create_cfg()
|
||||
sleep(1)
|
||||
if InstallationData.delete:
|
||||
GLib.idle_add(update_progress, progressbar, _("Deleting partition"))
|
||||
delete_partition()
|
||||
sleep(1)
|
||||
# destroy disk partition and create scheme
|
||||
if InstallationData.destroy:
|
||||
GLib.idle_add(update_progress, progressbar, _("Creating disk partition"))
|
||||
destroy_partition()
|
||||
sleep(1)
|
||||
# create partition
|
||||
if InstallationData.create:
|
||||
GLib.idle_add(update_progress, progressbar, _("Creating new partitions"))
|
||||
add_partition()
|
||||
sleep(1)
|
||||
progressbar_text = None
|
||||
process = Popen(
|
||||
command, shell=True, stdin=PIPE, stdout=PIPE, stderr=STDOUT, close_fds=True, universal_newlines=True
|
||||
)
|
||||
while True:
|
||||
line = process.stdout.readline()
|
||||
if not line:
|
||||
break
|
||||
progressbar_text = line.rstrip()
|
||||
GLib.idle_add(update_progress, progressbar, progressbar_text)
|
||||
# Those for next 4 line is for debugging only.
|
||||
# filer = open(f"{tmp}/tmp", "a")
|
||||
# filer.writelines(progressbar_text)
|
||||
# filer.close
|
||||
print(progressbar_text)
|
||||
if progressbar_text.rstrip() == "Installation finished!":
|
||||
Popen(f'python {install-station_dir}/end.py', shell=True, close_fds=True)
|
||||
else:
|
||||
Popen(f'python {install-station_dir}/error.py', shell=True, close_fds=True)
|
||||
Window.hide()
|
||||
|
||||
|
||||
class InstallWindow:
|
||||
|
||||
def __init__(self):
|
||||
self.vBox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, homogeneous=False, spacing=0)
|
||||
self.vBox.show()
|
||||
label = Gtk.Label(label=_("Installation in progress"), name="Header")
|
||||
label.set_property("height-request", 50)
|
||||
self.vBox.pack_start(label, False, False, 0)
|
||||
|
||||
hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, homogeneous=False, spacing=0, name="install")
|
||||
hbox.show()
|
||||
self.vBox.pack_end(hbox, True, True, 0)
|
||||
vbox2 = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, homogeneous=False, spacing=0)
|
||||
vbox2.show()
|
||||
label2 = Gtk.Label(name="sideText")
|
||||
|
||||
label2.set_markup(_(
|
||||
"Thank you for choosing GhostBSD!\n\n"
|
||||
"We believe every computer operating system should "
|
||||
"be simple, elegant, secure and protect your privacy"
|
||||
" while being easy to use. GhostBSD is simplifying "
|
||||
"FreeBSD for those who lack the technical expertise "
|
||||
"required to use it and lower the entry-level of "
|
||||
"using BSD. \n\nWe hope you'll enjoy our BSD "
|
||||
"operating system."
|
||||
))
|
||||
label2.set_justify(Gtk.Justification.LEFT)
|
||||
label2.set_line_wrap(True)
|
||||
# label2.set_max_width_chars(10)
|
||||
label2.set_alignment(0.0, 0.2)
|
||||
hbox2 = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, homogeneous=False, spacing=0, name="TransBox")
|
||||
hbox2.show()
|
||||
hbox.pack_start(hbox2, True, True, 0)
|
||||
hbox2.pack_start(label2, True, True, 30)
|
||||
image = Gtk.Image()
|
||||
image.set_from_file(f"{install-station_dir}/image/G_logo.gif")
|
||||
# image.set_size_request(width=256, height=256)
|
||||
image.show()
|
||||
hbox.pack_end(image, True, True, 20)
|
||||
|
||||
def get_model(self):
|
||||
return self.vBox
|
||||
|
||||
|
||||
class InstallProgress:
|
||||
|
||||
def __init__(self):
|
||||
self.pbar = Gtk.ProgressBar()
|
||||
self.pbar.set_show_text(True)
|
||||
command = f'sudo {pc_sysinstall} -c {installation_config}'
|
||||
thread = threading.Thread(
|
||||
target=read_output,
|
||||
args=(
|
||||
command,
|
||||
self.pbar
|
||||
),
|
||||
daemon=True
|
||||
)
|
||||
thread.start()
|
||||
self.pbar.show()
|
||||
|
||||
def get_progressbar(self):
|
||||
return self.pbar
|
||||
@@ -0,0 +1,80 @@
|
||||
"""
|
||||
Module to create the inner window for select what type of installation.
|
||||
"""
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gtk, Gdk
|
||||
from install_station.data import InstallationData
|
||||
import gettext
|
||||
|
||||
_ = gettext.gettext
|
||||
|
||||
cssProvider = Gtk.CssProvider()
|
||||
cssProvider.load_from_path('/usr/local/lib/install-station/ghostbsd-style.css')
|
||||
screen = Gdk.Screen.get_default()
|
||||
styleContext = Gtk.StyleContext()
|
||||
styleContext.add_provider_for_screen(
|
||||
screen,
|
||||
cssProvider,
|
||||
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION
|
||||
)
|
||||
|
||||
|
||||
class InstallTypes:
|
||||
# Class variables instead of instance variables
|
||||
ne = 'zfs'
|
||||
vbox1 = None
|
||||
|
||||
@classmethod
|
||||
def filesystem_type(cls, widget, val):
|
||||
# Only respond to activation, not deactivation
|
||||
if widget.get_active():
|
||||
cls.ne = val
|
||||
InstallationData.filesystem_type = val
|
||||
return
|
||||
|
||||
@classmethod
|
||||
def get_type(cls):
|
||||
return InstallationData.filesystem_type or cls.ne
|
||||
|
||||
@classmethod
|
||||
def get_model(cls):
|
||||
if cls.vbox1 is None:
|
||||
cls.initialize()
|
||||
return cls.vbox1
|
||||
|
||||
@classmethod
|
||||
def initialize(cls):
|
||||
cls.vbox1 = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, homogeneous=False, spacing=0)
|
||||
cls.vbox1.show()
|
||||
vbox2 = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, homogeneous=False, spacing=0)
|
||||
hbox1 = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, homogeneous=False, spacing=0)
|
||||
InstallationData.filesystem_type = cls.ne
|
||||
cls.vbox1.pack_start(hbox1, True, False, 0)
|
||||
hbox1.set_halign(Gtk.Align.CENTER)
|
||||
label = Gtk.Label(label=_("How do you want to install GhostBSD?"))
|
||||
label.set_alignment(0, 0.5)
|
||||
vbox2.pack_start(label, False, False, 10)
|
||||
# Create radio button group
|
||||
cls.full_zfs_button = Gtk.RadioButton(
|
||||
label=_("<b>Disks Configuration</b>\nInstall GhostBSD using Stripe, Mirror, RAIDZ1, RAIDZ2, or RAIDZ3 configurations.")
|
||||
)
|
||||
cls.full_zfs_button.get_child().set_use_markup(True)
|
||||
cls.full_zfs_button.get_child().set_line_wrap(True)
|
||||
vbox2.pack_start(cls.full_zfs_button, True, True, 10)
|
||||
cls.full_zfs_button.connect("toggled", cls.filesystem_type, "zfs")
|
||||
cls.full_zfs_button.show()
|
||||
|
||||
cls.custom_button = Gtk.RadioButton.new_with_label_from_widget(
|
||||
cls.full_zfs_button,
|
||||
_("<b>Multi-Boot Configuration</b>\nInstall GhostBSD with ZFS alongside other operating systems.")
|
||||
)
|
||||
cls.custom_button.get_child().set_use_markup(True)
|
||||
cls.custom_button.get_child().set_line_wrap(True)
|
||||
vbox2.pack_start(cls.custom_button, False, True, 10)
|
||||
cls.custom_button.connect("toggled", cls.filesystem_type, "custom")
|
||||
cls.custom_button.show()
|
||||
|
||||
hbox1.pack_start(vbox2, True, False, 150)
|
||||
vbox2.set_halign(Gtk.Align.CENTER)
|
||||
cls.full_zfs_button.set_active(True)
|
||||
@@ -0,0 +1,242 @@
|
||||
from gi.repository import Gtk
|
||||
import gettext
|
||||
from install_station.install import InstallProgress, InstallWindow
|
||||
from install_station.partition import DiskPartition
|
||||
from install_station.window import Window
|
||||
from install_station.data import InstallationData
|
||||
|
||||
gettext.bindtextdomain('install-station', '/usr/local/share/locale')
|
||||
gettext.textdomain('install-station')
|
||||
_ = gettext.gettext
|
||||
|
||||
|
||||
class Button:
|
||||
back_button = Gtk.Button(label=_('Back'))
|
||||
"""This button is used to go back to the previous page."""
|
||||
cancel_button = Gtk.Button(label=_('Cancel'))
|
||||
"""This button is used to quit and clean up."""
|
||||
next_button = Gtk.Button(label=_('Next'))
|
||||
"""This button is used to go to the next page."""
|
||||
_box = None
|
||||
|
||||
@classmethod
|
||||
def hide_all(cls):
|
||||
"""
|
||||
This method hides all buttons.
|
||||
"""
|
||||
cls.back_button.hide()
|
||||
cls.cancel_button.hide()
|
||||
cls.next_button.hide()
|
||||
|
||||
@classmethod
|
||||
def show_initial(cls):
|
||||
"""
|
||||
This method shows the initial buttons. Cancel and Next.
|
||||
"""
|
||||
cls.cancel_button.show()
|
||||
cls.next_button.show()
|
||||
|
||||
@classmethod
|
||||
def show_back(cls):
|
||||
"""
|
||||
This method shows the back button.
|
||||
"""
|
||||
cls.back_button.show()
|
||||
|
||||
@classmethod
|
||||
def hide_back(cls):
|
||||
"""
|
||||
This method hides the back button.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def box(cls):
|
||||
"""
|
||||
This method creates a box container of buttons aligned to the right.
|
||||
|
||||
Returns:
|
||||
Box container with buttons aligned to the right for navigation.
|
||||
"""
|
||||
if cls._box is None:
|
||||
# Use Box instead of Grid for better right-alignment control
|
||||
cls._box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, homogeneous=False, spacing=5)
|
||||
cls._box.set_halign(Gtk.Align.END) # Align the entire box to the right
|
||||
|
||||
cls.back_button.connect("clicked", Interface.back_page)
|
||||
cls._box.pack_start(cls.back_button, False, False, 0)
|
||||
|
||||
cls.cancel_button.connect("clicked", Interface.delete)
|
||||
cls._box.pack_start(cls.cancel_button, False, False, 0)
|
||||
|
||||
cls.next_button.connect("clicked", Interface.next_page)
|
||||
cls._box.pack_start(cls.next_button, False, False, 0)
|
||||
|
||||
cls._box.show()
|
||||
return cls._box
|
||||
|
||||
|
||||
class Interface:
|
||||
welcome = None
|
||||
installation_type = None
|
||||
custom_partition = None
|
||||
full_zfs = None
|
||||
boot_manager = None
|
||||
network_setup = None
|
||||
page = Gtk.Notebook()
|
||||
|
||||
@classmethod
|
||||
def get_interface(cls):
|
||||
interface_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, homogeneous=False, spacing=0)
|
||||
interface_box.show()
|
||||
interface_box.pack_start(cls.page, True, True, 0)
|
||||
cls.page.show()
|
||||
cls.page.set_show_tabs(False)
|
||||
cls.page.set_show_border(False)
|
||||
welcome_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, homogeneous=False, spacing=0)
|
||||
welcome_box.show()
|
||||
cls.welcome.initialize()
|
||||
get_types = cls.welcome.get_model()
|
||||
welcome_box.pack_start(get_types, True, True, 0)
|
||||
Window.set_title(_("Welcome to GhostBSD"))
|
||||
label = Gtk.Label(label=_("Welcome to GhostBSD"))
|
||||
cls.page.insert_page(welcome_box, label, 0)
|
||||
# Set what page to start at type of installation
|
||||
cls.page.set_current_page(0)
|
||||
cls.nbButton = Gtk.Notebook()
|
||||
interface_box.pack_end(cls.nbButton, False, False, 5)
|
||||
cls.nbButton.show()
|
||||
cls.nbButton.set_show_tabs(False)
|
||||
cls.nbButton.set_show_border(False)
|
||||
label = Gtk.Label(label=_("Button"))
|
||||
cls.nbButton.insert_page(Button.box(), label, 0)
|
||||
return interface_box
|
||||
|
||||
@classmethod
|
||||
def delete(cls, _widget, _event=None):
|
||||
"""Close the main window."""
|
||||
InstallationData.reset()
|
||||
Gtk.main_quit()
|
||||
|
||||
@classmethod
|
||||
def next_page(cls, _widget):
|
||||
if InstallationData.install_mode == "install":
|
||||
cls.next_install_page()
|
||||
else:
|
||||
cls.next_setup_page()
|
||||
|
||||
@classmethod
|
||||
def next_install_page(cls):
|
||||
"""Go to the next window."""
|
||||
page = cls.page.get_current_page()
|
||||
if page == 0:
|
||||
type_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, homogeneous=False, spacing=0)
|
||||
type_box.show()
|
||||
get_types = cls.installation_type.get_model()
|
||||
type_box.pack_start(get_types, True, True, 0)
|
||||
label = Gtk.Label(label=_("Installation Types"))
|
||||
cls.page.insert_page(type_box, label, 1)
|
||||
cls.page.next_page()
|
||||
cls.page.show_all()
|
||||
Button.show_initial()
|
||||
elif page == 1:
|
||||
Button.show_back()
|
||||
if InstallationData.filesystem_type == "custom":
|
||||
custom_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, homogeneous=False, spacing=0)
|
||||
custom_box.show()
|
||||
get_part = cls.custom_partition.get_model()
|
||||
custom_box.pack_start(get_part, True, True, 0)
|
||||
label = Gtk.Label(label=_("Custom Configuration"))
|
||||
cls.page.insert_page(custom_box, label, 2)
|
||||
cls.page.next_page()
|
||||
cls.page.show_all()
|
||||
Button.next_button.set_sensitive(False)
|
||||
elif InstallationData.filesystem_type == "zfs":
|
||||
zfs_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, homogeneous=False, spacing=0)
|
||||
zfs_box.show()
|
||||
get_zfs = cls.full_zfs.get_model()
|
||||
zfs_box.pack_start(get_zfs, True, True, 0)
|
||||
label = Gtk.Label(label=_("ZFS Configuration"))
|
||||
cls.page.insert_page(zfs_box, label, 2)
|
||||
cls.page.next_page()
|
||||
cls.page.show_all()
|
||||
Button.next_button.set_sensitive(False)
|
||||
elif page == 2:
|
||||
boot_manager_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, homogeneous=False, spacing=0)
|
||||
boot_manager_box.show()
|
||||
get_root = cls.boot_manager.get_model()
|
||||
boot_manager_box.pack_start(get_root, True, True, 0)
|
||||
label = Gtk.Label(label=_("Boot Option"))
|
||||
cls.page.insert_page(boot_manager_box, label, 3)
|
||||
Button.next_button.set_label(_("Install"))
|
||||
cls.page.next_page()
|
||||
cls.page.show_all()
|
||||
Button.next_button.set_sensitive(True)
|
||||
elif page == 3:
|
||||
installation_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, homogeneous=False, spacing=0)
|
||||
installation_box.show()
|
||||
install_window = InstallWindow()
|
||||
get_install = install_window.get_model()
|
||||
installation_box.pack_start(get_install, True, True, 0)
|
||||
label = Gtk.Label(label=_("Installation Progress"))
|
||||
cls.page.insert_page(installation_box, label, 8)
|
||||
cls.page.next_page()
|
||||
installation_progressbar = InstallProgress()
|
||||
progressbar = installation_progressbar.get_progressbar()
|
||||
box1 = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, homogeneous=False, spacing=0)
|
||||
box1.show()
|
||||
label = Gtk.Label(label=_("Progress Bar"))
|
||||
box1.pack_end(progressbar, False, False, 0)
|
||||
cls.nbButton.insert_page(box1, label, 4)
|
||||
cls.nbButton.next_page()
|
||||
current_page_widget = cls.page.get_nth_page(cls.page.get_current_page())
|
||||
title_text = cls.page.get_tab_label_text(current_page_widget)
|
||||
Window.set_title(title_text)
|
||||
|
||||
@classmethod
|
||||
def next_setup_page(cls):
|
||||
page = cls.page.get_current_page()
|
||||
if page == 0:
|
||||
Button.next_button.show()
|
||||
Button.next_button.set_sensitive(False)
|
||||
Window.set_title(_("Network Setup"))
|
||||
net_setup_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, homogeneous=False, spacing=0)
|
||||
net_setup_box.show()
|
||||
model = cls.network_setup.get_model()
|
||||
net_setup_box.pack_start(model, True, True, 0)
|
||||
label = Gtk.Label(label=_("Network Setup"))
|
||||
cls.page.insert_page(net_setup_box, label, 1)
|
||||
cls.page.next_page()
|
||||
cls.page.show_all()
|
||||
if page == 1:
|
||||
with open('/usr/home/ghostbsd/.xinitrc', 'w') as xinitrc:
|
||||
xinitrc.writelines('gsettings set org.mate.SettingsDaemon.plugins.housekeeping active true &\n')
|
||||
xinitrc.writelines('gsettings set org.mate.screensaver lock-enabled false &\n')
|
||||
xinitrc.writelines('exec ck-launch-session mate-session\n')
|
||||
Gtk.main_quit()
|
||||
|
||||
@classmethod
|
||||
def back_page(cls, _widget):
|
||||
"""Go back to the previous window."""
|
||||
current_page = cls.page.get_current_page()
|
||||
if current_page == 1:
|
||||
Button.hide_back()
|
||||
if current_page == 2:
|
||||
Button.hide_back()
|
||||
elif current_page == 3:
|
||||
Button.next_button.set_label(_("Next"))
|
||||
cls.page.prev_page()
|
||||
new_page = cls.page.get_current_page()
|
||||
if current_page == 2 and new_page == 1:
|
||||
# Reset partition configuration data when going back
|
||||
InstallationData.destroy = {}
|
||||
InstallationData.delete = []
|
||||
InstallationData.create = []
|
||||
InstallationData.new_partition = []
|
||||
InstallationData.scheme = ""
|
||||
InstallationData.disk = ""
|
||||
InstallationData.slice = ""
|
||||
InstallationData.zfs_config_data = []
|
||||
InstallationData.ufs_config_data = []
|
||||
# Clean up temporary directory if it exists
|
||||
DiskPartition.create_partition_database()
|
||||
Button.next_button.set_sensitive(True)
|
||||
@@ -0,0 +1,143 @@
|
||||
from gi.repository import Gtk, Gdk
|
||||
import os
|
||||
from install_station.system_calls import (
|
||||
language_dictionary,
|
||||
localize_system
|
||||
)
|
||||
from install_station.data import InstallationData, tmp, logo
|
||||
import gettext
|
||||
|
||||
gettext.bindtextdomain('install-station', '/usr/local/share/locale')
|
||||
gettext.textdomain('install-station')
|
||||
_ = gettext.gettext
|
||||
|
||||
# Ensure temp directory exists
|
||||
if not os.path.exists(tmp):
|
||||
os.makedirs(tmp)
|
||||
|
||||
lang_dictionary = language_dictionary()
|
||||
# Text to be replace be multiple language file.
|
||||
welltext = _("Select the language you want to use with GhostBSD.")
|
||||
|
||||
cssProvider = Gtk.CssProvider()
|
||||
cssProvider.load_from_path('/usr/local/lib/install-station/ghostbsd-style.css')
|
||||
screen = Gdk.Screen.get_default()
|
||||
styleContext = Gtk.StyleContext()
|
||||
styleContext.add_provider_for_screen(
|
||||
screen,
|
||||
cssProvider,
|
||||
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION
|
||||
)
|
||||
|
||||
|
||||
class Language:
|
||||
_instance = None
|
||||
|
||||
def __new__(cls):
|
||||
"""Implement singleton pattern."""
|
||||
if cls._instance is None:
|
||||
cls._instance = super(Language, cls).__new__(cls)
|
||||
cls._instance._initialized = False
|
||||
return cls._instance
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize only once."""
|
||||
if not self._initialized:
|
||||
self._language = None
|
||||
self._vbox1 = None
|
||||
self._initialize_ui()
|
||||
self._initialized = True
|
||||
|
||||
# On selection it overwrite the default language file.
|
||||
def _language_selection(self, tree_selection):
|
||||
model, treeiter = tree_selection.get_selected()
|
||||
if treeiter is not None:
|
||||
value = model[treeiter][0]
|
||||
self._language = lang_dictionary[value]
|
||||
InstallationData.language = value
|
||||
InstallationData.language_code = lang_dictionary[value]
|
||||
return
|
||||
|
||||
def _setup_language_columns(self, treeView):
|
||||
cell = Gtk.CellRendererText()
|
||||
column = Gtk.TreeViewColumn(None, cell, text=0)
|
||||
column_header = Gtk.Label(label=_('Language'))
|
||||
column_header.set_use_markup(True)
|
||||
column_header.show()
|
||||
column.set_widget(column_header)
|
||||
column.set_sort_column_id(0)
|
||||
treeView.append_column(column)
|
||||
return
|
||||
|
||||
# Initial definition.
|
||||
def _initialize_ui(self):
|
||||
# Add a Default vertical box
|
||||
self._vbox1 = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, homogeneous=False, spacing=0)
|
||||
self._vbox1.show()
|
||||
# Add a second vertical box
|
||||
grid = Gtk.Grid()
|
||||
title = Gtk.Label(label=_('Welcome To GhostBSD!'), name="Header")
|
||||
title.set_property("height-request", 50)
|
||||
self._vbox1.pack_start(title, False, False, 0)
|
||||
self._vbox1.pack_start(grid, True, True, 0)
|
||||
grid.set_row_spacing(10)
|
||||
grid.set_column_spacing(3)
|
||||
grid.set_column_homogeneous(True)
|
||||
grid.set_row_homogeneous(True)
|
||||
grid.set_margin_left(10)
|
||||
grid.set_margin_right(10)
|
||||
grid.set_margin_top(10)
|
||||
grid.set_margin_bottom(10)
|
||||
# Adding a Scrolling Window
|
||||
sw = Gtk.ScrolledWindow()
|
||||
sw.set_shadow_type(Gtk.ShadowType.ETCHED_IN)
|
||||
sw.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
|
||||
# Adding a treestore and store language in it.
|
||||
store = Gtk.TreeStore(str)
|
||||
for line in lang_dictionary:
|
||||
store.append(None, [line])
|
||||
treeview = Gtk.TreeView()
|
||||
treeview.set_model(store)
|
||||
treeview.set_rules_hint(True)
|
||||
self._setup_language_columns(treeview)
|
||||
tree_selection = treeview.get_selection()
|
||||
tree_selection.set_mode(Gtk.SelectionMode.SINGLE)
|
||||
tree_selection.connect("changed", self._language_selection)
|
||||
sw.add(treeview)
|
||||
sw.show()
|
||||
grid.attach(sw, 1, 2, 1, 9)
|
||||
# add text in a label.
|
||||
vbox2 = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, homogeneous=False, spacing=0)
|
||||
vbox2.set_border_width(10)
|
||||
vbox2.show()
|
||||
wellcome_text = Gtk.Label(label=welltext)
|
||||
wellcome_text.set_use_markup(True)
|
||||
table = Gtk.Table()
|
||||
table.attach(wellcome_text, 0, 1, 3, 4)
|
||||
vbox2.pack_start(table, False, False, 5)
|
||||
image = Gtk.Image()
|
||||
image.set_from_file(logo)
|
||||
image.show()
|
||||
# grid.attach(self.wellcome, 1, 1, 3, 1)
|
||||
vbox2.pack_start(image, True, True, 5)
|
||||
grid.attach(vbox2, 2, 2, 2, 9)
|
||||
grid.show()
|
||||
|
||||
@classmethod
|
||||
def get_model(cls):
|
||||
return cls()._vbox1
|
||||
|
||||
@classmethod
|
||||
def get_language(cls):
|
||||
"""Get the selected language."""
|
||||
return InstallationData.language_code or cls()._language
|
||||
|
||||
def save_selection(self):
|
||||
# Language is now saved in InstallationData automatically
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def save_language(cls):
|
||||
language_code = InstallationData.language_code or cls()._language
|
||||
if language_code:
|
||||
localize_system(language_code)
|
||||
@@ -0,0 +1,339 @@
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gtk, Gdk, GLib, GdkPixbuf
|
||||
import re
|
||||
import _thread
|
||||
from time import sleep
|
||||
from NetworkMgr.net_api import (
|
||||
networkdictionary,
|
||||
connectToSsid,
|
||||
delete_ssid_wpa_supplicant_config,
|
||||
nic_status
|
||||
)
|
||||
import gettext
|
||||
|
||||
gettext.bindtextdomain('install-station', '/usr/local/share/locale')
|
||||
gettext.textdomain('install-station')
|
||||
_ = gettext.gettext
|
||||
|
||||
logo = "/usr/local/lib/install-station/logo.png"
|
||||
cssProvider = Gtk.CssProvider()
|
||||
cssProvider.load_from_path('/usr/local/lib/install-station/ghostbsd-style.css')
|
||||
screen = Gdk.Screen.get_default()
|
||||
styleContext = Gtk.StyleContext()
|
||||
styleContext.add_provider_for_screen(
|
||||
screen,
|
||||
cssProvider,
|
||||
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION
|
||||
)
|
||||
|
||||
|
||||
class NetworkSetup:
|
||||
_instance = None
|
||||
|
||||
def __new__(cls, button3=None):
|
||||
"""Implement singleton pattern."""
|
||||
if cls._instance is None:
|
||||
cls._instance = super(NetworkSetup, cls).__new__(cls)
|
||||
cls._instance._initialized = False
|
||||
return cls._instance
|
||||
|
||||
@classmethod
|
||||
def get_model(cls):
|
||||
return cls().vbox1
|
||||
|
||||
@staticmethod
|
||||
def wifi_stat(bar):
|
||||
if bar > 75:
|
||||
return 'nm-signal-100'
|
||||
elif bar > 50:
|
||||
return 'nm-signal-75'
|
||||
elif bar > 25:
|
||||
return 'nm-signal-50'
|
||||
elif bar > 5:
|
||||
return 'nm-signal-25'
|
||||
else:
|
||||
return 'nm-signal-00'
|
||||
|
||||
def update_network_detection(self):
|
||||
cards = self.network_info['cards']
|
||||
card_list = list(cards.keys())
|
||||
r = re.compile("wlan")
|
||||
wlan_list = list(filter(r.match, card_list))
|
||||
wire_list = list(set(card_list).difference(wlan_list))
|
||||
cards = self.network_info['cards']
|
||||
if wire_list:
|
||||
for card in wire_list:
|
||||
if cards[card]['state']['connection'] == 'Connected':
|
||||
|
||||
wire_text = _('Network card connected to the internet')
|
||||
self.wire_connection_image.set_from_stock(Gtk.STOCK_YES, 5)
|
||||
print('Connected True')
|
||||
self.next_button.set_sensitive(True)
|
||||
break
|
||||
else:
|
||||
wire_text = _('Network card not connected to the internet')
|
||||
self.wire_connection_image.set_from_stock(Gtk.STOCK_NO, 5)
|
||||
else:
|
||||
wire_text = _('No network card detected')
|
||||
self.wire_connection_image.set_from_stock(Gtk.STOCK_NO, 5)
|
||||
|
||||
self.wire_connection_label.set_label(wire_text)
|
||||
|
||||
if wlan_list:
|
||||
for wlan_card in wlan_list:
|
||||
if cards[wlan_card]['state']['connection'] == 'Connected':
|
||||
wifi_text = _('WiFi card detected and connected to an access point')
|
||||
self.wifi_connection_image.set_from_stock(Gtk.STOCK_YES, 5)
|
||||
break
|
||||
else:
|
||||
wifi_text = _('WiFi card detected but not connected to an access point')
|
||||
self.wifi_connection_image.set_from_stock(Gtk.STOCK_NO, 5)
|
||||
else:
|
||||
wifi_text = _("WiFi card not detected or not supported")
|
||||
self.wifi_connection_image.set_from_stock(Gtk.STOCK_NO, 5)
|
||||
|
||||
self.wifi_connection_label.set_label(wifi_text)
|
||||
|
||||
def __init__(self, next_button=None):
|
||||
"""Initialize only once."""
|
||||
if not self._initialized:
|
||||
self.next_button = next_button
|
||||
self._initialize_ui()
|
||||
self._initialized = True
|
||||
|
||||
def _initialize_ui(self):
|
||||
self.network_info = networkdictionary()
|
||||
print(self.network_info)
|
||||
self.vbox1 = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, homogeneous=False, spacing=0)
|
||||
self.vbox1.show()
|
||||
title = Gtk.Label(label=_('Network Setup'), name="Header")
|
||||
title.set_property("height-request", 50)
|
||||
self.vbox1.pack_start(title, False, False, 0)
|
||||
cards = self.network_info['cards']
|
||||
card_list = list(cards.keys())
|
||||
r = re.compile("wlan")
|
||||
wlan_list = list(filter(r.match, card_list))
|
||||
wire_list = list(set(card_list).difference(wlan_list))
|
||||
|
||||
self.wire_connection_label = Gtk.Label()
|
||||
self.wire_connection_label.set_xalign(0.01)
|
||||
self.wire_connection_image = Gtk.Image()
|
||||
self.wifi_connection_label = Gtk.Label()
|
||||
self.wifi_connection_label.set_xalign(0.01)
|
||||
self.wifi_connection_image = Gtk.Image()
|
||||
|
||||
if wire_list:
|
||||
for card in wire_list:
|
||||
if cards[card]['state']['connection'] == 'Connected':
|
||||
wire_text = _('Network card connected to the internet')
|
||||
self.wire_connection_image.set_from_stock(Gtk.STOCK_YES, 5)
|
||||
print('Connected True')
|
||||
self.next_button.set_sensitive(True)
|
||||
break
|
||||
else:
|
||||
wire_text = _('Network card not connected to the internet')
|
||||
self.wire_connection_image.set_from_stock(Gtk.STOCK_NO, 5)
|
||||
else:
|
||||
wire_text = _('No network card detected')
|
||||
self.wire_connection_image.set_from_stock(Gtk.STOCK_NO, 5)
|
||||
|
||||
self.wire_connection_label.set_label(wire_text)
|
||||
wlan_card = ""
|
||||
if wlan_list:
|
||||
for wlan_card in wlan_list:
|
||||
if cards[wlan_card]['state']['connection'] == 'Connected':
|
||||
wifi_text = _('WiFi card detected and connected to an access point')
|
||||
self.wifi_connection_image.set_from_stock(Gtk.STOCK_YES, 5)
|
||||
break
|
||||
else:
|
||||
wifi_text = _('WiFi card detected but not connected to an access point')
|
||||
self.wifi_connection_image.set_from_stock(Gtk.STOCK_NO, 5)
|
||||
else:
|
||||
wifi_text = _('WiFi card not detected or not supported')
|
||||
self.wifi_connection_image.set_from_stock(Gtk.STOCK_NO, 5)
|
||||
|
||||
self.wifi_connection_label.set_label(wifi_text)
|
||||
|
||||
self.connection_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, homogeneous=True, spacing=20)
|
||||
if wlan_card:
|
||||
# add a default card variable
|
||||
sw = Gtk.ScrolledWindow()
|
||||
sw.set_shadow_type(Gtk.ShadowType.ETCHED_IN)
|
||||
sw.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
|
||||
self.store = Gtk.ListStore(GdkPixbuf.Pixbuf, str, str)
|
||||
for ssid in self.network_info['cards'][wlan_card]['info']:
|
||||
ssid_info = self.network_info['cards'][wlan_card]['info'][ssid]
|
||||
bar = ssid_info[4]
|
||||
stat = NetworkSetup.wifi_stat(bar)
|
||||
pixbuf = Gtk.IconTheme.get_default().load_icon(stat, 32, 0)
|
||||
self.store.append([pixbuf, ssid, f'{ssid_info}'])
|
||||
treeview = Gtk.TreeView()
|
||||
treeview.set_model(self.store)
|
||||
treeview.set_rules_hint(True)
|
||||
pixbuf_cell = Gtk.CellRendererPixbuf()
|
||||
pixbuf_column = Gtk.TreeViewColumn('Stat', pixbuf_cell)
|
||||
pixbuf_column.add_attribute(pixbuf_cell, "pixbuf", 0)
|
||||
pixbuf_column.set_resizable(True)
|
||||
treeview.append_column(pixbuf_column)
|
||||
cell = Gtk.CellRendererText()
|
||||
column = Gtk.TreeViewColumn('SSID', cell, text=1)
|
||||
column.set_sort_column_id(1)
|
||||
treeview.append_column(column)
|
||||
tree_selection = treeview.get_selection()
|
||||
tree_selection.set_mode(Gtk.SelectionMode.SINGLE)
|
||||
tree_selection.connect("changed", self.wifi_setup, wlan_card)
|
||||
sw.add(treeview)
|
||||
self.connection_box.pack_start(sw, True, True, 50)
|
||||
|
||||
main_grid = Gtk.Grid()
|
||||
main_grid.set_row_spacing(10)
|
||||
main_grid.set_column_spacing(10)
|
||||
main_grid.set_column_homogeneous(True)
|
||||
main_grid.set_row_homogeneous(True)
|
||||
self.vbox1.pack_start(main_grid, True, True, 10)
|
||||
main_grid.attach(self.wire_connection_image, 2, 1, 1, 1)
|
||||
main_grid.attach(self.wire_connection_label, 3, 1, 8, 1)
|
||||
main_grid.attach(self.wifi_connection_image, 2, 2, 1, 1)
|
||||
main_grid.attach(self.wifi_connection_label, 3, 2, 8, 1)
|
||||
main_grid.attach(self.connection_box, 1, 4, 10, 5)
|
||||
|
||||
def wifi_setup(self, tree_selection, wifi_card):
|
||||
model, treeiter = tree_selection.get_selected()
|
||||
if treeiter is not None:
|
||||
ssid = model[treeiter][1]
|
||||
ssid_info = self.network_info['cards'][wifi_card]['info'][ssid]
|
||||
caps = ssid_info[6]
|
||||
print(ssid) # added the code to authenticate.
|
||||
print(ssid_info)
|
||||
if caps == 'E' or caps == 'ES':
|
||||
if f'"{ssid}"' in open("/etc/wpa_supplicant.conf").read():
|
||||
self.try_to_connect_to_ssid(ssid, ssid_info, wifi_card)
|
||||
else:
|
||||
NetworkSetup.open_wpa_supplicant(ssid)
|
||||
self.try_to_connect_to_ssid(ssid, ssid_info, wifi_card)
|
||||
else:
|
||||
if f'"{ssid}"' in open('/etc/wpa_supplicant.conf').read():
|
||||
self.try_to_connect_to_ssid(ssid, ssid_info, wifi_card)
|
||||
else:
|
||||
self.authentication(ssid_info, wifi_card, False)
|
||||
|
||||
def add_to_wpa_supplicant(self, _widget, ssid_info, card):
|
||||
pwd = self.password.get_text()
|
||||
NetworkSetup.setup_wpa_supplicant(ssid_info[0], ssid_info, pwd)
|
||||
_thread.start_new_thread(
|
||||
self.try_to_connect_to_ssid,
|
||||
(ssid_info[0], ssid_info, card)
|
||||
)
|
||||
self.window.hide()
|
||||
|
||||
def try_to_connect_to_ssid(self, ssid, ssid_info, card):
|
||||
if connectToSsid(ssid, card) is False:
|
||||
delete_ssid_wpa_supplicant_config(ssid)
|
||||
GLib.idle_add(self.restart_authentication, ssid_info, card)
|
||||
else:
|
||||
for _ in list(range(30)):
|
||||
if nic_status(card) == 'associated':
|
||||
self.network_info = networkdictionary()
|
||||
print(self.network_info)
|
||||
self.update_network_detection()
|
||||
break
|
||||
sleep(1)
|
||||
else:
|
||||
delete_ssid_wpa_supplicant_config(ssid)
|
||||
GLib.idle_add(self.restart_authentication, ssid_info, card)
|
||||
return
|
||||
|
||||
def restart_authentication(self, ssid_info, card):
|
||||
self.authentication(ssid_info, card, True)
|
||||
|
||||
def on_check(self, widget):
|
||||
if widget.get_active():
|
||||
self.password.set_visibility(True)
|
||||
else:
|
||||
self.password.set_visibility(False)
|
||||
|
||||
def authentication(self, ssid_info, card, failed):
|
||||
self.window = Gtk.Window()
|
||||
self.window.set_title(_("Wi-Fi Network Authentication Required"))
|
||||
self.window.set_border_width(0)
|
||||
self.window.set_size_request(500, 200)
|
||||
box1 = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, homogeneous=False, spacing=0)
|
||||
self.window.add(box1)
|
||||
box1.show()
|
||||
box2 = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, homogeneous=False, spacing=10)
|
||||
box2.set_border_width(10)
|
||||
box1.pack_start(box2, True, True, 0)
|
||||
box2.show()
|
||||
# Creating MBR or GPT drive
|
||||
if failed:
|
||||
title = _("{ssid} Wi-Fi Network Authentication failed").format(ssid=ssid_info[0])
|
||||
else:
|
||||
title = _("Authentication required by {ssid} Wi-Fi Network").format(ssid=ssid_info[0])
|
||||
label = Gtk.Label(label=f"<b><span size='large'>{title}</span></b>")
|
||||
label.set_use_markup(True)
|
||||
pwd_label = Gtk.Label(label=_("Password:"))
|
||||
self.password = Gtk.Entry()
|
||||
self.password.set_visibility(False)
|
||||
check = Gtk.CheckButton(label=_("Show password"))
|
||||
check.connect("toggled", self.on_check)
|
||||
table = Gtk.Table(1, 2, True)
|
||||
table.attach(label, 0, 5, 0, 1)
|
||||
table.attach(pwd_label, 1, 2, 2, 3)
|
||||
table.attach(self.password, 2, 4, 2, 3)
|
||||
table.attach(check, 2, 4, 3, 4)
|
||||
box2.pack_start(table, False, False, 0)
|
||||
box2 = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, homogeneous=False, spacing=10)
|
||||
box2.set_border_width(5)
|
||||
box1.pack_start(box2, False, True, 0)
|
||||
box2.show()
|
||||
# Add create_scheme button
|
||||
cancel = Gtk.Button(stock=Gtk.STOCK_CANCEL)
|
||||
cancel.connect("clicked", self.close)
|
||||
connect = Gtk.Button(stock=Gtk.STOCK_CONNECT)
|
||||
connect.connect("clicked", self.add_to_wpa_supplicant, ssid_info, card)
|
||||
table = Gtk.Table(1, 2, True)
|
||||
table.set_col_spacings(10)
|
||||
table.attach(connect, 4, 5, 0, 1)
|
||||
table.attach(cancel, 3, 4, 0, 1)
|
||||
box2.pack_end(table, True, True, 5)
|
||||
self.window.show_all()
|
||||
return 'Done'
|
||||
|
||||
def close(self, _widget):
|
||||
self.window.hide()
|
||||
|
||||
@staticmethod
|
||||
def setup_wpa_supplicant(ssid, ssid_info, pwd):
|
||||
if 'RSN' in ssid_info[-1]:
|
||||
ws = '\nnetwork={'
|
||||
ws += f'\n ssid="{ssid}"'
|
||||
ws += '\n key_mgmt=WPA-PSK'
|
||||
ws += '\n proto=RSN'
|
||||
ws += f'\n psk="{pwd}"\n'
|
||||
ws += '}\n'
|
||||
elif 'WPA' in ssid_info[-1]:
|
||||
ws = '\nnetwork={'
|
||||
ws += f'\n ssid="{ssid}"'
|
||||
ws += '\n key_mgmt=WPA-PSK'
|
||||
ws += '\n proto=WPA'
|
||||
ws += f'\n psk="{pwd}"\n'
|
||||
ws += '}\n'
|
||||
else:
|
||||
ws = '\nnetwork={'
|
||||
ws += f'\n ssid="{ssid}"'
|
||||
ws += '\n key_mgmt=NONE'
|
||||
ws += '\n wep_tx_keyidx=0'
|
||||
ws += f'\n wep_key0={pwd}\n'
|
||||
ws += '}\n'
|
||||
wsf = open("/etc/wpa_supplicant.conf", 'a')
|
||||
wsf.writelines(ws)
|
||||
wsf.close()
|
||||
|
||||
@staticmethod
|
||||
def open_wpa_supplicant(ssid):
|
||||
ws = '\nnetwork={'
|
||||
ws += f'\n ssid={ssid}'
|
||||
ws += '\n key_mgmt=NONE\n}\n'
|
||||
with open("/etc/wpa_supplicant.conf", 'a') as wsf:
|
||||
wsf.writelines(ws)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,160 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import re
|
||||
import os
|
||||
from subprocess import Popen, run, PIPE
|
||||
from install_station.data import pc_sysinstall
|
||||
|
||||
|
||||
def replace_pattern(current, new, file):
|
||||
parser_file = open(file, 'r').read()
|
||||
parser_patched = re.sub(current, new, parser_file)
|
||||
save_parser_file = open(file, 'w')
|
||||
save_parser_file.writelines(parser_patched)
|
||||
save_parser_file.close()
|
||||
|
||||
|
||||
def language_dictionary():
|
||||
langs = Popen(f'{pc_sysinstall} query-langs', shell=True, stdin=PIPE,
|
||||
stdout=PIPE, universal_newlines=True,
|
||||
close_fds=True).stdout.readlines()
|
||||
dictionary = {}
|
||||
for line in langs:
|
||||
lang_list = line.rstrip()
|
||||
lang_name = lang_list.partition(' ')[2]
|
||||
lang_code = lang_list.partition(' ')[0]
|
||||
dictionary[lang_name] = lang_code
|
||||
return dictionary
|
||||
|
||||
|
||||
def localize_system(locale):
|
||||
slick_greeter = "/usr/local/share/xgreeters/slick-greeter.desktop"
|
||||
gtk_greeter = "/usr/local/share/xgreeters/lightdm-gtk-greeter.desktop"
|
||||
replace_pattern('lang=C', f'lang={locale}', '/etc/login.conf')
|
||||
replace_pattern('en_US', locale, '/etc/profile')
|
||||
replace_pattern('en_US', locale, '/usr/share/skel/dot.profile')
|
||||
|
||||
if os.path.exists(slick_greeter):
|
||||
replace_pattern(
|
||||
'Exec=slick-greeter',
|
||||
f'Exec=env LANG={locale}.UTF-8 slick-greeter',
|
||||
slick_greeter
|
||||
)
|
||||
elif os.path.exists(gtk_greeter):
|
||||
replace_pattern(
|
||||
'Exec=lightdm-gtk-greete',
|
||||
f'Exec=env LANG={locale}.UTF-8 lightdm-gtk-greeter',
|
||||
gtk_greeter
|
||||
)
|
||||
|
||||
|
||||
def keyboard_dictionary():
|
||||
xkeyboard_layouts = Popen(f'{pc_sysinstall} xkeyboard-layouts', shell=True,
|
||||
stdout=PIPE,
|
||||
universal_newlines=True).stdout.readlines()
|
||||
dictionary = {}
|
||||
for line in xkeyboard_layouts:
|
||||
keyboard_list = list(filter(None, line.rstrip().split(' ')))
|
||||
kb_name = keyboard_list[1].strip()
|
||||
kb_layouts = keyboard_list[0].strip()
|
||||
kb_variant = None
|
||||
dictionary[kb_name] = {'layout': kb_layouts, 'variant': kb_variant}
|
||||
|
||||
xkeyboard_variants = Popen(f'{pc_sysinstall} xkeyboard-variants',
|
||||
shell=True, stdout=PIPE,
|
||||
universal_newlines=True).stdout.readlines()
|
||||
for line in xkeyboard_variants:
|
||||
xkb_variant = line.rstrip()
|
||||
kb_name = xkb_variant.partition(':')[2].strip()
|
||||
keyboard_list = list(filter
|
||||
(None, xkb_variant.partition(':')[0].split()))
|
||||
kb_layouts = keyboard_list[1].strip()
|
||||
kb_variant = keyboard_list[0].strip()
|
||||
dictionary[kb_name] = {'layout': kb_layouts, 'variant': kb_variant}
|
||||
return dictionary
|
||||
|
||||
|
||||
def keyboard_models():
|
||||
xkeyboard_models = Popen(f'{pc_sysinstall} xkeyboard-models', shell=True,
|
||||
stdout=PIPE,
|
||||
universal_newlines=True).stdout.readlines()
|
||||
dictionary = {}
|
||||
for line in xkeyboard_models:
|
||||
kbm_name = line.rstrip().partition(' ')[2]
|
||||
kbm_code = line.rstrip().partition(' ')[0]
|
||||
dictionary[kbm_name] = kbm_code
|
||||
return dictionary
|
||||
|
||||
|
||||
def change_keyboard(kb_layout, kb_variant=None, kb_model=None):
|
||||
if kb_variant is None and kb_model is not None:
|
||||
run(f"setxkbmap -layout {kb_layout} -model {kb_model}", shell=True)
|
||||
elif kb_variant is not None and kb_model is None:
|
||||
run(f"setxkbmap -layout {kb_layout} -variant {kb_variant}", shell=True)
|
||||
elif kb_variant is not None and kb_model is not None:
|
||||
set_kb_cmd = f"setxkbmap -layout {kb_layout} -variant {kb_variant} " \
|
||||
f"-model {kb_model}"
|
||||
run(set_kb_cmd, shell=True)
|
||||
else:
|
||||
run(f"setxkbmap -layout {kb_layout}", shell=True)
|
||||
|
||||
|
||||
def set_keyboard(kb_layout, kb_variant=None, kb_model=None):
|
||||
pass
|
||||
|
||||
|
||||
def timezone_dictionary():
|
||||
tz_list = Popen(f'{pc_sysinstall} list-tzones', shell=True,
|
||||
stdout=PIPE, universal_newlines=True).stdout.readlines()
|
||||
city_list = []
|
||||
dictionary = {}
|
||||
last_continent = ''
|
||||
for zone in tz_list:
|
||||
zone_list = zone.partition(':')[0].rstrip().split('/')
|
||||
continent = zone_list[0]
|
||||
if continent != last_continent:
|
||||
city_list = []
|
||||
if len(zone_list) == 3:
|
||||
city = zone_list[1] + '/' + zone_list[2]
|
||||
elif len(zone_list) == 4:
|
||||
city = zone_list[1] + '/' + zone_list[2] + '/' + zone_list[3]
|
||||
else:
|
||||
city = zone_list[1]
|
||||
city_list.append(city)
|
||||
dictionary[continent] = city_list
|
||||
last_continent = continent
|
||||
return dictionary
|
||||
|
||||
|
||||
def zfs_disk_query():
|
||||
disk_output = Popen(
|
||||
f"{pc_sysinstall} disk-list",
|
||||
shell=True,
|
||||
stdin=PIPE,
|
||||
stdout=PIPE,
|
||||
universal_newlines=True,
|
||||
close_fds=True
|
||||
)
|
||||
return disk_output.stdout.readlines()
|
||||
|
||||
|
||||
def zfs_disk_size_query(disk):
|
||||
disk_info_output = Popen(
|
||||
f"{pc_sysinstall} disk-info {disk}",
|
||||
shell=True,
|
||||
stdin=PIPE,
|
||||
stdout=PIPE,
|
||||
universal_newlines=True,
|
||||
close_fds=True
|
||||
)
|
||||
return disk_info_output.stdout.readlines()[3].partition('=')[2]
|
||||
|
||||
|
||||
def set_admin_user(username, name, password, shell, homedir, hostname):
|
||||
# Set Root user
|
||||
run(f"echo '{password}' | pw usermod -n root -h 0", shell=True)
|
||||
cmd = f"echo '{password}' | pw useradd {username} -c {name} -h 0" \
|
||||
f" -s {shell} -m -d {homedir} -g wheel,operator"
|
||||
run(cmd, shell=True)
|
||||
run(f"sysrc hostname={hostname}", shell=True)
|
||||
run(f"hostname {hostname}", shell=True)
|
||||
@@ -0,0 +1,624 @@
|
||||
from gi.repository import Gtk, Gdk
|
||||
import gettext
|
||||
from install_station.common import password_strength
|
||||
from install_station.data import InstallationData, zfs_datasets, be_name, logo
|
||||
from install_station.partition import bios_or_uefi
|
||||
from install_station.system_calls import (
|
||||
zfs_disk_query,
|
||||
zfs_disk_size_query,
|
||||
)
|
||||
from install_station.interface_controller import Button
|
||||
|
||||
gettext.bindtextdomain('install-station', '/usr/local/share/locale')
|
||||
gettext.textdomain('install-station')
|
||||
_ = gettext.gettext
|
||||
|
||||
cssProvider = Gtk.CssProvider()
|
||||
cssProvider.load_from_path('/usr/local/lib/install-station/ghostbsd-style.css')
|
||||
screen = Gdk.Screen.get_default()
|
||||
styleContext = Gtk.StyleContext()
|
||||
styleContext.add_provider_for_screen(
|
||||
screen,
|
||||
cssProvider,
|
||||
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION
|
||||
)
|
||||
|
||||
|
||||
class ZFS:
|
||||
"""
|
||||
Utility class for ZFS configuration and disk management following the utility class pattern.
|
||||
|
||||
This class provides a GTK+ interface for configuring ZFS installations including:
|
||||
- Disk selection and validation
|
||||
- Pool type configuration (stripe, mirror, RAIDZ1/2/3)
|
||||
- Partition scheme selection (GPT/MBR)
|
||||
- Disk encryption setup with password verification
|
||||
- ZFS pool name configuration
|
||||
|
||||
The class follows a utility pattern with class methods and variables for state management,
|
||||
designed to integrate with the InstallationData system for configuration persistence.
|
||||
"""
|
||||
# Class variables instead of instance variables
|
||||
zfs_disk_list = []
|
||||
pool_type = 'stripe'
|
||||
scheme = 'GPT'
|
||||
zpool = False
|
||||
disk_encrypt = False
|
||||
mirror = 'single disk'
|
||||
vbox1 = None
|
||||
|
||||
# UI elements as class variables
|
||||
pool = None
|
||||
password = None
|
||||
repassword = None
|
||||
mirrorTips = None
|
||||
strenght_label = None
|
||||
img = None
|
||||
check_cell = None
|
||||
store = None
|
||||
|
||||
@classmethod
|
||||
def save_selection(cls):
|
||||
"""
|
||||
Save the current ZFS configuration to InstallationData.
|
||||
|
||||
Validates required fields and generates ZFS configuration data including:
|
||||
- Pool name and type (stripe, mirror, RAIDZ1/2/3)
|
||||
- Disk partitioning scheme and encryption settings
|
||||
- Boot environment and dataset configuration
|
||||
|
||||
Raises:
|
||||
ValueError: If required fields are missing or invalid
|
||||
"""
|
||||
# Validate required fields are populated
|
||||
if not cls.zfs_disk_list:
|
||||
raise ValueError("No disks selected for ZFS configuration")
|
||||
|
||||
if cls.zpool and not cls.pool.get_text().strip():
|
||||
raise ValueError("Pool name cannot be empty when zpool is enabled")
|
||||
|
||||
if cls.disk_encrypt and not cls.password.get_text().strip():
|
||||
raise ValueError("Password cannot be empty when disk encryption is enabled")
|
||||
|
||||
size = int(cls.zfs_disk_list[0].partition('-')[2].rstrip()) - 512
|
||||
swap = 0
|
||||
zfs_num = size - swap
|
||||
if cls.disk_encrypt is True:
|
||||
dgeli = '.eli'
|
||||
else:
|
||||
dgeli = ''
|
||||
|
||||
# Store configuration data in InstallationData instead of writing to file
|
||||
InstallationData.zfs_config_data = []
|
||||
|
||||
if cls.zpool is True:
|
||||
InstallationData.zfs_config_data.append(f"zpoolName={cls.pool.get_text()}\n")
|
||||
else:
|
||||
InstallationData.zfs_config_data.append("#zpoolName=None\n")
|
||||
InstallationData.zfs_config_data.append(f"beName={be_name}\n")
|
||||
InstallationData.zfs_config_data.append('ashift=12\n\n')
|
||||
disk = cls.zfs_disk_list[0].partition('-')[0].rstrip()
|
||||
InstallationData.zfs_config_data.append(f'disk0={disk}\n')
|
||||
InstallationData.zfs_config_data.append('partition=ALL\n')
|
||||
InstallationData.zfs_config_data.append(f'partscheme={cls.scheme}\n')
|
||||
InstallationData.zfs_config_data.append('commitDiskPart\n\n')
|
||||
if cls.pool_type == 'none':
|
||||
pool_disk = '\n'
|
||||
else:
|
||||
zfs_disk = cls.zfs_disk_list
|
||||
disk_len = len(zfs_disk) - 1
|
||||
num = 1
|
||||
mirror_dsk = ''
|
||||
while disk_len != 0:
|
||||
mirror_dsk += ' ' + zfs_disk[num].partition('-')[0].rstrip()
|
||||
print(mirror_dsk)
|
||||
num += 1
|
||||
disk_len -= 1
|
||||
pool_disk = f' ({cls.pool_type}:{mirror_dsk})\n'
|
||||
if bios_or_uefi() == "UEFI":
|
||||
zfs_num = zfs_num - 100
|
||||
else:
|
||||
zfs_num = zfs_num - 1
|
||||
# adding zero to use remaining space
|
||||
zfs_part = f'disk0-part=ZFS{dgeli} {zfs_num} {zfs_datasets}{pool_disk}'
|
||||
InstallationData.zfs_config_data.append(zfs_part)
|
||||
if swap != 0:
|
||||
InstallationData.zfs_config_data.append('disk0-part=swap 0 none\n')
|
||||
if cls.disk_encrypt is True:
|
||||
InstallationData.zfs_config_data.append(f'encpass={cls.password.get_text()}\n')
|
||||
else:
|
||||
InstallationData.zfs_config_data.append('#encpass=None\n')
|
||||
InstallationData.zfs_config_data.append('commitDiskLabel\n')
|
||||
|
||||
@classmethod
|
||||
def scheme_selection(cls, combobox):
|
||||
"""
|
||||
Handle partition scheme selection from combo box.
|
||||
|
||||
Args:
|
||||
combobox: ComboBox widget containing scheme options (GPT/MBR)
|
||||
"""
|
||||
model = combobox.get_model()
|
||||
index = combobox.get_active()
|
||||
data = model[index][0]
|
||||
cls.scheme = data.partition(':')[0]
|
||||
|
||||
@classmethod
|
||||
def mirror_selection(cls, combobox):
|
||||
"""
|
||||
Handle pool type selection and update UI accordingly.
|
||||
|
||||
Sets the pool type (stripe, mirror, RAIDZ1/2/3) and updates the tip text
|
||||
and next button sensitivity based on the number of selected disks.
|
||||
|
||||
Args:
|
||||
combobox: ComboBox widget containing pool type options
|
||||
"""
|
||||
model = combobox.get_model()
|
||||
index = combobox.get_active()
|
||||
data = model[index][0] # Get the internal value (English)
|
||||
cls.mirror = data
|
||||
if cls.mirror == "1+ disks Stripe":
|
||||
cls.pool_type = 'stripe'
|
||||
cls.mirrorTips.set_text(
|
||||
_("Please select 1 or more drive for stripe (select the smallest disk first)"))
|
||||
if len(cls.zfs_disk_list) >= 1:
|
||||
Button.next_button.set_sensitive(True)
|
||||
else:
|
||||
Button.next_button.set_sensitive(False)
|
||||
elif cls.mirror == "2+ disks Mirror":
|
||||
cls.pool_type = 'mirror'
|
||||
mir_msg1 = _("Please select 2 drive for mirroring (select the smallest disk first)")
|
||||
cls.mirrorTips.set_text(mir_msg1)
|
||||
if len(cls.zfs_disk_list) >= 2:
|
||||
Button.next_button.set_sensitive(True)
|
||||
else:
|
||||
Button.next_button.set_sensitive(False)
|
||||
elif cls.mirror == "3 disks RAIDZ1":
|
||||
cls.pool_type = 'raidz1'
|
||||
cls.mirrorTips.set_text(_("Please select 3 drive for RAIDZ1 (select the smallest disk first)"))
|
||||
if len(cls.zfs_disk_list) == 3:
|
||||
Button.next_button.set_sensitive(True)
|
||||
else:
|
||||
Button.next_button.set_sensitive(False)
|
||||
elif cls.mirror == "4 disks RAIDZ2":
|
||||
cls.pool_type = 'raidz2'
|
||||
cls.mirrorTips.set_text(_("Please select 4 drive for RAIDZ2 (select the smallest disk first)"))
|
||||
if len(cls.zfs_disk_list) == 4:
|
||||
Button.next_button.set_sensitive(True)
|
||||
else:
|
||||
Button.next_button.set_sensitive(False)
|
||||
elif cls.mirror == "5 disks RAIDZ3":
|
||||
cls.pool_type = 'raidz3'
|
||||
cls.mirrorTips.set_text(_("Please select 5 drive for RAIDZ3 (select the smallest disk first)"))
|
||||
if len(cls.zfs_disk_list) == 5:
|
||||
Button.next_button.set_sensitive(True)
|
||||
else:
|
||||
Button.next_button.set_sensitive(False)
|
||||
|
||||
@classmethod
|
||||
def on_check_poll(cls, widget):
|
||||
"""
|
||||
Handle custom pool name checkbox toggle.
|
||||
|
||||
Enables or disables the pool name entry field based on checkbox state.
|
||||
|
||||
Args:
|
||||
widget: CheckButton widget for pool name enable/disable
|
||||
"""
|
||||
if widget.get_active():
|
||||
cls.pool.set_sensitive(True)
|
||||
cls.zpool = True
|
||||
else:
|
||||
cls.pool.set_sensitive(False)
|
||||
cls.zpool = False
|
||||
|
||||
@classmethod
|
||||
def on_check_encrypt(cls, widget):
|
||||
"""
|
||||
Handle disk encryption checkbox toggle.
|
||||
|
||||
Enables or disables password fields and updates next button sensitivity
|
||||
based on encryption state and current disk selection.
|
||||
|
||||
Args:
|
||||
widget: CheckButton widget for disk encryption enable/disable
|
||||
"""
|
||||
if widget.get_active():
|
||||
cls.password.set_sensitive(True)
|
||||
cls.repassword.set_sensitive(True)
|
||||
cls.disk_encrypt = True
|
||||
Button.next_button.set_sensitive(False)
|
||||
else:
|
||||
cls.password.set_sensitive(False)
|
||||
cls.repassword.set_sensitive(False)
|
||||
cls.disk_encrypt = False
|
||||
if cls.mirror == "1+ disks Stripe":
|
||||
if len(cls.zfs_disk_list) >= 1:
|
||||
Button.next_button.set_sensitive(True)
|
||||
else:
|
||||
Button.next_button.set_sensitive(False)
|
||||
elif cls.mirror == "2+ disks Mirror":
|
||||
if len(cls.zfs_disk_list) >= 2:
|
||||
Button.next_button.set_sensitive(True)
|
||||
else:
|
||||
Button.next_button.set_sensitive(False)
|
||||
elif cls.mirror == "3 disks RAIDZ1":
|
||||
if len(cls.zfs_disk_list) == 3:
|
||||
Button.next_button.set_sensitive(True)
|
||||
else:
|
||||
Button.next_button.set_sensitive(False)
|
||||
elif cls.mirror == "4 disks RAIDZ2":
|
||||
if len(cls.zfs_disk_list) == 4:
|
||||
Button.next_button.set_sensitive(True)
|
||||
else:
|
||||
Button.next_button.set_sensitive(False)
|
||||
elif cls.mirror == "5 disks RAIDZ3":
|
||||
if len(cls.zfs_disk_list) == 5:
|
||||
Button.next_button.set_sensitive(True)
|
||||
else:
|
||||
Button.next_button.set_sensitive(False)
|
||||
|
||||
@classmethod
|
||||
def initialize(cls):
|
||||
"""
|
||||
Initialize the ZFS configuration UI following the utility class pattern.
|
||||
|
||||
Creates the main interface including:
|
||||
- Disk selection tree view with checkboxes
|
||||
- Pool type selection (stripe, mirror, RAIDZ1/2/3)
|
||||
- Pool name configuration
|
||||
- Partition scheme selection (GPT/MBR)
|
||||
- Disk encryption options with password fields
|
||||
|
||||
This method is called automatically by get_model() when the interface is first accessed.
|
||||
"""
|
||||
cls.vbox1 = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, homogeneous=False, spacing=0)
|
||||
cls.vbox1.show()
|
||||
# Chose disk
|
||||
sw = Gtk.ScrolledWindow(hexpand=True, vexpand=True)
|
||||
sw.set_shadow_type(Gtk.ShadowType.ETCHED_IN)
|
||||
sw.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
|
||||
cls.store = Gtk.TreeStore(str, str, str, 'gboolean')
|
||||
for disk in zfs_disk_query():
|
||||
dsk = disk.partition(':')[0].rstrip()
|
||||
dsk_name = disk.partition(':')[2].rstrip()
|
||||
dsk_size = zfs_disk_size_query(dsk).rstrip()
|
||||
cls.store.append(None, [dsk, dsk_size, dsk_name, False])
|
||||
treeview = Gtk.TreeView()
|
||||
treeview.set_model(cls.store)
|
||||
treeview.set_rules_hint(True)
|
||||
cls.check_cell = Gtk.CellRendererToggle()
|
||||
cls.check_cell.set_property('activatable', True)
|
||||
cls.check_cell.connect('toggled', cls.col1_toggled_cb, cls.store)
|
||||
cell = Gtk.CellRendererText()
|
||||
column = Gtk.TreeViewColumn(None, cell, text=0)
|
||||
column_header = Gtk.Label(label=_('Disk'))
|
||||
column_header.set_use_markup(True)
|
||||
column_header.show()
|
||||
column.set_widget(column_header)
|
||||
column.set_sort_column_id(0)
|
||||
cell2 = Gtk.CellRendererText()
|
||||
column2 = Gtk.TreeViewColumn(None, cell2, text=0)
|
||||
column_header2 = Gtk.Label(label=_('Size(MB)'))
|
||||
column_header2.set_use_markup(True)
|
||||
column_header2.show()
|
||||
column2.set_widget(column_header2)
|
||||
cell3 = Gtk.CellRendererText()
|
||||
column3 = Gtk.TreeViewColumn(None, cell3, text=0)
|
||||
column_header3 = Gtk.Label(label=_('Name'))
|
||||
column_header3.set_use_markup(True)
|
||||
column_header3.show()
|
||||
column3.set_widget(column_header3)
|
||||
column1 = Gtk.TreeViewColumn(_("Check"), cls.check_cell)
|
||||
column1.add_attribute(cls.check_cell, "active", 3)
|
||||
column.set_attributes(cell, text=0)
|
||||
column2.set_attributes(cell2, text=1)
|
||||
column3.set_attributes(cell3, text=2)
|
||||
treeview.append_column(column1)
|
||||
treeview.append_column(column)
|
||||
treeview.append_column(column2)
|
||||
treeview.append_column(column3)
|
||||
tree_selection = treeview.get_selection()
|
||||
tree_selection.set_mode(Gtk.SelectionMode.SINGLE)
|
||||
sw.add(treeview)
|
||||
sw.show()
|
||||
cls.mirrorTips = Gtk.Label(label=_('Please select one drive'))
|
||||
cls.mirrorTips.set_justify(Gtk.Justification.LEFT)
|
||||
cls.mirrorTips.set_alignment(0.01, 0.5)
|
||||
# Mirror, raidz and stripe
|
||||
cls.mirror = 'none'
|
||||
mirror_label = Gtk.Label(label=_('<b>Pool Type</b>'))
|
||||
mirror_label.set_use_markup(True)
|
||||
mirror_box = Gtk.ComboBox()
|
||||
mirror_store = Gtk.ListStore(str, str) # value, display_text
|
||||
mirror_store.append(["1+ disks Stripe", _("1+ disks Stripe")])
|
||||
mirror_store.append(["2+ disks Mirror", _("2+ disks Mirror")])
|
||||
mirror_store.append(["3 disks RAIDZ1", _("3 disks RAIDZ1")])
|
||||
mirror_store.append(["4 disks RAIDZ2", _("4 disks RAIDZ2")])
|
||||
mirror_store.append(["5 disks RAIDZ3", _("5 disks RAIDZ3")])
|
||||
mirror_box.set_model(mirror_store)
|
||||
renderer = Gtk.CellRendererText()
|
||||
mirror_box.pack_start(renderer, True)
|
||||
mirror_box.add_attribute(renderer, "text", 1) # Display column 1 (translated text)
|
||||
mirror_box.connect('changed', cls.mirror_selection)
|
||||
mirror_box.set_active(0)
|
||||
|
||||
# Pool Name
|
||||
cls.zpool = False
|
||||
pool_name_label = Gtk.Label(label=_('<b>Pool Name</b>'))
|
||||
pool_name_label.set_use_markup(True)
|
||||
cls.pool = Gtk.Entry()
|
||||
cls.pool.set_text('zroot')
|
||||
# Creating MBR or GPT drive
|
||||
scheme_label = Gtk.Label(label='<b>Partition Scheme</b>')
|
||||
scheme_label.set_use_markup(True)
|
||||
# Adding a combo box to selecting MBR or GPT sheme.
|
||||
cls.scheme = 'GPT'
|
||||
shemebox = Gtk.ComboBoxText()
|
||||
shemebox.append_text("GPT")
|
||||
shemebox.append_text("MBR")
|
||||
shemebox.connect('changed', cls.scheme_selection)
|
||||
shemebox.set_active(0)
|
||||
if bios_or_uefi() == "UEFI":
|
||||
shemebox.set_sensitive(False)
|
||||
else:
|
||||
shemebox.set_sensitive(True)
|
||||
# GELI Disk encryption
|
||||
cls.disk_encrypt = False
|
||||
encrypt_check = Gtk.CheckButton(label=_("Encrypt Disk"))
|
||||
encrypt_check.connect("toggled", cls.on_check_encrypt)
|
||||
encrypt_check.set_sensitive(True)
|
||||
# password
|
||||
cls.passwd_label = Gtk.Label(label=_("Password"))
|
||||
cls.password = Gtk.Entry()
|
||||
cls.password.set_sensitive(False)
|
||||
cls.password.set_visibility(False)
|
||||
cls.password.connect("changed", password_strength)
|
||||
cls.strenght_label = Gtk.Label()
|
||||
cls.strenght_label.set_alignment(0.1, 0.5)
|
||||
cls.vpasswd_label = Gtk.Label(label=_("Verify it"))
|
||||
cls.repassword = Gtk.Entry()
|
||||
cls.repassword.set_sensitive(False)
|
||||
cls.repassword.set_visibility(False)
|
||||
cls.repassword.connect("changed", cls.password_verification)
|
||||
# set image for password matching
|
||||
cls.img = Gtk.Image()
|
||||
cls.img.set_alignment(0.2, 0.5)
|
||||
# table = Gtk.Table(12, 12, True)
|
||||
grid = Gtk.Grid()
|
||||
grid.set_row_spacing(10)
|
||||
# grid.set_column_homogeneous(True)
|
||||
# grid.set_row_homogeneous(True)
|
||||
# grid.attach(Title, 1, 1, 10, 1)
|
||||
grid.attach(mirror_label, 1, 2, 1, 1)
|
||||
grid.attach(mirror_box, 2, 2, 1, 1)
|
||||
grid.attach(pool_name_label, 7, 2, 2, 1)
|
||||
grid.attach(cls.pool, 9, 2, 2, 1)
|
||||
grid.attach(cls.mirrorTips, 1, 3, 8, 1)
|
||||
# grid.attach(zfs4kcheck, 9, 3, 2, 1)
|
||||
grid.attach(sw, 1, 4, 10, 3)
|
||||
# grid.attach(scheme_label, 1, 9, 1, 1)
|
||||
# grid.attach(shemebox, 2, 9, 1, 1)
|
||||
# grid.attach(cls.swap_encrypt_check, 9, 15, 11, 12)
|
||||
# grid.attach(swap_mirror_check, 9, 15, 11, 12)
|
||||
# grid.attach(encrypt_check, 2, 8, 2, 1)
|
||||
# grid.attach(cls.passwd_label, 1, 9, 1, 1)
|
||||
# grid.attach(cls.password, 2, 9, 2, 1)
|
||||
# grid.attach(cls.strenght_label, 4, 9, 2, 1)
|
||||
# grid.attach(cls.vpasswd_label, 1, 10, 1, 1)
|
||||
# grid.attach(cls.repassword, 2, 10, 2, 1)
|
||||
# grid.attach(cls.img, 4, 10, 2, 1)
|
||||
cls.vbox1.pack_start(grid, True, True, 10)
|
||||
return
|
||||
|
||||
@classmethod
|
||||
def get_model(cls):
|
||||
"""
|
||||
Return the GTK widget model for the ZFS configuration interface.
|
||||
|
||||
Creates and initializes the UI if it doesn't exist yet.
|
||||
|
||||
Returns:
|
||||
Gtk.Box: The main container widget for the ZFS configuration interface
|
||||
"""
|
||||
if cls.vbox1 is None:
|
||||
cls.initialize()
|
||||
return cls.vbox1
|
||||
|
||||
@classmethod
|
||||
def check_if_small_disk(cls, size):
|
||||
"""
|
||||
Check if any selected disk is larger than the specified size.
|
||||
|
||||
Used to enforce the requirement that the smallest disk must be selected first
|
||||
for ZFS pool configurations.
|
||||
|
||||
Args:
|
||||
size: Size in MB to compare against selected disks
|
||||
|
||||
Returns:
|
||||
bool: True if any selected disk is larger than the specified size
|
||||
"""
|
||||
if len(cls.zfs_disk_list) != 0:
|
||||
for line in cls.zfs_disk_list:
|
||||
if int(line.partition('-')[2]) > int(size):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def col1_toggled_cb(cls, _cell, path, model):
|
||||
"""
|
||||
Handle disk selection checkbox toggle events.
|
||||
|
||||
Manages the disk selection list and updates next button sensitivity
|
||||
based on pool type requirements. Enforces the rule that the smallest
|
||||
disk must be selected first.
|
||||
|
||||
Args:
|
||||
_cell: CellRendererToggle that was clicked (unused)
|
||||
path: TreePath of the toggled row
|
||||
model: TreeStore model containing disk data
|
||||
|
||||
Returns:
|
||||
bool: Always returns True to indicate the event was handled
|
||||
"""
|
||||
model[path][3] = not model[path][3]
|
||||
if model[path][3] is False:
|
||||
cls.zfs_disk_list.remove(model[path][0] + "-" + model[path][1])
|
||||
if cls.mirror == "1+ disks Stripe":
|
||||
if len(cls.zfs_disk_list) >= 1:
|
||||
Button.next_button.set_sensitive(True)
|
||||
else:
|
||||
Button.next_button.set_sensitive(False)
|
||||
elif cls.mirror == "2+ disks Mirror":
|
||||
if len(cls.zfs_disk_list) >= 2:
|
||||
Button.next_button.set_sensitive(True)
|
||||
else:
|
||||
Button.next_button.set_sensitive(False)
|
||||
elif cls.mirror == "3 disks RAIDZ1":
|
||||
if len(cls.zfs_disk_list) == 3:
|
||||
Button.next_button.set_sensitive(True)
|
||||
else:
|
||||
Button.next_button.set_sensitive(False)
|
||||
elif cls.mirror == "4 disks RAIDZ2":
|
||||
if len(cls.zfs_disk_list) == 4:
|
||||
Button.next_button.set_sensitive(True)
|
||||
else:
|
||||
Button.next_button.set_sensitive(False)
|
||||
elif cls.mirror == "5 disks RAIDZ3":
|
||||
if len(cls.zfs_disk_list) == 5:
|
||||
Button.next_button.set_sensitive(True)
|
||||
else:
|
||||
Button.next_button.set_sensitive(False)
|
||||
else:
|
||||
if cls.check_if_small_disk(model[path][1]) is False:
|
||||
cls.zfs_disk_list.extend([model[path][0] + "-" + model[path][1]])
|
||||
if cls.mirror == "1+ disks Stripe":
|
||||
if len(cls.zfs_disk_list) >= 1:
|
||||
Button.next_button.set_sensitive(True)
|
||||
else:
|
||||
Button.next_button.set_sensitive(False)
|
||||
elif cls.mirror == "2+ disks Mirror":
|
||||
if len(cls.zfs_disk_list) >= 2:
|
||||
Button.next_button.set_sensitive(True)
|
||||
else:
|
||||
Button.next_button.set_sensitive(False)
|
||||
elif cls.mirror == "3 disks RAIDZ1":
|
||||
if len(cls.zfs_disk_list) == 3:
|
||||
Button.next_button.set_sensitive(True)
|
||||
else:
|
||||
Button.next_button.set_sensitive(False)
|
||||
elif cls.mirror == "4 disks RAIDZ2":
|
||||
if len(cls.zfs_disk_list) == 4:
|
||||
Button.next_button.set_sensitive(True)
|
||||
else:
|
||||
Button.next_button.set_sensitive(False)
|
||||
elif cls.mirror == "5 disks RAIDZ3":
|
||||
if len(cls.zfs_disk_list) == 5:
|
||||
Button.next_button.set_sensitive(True)
|
||||
else:
|
||||
Button.next_button.set_sensitive(False)
|
||||
else:
|
||||
cls.check_cell.set_sensitive(False)
|
||||
cls.small_disk_warning()
|
||||
|
||||
print(cls.zfs_disk_list)
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def small_disk_warning(cls):
|
||||
"""
|
||||
Display a warning dialog when disks are selected out of size order.
|
||||
|
||||
Shows a dialog informing the user that the smallest disk must be
|
||||
selected first and offers to reset all selections.
|
||||
"""
|
||||
window = Gtk.Window()
|
||||
window.set_title(_("Warning"))
|
||||
window.set_border_width(0)
|
||||
# window.set_size_request(480, 200)
|
||||
window.set_icon_from_file(logo)
|
||||
box1 = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, homogeneous=False, spacing=0)
|
||||
window.add(box1)
|
||||
box1.show()
|
||||
box2 = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, homogeneous=False, spacing=10)
|
||||
box2.set_border_width(10)
|
||||
box1.pack_start(box2, True, True, 0)
|
||||
box2.show()
|
||||
warning_text = _("Smallest disk need to be SELECTED first!\n")
|
||||
warning_text += _("All the disk selected will reset.")
|
||||
label = Gtk.Label(label=warning_text)
|
||||
# Add button
|
||||
box2.pack_start(label, True, True, 0)
|
||||
bbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, homogeneous=False, spacing=10)
|
||||
bbox.set_border_width(5)
|
||||
button = Gtk.Button(stock=Gtk.STOCK_OK)
|
||||
button.connect("clicked", cls.resset_selection, window)
|
||||
bbox.add(button)
|
||||
box2.pack_end(bbox, True, True, 5)
|
||||
window.show_all()
|
||||
|
||||
@classmethod
|
||||
def resset_selection(cls, _widget, window):
|
||||
"""
|
||||
Reset all disk selections and close the warning dialog.
|
||||
|
||||
Clears the disk selection list and unchecks all checkboxes in the tree view.
|
||||
|
||||
Args:
|
||||
_widget: Button widget that triggered the reset (unused)
|
||||
window: Warning dialog window to close
|
||||
"""
|
||||
cls.zfs_disk_list = []
|
||||
rows = len(cls.store)
|
||||
for row in range(0, rows):
|
||||
cls.store[row][3] = False
|
||||
row += 1
|
||||
cls.check_cell.set_sensitive(True)
|
||||
window.hide()
|
||||
|
||||
@classmethod
|
||||
def password_verification(cls, _widget):
|
||||
"""
|
||||
Verify that password and confirmation password fields match.
|
||||
|
||||
Updates the verification image and next button sensitivity based on
|
||||
password match status and current disk selection requirements.
|
||||
|
||||
Args:
|
||||
_widget: Entry widget that triggered the verification (unused)
|
||||
"""
|
||||
if cls.password.get_text() == cls.repassword.get_text():
|
||||
cls.img.set_from_stock(Gtk.STOCK_YES, 5)
|
||||
if cls.mirror == "1+ disks Stripe":
|
||||
if len(cls.zfs_disk_list) >= 1:
|
||||
Button.next_button.set_sensitive(True)
|
||||
else:
|
||||
Button.next_button.set_sensitive(False)
|
||||
elif cls.mirror == "2+ disks Mirror":
|
||||
if len(cls.zfs_disk_list) >= 2:
|
||||
Button.next_button.set_sensitive(True)
|
||||
else:
|
||||
Button.next_button.set_sensitive(False)
|
||||
elif cls.mirror == "3 disks RAIDZ1":
|
||||
if len(cls.zfs_disk_list) == 3:
|
||||
Button.next_button.set_sensitive(True)
|
||||
else:
|
||||
Button.next_button.set_sensitive(False)
|
||||
elif cls.mirror == "4 disks RAIDZ2":
|
||||
if len(cls.zfs_disk_list) == 4:
|
||||
Button.next_button.set_sensitive(True)
|
||||
else:
|
||||
Button.next_button.set_sensitive(False)
|
||||
elif cls.mirror == "5 disks RAIDZ3":
|
||||
if len(cls.zfs_disk_list) == 5:
|
||||
Button.next_button.set_sensitive(True)
|
||||
else:
|
||||
Button.next_button.set_sensitive(False)
|
||||
else:
|
||||
cls.img.set_from_stock(Gtk.STOCK_NO, 5)
|
||||
Button.next_button.set_sensitive(False)
|
||||
@@ -0,0 +1,293 @@
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gtk, Gdk, GdkPixbuf
|
||||
import gettext
|
||||
from install_station.system_calls import language_dictionary
|
||||
from install_station.interface_controller import Interface
|
||||
from install_station.data import InstallationData
|
||||
from install_station.window import Window
|
||||
|
||||
gettext.bindtextdomain('install-station', '/usr/local/share/locale')
|
||||
gettext.textdomain('install-station')
|
||||
_ = gettext.gettext
|
||||
|
||||
lang_dictionary = language_dictionary()
|
||||
|
||||
Messages = _("""To run GhostBSD without installing, select "Try GhostBSD."
|
||||
|
||||
To install GhostBSD on your computer hard disk drive, click "Install GhostBSD."
|
||||
|
||||
Note: Language selection only works when selecting "Try GhostBSD."
|
||||
When installing GhostBSD, the installation program is only in English.""")
|
||||
|
||||
cssProvider = Gtk.CssProvider()
|
||||
cssProvider.load_from_path('/usr/local/lib/install-station/ghostbsd-style.css')
|
||||
screen = Gdk.Screen.get_default()
|
||||
styleContext = Gtk.StyleContext()
|
||||
styleContext.add_provider_for_screen(
|
||||
screen,
|
||||
cssProvider,
|
||||
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION
|
||||
)
|
||||
|
||||
|
||||
class Welcome:
|
||||
"""
|
||||
Utility class for the welcome screen and initial mode selection following the utility class pattern.
|
||||
|
||||
This class provides a GTK+ interface for the initial GhostBSD welcome screen including:
|
||||
- Language selection from available system languages
|
||||
- Mode selection between "Install GhostBSD" and "Try GhostBSD"
|
||||
- Visual elements with images and instructional text
|
||||
- Integration with InstallationData for persistent configuration
|
||||
|
||||
The class follows a utility pattern with class methods and variables for state management,
|
||||
designed to integrate with the Interface controller for navigation flow.
|
||||
"""
|
||||
# Class variables instead of instance variables
|
||||
what = None
|
||||
language = None
|
||||
install_ghostbsd = None
|
||||
try_ghostbsd = None
|
||||
vbox1 = None
|
||||
|
||||
@classmethod
|
||||
def language_selection(cls, tree_selection):
|
||||
"""
|
||||
Handle language selection from the treeview.
|
||||
|
||||
Extracts the selected language from the tree view and updates both
|
||||
class variables and InstallationData with the language name and code.
|
||||
Also updates the UI text to reflect the new language selection.
|
||||
|
||||
Args:
|
||||
tree_selection: TreeSelection widget containing the user's language choice
|
||||
"""
|
||||
model, tree_iter = tree_selection.get_selected()
|
||||
if tree_iter is not None:
|
||||
value = model[tree_iter][0]
|
||||
language_code = lang_dictionary[value]
|
||||
cls.language = language_code
|
||||
InstallationData.language = value
|
||||
InstallationData.language_code = language_code
|
||||
print(f"Language selected: {value} ({language_code})")
|
||||
|
||||
# Update gettext to use the new language
|
||||
import os
|
||||
os.environ['LANGUAGE'] = language_code
|
||||
gettext.bindtextdomain('install-station', '/usr/local/share/locale')
|
||||
gettext.textdomain('install-station')
|
||||
|
||||
# Update the UI text with new translations
|
||||
cls.update_ui_text()
|
||||
return
|
||||
|
||||
@classmethod
|
||||
def update_ui_text(cls):
|
||||
"""
|
||||
Update all UI text elements with new translations after language change.
|
||||
"""
|
||||
# Reload the gettext function to pick up new locale
|
||||
_ = gettext.gettext
|
||||
|
||||
# Update UI elements if they exist
|
||||
if hasattr(cls, 'install_button') and cls.install_button:
|
||||
cls.install_button.set_label(_('Install GhostBSD'))
|
||||
|
||||
if hasattr(cls, 'try_button') and cls.try_button:
|
||||
cls.try_button.set_label(_('Try GhostBSD'))
|
||||
|
||||
if hasattr(cls, 'text_label') and cls.text_label:
|
||||
cls.text_label.set_text(_("""To run GhostBSD without installing, select "Try GhostBSD."
|
||||
|
||||
|
||||
To install GhostBSD on your computer hard disk drive, click "Install GhostBSD."
|
||||
|
||||
Note: Language selection only works when selecting "Try GhostBSD."
|
||||
When installing GhostBSD, the installation program is only in English."""))
|
||||
|
||||
if hasattr(cls, 'language_column_header') and cls.language_column_header:
|
||||
cls.language_column_header.set_text(_('Language'))
|
||||
|
||||
Window.set_title(_("Welcome to GhostBSD"))
|
||||
|
||||
@classmethod
|
||||
def language_columns(cls, treeview):
|
||||
"""
|
||||
Configure the language selection treeview with appropriate columns.
|
||||
|
||||
Creates a single column with a "Language" header for displaying
|
||||
available languages in the tree view.
|
||||
|
||||
Args:
|
||||
treeview: TreeView widget to configure with language column
|
||||
"""
|
||||
cell = Gtk.CellRendererText()
|
||||
column = Gtk.TreeViewColumn(None, cell, text=0)
|
||||
column_header = Gtk.Label(label=_('Language'))
|
||||
column_header.set_use_markup(True)
|
||||
column_header.show()
|
||||
column.set_widget(column_header)
|
||||
# Store reference for updating
|
||||
cls.language_column_header = column_header
|
||||
column.set_sort_column_id(0)
|
||||
treeview.append_column(column)
|
||||
|
||||
@classmethod
|
||||
def save_selection(cls):
|
||||
"""
|
||||
Save the current language selection.
|
||||
|
||||
This method is maintained for compatibility but language selection
|
||||
is now automatically saved to InstallationData when chosen.
|
||||
"""
|
||||
# Language is now saved in InstallationData automatically
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def install_system(cls, _widget):
|
||||
"""
|
||||
Handle "Install GhostBSD" button click.
|
||||
|
||||
Sets the installation mode to 'install' and navigates to the
|
||||
installation workflow pages.
|
||||
|
||||
Args:
|
||||
_widget: Button widget that triggered the action (unused)
|
||||
"""
|
||||
cls.what = 'install'
|
||||
InstallationData.install_mode = 'install'
|
||||
Interface.next_install_page()
|
||||
|
||||
@classmethod
|
||||
def try_system(cls, _widget):
|
||||
"""
|
||||
Handle "Try GhostBSD" button click.
|
||||
|
||||
Sets the installation mode to 'try' and navigates to the
|
||||
live system setup pages (typically network configuration).
|
||||
|
||||
Args:
|
||||
_widget: Button widget that triggered the action (unused)
|
||||
"""
|
||||
cls.what = 'try'
|
||||
InstallationData.install_mode = 'try'
|
||||
Interface.next_setup_page()
|
||||
|
||||
@classmethod
|
||||
def get_what(cls):
|
||||
"""
|
||||
Get the current installation mode.
|
||||
|
||||
Returns the installation mode from InstallationData if available,
|
||||
otherwise falls back to the class variable.
|
||||
|
||||
Returns:
|
||||
str: Current installation mode ('install' or 'try')
|
||||
"""
|
||||
return InstallationData.install_mode or cls.what
|
||||
|
||||
@classmethod
|
||||
def initialize(cls):
|
||||
"""
|
||||
Initialize the welcome screen UI following the utility class pattern.
|
||||
|
||||
Creates the main interface including:
|
||||
- Language selection tree view on the left side
|
||||
- Install and Try buttons with images on the right side
|
||||
- Instructional text explaining the options
|
||||
- Grid-based layout with proper spacing and margins
|
||||
|
||||
This method is called automatically by get_model() when the interface is first accessed.
|
||||
"""
|
||||
cls.what = None
|
||||
cls.language = None
|
||||
cls.vbox1 = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, homogeneous=False, spacing=0)
|
||||
cls.vbox1.show()
|
||||
main_grid = Gtk.Grid()
|
||||
cls.vbox1.pack_start(main_grid, True, True, 0)
|
||||
sw = Gtk.ScrolledWindow()
|
||||
sw.set_shadow_type(Gtk.ShadowType.ETCHED_IN)
|
||||
sw.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
|
||||
store = Gtk.TreeStore(str)
|
||||
for line in lang_dictionary:
|
||||
store.append(None, [line])
|
||||
treeview = Gtk.TreeView()
|
||||
treeview.set_model(store)
|
||||
treeview.set_rules_hint(True)
|
||||
cls.language_columns(treeview)
|
||||
tree_selection = treeview.get_selection()
|
||||
tree_selection.set_mode(Gtk.SelectionMode.SINGLE)
|
||||
tree_selection.connect("changed", cls.language_selection)
|
||||
sw.add(treeview)
|
||||
sw.show()
|
||||
vbox2 = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, homogeneous=False, spacing=0)
|
||||
vbox2.set_border_width(10)
|
||||
vbox2.show()
|
||||
pix_buf1 = GdkPixbuf.Pixbuf().new_from_file_at_scale(
|
||||
filename='/usr/local/lib/install-station/laptop.png',
|
||||
width=190,
|
||||
height=190,
|
||||
preserve_aspect_ratio=True
|
||||
)
|
||||
image1 = Gtk.Image.new_from_pixbuf(pix_buf1)
|
||||
image1.show()
|
||||
pix_buf2 = GdkPixbuf.Pixbuf().new_from_file_at_scale(
|
||||
filename='/usr/local/lib/install-station/disk.png',
|
||||
width=120,
|
||||
height=120,
|
||||
preserve_aspect_ratio=True)
|
||||
image2 = Gtk.Image.new_from_pixbuf(pix_buf2)
|
||||
image2.show()
|
||||
install_button = Gtk.Button(label=_('Install GhostBSD'), image=image1,
|
||||
image_position=2)
|
||||
install_button.set_always_show_image(True)
|
||||
install_button.connect("clicked", cls.install_system)
|
||||
try_button = Gtk.Button(label=_('Try GhostBSD'), image=image2,
|
||||
image_position=2)
|
||||
try_button.set_always_show_image(True)
|
||||
try_button.connect("clicked", cls.try_system)
|
||||
text_label = Gtk.Label(label=Messages)
|
||||
text_label.set_line_wrap(True)
|
||||
|
||||
# Store references for updating
|
||||
cls.install_button = install_button
|
||||
cls.try_button = try_button
|
||||
cls.text_label = text_label
|
||||
right_grid = Gtk.Grid()
|
||||
right_grid.set_row_spacing(10)
|
||||
right_grid.set_column_spacing(2)
|
||||
right_grid.set_column_homogeneous(True)
|
||||
right_grid.set_row_homogeneous(True)
|
||||
right_grid.set_margin_left(10)
|
||||
right_grid.set_margin_right(10)
|
||||
right_grid.set_margin_top(10)
|
||||
right_grid.set_margin_bottom(10)
|
||||
right_grid.attach(install_button, 1, 1, 1, 5)
|
||||
right_grid.attach(try_button, 2, 1, 1, 5)
|
||||
right_grid.attach(text_label, 1, 6, 2, 5)
|
||||
main_grid.set_row_spacing(10)
|
||||
main_grid.set_column_spacing(4)
|
||||
main_grid.set_column_homogeneous(True)
|
||||
main_grid.set_row_homogeneous(True)
|
||||
main_grid.set_margin_left(10)
|
||||
main_grid.set_margin_right(10)
|
||||
main_grid.set_margin_top(10)
|
||||
main_grid.set_margin_bottom(10)
|
||||
main_grid.attach(sw, 1, 1, 1, 10)
|
||||
main_grid.attach(right_grid, 2, 1, 3, 10)
|
||||
main_grid.show()
|
||||
|
||||
@classmethod
|
||||
def get_model(cls):
|
||||
"""
|
||||
Return the GTK widget model for the welcome screen interface.
|
||||
|
||||
Returns the main container widget that was created during initialization.
|
||||
|
||||
Returns:
|
||||
Gtk.Box: The main container widget for the welcome screen interface
|
||||
"""
|
||||
if cls.vbox1 is None:
|
||||
cls.initialize()
|
||||
return cls.vbox1
|
||||
@@ -0,0 +1,49 @@
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gtk
|
||||
|
||||
|
||||
class Window:
|
||||
window = Gtk.Window()
|
||||
|
||||
@classmethod
|
||||
def connect(cls, signal, callback):
|
||||
return cls.window.connect(signal, callback)
|
||||
|
||||
@classmethod
|
||||
def set_border_width(cls, width):
|
||||
return cls.window.set_border_width(width)
|
||||
|
||||
@classmethod
|
||||
def set_default_size(cls, width, height):
|
||||
return cls.window.set_default_size(width, height)
|
||||
|
||||
@classmethod
|
||||
def set_size_request(cls, width, height):
|
||||
return cls.window.set_size_request(width, height)
|
||||
|
||||
@classmethod
|
||||
def set_title(cls, title):
|
||||
return cls.window.set_title(title)
|
||||
|
||||
@classmethod
|
||||
def set_icon_from_file(cls, filename):
|
||||
return cls.window.set_icon_from_file(filename)
|
||||
|
||||
@classmethod
|
||||
def add(cls, widget):
|
||||
return cls.window.add(widget)
|
||||
|
||||
@classmethod
|
||||
def show_all(cls):
|
||||
return cls.window.show_all()
|
||||
|
||||
@classmethod
|
||||
def hide(cls):
|
||||
"""Hide the window."""
|
||||
return cls.window.hide()
|
||||
|
||||
@classmethod
|
||||
def __getattr__(cls, name):
|
||||
"""Fallback for any methods not explicitly defined."""
|
||||
return getattr(cls.window, name)
|
||||
Reference in New Issue
Block a user