Пример #1
0
    def on_fetch_button_clicked(self, *args):
        """Handler for the Fetch button"""

        with self._fetch_flag_lock:
            if self._fetching:
                # some other fetching/pre-processing running, give up
                return

        # prevent user from changing the URL in the meantime
        self._content_url_entry.set_sensitive(False)
        self._fetch_button.set_sensitive(False)
        url = self._content_url_entry.get_text()
        really_show(self._progress_box)
        really_show(self._progress_spinner)

        if not data_fetch.can_fetch_from(url):
            msg = _("Invalid or unsupported URL")
            # cannot start fetching
            self._progress_label.set_markup("<b>%s</b>" % msg)
            self._wrong_content(msg)
            return

        self._progress_label.set_text(_("Fetching content..."))
        self._progress_spinner.start()
        self._addon_data.content_url = url
        if url.endswith(".rpm"):
            self._addon_data.content_type = "rpm"
        elif any(url.endswith(arch_type) for arch_type in common.SUPPORTED_ARCHIVES):
            self._addon_data.content_type = "archive"
        else:
            self._addon_data.content_type = "datastream"

        self._fetch_data_and_initialize()
 def _update_luks_combo(self):
     if self._encryptCheckbutton.get_active():
         really_show(self._luks_label)
         really_show(self._luks_combo)
     else:
         really_hide(self._luks_label)
         really_hide(self._luks_combo)
Пример #3
0
    def _populate_raid(self):
        """ Set up the raid-specific portion of the device details.

            Hide the RAID level menu if this device type does not support RAID.
            Choose a default RAID level.
        """
        if not containerRaidLevelsSupported(self.device_type):
            for widget in [self._raidLevelLabel, self._raidLevelCombo]:
                really_hide(widget)
            return

        raid_level = self.raid_level or defaultContainerRaidLevel(
            self.device_type)
        raid_level_name = raidLevelSelection(raid_level)

        # Set a default RAID level in the combo.
        for (i, row) in enumerate(self._raidLevelCombo.get_model()):
            log.debug("container dialog: raid level %s", row[1])
            if row[1] == raid_level_name:
                self._raidLevelCombo.set_active(i)
                break

        for widget in [self._raidLevelLabel, self._raidLevelCombo]:
            really_show(widget)
        fancy_set_sensitive(self._raidLevelCombo, not self.exists)
Пример #4
0
    def _update_summary(self):
        summary_button = self.builder.get_object("summary_button")
        label = self.builder.get_object("summary_button_label")

        # We need to remove ancestor devices from the count.  Otherwise, we'll
        # end up in a situation where selecting one multipath device could
        # potentially show three devices selected (mpatha, sda, sdb for instance).
        count = len([
            disk for disk in self._selected_disks
            if disk not in self._ancestors
        ])

        summary = CP_(
            "GUI|Installation Destination|Filter",
            "{} _storage device selected",
            "{} _storage devices selected",
            count
        ).format(count)

        if count > 0:
            really_show(summary_button)
            label.set_text(summary)
            label.set_use_underline(True)
        else:
            really_hide(summary_button)
    def _populate_raid(self):
        """Set up the raid-specific portion of the device details.

        Hide the RAID level menu if this device type does not support RAID.
        Choose a default RAID level.
        """
        self._raidStoreFilter.set_visible_func(self._raid_level_visible)
        self._raidStoreFilter.refilter()

        if not self._supported_raid_levels:
            for widget in [self._raidLevelLabel, self._raidLevelCombo]:
                really_hide(widget)
            return

        raid_level = self._request.container_raid_level

        for (i, row) in enumerate(self._raidLevelCombo.get_model()):
            if row[1] == raid_level:
                self._raidLevelCombo.set_active(i)
                break

        for widget in [self._raidLevelLabel, self._raidLevelCombo]:
            really_show(widget)

        fancy_set_sensitive(self._raidLevelCombo,
                            self._permissions.container_raid_level)
