Passed
Pull Request — rhel8-branch (#148)
by Matěj
01:22
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
423
            msg = f"Opening SCAP content at {self._addon_data.preinst_content_path}"
424
            if self._addon_data.tailoring_path:
425
                msg += f" with tailoring {self._addon_data.preinst_tailoring_path}"
426
            else:
427
                msg += " without considering tailoring"
428
            log.info(msg)
429
430
            self._content_handler = scap_content_handler.SCAPContentHandler(
431
                self._addon_data.preinst_content_path,
432
                self._addon_data.preinst_tailoring_path)
433
        except Exception as e:
434
            log.error(str(e))
435
            self._invalid_content()
436
            # fetching done
437
            with self._fetch_flag_lock:
438
                self._fetching = False
439
440
            return
441
442
        self._ds_checklists = self._content_handler.get_data_streams_checklists()
443
        if self._using_ds:
444
            # populate the stores from items from the content
445
            add_ds_ids = GtkActionList()
446
            add_ds_ids.add_action(self._ds_store.clear)
447
            for dstream in self._ds_checklists.keys():
448
                add_ds_ids.add_action(self._add_ds_id, dstream)
449
            add_ds_ids.fire()
450
451
        self._update_ids_visibility()
452
453
        # refresh UI elements
454
        self.refresh()
455
456
        # let all initialization and configuration happen before we evaluate
457
        # the setup
458
        if not self._anaconda_spokes_initialized.is_set():
459
            # only wait (and log the messages) if the event is not set yet
460
            log.debug("OSCAP addon: waiting for all Anaconda spokes to be initialized")
461
            self._anaconda_spokes_initialized.wait()
462
            log.debug("OSCAP addon: all Anaconda spokes have been initialized - continuing")
463
464
        # try to switch to the chosen profile (if any)
465
        selected = self._switch_profile()
466
467
        if self._addon_data.profile_id and not selected:
468
            # profile ID given, but it was impossible to select it -> invalid
469
            # profile ID given
470
            self._invalid_profile_id()
471
            return
472
473
        # initialize the self._addon_data.rule_data
474
        self._addon_data.rule_data = self._rule_data
475
476
        # update the message store with the messages
477
        self._update_message_store()
478
479
        # all initialized, we can now let user set parameters
480
        fire_gtk_action(self._main_notebook.set_current_page, SET_PARAMS_PAGE)
481
482
        # and use control buttons
483
        fire_gtk_action(really_show, self._control_buttons)
484
485
        # fetching done
486
        with self._fetch_flag_lock:
487
            self._fetching = False
488
489
        # no error
490
        self._set_error(None)
491
492
    @property
493
    def _using_ds(self):
494
        return self._ds_checklists is not None
495
496
    @property
497
    def _current_ds_id(self):
498
        return get_combo_selection(self._ds_combo)
499
500
    @property
501
    def _current_xccdf_id(self):
502
        return get_combo_selection(self._xccdf_combo)
503
504
    @property
505
    def _current_profile_id(self):
506
        store, itr = self._profiles_selection.get_selected()
507
        if not store or not itr:
508
            return None
509
        else:
510
            return store[itr][0]
511
512
    def _add_ds_id(self, ds_id):
513
        """
514
        Add data stream ID to the data streams store.
515
516
        :param ds_id: data stream ID
517
        :type ds_id: str
518
519
        """
520
521
        self._ds_store.append([ds_id])
522
523
    @async_action_wait
524
    def _update_ids_visibility(self):
525
        """
526
        Updates visibility of the combo boxes that are used to select the DS
527
        and XCCDF IDs.
528
529
        """
530
531
        if self._using_ds:
532
            # only show the combo boxes if there are multiple data streams or
533
            # multiple xccdfs (IOW if there's something to choose from)
534
            ds_ids = list(self._ds_checklists.keys())
535
            if len(ds_ids) > 1 or len(self._ds_checklists[ds_ids[0]]) > 1:
536
                really_show(self._ids_box)
537
                return
538
539
        # not showing, hide instead
540
        really_hide(self._ids_box)
541
542
    @async_action_wait
543
    def _update_xccdfs_store(self):
544
        """
545
        Clears and repopulates the store with XCCDF IDs from the currently
546
        selected data stream.
547
548
        """
549
550
        if self._ds_checklists is None:
551
            # not initialized, cannot do anything
552
            return
553
554
        self._xccdf_store.clear()
555
        for xccdf_id in self._ds_checklists[self._current_ds_id]:
556
            self._xccdf_store.append([xccdf_id])
557
558
    @async_action_wait
559
    def _update_profiles_store(self):
560
        """
561
        Clears and repopulates the store with profiles from the currently
562
        selected data stream and checklist.
563
564
        """
565
566
        if self._content_handler is None:
567
            # not initialized, cannot do anything
568
            return
569
570
        if self._using_ds and (
571
                self._current_xccdf_id is None or self._current_ds_id is None):
572
            # not initialized, cannot do anything
573
            return
574
575
        self._profiles_store.clear()
576
        try:
577
            profiles = self._content_handler.get_profiles()
578
        except scap_content_handler.SCAPContentHandlerError as e:
579
            log.warning(str(e))
580
            self._invalid_content()
581
582
        for profile in profiles:
583
            profile_markup = '<span weight="bold">%s</span>\n%s' \
584
                                % (profile.title, profile.description)
585
            self._profiles_store.append([profile.id,
586
                                         profile_markup,
587
                                         profile.id == self._active_profile])
588
589
    def _add_message(self, message):
590
        """
591
        Add message to the store.
592
593
        :param message: message to be added
594
        :type message: org_fedora_oscap.common.RuleMessage
595
596
        """
597
598
        self._message_store.append([message.type, message.text])
599
600
    @dry_run_skip
601
    @async_action_wait
602
    def _update_message_store(self, report_only=False):
603
        """
604
        Updates the message store with messages from rule evaluation.
605
606
        :param report_only: wheter to do changes in configuration or just
607
                            report
608
        :type report_only: bool
609
610
        """
611
612
        self._message_store.clear()
613
614
        if not self._rule_data:
615
            # RuleData instance not initialized, cannot do anything
616
            return
617
618
        messages = self._rule_data.eval_rules(self.data, self._storage,
619
                                              report_only)
620
        if not messages:
621
            # no messages from the rules, add a message informing about that
622
            if not self._active_profile:
623
                # because of no profile
624
                message = common.RuleMessage(self.__class__,
625
                                             common.MESSAGE_TYPE_INFO,
626
                                             _("No profile selected"))
627
            else:
628
                # because of no pre-inst rules
629
                message = common.RuleMessage(self.__class__,
630
                                             common.MESSAGE_TYPE_INFO,
631
                                             _("No rules for the pre-installation phase"))
632
            self._add_message(message)
633
634
            # nothing more to be done
635
            return
636
637
        self._resolve_rootpw_issues(messages, report_only)
638
        for msg in messages:
639
            self._add_message(msg)
640
641
    def _resolve_rootpw_issues(self, messages, report_only):
642
        """Mitigate root password issues (which are not fatal in GUI)"""
643
        fatal_rootpw_msgs = [
644
            msg for msg in messages
645
            if msg.origin == rule_handling.PasswdRules and msg.type == common.MESSAGE_TYPE_FATAL]
646
647
        if fatal_rootpw_msgs:
648
            for msg in fatal_rootpw_msgs:
649
                # cannot just change the message type because it is a namedtuple
650
                messages.remove(msg)
651
652
                msg = common.RuleMessage(
653
                    self.__class__, common.MESSAGE_TYPE_WARNING, msg.text)
654
                messages.append(msg)
655
656
            if not report_only:
657
                users_proxy = USERS.get_proxy()
658
659
                self.__old_root_pw = users_proxy.RootPassword
660
                self.data.rootpw.password = None
661
                self.__old_root_pw_seen = users_proxy.IsRootpwKickstarted
662
                self.data.rootpw.seen = False
663
664
    def _revert_rootpw_changes(self):
665
        if self.__old_root_pw is not None:
666
            users_proxy = USERS.get_proxy()
667
668
            users_proxy.SetRootPassword(self.__old_root_pw)
669
            self.__old_root_pw = None
670
671
            users_proxy.SetRootpwKickstarted(self.__old_root_pw_seen)
672
            self.__old_root_pw_seen = None
673
674
    @async_action_wait
675
    def _unselect_profile(self, profile_id):
676
        """Unselects the given profile."""
677
678
        if not profile_id:
679
            # no profile specified, nothing to do
680
            return
681
682
        itr = self._profiles_store.get_iter_first()
683
        while itr:
684
            if self._profiles_store[itr][0] == profile_id:
685
                self._profiles_store.set_value(itr, 2, False)
686
            itr = self._profiles_store.iter_next(itr)
687
688
        if self._rule_data:
689
            # revert changes and clear rule_data (no longer valid)
690
            self._rule_data.revert_changes(self.data, self._storage)
691
            self._revert_rootpw_changes()
692
            self._rule_data = None
693
694
        self._active_profile = None
695
696
    @async_action_wait
697
    def _select_profile(self, profile_id):
698
        """Selects the given profile."""
699
700
        if not profile_id:
701
            # no profile specified, nothing to do
702
            return False
703
704
        ds = None
705
        xccdf = None
706
        if self._using_ds:
707
            ds = self._current_ds_id
708
            xccdf = self._current_xccdf_id
709
710
            if not all((ds, xccdf, profile_id)):
711
                # something is not set -> do nothing
712
                return False
713
714
        # get pre-install fix rules from the content
715
        try:
716
            self._rule_data = rule_handling.get_rule_data_from_content(
717
                profile_id, self._addon_data.preinst_content_path,
718
                ds, xccdf, self._addon_data.preinst_tailoring_path)
719
        except common.OSCAPaddonError as exc:
720
            log.error(
721
                "Failed to get rules for the profile '{}': {}"
722
                .format(profile_id, str(exc)))
723
            self._set_error(
724
                "Failed to get rules for the profile '{}'"
725
                .format(profile_id))
726
            return False
727
728
        itr = self._profiles_store.get_iter_first()
729
        while itr:
730
            if self._profiles_store[itr][0] == profile_id:
731
                self._profiles_store.set_value(itr, 2, True)
732
            itr = self._profiles_store.iter_next(itr)
733
734
        # remember the active profile
735
        self._active_profile = profile_id
736
737
        return True
738
739
    @async_action_wait
740
    @dry_run_skip
741
    def _switch_profile(self):
742
        """Switches to a current selected profile.
743
744
        :returns: whether some profile was selected or not
745
746
        """
747
748
        self._set_error(None)
749
        profile = self._current_profile_id
750
        if not profile:
751
            return False
752
753
        self._unselect_profile(self._active_profile)
754
        ret = self._select_profile(profile)
755
756
        # update messages according to the newly chosen profile
757
        self._update_message_store()
758
759
        return ret
760
761
    @set_ready
762
    def _set_error(self, msg):
763
        """Set or clear error message"""
764
        if msg:
765
            self._error = msg
766
            self.clear_info()
767
            self.set_error(msg)
768
        else:
769
            self._error = None
770
            self.clear_info()
771
772
    @async_action_wait
773
    def _general_content_problem(self):
774
        msg = _("There was an unexpected problem with the supplied content.")
775
        self._progress_label.set_markup("<b>%s</b>" % msg)
776
        self._wrong_content(msg)
777
778
    @async_action_wait
779
    def _invalid_content(self):
780
        """Callback for informing user about provided content invalidity."""
781
782
        msg = _("Invalid content provided. Enter a different URL, please.")
783
        self._progress_label.set_markup("<b>%s</b>" % msg)
784
        self._wrong_content(msg)
785
786
    @async_action_wait
787
    def _invalid_url(self):
788
        """Callback for informing user about provided URL invalidity."""
789
790
        msg = _("Invalid or unsupported content URL, please enter a different one.")
791
        self._progress_label.set_markup("<b>%s</b>" % msg)
792
        self._wrong_content(msg)
793
794
    @async_action_wait
795
    def _data_fetch_failed(self):
796
        """Adapts the UI if fetching data from entered URL failed"""
797
798
        msg = _("Failed to fetch content. Enter a different URL, please.")
799
        self._progress_label.set_markup("<b>%s</b>" % msg)
800
        self._wrong_content(msg)
801
802
    @async_action_wait
803
    def _network_problem(self):
804
        """Adapts the UI if network error was encountered during data fetch"""
805
806
        msg = _("Network error encountered when fetching data."
807
                " Please check that network is setup and working.")
808
        self._progress_label.set_markup("<b>%s</b>" % msg)
809
        self._wrong_content(msg)
810
811
    @async_action_wait
812
    def _integrity_check_failed(self):
813
        """Adapts the UI if integrity check fails"""
814
815
        msg = _("The integrity check of the content failed. Cannot use the content.")
816
        self._progress_label.set_markup("<b>%s</b>" % msg)
817
        self._wrong_content(msg)
818
819
    @async_action_wait
820
    def _extraction_failed(self, err_msg):
821
        """Adapts the UI if extracting data from entered URL failed"""
822
823
        msg = _("Failed to extract content (%s). Enter a different URL, "
824
                "please.") % err_msg
825
        self._progress_label.set_markup("<b>%s</b>" % msg)
826
        self._wrong_content(msg)
827
828
    @async_action_wait
829
    def _wrong_content(self, msg):
830
        self._addon_data.clear_all()
831
        really_hide(self._progress_spinner)
832
        self._fetch_button.set_sensitive(True)
833
        self._content_url_entry.set_sensitive(True)
834
        self._content_url_entry.grab_focus()
835
        self._content_url_entry.select_region(0, -1)
836
        self._set_error(msg)
837
838
    @async_action_wait
839
    def _invalid_profile_id(self):
840
        msg = _("Profile with ID '%s' not defined in the content. Select a different profile, please") % self._addon_data.profile_id
841
        self._set_error(msg)
842
        self._addon_data.profile_id = None
843
844
    @async_action_wait
845
    def _switch_dry_run(self, dry_run):
846
        self._choose_button.set_sensitive(not dry_run)
847
848
        if dry_run:
849
            # no profile can be selected in the dry-run mode
850
            self._unselect_profile(self._active_profile)
851
852
            # no messages in the dry-run mode
853
            self._message_store.clear()
854
            message = common.RuleMessage(self.__class__,
855
                                         common.MESSAGE_TYPE_INFO,
856
                                         _("Not applying security policy"))
857
            self._add_message(message)
858
859
            self._set_error(None)
860
        else:
861
            # mark the active profile as selected
862
            self._select_profile(self._active_profile)
863
            self._update_message_store()
864
865
    @async_action_wait
866
    def refresh(self):
867
        """
868
        The refresh method that is called every time the spoke is displayed.
869
        It should update the UI elements according to the contents of
870
        self.data.
871
872
        :see: pyanaconda.ui.common.UIObject.refresh
873
874
        """
875
876
        if not self._addon_data.content_defined:
877
            # hide the control buttons
878
            really_hide(self._control_buttons)
879
880
            # provide SSG if available
881
            if common.ssg_available():
882
                # show the SSG button and tweak the rest of the line
883
                # (the label)
884
                really_show(self._ssg_button)
885
                # TRANSLATORS: the other choice if SCAP Security Guide is also
886
                # available
887
                tip = _(" or enter data stream content or archive URL below:")
888
            else:
889
                # hide the SSG button
890
                really_hide(self._ssg_button)
891
                tip = _("No content found. Please enter data stream content or "
892
                        "archive URL below:")
893
894
            self._no_content_label.set_text(tip)
895
896
            # hide the progress box, no progress now
897
            with self._fetch_flag_lock:
898
                if not self._fetching:
899
                    really_hide(self._progress_box)
900
901
                    self._content_url_entry.set_sensitive(True)
902
                    self._fetch_button.set_sensitive(True)
903
904
                    if not self._content_url_entry.get_text():
905
                        # no text -> no info/warning
906
                        self._progress_label.set_text("")
907
908
            # switch to the page allowing user to enter content URL and fetch
909
            # it
910
            self._main_notebook.set_current_page(GET_CONTENT_PAGE)
911
            self._content_url_entry.grab_focus()
912
913
            # nothing more to do here
914
            return
915
        else:
916
            # show control buttons
917
            really_show(self._control_buttons)
918
919
            self._main_notebook.set_current_page(SET_PARAMS_PAGE)
920
921
        self._active_profile = self._addon_data.profile_id
922
923
        self._update_ids_visibility()
924
925
        if self._using_ds:
926
            if self._addon_data.datastream_id:
927
                set_combo_selection(self._ds_combo,
928
                                    self._addon_data.datastream_id,
929
                                    unset_first=True)
930
            else:
931
                try:
932
                    default_ds = next(iter(self._ds_checklists.keys()))
933
                    set_combo_selection(self._ds_combo, default_ds,
934
                                        unset_first=True)
935
                except StopIteration:
936
                    # no data stream available
937
                    pass
938
939
                if self._addon_data.datastream_id and self._addon_data.xccdf_id:
940
                    set_combo_selection(self._xccdf_combo,
941
                                        self._addon_data.xccdf_id,
942
                                        unset_first=True)
943
        else:
944
            # no combobox changes --> need to update profiles store manually
945
            self._update_profiles_store()
946
947
        if self._addon_data.profile_id:
948
            set_treeview_selection(self._profiles_view,
949
                                   self._addon_data.profile_id)
950
951
        self._rule_data = self._addon_data.rule_data
952
953
        self._update_message_store()
954
955
    def apply(self):
956
        """
957
        The apply method that is called when the spoke is left. It should
958
        update the contents of self.data with values set in the GUI elements.
959
960
        """
961
962
        if not self._addon_data.content_defined or not self._active_profile:
963
            # no errors for no content or no profile
964
            self._set_error(None)
965
966
        # store currently selected values to the addon data attributes
967
        if self._using_ds:
968
            self._addon_data.datastream_id = self._current_ds_id
969
            self._addon_data.xccdf_id = self._current_xccdf_id
970
971
        self._addon_data.profile_id = self._active_profile
972
973
        self._addon_data.rule_data = self._rule_data
974
975
        self._addon_data.dry_run = not self._dry_run_switch.get_active()
976
977
    def execute(self):
978
        """
979
        The excecute method that is called when the spoke is left. It is
980
        supposed to do all changes to the runtime environment according to
981
        the values set in the GUI elements.
982
983
        """
984
985
        # nothing to do here
986
        pass
987
988
    @property
989
    def ready(self):
990
        """
991
        The ready property that tells whether the spoke is ready (can be
992
        visited) or not.
993
994
        :rtype: bool
995
996
        """
997
998
        return self._ready
999
1000
    @property
1001
    def completed(self):
1002
        """
1003
        The completed property that tells whether all mandatory items on the
1004
        spoke are set, or not. The spoke will be marked on the hub as completed
1005
        or uncompleted acording to the returned value.
1006
1007
        :rtype: bool
1008
1009
        """
1010
1011
        # no error message in the store
1012
        return not self._error and all(row[0] != common.MESSAGE_TYPE_FATAL
1013
                                       for row in self._message_store)
1014
1015
    @property
1016
    @async_action_wait
1017
    def status(self):
1018
        """
1019
        The status property that is a brief string describing the state of the
1020
        spoke. It should describe whether all values are set and if possible
1021
        also the values themselves. The returned value will appear on the hub
1022
        below the spoke's title.
1023
1024
        :rtype: str
1025
1026
        """
1027
1028
        if self._error:
1029
            return _("Error fetching and loading content")
1030
1031
        if self._unitialized_status:
1032
            # not initialized
1033
            return self._unitialized_status
1034
1035
        if not self._addon_data.content_defined:
1036
            return _("No content found")
1037
1038
        if not self._active_profile:
1039
            return _("No profile selected")
1040
1041
        # update message store, something may changed from the last update
1042
        self._update_message_store(report_only=True)
1043
1044
        warning_found = False
1045
        for row in self._message_store:
1046
            if row[0] == common.MESSAGE_TYPE_FATAL:
1047
                return _("Misconfiguration detected")
1048
            elif row[0] == common.MESSAGE_TYPE_WARNING:
1049
                warning_found = True
1050
1051
        # TODO: at least the last two status messages need a better wording
1052
        if warning_found:
1053
            return _("Warnings appeared")
1054
1055
        return _("Everything okay")
1056
1057
    def on_ds_combo_changed(self, *args):
1058
        """Handler for the datastream ID change."""
1059
1060
        ds_id = self._current_ds_id
1061
        if not ds_id:
1062
            return
1063
1064
        self._update_xccdfs_store()
1065
        first_checklist = self._ds_checklists[ds_id][0]
1066
1067
        set_combo_selection(self._xccdf_combo, first_checklist)
1068
1069
    def on_xccdf_combo_changed(self, *args):
1070
        """Handler for the XCCDF ID change."""
1071
        self._content_handler.select_checklist(
1072
            self._current_ds_id, self._current_xccdf_id)
1073
1074
        # may take a while
1075
        self._update_profiles_store()
1076
1077
    @dry_run_skip
1078
    def on_profiles_selection_changed(self, *args):
1079
        """Handler for the profile selection change."""
1080
1081
        cur_profile = self._current_profile_id
1082
        if cur_profile:
1083
            if cur_profile != self._active_profile:
1084
                # new profile selected, make the selection button sensitive
1085
                self._choose_button.set_sensitive(True)
1086
            else:
1087
                # current active profile selected
1088
                self._choose_button.set_sensitive(False)
1089
1090
    @dry_run_skip
1091
    def on_profile_clicked(self, widget, event, *args):
1092
        """Handler for the profile being clicked on."""
1093
1094
        # if a profile is double-clicked, we should switch to it
1095
        if event.type == Gdk.EventType._2BUTTON_PRESS:
1096
            self._switch_profile()
1097
1098
            # active profile selected
1099
            self._choose_button.set_sensitive(False)
1100
1101
        # let the other actions hooked to the click happen as well
1102
        return False
1103
1104
    def on_profile_chosen(self, *args):
1105
        """
1106
        Handler for the profile being chosen
1107
        (e.g. "Select profile" button hit).
1108
1109
        """
1110
1111
        # switch profile
1112
        self._switch_profile()
1113
1114
        # active profile selected
1115
        self._choose_button.set_sensitive(False)
1116
1117
    def on_fetch_button_clicked(self, *args):
1118
        """Handler for the Fetch button"""
1119
1120
        with self._fetch_flag_lock:
1121
            if self._fetching:
1122
                # some other fetching/pre-processing running, give up
1123
                log.warn("Clicked the fetch button, although the GUI is in the fetching mode.")
1124
                return
1125
1126
        # prevent user from changing the URL in the meantime
1127
        self._content_url_entry.set_sensitive(False)
1128
        self._fetch_button.set_sensitive(False)
1129
        url = self._content_url_entry.get_text()
1130
        really_show(self._progress_box)
1131
        really_show(self._progress_spinner)
1132
1133
        if not data_fetch.can_fetch_from(url):
1134
            msg = _("Invalid or unsupported URL")
1135
            # cannot start fetching
1136
            self._progress_label.set_markup("<b>%s</b>" % msg)
1137
            self._wrong_content(msg)
1138
            return
1139
1140
        self._progress_label.set_text(_("Fetching content..."))
1141
        self._progress_spinner.start()
1142
        self._addon_data.content_url = url
1143
        if url.endswith(".rpm"):
1144
            self._addon_data.content_type = "rpm"
1145
        elif any(url.endswith(arch_type) for arch_type in common.SUPPORTED_ARCHIVES):
1146
            self._addon_data.content_type = "archive"
1147
        else:
1148
            self._addon_data.content_type = "datastream"
1149
1150
        self._fetch_data_and_initialize()
1151
1152
    def on_dry_run_toggled(self, switch, *args):
1153
        dry_run = not switch.get_active()
1154
        self._addon_data.dry_run = dry_run
1155
        self._switch_dry_run(dry_run)
1156
1157
    def on_change_content_clicked(self, *args):
1158
        self._unselect_profile(self._active_profile)
1159
        self._addon_data.clear_all()
1160
        self.refresh()
1161
1162
    def on_use_ssg_clicked(self, *args):
1163
        self.content_bringer.use_system_content()
1164
        self._fetch_data_and_initialize()
1165