From 53df522f2b8a989d756cc71ccc430935312b43ff Mon Sep 17 00:00:00 2001 From: Qixiang Wan Date: Feb 14 2017 08:17:31 +0000 Subject: new script: compose-create-next-dir A new script compose-create-next-dir is added, it can create empty directory for next compose, compose ID is determined by parsing the config file. get_compose_dir is copied from pungi with minor changes. Pungi is not intended to be used as a library, so not import pungi to use the function. Signed-off-by: Qixiang Wan --- diff --git a/bin/compose-create-next-dir b/bin/compose-create-next-dir new file mode 100755 index 0000000..7db22df --- /dev/null +++ b/bin/compose-create-next-dir @@ -0,0 +1,20 @@ +#!/usr/bin/env python +# -*- encoding: utf-8 -*- + +from __future__ import print_function + +import os +import sys + +here = sys.path[0] +if here != '/usr/bin': + sys.path[0] = os.path.dirname(here) + +from compose_utils import create_next_dir + +if __name__ == '__main__': + try: + create_next_dir.run() + except Exception as exc: + print(str(exc), file=sys.stderr) + sys.exit(1) diff --git a/compose_utils/create_next_dir.py b/compose_utils/create_next_dir.py new file mode 100644 index 0000000..8ef4664 --- /dev/null +++ b/compose_utils/create_next_dir.py @@ -0,0 +1,93 @@ +# -*- encoding: utf-8 -*- + +from __future__ import print_function + +import argparse +import errno +import os +import time + +import kobo.conf +from productmd.composeinfo import ComposeInfo + + +class MissingConfigOptionError(RuntimeError): + pass + + +def get_compose_dir(topdir, conf, compose_type="production", compose_date=None, compose_respin=None, compose_label=None): + ci = ComposeInfo() + try: + ci.release.name = conf["release_name"] + ci.release.short = conf["release_short"] + ci.release.version = conf["release_version"] + ci.release.is_layered = bool(conf.get("release_is_layered", False)) + ci.release.type = conf.get("release_type", "ga").lower() + ci.release.internal = bool(conf.get("release_internal", False)) + + if ci.release.is_layered: + ci.base_product.name = conf["base_product_name"] + ci.base_product.short = conf["base_product_short"] + ci.base_product.version = conf["base_product_version"] + ci.base_product.type = conf.get("base_product_type", "ga").lower() + except KeyError as ex: + raise MissingConfigOptionError("Option %s is missed from config" % str(ex)) + + ci.compose.label = compose_label + ci.compose.type = compose_type + ci.compose.date = compose_date or time.strftime("%Y%m%d", time.localtime()) + ci.compose.respin = compose_respin or 0 + + while 1: + ci.compose.id = ci.create_compose_id() + + compose_dir = os.path.join(topdir, ci.compose.id) + + exists = False + try: + os.makedirs(compose_dir) + except OSError as ex: + if ex.errno == errno.EEXIST: + exists = True + else: + raise + + if exists: + ci.compose.respin += 1 + continue + break + + return compose_dir + + +def main(args): + conf = kobo.conf.PyConfigParser() + conf.load_from_file(args.config) + next_dir = get_compose_dir(args.target_dir, conf, compose_type=args.type) + print(next_dir) + + +def run(arguments=None): + parser = argparse.ArgumentParser() + parser.add_argument( + '--config', + metavar='CONFIG', + required=True, + help='config file', + ) + parser.add_argument( + '--target-dir', + metavar='PATH', + required=True, + help='create compose dir under this directory', + ) + parser.add_argument( + '--type', + metavar='TYPE', + choices=('production', 'test', 'nightly', 'ci'), + default='production', + help='compose type', + ) + + args = parser.parse_args(arguments) + main(args) diff --git a/doc/compose-create-next-dir.1 b/doc/compose-create-next-dir.1 new file mode 100644 index 0000000..5382ce0 --- /dev/null +++ b/doc/compose-create-next-dir.1 @@ -0,0 +1,28 @@ +.TH compose-create-next-dir 1 +.SH NAME +compose-create-next-dir \- create empty directory for next compose +.SH SYNOPSIS +.B compose-create-next-dir +[\fIOPTIONS\fR...] +\fI--config\fR +\fICONFIG\fR +\fI--target-dir\fR +\fIPATH\fR +.SH DESCRIPTION +.B compose-create-next-dir +Get next compose ID by parsing config file and then create empty directory for the compose. +Compose ID will be printed to stdout after the compose dir is created successfully. +.SH OPTIONS +.TP +.BR \-h ", " \-\-help +Print help and exit. +.TP +.BR \-\-type=\fITYPE\fR +Compose type. Valid types: production, test, nightly, ci. Default is production. +.SH EXIT CODE +On success, this tools exits with status code of \fB0\fR. +On failure, the exit code will be \fB1\fR. +.SH BUGS +Please report bugs at +.br +https://pagure.io/compose-utils/issues diff --git a/tests/test_create_next_dir.py b/tests/test_create_next_dir.py new file mode 100644 index 0000000..5fc36a0 --- /dev/null +++ b/tests/test_create_next_dir.py @@ -0,0 +1,149 @@ +# -*- encoding: utf-8 -*- + +import mock +import unittest +import shutil +import tempfile +import os + +from freezegun import freeze_time +from StringIO import StringIO + +from compose_utils import create_next_dir + + +class CreateNextDirTest(unittest.TestCase): + def setUp(self): + self.topdir = tempfile.mkdtemp() + self.target_dir = tempfile.mkdtemp(prefix='_compose_', dir=self.topdir) + + def tearDown(self): + shutil.rmtree(self.topdir) + + def _write_config_from_string(self, string): + config_file = os.path.join(self.topdir, 'fake.conf') + lines = [l.strip() for l in string.split('\n') if l.strip() is not ''] + with open(config_file, 'w') as f: + for line in lines: + f.write('%s\n' % line) + return config_file + + @freeze_time("2016-01-01") + def test_create_next_production_compose_dir(self): + cfg = """ + release_name = "Dummy Product" + release_short = "DP" + release_version = "1.0" + """ + config = self._write_config_from_string(cfg) + with mock.patch('sys.stdout', new_callable=StringIO) as out: + create_next_dir.run(['--target-dir=%s' % self.target_dir, + '--config=%s' % config]) + compose_dir = os.path.join(self.target_dir, 'DP-1.0-20160101.0') + self.assertEqual(out.getvalue().strip(), compose_dir) + self.assertTrue(os.path.isdir(compose_dir)) + + @freeze_time("2016-01-01") + def test_create_next_test_compose_dir(self): + cfg = """ + release_name = "Dummy Product" + release_short = "DP" + release_version = "1.0" + """ + config = self._write_config_from_string(cfg) + with mock.patch('sys.stdout', new_callable=StringIO) as out: + create_next_dir.run(['--target-dir=%s' % self.target_dir, + '--config=%s' % config, + '--type=test']) + compose_dir = os.path.join(self.target_dir, 'DP-1.0-20160101.t.0') + self.assertEqual(out.getvalue().strip(), compose_dir) + self.assertTrue(os.path.isdir(compose_dir)) + + @freeze_time("2016-01-01") + def test_create_next_nightly_compose_dir(self): + cfg = """ + release_name = "Dummy Product" + release_short = "DP" + release_version = "1.0" + """ + config = self._write_config_from_string(cfg) + with mock.patch('sys.stdout', new_callable=StringIO) as out: + create_next_dir.run(['--target-dir=%s' % self.target_dir, + '--config=%s' % config, + '--type=nightly']) + compose_dir = os.path.join(self.target_dir, 'DP-1.0-20160101.n.0') + self.assertEqual(out.getvalue().strip(), compose_dir) + self.assertTrue(os.path.isdir(compose_dir)) + + @freeze_time("2016-01-01") + def test_create_next_ci_compose_dir(self): + cfg = """ + release_name = "Dummy Product" + release_short = "DP" + release_version = "1.0" + """ + config = self._write_config_from_string(cfg) + with mock.patch('sys.stdout', new_callable=StringIO) as out: + create_next_dir.run(['--target-dir=%s' % self.target_dir, + '--config=%s' % config, + '--type=ci']) + compose_dir = os.path.join(self.target_dir, 'DP-1.0-20160101.ci.0') + self.assertEqual(out.getvalue().strip(), compose_dir) + self.assertTrue(os.path.isdir(compose_dir)) + + @freeze_time("2016-01-01") + def test_compose_id_increment(self): + cfg = """ + release_name = "Dummy Product" + release_short = "DP" + release_version = "1.0" + """ + config = self._write_config_from_string(cfg) + + os.makedirs(os.path.join(self.target_dir, 'DP-1.0-20160101.0')) + with mock.patch('sys.stdout', new_callable=StringIO) as out: + create_next_dir.run(['--target-dir=%s' % self.target_dir, + '--config=%s' % config]) + + compose_dir = os.path.join(self.target_dir, 'DP-1.0-20160101.1') + self.assertEqual(out.getvalue().strip(), compose_dir) + self.assertTrue(os.path.isdir(compose_dir)) + + os.makedirs(os.path.join(self.target_dir, 'DP-1.0-20160101.2')) + with mock.patch('sys.stdout', new_callable=StringIO) as out: + create_next_dir.run(['--target-dir=%s' % self.target_dir, + '--config=%s' % config]) + + compose_dir = os.path.join(self.target_dir, 'DP-1.0-20160101.3') + self.assertEqual(out.getvalue().strip(), compose_dir) + self.assertTrue(os.path.isdir(compose_dir)) + + @freeze_time("2016-01-01") + def test_miss_option_in_config(self): + cfg = """ + release_name = "Dummy Product" + release_version = "1.0" + """ + config = self._write_config_from_string(cfg) + with self.assertRaises(create_next_dir.MissingConfigOptionError) as ctx: + create_next_dir.run(['--target-dir=%s' % self.target_dir, + '--config=%s' % config]) + + self.assertIn("Option 'release_short' is missed from config", str(ctx.exception)) + + @freeze_time("2016-01-01") + def test_no_permission_on_creating_compose_dir(self): + cfg = """ + release_name = "Dummy Product" + release_short = "DP" + release_version = "1.0" + """ + compose_dir = os.path.join(self.target_dir, 'DP-1.0-20160101.0') + config = self._write_config_from_string(cfg) + os.chmod(self.target_dir, 555) + with self.assertRaises(OSError) as ctx: + create_next_dir.run(['--target-dir=%s' % self.target_dir, + '--config=%s' % config]) + self.assertIn("[Errno 13] Permission denied: '%s'" % compose_dir, str(ctx.exception)) + # restore write permission so it can be removed + os.chmod(self.target_dir, 777)