Configuration file plugins support for nailgun
- Task serialization logic for pre/post hooks - PluginSerializer object added - API for interaction with Plugins db schema General API requests supported GET/POST /plugins GET/PUT/DELETE /plugins/<plugin_id> - Plugins and ClustersPlugins db models added ClusterPlugins used to identify enabled plugins for a given cluster - Configuration uploading logic stored in Plugin wrapper Co-authored-by: Dima Shulyak <dshulyak@mirantis.com> Co-authored-by: Evgeniy L <eli@mirantis.com> implements: blueprint cinder-neutron-plugins-in-fuel Change-Id: I986f5be9ed3c3adaf7583d1bfc546cbe705db9ec
This commit is contained in:
parent
52e777a8de
commit
7bf81cef44
@ -5,7 +5,7 @@ MarkupSafe==0.18
|
||||
Paste==1.7.5.1
|
||||
PyYAML==3.10
|
||||
requests>=1.2.3
|
||||
SQLAlchemy==0.7.9
|
||||
SQLAlchemy>=0.9.4
|
||||
Shotgun==0.1.0
|
||||
alembic==0.6.2
|
||||
amqplib==1.0.2
|
||||
|
@ -272,7 +272,6 @@ class SingleHandler(BaseHandler):
|
||||
self.validator.validate_update,
|
||||
instance=obj
|
||||
)
|
||||
|
||||
self.single.update(obj, data)
|
||||
return self.single.to_json(obj)
|
||||
|
||||
|
@ -38,7 +38,6 @@ from nailgun.task.manager import ClusterDeletionManager
|
||||
from nailgun.task.manager import ResetEnvironmentTaskManager
|
||||
from nailgun.task.manager import StopDeploymentTaskManager
|
||||
from nailgun.task.manager import UpdateEnvironmentTaskManager
|
||||
from nailgun import utils
|
||||
|
||||
|
||||
class ClusterHandler(SingleHandler):
|
||||
@ -130,9 +129,7 @@ class ClusterAttributesHandler(BaseHandler):
|
||||
if not cluster.attributes:
|
||||
raise self.http(500, "No attributes found!")
|
||||
|
||||
return {
|
||||
"editable": cluster.attributes.editable
|
||||
}
|
||||
return objects.Cluster.get_editable_attributes(cluster)
|
||||
|
||||
@content_json
|
||||
def PUT(self, cluster_id):
|
||||
@ -151,12 +148,8 @@ class ClusterAttributesHandler(BaseHandler):
|
||||
"after, or in deploy.")
|
||||
|
||||
data = self.checked_data()
|
||||
|
||||
for key, value in data.iteritems():
|
||||
setattr(cluster.attributes, key, value)
|
||||
|
||||
objects.Cluster.add_pending_changes(cluster, "attributes")
|
||||
return {"editable": cluster.attributes.editable}
|
||||
objects.Cluster.update_attributes(cluster, data)
|
||||
return objects.Cluster.get_editable_attributes(cluster)
|
||||
|
||||
@content_json
|
||||
def PATCH(self, cluster_id):
|
||||
@ -176,12 +169,8 @@ class ClusterAttributesHandler(BaseHandler):
|
||||
"after, or in deploy.")
|
||||
|
||||
data = self.checked_data()
|
||||
|
||||
cluster.attributes.editable = utils.dict_merge(
|
||||
cluster.attributes.editable, data['editable'])
|
||||
|
||||
objects.Cluster.add_pending_changes(cluster, "attributes")
|
||||
return {"editable": cluster.attributes.editable}
|
||||
objects.Cluster.patch_attributes(cluster, data)
|
||||
return objects.Cluster.get_editable_attributes(cluster)
|
||||
|
||||
|
||||
class ClusterAttributesDefaultsHandler(BaseHandler):
|
||||
|
@ -27,6 +27,8 @@ from nailgun.logger import logger
|
||||
from nailgun import objects
|
||||
|
||||
from nailgun.orchestrator import deployment_serializers
|
||||
from nailgun.orchestrator.plugins_serializers import post_deployment_serialize
|
||||
from nailgun.orchestrator.plugins_serializers import pre_deployment_serialize
|
||||
from nailgun.orchestrator import provisioning_serializers
|
||||
from nailgun.task.helpers import TaskHelper
|
||||
from nailgun.task.manager import DeploymentTaskManager
|
||||
@ -64,9 +66,6 @@ class DefaultOrchestratorInfo(NodesFilterMixin, BaseHandler):
|
||||
Need to redefine serializer variable
|
||||
"""
|
||||
|
||||
# Override this attribute
|
||||
_serializer = None
|
||||
|
||||
@content_json
|
||||
def GET(self, cluster_id):
|
||||
""":returns: JSONized default data which will be passed to orchestrator
|
||||
@ -75,8 +74,11 @@ class DefaultOrchestratorInfo(NodesFilterMixin, BaseHandler):
|
||||
"""
|
||||
cluster = self.get_object_or_404(objects.Cluster, cluster_id)
|
||||
nodes = self.get_nodes(cluster)
|
||||
return self._serializer.serialize(
|
||||
cluster, nodes, ignore_customized=True)
|
||||
|
||||
return self._serialize(cluster, nodes)
|
||||
|
||||
def _serialize(self, cluster, nodes):
|
||||
raise NotImplementedError('Override the method')
|
||||
|
||||
|
||||
class OrchestratorInfo(BaseHandler):
|
||||
@ -133,7 +135,9 @@ class OrchestratorInfo(BaseHandler):
|
||||
|
||||
class DefaultProvisioningInfo(DefaultOrchestratorInfo):
|
||||
|
||||
_serializer = provisioning_serializers
|
||||
def _serialize(self, cluster, nodes):
|
||||
return provisioning_serializers.serialize(
|
||||
cluster, nodes, ignore_customized=True)
|
||||
|
||||
def get_default_nodes(self, cluster):
|
||||
return TaskHelper.nodes_to_provision(cluster)
|
||||
@ -141,7 +145,27 @@ class DefaultProvisioningInfo(DefaultOrchestratorInfo):
|
||||
|
||||
class DefaultDeploymentInfo(DefaultOrchestratorInfo):
|
||||
|
||||
_serializer = deployment_serializers
|
||||
def _serialize(self, cluster, nodes):
|
||||
return deployment_serializers.serialize(
|
||||
cluster, nodes, ignore_customized=True)
|
||||
|
||||
def get_default_nodes(self, cluster):
|
||||
return TaskHelper.nodes_to_deploy(cluster)
|
||||
|
||||
|
||||
class DefaultPrePluginsHooksInfo(DefaultOrchestratorInfo):
|
||||
|
||||
def _serialize(self, cluster, nodes):
|
||||
return pre_deployment_serialize(cluster, nodes)
|
||||
|
||||
def get_default_nodes(self, cluster):
|
||||
return TaskHelper.nodes_to_deploy(cluster)
|
||||
|
||||
|
||||
class DefaultPostPluginsHooksInfo(DefaultOrchestratorInfo):
|
||||
|
||||
def _serialize(self, cluster, nodes):
|
||||
return post_deployment_serialize(cluster, nodes)
|
||||
|
||||
def get_default_nodes(self, cluster):
|
||||
return TaskHelper.nodes_to_deploy(cluster)
|
||||
|
46
nailgun/nailgun/api/v1/handlers/plugin.py
Normal file
46
nailgun/nailgun/api/v1/handlers/plugin.py
Normal file
@ -0,0 +1,46 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2014 Mirantis, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from nailgun.api.v1.handlers import base
|
||||
from nailgun.api.v1.handlers.base import content_json
|
||||
from nailgun.api.v1.validators import plugin
|
||||
from nailgun import objects
|
||||
|
||||
|
||||
class PluginHandler(base.SingleHandler):
|
||||
|
||||
validator = plugin.PluginValidator
|
||||
single = objects.Plugin
|
||||
|
||||
|
||||
class PluginCollectionHandler(base.CollectionHandler):
|
||||
|
||||
collection = objects.PluginCollection
|
||||
validator = plugin.PluginValidator
|
||||
|
||||
@content_json
|
||||
def POST(self):
|
||||
""":returns: JSONized REST object.
|
||||
:http: * 201 (object successfully created)
|
||||
* 400 (invalid object data specified)
|
||||
* 409 (object with such parameters already exists)
|
||||
"""
|
||||
data = self.checked_data()
|
||||
obj = self.collection.single.get_by_name_version(
|
||||
data['name'], data['version'])
|
||||
if obj:
|
||||
raise self.http(409, self.collection.single.to_json(obj))
|
||||
return super(PluginCollectionHandler, self).POST()
|
@ -55,6 +55,9 @@ from nailgun.api.v1.handlers.node import NodeCollectionHandler
|
||||
from nailgun.api.v1.handlers.node import NodeHandler
|
||||
from nailgun.api.v1.handlers.node import NodesAllocationStatsHandler
|
||||
|
||||
from nailgun.api.v1.handlers.plugin import PluginCollectionHandler
|
||||
from nailgun.api.v1.handlers.plugin import PluginHandler
|
||||
|
||||
from nailgun.api.v1.handlers.node import NodeCollectionNICsDefaultHandler
|
||||
from nailgun.api.v1.handlers.node import NodeCollectionNICsHandler
|
||||
from nailgun.api.v1.handlers.node import NodeNICsDefaultHandler
|
||||
@ -64,12 +67,15 @@ from nailgun.api.v1.handlers.notifications import NotificationCollectionHandler
|
||||
from nailgun.api.v1.handlers.notifications import NotificationHandler
|
||||
|
||||
from nailgun.api.v1.handlers.orchestrator import DefaultDeploymentInfo
|
||||
from nailgun.api.v1.handlers.orchestrator import DefaultPostPluginsHooksInfo
|
||||
from nailgun.api.v1.handlers.orchestrator import DefaultPrePluginsHooksInfo
|
||||
from nailgun.api.v1.handlers.orchestrator import DefaultProvisioningInfo
|
||||
from nailgun.api.v1.handlers.orchestrator import DeploymentInfo
|
||||
from nailgun.api.v1.handlers.orchestrator import DeploySelectedNodes
|
||||
from nailgun.api.v1.handlers.orchestrator import ProvisioningInfo
|
||||
from nailgun.api.v1.handlers.orchestrator import ProvisionSelectedNodes
|
||||
|
||||
|
||||
from nailgun.api.v1.handlers.registration import FuelKeyHandler
|
||||
from nailgun.api.v1.handlers.release import ReleaseCollectionHandler
|
||||
from nailgun.api.v1.handlers.release import ReleaseHandler
|
||||
@ -124,6 +130,10 @@ urls = (
|
||||
DefaultProvisioningInfo,
|
||||
r'/clusters/(?P<cluster_id>\d+)/generated/?$',
|
||||
ClusterGeneratedData,
|
||||
r'/clusters/(?P<cluster_id>\d+)/orchestrator/plugins_pre_hooks/?$',
|
||||
DefaultPrePluginsHooksInfo,
|
||||
r'/clusters/(?P<cluster_id>\d+)/orchestrator/plugins_post_hooks/?$',
|
||||
DefaultPostPluginsHooksInfo,
|
||||
|
||||
r'/clusters/(?P<cluster_id>\d+)/provision/?$',
|
||||
ProvisionSelectedNodes,
|
||||
@ -168,6 +178,11 @@ urls = (
|
||||
r'/tasks/(?P<obj_id>\d+)/?$',
|
||||
TaskHandler,
|
||||
|
||||
r'/plugins/(?P<obj_id>\d+)/?$',
|
||||
PluginHandler,
|
||||
r'/plugins/?$',
|
||||
PluginCollectionHandler,
|
||||
|
||||
r'/notifications/?$',
|
||||
NotificationCollectionHandler,
|
||||
r'/notifications/(?P<obj_id>\d+)/?$',
|
||||
|
34
nailgun/nailgun/api/v1/validators/plugin.py
Normal file
34
nailgun/nailgun/api/v1/validators/plugin.py
Normal file
@ -0,0 +1,34 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2014 Mirantis, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
|
||||
from nailgun.api.v1.validators.base import BasicValidator
|
||||
from nailgun.errors import errors
|
||||
|
||||
|
||||
class PluginValidator(BasicValidator):
|
||||
|
||||
@classmethod
|
||||
def validate_delete(cls, instance):
|
||||
if instance.clusters:
|
||||
raise errors.CannotDelete(
|
||||
"Can't delete plugin which is enabled"
|
||||
"for some environment."
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def validate_update(cls, data, instance):
|
||||
"""It should be possible to update plugin info."""
|
||||
return cls.validate(data)
|
@ -175,6 +175,11 @@ PROVISION_METHODS = Enum(
|
||||
'image'
|
||||
)
|
||||
|
||||
STAGES = Enum(
|
||||
'pre_deployment',
|
||||
'post_deployment'
|
||||
)
|
||||
|
||||
ACTION_TYPES = Enum(
|
||||
'http_request',
|
||||
'nailgun_task'
|
||||
|
@ -130,6 +130,28 @@ def upgrade_schema():
|
||||
default={}
|
||||
),
|
||||
sa.PrimaryKeyConstraint('id'))
|
||||
op.create_table(
|
||||
'plugins',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('name', sa.String(length=100), nullable=False),
|
||||
sa.Column('version', sa.String(length=32), nullable=False),
|
||||
sa.Column('description', sa.String(length=400), nullable=True),
|
||||
sa.Column('releases', JSON(), nullable=True),
|
||||
sa.Column('types', JSON(), nullable=True),
|
||||
sa.Column('package_version', sa.String(length=32), nullable=False),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('name', 'version', name='_name_version_unique')
|
||||
)
|
||||
op.create_table(
|
||||
'cluster_plugins',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('plugin_id', sa.Integer(), nullable=False),
|
||||
sa.Column('cluster_id', sa.Integer(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['cluster_id'], ['clusters.id'], ),
|
||||
sa.ForeignKeyConstraint(
|
||||
['plugin_id'], ['plugins.id'], ondelete='CASCADE'),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
|
||||
|
||||
def upgrade_releases():
|
||||
@ -180,6 +202,8 @@ def downgrade_schema():
|
||||
op.drop_table('action_logs')
|
||||
op.drop_table('master_node_settings')
|
||||
map(drop_enum, ENUMS)
|
||||
op.drop_table('cluster_plugins')
|
||||
op.drop_table('plugins')
|
||||
|
||||
|
||||
def downgrade_data():
|
||||
|
52
nailgun/nailgun/db/sqlalchemy/models/plugins.py
Normal file
52
nailgun/nailgun/db/sqlalchemy/models/plugins.py
Normal file
@ -0,0 +1,52 @@
|
||||
# Copyright 2014 Mirantis, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from sqlalchemy import Column
|
||||
from sqlalchemy import ForeignKey
|
||||
from sqlalchemy import Integer
|
||||
from sqlalchemy import String
|
||||
from sqlalchemy import UniqueConstraint
|
||||
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from nailgun.db.sqlalchemy.models.base import Base
|
||||
from nailgun.db.sqlalchemy.models.fields import JSON
|
||||
|
||||
|
||||
class ClusterPlugins(Base):
|
||||
|
||||
__tablename__ = 'cluster_plugins'
|
||||
id = Column(Integer, primary_key=True)
|
||||
plugin_id = Column(Integer, ForeignKey('plugins.id', ondelete='CASCADE'),
|
||||
nullable=False)
|
||||
cluster_id = Column(Integer, ForeignKey('clusters.id'))
|
||||
|
||||
|
||||
class Plugin(Base):
|
||||
|
||||
__tablename__ = 'plugins'
|
||||
|
||||
__table_args__ = (
|
||||
UniqueConstraint('name', 'version', name='_name_version_unique'),)
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
name = Column(String(100), nullable=False)
|
||||
version = Column(String(32), nullable=False)
|
||||
description = Column(String(400))
|
||||
releases = Column(JSON, default=[])
|
||||
types = Column(JSON, default=[])
|
||||
package_version = Column(String(32), nullable=False)
|
||||
clusters = relationship("Cluster",
|
||||
secondary=ClusterPlugins.__table__,
|
||||
backref="plugins")
|
@ -48,6 +48,8 @@ default_messages = {
|
||||
"TaskAlreadyRunning": "A task is already running",
|
||||
"InvalidReleaseId": "Release Id is invalid",
|
||||
"UnsupportedSerializer": "There are no serializers for a given cluster",
|
||||
"InvalidOperatingSystem": "Invalid operating system",
|
||||
"CannotFindPluginForRelease": "Cannot find plugin for the release",
|
||||
|
||||
# disk errors
|
||||
"NotEnoughFreeSpace": "Not enough free space",
|
||||
|
@ -40,3 +40,6 @@ from nailgun.objects.node import NodeCollection
|
||||
from nailgun.objects.capacity import CapacityLog
|
||||
|
||||
from nailgun.objects.master_node_settings import MasterNodeSettings
|
||||
|
||||
from nailgun.objects.plugin import Plugin
|
||||
from nailgun.objects.plugin import PluginCollection
|
||||
|
@ -33,6 +33,8 @@ from nailgun.logger import logger
|
||||
from nailgun.objects import NailgunCollection
|
||||
from nailgun.objects import NailgunObject
|
||||
|
||||
from nailgun.plugins.manager import PluginManager
|
||||
|
||||
from nailgun.settings import settings
|
||||
|
||||
from nailgun.utils import AttributesGenerator
|
||||
@ -224,6 +226,12 @@ class Cluster(NailgunObject):
|
||||
}
|
||||
)
|
||||
Attributes.generate_fields(attributes)
|
||||
# when attributes created we need to understand whether should plugin
|
||||
# be applied for created cluster
|
||||
plugin_attrs = PluginManager.get_plugin_attributes(instance)
|
||||
editable = dict(plugin_attrs, **instance.attributes.editable)
|
||||
instance.attributes.editable = editable
|
||||
db().flush()
|
||||
|
||||
@classmethod
|
||||
def get_attributes(cls, instance):
|
||||
@ -236,6 +244,29 @@ class Cluster(NailgunObject):
|
||||
models.Attributes.cluster_id == instance.id
|
||||
).first()
|
||||
|
||||
@classmethod
|
||||
def update_attributes(cls, instance, data):
|
||||
PluginManager.process_cluster_attributes(instance, data['editable'])
|
||||
|
||||
for key, value in data.iteritems():
|
||||
setattr(instance.attributes, key, value)
|
||||
cls.add_pending_changes(instance, "attributes")
|
||||
db().flush()
|
||||
|
||||
@classmethod
|
||||
def patch_attributes(cls, instance, data):
|
||||
PluginManager.process_cluster_attributes(instance, data['editable'])
|
||||
instance.attributes.editable = dict_merge(
|
||||
instance.attributes.editable, data['editable'])
|
||||
cls.add_pending_changes(instance, "attributes")
|
||||
db().flush()
|
||||
|
||||
@classmethod
|
||||
def get_editable_attributes(cls, instance):
|
||||
attrs = cls.get_attributes(instance)
|
||||
editable = attrs.editable
|
||||
return {'editable': editable}
|
||||
|
||||
@classmethod
|
||||
def get_network_manager(cls, instance=None):
|
||||
"""Get network manager for Cluster instance.
|
||||
|
37
nailgun/nailgun/objects/plugin.py
Normal file
37
nailgun/nailgun/objects/plugin.py
Normal file
@ -0,0 +1,37 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2014 Mirantis, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from nailgun.db.sqlalchemy.models import plugins as plugin_db_model
|
||||
from nailgun.objects import base
|
||||
from nailgun.objects.serializers import plugin
|
||||
|
||||
from nailgun.db import db
|
||||
|
||||
|
||||
class Plugin(base.NailgunObject):
|
||||
|
||||
model = plugin_db_model.Plugin
|
||||
serializer = plugin.PluginSerializer
|
||||
|
||||
@classmethod
|
||||
def get_by_name_version(cls, name, version):
|
||||
return db().query(cls.model).\
|
||||
filter_by(name=name, version=version).first()
|
||||
|
||||
|
||||
class PluginCollection(base.NailgunCollection):
|
||||
|
||||
single = Plugin
|
@ -93,10 +93,12 @@ class ReleaseOrchestratorData(NailgunObject):
|
||||
cls.render_path(value, context)
|
||||
|
||||
rendered_data['puppet_manifests_source'] = \
|
||||
cls.render_path(rendered_data['puppet_manifests_source'], context)
|
||||
cls.render_path(rendered_data.get(
|
||||
'puppet_manifests_source', 'default'), context)
|
||||
|
||||
rendered_data['puppet_modules_source'] = \
|
||||
cls.render_path(rendered_data['puppet_modules_source'], context)
|
||||
cls.render_path(rendered_data.get(
|
||||
'puppet_modules_source', 'default'), context)
|
||||
|
||||
return rendered_data
|
||||
|
||||
|
30
nailgun/nailgun/objects/serializers/plugin.py
Normal file
30
nailgun/nailgun/objects/serializers/plugin.py
Normal file
@ -0,0 +1,30 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2014 Mirantis, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from nailgun.objects.serializers.base import BasicSerializer
|
||||
|
||||
|
||||
class PluginSerializer(BasicSerializer):
|
||||
|
||||
fields = (
|
||||
"id",
|
||||
"name",
|
||||
"version",
|
||||
"description",
|
||||
"releases",
|
||||
"types",
|
||||
"package_version"
|
||||
)
|
@ -30,7 +30,8 @@ class ReleaseSerializer(BasicSerializer):
|
||||
"roles",
|
||||
"roles_metadata",
|
||||
"wizard_metadata",
|
||||
"state"
|
||||
"state",
|
||||
"attributes_metadata"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
|
224
nailgun/nailgun/orchestrator/plugins_serializers.py
Normal file
224
nailgun/nailgun/orchestrator/plugins_serializers.py
Normal file
@ -0,0 +1,224 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2014 Mirantis, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""Serializer for plugins tasks"""
|
||||
|
||||
|
||||
from nailgun import consts
|
||||
from nailgun.errors import errors
|
||||
from nailgun.logger import logger
|
||||
from nailgun.orchestrator.priority_serializers import PriorityStrategy
|
||||
from nailgun.plugins.manager import PluginManager
|
||||
|
||||
|
||||
def make_repo_task(uids, repo_data, repo_path):
|
||||
return {
|
||||
'type': 'upload_file',
|
||||
'uids': uids,
|
||||
'parameters': {
|
||||
'path': repo_path,
|
||||
'data': repo_data}}
|
||||
|
||||
|
||||
def make_ubuntu_repo_task(plugin_name, repo_url, uids):
|
||||
repo_data = ['deb {0}'.format(repo_url)]
|
||||
repo_path = '/etc/apt/sources.list.d/{0}.list'.format(plugin_name)
|
||||
|
||||
return make_repo_task(uids, repo_data, repo_path)
|
||||
|
||||
|
||||
def make_centos_repo_task(plugin_name, repo_url, uids):
|
||||
repo_data = '\n'.join([
|
||||
'[{0}]',
|
||||
'name=Plugin {0} repository',
|
||||
'baseurl={1}',
|
||||
'gpgcheck=0']).format(plugin_name, repo_url)
|
||||
repo_path = '/etc/yum.repos.d/{0}.repo'.format(plugin_name)
|
||||
|
||||
return make_repo_task(uids, repo_data, repo_path)
|
||||
|
||||
|
||||
def make_sync_scripts_task(uids, src, dst):
|
||||
return {
|
||||
'type': 'sync',
|
||||
'uids': uids,
|
||||
'parameters': {
|
||||
'src': src,
|
||||
'dst': dst}}
|
||||
|
||||
|
||||
def make_shell_task(uids, task, cwd):
|
||||
return {
|
||||
'type': 'shell',
|
||||
'uids': uids,
|
||||
'parameters': {
|
||||
'cmd': task['parameters']['cmd'],
|
||||
'timeout': task['parameters']['timeout'],
|
||||
'cwd': cwd}}
|
||||
|
||||
|
||||
def make_puppet_task(uids, task, cwd):
|
||||
return {
|
||||
'type': 'puppet',
|
||||
'uids': uids,
|
||||
'parameters': {
|
||||
'puppet_manifest': task['parameters']['puppet_manifest'],
|
||||
'puppet_modules': task['parameters']['puppet_modules'],
|
||||
'timeout': task['parameters']['timeout'],
|
||||
'cwd': cwd}}
|
||||
|
||||
|
||||
class BasePluginDeploymentHooksSerializer(object):
|
||||
|
||||
def __init__(self, cluster, nodes):
|
||||
self.cluster = cluster
|
||||
self.nodes = nodes
|
||||
self.priority = PriorityStrategy()
|
||||
|
||||
def deployment_tasks(self, plugins, stage):
|
||||
tasks = []
|
||||
|
||||
for plugin in plugins:
|
||||
puppet_tasks = filter(
|
||||
lambda t: (t['type'] == 'puppet' and
|
||||
t['stage'] == stage),
|
||||
plugin.tasks)
|
||||
shell_tasks = filter(
|
||||
lambda t: (t['type'] == 'shell' and
|
||||
t['stage'] == stage),
|
||||
plugin.tasks)
|
||||
|
||||
for task in shell_tasks:
|
||||
uids = self.get_uids_for_task(task)
|
||||
if not uids:
|
||||
continue
|
||||
tasks.append(make_shell_task(
|
||||
uids,
|
||||
task,
|
||||
plugin.slaves_scripts_path))
|
||||
|
||||
for task in puppet_tasks:
|
||||
uids = self.get_uids_for_task(task)
|
||||
if not uids:
|
||||
continue
|
||||
tasks.append(make_puppet_task(
|
||||
uids,
|
||||
task,
|
||||
plugin.slaves_scripts_path))
|
||||
|
||||
return tasks
|
||||
|
||||
def get_uids_for_tasks(self, tasks):
|
||||
uids = []
|
||||
for task in tasks:
|
||||
if isinstance(task['role'], list):
|
||||
for node in self.nodes:
|
||||
required_for_node = set(task['role']) & set(node.all_roles)
|
||||
if required_for_node:
|
||||
uids.append(node.id)
|
||||
elif task['role'] == '*':
|
||||
uids.extend([n.id for n in self.nodes])
|
||||
else:
|
||||
logger.warn(
|
||||
'Wrong task format, `role` should be a list or "*": %s',
|
||||
task)
|
||||
|
||||
return list(set(uids))
|
||||
|
||||
def get_uids_for_task(self, task):
|
||||
return self.get_uids_for_tasks([task])
|
||||
|
||||
|
||||
class PluginsPreDeploymentHooksSerializer(BasePluginDeploymentHooksSerializer):
|
||||
|
||||
def serialize(self):
|
||||
tasks = []
|
||||
plugins = PluginManager.get_cluster_plugins_with_tasks(self.cluster)
|
||||
tasks.extend(self.create_repositories(plugins))
|
||||
tasks.extend(self.sync_scripts(plugins))
|
||||
tasks.extend(self.deployment_tasks(plugins))
|
||||
self.priority.one_by_one(tasks)
|
||||
|
||||
return tasks
|
||||
|
||||
def create_repositories(self, plugins):
|
||||
operating_system = self.cluster.release.operating_system
|
||||
if operating_system == 'CentOS':
|
||||
make_repo = make_centos_repo_task
|
||||
elif operating_system == 'Ubuntu':
|
||||
make_repo = make_ubuntu_repo_task
|
||||
else:
|
||||
raise errors.InvalidOperatingSystem(
|
||||
'Operating system {0} is invalid'.format(operating_system))
|
||||
|
||||
repo_tasks = []
|
||||
for plugin in plugins:
|
||||
uids = self.get_uids_for_tasks(plugin.tasks)
|
||||
|
||||
# If there are not nodes for tasks execution
|
||||
# or if there are no files in repository
|
||||
if not uids or not plugin.repo_files(self.cluster):
|
||||
continue
|
||||
|
||||
repo_tasks.append(
|
||||
make_repo(
|
||||
plugin.full_name, plugin.repo_url(self.cluster), uids))
|
||||
|
||||
return repo_tasks
|
||||
|
||||
def sync_scripts(self, plugins):
|
||||
tasks = []
|
||||
for plugin in plugins:
|
||||
uids = self.get_uids_for_tasks(plugin.tasks)
|
||||
if not uids:
|
||||
continue
|
||||
tasks.append(make_sync_scripts_task(
|
||||
uids,
|
||||
plugin.master_scripts_path(self.cluster),
|
||||
plugin.slaves_scripts_path))
|
||||
|
||||
return tasks
|
||||
|
||||
def deployment_tasks(self, plugins):
|
||||
return super(
|
||||
PluginsPreDeploymentHooksSerializer, self).\
|
||||
deployment_tasks(plugins, consts.STAGES.pre_deployment)
|
||||
|
||||
|
||||
class PluginsPostDeploymentHooksSerializer(
|
||||
BasePluginDeploymentHooksSerializer):
|
||||
|
||||
def serialize(self):
|
||||
tasks = []
|
||||
plugins = PluginManager.get_cluster_plugins_with_tasks(self.cluster)
|
||||
tasks.extend(self.deployment_tasks(plugins))
|
||||
self.priority.one_by_one(tasks)
|
||||
return tasks
|
||||
|
||||
def deployment_tasks(self, plugins):
|
||||
return super(
|
||||
PluginsPostDeploymentHooksSerializer, self).\
|
||||
deployment_tasks(plugins, consts.STAGES.post_deployment)
|
||||
|
||||
|
||||
def pre_deployment_serialize(cluster, nodes):
|
||||
serializer = PluginsPreDeploymentHooksSerializer(cluster, nodes)
|
||||
return serializer.serialize()
|
||||
|
||||
|
||||
def post_deployment_serialize(cluster, nodes):
|
||||
serializer = PluginsPostDeploymentHooksSerializer(cluster, nodes)
|
||||
return serializer.serialize()
|
@ -45,30 +45,30 @@ class Priority(object):
|
||||
|
||||
|
||||
class PriorityStrategy(object):
|
||||
"""Set priorities for sequence of nodes using some strategy.
|
||||
"""Set priorities for sequence of tasks using some strategy.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
#: priority sequence generator
|
||||
self._priority = Priority()
|
||||
|
||||
def one_by_one(self, nodes):
|
||||
"""Deploy given nodes one by one."""
|
||||
for n in nodes:
|
||||
n['priority'] = self._priority.next()
|
||||
def one_by_one(self, tasks):
|
||||
"""Deploy given tasks one by one."""
|
||||
for t in tasks:
|
||||
t['priority'] = self._priority.next()
|
||||
|
||||
def in_parallel(self, nodes):
|
||||
"""Deploy given nodes in parallel mode."""
|
||||
def in_parallel(self, tasks):
|
||||
"""Deploy given tasks in parallel mode."""
|
||||
self._priority.next()
|
||||
for n in nodes:
|
||||
n['priority'] = self._priority.current
|
||||
for t in tasks:
|
||||
t['priority'] = self._priority.current
|
||||
|
||||
def in_parallel_by(self, nodes, amount):
|
||||
def in_parallel_by(self, tasks, amount):
|
||||
"""Deploy given nodes in parallel by chunks."""
|
||||
for index, node in enumerate(nodes):
|
||||
for index, task in enumerate(tasks):
|
||||
if index % amount == 0:
|
||||
self._priority.next()
|
||||
node['priority'] = self._priority.current
|
||||
task['priority'] = self._priority.current
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
|
13
nailgun/nailgun/plugins/__init__.py
Normal file
13
nailgun/nailgun/plugins/__init__.py
Normal file
@ -0,0 +1,13 @@
|
||||
# Copyright 2014 Mirantis, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
169
nailgun/nailgun/plugins/attr_plugin.py
Normal file
169
nailgun/nailgun/plugins/attr_plugin.py
Normal file
@ -0,0 +1,169 @@
|
||||
# Copyright 2014 Mirantis, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import glob
|
||||
import os
|
||||
from urlparse import urljoin
|
||||
|
||||
import yaml
|
||||
|
||||
from nailgun.logger import logger
|
||||
from nailgun.settings import settings
|
||||
|
||||
|
||||
class ClusterAttributesPlugin(object):
|
||||
"""Implements wrapper for plugin db model to provide
|
||||
logic related to configuration files.
|
||||
1. Uploading plugin provided cluster attributes
|
||||
2. Uploading tasks
|
||||
3. Enabling/Disabling of plugin based on cluster attributes
|
||||
4. Providing repositories/deployment scripts related info to clients
|
||||
"""
|
||||
|
||||
environment_config_name = 'environment_config.yaml'
|
||||
task_config_name = 'tasks.yaml'
|
||||
|
||||
def __init__(self, plugin):
|
||||
self.plugin = plugin
|
||||
self.plugin_path = os.path.join(
|
||||
settings.PLUGINS_PATH,
|
||||
self.full_name)
|
||||
self.config_file = os.path.join(
|
||||
self.plugin_path,
|
||||
self.environment_config_name)
|
||||
self.tasks = []
|
||||
|
||||
def _load_config(self, config):
|
||||
if os.access(config, os.R_OK):
|
||||
with open(config, "r") as conf:
|
||||
return yaml.load(conf.read())
|
||||
else:
|
||||
logger.warning("Config {0} is not readable.".format(config))
|
||||
|
||||
def get_plugin_attributes(self, cluster):
|
||||
"""Should be used for initial configuration uploading to
|
||||
custom storage. Will be invoked in 2 cases:
|
||||
1. Cluster is created but there was no plugins in system
|
||||
on that time, so when plugin is uploaded we need to iterate
|
||||
over all clusters and decide if plugin should be applied
|
||||
2. Plugins is uploaded before cluster creation, in this case
|
||||
we will iterate over all plugins and upload configuration for them
|
||||
|
||||
In this case attributes will be added to same cluster attributes
|
||||
model and stored in editable field
|
||||
"""
|
||||
config = {}
|
||||
if os.path.exists(self.config_file):
|
||||
config = self._load_config(self.config_file)
|
||||
if cluster.release.version in self.plugin_release_versions:
|
||||
attrs = config.get("attributes", {})
|
||||
attrs.update(self.metadata)
|
||||
return {self.plugin.name: attrs}
|
||||
return {}
|
||||
|
||||
def process_cluster_attributes(self, cluster, cluster_attrs):
|
||||
"""Checks cluster attributes for plugin related metadata.
|
||||
Then enable or disable plugin for cluster based on metadata
|
||||
enabled field.
|
||||
"""
|
||||
custom_attrs = cluster_attrs.get(self.plugin.name, {})
|
||||
if custom_attrs:
|
||||
enable = custom_attrs['metadata']['enabled']
|
||||
# value is true and plugin is not enabled for this cluster
|
||||
# that means plugin was enabled on this request
|
||||
if enable and cluster not in self.plugin.clusters:
|
||||
self.plugin.clusters.append(cluster)
|
||||
# value is false and plugin is enabled for this cluster
|
||||
# that means plugin was disabled on this request
|
||||
elif not enable and cluster in self.plugin.clusters:
|
||||
self.plugin.clusters.remove(cluster)
|
||||
|
||||
@property
|
||||
def metadata(self):
|
||||
return {u'metadata': {u'enabled': False, u'toggleable': True,
|
||||
u'weight': 70, u'label': self.plugin.name}}
|
||||
|
||||
def set_cluster_tasks(self, cluster):
|
||||
"""Loads plugins provided tasks from tasks config file and
|
||||
sets them to instance tasks variable.
|
||||
"""
|
||||
task_yaml = os.path.join(
|
||||
self.plugin_path,
|
||||
self.task_config_name)
|
||||
if os.path.exists(task_yaml):
|
||||
self.tasks = self._load_config(task_yaml)
|
||||
|
||||
def filter_tasks(self, tasks, stage):
|
||||
filtered = []
|
||||
for task in tasks:
|
||||
if stage and stage == task.get('stage'):
|
||||
filtered.append(task)
|
||||
return filtered
|
||||
|
||||
@property
|
||||
def plugin_release_versions(self):
|
||||
if not self.plugin.releases:
|
||||
return set()
|
||||
return set([rel['version'] for rel in self.plugin.releases])
|
||||
|
||||
@property
|
||||
def full_name(self):
|
||||
return u'{0}-{1}'.format(self.plugin.name, self.plugin.version)
|
||||
|
||||
def get_release_info(self, release):
|
||||
"""Returns plugin release information which corresponds to
|
||||
a provided release.
|
||||
"""
|
||||
os = release.operating_system.lower()
|
||||
version = release.version
|
||||
|
||||
release_info = filter(
|
||||
lambda r: (r['os'] == os and
|
||||
r['version'] == version),
|
||||
self.plugin.releases)
|
||||
|
||||
return release_info[0]
|
||||
|
||||
@property
|
||||
def slaves_scripts_path(self):
|
||||
return settings.PLUGINS_SLAVES_SCRIPTS_PATH.format(
|
||||
plugin_name=self.full_name)
|
||||
|
||||
def repo_files(self, cluster):
|
||||
release_info = self.get_release_info(cluster.release)
|
||||
repo_path = os.path.join(
|
||||
settings.PLUGINS_PATH,
|
||||
self.full_name,
|
||||
release_info['repository_path'],
|
||||
'*')
|
||||
return glob.glob(repo_path)
|
||||
|
||||
def repo_url(self, cluster):
|
||||
release_info = self.get_release_info(cluster.release)
|
||||
repo_base = settings.PLUGINS_REPO_URL.format(
|
||||
master_ip=settings.MASTER_IP,
|
||||
plugin_name=self.full_name)
|
||||
|
||||
return urljoin(repo_base, release_info['repository_path'])
|
||||
|
||||
def master_scripts_path(self, cluster):
|
||||
release_info = self.get_release_info(cluster.release)
|
||||
# NOTE(eli): we cannot user urljoin here, because it
|
||||
# works wrong in case, if protocol is rsync
|
||||
base_url = settings.PLUGINS_SLAVES_RSYNC.format(
|
||||
master_ip=settings.MASTER_IP,
|
||||
plugin_name=self.full_name)
|
||||
return '{0}{1}'.format(
|
||||
base_url,
|
||||
release_info['deployment_scripts_path'])
|
46
nailgun/nailgun/plugins/manager.py
Normal file
46
nailgun/nailgun/plugins/manager.py
Normal file
@ -0,0 +1,46 @@
|
||||
# Copyright 2014 Mirantis, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from nailgun.objects.plugin import PluginCollection
|
||||
|
||||
from nailgun.plugins.attr_plugin import ClusterAttributesPlugin
|
||||
|
||||
|
||||
class PluginManager(object):
|
||||
|
||||
@classmethod
|
||||
def process_cluster_attributes(cls, cluster, attrs, query=None):
|
||||
if query is None:
|
||||
query = PluginCollection.all()
|
||||
for plugin_db in query:
|
||||
attr_plugin = ClusterAttributesPlugin(plugin_db)
|
||||
attr_plugin.process_cluster_attributes(cluster, attrs)
|
||||
|
||||
@classmethod
|
||||
def get_plugin_attributes(cls, cluster):
|
||||
plugins_attrs = {}
|
||||
for plugin_db in PluginCollection.all():
|
||||
attr_plugin = ClusterAttributesPlugin(plugin_db)
|
||||
attrs = attr_plugin.get_plugin_attributes(cluster)
|
||||
plugins_attrs.update(attrs)
|
||||
return plugins_attrs
|
||||
|
||||
@classmethod
|
||||
def get_cluster_plugins_with_tasks(cls, cluster):
|
||||
attr_plugins = []
|
||||
for plugin_db in cluster.plugins:
|
||||
attr_pl = ClusterAttributesPlugin(plugin_db)
|
||||
attr_pl.set_cluster_tasks(cluster)
|
||||
attr_plugins.append(attr_pl)
|
||||
return attr_plugins
|
@ -67,6 +67,11 @@ DEFAULT_REPO:
|
||||
centos: "http://{MASTER_IP}:8080/centos/x86_64"
|
||||
ubuntu: "http://{MASTER_IP}:8080/ubuntu/x86_64 precise main"
|
||||
|
||||
PLUGINS_PATH: '/var/www/nailgun/plugins'
|
||||
PLUGINS_SLAVES_SCRIPTS_PATH: '/etc/fuel/plugins/{plugin_name}/'
|
||||
PLUGINS_REPO_URL: 'http://{master_ip}:8080/plugins/{plugin_name}/'
|
||||
PLUGINS_SLAVES_RSYNC: 'rsync://{master_ip}:/plugins/{plugin_name}/'
|
||||
|
||||
APP_LOG: &nailgun_log "/var/log/nailgun/app.log"
|
||||
API_LOG: &api_log "/var/log/nailgun/api.log"
|
||||
SYSLOG_DIR: &remote_syslog_dir "/var/log/remote/"
|
||||
|
@ -38,6 +38,7 @@ from nailgun.errors import errors
|
||||
from nailgun.logger import logger
|
||||
from nailgun.network.checker import NetworkCheck
|
||||
from nailgun.orchestrator import deployment_serializers
|
||||
from nailgun.orchestrator import plugins_serializers
|
||||
from nailgun.orchestrator import provisioning_serializers
|
||||
from nailgun.settings import settings
|
||||
from nailgun.task.fake import FAKE_THREADS
|
||||
@ -135,9 +136,12 @@ class DeploymentTask(object):
|
||||
db().add(n)
|
||||
db().flush()
|
||||
|
||||
# here we replace deployment data if user redefined them
|
||||
serialized_cluster = deployment_serializers.serialize(
|
||||
task.cluster, nodes)
|
||||
pre_deployment = plugins_serializers.pre_deployment_serialize(
|
||||
task.cluster, nodes)
|
||||
post_deployment = plugins_serializers.post_deployment_serialize(
|
||||
task.cluster, nodes)
|
||||
|
||||
# After serialization set pending_addition to False
|
||||
for node in nodes:
|
||||
@ -149,7 +153,9 @@ class DeploymentTask(object):
|
||||
'deploy_resp',
|
||||
{
|
||||
'task_uuid': task.uuid,
|
||||
'deployment_info': serialized_cluster
|
||||
'deployment_info': serialized_cluster,
|
||||
'pre_deployment': pre_deployment,
|
||||
'post_deployment': post_deployment
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -246,6 +246,8 @@ class TestHandlers(BaseIntegrationTest):
|
||||
|
||||
deployment_msg['args']['task_uuid'] = deploy_task_uuid
|
||||
deployment_msg['args']['deployment_info'] = deployment_info
|
||||
deployment_msg['args']['pre_deployment'] = []
|
||||
deployment_msg['args']['post_deployment'] = []
|
||||
|
||||
provision_nodes = []
|
||||
admin_net = self.env.network_manager.get_admin_network_group()
|
||||
@ -680,6 +682,8 @@ class TestHandlers(BaseIntegrationTest):
|
||||
|
||||
deployment_msg['args']['task_uuid'] = deploy_task_uuid
|
||||
deployment_msg['args']['deployment_info'] = deployment_info
|
||||
deployment_msg['args']['pre_deployment'] = []
|
||||
deployment_msg['args']['post_deployment'] = []
|
||||
|
||||
provision_nodes = []
|
||||
admin_net = self.env.network_manager.get_admin_network_group()
|
||||
|
217
nailgun/nailgun/test/integration/test_plugins_api.py
Normal file
217
nailgun/nailgun/test/integration/test_plugins_api.py
Normal file
@ -0,0 +1,217 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2014 Mirantis, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
|
||||
import mock
|
||||
import yaml
|
||||
|
||||
from nailgun import objects
|
||||
from nailgun.openstack.common import jsonutils
|
||||
from nailgun.test import base
|
||||
|
||||
|
||||
SAMPLE_PLUGIN = {
|
||||
'version': '1.1.0',
|
||||
'name': 'testing',
|
||||
'package_version': '1',
|
||||
'description': 'Enable to use plugin X for Neutron',
|
||||
'types': ['nailgun', 'repository', 'deployment_scripts'],
|
||||
'fuel_version': 6.0,
|
||||
'releases': [
|
||||
{'repository_path': 'repositories/ubuntu',
|
||||
'version': '2014.2-6.0', 'os': 'ubuntu',
|
||||
'mode': ['ha', 'multinode'],
|
||||
'deployment_scripts_path': 'deployment_scripts/'},
|
||||
{'repository_path': 'repositories/centos',
|
||||
'version': '2014.2-6.0', 'os': 'centos',
|
||||
'mode': ['ha', 'multinode'],
|
||||
'deployment_scripts_path': 'deployment_scripts/'}]}
|
||||
|
||||
ENVIRONMENT_CONFIG = {
|
||||
'attributes': {
|
||||
'lbaas_simple_text': {
|
||||
'value': 'Set default value',
|
||||
'type': 'text',
|
||||
'description': 'Description for text field',
|
||||
'weight': 25,
|
||||
'label': 'Text field'}}}
|
||||
|
||||
TASKS_CONFIG = [
|
||||
{'priority': 10,
|
||||
'role': ['controller'],
|
||||
'type': 'shell',
|
||||
'parameters': {'cmd': './lbaas_enable.sh', 'timeout': 42},
|
||||
'stage': 'post_deployment'},
|
||||
{'priority': 10,
|
||||
'role': '*',
|
||||
'type': 'shell',
|
||||
'parameters': {'cmd': 'echo all > /tmp/plugin.all', 'timeout': 42},
|
||||
'stage': 'pre_deployment'}]
|
||||
|
||||
|
||||
def get_config(config):
|
||||
def _get_config(*args):
|
||||
return mock.mock_open(read_data=yaml.dump(config))()
|
||||
return _get_config
|
||||
|
||||
|
||||
class BasePluginTest(base.BaseIntegrationTest):
|
||||
|
||||
def create_plugin(self):
|
||||
resp = self.app.post(
|
||||
base.reverse('PluginCollectionHandler'),
|
||||
jsonutils.dumps(SAMPLE_PLUGIN),
|
||||
headers=self.default_headers
|
||||
)
|
||||
return resp
|
||||
|
||||
def delete_plugin(self, plugin_id):
|
||||
resp = self.app.delete(
|
||||
base.reverse('PluginHandler', {'obj_id': plugin_id}),
|
||||
headers=self.default_headers
|
||||
)
|
||||
return resp
|
||||
|
||||
def create_cluster(self, nodes=None):
|
||||
nodes = nodes if nodes else []
|
||||
with mock.patch('nailgun.plugins.attr_plugin.os') as os:
|
||||
with mock.patch('nailgun.plugins.attr_plugin.open',
|
||||
create=True) as f_m:
|
||||
os.access.return_value = True
|
||||
os.path.exists.return_value = True
|
||||
f_m.side_effect = get_config(ENVIRONMENT_CONFIG)
|
||||
self.env.create(
|
||||
release_kwargs={'version': '2014.2-6.0',
|
||||
'operating_system': 'Ubuntu'},
|
||||
nodes_kwargs=nodes)
|
||||
return self.env.clusters[0]
|
||||
|
||||
def modify_plugin(self, cluster, plugin_name, enabled):
|
||||
editable_attrs = cluster.attributes.editable
|
||||
editable_attrs[plugin_name]['metadata']['enabled'] = enabled
|
||||
resp = self.app.put(
|
||||
base.reverse('ClusterAttributesHandler',
|
||||
{'cluster_id': cluster.id}),
|
||||
jsonutils.dumps({'editable': editable_attrs}),
|
||||
headers=self.default_headers)
|
||||
return resp
|
||||
|
||||
def enable_plugin(self, cluster, plugin_name):
|
||||
return self.modify_plugin(cluster, plugin_name, True)
|
||||
|
||||
def disable_plugin(self, cluster, plugin_name):
|
||||
return self.modify_plugin(cluster, plugin_name, False)
|
||||
|
||||
def get_pre_hooks(self, cluster):
|
||||
with mock.patch('nailgun.plugins.attr_plugin.glob') as glob:
|
||||
glob.glob.return_value = ['/some/path']
|
||||
with mock.patch('nailgun.plugins.attr_plugin.os') as os:
|
||||
with mock.patch('nailgun.plugins.attr_plugin.open',
|
||||
create=True) as f_m:
|
||||
os.access.return_value = True
|
||||
os.path.exists.return_value = True
|
||||
f_m.side_effect = get_config(TASKS_CONFIG)
|
||||
resp = self.app.get(
|
||||
base.reverse('DefaultPrePluginsHooksInfo',
|
||||
{'cluster_id': cluster.id}),
|
||||
headers=self.default_headers)
|
||||
return resp
|
||||
|
||||
def get_post_hooks(self, cluster):
|
||||
with mock.patch('nailgun.plugins.attr_plugin.os') as os:
|
||||
with mock.patch('nailgun.plugins.attr_plugin.open',
|
||||
create=True) as f_m:
|
||||
os.access.return_value = True
|
||||
os.path.exists.return_value = True
|
||||
f_m.side_effect = get_config(TASKS_CONFIG)
|
||||
resp = self.app.get(
|
||||
base.reverse('DefaultPostPluginsHooksInfo',
|
||||
{'cluster_id': cluster.id}),
|
||||
headers=self.default_headers)
|
||||
return resp
|
||||
|
||||
|
||||
class TestPluginsApi(BasePluginTest):
|
||||
|
||||
def test_plugin_created_on_post(self):
|
||||
resp = self.create_plugin()
|
||||
self.assertEqual(resp.status_code, 201)
|
||||
|
||||
def test_env_create_and_load_env_config(self):
|
||||
self.create_plugin()
|
||||
cluster = self.create_cluster()
|
||||
self.assertIn(SAMPLE_PLUGIN['name'], cluster.attributes.editable)
|
||||
|
||||
def test_enable_disable_plugin(self):
|
||||
resp = self.create_plugin()
|
||||
plugin = objects.Plugin.get_by_uid(resp.json['id'])
|
||||
cluster = self.create_cluster()
|
||||
self.assertEqual(plugin.clusters, [])
|
||||
resp = self.enable_plugin(cluster, plugin.name)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertIn(cluster, plugin.clusters)
|
||||
resp = self.disable_plugin(cluster, plugin.name)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertEqual(plugin.clusters, [])
|
||||
|
||||
def test_delete_plugin(self):
|
||||
resp = self.create_plugin()
|
||||
del_resp = self.delete_plugin(resp.json['id'])
|
||||
self.assertEqual(del_resp.status_code, 204)
|
||||
|
||||
def test_update_plugin(self):
|
||||
resp = self.create_plugin()
|
||||
data = resp.json
|
||||
data['package_version'] = 2
|
||||
plugin_id = data.pop('id')
|
||||
resp = self.app.put(
|
||||
base.reverse('PluginHandler', {'obj_id': plugin_id}),
|
||||
jsonutils.dumps(data),
|
||||
headers=self.default_headers
|
||||
)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
updated_data = resp.json
|
||||
updated_data.pop('id')
|
||||
self.assertEqual(updated_data, data)
|
||||
|
||||
|
||||
class TestPrePostHooks(BasePluginTest):
|
||||
|
||||
def setUp(self):
|
||||
super(TestPrePostHooks, self).setUp()
|
||||
self.create_plugin()
|
||||
self.cluster = self.create_cluster([
|
||||
{'roles': ['controller'], 'pending_addition': True},
|
||||
{'roles': ['compute'], 'pending_addition': True}])
|
||||
self.enable_plugin(self.cluster, SAMPLE_PLUGIN['name'])
|
||||
|
||||
def test_generate_pre_hooks(self):
|
||||
tasks = self.get_pre_hooks(self.cluster).json
|
||||
upload_file = [t for t in tasks if t['type'] == 'upload_file']
|
||||
rsync = [t for t in tasks if t['type'] == 'sync']
|
||||
self.assertEqual(len(upload_file), 1)
|
||||
self.assertEqual(len(rsync), 1)
|
||||
for t in tasks:
|
||||
#shoud uid be a string
|
||||
self.assertEqual(
|
||||
sorted(t['uids']), sorted([n.id for n in self.cluster.nodes]))
|
||||
|
||||
def test_generate_post_hooks(self):
|
||||
tasks = self.get_post_hooks(self.cluster).json
|
||||
self.assertEqual(len(tasks), 1)
|
||||
controller_id = [n.id for n in self.cluster.nodes
|
||||
if 'controller' in n.roles]
|
||||
self.assertEqual(controller_id, tasks[0]['uids'])
|
140
nailgun/nailgun/test/unit/test_attributes_plugin.py
Normal file
140
nailgun/nailgun/test/unit/test_attributes_plugin.py
Normal file
@ -0,0 +1,140 @@
|
||||
# Copyright 2014 Mirantis, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
|
||||
import os
|
||||
|
||||
import mock
|
||||
import yaml
|
||||
|
||||
from nailgun.db import db
|
||||
from nailgun.objects import Plugin
|
||||
from nailgun.plugins import attr_plugin
|
||||
from nailgun.settings import settings
|
||||
from nailgun.test import base
|
||||
|
||||
|
||||
SAMPLE_PLUGIN = {
|
||||
'version': '0.1.0',
|
||||
'name': 'testing_plugin',
|
||||
'package_version': '1',
|
||||
'description': 'Enable to use plugin X for Neutron',
|
||||
'types': ['nailgun', 'repository', 'deployment_scripts'],
|
||||
'fuel_version': 6.0,
|
||||
'releases': [
|
||||
{'repository_path': 'repositories/ubuntu',
|
||||
'version': '2014.2-6.0', 'os': 'ubuntu',
|
||||
'mode': ['ha', 'multinode'],
|
||||
'deployment_scripts_path': 'deployment_scripts/'},
|
||||
{'repository_path': 'repositories/centos',
|
||||
'version': '2014.2-6.0', 'os': 'centos',
|
||||
'mode': ['ha', 'multinode'],
|
||||
'deployment_scripts_path': 'deployment_scripts/'}]}
|
||||
|
||||
ENVIRONMENT_CONFIG = {
|
||||
'attributes': {
|
||||
'lbaas_simple_text': {
|
||||
'value': 'Set default value',
|
||||
'type': 'text',
|
||||
'description': 'Description for text field',
|
||||
'weight': 25,
|
||||
'label': 'Text field'}}}
|
||||
|
||||
|
||||
def get_config(*args):
|
||||
return mock.mock_open(read_data=yaml.dump(ENVIRONMENT_CONFIG))()
|
||||
|
||||
|
||||
class TestPlugin(base.BaseTestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestPlugin, self).setUp()
|
||||
self.plugin = Plugin.create(SAMPLE_PLUGIN)
|
||||
self.env.create(
|
||||
release_kwargs={'version': '2014.2-6.0',
|
||||
'operating_system': 'Ubuntu'})
|
||||
self.cluster = self.env.clusters[0]
|
||||
self.attr_plugin = attr_plugin.ClusterAttributesPlugin(self.plugin)
|
||||
db().flush()
|
||||
|
||||
@mock.patch('nailgun.plugins.attr_plugin.open', create=True)
|
||||
@mock.patch('nailgun.plugins.attr_plugin.os.access')
|
||||
@mock.patch('nailgun.plugins.attr_plugin.os.path.exists')
|
||||
def test_get_plugin_attributes(self, mexists, maccess, mopen):
|
||||
"""Should load attributes from environment_config.
|
||||
Attributes should contain provided attributes by plugin and
|
||||
also generated metadata
|
||||
"""
|
||||
maccess.return_value = True
|
||||
mexists.return_value = True
|
||||
mopen.side_effect = get_config
|
||||
attributes = self.attr_plugin.get_plugin_attributes(
|
||||
self.cluster)
|
||||
self.assertEqual(
|
||||
attributes['testing_plugin']['lbaas_simple_text'],
|
||||
ENVIRONMENT_CONFIG['attributes']['lbaas_simple_text'])
|
||||
self.assertEqual(
|
||||
attributes['testing_plugin']['metadata'],
|
||||
self.attr_plugin.metadata['metadata'])
|
||||
|
||||
def test_plugin_release_versions(self):
|
||||
"""Helper should return set of all release versions this plugin
|
||||
is applicable to.
|
||||
"""
|
||||
self.assertEqual(
|
||||
self.attr_plugin.plugin_release_versions, set(['2014.2-6.0']))
|
||||
|
||||
def test_full_name(self):
|
||||
"""Plugin full name should be made from name and version."""
|
||||
self.assertEqual(
|
||||
self.attr_plugin.full_name,
|
||||
'{0}-{1}'.format(self.plugin.name, self.plugin.version))
|
||||
|
||||
def test_get_release_info(self):
|
||||
"""Should return 1st plugin release info which matches
|
||||
provided release.
|
||||
"""
|
||||
release = self.attr_plugin.get_release_info(self.cluster.release)
|
||||
self.assertEqual(release, SAMPLE_PLUGIN['releases'][0])
|
||||
|
||||
def test_slaves_scripts_path(self):
|
||||
expected = settings.PLUGINS_SLAVES_SCRIPTS_PATH.format(
|
||||
plugin_name=self.attr_plugin.full_name)
|
||||
self.assertEqual(expected, self.attr_plugin.slaves_scripts_path)
|
||||
|
||||
@mock.patch('nailgun.plugins.attr_plugin.glob')
|
||||
def test_repo_files(self, glob_mock):
|
||||
self.attr_plugin.repo_files(self.cluster)
|
||||
expected_call = os.path.join(
|
||||
settings.PLUGINS_PATH,
|
||||
self.attr_plugin.full_name,
|
||||
'repositories/ubuntu',
|
||||
'*')
|
||||
glob_mock.glob.assert_called_once_with(expected_call)
|
||||
|
||||
@mock.patch('nailgun.plugins.attr_plugin.urljoin')
|
||||
def test_repo_url(self, murljoin):
|
||||
self.attr_plugin.repo_url(self.cluster)
|
||||
repo_base = settings.PLUGINS_REPO_URL.format(
|
||||
master_ip=settings.MASTER_IP,
|
||||
plugin_name=self.attr_plugin.full_name)
|
||||
murljoin.assert_called_once_with(repo_base, 'repositories/ubuntu')
|
||||
|
||||
def test_master_scripts_path(self):
|
||||
base_url = settings.PLUGINS_SLAVES_RSYNC.format(
|
||||
master_ip=settings.MASTER_IP,
|
||||
plugin_name=self.attr_plugin.full_name)
|
||||
expected = '{0}{1}'.format(base_url, 'deployment_scripts/')
|
||||
self.assertEqual(
|
||||
expected, self.attr_plugin.master_scripts_path(self.cluster))
|
@ -111,8 +111,8 @@ class TestHandlers(BaseIntegrationTest):
|
||||
'5.1': 'http://{MASTER_IP}:8080/centos/x86_64',
|
||||
'5.1-user': 'http://{MASTER_IP}:8080/centos-user/x86_64',
|
||||
},
|
||||
'puppet_manifests_source': 'rsync://{MASTER_IP}:/puppet/modules/',
|
||||
'puppet_modules_source': 'rsync://{MASTER_IP}:/puppet/manifests/',
|
||||
'puppet_modules_source': 'rsync://{MASTER_IP}:/puppet/modules/',
|
||||
'puppet_manifests_source': 'rsync://{MASTER_IP}:/puppet/manifests/'
|
||||
}
|
||||
|
||||
resp = self.app.put(
|
||||
@ -131,8 +131,8 @@ class TestHandlers(BaseIntegrationTest):
|
||||
'5.1': 'http://127.0.0.1:8080/centos/x86_64',
|
||||
'5.1-user': 'http://127.0.0.1:8080/centos-user/x86_64'})
|
||||
self.assertEqual(
|
||||
orchestrator_data['puppet_manifests_source'],
|
||||
orchestrator_data['puppet_modules_source'],
|
||||
'rsync://127.0.0.1:/puppet/modules/')
|
||||
self.assertEqual(
|
||||
orchestrator_data['puppet_modules_source'],
|
||||
orchestrator_data['puppet_manifests_source'],
|
||||
'rsync://127.0.0.1:/puppet/manifests/')
|
||||
|
@ -4,7 +4,7 @@ Mako==0.9.1
|
||||
MarkupSafe==0.18
|
||||
Paste==1.7.5.1
|
||||
PyYAML==3.10
|
||||
SQLAlchemy>=0.7.9,<=0.9.99
|
||||
SQLAlchemy>=0.9.4
|
||||
Shotgun==0.1.0
|
||||
alembic>=0.6.2
|
||||
amqplib==1.0.2
|
||||
|
Loading…
Reference in New Issue
Block a user