Пример #6
0
    def initialize(self):
        NormalSpoke.initialize(self)

        self._grabObjects()

        # I shouldn't have to do this outside GtkBuilder, but it really doesn't
        # want to let me pass in user data.
        self._autodetectButton.connect("toggled", self.on_source_toggled,
                                       self._autodetectBox)
        self._isoButton.connect("toggled", self.on_source_toggled,
                                self._isoBox)
        self._networkButton.connect("toggled", self.on_source_toggled,
                                    self._networkBox)

        # Show or hide the updates option based on the installclass
        if self.instclass.installUpdates:
            really_show(self._updatesBox)
        else:
            really_hide(self._updatesBox)

        self._repoNameWarningBox = self.builder.get_object(
            "repoNameWarningBox")
        self._repoNameWarningLabel = self.builder.get_object(
            "repoNameWarningLabel")

        self._repoNamesWarningBox = self.builder.get_object(
            "repoNamesWarningBox")
        self._repoNamesWarningLabel = self.builder.get_object(
            "repoNamesWarningLabel")

        threadMgr.add(
            AnacondaThread(name=constants.THREAD_SOURCE_WATCHER,
                           target=self._initialize))
Пример #7
0
    def on_fetch_button_clicked(self, *args):
        """Handler for the Fetch button"""

        with self._fetch_flag_lock:
            if self._fetching:
                # some other fetching/pre-processing running, give up
                return

        # prevent user from changing the URL in the meantime
        self._content_url_entry.set_sensitive(False)
        self._fetch_button.set_sensitive(False)
        url = self._content_url_entry.get_text()
        really_show(self._progress_box)
        really_show(self._progress_spinner)

        if not data_fetch.can_fetch_from(url):
            msg = _("Invalid or unsupported URL")
            # cannot start fetching
            self._progress_label.set_markup("<b>%s</b>" % msg)
            self._wrong_content(msg)
            return

        self._progress_label.set_text(_("Fetching content..."))
        self._progress_spinner.start()
        self._addon_data.content_url = url
        if url.endswith(".rpm"):
            self._addon_data.content_type = "rpm"
        elif any(
                url.endswith(arch_type)
                for arch_type in common.SUPPORTED_ARCHIVES):
            self._addon_data.content_type = "archive"
        else:
            self._addon_data.content_type = "datastream"

        self._fetch_data_and_initialize()
Пример #8
0
 def _display_repo_names_message(self):
     """ Displays a warning if the list of repo names is not valid.
         Returns the warning message displayed, if any.
     """
     warning_msg = self._verify_repo_names()
     if warning_msg:
         self._repoNamesWarningLabel.set_text(_(warning_msg))
         really_show(self._repoNamesWarningBox)
         self.set_warning(_("Duplicate repository names not allowed; choose a unique name for each repository."))
         self.window.show_all()
     else:
         self._repoNamesWarningLabel.set_text("")
         really_hide(self._repoNamesWarningBox)
         self.clear_info()
     return warning_msg
Пример #9
0
 def _display_repo_name_message(self, repo, name):
     """ Displays a warning if the repo name is not valid.
         Returns the warning message displayed, if any.
         :param repo: kickstart repository object
         :type repo: RepoData
         :param name: the designated name for the repo
         :type name: string
     """
     warning_msg = self._verify_repo_name(repo, name)
     if warning_msg:
         self._repoNameWarningLabel.set_text(_(warning_msg))
         really_show(self._repoNameWarningBox)
     else:
         self._repoNameWarningLabel.set_text("")
         really_hide(self._repoNameWarningBox)
     return warning_msg
Пример #10
0
 def _display_repo_name_message(self, repo, name):
     """ Displays a warning if the repo name is not valid.
         Returns the warning message displayed, if any.
         :param repo: kickstart repository object
         :type repo: RepoData
         :param name: the designated name for the repo
         :type name: string
     """
     warning_msg = self._verify_repo_name(repo, name)
     if warning_msg:
         self._repoNameWarningLabel.set_text(_(warning_msg))
         really_show(self._repoNameWarningBox)
     else:
         self._repoNameWarningLabel.set_text("")
         really_hide(self._repoNameWarningBox)
     return warning_msg
Пример #11
0
 def _display_repo_names_message(self):
     """ Displays a warning if the list of repo names is not valid.
         Returns the warning message displayed, if any.
     """
     warning_msg = self._verify_repo_names()
     if warning_msg:
         self._repoNamesWarningLabel.set_text(_(warning_msg))
         really_show(self._repoNamesWarningBox)
         self.set_warning(
             _("Duplicate repository names not allowed; choose a unique name for each repository."
               ))
         self.window.show_all()
     else:
         self._repoNamesWarningLabel.set_text("")
         really_hide(self._repoNamesWarningBox)
         self.clear_info()
     return warning_msg
