Passed
Pull Request — rhel8-branch (#148)
by Matěj
02:13
created

OSCAPSpoke._end_fetching()   A

Complexity

Conditions 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 2
dl 0
loc 3
rs 10
c 0
b 0
f 0
cc 1
nop 2
1
#
2
# Copyright (C) 2013  Red Hat, Inc.
3
#
4
# This copyrighted material is made available to anyone wishing to use,
5
# modify, copy, or redistribute it subject to the terms and conditions of
6
# the GNU General Public License v.2, or (at your option) any later version.
7
# This program is distributed in the hope that it will be useful, but WITHOUT
8
# ANY WARRANTY expressed or implied, including the implied warranties of
9
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General
10
# Public License for more details.  You should have received a copy of the
11
# GNU General Public License along with this program; if not, write to the
12
# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
13
# 02110-1301, USA.  Any Red Hat trademarks that are incorporated in the
14
# source code or documentation are not subject to the GNU General Public
15
# License and may only be used or replicated with the express permission of
16
# Red Hat, Inc.
17
#
18
# Red Hat Author(s): Vratislav Podzimek <[email protected]>
19
#
20
21
import threading
22
import logging
23
from functools import wraps
24
import pathlib
25
26
# the path to addons is in sys.path so we can import things
27
# from org_fedora_oscap
28
from org_fedora_oscap import common
29
from org_fedora_oscap import data_fetch
30
from org_fedora_oscap import rule_handling
31
from org_fedora_oscap import content_handling
32
from org_fedora_oscap import scap_content_handler
33
from org_fedora_oscap import utils
34
from org_fedora_oscap.common import dry_run_skip
35
from org_fedora_oscap.content_discovery import ContentBringer
36
from pyanaconda.threading import threadMgr, AnacondaThread
37
from pyanaconda.ui.gui.spokes import NormalSpoke
38
from pyanaconda.ui.communication import hubQ
39
from pyanaconda.ui.gui.utils import async_action_wait, really_hide, really_show
40
from pyanaconda.ui.gui.utils import set_treeview_selection, fire_gtk_action
41
from pyanaconda.ui.categories.system import SystemCategory
42
from pykickstart.errors import KickstartValueError
43
44
from pyanaconda.modules.common.constants.services import USERS
45
46
# pylint: disable-msg=E0611
47
from gi.repository import Gdk
48
49
log = logging.getLogger("anaconda")
50
51
_ = common._
52
N_ = common.N_
53
54
# export only the spoke, no helper functions, classes or constants
55
__all__ = ["OSCAPSpoke"]
56
57
# pages in the main notebook
58
SET_PARAMS_PAGE = 0
59
GET_CONTENT_PAGE = 1
60
61
62
class GtkActionList(object):
63
    """Class for scheduling Gtk actions to be all run at once."""
64
65
    def __init__(self):
66
        self._actions = []
67
68
    def add_action(self, func, *args):
69
        """Add Gtk action to be run later."""
70
71
        @async_action_wait
72
        def gtk_action():
73
            func(*args)
74
75
        self._actions.append(gtk_action)
76
77
    def fire(self):
78
        """Run all scheduled Gtk actions."""
79
80
        for action in self._actions:
81
            action()
82
83
        self._actions = []
84
85
86
# helper functions
87
def set_combo_selection(combo, item, unset_first=False):
88
    """
89
    Set selected item of the combobox.
90
91
    :return: True if successfully set, False otherwise
92
    :rtype: bool
93
94
    """
95
96
    if unset_first:
97
        combo.set_active_iter(None)
98
99
    model = combo.get_model()
100
    if not model:
101
        return False
102
103
    itr = model.get_iter_first()
104
    while itr:
105
        if model[itr][0] == item:
106
            combo.set_active_iter(itr)
107
            return True
108
109
        itr = model.iter_next(itr)
110
111
        return False
112
113
114
def get_combo_selection(combo):
115
    """
116
    Get the selected item of the combobox.
117
118
    :return: selected item or None
119
120
    """
121
122
    model = combo.get_model()
123
    itr = combo.get_active_iter()
124
    if not itr or not model:
125
        return None
126
127
    return model[itr][0]
128
129
130
def render_message_type(column, renderer, model, itr, user_data=None):
131
    # get message type from the first column
132
    value = model[itr][0]
133
134
    if value == common.MESSAGE_TYPE_FATAL:
135
        renderer.set_property("stock-id", "gtk-dialog-error")
136
    elif value == common.MESSAGE_TYPE_WARNING:
137
        renderer.set_property("stock-id", "gtk-dialog-warning")
138
    elif value == common.MESSAGE_TYPE_INFO:
139
        renderer.set_property("stock-id", "gtk-info")
140
    else:
141
        renderer.set_property("stock-id", "gtk-dialog-question")
142
143
144
def set_ready(func):
145
    @wraps(func)
146
    def decorated(self, *args, **kwargs):
147
        ret = func(self, *args, **kwargs)
148
149
        self._unitialized_status = None
150
        self._ready = True
151
        # pylint: disable-msg=E1101
152
        hubQ.send_ready(self.__class__.__name__, True)
153
        hubQ.send_message(self.__class__.__name__, self.status)
154
155
        return ret
156
157
    return decorated
158
159
160
class OSCAPSpoke(NormalSpoke):
161
    """
162
    Main class of the OSCAP addon spoke that will appear in the Security
163
    category on the Summary hub. It allows interactive choosing of the data
164
    stream, checklist and profile driving the evaluation and remediation of the
165
    available SCAP content in the installation process.
166
167
    :see: pyanaconda.ui.common.UIObject
168
    :see: pyanaconda.ui.common.Spoke
169
    :see: pyanaconda.ui.gui.GUIObject
170
171
    """
172
173
    # class attributes defined by API #
174
175
    # list all top-level objects from the .glade file that should be exposed
176
    # to the spoke or leave empty to extract everything
177
    builderObjects = ["OSCAPspokeWindow", "profilesStore", "changesStore",
178
                      "dsStore", "xccdfStore", "profilesStore",
179
                      ]
180
181
    # the name of the main window widget
182
    mainWidgetName = "OSCAPspokeWindow"
183
184
    # name of the .glade file in the same directory as this source
185
    uiFile = "oscap.glade"
186
187
    # id of the help content for this spoke
188
    help_id = "SecurityPolicySpoke"
189
190
    # domain of oscap-anaconda-addon translations
191
    translationDomain = "oscap-anaconda-addon"
192
193
    # category this spoke belongs to
194
    category = SystemCategory
195
196
    # spoke icon (will be displayed on the hub)
197
    # preferred are the -symbolic icons as these are used in Anaconda's spokes
198
    icon = "changes-prevent-symbolic"
199
200
    # title of the spoke (will be displayed on the hub)
201
    title = N_("_Security Policy")
202
    # The string "SECURITY POLICY" in oscap.glade is meant to be uppercase,
203
    # as it is displayed inside the spoke as the spoke label,
204
    # and spoke labels are all uppercase by a convention.
205
206
    # methods defined by API and helper methods #
207
    def __init__(self, data, storage, payload):
208
        """
209
        :see: pyanaconda.ui.common.Spoke.__init__
210
        :param data: data object passed to every spoke to load/store data
211
                     from/to it
212
        :type data: pykickstart.base.BaseHandler
213
        :param storage: object storing storage-related information
214
                        (disks, partitioning, bootloader, etc.)
215
        :type storage: blivet.Blivet
216
        :param payload: object storing packaging-related information
217
        :type payload: pyanaconda.packaging.Payload
218
219
        """
220
221
        NormalSpoke.__init__(self, data, storage, payload)
222
        self._addon_data = self.data.addons.org_fedora_oscap
223
        # workaround for https://bugzilla.redhat.com/show_bug.cgi?id=1673071
224
        self.title = _(self.title)
225
        self._storage = storage
226
        self._ready = False
227
228
        # the first status provided
229
        self._unitialized_status = _("Not ready")
230
231
        self._ds_checklists = None
232
233
        # used for changing profiles, stored as self._addon_data.rule_data when
234
        # leaving the spoke
235
        self._rule_data = None
236
237
        # used for storing previously set root password if we need to remove it
238
        # due to the chosen policy (so that we can put it back in case of
239
        # revert)
240
        self.__old_root_pw = None
241
242
        # used to check if the profile was changed or not
243
        self._active_profile = None
244
245
        # prevent multiple simultaneous data fetches
246
        self._fetching = False
247
        self._fetch_flag_lock = threading.Lock()
248
249
        self._error = None
250
251
        # wait for all Anaconda spokes to initialiuze
252
        self._anaconda_spokes_initialized = threading.Event()
253
        self.initialization_controller.init_done.connect(self._all_anaconda_spokes_initialized)
254
255
        self.content_bringer = ContentBringer(self._addon_data)
256
257
    def _all_anaconda_spokes_initialized(self):
258
        log.debug("OSCAP addon: Anaconda init_done signal triggered")
259
        self._anaconda_spokes_initialized.set()
260
261
    def initialize(self):
262
        """
263
        The initialize method that is called after the instance is created.
264
        The difference between __init__ and this method is that this may take
265
        a long time and thus could be called in a separated thread.
266
267
        :see: pyanaconda.ui.common.UIObject.initialize
268
269
        """
270
271
        NormalSpoke.initialize(self)
272
        column = self.builder.get_object("messageTypeColumn")
273
        renderer = self.builder.get_object("messageTypeRenderer")
274
        column.set_cell_data_func(renderer, render_message_type)
275
276
        # the main notebook containing two pages -- for settings parameters and
277
        # for entering content URL
278
        self._main_notebook = self.builder.get_object("mainNotebook")
279
280
        # the store that holds the messages that come from the rules evaluation
281
        self._message_store = self.builder.get_object("changesStore")
282
283
        # stores with data streams, checklists and profiles
284
        self._ds_store = self.builder.get_object("dsStore")
285
        self._xccdf_store = self.builder.get_object("xccdfStore")
286
        self._profiles_store = self.builder.get_object("profilesStore")
287
288
        # comboboxes for data streams and checklists
289
        self._ids_box = self.builder.get_object("idsBox")
290
        self._ds_combo = self.builder.get_object("dsCombo")
291
        self._xccdf_combo = self.builder.get_object("xccdfCombo")
292
293
        # profiles view and selection
294
        self._profiles_view = self.builder.get_object("profilesView")
295
        self._profiles_selection = self.builder.get_object("profilesSelection")
296
        selected_column = self.builder.get_object("selectedColumn")
297
        selected_renderer = self.builder.get_object("selectedRenderer")
298
        selected_column.set_cell_data_func(selected_renderer,
299
                                           self._render_selected)
300
301
        # button for switching profiles
302
        self._choose_button = self.builder.get_object("chooseProfileButton")
303
304
        # toggle switching the dry-run mode
305
        self._dry_run_switch = self.builder.get_object("dryRunSwitch")
306
307
        # control buttons
308
        self._control_buttons = self.builder.get_object("controlButtons")
309
310
        # content URL entering, content fetching, ...
311
        self._no_content_label = self.builder.get_object("noContentLabel")
312
        self._content_url_entry = self.builder.get_object("urlEntry")
313
        self._fetch_button = self.builder.get_object("fetchButton")
314
        self._progress_box = self.builder.get_object("progressBox")
315
        self._progress_spinner = self.builder.get_object("progressSpinner")
316
        self._progress_label = self.builder.get_object("progressLabel")
317
        self._ssg_button = self.builder.get_object("ssgButton")
318
319
        # if no content was specified and SSG is available, use it
320
        if not self._addon_data.content_type and common.ssg_available():
321
            self._addon_data.content_type = "scap-security-guide"
322
            self._addon_data.content_path = common.SSG_DIR + common.SSG_CONTENT
323
324
        if not self._addon_data.content_defined:
325
            # nothing more to be done now, the spoke is ready
326
            self._ready = True
327
328
            # no more being unitialized
329
            self._unitialized_status = None
330
331
            # user is going to enter the content URL
332
            self._content_url_entry.grab_focus()
333
334
            # pylint: disable-msg=E1101
335
            hubQ.send_ready(self.__class__.__name__, True)
336
        else:
337
            # else fetch data
338
            self._fetch_data_and_initialize()
339
340
    def _handle_error(self, exception):
341
        log.error(str(exception))
342
        if isinstance(exception, KickstartValueError):
343
            self._invalid_url()
344
        elif isinstance(exception, common.OSCAPaddonNetworkError):
345
            self._network_problem()
346
        elif isinstance(exception, data_fetch.DataFetchError):
347
            self._data_fetch_failed()
348
        elif isinstance(exception, common.ExtractionError):
349
            self._extraction_failed(str(exception))
350
        elif isinstance(exception, content_handling.ContentHandlingError):
351
            self._invalid_content()
352
        elif isinstance(exception, content_handling.ContentCheckError):
353
            self._integrity_check_failed()
354
        else:
355
            self._general_content_problem()
356
357
    def _end_fetching(self, fetched_content):
358
        # stop the spinner in any case
359
        fire_gtk_action(self._progress_spinner.stop)
360
361
    def _render_selected(self, column, renderer, model, itr, user_data=None):
362
        if model[itr][2]:
363
            renderer.set_property("stock-id", "gtk-apply")
364
        else:
365
            renderer.set_property("stock-id", None)
366
367
    def _fetch_data_and_initialize(self):
368
        """Fetch data from a specified URL and initialize everything."""
369
370
        with self._fetch_flag_lock:
371
            if self._fetching:
372
                # prevent multiple fetches running simultaneously
373
                return
374
            self._fetching = True
375
376
        thread_name = None
377
        if self._addon_data.content_url and self._addon_data.content_type != "scap-security-guide":
378
            thread_name = self.content_bringer.fetch_content(
379
                self._handle_error, self._addon_data.certificates)
380
381
        # pylint: disable-msg=E1101
382
        hubQ.send_message(self.__class__.__name__,
383
                          _("Fetching content data"))
384
        # pylint: disable-msg=E1101
385
        hubQ.send_not_ready(self.__class__.__name__)
386
        threadMgr.add(AnacondaThread(name="OSCAPguiWaitForDataFetchThread",
387
                                     target=self._init_after_data_fetch,
388
                                     args=(thread_name,)))
389
390
    @set_ready
391
    def _init_after_data_fetch(self, wait_for):
392
        """
393
        Waits for data fetching to be finished, extracts it (if needed),
394
        populates the stores and evaluates pre-installation fixes from the
395
        content and marks the spoke as ready in the end.
396
397
        :param wait_for: name of the thread to wait for (if any)
398
        :type wait_for: str or None
399
400
        """
401
        def update_progress_label(msg):
402
            fire_gtk_action(self._progress_label.set_text(msg))
403
404
        content_path = None
405
        actually_fetched_content = wait_for is not None
406
407
        if actually_fetched_content:
408
            content_path = self._addon_data.raw_preinst_content_path
409
410
        content = self.content_bringer.finish_content_fetch(
411
            wait_for, self._addon_data.fingerprint, update_progress_label,
412
            content_path, self._handle_error)
413
        if not content:
414
            with self._fetch_flag_lock:
415
                self._fetching = False
416
417
            return
418
419
        try:
420
            if actually_fetched_content:
421
                self.content_bringer.use_downloaded_content(content)
422
            log.info(f"Opening SCAP content at {self._addon_data.preinst_content_path} with tailoring {self._addon_data.preinst_tailoring_path}")
423
            self._content_handler = scap_content_handler.SCAPContentHandler(
424
                self._addon_data.preinst_content_path,
425
                self._addon_data.preinst_tailoring_path)
426
        except Exception as e:
427
            log.error(str(e))
428
            self._invalid_content()
429
            # fetching done
430
            with self._fetch_flag_lock:
431
                self._fetching = False
432
433
            return
434
435
        self._ds_checklists = self._content_handler.get_data_streams_checklists()
436
        if self._using_ds:
437
            # populate the stores from items from the content
438
            add_ds_ids = GtkActionList()
439
            add_ds_ids.add_action(self._ds_store.clear)
440
            for dstream in self._ds_checklists.keys():
441
                add_ds_ids.add_action(self._add_ds_id, dstream)
442
            add_ds_ids.fire()
443
444
        self._update_ids_visibility()
445
446
        # refresh UI elements
447
        self.refresh()
448
449
        # let all initialization and configuration happen before we evaluate
450
        # the setup
451
        if not self._anaconda_spokes_initialized.is_set():
452
            # only wait (and log the messages) if the event is not set yet
453
            log.debug("OSCAP addon: waiting for all Anaconda spokes to be initialized")
454
            self._anaconda_spokes_initialized.wait()
455
            log.debug("OSCAP addon: all Anaconda spokes have been initialized - continuing")
456
457
        # try to switch to the chosen profile (if any)
458
        selected = self._switch_profile()
459
460
        if self._addon_data.profile_id and not selected:
461
            # profile ID given, but it was impossible to select it -> invalid
462
            # profile ID given
463
            self._invalid_profile_id()
464
            return
465
466
        # initialize the self._addon_data.rule_data
467
        self._addon_data.rule_data = self._rule_data
468
469
        # update the message store with the messages
470
        self._update_message_store()
471
472
        # all initialized, we can now let user set parameters
473
        fire_gtk_action(self._main_notebook.set_current_page, SET_PARAMS_PAGE)
474
475
        # and use control buttons
476
        fire_gtk_action(really_show, self._control_buttons)
477
478
        # fetching done
479
        with self._fetch_flag_lock:
480
            self._fetching = False
481
482
        # no error
483
        self._set_error(None)
484
485
    @property
486
    def _using_ds(self):
487
        return self._ds_checklists is not None
488
489
    @property
490
    def _current_ds_id(self):
491
        return get_combo_selection(self._ds_combo)
492
493
    @property
494
    def _current_xccdf_id(self):
495
        return get_combo_selection(self._xccdf_combo)
496
497
    @property
498
    def _current_profile_id(self):
499
        store, itr = self._profiles_selection.get_selected()
500
        if not store or not itr:
501
            return None
502
        else:
503
            return store[itr][0]
504
505
    def _add_ds_id(self, ds_id):
506
        """
507
        Add data stream ID to the data streams store.
508
509
        :param ds_id: data stream ID
510
        :type ds_id: str
511
512
        """
513
514
        self._ds_store.append([ds_id])
515
516
    @async_action_wait
517
    def _update_ids_visibility(self):
518
        """
519
        Updates visibility of the combo boxes that are used to select the DS
520
        and XCCDF IDs.
521
522
        """
523
524
        if self._using_ds:
525
            # only show the combo boxes if there are multiple data streams or
526
            # multiple xccdfs (IOW if there's something to choose from)
527
            ds_ids = list(self._ds_checklists.keys())
528
            if len(ds_ids) > 1 or len(self._ds_checklists[ds_ids[0]]) > 1:
529
                really_show(self._ids_box)
530
                return
531
532
        # not showing, hide instead
533
        really_hide(self._ids_box)
534
535
    @async_action_wait
536
    def _update_xccdfs_store(self):
537
        """
538
        Clears and repopulates the store with XCCDF IDs from the currently
539
        selected data stream.
540
541
        """
542
543
        if self._ds_checklists is None:
544
            # not initialized, cannot do anything
545
            return
546
547
        self._xccdf_store.clear()
548
        for xccdf_id in self._ds_checklists[self._current_ds_id]:
549
            self._xccdf_store.append([xccdf_id])
550
551
    @async_action_wait
552
    def _update_profiles_store(self):
553
        """
554
        Clears and repopulates the store with profiles from the currently
555
        selected data stream and checklist.
556
557
        """
558
559
        if self._content_handler is None:
560
            # not initialized, cannot do anything
561
            return
562
563
        if self._using_ds and (
564
                self._current_xccdf_id is None or self._current_ds_id is None):
565
            # not initialized, cannot do anything
566
            return
567
568
        self._profiles_store.clear()
569
        try:
570
            profiles = self._content_handler.get_profiles()
571
        except scap_content_handler.SCAPContentHandlerError as e:
572
            log.warning(str(e))
573
            self._invalid_content()
574
575
        for profile in profiles:
576
            profile_markup = '<span weight="bold">%s</span>\n%s' \
577
                                % (profile.title, profile.description)
578
            self._profiles_store.append([profile.id,
579
                                         profile_markup,
580
                                         profile.id == self._active_profile])
581
582
    def _add_message(self, message):
583
        """
584
        Add message to the store.
585
586
        :param message: message to be added
587
        :type message: org_fedora_oscap.common.RuleMessage
588
589
        """
590
591
        self._message_store.append([message.type, message.text])
592
593
    @dry_run_skip
594
    @async_action_wait
595
    def _update_message_store(self, report_only=False):
596
        """
597
        Updates the message store with messages from rule evaluation.
598
599
        :param report_only: wheter to do changes in configuration or just
600
                            report
601
        :type report_only: bool
602
603
        """
604
605
        self._message_store.clear()
606
607
        if not self._rule_data:
608
            # RuleData instance not initialized, cannot do anything
609
            return
610
611
        messages = self._rule_data.eval_rules(self.data, self._storage,
612
                                              report_only)
613
        if not messages:
614
            # no messages from the rules, add a message informing about that
615
            if not self._active_profile:
616
                # because of no profile
617
                message = common.RuleMessage(self.__class__,
618
                                             common.MESSAGE_TYPE_INFO,
619
                                             _("No profile selected"))
620
            else:
621
                # because of no pre-inst rules
622
                message = common.RuleMessage(self.__class__,
623
                                             common.MESSAGE_TYPE_INFO,
624
                                             _("No rules for the pre-installation phase"))
625
            self._add_message(message)
626
627
            # nothing more to be done
628
            return
629
630
        self._resolve_rootpw_issues(messages, report_only)
631
        for msg in messages:
632
            self._add_message(msg)
633
634
    def _resolve_rootpw_issues(self, messages, report_only):
635
        """Mitigate root password issues (which are not fatal in GUI)"""
636
        fatal_rootpw_msgs = [
637
            msg for msg in messages
638
            if msg.origin == rule_handling.PasswdRules and msg.type == common.MESSAGE_TYPE_FATAL]
639
640
        if fatal_rootpw_msgs:
641
            for msg in fatal_rootpw_msgs:
642
                # cannot just change the message type because it is a namedtuple
643
                messages.remove(msg)
644
645
                msg = common.RuleMessage(
646
                    self.__class__, common.MESSAGE_TYPE_WARNING, msg.text)
647
                messages.append(msg)
648
649
            if not report_only:
650
                users_proxy = USERS.get_proxy()
651
652
                self.__old_root_pw = users_proxy.RootPassword
653
                self.data.rootpw.password = None
654
                self.__old_root_pw_seen = users_proxy.IsRootpwKickstarted
655
                self.data.rootpw.seen = False
656
657
    def _revert_rootpw_changes(self):
658
        if self.__old_root_pw is not None:
659
            users_proxy = USERS.get_proxy()
660
661
            users_proxy.SetRootPassword(self.__old_root_pw)
662
            self.__old_root_pw = None
663
664
            users_proxy.SetRootpwKickstarted(self.__old_root_pw_seen)
665
            self.__old_root_pw_seen = None
666
667
    @async_action_wait
668
    def _unselect_profile(self, profile_id):
669
        """Unselects the given profile."""
670
671
        if not profile_id:
672
            # no profile specified, nothing to do
673
            return
674
675
        itr = self._profiles_store.get_iter_first()
676
        while itr:
677
            if self._profiles_store[itr][0] == profile_id:
678
                self._profiles_store.set_value(itr, 2, False)
679
            itr = self._profiles_store.iter_next(itr)
680
681
        if self._rule_data:
682
            # revert changes and clear rule_data (no longer valid)
683
            self._rule_data.revert_changes(self.data, self._storage)
684
            self._revert_rootpw_changes()
685
            self._rule_data = None
686
687
        self._active_profile = None
688
689
    @async_action_wait
690
    def _select_profile(self, profile_id):
691
        """Selects the given profile."""
692
693
        if not profile_id:
694
            # no profile specified, nothing to do
695
            return False
696
697
        ds = None
698
        xccdf = None
699
        if self._using_ds:
700
            ds = self._current_ds_id
701
            xccdf = self._current_xccdf_id
702
703
            if not all((ds, xccdf, profile_id)):
704
                # something is not set -> do nothing
705
                return False
706
707
        # get pre-install fix rules from the content
708
        try:
709
            self._rule_data = rule_handling.get_rule_data_from_content(
710
                profile_id, self._addon_data.preinst_content_path,
711
                ds, xccdf, self._addon_data.preinst_tailoring_path)
712
        except common.OSCAPaddonError as exc:
713
            log.error(
714
                "Failed to get rules for the profile '{}': {}"
715
                .format(profile_id, str(exc)))
716
            self._set_error(
717
                "Failed to get rules for the profile '{}'"
718
                .format(profile_id))
719
            return False
720
721
        itr = self._profiles_store.get_iter_first()
722
        while itr:
723
            if self._profiles_store[itr][0] == profile_id:
724
                self._profiles_store.set_value(itr, 2, True)
725
            itr = self._profiles_store.iter_next(itr)
726
727
        # remember the active profile
728
        self._active_profile = profile_id
729
730
        return True
731
732
    @async_action_wait
733
    @dry_run_skip
734
    def _switch_profile(self):
735
        """Switches to a current selected profile.
736
737
        :returns: whether some profile was selected or not
738
739
        """
740
741
        self._set_error(None)
742
        profile = self._current_profile_id
743
        if not profile:
744
            return False
745
746
        self._unselect_profile(self._active_profile)
747
        ret = self._select_profile(profile)
748
749
        # update messages according to the newly chosen profile
750
        self._update_message_store()
751
752
        return ret
753
754
    @set_ready
755
    def _set_error(self, msg):
756
        """Set or clear error message"""
757
        if msg:
758
            self._error = msg
759
            self.clear_info()
760
            self.set_error(msg)
761
        else:
762
            self._error = None
763
            self.clear_info()
764
765
    @async_action_wait
766
    def _general_content_problem(self):
767
        msg = _("There was an unexpected problem with the supplied content.")
768
        self._progress_label.set_markup("<b>%s</b>" % msg)
769
        self._wrong_content(msg)
770
771
    @async_action_wait
772
    def _invalid_content(self):
773
        """Callback for informing user about provided content invalidity."""
774
775
        msg = _("Invalid content provided. Enter a different URL, please.")
776
        self._progress_label.set_markup("<b>%s</b>" % msg)
777
        self._wrong_content(msg)
778
779
    @async_action_wait
780
    def _invalid_url(self):
781
        """Callback for informing user about provided URL invalidity."""
782
783
        msg = _("Invalid or unsupported content URL, please enter a different one.")
784
        self._progress_label.set_markup("<b>%s</b>" % msg)
785
        self._wrong_content(msg)
786
787
    @async_action_wait
788
    def _data_fetch_failed(self):
789
        """Adapts the UI if fetching data from entered URL failed"""
790
791
        msg = _("Failed to fetch content. Enter a different URL, please.")
792
        self._progress_label.set_markup("<b>%s</b>" % msg)
793
        self._wrong_content(msg)
794
795
    @async_action_wait
796
    def _network_problem(self):
797
        """Adapts the UI if network error was encountered during data fetch"""
798
799
        msg = _("Network error encountered when fetching data."
800
                " Please check that network is setup and working.")
801
        self._progress_label.set_markup("<b>%s</b>" % msg)
802
        self._wrong_content(msg)
803
804
    @async_action_wait
805
    def _integrity_check_failed(self):
806
        """Adapts the UI if integrity check fails"""
807
808
        msg = _("The integrity check of the content failed. Cannot use the content.")
809
        self._progress_label.set_markup("<b>%s</b>" % msg)
810
        self._wrong_content(msg)
811
812
    @async_action_wait
813
    def _extraction_failed(self, err_msg):
814
        """Adapts the UI if extracting data from entered URL failed"""
815
816
        msg = _("Failed to extract content (%s). Enter a different URL, "
817
                "please.") % err_msg
818
        self._progress_label.set_markup("<b>%s</b>" % msg)
819
        self._wrong_content(msg)
820
821
    @async_action_wait
822
    def _wrong_content(self, msg):
823
        self._addon_data.clear_all()
824
        really_hide(self._progress_spinner)
825
        self._fetch_button.set_sensitive(True)
826
        self._content_url_entry.set_sensitive(True)
827
        self._content_url_entry.grab_focus()
828
        self._content_url_entry.select_region(0, -1)
829
        self._set_error(msg)
830
831
    @async_action_wait
832
    def _invalid_profile_id(self):
833
        msg = _("Profile with ID '%s' not defined in the content. Select a different profile, please") % self._addon_data.profile_id
834
        self._set_error(msg)
835
        self._addon_data.profile_id = None
836
837
    @async_action_wait
838
    def _switch_dry_run(self, dry_run):
839
        self._choose_button.set_sensitive(not dry_run)
840
841
        if dry_run:
842
            # no profile can be selected in the dry-run mode
843
            self._unselect_profile(self._active_profile)
844
845
            # no messages in the dry-run mode
846
            self._message_store.clear()
847
            message = common.RuleMessage(self.__class__,
848
                                         common.MESSAGE_TYPE_INFO,
849
                                         _("Not applying security policy"))
850
            self._add_message(message)
851
852
            self._set_error(None)
853
        else:
854
            # mark the active profile as selected
855
            self._select_profile(self._active_profile)
856
            self._update_message_store()
857
858
    @async_action_wait
859
    def refresh(self):
860
        """
861
        The refresh method that is called every time the spoke is displayed.
862
        It should update the UI elements according to the contents of
863
        self.data.
864
865
        :see: pyanaconda.ui.common.UIObject.refresh
866
867
        """
868
869
        if not self._addon_data.content_defined:
870
            # hide the control buttons
871
            really_hide(self._control_buttons)
872
873
            # provide SSG if available
874
            if common.ssg_available():
875
                # show the SSG button and tweak the rest of the line
876
                # (the label)
877
                really_show(self._ssg_button)
878
                # TRANSLATORS: the other choice if SCAP Security Guide is also
879
                # available
880
                tip = _(" or enter data stream content or archive URL below:")
881
            else:
882
                # hide the SSG button
883
                really_hide(self._ssg_button)
884
                tip = _("No content found. Please enter data stream content or "
885
                        "archive URL below:")
886
887
            self._no_content_label.set_text(tip)
888
889
            # hide the progress box, no progress now
890
            with self._fetch_flag_lock:
891
                if not self._fetching:
892
                    really_hide(self._progress_box)
893
894
                    self._content_url_entry.set_sensitive(True)
895
                    self._fetch_button.set_sensitive(True)
896
897
                    if not self._content_url_entry.get_text():
898
                        # no text -> no info/warning
899
                        self._progress_label.set_text("")
900
901
            # switch to the page allowing user to enter content URL and fetch
902
            # it
903
            self._main_notebook.set_current_page(GET_CONTENT_PAGE)
904
            self._content_url_entry.grab_focus()
905
906
            # nothing more to do here
907
            return
908
        else:
909
            # show control buttons
910
            really_show(self._control_buttons)
911
912
            self._main_notebook.set_current_page(SET_PARAMS_PAGE)
913
914
        self._active_profile = self._addon_data.profile_id
915
916
        self._update_ids_visibility()
917
918
        if self._using_ds:
919
            if self._addon_data.datastream_id:
920
                set_combo_selection(self._ds_combo,
921
                                    self._addon_data.datastream_id,
922
                                    unset_first=True)
923
            else:
924
                try:
925
                    default_ds = next(iter(self._ds_checklists.keys()))
926
                    set_combo_selection(self._ds_combo, default_ds,
927
                                        unset_first=True)
928
                except StopIteration:
929
                    # no data stream available
930
                    pass
931
932
                if self._addon_data.datastream_id and self._addon_data.xccdf_id:
933
                    set_combo_selection(self._xccdf_combo,
934
                                        self._addon_data.xccdf_id,
935
                                        unset_first=True)
936
        else:
937
            # no combobox changes --> need to update profiles store manually
938
            self._update_profiles_store()
939
940
        if self._addon_data.profile_id:
941
            set_treeview_selection(self._profiles_view,
942
                                   self._addon_data.profile_id)
943
944
        self._rule_data = self._addon_data.rule_data
945
946
        self._update_message_store()
947
948
    def apply(self):
949
        """
950
        The apply method that is called when the spoke is left. It should
951
        update the contents of self.data with values set in the GUI elements.
952
953
        """
954
955
        if not self._addon_data.content_defined or not self._active_profile:
956
            # no errors for no content or no profile
957
            self._set_error(None)
958
959
        # store currently selected values to the addon data attributes
960
        if self._using_ds:
961
            self._addon_data.datastream_id = self._current_ds_id
962
            self._addon_data.xccdf_id = self._current_xccdf_id
963
964
        self._addon_data.profile_id = self._active_profile
965
966
        self._addon_data.rule_data = self._rule_data
967
968
        self._addon_data.dry_run = not self._dry_run_switch.get_active()
969
970
    def execute(self):
971
        """
972
        The excecute method that is called when the spoke is left. It is
973
        supposed to do all changes to the runtime environment according to
974
        the values set in the GUI elements.
975
976
        """
977
978
        # nothing to do here
979
        pass
980
981
    @property
982
    def ready(self):
983
        """
984
        The ready property that tells whether the spoke is ready (can be
985
        visited) or not.
986
987
        :rtype: bool
988
989
        """
990
991
        return self._ready
992
993
    @property
994
    def completed(self):
995
        """
996
        The completed property that tells whether all mandatory items on the
997
        spoke are set, or not. The spoke will be marked on the hub as completed
998
        or uncompleted acording to the returned value.
999
1000
        :rtype: bool
1001
1002
        """
1003
1004
        # no error message in the store
1005
        return not self._error and all(row[0] != common.MESSAGE_TYPE_FATAL
1006
                                       for row in self._message_store)
1007
1008
    @property
1009
    @async_action_wait
1010
    def status(self):
1011
        """
1012
        The status property that is a brief string describing the state of the
1013
        spoke. It should describe whether all values are set and if possible
1014
        also the values themselves. The returned value will appear on the hub
1015
        below the spoke's title.
1016
1017
        :rtype: str
1018
1019
        """
1020
1021
        if self._error:
1022
            return _("Error fetching and loading content")
1023
1024
        if self._unitialized_status:
1025
            # not initialized
1026
            return self._unitialized_status
1027
1028
        if not self._addon_data.content_defined:
1029
            return _("No content found")
1030
1031
        if not self._active_profile:
1032
            return _("No profile selected")
1033
1034
        # update message store, something may changed from the last update
1035
        self._update_message_store(report_only=True)
1036
1037
        warning_found = False
1038
        for row in self._message_store:
1039
            if row[0] == common.MESSAGE_TYPE_FATAL:
1040
                return _("Misconfiguration detected")
1041
            elif row[0] == common.MESSAGE_TYPE_WARNING:
1042
                warning_found = True
1043
1044
        # TODO: at least the last two status messages need a better wording
1045
        if warning_found:
1046
            return _("Warnings appeared")
1047
1048
        return _("Everything okay")
1049
1050
    def on_ds_combo_changed(self, *args):
1051
        """Handler for the datastream ID change."""
1052
1053
        ds_id = self._current_ds_id
1054
        if not ds_id:
1055
            return
1056
1057
        self._update_xccdfs_store()
1058
        first_checklist = self._ds_checklists[ds_id][0]
1059
1060
        set_combo_selection(self._xccdf_combo, first_checklist)
1061
1062
    def on_xccdf_combo_changed(self, *args):
1063
        """Handler for the XCCDF ID change."""
1064
        self._content_handler.select_checklist(
1065
            self._current_ds_id, self._current_xccdf_id)
1066
1067
        # may take a while
1068
        self._update_profiles_store()
1069
1070
    @dry_run_skip
1071
    def on_profiles_selection_changed(self, *args):
1072
        """Handler for the profile selection change."""
1073
1074
        cur_profile = self._current_profile_id
1075
        if cur_profile:
1076
            if cur_profile != self._active_profile:
1077
                # new profile selected, make the selection button sensitive
1078
                self._choose_button.set_sensitive(True)
1079
            else:
1080
                # current active profile selected
1081
                self._choose_button.set_sensitive(False)
1082
1083
    @dry_run_skip
1084
    def on_profile_clicked(self, widget, event, *args):
1085
        """Handler for the profile being clicked on."""
1086
1087
        # if a profile is double-clicked, we should switch to it
1088
        if event.type == Gdk.EventType._2BUTTON_PRESS:
1089
            self._switch_profile()
1090
1091
            # active profile selected
1092
            self._choose_button.set_sensitive(False)
1093
1094
        # let the other actions hooked to the click happen as well
1095
        return False
1096
1097
    def on_profile_chosen(self, *args):
1098
        """
1099
        Handler for the profile being chosen
1100
        (e.g. "Select profile" button hit).
1101
1102
        """
1103
1104
        # switch profile
1105
        self._switch_profile()
1106
1107
        # active profile selected
1108
        self._choose_button.set_sensitive(False)
1109
1110
    def on_fetch_button_clicked(self, *args):
1111
        """Handler for the Fetch button"""
1112
1113
        with self._fetch_flag_lock:
1114
            if self._fetching:
1115
                # some other fetching/pre-processing running, give up
1116
                log.warn("Clicked the fetch button, although the GUI is in the fetching mode.")
1117
                return
1118
1119
        # prevent user from changing the URL in the meantime
1120
        self._content_url_entry.set_sensitive(False)
1121
        self._fetch_button.set_sensitive(False)
1122
        url = self._content_url_entry.get_text()
1123
        really_show(self._progress_box)
1124
        really_show(self._progress_spinner)
1125
1126
        if not data_fetch.can_fetch_from(url):
1127
            msg = _("Invalid or unsupported URL")
1128
            # cannot start fetching
1129
            self._progress_label.set_markup("<b>%s</b>" % msg)
1130
            self._wrong_content(msg)
1131
            return
1132
1133
        self._progress_label.set_text(_("Fetching content..."))
1134
        self._progress_spinner.start()
1135
        self._addon_data.content_url = url
1136
        if url.endswith(".rpm"):
1137
            self._addon_data.content_type = "rpm"
1138
        elif any(url.endswith(arch_type) for arch_type in common.SUPPORTED_ARCHIVES):
1139
            self._addon_data.content_type = "archive"
1140
        else:
1141
            self._addon_data.content_type = "datastream"
1142
1143
        self._fetch_data_and_initialize()
1144
1145
    def on_dry_run_toggled(self, switch, *args):
1146
        dry_run = not switch.get_active()
1147
        self._addon_data.dry_run = dry_run
1148
        self._switch_dry_run(dry_run)
1149
1150
    def on_change_content_clicked(self, *args):
1151
        self._unselect_profile(self._active_profile)
1152
        self._addon_data.clear_all()
1153
        self.refresh()
1154
1155
    def on_use_ssg_clicked(self, *args):
1156
        self.content_bringer.use_system_content()
1157
        self._fetch_data_and_initialize()
1158