Merge "Introduced packaging API"
This commit is contained in:
commit
c88b21ee95
|
@ -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",
|
||||
]
|
||||
|
|
|
@ -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
|
|
@ -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"
|
||||
]
|
||||
|
|
|
@ -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)
|
|
@ -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
|
||||
"""
|
||||
|
|
|
@ -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'
|
||||
}
|
||||
)
|
|
@ -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
|
||||
)
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
Loading…
Reference in New Issue