Merge #273 Deduplicate configuration a bit

This commit is contained in:
Dennis Gilmore 2016-04-29 12:26:51 +00:00
commit c80b7c6894
10 changed files with 544 additions and 95 deletions

View File

@ -735,6 +735,47 @@ Example
] ]
Common options for Live Images, Live Media and Image Build
==========================================================
All images can have ``ksurl``, ``version``, ``release`` and ``target``
specified. Since this can create a lot of duplication, there are global options
that can be used instead.
For each of the phases, if the option is not specified for a particular
deliverable, an option named ``<PHASE_NAME>_<OPTION>`` is checked. If that is
not specified either, the last fallback is ``global_<OPTION>``. If even that is
unset, the value is considered to not be specified.
The kickstart URL is configured by these options.
* ``global_ksurl`` -- global fallback setting
* ``live_media_ksurl``
* ``image_build_ksurl``
* ``live_images_ksurl``
Target is specified by these settings. For live images refer to ``live_target``.
* ``global_target`` -- global fallback setting
* ``live_media_target``
* ``image_build_target``
Version is specified by these options.
* ``global_version`` -- global fallback setting
* ``live_media_version``
* ``image_build_version``
* ``live_images_version``
Release is specified by these options. If set explicitly to ``None``, a value
will be generated based on date, compose type and respin.
* ``global_release`` -- global fallback setting
* ``live_media_release``
* ``image_build_release``
* ``live_images_release``
Live Images Settings Live Images Settings
==================== ====================

View File