Пример #12
0
    def _update_ids_visibility(self):
        """
        Updates visibility of the combo boxes that are used to select the DS
        and XCCDF IDs.

        """

        if self._using_ds:
            # only show the combo boxes if there are multiple data streams or
            # multiple xccdfs (IOW if there's something to choose from)
            ds_ids = list(self._ds_checklists.keys())
            if len(ds_ids) > 1 or len(self._ds_checklists[ds_ids[0]]) > 1:
                really_show(self._ids_box)
                return

        # not showing, hide instead
        really_hide(self._ids_box)
Пример #13
0
    def _update_ids_visibility(self):
        """
        Updates visibility of the combo boxes that are used to select the DS
        and XCCDF IDs.

        """

        if self._using_ds:
            # only show the combo boxes if there are multiple data streams or
            # multiple xccdfs (IOW if there's something to choose from)
            ds_ids = list(self._ds_checklists.keys())
            if len(ds_ids) > 1 or len(self._ds_checklists[ds_ids[0]]) > 1:
                really_show(self._ids_box)
                return

        # not showing, hide instead
        really_hide(self._ids_box)
Пример #14
0
    def initialize(self):
        NormalSpoke.initialize(self)

        self._grabObjects()

        # I shouldn't have to do this outside GtkBuilder, but it really doesn't
        # want to let me pass in user data.
        self._autodetectButton.connect("toggled", self.on_source_toggled, self._autodetectBox)
        self._isoButton.connect("toggled", self.on_source_toggled, self._isoBox)
        self._networkButton.connect("toggled", self.on_source_toggled, self._networkBox)

        # Show or hide the updates option based on the installclass
        if self.instclass.installUpdates:
            really_show(self._updatesBox)
        else:
            really_hide(self._updatesBox)

        threadMgr.add(AnacondaThread(name=constants.THREAD_SOURCE_WATCHER, target=self._initialize))
Пример #15
0
    def _set_status(self, error_message):
        """Set UI element states according to passphrase check results.

        NOTE: This method is called every time the checker finishes running all checks.
        """
        success = not error_message
        if success:
            really_hide(self._passphrase_warning_image)
            really_hide(self._passphrase_warning_label)
        else:
            if not self._ascii_check.result.success:
                # ASCII check runs last, so if just it has failed the result is only a warning
                result_icon = "dialog-warning-symbolic"
            else:
                # something else failed and that's a critical error
                result_icon = "dialog-error-symbolic"
            self._passphrase_warning_image.set_from_icon_name(
                result_icon, Gtk.IconSize.BUTTON)
            self._passphrase_warning_label.set_text(error_message)
            really_show(self._passphrase_warning_image)
            really_show(self._passphrase_warning_label)

        # The save button should only be sensitive if both passphrases match
        # and are valid enough for current policy
        self._passphrase_good_enough = False
        if self._checker.success:
            self._passphrase_good_enough = True
        elif len(
                self._checker.failed_checks
        ) == 1 and self._validity_check in self._checker._failed_checks:
            # only the password validity check failed
            if self._checker.policy.is_strict:
                # this is not fine for the strict password policy
                self._passphrase_good_enough = False
            else:
                # but is totally fine under the non-strict policy
                self._passphrase_good_enough = True
        elif len(self._checker.failed_checks
                 ) == 1 and self._ascii_check in self._checker._failed_checks:
            # enable the save button if only the ascii check has failed
            self._passphrase_good_enough = True

        # set the save button sensitivity accordingly
        self._save_button.set_sensitive(self._passphrase_good_enough)
Пример #16
0
    def set_status(self, inputcheck):
        # Set the warning message with the result from the first failed check
        failed_check = next(self.failed_checks_with_message, None)

        if failed_check:
            result_icon, result_message = failed_check.check_status
            self._passphrase_warning_image.set_from_icon_name(result_icon, Gtk.IconSize.BUTTON)
            self._passphrase_warning_label.set_text(result_message)
            really_show(self._passphrase_warning_image)
            really_show(self._passphrase_warning_label)
        else:
            really_hide(self._passphrase_warning_image)
            really_hide(self._passphrase_warning_label)

        # The save button should only be sensitive if the match check passes
        if self._passphrase_match_check.check_status == InputCheck.CHECK_OK and \
                (not self.policy.strict or self._strength_check.check_status == InputCheck.CHECK_OK):
            self._save_button.set_sensitive(True)
        else:
            self._save_button.set_sensitive(False)
