Sign Up
Log In
Log In
or
Sign Up
Places
All Projects
Status Monitor
Collapse sidebar
SUSE:SLE-12:Update
salt.2353
0029-Make-use-of-checksum-configurable-defaults...
Overview
Repositories
Revisions
Requests
Users
Attributes
Meta
File 0029-Make-use-of-checksum-configurable-defaults-to-MD5-SH.patch of Package salt.2353
From 2220c5a0ae800988bf83c39b458a8747f01186c0 Mon Sep 17 00:00:00 2001 From: Bo Maryniuk <bo@suse.de> Date: Fri, 12 Feb 2016 16:16:12 +0100 Subject: [PATCH 29/29] Make use of checksum configurable (defaults to MD5, SHA256 suggested) Set config hash_type to SHA1 Set default hash as SHA1 in config and explain why. Use hash_type configuration for the Cloud Use configurable hash_type for general Key fingerprinting Use SHA1 hash by default Use SHA1 hash by default in Tomcat module, refactor for support different algorithms Use SHA1 by default instead of MD5 Remove SHA1 to SHA265 by default Add note to the Tomcat module for SHA256 Remove sha1 to sha265 Remove SHA1 for SHA256 Remove SHA1 in favor of SHA256 Use MD5 hash algorithm by default (until deprecated) Create a mixin class that will be reused in the similar instances (daemons) Use mixin for the daemon classes Report environment failure, if any Verify if hash_type is using vulnerable algorithms Standardize logging Add daemons unit test to verify hash_type settings Fix PyLint --- conf/master | 5 +- conf/minion | 8 +- conf/proxy | 9 +- salt/cli/daemons.py | 83 +++++++++++++----- salt/cloud/__init__.py | 4 +- salt/crypt.py | 10 +-- salt/key.py | 4 +- salt/modules/key.py | 10 +-- salt/modules/tomcat.py | 26 ++---- salt/modules/win_file.py | 2 +- salt/utils/__init__.py | 13 +-- salt/utils/cloud.py | 3 +- tests/unit/daemons_test.py | 209 +++++++++++++++++++++++++++++++++++++++++++++ 13 files changed, 319 insertions(+), 67 deletions(-) create mode 100644 tests/unit/daemons_test.py diff --git a/conf/master b/conf/master index 36657e8..cf05ec4 100644 --- a/conf/master +++ b/conf/master @@ -466,9 +466,12 @@ syndic_user: salt #default_top: base # The hash_type is the hash to use when discovering the hash of a file on -# the master server. The default is md5, but sha1, sha224, sha256, sha384 +# the master server. The default is md5 but sha1, sha224, sha256, sha384 # and sha512 are also supported. # +# WARNING: While md5 is supported, do not use it due to the high chance +# of possible collisions and thus security breach. +# # Prior to changing this value, the master should be stopped and all Salt # caches should be cleared. #hash_type: md5 diff --git a/conf/minion b/conf/minion index 2307f70..e17ec61 100644 --- a/conf/minion +++ b/conf/minion @@ -440,12 +440,14 @@ #fileserver_limit_traversal: False # The hash_type is the hash to use when discovering the hash of a file in -# the local fileserver. The default is md5, but sha1, sha224, sha256, sha384 -# and sha512 are also supported. +# the local fileserver. The default is sha256, sha224, sha384 and sha512 are also supported. +# +# WARNING: While md5 and sha1 are also supported, do not use it due to the high chance +# of possible collisions and thus security breach. # # Warning: Prior to changing this value, the minion should be stopped and all # Salt caches should be cleared. -#hash_type: md5 +#hash_type: sha256 # The Salt pillar is searched for locally if file_client is set to local. If # this is the case, and pillar data is defined, then the pillar_roots need to diff --git a/conf/proxy b/conf/proxy index 472df35..0de6af8 100644 --- a/conf/proxy +++ b/conf/proxy @@ -419,12 +419,15 @@ #fileserver_limit_traversal: False # The hash_type is the hash to use when discovering the hash of a file in -# the local fileserver. The default is md5, but sha1, sha224, sha256, sha384 -# and sha512 are also supported. +# the local fileserver. The default is sha256 but sha224, sha384 and sha512 +# are also supported. +# +# WARNING: While md5 and sha1 are also supported, do not use it due to the high chance +# of possible collisions and thus security breach. # # Warning: Prior to changing this value, the minion should be stopped and all # Salt caches should be cleared. -#hash_type: md5 +#hash_type: sha256 # The Salt pillar is searched for locally if file_client is set to local. If # this is the case, and pillar data is defined, then the pillar_roots need to diff --git a/salt/cli/daemons.py b/salt/cli/daemons.py index 7f8b8c8..b0e7b20 100644 --- a/salt/cli/daemons.py +++ b/salt/cli/daemons.py @@ -58,7 +58,50 @@ from salt.exceptions import SaltSystemExit logger = salt.log.setup.logging.getLogger(__name__) -class Master(parsers.MasterOptionParser): +class DaemonsMixin(object): # pylint: disable=no-init + ''' + Uses the same functions for all daemons + ''' + def verify_hash_type(self): + ''' + Verify and display a nag-messsage to the log if vulnerable hash-type is used. + + :return: + ''' + if self.config['hash_type'].lower() in ['md5', 'sha1']: + logger.warning('IMPORTANT: Do not use {h_type} hashing algorithm! Please set "hash_type" to ' + 'SHA256 in Salt {d_name} config!'.format( + h_type=self.config['hash_type'], d_name=self.__class__.__name__)) + + def start_log_info(self): + ''' + Say daemon starting. + + :return: + ''' + logger.info('The Salt {d_name} is starting up'.format(d_name=self.__class__.__name__)) + + def shutdown_log_info(self): + ''' + Say daemon shutting down. + + :return: + ''' + logger.info('The Salt {d_name} is shut down'.format(d_name=self.__class__.__name__)) + + def environment_failure(self, error): + ''' + Log environment failure for the daemon and exit with the error code. + + :param error: + :return: + ''' + logger.exception('Failed to create environment for {d_name}: {reason}'.format( + d_name=self.__class__.__name__, reason=error.message)) + sys.exit(error.errno) + + +class Master(parsers.MasterOptionParser, DaemonsMixin): # pylint: disable=no-init ''' Creates a master server ''' @@ -114,8 +157,7 @@ class Master(parsers.MasterOptionParser): for syndic_file in os.listdir(self.config['syndic_dir']): os.remove(os.path.join(self.config['syndic_dir'], syndic_file)) except OSError as err: - logger.exception('Failed to prepare salt environment') - sys.exit(err.errno) + self.environment_failure(err) self.setup_logfile_logger() verify_log(self.config) @@ -153,17 +195,18 @@ class Master(parsers.MasterOptionParser): ''' self.prepare() if check_user(self.config['user']): - logger.info('The salt master is starting up') + self.verify_hash_type() + self.start_log_info() self.master.start() def shutdown(self): ''' If sub-classed, run any shutdown operations on this method. ''' - logger.info('The salt master is shut down') + self.shutdown_log_info() -class Minion(parsers.MinionOptionParser): # pylint: disable=no-init +class Minion(parsers.MinionOptionParser, DaemonsMixin): # pylint: disable=no-init ''' Create a minion server ''' @@ -226,8 +269,7 @@ class Minion(parsers.MinionOptionParser): # pylint: disable=no-init verify_files([logfile], self.config['user']) os.umask(current_umask) except OSError as err: - logger.exception('Failed to prepare salt environment') - sys.exit(err.errno) + self.environment_failure(err) self.setup_logfile_logger() verify_log(self.config) @@ -273,7 +315,8 @@ class Minion(parsers.MinionOptionParser): # pylint: disable=no-init try: self.prepare() if check_user(self.config['user']): - logger.info('The salt minion is starting up') + self.verify_hash_type() + self.start_log_info() self.minion.tune_in() finally: self.shutdown() @@ -310,10 +353,10 @@ class Minion(parsers.MinionOptionParser): # pylint: disable=no-init ''' If sub-classed, run any shutdown operations on this method. ''' - logger.info('The salt minion is shut down') + self.shutdown_log_info() -class ProxyMinion(parsers.ProxyMinionOptionParser): # pylint: disable=no-init +class ProxyMinion(parsers.ProxyMinionOptionParser, DaemonsMixin): # pylint: disable=no-init ''' Create a proxy minion server ''' @@ -388,8 +431,7 @@ class ProxyMinion(parsers.ProxyMinionOptionParser): # pylint: disable=no-init os.umask(current_umask) except OSError as err: - logger.exception('Failed to prepare salt environment') - sys.exit(err.errno) + self.environment_failure(err) self.setup_logfile_logger() verify_log(self.config) @@ -431,7 +473,8 @@ class ProxyMinion(parsers.ProxyMinionOptionParser): # pylint: disable=no-init try: self.prepare() if check_user(self.config['user']): - logger.info('The proxy minion is starting up') + self.verify_hash_type() + self.start_log_info() self.minion.tune_in() except (KeyboardInterrupt, SaltSystemExit) as exc: logger.warn('Stopping the Salt Proxy Minion') @@ -449,10 +492,10 @@ class ProxyMinion(parsers.ProxyMinionOptionParser): # pylint: disable=no-init if hasattr(self, 'minion') and 'proxymodule' in self.minion.opts: proxy_fn = self.minion.opts['proxymodule'].loaded_base_name + '.shutdown' self.minion.opts['proxymodule'][proxy_fn](self.minion.opts) - logger.info('The proxy minion is shut down') + self.shutdown_log_info() -class Syndic(parsers.SyndicOptionParser): +class Syndic(parsers.SyndicOptionParser, DaemonsMixin): # pylint: disable=no-init ''' Create a syndic server ''' @@ -488,8 +531,7 @@ class Syndic(parsers.SyndicOptionParser): verify_files([logfile], self.config['user']) os.umask(current_umask) except OSError as err: - logger.exception('Failed to prepare salt environment') - sys.exit(err.errno) + self.environment_failure(err) self.setup_logfile_logger() verify_log(self.config) @@ -521,7 +563,8 @@ class Syndic(parsers.SyndicOptionParser): ''' self.prepare() if check_user(self.config['user']): - logger.info('The salt syndic is starting up') + self.verify_hash_type() + self.start_log_info() try: self.syndic.tune_in() except KeyboardInterrupt: @@ -532,4 +575,4 @@ class Syndic(parsers.SyndicOptionParser): ''' If sub-classed, run any shutdown operations on this method. ''' - logger.info('The salt syndic is shut down') + self.shutdown_log_info() diff --git a/salt/cloud/__init__.py b/salt/cloud/__init__.py index 77186a4..733b403 100644 --- a/salt/cloud/__init__.py +++ b/salt/cloud/__init__.py @@ -2036,7 +2036,7 @@ class Map(Cloud): master_temp_pub = salt.utils.mkstemp() with salt.utils.fopen(master_temp_pub, 'w') as mtp: mtp.write(pub) - master_finger = salt.utils.pem_finger(master_temp_pub) + master_finger = salt.utils.pem_finger(master_temp_pub, sum_type=self.opts['hash_type']) os.unlink(master_temp_pub) if master_profile.get('make_minion', True) is True: @@ -2121,7 +2121,7 @@ class Map(Cloud): # mitigate man-in-the-middle attacks master_pub = os.path.join(self.opts['pki_dir'], 'master.pub') if os.path.isfile(master_pub): - master_finger = salt.utils.pem_finger(master_pub) + master_finger = salt.utils.pem_finger(master_pub, sum_type=self.opts['hash_type']) opts = self.opts.copy() if self.opts['parallel']: diff --git a/salt/crypt.py b/salt/crypt.py index 907ec0c..eaf6d72 100644 --- a/salt/crypt.py +++ b/salt/crypt.py @@ -558,11 +558,11 @@ class AsyncAuth(object): if self.opts.get('syndic_master', False): # Is syndic syndic_finger = self.opts.get('syndic_finger', self.opts.get('master_finger', False)) if syndic_finger: - if salt.utils.pem_finger(m_pub_fn) != syndic_finger: + if salt.utils.pem_finger(m_pub_fn, sum_type=self.opts['hash_type']) != syndic_finger: self._finger_fail(syndic_finger, m_pub_fn) else: if self.opts.get('master_finger', False): - if salt.utils.pem_finger(m_pub_fn) != self.opts['master_finger']: + if salt.utils.pem_finger(m_pub_fn, sum_type=self.opts['hash_type']) != self.opts['master_finger']: self._finger_fail(self.opts['master_finger'], m_pub_fn) auth['publish_port'] = payload['publish_port'] raise tornado.gen.Return(auth) @@ -1071,11 +1071,11 @@ class SAuth(AsyncAuth): if self.opts.get('syndic_master', False): # Is syndic syndic_finger = self.opts.get('syndic_finger', self.opts.get('master_finger', False)) if syndic_finger: - if salt.utils.pem_finger(m_pub_fn) != syndic_finger: + if salt.utils.pem_finger(m_pub_fn, sum_type=self.opts['hash_type']) != syndic_finger: self._finger_fail(syndic_finger, m_pub_fn) else: if self.opts.get('master_finger', False): - if salt.utils.pem_finger(m_pub_fn) != self.opts['master_finger']: + if salt.utils.pem_finger(m_pub_fn, sum_type=self.opts['hash_type']) != self.opts['master_finger']: self._finger_fail(self.opts['master_finger'], m_pub_fn) auth['publish_port'] = payload['publish_port'] return auth @@ -1089,7 +1089,7 @@ class SAuth(AsyncAuth): 'this minion is not subject to a man-in-the-middle attack.' .format( finger, - salt.utils.pem_finger(master_key) + salt.utils.pem_finger(master_key, sum_type=self.opts['hash_type']) ) ) sys.exit(42) diff --git a/salt/key.py b/salt/key.py index 08086a0..e4cb4eb 100644 --- a/salt/key.py +++ b/salt/key.py @@ -933,7 +933,7 @@ class Key(object): path = os.path.join(self.opts['pki_dir'], key) else: path = os.path.join(self.opts['pki_dir'], status, key) - ret[status][key] = salt.utils.pem_finger(path) + ret[status][key] = salt.utils.pem_finger(path, sum_type=self.opts['hash_type']) return ret def finger_all(self): @@ -948,7 +948,7 @@ class Key(object): path = os.path.join(self.opts['pki_dir'], key) else: path = os.path.join(self.opts['pki_dir'], status, key) - ret[status][key] = salt.utils.pem_finger(path) + ret[status][key] = salt.utils.pem_finger(path, sum_type=self.opts['hash_type']) return ret diff --git a/salt/modules/key.py b/salt/modules/key.py index 12762df..3e16c2d 100644 --- a/salt/modules/key.py +++ b/salt/modules/key.py @@ -21,9 +21,8 @@ def finger(): salt '*' key.finger ''' - return salt.utils.pem_finger( - os.path.join(__opts__['pki_dir'], 'minion.pub') - ) + return salt.utils.pem_finger(os.path.join(__opts__['pki_dir'], 'minion.pub'), + sum_type=__opts__.get('hash_type', 'md5')) def finger_master(): @@ -36,6 +35,5 @@ def finger_master(): salt '*' key.finger_master ''' - return salt.utils.pem_finger( - os.path.join(__opts__['pki_dir'], 'minion_master.pub') - ) + return salt.utils.pem_finger(os.path.join(__opts__['pki_dir'], 'minion_master.pub'), + sum_type=__opts__.get('hash_type', 'md5')) diff --git a/salt/modules/tomcat.py b/salt/modules/tomcat.py index d3df2dc..4a7f0eb 100644 --- a/salt/modules/tomcat.py +++ b/salt/modules/tomcat.py @@ -610,7 +610,7 @@ def deploy_war(war, def passwd(passwd, user='', - alg='md5', + alg='sha1', realm=None): ''' This function replaces the $CATALINA_HOME/bin/digest.sh script @@ -625,23 +625,15 @@ def passwd(passwd, salt '*' tomcat.passwd secret tomcat sha1 salt '*' tomcat.passwd secret tomcat sha1 'Protected Realm' ''' - if alg == 'md5': - m = hashlib.md5() - elif alg == 'sha1': - m = hashlib.sha1() - else: - return False - - if realm: - m.update('{0}:{1}:{2}'.format( - user, - realm, - passwd, - )) - else: - m.update(passwd) + # Shouldn't it be SHA265 instead of SHA1? + digest = hasattr(hashlib, alg) and getattr(hashlib, alg) or None + if digest: + if realm: + digest.update('{0}:{1}:{2}'.format(user, realm, passwd,)) + else: + digest.update(passwd) - return m.hexdigest() + return digest and digest.hexdigest() or False # Non-Manager functions diff --git a/salt/modules/win_file.py b/salt/modules/win_file.py index 7911bfc..5ea31ae 100644 --- a/salt/modules/win_file.py +++ b/salt/modules/win_file.py @@ -842,7 +842,7 @@ def chgrp(path, group): return None -def stats(path, hash_type='md5', follow_symlinks=True): +def stats(path, hash_type='sha256', follow_symlinks=True): ''' Return a dict containing the stats for a given file diff --git a/salt/utils/__init__.py b/salt/utils/__init__.py index c6a3fd3..4e40caf 100644 --- a/salt/utils/__init__.py +++ b/salt/utils/__init__.py @@ -858,10 +858,11 @@ def path_join(*parts): )) -def pem_finger(path=None, key=None, sum_type='md5'): +def pem_finger(path=None, key=None, sum_type='sha256'): ''' Pass in either a raw pem string, or the path on disk to the location of a - pem file, and the type of cryptographic hash to use. The default is md5. + pem file, and the type of cryptographic hash to use. The default is SHA256. + The fingerprint of the pem will be returned. If neither a key nor a path are passed in, a blank string will be returned. @@ -1979,7 +1980,7 @@ def safe_walk(top, topdown=True, onerror=None, followlinks=True, _seen=None): yield top, dirs, nondirs -def get_hash(path, form='md5', chunk_size=65536): +def get_hash(path, form='sha256', chunk_size=65536): ''' Get the hash sum of a file @@ -1989,10 +1990,10 @@ def get_hash(path, form='md5', chunk_size=65536): ``get_sum`` cannot really be trusted since it is vulnerable to collisions: ``get_sum(..., 'xyz') == 'Hash xyz not supported'`` ''' - try: - hash_type = getattr(hashlib, form) - except (AttributeError, TypeError): + hash_type = hasattr(hashlib, form) and getattr(hashlib, form) or None + if hash_type is None: raise ValueError('Invalid hash type: {0}'.format(form)) + with salt.utils.fopen(path, 'rb') as ifile: hash_obj = hash_type() # read the file in in chunks, not the entire file diff --git a/salt/utils/cloud.py b/salt/utils/cloud.py index d546e51..7a21166 100644 --- a/salt/utils/cloud.py +++ b/salt/utils/cloud.py @@ -2421,6 +2421,7 @@ def init_cachedir(base=None): def request_minion_cachedir( minion_id, + opts=None, fingerprint='', pubkey=None, provider=None, @@ -2440,7 +2441,7 @@ def request_minion_cachedir( if not fingerprint: if pubkey is not None: - fingerprint = salt.utils.pem_finger(key=pubkey) + fingerprint = salt.utils.pem_finger(key=pubkey, sum_type=(opts and opts.get('hash_type') or 'sha256')) init_cachedir(base) diff --git a/tests/unit/daemons_test.py b/tests/unit/daemons_test.py new file mode 100644 index 0000000..47d5e8a --- /dev/null +++ b/tests/unit/daemons_test.py @@ -0,0 +1,209 @@ +# -*- coding: utf-8 -*- +''' + :codeauthor: :email:`Bo Maryniuk <bo@suse.de>` +''' + +# Import python libs +from __future__ import absolute_import + +# Import Salt Testing libs +from salttesting import TestCase, skipIf +from salttesting.helpers import ensure_in_syspath +from salttesting.mock import patch, MagicMock, NO_MOCK, NO_MOCK_REASON + +ensure_in_syspath('../') + +# Import Salt libs +import integration +from salt.cli import daemons + + +class LoggerMock(object): + ''' + Logger data collector + ''' + + def __init__(self): + ''' + init + :return: + ''' + self.reset() + + def reset(self): + ''' + Reset values + + :return: + ''' + self.last_message = self.last_type = None + + def info(self, data): + ''' + Collects the data from the logger of info type. + + :param data: + :return: + ''' + self.last_message = data + self.last_type = 'info' + + def warning(self, data): + ''' + Collects the data from the logger of warning type. + + :param data: + :return: + ''' + self.last_message = data + self.last_type = 'warning' + + +@skipIf(NO_MOCK, NO_MOCK_REASON) +class DaemonsStarterTestCase(TestCase, integration.SaltClientTestCaseMixIn): + ''' + Unit test for the daemons starter classes. + ''' + + def test_master_daemon_hash_type_verified(self): + ''' + Verify if Master is verifying hash_type config option. + + :return: + ''' + def _create_master(): + ''' + Create master instance + :return: + ''' + master = daemons.Master() + master.config = {'user': 'dummy', 'hash_type': alg} + for attr in ['master', 'start_log_info', 'prepare']: + setattr(master, attr, MagicMock()) + + return master + + _logger = LoggerMock() + with patch('salt.cli.daemons.check_user', MagicMock(return_value=True)): + with patch('salt.cli.daemons.logger', _logger): + for alg in ['md5', 'sha1']: + _create_master().start() + self.assertEqual(_logger.last_type, 'warning') + self.assertTrue(_logger.last_message) + self.assertTrue(_logger.last_message.find('Do not use {alg}'.format(alg=alg)) > -1) + + _logger.reset() + + for alg in ['sha224', 'sha256', 'sha384', 'sha512']: + _create_master().start() + self.assertEqual(_logger.last_type, None) + self.assertFalse(_logger.last_message) + + def test_minion_daemon_hash_type_verified(self): + ''' + Verify if Minion is verifying hash_type config option. + + :return: + ''' + + def _create_minion(): + ''' + Create minion instance + :return: + ''' + obj = daemons.Minion() + obj.config = {'user': 'dummy', 'hash_type': alg} + for attr in ['minion', 'start_log_info', 'prepare', 'shutdown']: + setattr(obj, attr, MagicMock()) + + return obj + + _logger = LoggerMock() + with patch('salt.cli.daemons.check_user', MagicMock(return_value=True)): + with patch('salt.cli.daemons.logger', _logger): + for alg in ['md5', 'sha1']: + _create_minion().start() + self.assertEqual(_logger.last_type, 'warning') + self.assertTrue(_logger.last_message) + self.assertTrue(_logger.last_message.find('Do not use {alg}'.format(alg=alg)) > -1) + + _logger.reset() + + for alg in ['sha224', 'sha256', 'sha384', 'sha512']: + _create_minion().start() + self.assertEqual(_logger.last_type, None) + self.assertFalse(_logger.last_message) + + def test_proxy_minion_daemon_hash_type_verified(self): + ''' + Verify if ProxyMinion is verifying hash_type config option. + + :return: + ''' + + def _create_proxy_minion(): + ''' + Create proxy minion instance + :return: + ''' + obj = daemons.ProxyMinion() + obj.config = {'user': 'dummy', 'hash_type': alg} + for attr in ['minion', 'start_log_info', 'prepare', 'shutdown']: + setattr(obj, attr, MagicMock()) + + return obj + + _logger = LoggerMock() + with patch('salt.cli.daemons.check_user', MagicMock(return_value=True)): + with patch('salt.cli.daemons.logger', _logger): + for alg in ['md5', 'sha1']: + _create_proxy_minion().start() + self.assertEqual(_logger.last_type, 'warning') + self.assertTrue(_logger.last_message) + self.assertTrue(_logger.last_message.find('Do not use {alg}'.format(alg=alg)) > -1) + + _logger.reset() + + for alg in ['sha224', 'sha256', 'sha384', 'sha512']: + _create_proxy_minion().start() + self.assertEqual(_logger.last_type, None) + self.assertFalse(_logger.last_message) + + def test_syndic_daemon_hash_type_verified(self): + ''' + Verify if Syndic is verifying hash_type config option. + + :return: + ''' + + def _create_syndic(): + ''' + Create syndic instance + :return: + ''' + obj = daemons.Syndic() + obj.config = {'user': 'dummy', 'hash_type': alg} + for attr in ['syndic', 'start_log_info', 'prepare', 'shutdown']: + setattr(obj, attr, MagicMock()) + + return obj + + _logger = LoggerMock() + with patch('salt.cli.daemons.check_user', MagicMock(return_value=True)): + with patch('salt.cli.daemons.logger', _logger): + for alg in ['md5', 'sha1']: + _create_syndic().start() + self.assertEqual(_logger.last_type, 'warning') + self.assertTrue(_logger.last_message) + self.assertTrue(_logger.last_message.find('Do not use {alg}'.format(alg=alg)) > -1) + + _logger.reset() + + for alg in ['sha224', 'sha256', 'sha384', 'sha512']: + _create_syndic().start() + self.assertEqual(_logger.last_type, None) + self.assertFalse(_logger.last_message) + +if __name__ == '__main__': + from integration import run_tests + run_tests(DaemonsStarterTestCase, needs_daemon=False) -- 2.7.2
Locations
Projects
Search
Status Monitor
Help
OpenBuildService.org
Documentation
API Documentation
Code of Conduct
Contact
Support
@OBShq
Terms
openSUSE Build Service is sponsored by
The Open Build Service is an
openSUSE project
.
Sign Up
Log In
Places
Places
All Projects
Status Monitor