diff --git a/etc/trove/trove-taskmanager.conf.sample b/etc/trove/trove-taskmanager.conf.sample index dece4c319f..72f390e161 100644 --- a/etc/trove/trove-taskmanager.conf.sample +++ b/etc/trove/trove-taskmanager.conf.sample @@ -60,6 +60,7 @@ trove_auth_url = http://0.0.0.0:5000/v2.0 #nova_compute_url = http://localhost:8774/v2 #cinder_url = http://localhost:8776/v1 #swift_url = http://localhost:8080/v1/AUTH_ +#neutron_url = http://localhost:9696/ # nova_compute_url, cinder_url, swift_url, and heat_url can all be fetched # from Keystone. To fetch from Keystone, comment out nova_compute_url, @@ -75,6 +76,8 @@ trove_auth_url = http://0.0.0.0:5000/v2.0 #swift_service_type = object-store # Service type to use when searching catalog. #heat_service_type = orchestration +# Service type to use when searching catalog. +#neutron_service_type = network # Config options for enabling volume service trove_volume_support = True @@ -119,6 +122,10 @@ dns_instance_entry_factory = trove.dns.designate.driver.DesignateInstanceEntryFa dns_endpoint_url = http://127.0.0.1/v1/ dns_service_type = dns +# Neutron +network_driver = trove.network.nova.NovaNetwork +default_neutron_networks = + # Trove Security Groups for Instances trove_security_groups_support = True trove_security_group_rule_cidr = 0.0.0.0/0 @@ -132,8 +139,13 @@ agent_call_high_timeout = 150 use_nova_server_volume = False # Config option for filtering the IP address that DNS uses +# For nova-network, set this to the appropriate network label defined in nova +# For neutron, set this to .* since users can specify custom network labels +# You can also optionally specify regex'es to match the actual IP addresses +# ip_regex (white-list) is applied before black_list_regex in the filter chain network_label_regex = ^private$ #ip_regex = ^(15.|123.) +#black_list_regex = ^(10.0.0.) # Datastore templates template_path = /etc/trove/templates/ diff --git a/etc/trove/trove.conf.sample b/etc/trove/trove.conf.sample index 50348a195e..0a7600aad4 100644 --- a/etc/trove/trove.conf.sample +++ b/etc/trove/trove.conf.sample @@ -76,6 +76,7 @@ trove_auth_url = http://0.0.0.0:5000/v2.0 #nova_compute_url = http://localhost:8774/v2 #cinder_url = http://localhost:8776/v1 #swift_url = http://localhost:8080/v1/AUTH_ +#neutron_url = http://localhost:9696/ # nova_compute_url, cinder_url, swift_url, and heat_url can all be fetched # from Keystone. To fetch from Keystone, comment out nova_compute_url, @@ -91,10 +92,18 @@ trove_auth_url = http://0.0.0.0:5000/v2.0 #swift_service_type = object-store # Service type to use when searching catalog. #heat_service_type = orchestration +# Service type to use when searching catalog. +#neutron_service_type = network # Config option for showing the IP address that nova doles out +# For nova-network, set this to the appropriate network label defined in nova +# For neutron, set this to .* since users can specify custom network labels +# You can also optionally specify regex'es to match the actual IP addresses +# ip_regex (white-list) is applied before black_list_regex in the filter chain network_label_regex = ^private$ +#network_label_regex = .* //with neutron enabled #ip_regex = ^(15.|123.) +#black_list_regex = ^10.0.0. # Config options for enabling volume service trove_volume_support = True @@ -129,6 +138,11 @@ dns_instance_entry_factory = trove.dns.designate.driver.DesignateInstanceEntryFa dns_endpoint_url = http://127.0.0.1/v1/ dns_service_type = dns +# Neutron +network_driver = trove.network.nova.NovaNetwork +default_neutron_networks = + + # Taskmanager queue name taskmanager_queue = taskmanager diff --git a/etc/trove/trove.conf.test b/etc/trove/trove.conf.test index 3ef007c52e..6deb23e8a7 100644 --- a/etc/trove/trove.conf.test +++ b/etc/trove/trove.conf.test @@ -79,6 +79,7 @@ nova_service_name = Compute Service # Config option for showing the IP address that nova doles out network_label_regex = ^private$ ip_regex = ^(15.|123.) +black_list_regex = ^(10.0.0.) # Config options for enabling volume service trove_volume_support = True diff --git a/requirements.txt b/requirements.txt index 670567bc1f..e88b3ec928 100644 --- a/requirements.txt +++ b/requirements.txt @@ -18,6 +18,7 @@ python-cinderclient>=1.0.7 python-keystoneclient>=0.9.0 python-swiftclient>=2.0.2 python-designateclient>=1.0.0 +python-neutronclient>=2.3.5,<3 iso8601>=0.1.9 jsonschema>=2.0.0,<3.0.0 Jinja2 diff --git a/trove/common/cfg.py b/trove/common/cfg.py index d7ac44ca1b..0c19815320 100644 --- a/trove/common/cfg.py +++ b/trove/common/cfg.py @@ -55,6 +55,9 @@ common_opts = [ cfg.StrOpt('nova_compute_url', help='URL without the tenant segment.'), cfg.StrOpt('nova_compute_service_type', default='compute', help='Service type to use when searching catalog.'), + cfg.StrOpt('neutron_url', help='URL without the tenant segment.'), + cfg.StrOpt('neutron_service_type', default='network', + help='Service type to use when searching catalog.'), cfg.StrOpt('cinder_url', help='URL without the tenant segment.'), cfg.StrOpt('cinder_service_type', default='volumev2', help='Service type to use when searching catalog.'), @@ -214,6 +217,8 @@ common_opts = [ default='trove.common.remote.guest_client'), cfg.StrOpt('remote_nova_client', default='trove.common.remote.nova_client'), + cfg.StrOpt('remote_neutron_client', + default='trove.common.remote.neutron_client'), cfg.StrOpt('remote_cinder_client', default='trove.common.remote.cinder_client'), cfg.StrOpt('remote_heat_client', @@ -240,6 +245,7 @@ common_opts = [ help="Admin tenant used to connect to nova.", secret=True), cfg.StrOpt('network_label_regex', default='^private$'), cfg.StrOpt('ip_regex', default=None), + cfg.StrOpt('black_list_regex', default=None), cfg.StrOpt('cloudinit_location', default='/etc/trove/cloudinit', help="Path to folder with cloudinit scripts."), cfg.StrOpt('guest_config', @@ -259,18 +265,21 @@ common_opts = [ default=['json'], help='Filetype endings not to be reattached to an ID ' 'by the utils method correct_id_with_req.'), - cfg.ListOpt('default_neutron_networks', - default=[], - help='List of network IDs which should be attached' - ' to instance when networks are not specified' - ' in API call.'), + cfg.ListOpt('default_neutron_networks', default=[], + help='List of IDs for management networks which should be ' + ' attached to the instance regardless of what NICs' + ' are specified in the create API call.'), cfg.IntOpt('max_header_line', default=16384, help='Maximum line size of message headers to be accepted. ' 'max_header_line may need to be increased when using ' 'large tokens (typically those generated by the ' 'Keystone v3 API with big service catalogs).'), cfg.StrOpt('conductor_manager', default='trove.conductor.manager.Manager', - help='Qualified class name to use for conductor manager.') + help='Qualified class name to use for conductor manager.'), + cfg.StrOpt('network_driver', default='trove.network.nova.NovaNetwork', + help="Describes the actual network manager used for " + "the management of network attributes " + "(security groups, floating IPs, etc.)") ] # Datastore specific option groups diff --git a/trove/common/models.py b/trove/common/models.py index 8657702225..d8fd384612 100644 --- a/trove/common/models.py +++ b/trove/common/models.py @@ -15,7 +15,11 @@ """Model classes that form the core of instances functionality.""" +from trove.openstack.common.importutils import import_class from trove.common import remote +from trove.common import cfg + +CONF = cfg.CONF class ModelBase(object): @@ -94,6 +98,17 @@ class RemoteModelBase(ModelBase): return self._data_item(self._data_object) +class NetworkRemoteModelBase(RemoteModelBase): + + network_driver = None + + @classmethod + def get_driver(cls, context): + if not cls.network_driver: + cls.network_driver = import_class(CONF.network_driver) + return cls.network_driver(context) + + class NovaRemoteModelBase(RemoteModelBase): @classmethod diff --git a/trove/common/remote.py b/trove/common/remote.py index c73c76f8b1..82d98707fc 100644 --- a/trove/common/remote.py +++ b/trove/common/remote.py @@ -162,9 +162,25 @@ def swift_client(context): return client +def neutron_client(context): + from neutronclient.v2_0 import client as NeutronClient + if CONF.neutron_url: + # neutron endpoint url / publicURL does not include tenant segment + url = CONF.neutron_url + else: + url = get_endpoint(context.service_catalog, + service_type=CONF.neutron_service_type, + endpoint_region=CONF.os_region_name) + + client = NeutronClient.Client(token=context.auth_token, + endpoint_url=url) + return client + + create_dns_client = import_class(CONF.remote_dns_client) create_guest_client = import_class(CONF.remote_guest_client) create_nova_client = import_class(CONF.remote_nova_client) create_swift_client = import_class(CONF.remote_swift_client) create_cinder_client = import_class(CONF.remote_cinder_client) create_heat_client = import_class(CONF.remote_heat_client) +create_neutron_client = import_class(CONF.remote_neutron_client) diff --git a/trove/extensions/security_group/models.py b/trove/extensions/security_group/models.py index 6117e4b2db..ce1bc0f3ec 100644 --- a/trove/extensions/security_group/models.py +++ b/trove/extensions/security_group/models.py @@ -17,15 +17,13 @@ """ Model classes for Security Groups and Security Group Rules on instances. """ -import trove.common.remote from trove.common import cfg from trove.common import exception from trove.db.models import DatabaseModelBase -from trove.common.models import NovaRemoteModelBase +from trove.common.models import NetworkRemoteModelBase from trove.openstack.common import log as logging from trove.openstack.common.gettextutils import _ -from novaclient import exceptions as nova_exceptions CONF = cfg.CONF LOG = logging.getLogger(__name__) @@ -207,7 +205,7 @@ class SecurityGroupInstanceAssociation(DatabaseModelBase): return association.instance_id -class RemoteSecurityGroup(NovaRemoteModelBase): +class RemoteSecurityGroup(NetworkRemoteModelBase): _data_fields = ['id', 'name', 'description', 'rules'] @@ -216,65 +214,37 @@ class RemoteSecurityGroup(NovaRemoteModelBase): msg = "Security Group does not have id defined!" raise exception.InvalidModelError(msg) elif security_group is None: - try: - client = trove.common.remote.create_nova_client(context) - self._data_object = client.security_groups.get(id) - except nova_exceptions.NotFound as e: - raise exception.NotFound(id=id) - except nova_exceptions.ClientException as e: - raise exception.TroveError(str(e)) + driver = self.get_driver(context) + self._data_object = driver.get_sec_group_by_id(group_id=id) else: self._data_object = security_group @classmethod def create(cls, name, description, context): """Creates a new Security Group.""" - client = trove.common.remote.create_nova_client(context) - try: - sec_group = client.security_groups.create(name=name, - description=description) - except nova_exceptions.ClientException as e: - LOG.exception('Failed to create remote security group') - raise exception.SecurityGroupCreationError(str(e)) - + driver = cls.get_driver(context) + sec_group = driver.create_security_group( + name=name, description=description) return RemoteSecurityGroup(security_group=sec_group) @classmethod def delete(cls, sec_group_id, context): - client = trove.common.remote.create_nova_client(context) - - try: - client.security_groups.delete(sec_group_id) - except nova_exceptions.ClientException as e: - LOG.exception('Failed to delete remote security group') - raise exception.SecurityGroupDeletionError(str(e)) + """Deletes a Security Group.""" + driver = cls.get_driver(context) + driver.delete_security_group(sec_group_id) @classmethod def add_rule(cls, sec_group_id, protocol, from_port, to_port, cidr, context): + """Adds a new rule to an existing security group.""" + driver = cls.get_driver(context) + sec_group_rule = driver.add_security_group_rule( + sec_group_id, protocol, from_port, to_port, cidr) - client = trove.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 as e: - LOG.exception('Failed to add rule to remote security group') - raise exception.SecurityGroupRuleCreationError(str(e)) + return sec_group_rule.id @classmethod def delete_rule(cls, sec_group_rule_id, context): - client = trove.common.remote.create_nova_client(context) - - try: - client.security_group_rules.delete(sec_group_rule_id) - - except nova_exceptions.ClientException as e: - LOG.exception('Failed to delete rule to remote security group') - raise exception.SecurityGroupRuleDeletionError(str(e)) + """Deletes a rule from an existing security group.""" + driver = cls.get_driver(context) + driver.delete_security_group_rule(sec_group_rule_id) diff --git a/trove/instance/models.py b/trove/instance/models.py index e392bb7470..4da31e8ecc 100644 --- a/trove/instance/models.py +++ b/trove/instance/models.py @@ -48,9 +48,12 @@ CONF = cfg.CONF LOG = logging.getLogger(__name__) -def filter_ips(ips, regex): - """Filter out IPs not matching regex.""" - return [ip for ip in ips if re.search(regex, ip)] +def filter_ips(ips, white_list_regex, black_list_regex): + """Return IPs matching white_list_regex and + Filter out IPs matching black_list_regex. + """ + return [ip for ip in ips if re.search(white_list_regex, ip) + and not re.search(black_list_regex, ip)] def load_server(context, instance_id, server_id): @@ -204,8 +207,8 @@ class SimpleInstance(object): IPs.extend([addr.get('addr') for addr in self.addresses[label]]) # Includes ip addresses that match the regexp pattern - if CONF.ip_regex: - IPs = filter_ips(IPs, CONF.ip_regex) + if CONF.ip_regex and CONF.black_list_regex: + IPs = filter_ips(IPs, CONF.ip_regex, CONF.black_list_regex) return IPs @property @@ -640,10 +643,11 @@ class Instance(BuiltInstance): datastore1=backup_info.datastore.name, datastore2=datastore.name) - if not nics and CONF.default_neutron_networks: + if not nics: nics = [] - for net_id in CONF.default_neutron_networks: - nics.append({"net-id": net_id}) + if CONF.default_neutron_networks: + nics = [{"net-id": net_id} + for net_id in CONF.default_neutron_networks] + nics def _create_resources(): diff --git a/trove/network/__init__.py b/trove/network/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/trove/network/base.py b/trove/network/base.py new file mode 100644 index 0000000000..572b9f3b29 --- /dev/null +++ b/trove/network/base.py @@ -0,0 +1,51 @@ +# Copyright 2014 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. +# +import abc + +import six + + +@six.add_metaclass(abc.ABCMeta) +class NetworkDriver(object): + """Base Network Driver class to abstract the network driver used.""" + + @abc.abstractmethod + def get_sec_group_by_id(self, group_id): + """ + Returns security group with given group_id + """ + + @abc.abstractmethod + def create_security_group(self, name, description): + """ + Creates the security group with given name and description + """ + + @abc.abstractmethod + def delete_security_group(self, sec_group_id): + """Deletes the security group by given ID.""" + + @abc.abstractmethod + def add_security_group_rule(self, sec_group_id, protocol, + from_port, to_port, cidr): + """ + Adds the rule identified by the security group ID, + transport protocol, port range: from -> to, CIDR. + """ + + @abc.abstractmethod + def delete_security_group_rule(self, sec_group_rule_id): + """Deletes the rule by given ID.""" diff --git a/trove/network/neutron.py b/trove/network/neutron.py new file mode 100644 index 0000000000..b14e94fcda --- /dev/null +++ b/trove/network/neutron.py @@ -0,0 +1,143 @@ +# Copyright 2014 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 trove.common import exception +from trove.common import remote +from trove.network import base +from trove.openstack.common import log as logging +from neutronclient.common import exceptions as neutron_exceptions + + +LOG = logging.getLogger(__name__) + +CONST = {'IPv4': "IPv4", + 'IPv6': "IPv6", + 'INGRESS': "ingress", + 'EGRESS': "egress", + 'PROTO_NAME_TCP': 'tcp', + 'PROTO_NAME_ICMP': 'icmp', + 'PROTO_NAME_ICMP_V6': 'icmpv6', + 'PROTO_NAME_UDP': 'udp'} + + +class NovaNetworkStruct(object): + def __init__(self, **properties): + self.__dict__.update(properties) + + +class NeutronDriver(base.NetworkDriver): + + def __init__(self, context): + try: + self.client = remote.create_neutron_client(context) + except neutron_exceptions.NeutronClientException as e: + raise exception.TroveError(str(e)) + + def get_sec_group_by_id(self, group_id): + try: + return self.client.show_security_group(security_group=group_id) + except neutron_exceptions.NeutronClientException as e: + LOG.exception('Failed to get remote security group') + raise exception.TroveError(str(e)) + + def create_security_group(self, name, description): + try: + sec_group_body = {"security_group": {"name": name, + "description": description}} + sec_group = self.client.create_security_group(body=sec_group_body) + return self._convert_to_nova_security_group_format( + sec_group.get('security_group', sec_group)) + + except neutron_exceptions.NeutronClientException as e: + LOG.exception('Failed to create remote security group') + raise exception.SecurityGroupCreationError(str(e)) + + def delete_security_group(self, sec_group_id): + try: + self.client.delete_security_group(security_group=sec_group_id) + except neutron_exceptions.NeutronClientException as e: + LOG.exception('Failed to delete remote security group') + raise exception.SecurityGroupDeletionError(str(e)) + + def add_security_group_rule(self, sec_group_id, protocol, + from_port, to_port, cidr, + direction=CONST['INGRESS'], + ethertype=CONST['IPv4']): + try: + secgroup_rule_body = {"security_group_rule": + {"security_group_id": sec_group_id, + "protocol": protocol, + "port_range_min": from_port, + "port_range_max": to_port, + "remote_ip_prefix": cidr, + "direction": direction, # ingress | egress + "ethertype": ethertype, # IPv4 | IPv6 + }} + + secgroup_rule = self.client.create_security_group_rule( + secgroup_rule_body) + return self._convert_to_nova_security_group_rule_format( + secgroup_rule.get('security_group_rule', secgroup_rule)) + except neutron_exceptions.NeutronClientException as e: + # ignore error if rule already exists + if e.status_code == 409: + LOG.exception("secgroup rule already exists") + else: + LOG.exception('Failed to add rule to remote security group') + raise exception.SecurityGroupRuleCreationError(str(e)) + + def delete_security_group_rule(self, sec_group_rule_id): + try: + self.client.delete_security_group_rule( + security_group_rule=sec_group_rule_id) + + except neutron_exceptions.NeutronClientException as e: + LOG.exception('Failed to delete rule to remote security group') + raise exception.SecurityGroupRuleDeletionError(str(e)) + + def _convert_to_nova_security_group_format(self, security_group): + nova_group = {} + nova_group['id'] = security_group['id'] + nova_group['description'] = security_group['description'] + nova_group['name'] = security_group['name'] + nova_group['project_id'] = security_group['tenant_id'] + nova_group['rules'] = [] + for rule in security_group.get('security_group_rules', []): + if rule['direction'] == 'ingress': + nova_group['rules'].append( + self._convert_to_nova_security_group_rule_format(rule)) + + return NovaNetworkStruct(**nova_group) + + def _convert_to_nova_security_group_rule_format(self, rule): + nova_rule = {} + nova_rule['id'] = rule['id'] + nova_rule['parent_group_id'] = rule['security_group_id'] + nova_rule['protocol'] = rule['protocol'] + if (nova_rule['protocol'] and rule.get('port_range_min') is None and + rule.get('port_range_max') is None): + if rule['protocol'].upper() in ['TCP', 'UDP']: + nova_rule['from_port'] = 1 + nova_rule['to_port'] = 65535 + else: + nova_rule['from_port'] = -1 + nova_rule['to_port'] = -1 + else: + nova_rule['from_port'] = rule.get('port_range_min') + nova_rule['to_port'] = rule.get('port_range_max') + nova_rule['group_id'] = rule['remote_group_id'] + nova_rule['cidr'] = rule.get('remote_ip_prefix') + return NovaNetworkStruct(**nova_rule) diff --git a/trove/network/nova.py b/trove/network/nova.py new file mode 100644 index 0000000000..b6f27653b4 --- /dev/null +++ b/trove/network/nova.py @@ -0,0 +1,80 @@ +# Copyright 2014 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 trove.common import exception +from trove.common import remote +from trove.network import base +from trove.openstack.common import log as logging +from novaclient import exceptions as nova_exceptions + + +LOG = logging.getLogger(__name__) + + +class NovaNetwork(base.NetworkDriver): + + def __init__(self, context): + try: + self.client = remote.create_nova_client( + context) + except nova_exceptions.ClientException as e: + raise exception.TroveError(str(e)) + + def get_sec_group_by_id(self, group_id): + try: + return self.client.security_groups.get(group_id) + except nova_exceptions.ClientException as e: + LOG.exception('Failed to get remote security group') + raise exception.TroveError(str(e)) + + def create_security_group(self, name, description): + try: + sec_group = self.client.security_groups.create( + name=name, description=description) + return sec_group + except nova_exceptions.ClientException as e: + LOG.exception('Failed to create remote security group') + raise exception.SecurityGroupCreationError(str(e)) + + def delete_security_group(self, sec_group_id): + try: + self.client.security_groups.delete(sec_group_id) + except nova_exceptions.ClientException as e: + LOG.exception('Failed to delete remote security group') + raise exception.SecurityGroupDeletionError(str(e)) + + def add_security_group_rule(self, sec_group_id, protocol, + from_port, to_port, cidr): + try: + sec_group_rule = self.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 + except nova_exceptions.ClientException as e: + LOG.exception('Failed to add rule to remote security group') + raise exception.SecurityGroupRuleCreationError(str(e)) + + def delete_security_group_rule(self, sec_group_rule_id): + try: + self.client.security_group_rules.delete(sec_group_rule_id) + + except nova_exceptions.ClientException as e: + LOG.exception('Failed to delete rule to remote security group') + raise exception.SecurityGroupRuleDeletionError(str(e)) diff --git a/trove/tests/api/instances.py b/trove/tests/api/instances.py index ca6af65d12..cf72a2376a 100644 --- a/trove/tests/api/instances.py +++ b/trove/tests/api/instances.py @@ -20,6 +20,7 @@ import unittest GROUP = "dbaas.guest" +GROUP_NEUTRON = "dbaas.neutron" GROUP_START = "dbaas.guest.initialize" GROUP_START_SIMPLE = "dbaas.guest.initialize.simple" GROUP_TEST = "dbaas.guest.test" @@ -46,6 +47,7 @@ from proboscis import after_class from proboscis import test from proboscis import SkipTest from proboscis.asserts import assert_equal +from proboscis.asserts import assert_false from proboscis.asserts import assert_not_equal from proboscis.asserts import assert_raises from proboscis.asserts import assert_is_not_none @@ -55,6 +57,7 @@ from proboscis.asserts import fail from trove import tests from trove.tests.config import CONFIG from trove.tests.util import create_dbaas_client +from trove.tests.util import create_nova_client from trove.tests.util.usage import create_usage_verifier from trove.tests.util import dns_checker from trove.tests.util import iso_time @@ -677,6 +680,89 @@ class CreateInstance(object): check.volume() +@test(depends_on_classes=[InstanceSetup], groups=[GROUP_NEUTRON]) +class CreateInstanceWithNeutron(unittest.TestCase): + @time_out(TIMEOUT_INSTANCE_CREATE) + def setUp(self): + if not CONFIG.values.get('neutron_enabled'): + raise SkipTest("neutron is not enabled, skipping") + + user = test_config.users.find_user( + Requirements(is_admin=False, services=["nova", "trove"])) + self.nova_client = create_nova_client(user) + self.dbaas_client = create_dbaas_client(user) + + self.result = None + self.instance_name = ("TEST_INSTANCE_CREATION_WITH_NICS" + + str(datetime.now())) + databases = [] + self.default_cidr = CONFIG.values.get('shared_network_subnet', None) + if VOLUME_SUPPORT: + volume = {'size': 1} + else: + volume = None + + self.result = self.dbaas_client.instances.create( + self.instance_name, + instance_info.dbaas_flavor_href, + volume, databases) + self.instance_id = self.result.id + + def verify_instance_is_active(): + result = self.dbaas_client.instances.get(self.instance_id) + if result.status == "ACTIVE": + return True + else: + assert_equal("BUILD", result.status) + return False + + poll_until(verify_instance_is_active) + + def tearDown(self): + if self.result.id is not None: + self.dbaas_client.instances.delete(self.result.id) + while True: + try: + self.dbaas_client.instances.get(self.result.id) + except exceptions.NotFound: + return True + time.sleep(1) + + def check_ip_within_network(self, ip, network): + octet_list = str(ip).split(".") + + octets, mask = str(network).split("/") + octet_list_ = octets.split(".") + + for i in range(int(mask) / 8): + if octet_list[i] != octet_list_[i]: + return False + + return True + + def test_ip_within_cidr(self): + nova_instance = None + for server in self.nova_client.servers.list(): + if str(server.name) == self.instance_name: + nova_instance = server + break + + if nova_instance is None: + fail("instance created with neutron enabled is not found in nova") + + for address in nova_instance.addresses['private']: + ip = address['addr'] + assert_true(self.check_ip_within_network(ip, self.default_cidr)) + + # black list filtered ip not visible via troveclient + trove_instance = self.dbaas_client.instances.get(self.result.id) + + for ip in trove_instance.ip: + if str(ip).startswith('10.'): + assert_true(self.check_ip_within_network(ip, "10.0.0.0/24")) + assert_false(self.check_ip_within_network(ip, "10.0.1.0/24")) + + @test(depends_on_classes=[CreateInstance], groups=[GROUP, GROUP_START, diff --git a/trove/tests/fakes/nova.py b/trove/tests/fakes/nova.py index bf39a7066d..d32de768c0 100644 --- a/trove/tests/fakes/nova.py +++ b/trove/tests/fakes/nova.py @@ -289,9 +289,11 @@ class FakeServers(object): raise nova_exceptions.ClientException("The requested availability " "zone is not available.") - if nics is not None and nics.port_id == 'UNKNOWN': - raise nova_exceptions.ClientException("The requested availability " - "zone is not available.") + if nics: + if 'port-id' in nics[0] and nics[0]['port-id'] == "UNKNOWN": + raise nova_exceptions.ClientException("The requested " + "port-id is not " + "available.") server.schedule_status("ACTIVE", 1) LOG.info(_("FAKE_SERVERS_DB : %s") % str(FAKE_SERVERS_DB)) diff --git a/trove/tests/unittests/instance/test_instance_models.py b/trove/tests/unittests/instance/test_instance_models.py index 6132e9e75b..1160bc5cd8 100644 --- a/trove/tests/unittests/instance/test_instance_models.py +++ b/trove/tests/unittests/instance/test_instance_models.py @@ -40,30 +40,35 @@ class SimpleInstanceTest(TestCase): "public": [{"addr": "15.123.123.123"}]} self.orig_conf = CONF.network_label_regex self.orig_ip_regex = CONF.ip_regex + self.orig_black_list_regex = CONF.black_list_regex def tearDown(self): super(SimpleInstanceTest, self).tearDown() CONF.network_label_regex = self.orig_conf CONF.ip_start = None - CONF.ip_regex = self.orig_ip_regex def test_get_root_on_create(self): root_on_create_val = Instance.get_root_on_create('redis') self.assertFalse(root_on_create_val) - def test_filter_ips(self): + def test_filter_ips_white_list(self): CONF.network_label_regex = '.*' CONF.ip_regex = '^(15.|123.)' + CONF.black_list_regex = '^10.123.123.*' ip = self.instance.get_visible_ip_addresses() - ip = filter_ips(ip, CONF.ip_regex) + ip = filter_ips(ip, CONF.ip_regex, CONF.black_list_regex) self.assertTrue(len(ip) == 2) self.assertTrue('123.123.123.123' in ip) self.assertTrue('15.123.123.123' in ip) - def test_one_network_label_exact(self): - CONF.network_label_regex = '^internal$' + def test_filter_ips_black_list(self): + CONF.network_label_regex = '.*' + CONF.ip_regex = '.*' + CONF.black_list_regex = '^10.123.123.*' ip = self.instance.get_visible_ip_addresses() - self.assertEqual(['10.123.123.123'], ip) + ip = filter_ips(ip, CONF.ip_regex, CONF.black_list_regex) + self.assertTrue(len(ip) == 2) + self.assertTrue('10.123.123.123' not in ip) def test_one_network_label(self): CONF.network_label_regex = 'public' diff --git a/trove/tests/unittests/network/__init__.py b/trove/tests/unittests/network/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/trove/tests/unittests/network/test_neutron_driver.py b/trove/tests/unittests/network/test_neutron_driver.py new file mode 100644 index 0000000000..f40b6e6983 --- /dev/null +++ b/trove/tests/unittests/network/test_neutron_driver.py @@ -0,0 +1,115 @@ +# Copyright 2014 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. +# + +import testtools +from mock import MagicMock +from mock import Mock +from neutronclient.common import exceptions as neutron_exceptions +from neutronclient.v2_0 import client as NeutronClient +from trove.common import exception +from trove.common import remote +from trove.common.models import NetworkRemoteModelBase +from trove.network import neutron +from trove.network.neutron import NeutronDriver as driver +from trove.extensions.security_group.models import RemoteSecurityGroup + + +class NeutronDriverTest(testtools.TestCase): + def setUp(self): + super(NeutronDriverTest, self).setUp() + self.context = Mock() + self.orig_neutron_driver = NetworkRemoteModelBase.get_driver + self.orig_create_sg = driver.create_security_group + self.orig_add_sg_rule = driver.add_security_group_rule + self.orig_del_sg_rule = driver.delete_security_group_rule + self.orig_del_sg = driver.delete_security_group + NetworkRemoteModelBase.get_driver = Mock(return_value=driver) + + def tearDown(self): + super(NeutronDriverTest, self).tearDown() + NetworkRemoteModelBase.get_driver = self.orig_neutron_driver + driver.create_security_group = self.orig_create_sg + driver.add_security_group_rule = self.orig_add_sg_rule + driver.delete_security_group_rule = self.orig_del_sg_rule + driver.delete_security_group = self.orig_del_sg + + def test_create_security_group(self): + driver.create_security_group = Mock() + RemoteSecurityGroup.create(name=Mock(), description=Mock(), + context=self.context) + self.assertEqual(1, driver.create_security_group.call_count) + + def test_add_security_group_rule(self): + driver.add_security_group_rule = Mock() + RemoteSecurityGroup.add_rule(sec_group_id=Mock(), protocol=Mock(), + from_port=Mock(), to_port=Mock(), + cidr=Mock(), context=self.context) + self.assertEqual(1, driver.add_security_group_rule.call_count) + + def test_delete_security_group_rule(self): + driver.delete_security_group_rule = Mock() + RemoteSecurityGroup.delete_rule(sec_group_rule_id=Mock(), + context=self.context) + self.assertEqual(1, driver.delete_security_group_rule.call_count) + + def test_delete_security_group(self): + driver.delete_security_group = Mock() + RemoteSecurityGroup.delete(sec_group_id=Mock(), + context=self.context) + self.assertEqual(1, driver.delete_security_group.call_count) + + +class NeutronDriverExceptionTest(testtools.TestCase): + def setUp(self): + super(NeutronDriverExceptionTest, self).setUp() + self.context = Mock() + self.orig_neutron_driver = NetworkRemoteModelBase.get_driver + self.orig_NeutronClient = NeutronClient.Client + self.orig_get_endpoint = remote.get_endpoint + remote.get_endpoint = MagicMock(return_value="neutron_url") + mock_driver = neutron.NeutronDriver(self.context) + NetworkRemoteModelBase.get_driver = MagicMock( + return_value=mock_driver) + + NeutronClient.Client = Mock( + side_effect=neutron_exceptions.NeutronClientException()) + + def tearDown(self): + super(NeutronDriverExceptionTest, self).tearDown() + NetworkRemoteModelBase.get_driver = self.orig_neutron_driver + NeutronClient.Client = self.orig_NeutronClient + remote.get_endpoint = self.orig_get_endpoint + + def test_create_sg_with_exception(self): + self.assertRaises(exception.SecurityGroupCreationError, + RemoteSecurityGroup.create, + "sg_name", "sg_desc", self.context) + + def test_add_sg_rule_with_exception(self): + self.assertRaises(exception.SecurityGroupRuleCreationError, + RemoteSecurityGroup.add_rule, + "12234", "tcp", "22", "22", + "0.0.0.0/8", self.context) + + def test_delete_sg_rule_with_exception(self): + self.assertRaises(exception.SecurityGroupRuleDeletionError, + RemoteSecurityGroup.delete_rule, + "12234", self.context) + + def test_delete_sg_with_exception(self): + self.assertRaises(exception.SecurityGroupDeletionError, + RemoteSecurityGroup.delete, + "123445", self.context)