pungi/pungi/phases/base.py
Lubomír Sedlář f9a6c8418f Add JSON Schema for configuration
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>
2016-09-01 10:56:15 +02:00

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']