diff --git a/etc/reddwarf/reddwarf.conf.sample b/etc/reddwarf/reddwarf.conf.sample index d294a7f07a..729d4389a8 100644 --- a/etc/reddwarf/reddwarf.conf.sample +++ b/etc/reddwarf/reddwarf.conf.sample @@ -81,6 +81,12 @@ agent_call_high_timeout = 150 # Reboot time out for instances reboot_time_out = 60 +# Reddwarf Security Groups for Instances +reddwarf_security_groups_support = True +reddwarf_security_group_rule_protocol = tcp +reddwarf_security_group_rule_port = 3306 + + # ============ notifer queue kombu connection options ======================== notifier_queue_hostname = localhost diff --git a/reddwarf/common/cfg.py b/reddwarf/common/cfg.py index 17abf0b075..679d9c1854 100644 --- a/reddwarf/common/cfg.py +++ b/reddwarf/common/cfg.py @@ -108,6 +108,9 @@ common_opts = [ cfg.IntOpt('http_put_rate', default=200), cfg.BoolOpt('hostname_require_ipv4', default=True, help="Require user hostnames to be IPv4 addresses."), + cfg.BoolOpt('reddwarf_security_groups_support', default=True), + cfg.StrOpt('reddwarf_security_group_rule_protocol', default='tcp'), + cfg.IntOpt('reddwarf_security_group_rule_port', default=3306), ] diff --git a/reddwarf/common/exception.py b/reddwarf/common/exception.py index f0eea1ff25..6fa52ca1b1 100644 --- a/reddwarf/common/exception.py +++ b/reddwarf/common/exception.py @@ -23,7 +23,6 @@ from reddwarf.openstack.common.gettextutils import _ from webob import exc - ClientConnectionError = openstack_exception.ClientConnectionError ProcessExecutionError = processutils.ProcessExecutionError DatabaseMigrationError = openstack_exception.DatabaseMigrationError @@ -230,3 +229,23 @@ class BackupCreationError(ReddwarfError): class BackupUpdateError(ReddwarfError): message = _("Unable to update Backup table in db") + + +class SecurityGroupCreationError(ReddwarfError): + + message = _("Failed to create Security Group.") + + +class SecurityGroupDeletionError(ReddwarfError): + + message = _("Failed to delete Security Group.") + + +class SecurityGroupRuleCreationError(ReddwarfError): + + message = _("Failed to create Security Group Rule.") + + +class SecurityGroupRuleDeletionError(ReddwarfError): + + message = _("Failed to delete Security Group Rule.") diff --git a/reddwarf/common/wsgi.py b/reddwarf/common/wsgi.py index d68562c75f..48b8684dcc 100644 --- a/reddwarf/common/wsgi.py +++ b/reddwarf/common/wsgi.py @@ -86,6 +86,12 @@ CUSTOM_SERIALIZER_METADATA = { 'database': {'name': ''}, 'user': {'name': '', 'password': ''}, 'account': {'id': ''}, + 'security_group': {'id': '', 'name': '', 'description': '', 'user': '', + 'tenant_id': ''}, + 'security_group_rule': {'id': '', 'group_id': '', 'protocol': '', + 'from_port': '', 'to_port': '', 'cidr': ''}, + 'security_group_instance_association': {'id': '', 'security_group_id': '', + 'instance_id': ''}, # mgmt/host 'host': {'instanceCount': '', 'name': '', 'usedRAM': '', 'totalRAM': '', 'percentUsed': ''}, @@ -104,6 +110,7 @@ CUSTOM_SERIALIZER_METADATA = { 'version': '', 'vmRss': '', 'fdSize': ''}, #mgmt/instance/root 'root_history': {'enabled': '', 'id': '', 'user': ''}, + } @@ -373,7 +380,7 @@ class Controller(object): exception.ModelNotFoundError, exception.UserNotFound, exception.DatabaseNotFound, - exception.QuotaResourceUnknown + exception.QuotaResourceUnknown, ], webob.exc.HTTPConflict: [], webob.exc.HTTPRequestEntityTooLarge: [ diff --git a/reddwarf/db/models.py b/reddwarf/db/models.py index 14a654770b..ceb27ce514 100644 --- a/reddwarf/db/models.py +++ b/reddwarf/db/models.py @@ -29,7 +29,10 @@ class DatabaseModelBase(models.ModelBase): @classmethod def create(cls, **values): - values['id'] = utils.generate_uuid() + if 'id' not in values: + values['id'] = utils.generate_uuid() + if hasattr(cls, 'deleted') and 'deleted' not in values: + values['deleted'] = False values['created'] = utils.utcnow() instance = cls(**values).save() if not instance.is_valid(): @@ -40,6 +43,10 @@ class DatabaseModelBase(models.ModelBase): def db_api(self): return get_db_api() + @property + def preserve_on_delete(self): + return hasattr(self, 'deleted') and hasattr(self, 'deleted_at') + def save(self): if not self.is_valid(): raise exception.InvalidModelError(errors=self.errors) @@ -52,7 +59,13 @@ class DatabaseModelBase(models.ModelBase): self['updated'] = utils.utcnow() LOG.debug(_("Deleting %s: %s") % (self.__class__.__name__, self.__dict__)) - return self.db_api.delete(self) + + if self.preserve_on_delete: + self['deleted_at'] = utils.utcnow() + self['deleted'] = True + return self.db_api.save(self) + else: + return self.db_api.delete(self) def update(self, **values): for key in values: diff --git a/reddwarf/db/sqlalchemy/mappers.py b/reddwarf/db/sqlalchemy/mappers.py index 6b1e46a946..5055ce38bf 100644 --- a/reddwarf/db/sqlalchemy/mappers.py +++ b/reddwarf/db/sqlalchemy/mappers.py @@ -46,6 +46,13 @@ def map(engine, models): Table('reservations', meta, autoload=True)) orm.mapper(models['backups'], Table('backups', meta, autoload=True)) + orm.mapper(models['security_group'], + Table('security_groups', meta, autoload=True)) + orm.mapper(models['security_group_rule'], + Table('security_group_rules', meta, autoload=True)) + orm.mapper(models['security_group_instance_association'], + Table('security_group_instance_associations', meta, + autoload=True)) def mapping_exists(model): diff --git a/reddwarf/db/sqlalchemy/migrate_repo/versions/013_add_security_group_artifacts.py b/reddwarf/db/sqlalchemy/migrate_repo/versions/013_add_security_group_artifacts.py new file mode 100644 index 0000000000..cd1e450674 --- /dev/null +++ b/reddwarf/db/sqlalchemy/migrate_repo/versions/013_add_security_group_artifacts.py @@ -0,0 +1,98 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 OpenStack LLC. +# All Rights Reserved. +# +# 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 ForeignKey +from sqlalchemy.schema import Column +from sqlalchemy.schema import MetaData + +from reddwarf.db.sqlalchemy.migrate_repo.schema import create_tables +from reddwarf.db.sqlalchemy.migrate_repo.schema import drop_tables +from reddwarf.db.sqlalchemy.migrate_repo.schema import Boolean +from reddwarf.db.sqlalchemy.migrate_repo.schema import Integer +from reddwarf.db.sqlalchemy.migrate_repo.schema import String +from reddwarf.db.sqlalchemy.migrate_repo.schema import DateTime +from reddwarf.db.sqlalchemy.migrate_repo.schema import Table + + +meta = MetaData() + +security_groups = Table( + 'security_groups', + meta, + Column('id', String(length=36), primary_key=True, nullable=False), + Column('name', String(length=255)), + Column('description', String(length=255)), + Column('user', String(length=255)), + Column('tenant_id', String(length=255)), + Column('created', DateTime()), + Column('updated', DateTime()), + Column('deleted', Boolean(), default=0), + Column('deleted_at', DateTime()), +) + +security_group_instance_associations = Table( + 'security_group_instance_associations', + meta, + Column('id', String(length=36), primary_key=True, nullable=False), + Column('security_group_id', String(length=36), + ForeignKey('security_groups.id', ondelete="CASCADE", + onupdate="CASCADE")), + Column('instance_id', String(length=36), + ForeignKey('instances.id', ondelete="CASCADE", + onupdate="CASCADE")), + Column('created', DateTime()), + Column('updated', DateTime()), + Column('deleted', Boolean(), default=0), + Column('deleted_at', DateTime()), +) + +security_group_rules = Table( + 'security_group_rules', + meta, + Column('id', String(length=36), primary_key=True, nullable=False), + Column('group_id', String(length=36), + ForeignKey('security_groups.id', ondelete="CASCADE", + onupdate="CASCADE")), + Column('parent_group_id', String(length=36), + ForeignKey('security_groups.id', ondelete="CASCADE", + onupdate="CASCADE")), + Column('protocol', String(length=255)), + Column('from_port', Integer()), + Column('to_port', Integer()), + Column('cidr', String(length=255)), + Column('created', DateTime()), + Column('updated', DateTime()), + Column('deleted', Boolean(), default=0), + Column('deleted_at', DateTime()), +) + + +def upgrade(migrate_engine): + meta.bind = migrate_engine + instances = Table( + 'instances', + meta, + autoload=True, + ) + create_tables([security_groups, security_group_rules, + security_group_instance_associations]) + + +def downgrade(migrate_engine): + meta.bind = migrate_engine + drop_tables([security_group_instance_associations, + security_group_rules, security_groups]) diff --git a/reddwarf/db/sqlalchemy/session.py b/reddwarf/db/sqlalchemy/session.py index 4baa443928..5e2f0aeb8b 100644 --- a/reddwarf/db/sqlalchemy/session.py +++ b/reddwarf/db/sqlalchemy/session.py @@ -47,6 +47,7 @@ def configure_db(options, models_mapper=None): from reddwarf.guestagent import models as agent_models from reddwarf.quota import models as quota_models from reddwarf.backup import models as backup_models + from reddwarf.extensions.security_group import models as secgrp_models model_modules = [ base_models, @@ -55,6 +56,7 @@ def configure_db(options, models_mapper=None): agent_models, quota_models, backup_models, + secgrp_models, ] models = {} diff --git a/reddwarf/extensions/security_group.py b/reddwarf/extensions/security_group.py new file mode 100644 index 0000000000..007cf6b8f8 --- /dev/null +++ b/reddwarf/extensions/security_group.py @@ -0,0 +1,73 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 Hewlett-Packard Development Company, L.P. +# All Rights Reserved. +# +# 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 reddwarf.openstack.common import log as logging + +from reddwarf.common import extensions +from reddwarf.common import wsgi +from reddwarf.common import cfg +from reddwarf.extensions.security_group import service + + +LOG = logging.getLogger(__name__) +CONF = cfg.CONF + + +# The Extensions module from openstack common expects the classname of the +# extension to be loaded to be the exact same as the filename, except with +# a capital first letter. That's the reason this class has such a funky name. +class Security_group(extensions.ExtensionsDescriptor): + + def get_name(self): + return "SecurityGroup" + + def get_description(self): + return "Security Group related operations such as list \ +security groups and manage security group rules." + + def get_alias(self): + return "SecurityGroup" + + def get_namespace(self): + return "http://TBD" + + def get_updated(self): + return "2012-02-26T17:25:27-08:00" + + def get_resources(self): + resources = [] + serializer = wsgi.ReddwarfResponseSerializer( + body_serializers={'application/xml': + wsgi.ReddwarfXMLDictSerializer()}) + + if CONF.reddwarf_security_groups_support: + security_groups = extensions.ResourceExtension( + '{tenant_id}/security_groups', + service.SecurityGroupController(), + deserializer=wsgi.ReddwarfRequestDeserializer(), + serializer=serializer) + resources.append(security_groups) + + security_group_rules = extensions.ResourceExtension( + '{tenant_id}/security_group_rules', + service.SecurityGroupRuleController(), + deserializer=wsgi.ReddwarfRequestDeserializer(), + serializer=serializer) + resources.append(security_group_rules) + + return resources diff --git a/reddwarf/extensions/security_group/__init__.py b/reddwarf/extensions/security_group/__init__.py new file mode 100644 index 0000000000..7cdf9465ca --- /dev/null +++ b/reddwarf/extensions/security_group/__init__.py @@ -0,0 +1,17 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 Hewlett-Packard Development Company, L.P. +# All Rights Reserved. +# +# 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. +# diff --git a/reddwarf/extensions/security_group/models.py b/reddwarf/extensions/security_group/models.py new file mode 100644 index 0000000000..bfaeed3ca0 --- /dev/null +++ b/reddwarf/extensions/security_group/models.py @@ -0,0 +1,264 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 Hewlett-Packard Development Company, L.P. +# All Rights Reserved. +# +# 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. +# + +""" +Model classes for Security Groups and Security Group Rules on instances. +""" +import reddwarf.common.remote +from reddwarf.common import cfg +from reddwarf.common import exception +from reddwarf.db.models import DatabaseModelBase +from reddwarf.common.models import NovaRemoteModelBase +from reddwarf.openstack.common import log as logging +from reddwarf.openstack.common.gettextutils import _ + +from novaclient import exceptions as nova_exceptions + +CONF = cfg.CONF +LOG = logging.getLogger(__name__) + + +def persisted_models(): + return { + 'security_group': SecurityGroup, + 'security_group_rule': SecurityGroupRule, + 'security_group_instance_association': + SecurityGroupInstanceAssociation, + } + + +class SecurityGroup(DatabaseModelBase): + _data_fields = ['id', 'name', 'description', 'user', 'tenant_id', + 'created', 'updated', 'deleted', 'deleted_at'] + + @classmethod + def create_sec_group(cls, name, description, context): + try: + remote_sec_group = RemoteSecurityGroup.create(name, + description, + context) + + if not remote_sec_group: + raise exception.SecurityGroupCreationError( + "Failed to create Security Group") + else: + return cls.create( + id=remote_sec_group.data()['id'], + name=name, + description=description, + user=context.user, + tenant_id=context.tenant) + + except exception.SecurityGroupCreationError, e: + LOG.exception("Failed to create remote security group") + raise e + + @classmethod + def create_for_instance(cls, instance_id, context): + # Create a new security group + name = _("SecGroup_%s") % instance_id + description = \ + _("Default Security Group For DBaaS Instance <%s>") % instance_id + sec_group = cls.create_sec_group(name, description, context) + + # Currently this locked down by default, since we don't create any + # default security group rules for the security group. + + # Create security group instance association + SecurityGroupInstanceAssociation.create( + security_group_id=sec_group["id"], + instance_id=instance_id) + + return sec_group + + @classmethod + def get_security_group_by_id_or_instance_id(self, id, tenant_id): + try: + return SecurityGroup.find_by(id=id, + tenant_id=tenant_id, + deleted=False) + except exception.ModelNotFoundError: + return SecurityGroupInstanceAssociation.\ + get_security_group_by_instance_id(id) + + def get_rules(self): + return SecurityGroupRule.find_all(group_id=self.id, + deleted=False) + + def delete(self, context): + try: + sec_group_rules = self.get_rules() + if sec_group_rules: + for rule in sec_group_rules: + rule.delete(context) + + RemoteSecurityGroup.delete(self.id, context) + super(SecurityGroup, self).delete() + + except exception.ReddwarfError: + LOG.exception('Failed to delete security group') + raise exception.ReddwarfError("Failed to delete Security Group") + + @classmethod + def delete_for_instance(cls, instance_id, context): + association = SecurityGroupInstanceAssociation.find_by( + instance_id=instance_id, + deleted=False) + if association: + sec_group = association.get_security_group() + sec_group.delete(context) + association.delete() + + +class SecurityGroupRule(DatabaseModelBase): + _data_fields = ['id', 'parent_group_id', 'protocol', 'from_port', + 'to_port', 'cidr', 'group_id', 'created', 'updated', + 'deleted', 'deleted_at'] + + @classmethod + def create_sec_group_rule(cls, sec_group, protocol, from_port, + to_port, cidr, context): + try: + remote_rule_id = RemoteSecurityGroup.add_rule( + sec_group_id=sec_group['id'], + protocol=protocol, + from_port=from_port, + to_port=to_port, + cidr=cidr, + context=context) + + if not remote_rule_id: + raise exception.SecurityGroupRuleCreationError( + "Failed to create Security Group Rule") + else: + # Create db record + return cls.create( + id=remote_rule_id, + protocol=protocol, + from_port=from_port, + to_port=to_port, + cidr=cidr, + group_id=sec_group['id']) + + except exception.SecurityGroupRuleCreationError, e: + LOG.exception("Failed to create remote security group") + raise e + + def get_security_group(self, tenant_id): + return SecurityGroup.find_by(id=self.group_id, + tenant_id=tenant_id, + deleted=False) + + def delete(self, context): + try: + # Delete Remote Security Group Rule + RemoteSecurityGroup.delete_rule(self.id, context) + super(SecurityGroupRule, self).delete() + except exception.ReddwarfError: + LOG.exception('Failed to delete security group') + raise exception.SecurityGroupRuleDeletionError( + "Failed to delete Security Group") + + +class SecurityGroupInstanceAssociation(DatabaseModelBase): + _data_fields = ['id', 'security_group_id', 'instance_id', + 'created', 'updated', 'deleted', 'deleted_at'] + + def get_security_group(self): + return SecurityGroup.find_by(id=self.security_group_id, + deleted=False) + + @classmethod + def get_security_group_by_instance_id(cls, id): + association = SecurityGroupInstanceAssociation.find_by( + instance_id=id, + deleted=False) + return association.get_security_group() + + +class RemoteSecurityGroup(NovaRemoteModelBase): + + _data_fields = ['id', 'name', 'description', 'rules'] + + def __init__(self, security_group=None, id=None, context=None): + if id is None and security_group is None: + msg = "Security Group does not have id defined!" + raise exception.InvalidModelError(msg) + elif security_group is None: + try: + client = reddwarf.common.remote.create_nova_client(context) + self._data_object = client.security_groups.get(id) + except nova_exceptions.NotFound, e: + raise exception.NotFound(id=id) + except nova_exceptions.ClientException, e: + raise exception.ReddwarfError(str(e)) + else: + self._data_object = security_group + + @classmethod + def create(cls, name, description, context): + """Creates a new Security Group""" + client = reddwarf.common.remote.create_nova_client(context) + try: + sec_group = client.security_groups.create(name=name, + description=description) + except nova_exceptions.ClientException, e: + LOG.exception('Failed to create remote security group') + raise exception.SecurityGroupCreationError(str(e)) + + return RemoteSecurityGroup(security_group=sec_group) + + @classmethod + def delete(cls, sec_group_id, context): + client = reddwarf.common.remote.create_nova_client(context) + + try: + client.security_groups.delete(sec_group_id) + except nova_exceptions.ClientException, e: + LOG.exception('Failed to delete remote security group') + raise exception.SecurityGroupDeletionError(str(e)) + + @classmethod + def add_rule(cls, sec_group_id, protocol, from_port, + to_port, cidr, context): + + client = reddwarf.common.remote.create_nova_client(context) + + try: + sec_group_rule = client.security_group_rules.create( + parent_group_id=sec_group_id, + ip_protocol=protocol, + from_port=from_port, + to_port=to_port, + cidr=cidr) + + return sec_group_rule.id + except nova_exceptions.ClientException, e: + LOG.exception('Failed to add rule to remote security group') + raise exception.SecurityGroupRuleCreationError(str(e)) + + @classmethod + def delete_rule(cls, sec_group_rule_id, context): + client = reddwarf.common.remote.create_nova_client(context) + + try: + client.security_group_rules.delete(sec_group_rule_id) + + except nova_exceptions.ClientException, e: + LOG.exception('Failed to delete rule to remote security group') + raise exception.SecurityGroupRuleDeletionError(str(e)) diff --git a/reddwarf/extensions/security_group/service.py b/reddwarf/extensions/security_group/service.py new file mode 100644 index 0000000000..1da3bac4cf --- /dev/null +++ b/reddwarf/extensions/security_group/service.py @@ -0,0 +1,117 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 Hewlett-Packard Development Company, L.P. +# All Rights Reserved. +# +# 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 reddwarf.common import exception +from reddwarf.common import wsgi +from reddwarf.common import cfg +from reddwarf.extensions.security_group import models +from reddwarf.extensions.security_group import views +from reddwarf.openstack.common import log as logging +from reddwarf.openstack.common.gettextutils import _ + +CONF = cfg.CONF +LOG = logging.getLogger(__name__) + + +class SecurityGroupController(wsgi.Controller): + """Controller for security groups functionality""" + + def index(self, req, tenant_id): + """Return all security groups tied to a particular tenant_id.""" + LOG.debug("Index() called with %s" % (tenant_id)) + + sec_groups = models.SecurityGroup().find_all(tenant_id=tenant_id, + deleted=False) + + # Construct the mapping from Security Groups to Security Group Rules + rules_map = dict([(g.id, g.get_rules()) for g in sec_groups]) + + return wsgi.Result( + views.SecurityGroupsView(sec_groups, + rules_map, + req, tenant_id).list(), 200) + + def show(self, req, tenant_id, id): + """Return a single security group.""" + LOG.debug("Show() called with %s, %s" % (tenant_id, id)) + + sec_group = \ + models.SecurityGroup.get_security_group_by_id_or_instance_id( + id, tenant_id) + + return wsgi.Result( + views.SecurityGroupView(sec_group, + sec_group.get_rules(), + req, tenant_id).show(), 200) + + +class SecurityGroupRuleController(wsgi.Controller): + """Controller for security group rule functionality""" + + def delete(self, req, tenant_id, id): + LOG.debug("Delete Security Group Rule called %s, %s" % (tenant_id, id)) + + context = req.environ[wsgi.CONTEXT_KEY] + sec_group_rule = models.SecurityGroupRule.find_by(id=id, deleted=False) + sec_group = sec_group_rule.get_security_group(tenant_id) + + if sec_group is None: + LOG.error("Attempting to delete Group Rule that does not exist or " + "does not belong to tenant %s" % tenant_id) + raise exception.Forbidden("Unauthorized") + + sec_group_rule.delete(context) + return wsgi.Result(None, 204) + + def create(self, req, body, tenant_id): + LOG.debug("Creating a Security Group Rule for tenant '%s'" % tenant_id) + + context = req.environ[wsgi.CONTEXT_KEY] + self._validate_create_body(body) + + sec_group_id = body['security_group_rule']['group_id'] + sec_group = models.SecurityGroup.find_by(id=sec_group_id, + tenant_id=tenant_id, + deleted=False) + + sec_group_rule = models.SecurityGroupRule.create_sec_group_rule( + sec_group, + CONF.reddwarf_security_group_rule_protocol, + CONF.reddwarf_security_group_rule_port, + CONF.reddwarf_security_group_rule_port, + body['security_group_rule']['cidr'], + context) + + resultView = views.SecurityGroupRulesView(sec_group_rule, + req, + tenant_id).create() + return wsgi.Result(resultView, 201) + + def _validate_create_body(self, body): + try: + # TODO: Add some better validation here around ports, + # protocol, and cidr values. + body['security_group_rule'] + body['security_group_rule']['group_id'] + body['security_group_rule']['cidr'] + except KeyError as e: + LOG.error(_("Create Security Group Rules Required field(s) " + "- %s") % e) + raise exception.SecurityGroupRuleCreationError( + "Required element/key - %s was not specified" % e) diff --git a/reddwarf/extensions/security_group/views.py b/reddwarf/extensions/security_group/views.py new file mode 100644 index 0000000000..08c70d6e55 --- /dev/null +++ b/reddwarf/extensions/security_group/views.py @@ -0,0 +1,124 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 Hewlett-Packard Development Company, L.P. +# All Rights Reserved. +# +# 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 reddwarf.openstack.common import log as logging +import os + +LOG = logging.getLogger(__name__) + + +def _base_url(req): + return req.application_url + + +class SecurityGroupView(object): + + def __init__(self, secgroup, rules, req, tenant_id): + self.secgroup = secgroup + self.rules = rules + self.request = req + self.tenant_id = tenant_id + + def _build_links(self): + """Build the links for the secgroup""" + base_url = _base_url(self.request) + href = os.path.join(base_url, self.tenant_id, + "security-groups", str(self.secgroup['id'])) + links = [ + { + 'rel': 'self', + 'href': href + } + ] + return links + + def _build_rules(self): + rules = [] + + if self.rules is None: + return rules + + for rule in self.rules: + rules.append({'id': str(rule['id']), + 'protocol': rule['protocol'], + 'from_port': rule['from_port'], + 'to_port': rule['to_port'], + 'cidr': rule['cidr'], + }) + return rules + + def data(self): + return {"id": self.secgroup['id'], + "name": self.secgroup['name'], + "description": self.secgroup['description'], + "rules": self._build_rules(), + "links": self._build_links(), + "created": self.secgroup['created'], + "updated": self.secgroup['updated'] + } + + def show(self): + return {"security_group": self.data()} + + def create(self): + return self.show() + + +class SecurityGroupsView(object): + + def __init__(self, secgroups, rules_dict, req, tenant_id): + self.secgroups = secgroups + self.rules = rules_dict + self.request = req + self.tenant_id = tenant_id + + def list(self): + groups_data = [] + + for secgroup in self.secgroups: + rules = \ + self.rules[secgroup['id']] if self.rules is not None else None + groups_data.append(SecurityGroupView(secgroup, + rules, + self.request, + self.tenant_id).data()) + + return {"security_groups": groups_data} + + +class SecurityGroupRulesView(object): + + def __init__(self, rule, req, tenant_id): + self.rule = rule + self.request = req + self.tenant_id = tenant_id + + def _build_create(self): + return {"security_group_rule": + {"id": str(self.rule['id']), + "security_group_id": self.rule['group_id'], + "protocol": self.rule['protocol'], + "from_port": self.rule['from_port'], + "to_port": self.rule['to_port'], + "cidr": self.rule['cidr'], + "created": self.rule['created'] + } + } + + def create(self): + return self._build_create() diff --git a/reddwarf/instance/models.py b/reddwarf/instance/models.py index 2de461a36b..16b22281ba 100644 --- a/reddwarf/instance/models.py +++ b/reddwarf/instance/models.py @@ -29,6 +29,7 @@ from reddwarf.common.remote import create_dns_client from reddwarf.common.remote import create_guest_client from reddwarf.common.remote import create_nova_client from reddwarf.common.remote import create_nova_volume_client +from reddwarf.extensions.security_group.models import SecurityGroup from reddwarf.db import models as dbmodels from reddwarf.instance.tasks import InstanceTask from reddwarf.instance.tasks import InstanceTasks @@ -372,6 +373,10 @@ class BaseInstance(SimpleInstance): time_now = datetime.now() self.update_db(deleted=True, deleted_at=time_now, task_status=InstanceTasks.NONE) + # Delete associated security group + if CONF.reddwarf_security_groups_support: + SecurityGroup.delete_for_instance(self.db_info.id, + self.context) @property def guest(self): @@ -428,6 +433,7 @@ class Instance(BuiltInstance): databases, users, service_type, volume_size): def _create_resources(): client = create_nova_client(context) + security_groups = None try: flavor = client.flavors.get(flavor_id) except nova_exceptions.NotFound: @@ -450,10 +456,17 @@ class Instance(BuiltInstance): db_info.hostname = hostname db_info.save() + if CONF.reddwarf_security_groups_support: + security_group = SecurityGroup.create_for_instance( + db_info.id, + context) + security_groups = [security_group["name"]] + task_api.API(context).create_instance(db_info.id, name, flavor_id, flavor.ram, image_id, databases, users, - service_type, volume_size) + service_type, volume_size, + security_groups) return SimpleInstance(context, db_info, service_status) diff --git a/reddwarf/taskmanager/api.py b/reddwarf/taskmanager/api.py index c093763f2f..0ff204badb 100644 --- a/reddwarf/taskmanager/api.py +++ b/reddwarf/taskmanager/api.py @@ -89,9 +89,11 @@ class API(ManagerAPI): self._cast("delete_instance", instance_id=instance_id) def create_instance(self, instance_id, name, flavor_id, flavor_ram, - image_id, databases, users, service_type, volume_size): + image_id, databases, users, service_type, volume_size, + security_groups): LOG.debug("Making async call to create instance %s " % instance_id) self._cast("create_instance", instance_id=instance_id, name=name, flavor_id=flavor_id, flavor_ram=flavor_ram, image_id=image_id, databases=databases, users=users, - service_type=service_type, volume_size=volume_size) + service_type=service_type, volume_size=volume_size, + security_groups=security_groups) diff --git a/reddwarf/taskmanager/manager.py b/reddwarf/taskmanager/manager.py index 6d2bcf78d1..b93059e3b6 100644 --- a/reddwarf/taskmanager/manager.py +++ b/reddwarf/taskmanager/manager.py @@ -70,8 +70,8 @@ class Manager(periodic_task.PeriodicTasks): def create_instance(self, context, instance_id, name, flavor_id, flavor_ram, image_id, databases, users, service_type, - volume_size): + volume_size, security_groups): instance_tasks = FreshInstanceTasks.load(context, instance_id) instance_tasks.create_instance(flavor_id, flavor_ram, image_id, databases, users, service_type, - volume_size) + volume_size, security_groups) diff --git a/reddwarf/taskmanager/models.py b/reddwarf/taskmanager/models.py index e4f9f9b244..cb830d788f 100644 --- a/reddwarf/taskmanager/models.py +++ b/reddwarf/taskmanager/models.py @@ -58,17 +58,20 @@ use_nova_server_volume = CONF.use_nova_server_volume class FreshInstanceTasks(FreshInstance): def create_instance(self, flavor_id, flavor_ram, image_id, - databases, users, service_type, volume_size): + databases, users, service_type, volume_size, + security_groups): if use_nova_server_volume: server, volume_info = self._create_server_volume( flavor_id, image_id, + security_groups, service_type, volume_size) else: server, volume_info = self._create_server_volume_individually( flavor_id, image_id, + security_groups, service_type, volume_size) try: @@ -85,8 +88,8 @@ class FreshInstanceTasks(FreshInstance): if not self.db_info.task_status.is_error: self.update_db(task_status=inst_models.InstanceTasks.NONE) - def _create_server_volume(self, flavor_id, image_id, service_type, - volume_size): + def _create_server_volume(self, flavor_id, image_id, security_groups, + service_type, volume_size): server = None try: nova_client = create_nova_client(self.context) @@ -99,8 +102,10 @@ class FreshInstanceTasks(FreshInstance): volume_ref = {'size': volume_size, 'name': volume_name, 'description': volume_desc} - server = nova_client.servers.create(name, image_id, flavor_id, - files=files, volume=volume_ref) + server = nova_client.servers.create( + name, image_id, flavor_id, + files=files, volume=volume_ref, + security_groups=security_groups) LOG.debug(_("Created new compute instance %s.") % server.id) server_dict = server._info @@ -123,7 +128,8 @@ class FreshInstanceTasks(FreshInstance): return server, volume_info def _create_server_volume_individually(self, flavor_id, image_id, - service_type, volume_size): + security_groups, service_type, + volume_size): volume_info = None block_device_mapping = None server = None @@ -136,8 +142,8 @@ class FreshInstanceTasks(FreshInstance): self._log_and_raise(e, msg, err) try: - server = self._create_server(flavor_id, image_id, service_type, - block_device_mapping) + server = self._create_server(flavor_id, image_id, security_groups, + service_type, block_device_mapping) server_id = server.id # Save server ID. self.update_db(compute_instance_id=server_id) @@ -212,7 +218,7 @@ class FreshInstanceTasks(FreshInstance): 'volumes': volumes} return volume_info - def _create_server(self, flavor_id, image_id, + def _create_server(self, flavor_id, image_id, security_groups, service_type, block_device_mapping): nova_client = create_nova_client(self.context) files = {"/etc/guest_info": ("[DEFAULT]\nguest_id=%s\n" @@ -222,6 +228,7 @@ class FreshInstanceTasks(FreshInstance): bdmap = block_device_mapping server = nova_client.servers.create(name, image_id, flavor_id, files=files, + security_groups=security_groups, block_device_mapping=bdmap) LOG.debug(_("Created new compute instance %s.") % server.id) return server diff --git a/reddwarf/tests/api/instances.py b/reddwarf/tests/api/instances.py index e9de0e3ac7..b73db761b5 100644 --- a/reddwarf/tests/api/instances.py +++ b/reddwarf/tests/api/instances.py @@ -31,6 +31,7 @@ GROUP_STOP = "dbaas.guest.shutdown" GROUP_USERS = "dbaas.api.users" GROUP_ROOT = "dbaas.api.root" GROUP_DATABASES = "dbaas.api.databases" +GROUP_SECURITY_GROUPS = "dbaas.api.security_groups" from datetime import datetime from nose.plugins.skip import SkipTest @@ -51,6 +52,7 @@ from proboscis.asserts import assert_not_equal from proboscis.asserts import assert_raises from proboscis.asserts import assert_is from proboscis.asserts import assert_is_none +from proboscis.asserts import assert_is_not_none from proboscis.asserts import assert_is_not from proboscis.asserts import assert_true from proboscis.asserts import Check @@ -67,6 +69,7 @@ from reddwarf.tests.util import skip_if_xml from reddwarf.tests.util import string_in_list from reddwarf.tests.util import poll_until from reddwarf.tests.util.check import AttrCheck +from reddwarf.tests.util.check import TypeCheck class InstanceTestInfo(object): @@ -451,6 +454,106 @@ def assert_unprocessable(func, *args): pass # Good +@test(depends_on_classes=[CreateInstance], + groups=[GROUP, GROUP_SECURITY_GROUPS], + runs_after_groups=[tests.PRE_INSTANCES]) +class SecurityGroupsTest(object): + + @before_class + def setUp(self): + self.testSecurityGroup = dbaas.security_groups.get( + instance_info.id) + self.secGroupName = "SecGroup_%s" % instance_info.id + self.secGroupDescription = \ + "Default Security Group For DBaaS Instance <%s>" % instance_info.id + + @test + def test_created_security_group(self): + assert_is_not_none(self.testSecurityGroup) + with TypeCheck('SecurityGroup', self.testSecurityGroup) as secGrp: + secGrp.has_field('id', basestring) + secGrp.has_field('name', basestring) + secGrp.has_field('description', basestring) + secGrp.has_field('created', basestring) + secGrp.has_field('updated', basestring) + assert_equal(self.testSecurityGroup.name, self.secGroupName) + assert_equal(self.testSecurityGroup.description, + self.secGroupDescription) + + @test + def test_list_security_group(self): + securityGroupList = dbaas.security_groups.list() + assert_is_not_none(securityGroupList) + securityGroup = [x for x in securityGroupList + if x.name in self.secGroupName] + assert_is_not_none(securityGroup) + + @test + def test_get_security_group(self): + securityGroup = dbaas.security_groups.get(self.testSecurityGroup.id) + assert_is_not_none(securityGroup) + assert_equal(securityGroup.name, self.secGroupName) + assert_equal(securityGroup.description, self.secGroupDescription) + + +@test(depends_on_classes=[SecurityGroupsTest], + groups=[GROUP, GROUP_SECURITY_GROUPS], + runs_after_groups=[tests.PRE_INSTANCES]) +class SecurityGroupsRulesTest(object): + + @before_class + def setUp(self): + self.testSecurityGroup = dbaas.security_groups.get( + instance_info.id) + self.secGroupName = "SecGroup_%s" % instance_info.id + self.secGroupDescription = \ + "Default Security Group For DBaaS Instance <%s>" % instance_info.id + + @test + def test_create_security_group_rule(self): + self.testSecurityGroupRule = dbaas.security_group_rules.create( + group_id=self.testSecurityGroup.id, + protocol="tcp", + from_port=3306, + to_port=3306, + cidr="0.0.0.0/0") + assert_is_not_none(self.testSecurityGroupRule) + with TypeCheck('SecurityGroupRule', + self.testSecurityGroupRule) as secGrpRule: + secGrpRule.has_field('id', basestring) + secGrpRule.has_field('security_group_id', basestring) + secGrpRule.has_field('protocol', basestring) + secGrpRule.has_field('cidr', basestring) + secGrpRule.has_field('from_port', int) + secGrpRule.has_field('to_port', int) + secGrpRule.has_field('created', basestring) + assert_equal(self.testSecurityGroupRule.security_group_id, + self.testSecurityGroup.id) + assert_equal(self.testSecurityGroupRule.protocol, "tcp") + assert_equal(int(self.testSecurityGroupRule.from_port), 3306) + assert_equal(int(self.testSecurityGroupRule.to_port), 3306) + assert_equal(self.testSecurityGroupRule.cidr, "0.0.0.0/0") + + @test + def test_deep_list_security_group_with_rules(self): + securityGroupList = dbaas.security_groups.list() + assert_is_not_none(securityGroupList) + securityGroup = [x for x in securityGroupList + if x.name in self.secGroupName] + assert_is_not_none(securityGroup[0]) + assert_equal(len(securityGroup[0].rules), 1) + + @test + def test_delete_security_group_rule(self): + dbaas.security_group_rules.delete(self.testSecurityGroupRule.id) + securityGroupList = dbaas.security_groups.list() + assert_is_not_none(securityGroupList) + securityGroup = [x for x in securityGroupList + if x.name in self.secGroupName] + assert_is_not_none(securityGroup[0]) + assert_equal(len(securityGroup[0].rules), 0) + + @test(depends_on_classes=[CreateInstance], groups=[GROUP, GROUP_START, diff --git a/reddwarf/tests/fakes/nova.py b/reddwarf/tests/fakes/nova.py index 994cbfbfd5..787f5816bb 100644 --- a/reddwarf/tests/fakes/nova.py +++ b/reddwarf/tests/fakes/nova.py @@ -256,7 +256,7 @@ class FakeServers(object): server.owner.tenant == self.context.tenant) def create(self, name, image_id, flavor_ref, files=None, - block_device_mapping=None, volume=None): + block_device_mapping=None, volume=None, security_groups=None): id = "FAKE_%s" % uuid.uuid4() if volume: volume = self.volumes.create(volume['size'], volume['name'], @@ -644,6 +644,93 @@ class FakeRdStorages(object): return [self.storages[name] for name in self.storages] +class FakeSecurityGroup(object): + + def __init__(self, name=None, description=None, context=None): + self.name = name + self.description = description + self.id = "FAKE_SECGRP_%s" % uuid.uuid4() + self.rules = {} + + def get_id(self): + return self.id + + def add_rule(self, fakeSecGroupRule): + self.rules.append(fakeSecGroupRule) + return self.rules + + def get_rules(self): + result = "" + for rule in self.rules: + result = result + rule.data() + return result + + def data(self): + return { + 'id': self.id, + 'name': self.name, + 'description': self.description + } + + +class FakeSecurityGroups(object): + + def __init__(self, context=None): + self.context = context + self.securityGroups = {} + + def create(self, name=None, description=None): + secGrp = FakeSecurityGroup(name, description) + self.securityGroups[secGrp.get_id()] = secGrp + return secGrp + + def list(self): + pass + + +class FakeSecurityGroupRule(object): + + def __init__(self, ip_protocol=None, from_port=None, to_port=None, + cidr=None, parent_group_id=None, context=None): + self.group_id = parent_group_id + self.protocol = ip_protocol + self.from_port = from_port + self.to_port = to_port + self.cidr = cidr + self.context = context + self.id = "FAKE_SECGRP_RULE_%s" % uuid.uuid4() + + def get_id(self): + return self.id + + def data(self): + return { + 'id': self.id, + 'group_id': self.group_id, + 'protocol': self.protocol, + 'from_port': self.from_port, + 'to_port': self.to_port, + 'cidr': self.cidr + } + + +class FakeSecurityGroupRules(object): + + def __init__(self, context=None): + self.context = context + self.securityGroupRules = {} + + def create(self, parent_group_id, ip_protocol, from_port, to_port, cidr): + secGrpRule = FakeSecurityGroupRule(ip_protocol, from_port, to_port, + cidr, parent_group_id) + self.securityGroupRules[secGrpRule.get_id()] = secGrpRule + return secGrpRule + + def delete(self, id): + if id in self.securityGroupRules: + del self.securityGroupRules[id] + + class FakeClient(object): def __init__(self, context): @@ -656,6 +743,8 @@ class FakeClient(object): self.rdhosts = FakeHosts(self.servers) self.rdstorage = FakeRdStorages() self.rdservers = FakeRdServers(self.servers) + self.security_groups = FakeSecurityGroups(context) + self.security_group_rules = FakeSecurityGroupRules(context) def get_server_volumes(self, server_id): return self.servers.get_server_volumes(server_id) diff --git a/reddwarf/tests/unittests/secgroups/__init__.py b/reddwarf/tests/unittests/secgroups/__init__.py new file mode 100644 index 0000000000..d65c689a83 --- /dev/null +++ b/reddwarf/tests/unittests/secgroups/__init__.py @@ -0,0 +1,16 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 OpenStack LLC. +# All Rights Reserved. +# +# 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. diff --git a/reddwarf/tests/unittests/secgroups/test_security_group.py b/reddwarf/tests/unittests/secgroups/test_security_group.py new file mode 100644 index 0000000000..c1b4600455 --- /dev/null +++ b/reddwarf/tests/unittests/secgroups/test_security_group.py @@ -0,0 +1,80 @@ +# Copyright 2012 OpenStack LLC +# +# 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 testtools +import reddwarf.common.remote +from mock import Mock +from reddwarf.extensions.security_group import models +from reddwarf.common import exception +from reddwarf.tests.fakes import nova + +from novaclient import exceptions as nova_exceptions + + +""" +Unit tests for testing the exceptions raised by Security Groups +""" + + +class Security_Group_Exceptions_Test(testtools.TestCase): + + def setUp(self): + super(Security_Group_Exceptions_Test, self).setUp() + self.createNovaClient = reddwarf.common.remote.create_nova_client + self.context = Mock() + self.FakeClient = nova.fake_create_nova_client(self.context) + + fException = Mock(side_effect= + lambda *args, **kwargs: + self._raise(nova_exceptions.ClientException("Test"))) + + self.FakeClient.security_groups.create = fException + self.FakeClient.security_groups.delete = fException + self.FakeClient.security_group_rules.create = fException + self.FakeClient.security_group_rules.delete = fException + + reddwarf.common.remote.create_nova_client = \ + lambda c: self._return_mocked_nova_client(c) + + def tearDown(self): + super(Security_Group_Exceptions_Test, self).tearDown() + reddwarf.common.remote.create_nova_client = self.createNovaClient + + def _return_mocked_nova_client(self, context): + return self.FakeClient + + def _raise(self, ex): + raise ex + + def test_failed_to_create_security_group(self): + self.assertRaises(exception.SecurityGroupCreationError, + models.RemoteSecurityGroup.create, + "TestName", + "TestDescription", + self.context) + + def test_failed_to_delete_security_group(self): + self.assertRaises(exception.SecurityGroupDeletionError, + models.RemoteSecurityGroup.delete, + 1, self.context) + + def test_failed_to_create_security_group_rule(self): + self.assertRaises(exception.SecurityGroupRuleCreationError, + models.RemoteSecurityGroup.add_rule, + 1, "tcp", 3306, 3306, "0.0.0.0/0", self.context) + + def test_failed_to_delete_security_group_rule(self): + self.assertRaises(exception.SecurityGroupRuleDeletionError, + models.RemoteSecurityGroup.delete_rule, + 1, self.context)