The schema is written in Python to reduce duplication. When configuration is loaded, the validation checks if it's correct and fills in default values. There is a custom extension to the schema to report deprecated options. The config dependencies are implemented as a separate pass. While it's technically possible to express the dependencies in the schema itself, the error messages are not very helpful and it makes the schema much harder to read. Phases no longer define `config_options`. New options should be added to the schema. Since the default values are populated automatically during validation, there is no need to duplicate them into the code. The `pungi-config-validate` script is updated to use the schema and report errors even for deeply nested fields. The dependencies are updated: pungi now depends on `python-jsonschema` (which is already available in Fedora). Signed-off-by: Lubomír Sedlář <lsedlar@redhat.com>
148 lines
5.0 KiB
Python
148 lines
5.0 KiB
Python
# -*- coding: utf-8 -*-
|
|
|
|
|
|
# This program is free software; you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation; version 2 of the License.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU Library General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program; if not, write to the Free Software
|
|
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
|
|
|
|
from pungi import util
|
|
|
|
|
|
class PhaseBase(object):
|
|
|
|
def __init__(self, compose):
|
|
self.compose = compose
|
|
self.msg = "---------- PHASE: %s ----------" % self.name.upper()
|
|
self.finished = False
|
|
self._skipped = False
|
|
|
|
def validate(self):
|
|
pass
|
|
|
|
def conf_assert_str(self, name):
|
|
missing = []
|
|
invalid = []
|
|
if name not in self.compose.conf:
|
|
missing.append(name)
|
|
elif not isinstance(self.compose.conf[name], str):
|
|
invalid.append(name, type(self.compose.conf[name]), str)
|
|
return missing, invalid
|
|
|
|
def skip(self):
|
|
if self._skipped:
|
|
return True
|
|
if self.compose.just_phases and self.name not in self.compose.just_phases:
|
|
return True
|
|
if self.name in self.compose.skip_phases:
|
|
return True
|
|
if self.name in self.compose.conf["skip_phases"]:
|
|
return True
|
|
return False
|
|
|
|
def start(self):
|
|
self._skipped = self.skip()
|
|
if self._skipped:
|
|
self.compose.log_warning("[SKIP ] %s" % self.msg)
|
|
self.finished = True
|
|
return
|
|
self.compose.log_info("[BEGIN] %s" % self.msg)
|
|
self.compose.notifier.send('phase-start', phase_name=self.name)
|
|
self.run()
|
|
|
|
def stop(self):
|
|
if self.finished:
|
|
return
|
|
if hasattr(self, "pool"):
|
|
self.pool.stop()
|
|
self.finished = True
|
|
self.compose.log_info("[DONE ] %s" % self.msg)
|
|
self.compose.notifier.send('phase-stop', phase_name=self.name)
|
|
|
|
def run(self):
|
|
raise NotImplementedError
|
|
|
|
|
|
class ConfigGuardedPhase(PhaseBase):
|
|
"""A phase that is skipped unless config option is set."""
|
|
|
|
def skip(self):
|
|
if super(ConfigGuardedPhase, self).skip():
|
|
return True
|
|
if not self.compose.conf.get(self.name):
|
|
self.compose.log_info("Config section '%s' was not found. Skipping." % self.name)
|
|
return True
|
|
return False
|
|
|
|
|
|
class ImageConfigMixin(object):
|
|
"""
|
|
A mixin for phase that needs to access image related settings: ksurl,
|
|
version, target and release.
|
|
|
|
First, it checks config object given as argument, then it checks
|
|
phase-level configuration and finally falls back to global configuration.
|
|
"""
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super(ImageConfigMixin, self).__init__(*args, **kwargs)
|
|
self._phase_ksurl = None
|
|
|
|
def get_config(self, cfg, opt):
|
|
return cfg.get(
|
|
opt, self.compose.conf.get(
|
|
'%s_%s' % (self.name, opt), self.compose.conf.get(
|
|
'global_%s' % opt)))
|
|
|
|
def get_release(self, cfg):
|
|
"""
|
|
If release is set explicitly to None, replace it with date and respin.
|
|
Uses configuration passed as argument, phase specific settings and
|
|
global settings.
|
|
"""
|
|
for key, conf in [('release', cfg),
|
|
('%s_release' % self.name, self.compose.conf),
|
|
('global_release', self.compose.conf)]:
|
|
if key in conf:
|
|
return conf[key] or self.compose.image_release
|
|
return None
|
|
|
|
def get_ksurl(self, cfg):
|
|
"""
|
|
Get ksurl from `cfg`. If not present, fall back to phase defined one or
|
|
global one.
|
|
"""
|
|
if 'ksurl' in cfg:
|
|
return util.resolve_git_url(cfg['ksurl'])
|
|
if '%s_ksurl' % self.name in self.compose.conf:
|
|
return self.phase_ksurl
|
|
if 'global_ksurl' in self.compose.conf:
|
|
return self.global_ksurl
|
|
return None
|
|
|
|
@property
|
|
def phase_ksurl(self):
|
|
"""Get phase level ksurl, making sure to resolve it only once."""
|
|
# The phase-level setting is cached as instance attribute of the phase.
|
|
if not self._phase_ksurl:
|
|
ksurl = self.compose.conf.get('%s_ksurl' % self.name)
|
|
self._phase_ksurl = util.resolve_git_url(ksurl)
|
|
return self._phase_ksurl
|
|
|
|
@property
|
|
def global_ksurl(self):
|
|
"""Get global ksurl setting, making sure to resolve it only once."""
|
|
# The global setting is cached in the configuration object.
|
|
if 'private_global_ksurl' not in self.compose.conf:
|
|
ksurl = self.compose.conf.get('global_ksurl')
|
|
self.compose.conf['private_global_ksurl'] = util.resolve_git_url(ksurl)
|
|
return self.compose.conf['private_global_ksurl']
|