Пример #17
0
    def set_status(self, inputcheck):
        # Set the warning message with the result from the first failed check
        failed_check = next(self.failed_checks_with_message, None)

        if failed_check:
            result_icon, result_message = failed_check.check_status
            self._passphrase_warning_image.set_from_icon_name(
                result_icon, Gtk.IconSize.BUTTON)
            self._passphrase_warning_label.set_text(result_message)
            really_show(self._passphrase_warning_image)
            really_show(self._passphrase_warning_label)
        else:
            really_hide(self._passphrase_warning_image)
            really_hide(self._passphrase_warning_label)

        # The save button should only be sensitive if the match check passes
        if self._passphrase_match_check.check_status == InputCheck.CHECK_OK and \
                (not self.policy.strict or self._strength_check.check_status == InputCheck.CHECK_OK):
            self._save_button.set_sensitive(True)
        else:
            self._save_button.set_sensitive(False)
Пример #18
0
    def _set_status(self, error_message):
        """Set UI element states according to passphrase check results.

        NOTE: This method is called every time the checker finishes running all checks.
        """
        success = not error_message
        if success:
            really_hide(self._passphrase_warning_image)
            really_hide(self._passphrase_warning_label)
        else:
            if not self._ascii_check.result.success:
                # ASCII check runs last, so if just it has failed the result is only a warning
                result_icon = "dialog-warning"
            else:
                # something else failed and that's a critical error
                result_icon = "dialog-error"
            self._passphrase_warning_image.set_from_icon_name(result_icon, Gtk.IconSize.BUTTON)
            self._passphrase_warning_label.set_text(error_message)
            really_show(self._passphrase_warning_image)
            really_show(self._passphrase_warning_label)

        # The save button should only be sensitive if both passphrases match
        # and are valid enough for current policy
        self._passphrase_good_enough = False
        if self._checker.success:
            self._passphrase_good_enough = True
        elif len(self._checker.failed_checks) == 1 and self._validity_check in self._checker._failed_checks:
            # only the password validity check failed
            if self._checker.policy.strict:
                # this is not fine for the strict password policy
                self._passphrase_good_enough = False
            else:
                # but is totally fine under the non-strict policy
                self._passphrase_good_enough = True
        elif len(self._checker.failed_checks) == 1 and self._ascii_check in self._checker._failed_checks:
            # enable the save button if only the ascii check has failed
            self._passphrase_good_enough = True

        # set the save button sensitivity accordingly
        self._save_button.set_sensitive(self._passphrase_good_enough)
Пример #19
0
    def _populate_raid(self):
        """ Set up the raid-specific portion of the device details.

            Hide the RAID level menu if this device type does not support RAID.
            Choose a default RAID level.
        """
        if not containerRaidLevelsSupported(self.device_type):
            for widget in [self._raidLevelLabel, self._raidLevelCombo]:
                really_hide(widget)
            return

        raid_level = self.raid_level or defaultContainerRaidLevel(self.device_type)
        raid_level_name = raidLevelSelection(raid_level)

        # Set a default RAID level in the combo.
        for (i, row) in enumerate(self._raidLevelCombo.get_model()):
            log.debug("container dialog: raid level %s", row[1])
            if row[1] == raid_level_name:
                self._raidLevelCombo.set_active(i)
                break

        for widget in [self._raidLevelLabel, self._raidLevelCombo]:
            really_show(widget)
        fancy_set_sensitive(self._raidLevelCombo, not self.exists)
