diff --git a/packetary/api/__init__.py b/packetary/api/__init__.py index 868dcb3..c404e31 100644 --- a/packetary/api/__init__.py +++ b/packetary/api/__init__.py @@ -19,11 +19,13 @@ from packetary.api.context import Configuration from packetary.api.context import Context from packetary.api.options import RepositoryCopyOptions +from packetary.api.packaging import PackagingApi from packetary.api.repositories import RepositoryApi __all__ = [ "Configuration", "Context", + "PackagingApi", "RepositoryApi", "RepositoryCopyOptions", ] diff --git a/packetary/api/packaging.py b/packetary/api/packaging.py new file mode 100644 index 0000000..098ba34 --- /dev/null +++ b/packetary/api/packaging.py @@ -0,0 +1,77 @@ +# -*- coding: utf-8 -*- + +# Copyright 2016 Mirantis, Inc. +# +# 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; either version 2 of the License, or +# (at your option) any later version. +# +# 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 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +import logging +import os.path + +from packetary.api.context import Context +from packetary.api.validators import declare_schema +from packetary.controllers import PackagingController +from packetary.library import utils + + +logger = logging.getLogger(__package__) + + +class PackagingApi(object): + """Provides high-level API to build packages.""" + + def __init__(self, controller): + """Initialises. + + :param controller: the packaging controller. + :type controller: PackagingController + """ + self.controller = controller + + def _get_data_schema(self): + return { + '$schema': 'http://json-schema.org/draft-04/schema#', + 'type': 'array', + 'items': self.controller.get_data_schema() + } + + @classmethod + def create(cls, config, driver_type, driver_config): + """Creates the packaging API instance. + + :param config: the global config + :param driver_type: the name of driver which will be used + :param driver_config: the config of driver + + :return PackagingApi instance + """ + context = config if isinstance(config, Context) else Context(config) + return cls( + PackagingController.load(context, driver_type, driver_config) + ) + + @declare_schema(sources=_get_data_schema) + def build_packages(self, sources, output_dir): + """Builds new package(s). + + :param sources: list descriptions of packages for building + :param output_dir: directory for new packages + :return: list of names of packages which was built + """ + output_dir = os.path.abspath(output_dir) + utils.ensure_dir_exist(output_dir) + packages = [] + for source in sources: + self.controller.build_packages(source, output_dir, packages.append) + return packages diff --git a/packetary/controllers/__init__.py b/packetary/controllers/__init__.py index a34146c..59c76fa 100644 --- a/packetary/controllers/__init__.py +++ b/packetary/controllers/__init__.py @@ -16,8 +16,10 @@ # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +from packetary.controllers.packaging import PackagingController from packetary.controllers.repository import RepositoryController __all__ = [ + "PackagingController", "RepositoryController" ] diff --git a/packetary/controllers/packaging.py b/packetary/controllers/packaging.py new file mode 100644 index 0000000..f763cd6 --- /dev/null +++ b/packetary/controllers/packaging.py @@ -0,0 +1,70 @@ +# -*- coding: utf-8 -*- + +# Copyright 2016 Mirantis, Inc. +# +# 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; either version 2 of the License, or +# (at your option) any later version. +# +# 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 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +import logging + +import six +import stevedore + +logger = logging.getLogger(__package__) + +urljoin = six.moves.urllib.parse.urljoin + + +class PackagingController(object): + """Implements low-level functionality to communicate with drivers.""" + + _drivers = None + + def __init__(self, context, driver): + self.context = context + self.driver = driver + + @classmethod + def load(cls, context, driver_name, driver_config): + """Creates the packaging manager.""" + if cls._drivers is None: + cls._drivers = stevedore.ExtensionManager( + "packetary.packaging_drivers", invoke_on_load=True, + invoke_args=(driver_config,) + ) + try: + driver = cls._drivers[driver_name].obj + except KeyError: + raise NotImplementedError( + "The driver {0} is not supported yet.".format(driver_name) + ) + return cls(context, driver) + + def get_data_schema(self): + """Return jsonschema to validate data, which will be pass to driver + + :return : Return a jsonschema represented as a dict + """ + return self.driver.get_data_schema() + + def build_packages(self, data, output_dir, consumer): + """Build package from sources. + + :param data: the input data for building packages, + the format of data depends on selected driver + :param output_dir: directory for new packages + :param consumer: callable, that will be called for each built package + """ + # TODO(bgaifullin) Add downloading sources and specs from URL + return self.driver.build_packages(data, output_dir, consumer) diff --git a/packetary/drivers/base.py b/packetary/drivers/base.py index efd0f3b..a17fd8b 100644 --- a/packetary/drivers/base.py +++ b/packetary/drivers/base.py @@ -114,3 +114,31 @@ class RepositoryDriverBase(object): @abc.abstractmethod def get_repository_data_schema(self): """Gets the json scheme for repository data validation.""" + + +@six.add_metaclass(abc.ABCMeta) +class PackagingDriverBase(object): + """The super class for Packaging Drivers. + + For implementing support of new type of packaging: + - inherit this class + - implement all abstract methods + - register implementation in 'packetary.packaging_drivers' namespace + """ + + def __init__(self): + self.logger = logging.getLogger(__package__) + + @abc.abstractmethod + def get_data_schema(self): + """Gets the json-schema to validate input data.""" + + @abc.abstractmethod + def build_packages(self, data, output_dir, consumer): + """Build package from sources. + + :param data: the input data for building packages, + the format of data depends on selected driver + :param output_dir: directory for new packages + :param consumer: callable, that will be called for each built package + """ diff --git a/packetary/tests/test_packaging_api.py b/packetary/tests/test_packaging_api.py new file mode 100644 index 0000000..6457c4b --- /dev/null +++ b/packetary/tests/test_packaging_api.py @@ -0,0 +1,90 @@ +# -*- coding: utf-8 -*- + +# Copyright 2015 Mirantis, Inc. +# +# 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; either version 2 of the License, or +# (at your option) any later version. +# +# 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 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +import mock + +from packetary import api +from packetary import controllers + +from packetary.tests import base +from packetary.tests.stubs import helpers + + +class TestPackagingApi(base.TestCase): + def setUp(self): + super(TestPackagingApi, self).setUp() + self.controller = helpers.CallbacksAdapter( + spec=controllers.PackagingController + ) + self.controller.get_data_schema.return_value = {} + self.api = api.PackagingApi(self.controller) + + @mock.patch("packetary.api.packaging.Context", autospec=True) + @mock.patch("packetary.api.packaging.PackagingController", autospec=True) + @mock.patch("packetary.api.packaging.isinstance", + new=mock.MagicMock(return_value=False), create=True) + def test_create_with_config(self, controller_mock, context_mock): + config = mock.MagicMock() + api.PackagingApi.create(config, "test_driver", "driver_config") + context_mock.assert_called_with(config) + controller_mock.load.assert_called_with( + context_mock.return_value, "test_driver", "driver_config" + ) + + @mock.patch("packetary.api.packaging.Context", autospec=True) + @mock.patch("packetary.api.packaging.PackagingController", autospec=True) + @mock.patch("packetary.api.packaging.isinstance", + new=mock.MagicMock(return_value=True), create=True) + def test_create_with_context(self, controller_mock, context_mock): + context = mock.MagicMock() + api.PackagingApi.create(context, "test_driver", "driver_config") + controller_mock.load.assert_called_with( + context, "test_driver", "driver_config" + ) + self.assertEqual(0, context_mock.call_count) + + @mock.patch("packetary.api.validators.jsonschema") + @mock.patch("packetary.api.packaging.os") + @mock.patch("packetary.api.packaging.utils") + def test_build_packages(self, utils_mock, os_mock, jsonschema_mock): + data = [ + {'sources': '/sources1'}, + {'sources': '/sources2'} + ] + output_dir = '/tmp' + self.controller.build_packages.side_effect = [ + ['package1.src', 'package1.bin'], + ['package2.src', 'package2.bin'] + ] + packages = self.api.build_packages(data, output_dir) + self.assertEqual( + ['package1.src', 'package1.bin', 'package2.src', 'package2.bin'], + packages + ) + os_mock.path.abspath.assert_called_once_with(output_dir) + utils_mock.ensure_dir_exist.assert_called_once_with( + os_mock.path.abspath.return_value + ) + jsonschema_mock.validate.assert_called_with( + data, + { + 'items': self.controller.get_data_schema.return_value, + '$schema': 'http://json-schema.org/draft-04/schema#', + 'type': 'array' + } + ) diff --git a/packetary/tests/test_packaging_controller.py b/packetary/tests/test_packaging_controller.py new file mode 100644 index 0000000..fd521f4 --- /dev/null +++ b/packetary/tests/test_packaging_controller.py @@ -0,0 +1,70 @@ +# -*- coding: utf-8 -*- + +# Copyright 2016 Mirantis, Inc. +# +# 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; either version 2 of the License, or +# (at your option) any later version. +# +# 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 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +import mock + +from packetary.controllers import PackagingController +from packetary.drivers.base import PackagingDriverBase + +from packetary.tests import base + + +class TestPackagingController(base.TestCase): + def setUp(self): + super(TestPackagingController, self).setUp() + self.driver = mock.MagicMock(spec=PackagingDriverBase) + self.controller = PackagingController("contex", self.driver) + + @mock.patch("packetary.controllers.packaging.stevedore") + def test_load_fail_if_unknown_driver(self, stevedore): + stevedore.ExtensionManager.return_value = {} + self.assertRaisesRegexp( + NotImplementedError, + "The driver unknown_driver is not supported yet.", + PackagingController.load, "contex", "unknown_driver", "config" + ) + + @mock.patch("packetary.controllers.packaging.stevedore") + def test_load_driver(self, stevedore): + stevedore.ExtensionManager.return_value = { + "test": mock.MagicMock(obj=self.driver) + } + PackagingController._drivers = None + controller = PackagingController.load("context", "test", "config") + self.assertIs(self.driver, controller.driver) + stevedore.ExtensionManager.assert_called_once_with( + "packetary.packaging_drivers", invoke_on_load=True, + invoke_args=("config",) + ) + + def test_get_data_schema(self): + self.driver.get_data_schema.return_value = {} + self.assertIs( + self.driver.get_data_schema.return_value, + self.controller.get_data_schema() + ) + self.driver.get_data_schema.assert_called_once_with() + + def test_build_packages(self): + data = {'sources': '/sources'} + output_dir = '/tmp/' + callback = mock.MagicMock() + self.controller.build_packages(data, output_dir, callback) + self.driver.build_packages.assert_called_once_with( + data, output_dir, callback + ) diff --git a/packetary/tests/test_repository_api.py b/packetary/tests/test_repository_api.py index 5f0231d..58de6d5 100644 --- a/packetary/tests/test_repository_api.py +++ b/packetary/tests/test_repository_api.py @@ -20,18 +20,21 @@ import copy import mock from packetary import api -from packetary.controllers.repository import RepositoryController +from packetary import controllers from packetary import schemas from packetary.tests import base from packetary.tests.stubs import generator -from packetary.tests.stubs.helpers import CallbacksAdapter +from packetary.tests.stubs import helpers @mock.patch("packetary.api.validators.jsonschema") class TestRepositoryApi(base.TestCase): def setUp(self): - self.controller = CallbacksAdapter(spec=RepositoryController) + super(TestRepositoryApi, self).setUp() + self.controller = helpers.CallbacksAdapter( + spec=controllers.RepositoryController + ) self.api = api.RepositoryApi(self.controller) self.schema = {} self.controller.get_repository_data_schema.return_value = self.schema diff --git a/packetary/tests/test_repository_contoller.py b/packetary/tests/test_repository_contoller.py index a407eaa..12935c5 100644 --- a/packetary/tests/test_repository_contoller.py +++ b/packetary/tests/test_repository_contoller.py @@ -35,7 +35,9 @@ class TestRepositoryController(base.TestCase): self.context.async_section.return_value = Executor() self.ctrl = RepositoryController(self.context, self.driver, "x86_64") - def test_load_fail_if_unknown_driver(self): + @mock.patch("packetary.controllers.repository.stevedore") + def test_load_fail_if_unknown_driver(self, stevedore): + stevedore.ExtensionManager.return_value = {} with self.assertRaisesRegexp(NotImplementedError, "unknown_driver"): RepositoryController.load( self.context, @@ -51,6 +53,9 @@ class TestRepositoryController(base.TestCase): RepositoryController._drivers = None controller = RepositoryController.load(self.context, "test", "x86_64") self.assertIs(self.driver, controller.driver) + stevedore.ExtensionManager.assert_called_once_with( + "packetary.repository_drivers", invoke_on_load=True + ) def test_load_repositories(self): repo_data = {"name": "test", "uri": "file:///test1"} @@ -93,12 +98,18 @@ class TestRepositoryController(base.TestCase): clone.url = "/root/repo" self.driver.fork_repository.return_value = clone self.context.connection.retrieve.side_effect = [0, 10] - self.ctrl.fork_repository(repo, "./repo", None) + self.assertIs( + clone, + self.ctrl.fork_repository(repo, "./repo", None) + ) self.driver.fork_repository.assert_called_once_with( self.context.connection, repo, "./repo/test", None ) repo.path = "os" - self.ctrl.fork_repository(repo, "./repo", None) + self.assertIs( + clone, + self.ctrl.fork_repository(repo, "./repo", None) + ) self.driver.fork_repository.assert_called_with( self.context.connection, repo, "./repo/os", None )