Skip to content

Commit

Permalink
Introduced the Model class.
Browse files Browse the repository at this point in the history
The class unifies the content workflow between kickstart and GUI installations.
It provides methods to work with the content in non-interactive and interactive modes
using a system of callbacks.
  • Loading branch information
matejak committed Jun 7, 2021
1 parent 8aa7866 commit 2be5070
Show file tree
Hide file tree
Showing 3 changed files with 372 additions and 130 deletions.
107 changes: 41 additions & 66 deletions org_fedora_oscap/gui/spokes/oscap.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import threading
import logging
from functools import wraps
import pathlib

# the path to addons is in sys.path so we can import things
# from org_fedora_oscap
Expand All @@ -30,6 +31,7 @@
from org_fedora_oscap import content_handling
from org_fedora_oscap import utils
from org_fedora_oscap.common import dry_run_skip
from org_fedora_oscap.model import Model
from pyanaconda.threading import threadMgr, AnacondaThread
from pyanaconda.ui.gui.spokes import NormalSpoke
from pyanaconda.ui.communication import hubQ
Expand Down Expand Up @@ -251,6 +253,8 @@ def __init__(self, data, storage, payload):
self._anaconda_spokes_initialized = threading.Event()
self.initialization_controller.init_done.connect(self._all_anaconda_spokes_initialized)

self.model = Model(None)

def _all_anaconda_spokes_initialized(self):
log.debug("OSCAP addon: Anaconda init_done signal triggered")
self._anaconda_spokes_initialized.set()
Expand Down Expand Up @@ -334,6 +338,27 @@ def initialize(self):
# else fetch data
self._fetch_data_and_initialize()

def _handle_error(self, exception):
log.error(str(exception))
if isinstance(exception, KickstartValueError):
self._invalid_url()
elif isinstance(exception, common.OSCAPaddonNetworkError):
self._network_problem()
elif isinstance(exception, data_fetch.DataFetchError):
self._data_fetch_failed()
elif isinstance(exception, common.ExtractionError):
self._extraction_failed(str(exception))
elif isinstance(exception, content_handling.ContentHandlingError):
self._invalid_content()
elif isinstance(exception, content_handling.ContentCheckError):
self._integrity_check_failed()
else:
raise exception

def _end_fetching(self, fetched_content):
# stop the spinner in any case
fire_gtk_action(self._progress_spinner.stop)

def _render_selected(self, column, renderer, model, itr, user_data=None):
if model[itr][2]:
renderer.set_property("stock-id", "gtk-apply")
Expand All @@ -350,24 +375,10 @@ def _fetch_data_and_initialize(self):
self._fetching = True

thread_name = None
if any(self._addon_data.content_url.startswith(net_prefix)
for net_prefix in data_fetch.NET_URL_PREFIXES):
# need to fetch data over network
try:
thread_name = data_fetch.wait_and_fetch_net_data(
self._addon_data.content_url,
self._addon_data.raw_preinst_content_path,
self._addon_data.certificates)
except common.OSCAPaddonNetworkError:
self._network_problem()
with self._fetch_flag_lock:
self._fetching = False
return
except KickstartValueError:
self._invalid_url()
with self._fetch_flag_lock:
self._fetching = False
return
if self._addon_data.content_url and self._addon_data.content_type != "scap-security-guide":
self.model.CONTENT_DOWNLOAD_LOCATION = pathlib.Path(common.INSTALLATION_CONTENT_DIR)
self.model.content_uri = self._addon_data.content_url
thread_name = self.model.fetch_content(self._addon_data.certificates, self._handle_error)