@ -15,6 +15,7 @@
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
from pungi.checks import validate_options from pungi.checks import validate_options
from pungi import util
class PhaseBase(object): class PhaseBase(object):
@ -84,3 +85,67 @@ class ConfigGuardedPhase(PhaseBase):
self.compose.log_info("Config section '%s' was not found. Skipping." % self.name) self.compose.log_info("Config section '%s' was not found. Skipping." % self.name)
return True return True
return False 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(
'{}_{}'.format(self.name, opt), self.compose.conf.get(
'global_{}'.format(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),
('{}_release'.format(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 '{}_ksurl'.format(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('{}_ksurl'.format(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 '_global_ksurl' not in self.compose.conf:
ksurl = self.compose.conf.get('global_ksurl')
self.compose.conf['_global_ksurl'] = util.resolve_git_url(ksurl)
return self.compose.conf['_global_ksurl']

View File

@ -5,8 +5,8 @@ import os
import time import time
from kobo import shortcuts from kobo import shortcuts
from pungi.util import get_variant_data, resolve_git_url, makedirs, get_mtime, get_file_size, failable from pungi.util import get_variant_data, makedirs, get_mtime, get_file_size, failable
from pungi.phases.base import PhaseBase from pungi.phases import base
from pungi.linker import Linker from pungi.linker import Linker
from pungi.paths import translate_path from pungi.paths import translate_path
from pungi.wrappers.kojiwrapper import KojiWrapper from pungi.wrappers.kojiwrapper import KojiWrapper
@ -14,21 +14,41 @@ from kobo.threads import ThreadPool, WorkerThread
from productmd.images import Image from productmd.images import Image
class ImageBuildPhase(PhaseBase): class ImageBuildPhase(base.ImageConfigMixin, base.ConfigGuardedPhase):
"""class for wrapping up koji image-build""" """class for wrapping up koji image-build"""
name = "image_build" name = "image_build"
def __init__(self, compose): config_options = [
PhaseBase.__init__(self, compose) {
self.pool = ThreadPool(logger=self.compose._logger) "name": "image_build",
"expected_types": [dict],
"optional": True,
},
{
"name": "image_build_ksurl",
"expected_types": [str],
"optional": True,
},
{
"name": "image_build_target",
"expected_types": [str],
"optional": True,
},
{
"name": "image_build_release",
"expected_types": [str, type(None)],
"optional": True,
},
{
"name": "image_build_version",
"expected_types": [str],
"optional": True,
},
]
def skip(self): def __init__(self, compose):
if PhaseBase.skip(self): super(ImageBuildPhase, self).__init__(compose)
return True self.pool = ThreadPool(logger=self.compose._logger)
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
def _get_install_tree(self, image_conf, variant): def _get_install_tree(self, image_conf, variant):
""" """
@ -100,14 +120,20 @@ class ImageBuildPhase(PhaseBase):
continue continue
# Replace possible ambiguous ref name with explicit hash. # Replace possible ambiguous ref name with explicit hash.
if 'ksurl' in image_conf['image-build']: ksurl = self.get_ksurl(image_conf['image-build'])
image_conf["image-build"]['ksurl'] = resolve_git_url(image_conf["image-build"]['ksurl']) if ksurl:
image_conf["image-build"]['ksurl'] = ksurl
image_conf["image-build"]["variant"] = variant image_conf["image-build"]["variant"] = variant
image_conf["image-build"]["install_tree"] = self._get_install_tree(image_conf['image-build'], variant) image_conf["image-build"]["install_tree"] = self._get_install_tree(image_conf['image-build'], variant)
self._set_release(image_conf['image-build']) release = self.get_release(image_conf['image-build'])
if release:
image_conf['image-build']['release'] = release
image_conf['image-build']['version'] = self.get_config(image_conf['image-build'], 'version')
image_conf['image-build']['target'] = self.get_config(image_conf['image-build'], 'target')
# transform format into right 'format' for image-build # transform format into right 'format' for image-build
# e.g. 'docker,qcow2' # e.g. 'docker,qcow2'

View File

@ -150,6 +150,27 @@ class InitPhase(PhaseBase):
"optional": True, "optional": True,
}, },
# Configuration shared by all image building phases.
{
"name": "global_ksurl",
"expected_types": [str],
"optional": True,
},
{
"name": "global_target",
"expected_types": [str],
"optional": True,
},
{
"name": "global_release",
"expected_types": [str, type(None)],
"optional": True,
},
{
"name": "global_version",
"expected_types": [str],
"optional": True,
},
) )

View File

@ -27,8 +27,8 @@ from productmd.images import Image
from pungi.wrappers.kojiwrapper import KojiWrapper from pungi.wrappers.kojiwrapper import KojiWrapper
from pungi.wrappers.iso import IsoWrapper from pungi.wrappers.iso import IsoWrapper
from pungi.phases.base import PhaseBase from pungi.phases import base
from pungi.util import get_arch_variant_data, resolve_git_url, makedirs, get_mtime, get_file_size, failable from pungi.util import get_arch_variant_data, makedirs, get_mtime, get_file_size, failable
from pungi.paths import translate_path from pungi.paths import translate_path
@ -38,8 +38,8 @@ if sys.version_info[0] == 3:
return (a > b) - (a < b) return (a > b) - (a < b)
class LiveImagesPhase(PhaseBase): class LiveImagesPhase(base.ImageConfigMixin, base.ConfigGuardedPhase):
name = "liveimages" name = "live_images"
config_options = ( config_options = (
{ {
@ -71,20 +71,28 @@ class LiveImagesPhase(PhaseBase):
"name": "live_images_no_rename", "name": "live_images_no_rename",
"expected_types": [bool], "expected_types": [bool],
"optional": True, "optional": True,
} },
{
"name": "live_images_ksurl",
"expected_types": [str],
"optional": True,
},
{
"name": "live_images_release",
"expected_types": [str, type(None)],
"optional": True,
},
{
"name": "live_images_version",
"expected_types": [str],
"optional": True,
},
) )
def __init__(self, compose): def __init__(self, compose):
PhaseBase.__init__(self, compose) super(LiveImagesPhase, self).__init__(compose)
self.pool = ThreadPool(logger=self.compose._logger) self.pool = ThreadPool(logger=self.compose._logger)
def skip(self):
if PhaseBase.skip(self):
return True
if not self.compose.conf.get("live_images"):
return True
return False
def _get_extra_repos(self, arch, variant, extras): def _get_extra_repos(self, arch, variant, extras):
repo = [] repo = []
for extra in extras: for extra in extras:
@ -109,19 +117,13 @@ class LiveImagesPhase(PhaseBase):
repos.extend(self._get_extra_repos(arch, variant, force_list(data.get('repo_from', [])))) repos.extend(self._get_extra_repos(arch, variant, force_list(data.get('repo_from', []))))
return repos return repos
def _get_release(self, image_conf):
"""If release is set explicitly to None, replace it with date and respin."""
if 'release' in image_conf and image_conf['release'] is None:
return self.compose.image_release
return image_conf.get('release', None)
def run(self): def run(self):
symlink_isos_to = self.compose.conf.get("symlink_isos_to", None) symlink_isos_to = self.compose.conf.get("symlink_isos_to", None)
commands = [] commands = []
for variant in self.compose.variants.values(): for variant in self.compose.variants.values():
for arch in variant.arches + ["src"]: for arch in variant.arches + ["src"]:
for data in get_arch_variant_data(self.compose.conf, "live_images", arch, variant): for data in get_arch_variant_data(self.compose.conf, self.name, arch, variant):
subvariant = data.get('subvariant', variant.uid) subvariant = data.get('subvariant', variant.uid)
type = data.get('type', 'live') type = data.get('type', 'live')
@ -138,12 +140,12 @@ class LiveImagesPhase(PhaseBase):
cmd = { cmd = {
"name": data.get('name'), "name": data.get('name'),
"version": data.get("version", None), "version": self.get_config(data, 'version'),
"release": self._get_release(data), "release": self.get_release(data),
"dest_dir": dest_dir, "dest_dir": dest_dir,
"build_arch": arch, "build_arch": arch,
"ks_file": data['kickstart'], "ks_file": data['kickstart'],
"ksurl": None, "ksurl": self.get_ksurl(data),
# Used for images wrapped in RPM # Used for images wrapped in RPM
"specfile": data.get("specfile", None), "specfile": data.get("specfile", None),
# Scratch (only taken in consideration if specfile # Scratch (only taken in consideration if specfile
@ -157,9 +159,6 @@ class LiveImagesPhase(PhaseBase):
"subvariant": subvariant, "subvariant": subvariant,
} }
if 'ksurl' in data:
cmd['ksurl'] = resolve_git_url(data['ksurl'])
cmd["repos"] = self._get_repos(arch, variant, data) cmd["repos"] = self._get_repos(arch, variant, data)
# Signing of the rpm wrapped image # Signing of the rpm wrapped image
@ -194,11 +193,6 @@ class LiveImagesPhase(PhaseBase):
return self.compose.get_image_name(arch, variant, disc_type=disc_type, return self.compose.get_image_name(arch, variant, disc_type=disc_type,
disc_num=None, format=format) disc_num=None, format=format)
def stop(self, *args, **kwargs):
PhaseBase.stop(self, *args, **kwargs)
if self.skip():
return
class CreateLiveImageThread(WorkerThread): class CreateLiveImageThread(WorkerThread):
EXTS = ('.iso', '.raw.xz') EXTS = ('.iso', '.raw.xz')

View File

@ -4,8 +4,8 @@ import os
import time import time
from kobo import shortcuts from kobo import shortcuts
from pungi.util import get_variant_data, resolve_git_url, makedirs, get_mtime, get_file_size, failable from pungi.util import get_variant_data, makedirs, get_mtime, get_file_size, failable
from pungi.phases.base import PhaseBase from pungi.phases.base import ConfigGuardedPhase, ImageConfigMixin
from pungi.linker import Linker from pungi.linker import Linker
from pungi.paths import translate_path from pungi.paths import translate_path
from pungi.wrappers.kojiwrapper import KojiWrapper from pungi.wrappers.kojiwrapper import KojiWrapper
@ -13,7 +13,7 @@ from kobo.threads import ThreadPool, WorkerThread
from productmd.images import Image from productmd.images import Image
class LiveMediaPhase(PhaseBase): class LiveMediaPhase(ImageConfigMixin, ConfigGuardedPhase):
"""class for wrapping up koji spin-livemedia""" """class for wrapping up koji spin-livemedia"""
name = 'live_media' name = 'live_media'
@ -37,21 +37,17 @@ class LiveMediaPhase(PhaseBase):
"name": "live_media_release", "name": "live_media_release",
"expected_types": [str, type(None)], "expected_types": [str, type(None)],
"optional": True, "optional": True,
} },
{
"name": "live_media_version",
"expected_types": [str],
"optional": True,
},
) )
def __init__(self, compose): def __init__(self, compose):
super(LiveMediaPhase, self).__init__(compose) super(LiveMediaPhase, self).__init__(compose)
self.pool = ThreadPool(logger=self.compose._logger) self.pool = ThreadPool(logger=self.compose._logger)
self._global_ksurl = None
def skip(self):
if super(LiveMediaPhase, 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
def _get_repos(self, image_conf, variant): def _get_repos(self, image_conf, variant):
""" """
@ -84,15 +80,6 @@ class LiveMediaPhase(PhaseBase):
arches = set(image_conf.get('arches', [])) & arches arches = set(image_conf.get('arches', [])) & arches
return sorted(arches) return sorted(arches)
def _get_release(self, image_conf):
"""If release is set explicitly to None, replace it with date and respin.
Uses both image configuration and global config.
"""
for key, conf in [('release', image_conf), ('live_media_release', self.compose.conf)]:
if key in conf and conf[key] is None:
return self.compose.image_release
return image_conf.get('release', self.compose.conf.get('live_media_release'))
def _get_install_tree(self, image_conf, variant): def _get_install_tree(self, image_conf, variant):
if 'install_tree_from' in image_conf: if 'install_tree_from' in image_conf:
variant_uid = image_conf['install_tree_from'] variant_uid = image_conf['install_tree_from']
@ -107,23 +94,6 @@ class LiveMediaPhase(PhaseBase):
self.compose.paths.compose.os_tree('$basearch', variant, create_dir=False) self.compose.paths.compose.os_tree('$basearch', variant, create_dir=False)
) )
@property
def global_ksurl(self):
"""Get globally configure kickstart URL. It will only be resolved once."""
if not self._global_ksurl:
ksurl = self.compose.conf.get('live_media_ksurl')
self._global_ksurl = resolve_git_url(ksurl)
return self._global_ksurl
def _get_ksurl(self, image_conf):
"""Get ksurl from `image_conf`. If not present, fall back to global one."""
if 'ksurl' in image_conf:
return resolve_git_url(image_conf['ksurl'])
return self.global_ksurl
def _get_config(self, image_conf, opt):
return image_conf.get(opt, self.compose.conf.get('live_media_' + opt))
def run(self): def run(self):
for variant in self.compose.get_variants(): for variant in self.compose.get_variants():
arches = set([x for x in variant.arches if x != 'src']) arches = set([x for x in variant.arches if x != 'src'])
@ -132,20 +102,20 @@ class LiveMediaPhase(PhaseBase):
name = image_conf.get( name = image_conf.get(
'name', "%s-%s-Live" % (self.compose.ci_base.release.short, subvariant)) 'name', "%s-%s-Live" % (self.compose.ci_base.release.short, subvariant))
config = { config = {
'target': self._get_config(image_conf, 'target'), 'target': self.get_config(image_conf, 'target'),
'arches': self._get_arches(image_conf, arches), 'arches': self._get_arches(image_conf, arches),
'ksfile': image_conf['kickstart'], 'ksfile': image_conf['kickstart'],
'ksurl': self._get_ksurl(image_conf), 'ksurl': self.get_ksurl(image_conf),
'ksversion': image_conf.get('ksversion'), 'ksversion': image_conf.get('ksversion'),
'scratch': image_conf.get('scratch', False), 'scratch': image_conf.get('scratch', False),
'release': self._get_release(image_conf), 'release': self.get_release(image_conf),
'skip_tag': image_conf.get('skip_tag'), 'skip_tag': image_conf.get('skip_tag'),
'name': name, 'name': name,
'subvariant': subvariant, 'subvariant': subvariant,
'title': image_conf.get('title'), 'title': image_conf.get('title'),
'repo': self._get_repos(image_conf, variant), 'repo': self._get_repos(image_conf, variant),
'install_tree': self._get_install_tree(image_conf, variant), 'install_tree': self._get_install_tree(image_conf, variant),
'version': self._get_config(image_conf, 'version'), 'version': self.get_config(image_conf, 'version'),
} }
self.pool.add(LiveMediaThread(self.pool)) self.pool.add(LiveMediaThread(self.pool))
self.pool.queue_put((self.compose, variant, config)) self.pool.queue_put((self.compose, variant, config))

