Passed
Pull Request — rhel8-branch (#221)
by Matěj
01:29
created

org_fedora_oscap.gui.spokes.oscap   F

Complexity

Total Complexity 159

Size/Duplication

Total Lines 1167
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 159
eloc 608
dl 0
loc 1167
rs 1.992
c 0
b 0
f 0

53 Methods

Rating   Name   Duplication   Size   Complexity  
A OSCAPSpoke._extraction_failed() 0 8 1
A OSCAPSpoke._data_fetch_failed() 0 7 1
A OSCAPSpoke._network_problem() 0 8 1
A OSCAPSpoke._invalid_content() 0 7 1
A OSCAPSpoke._invalid_url() 0 7 1
A OSCAPSpoke._integrity_check_failed() 0 7 1
A GtkActionList.fire() 0 7 2
A GtkActionList.__init__() 0 2 1
A GtkActionList.add_action() 0 8 1
A OSCAPSpoke._all_anaconda_spokes_initialized() 0 3 1
B OSCAPSpoke._select_profile() 0 42 7
A OSCAPSpoke.on_change_content_clicked() 0 4 1
A OSCAPSpoke.__init__() 0 49 1
A OSCAPSpoke._revert_rootpw_changes() 0 9 2
A OSCAPSpoke.on_xccdf_combo_changed() 0 7 1
A OSCAPSpoke.on_profile_chosen() 0 12 1
A OSCAPSpoke._resolve_rootpw_issues() 0 22 4
A OSCAPSpoke._wrong_content() 0 9 1
A OSCAPSpoke._update_xccdfs_store() 0 15 3
A OSCAPSpoke.on_dry_run_toggled() 0 4 1
A OSCAPSpoke._add_ds_id() 0 10 1
A OSCAPSpoke.on_ds_combo_changed() 0 11 2
A OSCAPSpoke.apply() 0 21 4
A OSCAPSpoke._current_profile_id() 0 7 3
A OSCAPSpoke._using_ds() 0 3 1
A OSCAPSpoke.ready() 0 11 1
A OSCAPSpoke.on_profiles_selection_changed() 0 12 3
D OSCAPSpoke.refresh() 0 89 12
A OSCAPSpoke._switch_dry_run() 0 20 2
A OSCAPSpoke._fetch_data_and_initialize() 0 22 5
B OSCAPSpoke._update_profiles_store() 0 30 7
A OSCAPSpoke.on_profile_clicked() 0 13 2
A OSCAPSpoke._current_xccdf_id() 0 3 1
A OSCAPSpoke._set_error() 0 10 2
A OSCAPSpoke.completed() 0 14 1
A OSCAPSpoke._end_fetching() 0 3 1
A OSCAPSpoke._still_fetching() 0 2 1
A OSCAPSpoke.on_use_ssg_clicked() 0 3 1
A OSCAPSpoke._general_content_problem() 0 5 1
A OSCAPSpoke._invalid_profile_id() 0 5 1
C OSCAPSpoke.status() 0 41 9
A OSCAPSpoke._add_message() 0 10 1
B OSCAPSpoke.initialize() 0 78 4
A OSCAPSpoke._current_ds_id() 0 3 1
A OSCAPSpoke.execute() 0 10 1
A OSCAPSpoke._unselect_profile() 0 21 5
B OSCAPSpoke.on_fetch_button_clicked() 0 34 6
F OSCAPSpoke._init_after_data_fetch() 0 101 14
A OSCAPSpoke._update_ids_visibility() 0 18 4
A OSCAPSpoke._switch_profile() 0 21 2
A OSCAPSpoke._render_selected() 0 5 2
B OSCAPSpoke._handle_error() 0 16 7
B OSCAPSpoke._update_message_store() 0 40 5

4 Functions

Rating   Name   Duplication   Size   Complexity  
A render_message_type() 0 12 4
A set_combo_selection() 0 25 5
A get_combo_selection() 0 14 3
A set_ready() 0 13 1

How to fix   Complexity   

Complexity

Complex classes like org_fedora_oscap.gui.spokes.oscap often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

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