# pylint: disable-msg=E1101
hubQ.send_message(self.__class__.__name__,
Expand All @@ -389,60 +400,24 @@ def _init_after_data_fetch(self, wait_for):
:type wait_for: str or None
"""
content_path = None
actually_fetched_content = wait_for is not None

try:
threadMgr.wait(wait_for)
except data_fetch.DataFetchError:
self._data_fetch_failed()
if actually_fetched_content:
content_path = self._addon_data.raw_preinst_content_path

content = self.model.finish_content_fetch(wait_for, self._addon_data.fingerprint, lambda msg: log.info(msg), content_path, self._end_fetching, self._handle_error)
if not content:
with self._fetch_flag_lock:
self._fetching = False
return
finally:
# stop the spinner in any case
fire_gtk_action(self._progress_spinner.stop)

if self._addon_data.fingerprint:
hash_obj = utils.get_hashing_algorithm(self._addon_data.fingerprint)
digest = utils.get_file_fingerprint(self._addon_data.raw_preinst_content_path,
hash_obj)
if digest != self._addon_data.fingerprint:
self._integrity_check_failed()
# fetching done
with self._fetch_flag_lock:
self._fetching = False
return

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

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

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

try:
preferred_content = self._addon_data.get_preferred_content(content)
self._content_handling_cls = content.get_file_handler(preferred_content)
if actually_fetched_content:
self._addon_data.update_with_content(content)
self._content_handler = self._content_handling_cls(self._addon_data.preinst_content_path,
self._addon_data.preinst_tailoring_path)
except content_handling.ContentHandlingError:
Expand Down
118 changes: 54 additions & 64 deletions org_fedora_oscap/ks/oscap.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import os
import time
import logging
import pathlib

from pyanaconda.addons import AddonData
from pyanaconda.core.configuration.anaconda import conf
Expand All @@ -35,7 +36,8 @@
from pykickstart.errors import KickstartParseError, KickstartValueError
from org_fedora_oscap import utils, common, rule_handling, data_fetch
from org_fedora_oscap.common import SUPPORTED_ARCHIVES, _
from org_fedora_oscap.content_handling import ContentCheckError
from org_fedora_oscap.content_handling import ContentCheckError, ContentHandlingError
from org_fedora_oscap import model

log = logging.getLogger("anaconda")

Expand Down Expand Up @@ -102,6 +104,9 @@ def __init__(self, name, just_clear=False):
self.rule_data = rule_handling.RuleData()
self.dry_run = False

self.model = model.Model(self)
self.model.CONTENT_DOWNLOAD_LOCATION = pathlib.Path(common.INSTALLATION_CONTENT_DIR)

def __str__(self):
"""
What should end up in the resulting kickstart file, i.e. string
Expand Down Expand Up @@ -368,17 +373,24 @@ def postinst_tailoring_path(self):
return utils.join_paths(common.TARGET_CONTENT_DIR,
self.tailoring_path)

def _fetch_content_and_initialize(self):
"""Fetch content and initialize from it"""
def get_preferred_content(self, content):
if self.content_path:
preferred_content = content.find_expected_usable_content(self.content_path)
else:
preferred_content = content.select_main_usable_content()

if self.tailoring_path:
if self.tailoring_path != str(content.tailoring.relative_to(content.root)):
msg = f"Expected a tailoring {self.tailoring_path}, but it couldn't be found"
raise content_handling.ContentHandlingError(msg)
return preferred_content

def update_with_content(self, content):
preferred_content = self.get_preferred_content(content)

data_fetch.fetch_data(self.content_url, self.raw_preinst_content_path,
self.certificates)
# RPM is an archive at this phase
if self.content_type in ("archive", "rpm"):
# extract the content
common.extract_data(self.raw_preinst_content_path,
common.INSTALLATION_CONTENT_DIR,
[self.content_path])
self.content_path = str(preferred_content.relative_to(content.root))
if content.tailoring:
self.tailoring_path = str(content.tailoring.relative_to(content.root))

def _terminate(self, message):
if flags.flags.automatedInstall and not flags.flags.ksprompt:
Expand All @@ -399,7 +411,22 @@ def _terminate(self, message):
while True:
time.sleep(100000)

def _handle_error(self, exception):
log.error("Failed to fetch and initialize SCAP content!")

if isinstance(exception, ContentCheckError):
msg = _("The integrity check of the security content failed.\n" +
"The installation should be aborted. Do you wish to continue anyway?")
self._terminate(msg)
elif (isinstance(exception, common.OSCAPaddonError)
or isinstance(exception, data_fetch.DataFetchError)):
msg = _("There was an error fetching and loading the security content:\n" +
"%s\n" +
"The installation should be aborted. Do you wish to continue anyway?") % str(exception)
self._terminate(msg)

else:
raise exception

def setup(self, storage, ksdata, payload):
"""
Expand All @@ -420,61 +447,24 @@ def setup(self, storage, ksdata, payload):
# selected
return

thread_name = None
if not os.path.exists(self.preinst_content_path) and not os.path.exists(self.raw_preinst_content_path):
# content not available/fetched yet
try:
self._fetch_content_and_initialize()
except (common.OSCAPaddonError, data_fetch.DataFetchError) as e:
log.error("Failed to fetch and initialize SCAP content!")
msg = _("There was an error fetching and loading the security content:\n" +
"%s\n" +
"The installation should be aborted. Do you wish to continue anyway?") % e

if flags.flags.automatedInstall and not flags.flags.ksprompt:
# cannot have ask in a non-interactive kickstart
# installation
raise errors.CmdlineError(msg)

answ = errors.errorHandler.ui.showYesNoQuestion(msg)
if answ == errors.ERROR_CONTINUE:
# prevent any futher actions here by switching to the dry
# run mode and let things go on
self.dry_run = True
return
else:
# Let's sleep forever to prevent any further actions and
# wait for the main thread to quit the process.
progressQ.send_quit(1)
while True:
time.sleep(100000)

# check fingerprint if given
if self.fingerprint:
hash_obj = utils.get_hashing_algorithm(self.fingerprint)
digest = utils.get_file_fingerprint(self.raw_preinst_content_path,
hash_obj)
if digest != self.fingerprint:
log.error("Failed to fetch and initialize SCAP content!")
msg = _("The integrity check of the security content failed.\n" +
"The installation should be aborted. Do you wish to continue anyway?")

if flags.flags.automatedInstall and not flags.flags.ksprompt:
# cannot have ask in a non-interactive kickstart
# installation
raise errors.CmdlineError(msg)

answ = errors.errorHandler.ui.showYesNoQuestion(msg)
if answ == errors.ERROR_CONTINUE:
# prevent any futher actions here by switching to the dry
# run mode and let things go on
self.dry_run = True
return
else:
# Let's sleep forever to prevent any further actions and
# wait for the main thread to quit the process.
progressQ.send_quit(1)
while True:
time.sleep(100000)
self.model.content_uri = self.content_url
thread_name = self.model.fetch_content(self.certificates, self._handle_error)

content = self.model.finish_content_fetch(thread_name, self.fingerprint, lambda msg: log.info(msg), self.raw_preinst_content_path, lambda content: content, self._handle_error)

if not content:
return

try:
# just check that preferred content exists
_ = self.get_preferred_content(content)
except Exception as exc:
self._terminate(str(exc))
return

self.rule_data = rule_handling.get_rule_data_from_content(
self.profile_id, self.preinst_content_path,
self.datastream_id, self.xccdf_id, self.preinst_tailoring_path)
Expand Down
Loading

0 comments on commit 2be5070

Please sign in to comment.