def test_kill_should_run_taskkill(self, mock_subprocess, *args): """Kill valid package status.""" sm = WinSvcManagerNSSM(svc_conf=CONF_STUB) sm.kill() cmd = "taskkill /f /fi \"SERVICES eq the_displayname\"" mock_subprocess.assert_called_once_with(cmd, **RUN_COMMAND_DEFAULT_KWARGS)
def test_status_should_run_status(self, mock_subprocess, *args): """Get valid package status.""" sm = WinSvcManagerNSSM(svc_conf=CONF_STUB) sm.status() cmd = 'command status the_displayname' mock_subprocess.assert_called_once_with(cmd, **RUN_COMMAND_DEFAULT_KWARGS)
def test_enable_should_set_auto_start(self, mock_subprocess, *args): """Enable valid package.""" sm = WinSvcManagerNSSM(svc_conf=CONF_STUB) sm.enable() cmd = 'command set the_displayname start SERVICE_AUTO_START' mock_subprocess.assert_called_once_with(cmd, **RUN_COMMAND_DEFAULT_KWARGS)
def test_remove_should_run_remove_command(self, mock_subprocess, *args): """Remove valid package.""" sm = WinSvcManagerNSSM(svc_conf=CONF_STUB) sm.remove() cmd = 'command remove the_displayname confirm' mock_subprocess.assert_called_once_with(cmd, **RUN_COMMAND_DEFAULT_KWARGS)
def test_stop_timeout_should_run_kill(self, mock_subprocess, *args): """Kill valid package after stop issues. #D2IQ-66446""" mock_subprocess.side_effect = exceptions.ServiceManagerCommandError() mock_kill = mock.Mock() sm = WinSvcManagerNSSM(svc_conf=CONF_STUB) sm.kill = mock_kill sm.stop() assert mock_kill.called
def test_displayname_before_name(): """ If name and displayname are both set, displayname is used """ conf = { 'service': { 'application': 'an_app', 'displayname': 'a_displayname', 'name': 'a_name', } } opts = {'svc_conf': conf} sm = WinSvcManagerNSSM(**opts) assert sm.svc_name == 'a_displayname' assert sm._get_svc_setup_pchain() == [('install', ['a_displayname', 'an_app'])]
def __init__(self, pkg_id=None, istor_nodes=None, cluster_conf=None, manifest=None): """Constructor. :param pkg_id: PackageId, package ID :param istor_nodes: IStorNodes, DC/OS installation storage nodes (set of pathlib.Path objects) :param cluster_conf: dict, configparser.ConfigParser.read_dict() compatible data. DC/OS cluster setup parameters :param manifest: PackageManifest, DC/OS package manifest object """ if manifest is not None: assert isinstance(manifest, PackageManifest), ( f'Argument: manifest:' f' Got {type(manifest).__name__} instead of PackageManifest') self.manifest = manifest else: self.manifest = PackageManifest(pkg_id, istor_nodes, cluster_conf) if self.manifest.pkg_extcfg: self.ext_manager = PkgInstExtrasManager( ext_conf=self.manifest.pkg_extcfg) else: self.ext_manager = None if self.manifest.pkg_svccfg: self.svc_manager = WinSvcManagerNSSM( svc_conf=self.manifest.pkg_svccfg) else: self.svc_manager = None
def __init__(self, pkg_id: PackageId = None, istor_nodes: IStorNodes = None, cluster_conf: dict = None, extra_context: dict = None, manifest: PackageManifest = None): """Constructor. :param pkg_id: PackageId, package ID :param istor_nodes: IStorNodes, DC/OS installation storage nodes (set of pathlib.Path objects) :param cluster_conf: dict, configparser.ConfigParser.read_dict() compatible data. DC/OS cluster setup parameters :param extra_context: dict, extra 'key=value' data to be added to the resource rendering context :param manifest: PackageManifest, DC/OS package manifest object """ self.msg_src = self.__class__.__name__ if manifest is None: manifest = PackageManifest(pkg_id=pkg_id, istor_nodes=istor_nodes, cluster_conf=cluster_conf, extra_context=extra_context) self.manifest = manifest LOG.debug(f'{self.msg_src}: {self.manifest.pkg_id.pkg_id}: Manifest:' f' {self.manifest}') self.cfg_manager = PkgConfManager(pkg_manifest=self.manifest) LOG.debug(f'{self.msg_src}: {self.manifest.pkg_id.pkg_id}:' f' Package configuration manager: {self.cfg_manager}') if self.manifest.pkg_extcfg: self.ext_manager = PkgInstExtrasManager(pkg_manifest=self.manifest) else: self.ext_manager = None LOG.debug(f'{self.msg_src}: {self.manifest.pkg_id.pkg_id}:' f' Installation extras manager: {self.ext_manager}') if self.manifest.pkg_svccfg: self.svc_manager = WinSvcManagerNSSM( svc_conf=self.manifest.pkg_svccfg) else: self.svc_manager = None LOG.debug(f'{self.msg_src}: {self.manifest.pkg_id.pkg_id}:' f' Service manager: {self.svc_manager}')
def test_no_service(): """ Empty configuration fails """ conf = {} opts = {'svc_conf': conf} with pytest.raises(ServiceConfigError) as e: WinSvcManagerNSSM(**opts) assert 'Section not found: service' in str(e)
def test_no_application(): """ Missing application name fails """ conf = {'service': {'displayname': ''}} opts = {'svc_conf': conf} with pytest.raises(ServiceConfigError) as e: WinSvcManagerNSSM(**opts) assert 'Required parameter unavailable: application' in str(e)
def test_parameter_names_should_be_same_as_init(self): """Initialize class with all parameters.""" sm = WinSvcManagerNSSM(svc_conf=CONF_STUB) assert sm.svc_name == 'the_displayname' assert sm.svc_exec == 'the_application' intersection = list( set(sm.svc_pnames_bulk) & set(CONF_STUB['service'].keys())) assert len(intersection) == 19
def test_name_implies_displayname(): """ Display name can be derived from name """ conf = { 'service': { 'application': '', 'name': 'a_name', } } opts = {'svc_conf': conf} sm = WinSvcManagerNSSM(**opts) assert sm.svc_name == 'a_name' assert set(sm.svc_pnames_bulk) == set(('application', 'name'))
def execute(self): """Execute command.""" msg_src = self.__class__.__name__ for pkg_manifest in self.config.inst_storage.get_pkgactive(): pkg_id = pkg_manifest.pkg_id LOG.debug(f'{msg_src}: Execute: Manifest: {pkg_manifest.body}') svc_conf = cfp.ConfigParser() svc_conf.read_dict(pkg_manifest.svc_conf) cluster_conf = cfp.ConfigParser() cluster_conf.read_dict(self.config.cluster_conf) svc_manager = WinSvcManagerNSSM( svc_conf=svc_conf, cluster_conf=cluster_conf ) try: ret_code, stdout, stderr = svc_manager.status() except svcm_exc.ServiceManagerCommandError as e: err_msg = (f'Execute: Get service status (initial):' f' {pkg_id.pkg_name}: {e}') raise cr_exc.StartCommandError(err_msg) else: LOG.debug( f'{msg_src}: Execute: Get service status (initial):' f' {pkg_id.pkg_name}: stdout[{stdout}] stderr[{stderr}]' ) svc_status = str(stdout).strip().rstrip('\n') if svc_status == SVC_STATUS.STOPPED: try: svc_manager.start() ret_code, stdout, stderr = svc_manager.status() LOG.debug( f'{msg_src}: Execute: Get service status (final):' f' {pkg_id.pkg_name}: stdout[{stdout}]' f' stderr[{stderr}]' ) svc_status = str(stdout).strip().rstrip('\n') if svc_status != SVC_STATUS.RUNNING: err_msg = (f'Execute: Service failed to start:' f' {pkg_id.pkg_name}: {svc_status}') raise cr_exc.StartCommandError(err_msg) except svcm_exc.ServiceManagerCommandError as e: err_msg = (f'Execute: Get service status (final):' f' {pkg_id.pkg_name}: {e}') raise cr_exc.StartCommandError(err_msg) elif svc_status == SVC_STATUS.RUNNING: LOG.warning(f'{msg_src}: Execute: Service is already running:' f' {pkg_id.pkg_name}') else: err_msg = (f'Execute: Invalid service status:' f' {pkg_id.pkg_name}: {svc_status}') raise cr_exc.StartCommandError(err_msg)
def __init__(self, pkg_id, pkgrepo_dpath, pkgactive_dpath, cluster_conf): """Constructor. :param pkg_id: PackageId, package ID :param pkgrepo_dpath: pathlib.Path, local package repository dir :param pkgactive_dpath: pathlib.Path, active packages index dir :param cluster_conf: dict, configparser.ConfigParser.read_dict() compatible data. DC/OS cluster setup parameters """ self.manifest = PackageManifest(pkg_id, pkgrepo_dpath, pkgactive_dpath) self.ini_manager = None self.svc_conf = cfp.ConfigParser() self.svc_conf.read_dict(self.manifest.svc_conf) self.cluster_conf = cfp.ConfigParser() self.cluster_conf.read_dict(cluster_conf) self.svc_manager = WinSvcManagerNSSM(svc_conf=self.svc_conf, cluster_conf=self.cluster_conf)
def test_display_name_only_in_configuration_should_fail(self): """No service configuration application display name only validation exception.""" conf = {'service': {'displayname': 'test'}} with pytest.raises(exceptions.ServiceConfigError): WinSvcManagerNSSM(svc_conf=conf)
def test_configuration_without_name_should_fail(self): """No service configuration name validation exception.""" conf = {'service': {'something': ''}} with pytest.raises(exceptions.ServiceConfigError): WinSvcManagerNSSM(svc_conf=conf)
def test_empty_configuration_should_fail(self): """No service configuration validation exception.""" with pytest.raises(exceptions.ServiceConfigError): WinSvcManagerNSSM()
def test_all_parameters(): """ All parameters get translated to command arguments """ conf = { 'service': { 'description': 'the_description', 'displayname': 'the_displayname', 'name': 'the_name', 'application': 'the_application', 'appdirectory': 'the_appdirectory', 'appparameters': 'the_appparameters', 'start': 'the_start', 'dependonservice': 'the_dependonservice', 'appstdout': 'the_appstdout', 'appstderr': 'the_appstderr', 'appenvironmentextra': 'the_appenvironmentextra', 'appeventsstartpre': 'the_appevents_start_pre', 'appeventsstartpost': 'the_appevents_start_post', 'appeventsstoppre': 'the_appevents_stop_pre', 'appeventsexitpost': 'the_appevents_exit_post', 'appeventsrotatepre': 'the_appevents_rotate_pre', 'appeventsrotatepost': 'the_appevents_rotate_post', 'appeventspowerchange': 'the_appevents_power_change', 'appeventspowerresume': 'the_appevents_power_resume', 'appredirecthook': 'the_appredirecthook', } } opts = {'svc_conf': conf} sm = WinSvcManagerNSSM(**opts) assert sm.svc_name == 'the_displayname' assert sm.svc_exec == 'the_application' expected_names = set(conf['service'].keys()) # if `displayname` is set, then `name` is removed expected_names.remove('name') assert set(sm.svc_pnames_bulk) == expected_names assert sm._get_svc_setup_pchain() == [ ('install', ['the_displayname', 'the_application']), ('set', ['the_displayname', 'description', 'the_description']), ('set', ['the_displayname', 'appdirectory', 'the_appdirectory']), ('set', ['the_displayname', 'appparameters', 'the_appparameters']), ('set', ['the_displayname', 'start', 'the_start']), ('set', ['the_displayname', 'dependonservice', 'the_dependonservice']), ('set', ['the_displayname', 'appstdout', 'the_appstdout']), ('set', ['the_displayname', 'appstderr', 'the_appstderr']), ('set', ['the_displayname', 'appenvironmentextra', 'the_appenvironmentextra']), ('set', [ 'the_displayname', 'appevents', 'start/pre', 'the_appevents_start_pre' ]), ('set', [ 'the_displayname', 'appevents', 'start/post', 'the_appevents_start_post' ]), ('set', [ 'the_displayname', 'appevents', 'stop/pre', 'the_appevents_stop_pre' ]), ('set', [ 'the_displayname', 'appevents', 'exit/post', 'the_appevents_exit_post' ]), ('set', [ 'the_displayname', 'appevents', 'rotate/pre', 'the_appevents_rotate_pre' ]), ('set', [ 'the_displayname', 'appevents', 'rotate/post', 'the_appevents_rotate_post' ]), ('set', [ 'the_displayname', 'appevents', 'power/change', 'the_appevents_power_change' ]), ('set', [ 'the_displayname', 'appevents', 'power/resume', 'the_appevents_power_resume' ]), ('set', ['the_displayname', 'appredirecthook', 'the_appredirecthook']), ]
def test_empty_exec_path_should_fail(self): """Setup package with valid but not non executable configuration.""" sm = WinSvcManagerNSSM(svc_conf=CONF_STUB) with pytest.raises(exceptions.ServiceManagerSetupError): sm.setup()
def test_setup_should_exec_18_commands(self, mock_subprocess, *args): """Setup valid package.""" sm = WinSvcManagerNSSM(svc_conf=CONF_STUB) sm.setup() assert mock_subprocess.call_count == 18
def test_subprocess_error_should_fail(self, *args): """Setup package with valid but not non executable configuration.""" sm = WinSvcManagerNSSM(svc_conf=CONF_STUB) with pytest.raises(exceptions.ServiceManagerCommandError): sm.setup()
class Package: """Package manager.""" def __init__(self, pkg_id: PackageId = None, istor_nodes: IStorNodes = None, cluster_conf: dict = None, extra_context: dict = None, manifest: PackageManifest = None): """Constructor. :param pkg_id: PackageId, package ID :param istor_nodes: IStorNodes, DC/OS installation storage nodes (set of pathlib.Path objects) :param cluster_conf: dict, configparser.ConfigParser.read_dict() compatible data. DC/OS cluster setup parameters :param extra_context: dict, extra 'key=value' data to be added to the resource rendering context :param manifest: PackageManifest, DC/OS package manifest object """ self.msg_src = self.__class__.__name__ if manifest is None: manifest = PackageManifest( pkg_id=pkg_id, istor_nodes=istor_nodes, cluster_conf=cluster_conf, extra_context=extra_context ) self.manifest = manifest self.cfg_manager = PkgConfManager(pkg_manifest=self.manifest) if self.manifest.pkg_extcfg: self.ext_manager = PkgInstExtrasManager(pkg_manifest=self.manifest) else: self.ext_manager = None LOG.debug(f'{self.msg_src}: {self.manifest.pkg_id.pkg_id}:' f' Installation extras manager: {self.ext_manager}') if self.manifest.pkg_svccfg: self.svc_manager = WinSvcManagerNSSM( svc_conf=self.manifest.pkg_svccfg ) else: self.svc_manager = None LOG.debug(f'{self.msg_src}: {self.manifest.pkg_id.pkg_id}:' f' Service manager: {self.svc_manager}') @property def id(self): """""" return self.manifest.pkg_id def handle_config_setup(self, mheading: str=None): """Execute steps on setting up package configuration files. :param mheading: str, descriptive heading to be added to error/log messages """ mhd_base = f'{self.msg_src}: {self.id.pkg_name}' mheading = f'{mheading}: {mhd_base}' if mheading else mhd_base LOG.debug(f'{mheading}: Setup configuration: ...') try: self.cfg_manager.setup_conf() except cfgm_exc.PkgConfNotFoundError as e: LOG.debug(f'{mheading}: Setup configuration: NOP') else: LOG.debug(f'{mheading}: Setup configuration: OK') def handle_inst_extras(self, mheading: str=None): """Process package's extra installation options. :param mheading: str, descriptive heading to be added to error/log messages """ mhd_base = f'{self.msg_src}: {self.id.pkg_name}' mheading = f'{mheading}: {mhd_base}' if mheading else mhd_base if self.ext_manager: LOG.debug(f'{mheading}: Handle extra installation options: ...') self.ext_manager.handle_install_extras() LOG.debug(f'{mheading}: Handle extra installation options: OK') else: LOG.debug(f'{mheading}: Handle extra installation options: NOP') def handle_uninst_extras(self, mheading: str=None): """Process package's extra uninstall options. :param mheading: str, descriptive heading to be added to error/log messages """ mhd_base = f'{self.msg_src}: {self.id.pkg_name}' mheading = f'{mheading}: {mhd_base}' if mheading else mhd_base if self.ext_manager: LOG.debug(f'{mheading}: Handle extra uninstall options: ...') self.ext_manager.handle_uninstall_extras() LOG.debug(f'{mheading}: Handle extra uninstall options: OK') else: LOG.debug(f'{mheading}: Handle extra uninstall options: NOP') def handle_svc_setup(self, mheading: str=None): """Execute steps on package's service setup. :param mheading: str, descriptive heading to be added to error/log messages """ mhd_base = f'{self.msg_src}: {self.id.pkg_name}' mheading = f'{mheading}: {mhd_base}' if mheading else mhd_base no_svc_marker = 'service does not exist as an installed service.' if self.svc_manager: svc_name = self.svc_manager.svc_name LOG.debug(f'{mheading}: Setup service: {svc_name}: ...') try: ret_code, stdout, stderr = self.svc_manager.status() except svcm_exc.ServiceManagerCommandError as e: exc_msg_line = str(e).replace('\n', '').strip() LOG.debug(f'{mheading}: Setup service: {svc_name}:' f' Get initial service status: {exc_msg_line}') if not exc_msg_line.endswith(no_svc_marker): err_msg = f'{mheading}: Setup service: {svc_name}:' \ f' Get initial service status: {e}' raise svcm_exc.ServiceSetupError(err_msg) from e # Setup a service try: self.svc_manager.setup() except svcm_exc.ServiceManagerCommandError as e: err_msg = f'{mheading}: Setup service: {svc_name}: {e}' raise svcm_exc.ServiceSetupError(err_msg) from e else: LOG.debug( f'{mheading}: Setup service: {svc_name}: Get initial' f' service status: stdout[{stdout}] stderr[{stderr}]' ) svc_status = str(stdout).strip().rstrip('\n') # Wipe existing service try: if svc_status != SVC_STATUS.STOPPED: self.svc_manager.stop() self.svc_manager.remove() LOG.debug(f'{mheading}: Wipe existing service:' f' {svc_name}: OK') except svcm_exc.ServiceManagerCommandError as e: err_msg = (f'{mheading}: Wipe existing service:' f' {svc_name}: {e}') raise svcm_exc.ServiceSetupError(err_msg) from e # Setup a replacement service try: self.svc_manager.setup() ret_code, stdout, stderr = (self.svc_manager.status()) svc_status = str(stdout).strip().rstrip('\n') except svcm_exc.ServiceManagerCommandError as e: err_msg = (f'{mheading}: Setup replacement service:' f' {svc_name}: {e}') raise svcm_exc.ServiceSetupError(err_msg) from e else: if svc_status != SVC_STATUS.STOPPED: err_msg = ( f'{mheading}: Setup replacement service:' f' {svc_name}: Invalid status: {svc_status}' ) raise svcm_exc.ServiceSetupError(err_msg) LOG.debug(f'{mheading}: Setup service: {svc_name}: OK') else: LOG.debug(f'{mheading}: Setup service: NOP') def handle_svc_wipe(self, mheading: str=None): """Execute steps on package's service wipe off. :param mheading: str, descriptive heading to be added to error/log messages """ mhd_base = f'{self.msg_src}: {self.id.pkg_name}' mheading = f'{mheading}: {mhd_base}' if mheading else mhd_base no_svc_marker = 'service does not exist as an installed service.' if self.svc_manager: svc_name = self.svc_manager.svc_name LOG.debug(f'{mheading}: Wipe service: {svc_name}: ...') try: ret_code, stdout, stderr = self.svc_manager.status() except svcm_exc.ServiceManagerCommandError as e: exc_msg_line = str(e).replace('\n', '').strip() LOG.debug(f'{mheading}: Wipe service: {svc_name}:' f' Get initial service status: {exc_msg_line}') if not exc_msg_line.endswith(no_svc_marker): err_msg = f'{mheading}: Wipe service: {svc_name}: {e}' raise svcm_exc.ServiceWipeError(err_msg) from e LOG.debug(f'{mheading}: Wipe service: NOP') else: LOG.debug( f'{mheading}: Wipe service: {svc_name}: Get initial' f' service status: stdout[{stdout}] stderr[{stderr}]' ) svc_status = str(stdout).strip().rstrip('\n') # Try to remove existing service try: if svc_status != SVC_STATUS.STOPPED: self.svc_manager.stop() self.svc_manager.remove() LOG.debug(f'{mheading}: Wipe service: {svc_name}: OK') except svcm_exc.ServiceManagerCommandError as e: err_msg = (f'{mheading}: Wipe existing service:' f' {svc_name}: {e}') raise svcm_exc.ServiceWipeError(err_msg) from e else: LOG.debug(f'{mheading}: Wipe service: NOP') def handle_svc_start(self, mheading: str=None): """Execute steps on package's service start. :param mheading: str, descriptive heading to be added to error/log messages """ mhd_base = f'{self.msg_src}: {self.id.pkg_name}' mheading = f'{mheading}: {mhd_base}' if mheading else mhd_base no_svc_marker = 'service does not exist as an installed service.' # TODO: Move stuff from the CmdStart.service_start() method here. # Overall design of this method should resemble one of the # Package.handle_svc_stop() method. def handle_svc_stop(self, mheading: str=None): """Execute steps on package's service stop. :param mheading: str, descriptive heading to be added to error/log messages """ mhd_base = f'{self.msg_src}: {self.id.pkg_name}' mheading = f'{mheading}: {mhd_base}' if mheading else mhd_base no_svc_marker = 'service does not exist as an installed service.' if self.svc_manager: svc_name = self.svc_manager.svc_name LOG.debug(f'{mheading}: Stop service: {svc_name}: ...') try: ret_code, stdout, stderr = self.svc_manager.status() except svcm_exc.ServiceManagerCommandError as e: exc_msg_line = str(e).replace('\n', '').strip() LOG.debug(f'{mheading}: Stop service: {svc_name}:' f' Get initial service status: {exc_msg_line}') if not exc_msg_line.endswith(no_svc_marker): err_msg = f'{mheading}: Stop service: {svc_name}: {e}' raise svcm_exc.ServiceStopError(err_msg) from e LOG.debug(f'{mheading}: Stop service: NOP') else: LOG.debug( f'{mheading}: Stop service: {svc_name}: Get initial' f' service status: stdout[{stdout}] stderr[{stderr}]' ) svc_status = str(stdout).strip().rstrip('\n') # Stop existing service try: if svc_status != SVC_STATUS.STOPPED: self.svc_manager.stop() LOG.debug(f'{mheading}: Stop service: {svc_name}: OK') except svcm_exc.ServiceManagerCommandError as e: err_msg = f'{mheading}: Stop service: {svc_name}: {e}' raise svcm_exc.ServiceStopError(err_msg) from e else: LOG.debug(f'{mheading}: Stop service: NOP') def handle_vardata_wipe(self, mheading: str=None): """Execute steps on wiping of package's variable data. :param mheading: str, descriptive heading to be added to error/log messages """ mhd_base = f'{self.msg_src}: {self.id.pkg_name}' mheading = f'{mheading}: {mhd_base}' if mheading else mhd_base work_dpath = getattr(self.manifest.istor_nodes, ISTOR_NODE.WORK) run_dpath = getattr(self.manifest.istor_nodes, ISTOR_NODE.RUN) for host_dpath in work_dpath, run_dpath: dpath = host_dpath.joinpath(self.id.pkg_name) try: cm_utl.rmdir(str(dpath), recursive=True) dpath.mkdir(parents=True, exist_ok=True) LOG.debug(f'{mheading}: Wipe variable data: {dpath}: OK') except (OSError, RuntimeError) as e: err_msg = (f'{mheading}: Wipe variable data: {dpath}:' f' {type(e).__name__}: {e}') raise cr_exc.RCError(err_msg) from e def save_manifest(self, mheading: str=None, dpath: Path=None): """Save package's manifest to filesystem. :param mheading: str, descriptive heading to be added to error/log messages :param dpath: Path, absolute path to the host directory where to save to """ mhd_base = f'{self.msg_src}: {self.id.pkg_name}' mheading = f'{mheading}: {mhd_base}' if mheading else mhd_base try: cm_utl.rmdir(str(dpath), recursive=True) dpath.mkdir(parents=True) self.manifest.save(dpath) except (OSError, RuntimeError, cr_exc.RCError) as e: err_msg = f'{mheading}: Register package: {self.id}: {e}' raise cr_exc.RCError(err_msg) from e def delete_manifest(self, mheading: str=None, dpath: Path=None): """Delete package's manifest from filesystem. :param mheading: str, descriptive heading to be added to error/log messages :param dpath: Path, absolute path to the host directory where to delete from """ mhd_base = f'{self.msg_src}: {self.id.pkg_name}' mheading = f'{mheading}: {mhd_base}' if mheading else mhd_base try: self.manifest.delete(dpath) except (OSError, RuntimeError, cr_exc.RCError) as e: err_msg = f'{mheading}: Deregister package: {self.id}: {e}' raise cr_exc.RCError(err_msg) from e