Introduced packaging API

This change introduces API for building packages.
The commit includes Controller object and interface for PackagingDriver.

Change-Id: I1ce0f746c1cbc5cf8ff29d08d09175acb5d79586
This commit is contained in:
Bulat Gaifullin 2016-06-21 13:00:50 +03:00
parent f659d0ed63
commit 406bdaa7e2
9 changed files with 359 additions and 6 deletions

View File

@ -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",
]

View File

@ -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

View File

@ -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"
]

View File

@ -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)

View File

@ -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
"""

View File

@ -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'
}
)

View File

@ -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
)

View File

@ -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

View File

@ -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
)