def method_test(self): """Test if a method can be correctly connected to a signal.""" signal = Signal() foo = FooClass() self.assertIsNone(foo.var) # connect the signal signal.connect(foo.set_var) # trigger the signal signal.emit("bar") # check if the callback triggered correctly self.assertEqual(foo.var, "bar") # try to trigger the signal again signal.emit("baz") self.assertEqual(foo.var, "baz") # now try to disconnect the signal signal.disconnect(foo.set_var) # check that calling the signal again # no longer triggers the callback signal.emit("anaconda") self.assertEqual(foo.var, "baz")
def clear_test(self): """Test if the clear() method correctly clears any connected callbacks.""" def set_var(value): self.var = value signal = Signal() foo = FooClass() lambda_foo = FooClass() self.assertIsNone(foo.var) self.assertIsNone(lambda_foo.var) self.assertIsNone(self.var) # connect the callbacks signal.connect(set_var) signal.connect(foo.set_var) # pylint: disable=unnecessary-lambda signal.connect(lambda x: lambda_foo.set_var(x)) # trigger the signal signal.emit("bar") # check that the callbacks were triggered self.assertEqual(self.var, "bar") self.assertEqual(foo.var, "bar") self.assertEqual(lambda_foo.var, "bar") # clear the callbacks signal.clear() # trigger the signal again signal.emit("anaconda") # check that the callbacks were not triggered self.assertEqual(self.var, "bar") self.assertEqual(foo.var, "bar") self.assertEqual(lambda_foo.var, "bar")
def lambda_test(self): """Test if a lambda can be correctly connected to a signal.""" foo = FooClass() signal = Signal() self.assertIsNone(foo.var) # connect the signal # pylint: disable=unnecessary-lambda lambda_instance = lambda x: foo.set_var(x) signal.connect(lambda_instance) # trigger the signal signal.emit("bar") # check if the callback triggered correctly self.assertEqual(foo.var, "bar") # try to trigger the signal again signal.emit("baz") self.assertEqual(foo.var, "baz") # now try to disconnect the signal signal.disconnect(lambda_instance) # check that calling the signal again # no longer triggers the callback signal.emit("anaconda") self.assertEqual(foo.var, "baz")
def function_test(self): """Test if a local function can be correctly connected to a signal.""" # create a local function def set_var(value): self.var = value signal = Signal() self.assertIsNone(self.var) # connect the signal signal.connect(set_var) # trigger the signal signal.emit("bar") # check if the callback triggered correctly self.assertEqual(self.var, "bar") # try to trigger the signal again signal.emit("baz") self.assertEqual(self.var, "baz") # now try to disconnect the signal signal.disconnect(set_var) # check that calling the signal again # no longer triggers the callback signal.emit("anaconda") self.assertEqual(self.var, "baz")
def signal_chain_test(self): """Check if signals can be chained together.""" foo = FooClass() self.assertIsNone(foo.var) signal1 = Signal() signal1.connect(foo.set_var) signal2 = Signal() signal2.connect(signal1.emit) signal3 = Signal() signal3.connect(signal2.emit) # trigger the chain signal3.emit("bar") # check if the initial callback was triggered self.assertEqual(foo.var, "bar")
class TaskQueue(BaseTask): """TaskQueue represents a queue of TaskQueues or Tasks. TaskQueues and Tasks can be mixed in a single TaskQueue. """ def __init__(self, name, status_message=None): super(TaskQueue, self).__init__(name=name) self._status_message = status_message self._current_task_number = None self._current_queue_number = None # the list backing this TaskQueue instance self._list = [] # triggered if a TaskQueue contained in this one was started/completed self.queue_started = Signal() self.queue_completed = Signal() # triggered when a task is started self.task_started = Signal() self.task_completed = Signal() # connect to the task & queue started signals for # progress reporting purposes self.queue_started.connect(self._queue_started_cb) self.task_started.connect(self._task_started_cb) @synchronized def _queue_started_cb(self, *args): self._current_queue_number += 1 @synchronized def _task_started_cb(self, *args): self._current_task_number += 1 @property def status_message(self): """A status message describing the Queue is trying to achieve. Eq. "Converting all foo into bar." The current main usecase is to set the ProgressHub status message when a TaskQueue is started. :returns: a status message :rtype: str """ return self._status_message @property @synchronized def queue_count(self): """Returns number of TaskQueues contained in this and all nested TaskQueues. :returns: number of queues :rtype: int """ queue_count = 0 for item in self: # count only queues if isinstance(item, TaskQueue): # count the queue itself queue_count += 1 # and its contents queue_count += item.queue_count return queue_count @property @synchronized def task_count(self): """Returns number of tasks contained in this and all nested TaskQueues. :returns: number of tasks :rtype: int """ task_count = 0 for item in self: if isinstance(item, Task): # count tasks task_count += 1 elif isinstance(item, TaskQueue): # count tasks in nested queues task_count += item.task_count return task_count @property @synchronized def current_task_number(self): """Number of the currently running task (if any). :returns: number of the currently running task (if any) :rtype: int or None if no task is currently running """ return self._current_task_number @property @synchronized def current_queue_number(self): """Number of the currently running task queue (if any). :returns: number of the currently running task queue (if any) :rtype: int or None if no task queue is currently running """ return self._current_queue_number @property @synchronized def progress(self): """Task queue processing progress. The progress is reported as a floating point number from 0.0 to 1.0. :returns: task queue processing progress :rtype: float """ if self.current_task_number: return self.task_count / self.current_task_number else: return 0.0 @property @synchronized def summary(self): """Return a multi-line summary of the contents of the task queue. :returns: summary of task queue contents :rtype: str """ if self.parent is None: message = "Top-level task queue: %s\n" % self.name # this is the top-level queue, so add some "global" stats message += "Number of task queues: %d\n" % self.queue_count message += "Number of tasks: %d\n" % self.task_count message += "Task & task group listing:\n" else: message = "Task queue: %s\n" % self.name for item in self: for line in item.summary.splitlines(): message += " %s\n" % line # remove trailing newlines from the top level message if self.parent is None and message[-1] == "\n": message = message.rstrip("\n") return message @property @synchronized def parent(self): """The parent task queue of this task queue (if any). :returns: parent of this task queue (if any) :rtype: TaskQueue instance or None """ return self._parent @parent.setter @synchronized def parent(self, parent_item): # check if a parent is already set if self._parent is not None: # disconnect from the previous parent first self.started.disconnect(self._parent.queue_started.emit) self.completed.disconnect(self._parent.queue_completed.emit) self.queue_started.disconnect(self._parent.queue_started.emit) self.queue_completed.disconnect(self._parent.queue_completed.emit) self.task_started.disconnect(self._parent.task_started.emit) self.task_completed.disconnect(self._parent.task_completed.emit) # set the parent self._parent = parent_item # Connect own signals "up" to the parent, # so that it is possible to monitor how all nested TaskQueues and Tasks # are running from the top-level element. # connect own start/completion signal to parents queue start/completion signal self.started.connect(self._parent.queue_started.emit) self.completed.connect(self._parent.queue_completed.emit) # propagate start/completion signals from nested queues/tasks self.queue_started.connect(self._parent.queue_started.emit) self.queue_completed.connect(self._parent.queue_completed.emit) self.task_started.connect(self._parent.task_started.emit) self.task_completed.connect(self._parent.task_completed.emit) def start(self): """Start processing of the task queue.""" do_start = False with self._lock: # the task queue can only be started once if self.running or self.done: if self.running: # attempt to start a task that is already running log.error("Can't start task queue %s - already running.") else: # attempt to start a task that an already finished task log.error("Can't start task queue %s - already done.") else: do_start = True self._running = True if self.task_count: # only set the initial task number if we have some tasks self._current_task_number = 0 self._current_queue_number = 0 else: log.warning( "Attempting to start an empty task queue (%s).", self.name) if do_start: # go over all task groups and their tasks in order self.started.emit(self) if len(self) == 0: log.warning("The task group %s is empty.", self.name) for item in self: # start the item (TaskQueue/Task) item.start() # we are done, set the task queue state accordingly with self._lock: self._running = False self._done = True # also set the current task variables accordingly as we no longer process a task self._current_task_number = None self._current_queue_number = None # trigger the "completed" signals self.completed.emit(self) # implement the Python list "interface" and make sure parent is always # set to a correct value @synchronized def append(self, item): item.parent = self self._list.append(item) @synchronized def insert(self, index, item): item.parent = self self._list.insert(index, item) @synchronized def __setitem__(self, index, item): item.parent = self return self._list.__setitem__(index, item) @synchronized def __len__(self): return self._list.__len__() @synchronized def count(self): return self._list.count() @synchronized def __getitem__(self, ii): return self._list[ii] @synchronized def __delitem__(self, index): self._list[index].parent = None del self._list[index] @synchronized def pop(self): item = self._list.pop() item.parent = None return item @synchronized def clear(self): for item in self._list: item.parent = None self._list.clear()
class TaskQueue(BaseTask): """TaskQueue represents a queue of TaskQueues or Tasks. TaskQueues and Tasks can be mixed in a single TaskQueue. """ def __init__(self, name, status_message=None): super(TaskQueue, self).__init__(name=name) self._status_message = status_message self._current_task_number = None self._current_queue_number = None # the list backing this TaskQueue instance self._list = [] # triggered if a TaskQueue contained in this one was started/completed self.queue_started = Signal() self.queue_completed = Signal() # triggered when a task is started self.task_started = Signal() self.task_completed = Signal() # connect to the task & queue started signals for # progress reporting purposes self.queue_started.connect(self._queue_started_cb) self.task_started.connect(self._task_started_cb) @synchronized def _queue_started_cb(self, *args): self._current_queue_number += 1 @synchronized def _task_started_cb(self, *args): self._current_task_number += 1 @property def status_message(self): """A status message describing the Queue is trying to achieve. Eq. "Converting all foo into bar." The current main usecase is to set the ProgressHub status message when a TaskQueue is started. :returns: a status message :rtype: str """ return self._status_message @property @synchronized def queue_count(self): """Returns number of TaskQueues contained in this and all nested TaskQueues. :returns: number of queues :rtype: int """ queue_count = 0 for item in self: # count only queues if isinstance(item, TaskQueue): # count the queue itself queue_count += 1 # and its contents queue_count += item.queue_count return queue_count @property @synchronized def task_count(self): """Returns number of tasks contained in this and all nested TaskQueues. :returns: number of tasks :rtype: int """ task_count = 0 for item in self: if isinstance(item, Task): # count tasks task_count += 1 elif isinstance(item, TaskQueue): # count tasks in nested queues task_count += item.task_count return task_count @property @synchronized def current_task_number(self): """Number of the currently running task (if any). :returns: number of the currently running task (if any) :rtype: int or None if no task is currently running """ return self._current_task_number @property @synchronized def current_queue_number(self): """Number of the currently running task queue (if any). :returns: number of the currently running task queue (if any) :rtype: int or None if no task queue is currently running """ return self._current_queue_number @property @synchronized def progress(self): """Task queue processing progress. The progress is reported as a floating point number from 0.0 to 1.0. :returns: task queue processing progress :rtype: float """ if self.current_task_number: return self.task_count / self.current_task_number else: return 0.0 @property @synchronized def summary(self): """Return a multi-line summary of the contents of the task queue. :returns: summary of task queue contents :rtype: str """ if self.parent is None: message = "Top-level task queue: %s\n" % self.name # this is the top-level queue, so add some "global" stats message += "Number of task queues: %d\n" % self.queue_count message += "Number of tasks: %d\n" % self.task_count message += "Task & task group listing:\n" else: message = "Task queue: %s\n" % self.name for item in self: for line in item.summary.splitlines(): message += " %s\n" % line # remove trailing newlines from the top level message if self.parent is None and message[-1] == "\n": message = message.rstrip("\n") return message @property @synchronized def parent(self): """The parent task queue of this task queue (if any). :returns: parent of this task queue (if any) :rtype: TaskQueue instance or None """ return self._parent @parent.setter @synchronized def parent(self, parent_item): # check if a parent is already set if self._parent is not None: # disconnect from the previous parent first self.started.disconnect(self._parent.queue_started.emit) self.completed.disconnect(self._parent.queue_completed.emit) self.queue_started.disconnect(self._parent.queue_started.emit) self.queue_completed.disconnect(self._parent.queue_completed.emit) self.task_started.disconnect(self._parent.task_started.emit) self.task_completed.disconnect(self._parent.task_completed.emit) # set the parent self._parent = parent_item # Connect own signals "up" to the parent, # so that it is possible to monitor how all nested TaskQueues and Tasks # are running from the top-level element. # connect own start/completion signal to parents queue start/completion signal self.started.connect(self._parent.queue_started.emit) self.completed.connect(self._parent.queue_completed.emit) # propagate start/completion signals from nested queues/tasks self.queue_started.connect(self._parent.queue_started.emit) self.queue_completed.connect(self._parent.queue_completed.emit) self.task_started.connect(self._parent.task_started.emit) self.task_completed.connect(self._parent.task_completed.emit) def start(self): """Start processing of the task queue.""" do_start = False with self._lock: # the task queue can only be started once if self.running or self.done: if self.running: # attempt to start a task that is already running log.error("Can't start task queue %s - already running.") else: # attempt to start a task that an already finished task log.error("Can't start task queue %s - already done.") else: do_start = True self._running = True if self.task_count: # only set the initial task number if we have some tasks self._current_task_number = 0 self._current_queue_number = 0 else: log.warning("Attempting to start an empty task queue (%s).", self.name) if do_start: # go over all task groups and their tasks in order self.started.emit(self) if len(self) == 0: log.warning("The task group %s is empty.", self.name) for item in self: # start the item (TaskQueue/Task) item.start() # we are done, set the task queue state accordingly with self._lock: self._running = False self._done = True # also set the current task variables accordingly as we no longer process a task self._current_task_number = None self._current_queue_number = None # trigger the "completed" signals self.completed.emit(self) # implement the Python list "interface" and make sure parent is always # set to a correct value @synchronized def append(self, item): item.parent = self self._list.append(item) @synchronized def insert(self, index, item): item.parent = self self._list.insert(index, item) @synchronized def __setitem__(self, index, item): item.parent = self return self._list.__setitem__(index, item) @synchronized def __len__(self): return self._list.__len__() @synchronized def count(self): return self._list.count() @synchronized def __getitem__(self, ii): return self._list[ii] @synchronized def __delitem__(self, index): self._list[index].parent = None del self._list[index] @synchronized def pop(self): item = self._list.pop() item.parent = None return item @synchronized def clear(self): for item in self._list: item.parent = None self._list.clear()
class DasdFormatting(object): """Class for formatting DASDs.""" def __init__(self): self._dasds = [] self._can_format_unformatted = True self._can_format_ldl = True self._report = Signal() self._report.connect(log.debug) @staticmethod def is_supported(): """Is DASD formatting supported on this machine?""" return arch.is_s390() @property def report(self): """Signal for the progress reporting. Emits messages during the formatting. """ return self._report @property def dasds(self): """List of found DASDs to format.""" return self._dasds @property def dasds_summary(self): """Returns a string summary of DASDs to format.""" return "\n".join(map(self.get_dasd_info, self.dasds)) def get_dasd_info(self, disk): """Returns a string with description of a DASD.""" return "/dev/" + disk.name + " (" + disk.busid + ")" def _is_dasd(self, disk): """Is it a DASD disk?""" return disk.type == "dasd" def _is_unformatted_dasd(self, disk): """Is it an unformatted DASD?""" return self._is_dasd(disk) and blockdev.s390.dasd_needs_format( disk.busid) def _is_ldl_dasd(self, disk): """Is it an LDL DASD?""" return self._is_dasd(disk) and blockdev.s390.dasd_is_ldl(disk.name) def _get_unformatted_dasds(self, disks): """Returns a list of unformatted DASDs.""" result = [] if not self._can_format_unformatted: log.debug("We are not allowed to format unformatted DASDs.") return result for disk in disks: if self._is_unformatted_dasd(disk): log.debug("Found unformatted DASD: %s", self.get_dasd_info(disk)) result.append(disk) return result def _get_ldl_dasds(self, disks): """Returns a list of LDL DASDs.""" result = [] if not self._can_format_ldl: log.debug("We are not allowed to format LDL DASDs.") return result for disk in disks: if self._is_ldl_dasd(disk): log.debug("Found LDL DASD: %s", self.get_dasd_info(disk)) result.append(disk) return result def update_restrictions(self, data): """Read kickstart data to update the restrictions.""" self._can_format_unformatted = data.zerombr.zerombr self._can_format_ldl = data.clearpart.cdl def search_disks(self, disks): """Search for a list of disks for DASDs to format.""" self._dasds = list( set( self._get_unformatted_dasds(disks) + self._get_ldl_dasds(disks))) def should_run(self): """Should we run the formatting?""" return bool(self._dasds) def do_format(self, disk): """Format a disk.""" try: self.report.emit(_("Formatting %s") % self.get_dasd_info(disk)) blockdev.s390.dasd_format(disk.name) except blockdev.S390Error as err: self.report.emit( _("Failed formatting %s") % self.get_dasd_info(disk)) log.error(err) def run(self, storage, data): """Format all found DASDs and update the storage. This method could be run in a separate thread. """ # Check if we have something to format. if not self._dasds: self.report.emit(_("Nothing to format")) return # Format all found DASDs. self.report.emit(_("Formatting DASDs")) for disk in self._dasds: self.do_format(disk) # Update the storage. self.report.emit(_("Probing storage")) storage_initialize(storage, data, storage.devicetree.protected_dev_names) # Update also the storage snapshot to reflect the changes. if on_disk_storage.created: on_disk_storage.dispose_snapshot() on_disk_storage.create_snapshot(storage) @staticmethod def run_automatically(storage, data, callback=None): """Run the DASD formatting automatically. This method could be run in a separate thread. """ if not flags.automatedInstall: return if not DasdFormatting.is_supported(): return disks = getDisks(storage.devicetree) formatting = DasdFormatting() formatting.update_restrictions(data) formatting.search_disks(disks) if not formatting.should_run(): return if callback: formatting.report.connect(callback) formatting.run(storage, data) if callback: formatting.report.disconnect(callback)
class Hub(object, metaclass=ABCMeta): """A Hub is an overview UI screen. A Hub consists of one or more grids of configuration options that the user may choose from. Each grid is provided by a SpokeCategory, and each option is provided by a Spoke. When the user dives down into a Spoke and is finished interacting with it, they are returned to the Hub. Some Spokes are required. The user must interact with all required Spokes before they are allowed to proceed to the next stage of installation. From a layout perspective, a Hub is the entirety of the screen, though the screen itself can be roughly divided into thirds. The top third is some basic navigation information (where you are, what you're installing). The middle third is the grid of Spokes. The bottom third is an action area providing additional buttons (quit, continue) or progress information (during package installation). Installation may consist of multiple chained Hubs, or Hubs with additional standalone screens either before or after them. """ def __init__(self, storage, payload, instclass): """Create a new Hub instance. The arguments this base class accepts defines the API that Hubs have to work with. A Hub does not get free reign over everything in the anaconda class, as that would be a big mess. Instead, a Hub may count on the following: data -- An instance of a pykickstart Handler object. The Hub uses this to populate its UI with defaults and to pass results back after it has run. The data property must be implemented by classes inheriting from Hub. storage -- An instance of storage.Storage. This is useful for determining what storage devices are present and how they are configured. payload -- An instance of a payload.Payload subclass. This is useful for displaying and selecting packages to install, and in carrying out the actual installation. instclass -- An instance of a BaseInstallClass subclass. This is useful for determining distribution-specific installation information like default package selections and default partitioning. """ self._storage = storage self.payload = payload self.instclass = instclass self.paths = {} self._spokes = {} # entry and exit signals # - get the hub instance as a single argument self.entered = Signal() self.exited = Signal() # connect the default callbacks self.entered.connect(self.entry_logger) self.exited.connect(self.exit_logger) @abstractproperty def data(self): pass @property def storage(self): return self._storage def set_path(self, path_id, paths): """Update the paths attribute with list of tuples in the form (module name format string, directory name)""" self.paths[path_id] = paths def entry_logger(self, hub_instance): """Log immediately before this hub is about to be displayed on the screen. Subclasses may override this method if they want to log more specific information, but an overridden method should finish by calling this method so the entry will be logged. Note that due to how the GUI flows, hubs are only entered once - when they are initially displayed. Going to a spoke from a hub and then coming back to the hub does not count as exiting and entering. """ log.debug("Entered hub: %s", hub_instance) def _collectCategoriesAndSpokes(self): """This method is provided so that is can be overridden in a subclass by a custom collect method. One example of such usage is the Initial Setup application. """ return collectCategoriesAndSpokes(self.paths, self.__class__, self.data.displaymode.displayMode) def exit_logger(self, hub_instance): """Log when a user leaves the hub. Subclasses may override this method if they want to log more specific information, but an overridden method should finish by calling this method so the exit will be logged. Note that due to how the GUI flows, hubs are not exited when the user selects a spoke from the hub. They are only exited when the continue or quit button is clicked on the hub. """ log.debug("Left hub: %s", hub_instance) def __repr__(self): """Return the class name as representation. Returning the class name should be enough the uniquely identify a hub. """ return self.__class__.__name__
class Spoke(object, metaclass=ABCMeta): """A Spoke is a single configuration screen. There are several different places where a Spoke can be displayed, each of which will have its own unique class. A Spoke is typically used when an element in the Hub is selected but can also be displayed before a Hub or between multiple Hubs. What amount of the UI layout a Spoke provides depends upon where it is to be shown. Regardless, the UI of a Spoke should be given by an interface description file like glade as often as possible, though this is not a strict requirement. Class attributes: category -- Under which SpokeCategory shall this Spoke be displayed in the Hub? This is a reference to a Hub subclass (not an object, but the class itself). If no category is given, this Spoke will not be displayed. Note that category is not required for any Spokes appearing before or after a Hub. icon -- The name of the icon to be displayed in the SpokeSelector widget corresponding to this Spoke instance. If no icon is given, the default from SpokeSelector will be used. title -- The title to be displayed in the SpokeSelector widget corresponding to this Spoke instance. If no title is given, the default from SpokeSelector will be used. """ category = None icon = None title = None def __init__(self, storage, payload, instclass): """Create a new Spoke instance. The arguments this base class accepts defines the API that spokes have to work with. A Spoke does not get free reign over everything in the anaconda class, as that would be a big mess. Instead, a Spoke may count on the following: data -- An instance of a pykickstart Handler object. The Spoke uses this to populate its UI with defaults and to pass results back after it has run. The data property must be implemented by classes inherting from Spoke. storage -- An instance of storage.Storage. This is useful for determining what storage devices are present and how they are configured. payload -- An instance of a payload.Payload subclass. This is useful for displaying and selecting packages to install, and in carrying out the actual installation. instclass -- An instance of a BaseInstallClass subclass. This is useful for determining distribution-specific installation information like default package selections and default partitioning. """ self._storage = storage self.payload = payload self.instclass = instclass self.applyOnSkip = False self.visitedSinceApplied = True # entry and exit signals # - get the hub instance as a single argument self.entered = Signal() self.exited = Signal() # connect default callbacks for the signals self.entered.connect(self.entry_logger) self.entered.connect(self._mark_screen_visited) self.exited.connect(self.exit_logger) @abstractproperty def data(self): pass @property def storage(self): return self._storage @classmethod def should_run(cls, environment, data): """This method is responsible for beginning Spoke initialization. It should return True if the spoke is to be shown while in <environment> and False if it should be skipped. It might be called multiple times, with or without (None) the data argument. """ return environment == ANACONDA_ENVIRON def apply(self): """Apply the selections made on this Spoke to the object's preset data object. This method must be provided by every subclass. """ raise NotImplementedError @property def changed(self): """Have the values on the spoke changed since the last time it was run? If not, the apply and execute methods will be skipped. This is to avoid the spoke doing potentially long-lived and destructive actions that are completely unnecessary. """ return True @property def configured(self): """This method returns a list of textual ids that should be written into the after-install customization status file for the firstboot and GIE to know that the spoke was configured and what value groups were provided.""" return ["%s.%s" % (self.__class__.__module__, self.__class__.__name__)] @property def completed(self): """Has this spoke been visited and completed? If not and the spoke is mandatory, a special warning icon will be shown on the Hub beside the spoke, and a highlighted message will be shown at the bottom of the Hub. Installation will not be allowed to proceed until all mandatory spokes are complete. WARNING: This can be called before the spoke is finished initializing if the spoke starts a thread. It should make sure it doesn't access things until they are completely setup. """ return False @property def sensitive(self): """May the user click on this spoke's selector and be taken to the spoke? This is different from the showable property. A spoke that is not sensitive will still be shown on the hub, but the user may not enter it. This is also different from the ready property. A spoke that is not ready may not be entered, but the spoke may become ready in the future. A spoke that is not sensitive will likely not become so. Most spokes will not want to override this method. """ return True @property def mandatory(self): """Mark this spoke as mandatory. Installation will not be allowed to proceed until all mandatory spokes are complete. Spokes are mandatory unless marked as not being so. """ return True def execute(self): """Cause the data object to take effect on the target system. This will usually be as simple as calling one or more of the execute methods on the data object. This method does not need to be provided by all subclasses. This method will be called in two different places: (1) Immediately after initialize on kickstart installs. (2) Immediately after apply in all cases. """ pass @property def status(self): """Given the current status of whatever this Spoke configures, return a very brief string. The purpose of this is to display something on the Hub under the Spoke's title so the user can tell at a glance how things are configured. A spoke's status line on the Hub can also be overloaded to provide information about why a Spoke is not yet ready, or if an error has occurred when setting it up. This can be done by calling send_message from pyanaconda.ui.communication with the target Spoke's class name and the message to be displayed. If the Spoke was not yet ready when send_message was called, the message will be overwritten with the value of this status property when the Spoke becomes ready. """ raise NotImplementedError def _mark_screen_visited(self, spoke_instance): """Report the spoke screen as visited to the Spoke Access Manager.""" screen_access.sam.mark_screen_visited(spoke_instance.__class__.__name__) def entry_logger(self, spoke_instance): """Log immediately before this spoke is about to be displayed on the screen. Subclasses may override this method if they want to log more specific information, but an overridden method should finish by calling this method so the entry will be logged. """ log.debug("Entered spoke: %s", spoke_instance) def exit_logger(self, spoke_instance): """Log when a user leaves the spoke. Subclasses may override this method if they want to log more specific information, but an overridden method should finish by calling this method so the exit will be logged. """ log.debug("Left spoke: %s", spoke_instance) def finished(self): """Called when exiting the Summary Hub This can be used to cleanup the spoke before continuing the installation. This method is optional. """ pass # Initialization controller related code # # - initialization_controller # -> The controller for this spokes and all others on the given hub. # -> The controller has the init_done signal that can be used to trigger # actions that should happen once all spokes on the given Hub have # finished initialization. # -> If there is no Hub (standalone spoke) the property is None # # - initialize_start() # -> Should be called when Spoke initialization is started. # -> Needs to be called explicitly, if we called it for every spoke by default # then any spoke that does not call initialize_done() would prevent the # controller form ever triggering the init_done signal. # # - initialize_done() # -> Must be called by every spoke that calls initialize_start() or else the init_done # signal will never be emitted. @property def initialization_controller(self): # standalone spokes don't have a category if self.category: return lifecycle.get_controller_by_category(category_name=self.category.__name__) else: return None def initialize_start(self): # get the correct controller for this spoke spoke_controller = self.initialization_controller # check if there actually is a controller for this spoke, there might not be one # if this is a standalone spoke if spoke_controller: spoke_controller.module_init_start(self) def initialize_done(self): # get the correct controller for this spoke spoke_controller = self.initialization_controller # check if there actually is a controller for this spoke, there might not be one # if this is a standalone spoke if spoke_controller: spoke_controller.module_init_done(self) def __repr__(self): """Return the class name as representation. Returning the class name should be enough the uniquely identify a spoke. """ return self.__class__.__name__
class Hub(object, metaclass=ABCMeta): """A Hub is an overview UI screen. A Hub consists of one or more grids of configuration options that the user may choose from. Each grid is provided by a SpokeCategory, and each option is provided by a Spoke. When the user dives down into a Spoke and is finished interacting with it, they are returned to the Hub. Some Spokes are required. The user must interact with all required Spokes before they are allowed to proceed to the next stage of installation. From a layout perspective, a Hub is the entirety of the screen, though the screen itself can be roughly divided into thirds. The top third is some basic navigation information (where you are, what you're installing). The middle third is the grid of Spokes. The bottom third is an action area providing additional buttons (quit, continue) or progress information (during package installation). Installation may consist of multiple chained Hubs, or Hubs with additional standalone screens either before or after them. """ def __init__(self, storage, payload, instclass): """Create a new Hub instance. The arguments this base class accepts defines the API that Hubs have to work with. A Hub does not get free reign over everything in the anaconda class, as that would be a big mess. Instead, a Hub may count on the following: data -- An instance of a pykickstart Handler object. The Hub uses this to populate its UI with defaults and to pass results back after it has run. The data property must be implemented by classes inheriting from Hub. storage -- An instance of storage.Storage. This is useful for determining what storage devices are present and how they are configured. payload -- An instance of a packaging.Payload subclass. This is useful for displaying and selecting packages to install, and in carrying out the actual installation. instclass -- An instance of a BaseInstallClass subclass. This is useful for determining distribution-specific installation information like default package selections and default partitioning. """ self._storage = storage self.payload = payload self.instclass = instclass self.paths = {} self._spokes = {} # entry and exit signals # - get the hub instance as a single argument self.entered = Signal() self.exited = Signal() # connect the default callbacks self.entered.connect(self.entry_logger) self.exited.connect(self.exit_logger) @abstractproperty def data(self): pass @property def storage(self): return self._storage def set_path(self, path_id, paths): """Update the paths attribute with list of tuples in the form (module name format string, directory name)""" self.paths[path_id] = paths def entry_logger(self, hub_instance): """Log immediately before this hub is about to be displayed on the screen. Subclasses may override this method if they want to log more specific information, but an overridden method should finish by calling this method so the entry will be logged. Note that due to how the GUI flows, hubs are only entered once - when they are initially displayed. Going to a spoke from a hub and then coming back to the hub does not count as exiting and entering. """ log.debug("Entered hub: %s", hub_instance) def _collectCategoriesAndSpokes(self): """This method is provided so that is can be overridden in a subclass by a custom collect method. One example of such usage is the Initial Setup application. """ return collectCategoriesAndSpokes(self.paths, self.__class__, self.data.displaymode.displayMode) def exit_logger(self, hub_instance): """Log when a user leaves the hub. Subclasses may override this method if they want to log more specific information, but an overridden method should finish by calling this method so the exit will be logged. Note that due to how the GUI flows, hubs are not exited when the user selects a spoke from the hub. They are only exited when the continue or quit button is clicked on the hub. """ log.debug("Left hub: %s", hub_instance) def __repr__(self): """Return the class name as representation. Returning the class name should be enough the uniquely identify a hub. """ return self.__class__.__name__
class Spoke(object, metaclass=ABCMeta): """A Spoke is a single configuration screen. There are several different places where a Spoke can be displayed, each of which will have its own unique class. A Spoke is typically used when an element in the Hub is selected but can also be displayed before a Hub or between multiple Hubs. What amount of the UI layout a Spoke provides depends upon where it is to be shown. Regardless, the UI of a Spoke should be given by an interface description file like glade as often as possible, though this is not a strict requirement. Class attributes: category -- Under which SpokeCategory shall this Spoke be displayed in the Hub? This is a reference to a Hub subclass (not an object, but the class itself). If no category is given, this Spoke will not be displayed. Note that category is not required for any Spokes appearing before or after a Hub. icon -- The name of the icon to be displayed in the SpokeSelector widget corresponding to this Spoke instance. If no icon is given, the default from SpokeSelector will be used. title -- The title to be displayed in the SpokeSelector widget corresponding to this Spoke instance. If no title is given, the default from SpokeSelector will be used. """ category = None icon = None title = None def __init__(self, storage, payload, instclass): """Create a new Spoke instance. The arguments this base class accepts defines the API that spokes have to work with. A Spoke does not get free reign over everything in the anaconda class, as that would be a big mess. Instead, a Spoke may count on the following: data -- An instance of a pykickstart Handler object. The Spoke uses this to populate its UI with defaults and to pass results back after it has run. The data property must be implemented by classes inherting from Spoke. storage -- An instance of storage.Storage. This is useful for determining what storage devices are present and how they are configured. payload -- An instance of a packaging.Payload subclass. This is useful for displaying and selecting packages to install, and in carrying out the actual installation. instclass -- An instance of a BaseInstallClass subclass. This is useful for determining distribution-specific installation information like default package selections and default partitioning. """ self._storage = storage self.payload = payload self.instclass = instclass self.applyOnSkip = False self.visitedSinceApplied = True # entry and exit signals # - get the hub instance as a single argument self.entered = Signal() self.exited = Signal() # connect default callbacks for the signals self.entered.connect(self.entry_logger) self.entered.connect(self._mark_screen_visited) self.exited.connect(self.exit_logger) @abstractproperty def data(self): pass @property def storage(self): return self._storage @classmethod def should_run(cls, environment, data): """This method is responsible for beginning Spoke initialization. It should return True if the spoke is to be shown while in <environment> and False if it should be skipped. It might be called multiple times, with or without (None) the data argument. """ return environment == ANACONDA_ENVIRON def apply(self): """Apply the selections made on this Spoke to the object's preset data object. This method must be provided by every subclass. """ raise NotImplementedError @property def changed(self): """Have the values on the spoke changed since the last time it was run? If not, the apply and execute methods will be skipped. This is to avoid the spoke doing potentially long-lived and destructive actions that are completely unnecessary. """ return True @property def configured(self): """This method returns a list of textual ids that should be written into the after-install customization status file for the firstboot and GIE to know that the spoke was configured and what value groups were provided.""" return ["%s.%s" % (self.__class__.__module__, self.__class__.__name__)] @property def completed(self): """Has this spoke been visited and completed? If not and the spoke is mandatory, a special warning icon will be shown on the Hub beside the spoke, and a highlighted message will be shown at the bottom of the Hub. Installation will not be allowed to proceed until all mandatory spokes are complete. WARNING: This can be called before the spoke is finished initializing if the spoke starts a thread. It should make sure it doesn't access things until they are completely setup. """ return False @property def sensitive(self): """May the user click on this spoke's selector and be taken to the spoke? This is different from the showable property. A spoke that is not sensitive will still be shown on the hub, but the user may not enter it. This is also different from the ready property. A spoke that is not ready may not be entered, but the spoke may become ready in the future. A spoke that is not sensitive will likely not become so. Most spokes will not want to override this method. """ return True @property def mandatory(self): """Mark this spoke as mandatory. Installation will not be allowed to proceed until all mandatory spokes are complete. Spokes are mandatory unless marked as not being so. """ return True def execute(self): """Cause the data object to take effect on the target system. This will usually be as simple as calling one or more of the execute methods on the data object. This method does not need to be provided by all subclasses. This method will be called in two different places: (1) Immediately after initialize on kickstart installs. (2) Immediately after apply in all cases. """ pass @property def status(self): """Given the current status of whatever this Spoke configures, return a very brief string. The purpose of this is to display something on the Hub under the Spoke's title so the user can tell at a glance how things are configured. A spoke's status line on the Hub can also be overloaded to provide information about why a Spoke is not yet ready, or if an error has occurred when setting it up. This can be done by calling send_message from pyanaconda.ui.communication with the target Spoke's class name and the message to be displayed. If the Spoke was not yet ready when send_message was called, the message will be overwritten with the value of this status property when the Spoke becomes ready. """ raise NotImplementedError def _mark_screen_visited(self, spoke_instance): """Report the spoke screen as visited to the Spoke Access Manager.""" screen_access.sam.mark_screen_visited(spoke_instance.__class__.__name__) def entry_logger(self, spoke_instance): """Log immediately before this spoke is about to be displayed on the screen. Subclasses may override this method if they want to log more specific information, but an overridden method should finish by calling this method so the entry will be logged. """ log.debug("Entered spoke: %s", spoke_instance) def exit_logger(self, spoke_instance): """Log when a user leaves the spoke. Subclasses may override this method if they want to log more specific information, but an overridden method should finish by calling this method so the exit will be logged. """ log.debug("Left spoke: %s", spoke_instance) def finished(self): """Called when exiting the Summary Hub This can be used to cleanup the spoke before continuing the installation. This method is optional. """ pass # Initialization controller related code # # - initialization_controller # -> The controller for this spokes and all others on the given hub. # -> The controller has the init_done signal that can be used to trigger # actions that should happen once all spokes on the given Hub have # finished initialization. # -> If there is no Hub (standalone spoke) the property is None # # - initialize_start() # -> Should be called when Spoke initialization is started. # -> Needs to be called explicitly, if we called it for every spoke by default # then any spoke that does not call initialize_done() would prevent the # controller form ever triggering the init_done signal. # # - initialize_done() # -> Must be called by every spoke that calls initialize_start() or else the init_done # signal will never be emitted. @property def initialization_controller(self): # standalone spokes don't have a category if self.category: return lifecycle.get_controller_by_category(category_name=self.category.__name__) else: return None def initialize_start(self): # get the correct controller for this spoke spoke_controller = self.initialization_controller # check if there actually is a controller for this spoke, there might not be one # if this is a standalone spoke if spoke_controller: spoke_controller.module_init_start(self) def initialize_done(self): # get the correct controller for this spoke spoke_controller = self.initialization_controller # check if there actually is a controller for this spoke, there might not be one # if this is a standalone spoke if spoke_controller: spoke_controller.module_init_done(self) def __repr__(self): """Return the class name as representation. Returning the class name should be enough the uniquely identify a spoke. """ return self.__class__.__name__