Пример #20
0
    def refresh(self):
        """
        The refresh method that is called every time the spoke is displayed.
        It should update the UI elements according to the contents of
        self.data.

        :see: pyanaconda.ui.common.UIObject.refresh

        """

        if not self._addon_data.content_defined:
            # hide the control buttons
            really_hide(self._control_buttons)

            # provide SSG if available
            if common.ssg_available():
                # show the SSG button and tweak the rest of the line
                # (the label)
                really_show(self._ssg_button)
                # TRANSLATORS: the other choice if SCAP Security Guide is also
                # available
                tip = _(" or enter data stream content or archive URL below:")
            else:
                # hide the SSG button
                really_hide(self._ssg_button)
                tip = _(
                    "No content found. Please enter data stream content or "
                    "archive URL below:")

            self._no_content_label.set_text(tip)

            # hide the progress box, no progress now
            with self._fetch_flag_lock:
                if not self._fetching:
                    really_hide(self._progress_box)

                    self._content_url_entry.set_sensitive(True)
                    self._fetch_button.set_sensitive(True)

                    if not self._content_url_entry.get_text():
                        # no text -> no info/warning
                        self._progress_label.set_text("")

            # switch to the page allowing user to enter content URL and fetch
            # it
            self._main_notebook.set_current_page(GET_CONTENT_PAGE)
            self._content_url_entry.grab_focus()

            # nothing more to do here
            return
        else:
            # show control buttons
            really_show(self._control_buttons)

            self._main_notebook.set_current_page(SET_PARAMS_PAGE)

        self._active_profile = self._addon_data.profile_id

        self._update_ids_visibility()

        if self._using_ds:
            if self._addon_data.datastream_id:
                set_combo_selection(self._ds_combo,
                                    self._addon_data.datastream_id,
                                    unset_first=True)
            else:
                try:
                    default_ds = next(iter(self._ds_checklists.keys()))
                    set_combo_selection(self._ds_combo,
                                        default_ds,
                                        unset_first=True)
                except StopIteration:
                    # no data stream available
                    pass

                if self._addon_data.datastream_id and self._addon_data.xccdf_id:
                    set_combo_selection(self._xccdf_combo,
                                        self._addon_data.xccdf_id,
                                        unset_first=True)
        else:
            # no combobox changes --> need to update profiles store manually
            self._update_profiles_store()

        if self._addon_data.profile_id:
            set_treeview_selection(self._profiles_view,
                                   self._addon_data.profile_id)

        self._rule_data = self._addon_data.rule_data

        self._update_message_store()
Пример #21
0
 def _onSelectorAdded(self, container, widget, label):
     really_show(label)
Пример #22
0
 def _on_selector_added(self, container, widget, label):
     really_show(label)
Пример #23
0
 def _onDataAdded(self, container, widget):
     really_show(self._dataLabel)
Пример #24
0
    def refresh(self):
        """
        The refresh method that is called every time the spoke is displayed.
        It should update the UI elements according to the contents of
        self.data.

        :see: pyanaconda.ui.common.UIObject.refresh

        """

        if not self._addon_data.content_defined:
            # hide the control buttons
            really_hide(self._control_buttons)

            # provide SSG if available
            if common.ssg_available():
                # show the SSG button and tweak the rest of the line
                # (the label)
                really_show(self._ssg_button)
                # TRANSLATORS: the other choice if SCAP Security Guide is also
                # available
                tip = _(" or enter data stream content or archive URL below:")
            else:
                # hide the SSG button
                really_hide(self._ssg_button)
                tip = _("No content found. Please enter data stream content or "
                        "archive URL below:")

            self._no_content_label.set_text(tip)

            # hide the progress box, no progress now
            with self._fetch_flag_lock:
                if not self._fetching:
                    really_hide(self._progress_box)

                    self._content_url_entry.set_sensitive(True)
                    self._fetch_button.set_sensitive(True)

                    if not self._content_url_entry.get_text():
                        # no text -> no info/warning
                        self._progress_label.set_text("")

            # switch to the page allowing user to enter content URL and fetch
            # it
            self._main_notebook.set_current_page(GET_CONTENT_PAGE)
            self._content_url_entry.grab_focus()

            # nothing more to do here
            return
        else:
            # show control buttons
            really_show(self._control_buttons)

            self._main_notebook.set_current_page(SET_PARAMS_PAGE)

        self._active_profile = self._addon_data.profile_id

        self._update_ids_visibility()

        if self._using_ds:
            if self._addon_data.datastream_id:
                set_combo_selection(self._ds_combo,
                                    self._addon_data.datastream_id,
                                    unset_first=True)
            else:
                try:
                    default_ds = next(iter(self._ds_checklists.keys()))
                    set_combo_selection(self._ds_combo, default_ds,
                                        unset_first=True)
                except StopIteration:
                    # no data stream available
                    pass

                if self._addon_data.datastream_id and self._addon_data.xccdf_id:
                    set_combo_selection(self._xccdf_combo,
                                        self._addon_data.xccdf_id,
                                        unset_first=True)
        else:
            # no combobox changes --> need to update profiles store manually
            self._update_profiles_store()

        if self._addon_data.profile_id:
            set_treeview_selection(self._profiles_view,
                                   self._addon_data.profile_id)

        self._rule_data = self._addon_data.rule_data

        self._update_message_store()