View File

@ -98,6 +98,65 @@ class TestImageBuildPhase(PungiTestCase):
[mock.call((compose, client_args)), [mock.call((compose, client_args)),
mock.call((compose, server_args))]) mock.call((compose, server_args))])
@mock.patch('pungi.phases.image_build.ThreadPool')
def test_image_build_phase_global_options(self, ThreadPool):
compose = DummyCompose(self.topdir, {
'image_build_ksurl': 'git://git.fedorahosted.org/git/spin-kickstarts.git',
'image_build_release': None,
'image_build_target': 'f24',
'image_build_version': 'Rawhide',
'image_build': {
'^Server$': [
{
'image-build': {
'format': [('docker', 'tar.xz')],
'name': 'Fedora-Docker-Base',
'kickstart': "fedora-docker-base.ks",
'distro': 'Fedora-20',
'disk_size': 3
}
}
]
},
'koji_profile': 'koji',
})
phase = ImageBuildPhase(compose)
phase.run()
self.maxDiff = None
# assert at least one thread was started
self.assertTrue(phase.pool.add.called)
server_args = {
"format": [('docker', 'tar.xz')],
"image_conf": {
'image-build': {
'install_tree': self.topdir + '/compose/Server/$arch/os',
'kickstart': 'fedora-docker-base.ks',
'format': 'docker',
'repo': self.topdir + '/compose/Server/$arch/os',
'variant': compose.variants['Server'],
'target': 'f24',
'disk_size': 3,
'name': 'Fedora-Docker-Base',
'arches': 'amd64,x86_64',
'version': 'Rawhide',
'ksurl': 'git://git.fedorahosted.org/git/spin-kickstarts.git',
'distro': 'Fedora-20',
'release': '20151203.t.0',
}
},
"conf_file": self.topdir + '/work/image-build/Server/docker_Fedora-Docker-Base.cfg',
"image_dir": self.topdir + '/compose/Server/%(arch)s/images',
"relative_image_dir": 'Server/%(arch)s/images',
"link_type": 'hardlink-or-copy',
"scratch": False,
}
self.maxDiff = None
self.assertItemsEqual(phase.pool.queue_put.mock_calls,
[mock.call((compose, server_args))])
@mock.patch('pungi.phases.image_build.ThreadPool') @mock.patch('pungi.phases.image_build.ThreadPool')
def test_image_build_filter_all_variants(self, ThreadPool): def test_image_build_filter_all_variants(self, ThreadPool):
compose = DummyCompose(self.topdir, { compose = DummyCompose(self.topdir, {
@ -321,7 +380,7 @@ class TestImageBuildPhase(PungiTestCase):
args, kwargs = phase.pool.queue_put.call_args args, kwargs = phase.pool.queue_put.call_args
self.assertTrue(args[0][1].get('scratch')) self.assertTrue(args[0][1].get('scratch'))
@mock.patch('pungi.phases.image_build.resolve_git_url') @mock.patch('pungi.util.resolve_git_url')
@mock.patch('pungi.phases.image_build.ThreadPool') @mock.patch('pungi.phases.image_build.ThreadPool')
def test_image_build_resolve_ksurl(self, ThreadPool, resolve_git_url): def test_image_build_resolve_ksurl(self, ThreadPool, resolve_git_url):
compose = DummyCompose(self.topdir, { compose = DummyCompose(self.topdir, {

View File

@ -221,7 +221,7 @@ class TestLiveImagesPhase(PungiTestCase):
'amd64'))]) 'amd64'))])
@mock.patch('pungi.phases.live_images.ThreadPool') @mock.patch('pungi.phases.live_images.ThreadPool')
@mock.patch('pungi.phases.live_images.resolve_git_url') @mock.patch('pungi.util.resolve_git_url')
def test_spin_appliance(self, resolve_git_url, ThreadPool): def test_spin_appliance(self, resolve_git_url, ThreadPool):
compose = DummyCompose(self.topdir, { compose = DummyCompose(self.topdir, {
'live_images': [ 'live_images': [
@ -270,6 +270,110 @@ class TestLiveImagesPhase(PungiTestCase):
self.assertEqual(resolve_git_url.mock_calls, self.assertEqual(resolve_git_url.mock_calls,
[mock.call('https://git.example.com/kickstarts.git?#HEAD')]) [mock.call('https://git.example.com/kickstarts.git?#HEAD')])
@mock.patch('pungi.phases.live_images.ThreadPool')
@mock.patch('pungi.util.resolve_git_url')
def test_spin_appliance_phase_global_settings(self, resolve_git_url, ThreadPool):
compose = DummyCompose(self.topdir, {
'live_images_ksurl': 'https://git.example.com/kickstarts.git?#HEAD',
'live_images_release': None,
'live_images_version': 'Rawhide',
'live_images': [
('^Client$', {
'amd64': {
'kickstart': 'test.ks',
'additional_repos': ['http://example.com/repo/'],
'repo_from': ['Everything'],
'type': 'appliance',
}
})
],
})
resolve_git_url.return_value = 'https://git.example.com/kickstarts.git?#CAFEBABE'
phase = LiveImagesPhase(compose)
phase.run()
# assert at least one thread was started
self.assertTrue(phase.pool.add.called)
self.maxDiff = None
self.assertItemsEqual(phase.pool.queue_put.mock_calls,
[mock.call((compose,
{'ks_file': 'test.ks',
'build_arch': 'amd64',
'dest_dir': self.topdir + '/compose/Client/amd64/images',
'scratch': False,
'repos': [self.topdir + '/compose/Client/amd64/os',
'http://example.com/repo/',
self.topdir + '/compose/Everything/amd64/os'],
'label': '',
'name': None,
'filename': 'image-name',
'version': 'Rawhide',
'specfile': None,
'sign': False,
'type': 'appliance',
'release': '20151203.t.0',
'subvariant': 'Client',
'ksurl': 'https://git.example.com/kickstarts.git?#CAFEBABE'},
compose.variants['Client'],
'amd64'))])
self.assertEqual(resolve_git_url.mock_calls,
[mock.call('https://git.example.com/kickstarts.git?#HEAD')])
@mock.patch('pungi.phases.live_images.ThreadPool')
@mock.patch('pungi.util.resolve_git_url')
def test_spin_appliance_global_settings(self, resolve_git_url, ThreadPool):
compose = DummyCompose(self.topdir, {
'global_ksurl': 'https://git.example.com/kickstarts.git?#HEAD',
'global_release': None,
'global_version': 'Rawhide',
'live_images': [
('^Client$', {
'amd64': {
'kickstart': 'test.ks',
'additional_repos': ['http://example.com/repo/'],
'repo_from': ['Everything'],
'type': 'appliance',
}
})
],
})
resolve_git_url.return_value = 'https://git.example.com/kickstarts.git?#CAFEBABE'
phase = LiveImagesPhase(compose)
phase.run()
# assert at least one thread was started
self.assertTrue(phase.pool.add.called)
self.maxDiff = None
self.assertItemsEqual(phase.pool.queue_put.mock_calls,
[mock.call((compose,
{'ks_file': 'test.ks',
'build_arch': 'amd64',
'dest_dir': self.topdir + '/compose/Client/amd64/images',
'scratch': False,
'repos': [self.topdir + '/compose/Client/amd64/os',
'http://example.com/repo/',
self.topdir + '/compose/Everything/amd64/os'],
'label': '',
'name': None,
'filename': 'image-name',
'version': 'Rawhide',
'specfile': None,
'sign': False,
'type': 'appliance',
'release': '20151203.t.0',
'subvariant': 'Client',
'ksurl': 'https://git.example.com/kickstarts.git?#CAFEBABE'},
compose.variants['Client'],
'amd64'))])
self.assertEqual(resolve_git_url.mock_calls,
[mock.call('https://git.example.com/kickstarts.git?#HEAD')])
@mock.patch('pungi.phases.live_images.ThreadPool') @mock.patch('pungi.phases.live_images.ThreadPool')
def test_live_image_build_custom_type(self, ThreadPool): def test_live_image_build_custom_type(self, ThreadPool):
compose = DummyCompose(self.topdir, { compose = DummyCompose(self.topdir, {

View File

@ -75,9 +75,9 @@ class TestLiveMediaPhase(PungiTestCase):
'subvariant': 'Server', 'subvariant': 'Server',
}))]) }))])
@mock.patch('pungi.phases.livemedia_phase.resolve_git_url') @mock.patch('pungi.util.resolve_git_url')
@mock.patch('pungi.phases.livemedia_phase.ThreadPool') @mock.patch('pungi.phases.livemedia_phase.ThreadPool')
def test_live_media_with_global_opts(self, ThreadPool, resolve_git_url): def test_live_media_with_phase_global_opts(self, ThreadPool, resolve_git_url):
compose = DummyCompose(self.topdir, { compose = DummyCompose(self.topdir, {
'live_media_ksurl': 'git://example.com/repo.git#HEAD', 'live_media_ksurl': 'git://example.com/repo.git#HEAD',
'live_media_target': 'f24', 'live_media_target': 'f24',
@ -171,6 +171,102 @@ class TestLiveMediaPhase(PungiTestCase):
'subvariant': 'Server', 'subvariant': 'Server',
}))]) }))])
@mock.patch('pungi.util.resolve_git_url')
@mock.patch('pungi.phases.livemedia_phase.ThreadPool')
def test_live_media_with_global_opts(self, ThreadPool, resolve_git_url):
compose = DummyCompose(self.topdir, {
'global_ksurl': 'git://example.com/repo.git#HEAD',
'global_target': 'f24',
'global_release': 'RRR',
'global_version': 'Rawhide',
'live_media': {
'^Server$': [
{
'kickstart': 'file.ks',
'name': 'Fedora Server Live',
},
{
'kickstart': 'different.ks',
'name': 'Fedora Server Live',
},
{
'kickstart': 'yet-another.ks',
'name': 'Fedora Server Live',
'ksurl': 'git://different.com/repo.git',
'target': 'f25',
'release': 'XXX',
'version': '25',
}
]
},
'koji_profile': 'koji',
})
resolve_git_url.return_value = 'git://example.com/repo.git#BEEFCAFE'
phase = LiveMediaPhase(compose)
phase.run()
self.assertTrue(phase.pool.add.called)
self.assertItemsEqual(resolve_git_url.mock_calls,
[mock.call('git://example.com/repo.git#HEAD'),
mock.call('git://different.com/repo.git')])
self.assertEqual(phase.pool.queue_put.call_args_list,
[mock.call((compose,
compose.variants['Server'],
{
'arches': ['amd64', 'x86_64'],
'ksfile': 'file.ks',
'ksurl': 'git://example.com/repo.git#BEEFCAFE',
'ksversion': None,
'name': 'Fedora Server Live',
'release': 'RRR',
'repo': [self.topdir + '/compose/Server/$basearch/os'],
'scratch': False,
'skip_tag': None,
'target': 'f24',
'title': None,
'install_tree': self.topdir + '/compose/Server/$basearch/os',
'version': 'Rawhide',
'subvariant': 'Server',
})),
mock.call((compose,
compose.variants['Server'],
{
'arches': ['amd64', 'x86_64'],
'ksfile': 'different.ks',
'ksurl': 'git://example.com/repo.git#BEEFCAFE',
'ksversion': None,
'name': 'Fedora Server Live',
'release': 'RRR',
'repo': [self.topdir + '/compose/Server/$basearch/os'],
'scratch': False,
'skip_tag': None,
'target': 'f24',
'title': None,
'install_tree': self.topdir + '/compose/Server/$basearch/os',
'version': 'Rawhide',
'subvariant': 'Server',
})),
mock.call((compose,
compose.variants['Server'],
{
'arches': ['amd64', 'x86_64'],
'ksfile': 'yet-another.ks',
'ksurl': 'git://example.com/repo.git#BEEFCAFE',
'ksversion': None,
'name': 'Fedora Server Live',
'release': 'XXX',
'repo': [self.topdir + '/compose/Server/$basearch/os'],
'scratch': False,
'skip_tag': None,
'target': 'f25',
'title': None,
'install_tree': self.topdir + '/compose/Server/$basearch/os',
'version': '25',
'subvariant': 'Server',
}))])
@mock.patch('pungi.phases.livemedia_phase.ThreadPool') @mock.patch('pungi.phases.livemedia_phase.ThreadPool')
def test_live_media_non_existing_install_tree(self, ThreadPool): def test_live_media_non_existing_install_tree(self, ThreadPool):
compose = DummyCompose(self.topdir, { compose = DummyCompose(self.topdir, {
@ -217,7 +313,7 @@ class TestLiveMediaPhase(PungiTestCase):
with self.assertRaisesRegexp(RuntimeError, r'no.+Missing.+when building.+Server'): with self.assertRaisesRegexp(RuntimeError, r'no.+Missing.+when building.+Server'):
phase.run() phase.run()
@mock.patch('pungi.phases.livemedia_phase.resolve_git_url') @mock.patch('pungi.util.resolve_git_url')
@mock.patch('pungi.phases.livemedia_phase.ThreadPool') @mock.patch('pungi.phases.livemedia_phase.ThreadPool')
def test_live_media_full(self, ThreadPool, resolve_git_url): def test_live_media_full(self, ThreadPool, resolve_git_url):
compose = DummyCompose(self.topdir, { compose = DummyCompose(self.topdir, {

73
tests/test_phase_base.py Normal file
View File

@ -0,0 +1,73 @@
#!/usr/bin/env python2
# -*- coding: utf-8 -*-
import mock
import unittest
import os
import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
from pungi.phases import base
from tests.helpers import DummyCompose, PungiTestCase
class Phase1(base.ImageConfigMixin, base.PhaseBase):
name = 'phase1'
class Phase2(base.ImageConfigMixin, base.PhaseBase):
name = 'phase2'
class Phase3(base.ImageConfigMixin, base.PhaseBase):
name = 'phase3'
class DummyResolver(object):
def __init__(self):
self.num = 0
def __call__(self, url):
self.num += 1
return url.replace('HEAD', 'RES' + str(self.num))
class ImageConfigMixinTestCase(PungiTestCase):
@mock.patch('pungi.util.resolve_git_url', new_callable=DummyResolver)
def test_git_url_resolved_once(self, resolve_git_url):
compose = DummyCompose(self.topdir, {
'global_ksurl': 'git://example.com/repo.git?#HEAD',
'phase1_ksurl': 'git://example.com/another.git?#HEAD',
})
p1 = Phase1(compose)
p2 = Phase2(compose)
p3 = Phase3(compose)
self.assertEqual(p1.get_ksurl({}),
'git://example.com/another.git?#RES1')
# Phase-level setting retrieved second time.
self.assertEqual(p1.get_ksurl({}),
'git://example.com/another.git?#RES1')
self.assertEqual(p2.get_ksurl({}),
'git://example.com/repo.git?#RES2')
# Global setting retrieved again from same phase.
self.assertEqual(p2.get_ksurl({}),
'git://example.com/repo.git?#RES2')
# Global setting retrieved from another phase.
self.assertEqual(p3.get_ksurl({}),
'git://example.com/repo.git?#RES2')
# Local setting ignores global ones.
self.assertEqual(p3.get_ksurl({'ksurl': 'git://example.com/more.git?#HEAD'}),
'git://example.com/more.git?#RES3')
self.assertEqual(resolve_git_url.num, 3, 'Resolver was not called three times')
if __name__ == "__main__":
unittest.main()