Пример #25
0
 def _onDataAdded(self, container, widget):
     really_show(self._dataLabel)
Пример #26
0
    def _init_after_data_fetch(self, wait_for):
        """
        Waits for data fetching to be finished, extracts it (if needed),
        populates the stores and evaluates pre-installation fixes from the
        content and marks the spoke as ready in the end.

        :param wait_for: name of the thread to wait for (if any)
        :type wait_for: str or None

        """

        try:
            threadMgr.wait(wait_for)
        except data_fetch.DataFetchError:
            self._data_fetch_failed()
            with self._fetch_flag_lock:
                self._fetching = False
            return
        finally:
            # stop the spinner in any case
            fire_gtk_action(self._progress_spinner.stop)

        if self._addon_data.fingerprint:
            hash_obj = utils.get_hashing_algorithm(self._addon_data.fingerprint)
            digest = utils.get_file_fingerprint(\
                                       self._addon_data.raw_preinst_content_path,
                                       hash_obj)
            if digest != self._addon_data.fingerprint:
                msg = _("Integrity check failed")
                raise content_handling.ContentCheckError(msg)

        # RPM is an archive at this phase
        if self._addon_data.content_type in ("archive", "rpm"):
            # extract the content
            try:
                fpaths = common.extract_data(\
                                    self._addon_data.raw_preinst_content_path,
                                    common.INSTALLATION_CONTENT_DIR,
                                    [self._addon_data.xccdf_path])
            except common.ExtractionError as err:
                self._extraction_failed(err.message)
                # fetching done
                with self._fetch_flag_lock:
                    self._fetching = False
                return

            # and populate missing fields
            self._content_handling_cls, files = \
                                 content_handling.explore_content_files(fpaths)
            files = common.strip_content_dir(files)

            # pylint: disable-msg=E1103
            self._addon_data.xccdf_path = self._addon_data.xccdf_path or files.xccdf
            self._addon_data.cpe_path = self._addon_data.cpe_path or files.cpe
            self._addon_data.tailoring_path = (self._addon_data.tailoring_path or
                                               files.tailoring)
        elif self._addon_data.content_type == "datastream":
            self._content_handling_cls = content_handling.DataStreamHandler
        elif self._addon_data.content_type == "scap-security-guide":
            self._content_handling_cls = content_handling.BenchmarkHandler
        else:
            raise common.OSCAPaddonError("Unsupported content type")

        try:
            self._content_handler = self._content_handling_cls(\
                                      self._addon_data.preinst_content_path,
                                      self._addon_data.preinst_tailoring_path)
        except content_handling.ContentHandlingError:
            self._invalid_content()
            # fetching done
            with self._fetch_flag_lock:
                self._fetching = False
            return

        if self._using_ds:
            # populate the stores from items from the content
            self._ds_checklists = self._content_handler.get_data_streams_checklists()
            for dstream in self._ds_checklists.iterkeys():
                self._add_ds_id(dstream)
        else:
            # hide the labels and comboboxes for datastream-id and xccdf-id
            # selection
            fire_gtk_action(really_hide, self._ids_box)

        # refresh UI elements
        self.refresh()

        # try to switch to the chosen profile (if any)
        self._switch_profile()

        # initialize the self._addon_data.rule_data
        self._addon_data.rule_data = self._rule_data

        # update the message store with the messages
        self._update_message_store()

        # no more being unitialized
        self._unitialized_status = None
        self._ready = True

        # all initialized, we can now let user set parameters
        self._main_notebook.set_current_page(SET_PARAMS_PAGE)

        # and use control buttons
        really_show(self._control_buttons)

        # pylint: disable-msg=E1101
        hubQ.send_ready(self.__class__.__name__, True)
        hubQ.send_message(self.__class__.__name__, self.status)

        # fetching done
        with self._fetch_flag_lock:
            self._fetching = False