[apic-mapping] Allowed VM Name extension for L3 Policy

This patch defines a new extension: cisco_apic_gbp_allowed_vm_name,
for the apic policy drivers. An extension attribute:
allowed_vm_names, that extends the L3 Policy definition, is
being introduced in this extension.

A corresponding extension driver: apic_allowed_vm_name, that processes
this extension, is also being added. This extension driver should be
configured for this extension to be available. The driver name should be
added to the existing list of extension drivers under:
[group_policy]
extension_drivers=<existing_ext_drivers>,apic_allowed_vm_name

The allowed_vm_names attribute is a list of regexes. Each regex can
be up to 255 characters long.

While during the port-binding phase, we will also enforce the regex
checking against the VM name from Nova. Only those VM names matching
one of those regexes will be allowed.

A CLI option: --allowed_vm_names will be provided for the
L3 Policy create and update operations. This CLI option will accept
a comma separated string as the option value.

Change-Id: I4602919df9a0458eb255b93399c70f64dfeeb863
This commit is contained in:
Kent Wu 2016-10-11 17:16:32 -07:00
parent 7dc9e64c94
commit 29cd855015
9 changed files with 415 additions and 11 deletions

View File

@ -0,0 +1,50 @@
# 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 neutron.db import model_base
import sqlalchemy as sa
class ApicAllowedVMNameDB(model_base.BASEV2):
__tablename__ = 'gp_apic_mapping_allowed_vm_names'
l3_policy_id = sa.Column(sa.String(36),
sa.ForeignKey('gp_l3_policies.id',
ondelete="CASCADE"),
primary_key=True)
allowed_vm_name = sa.Column(sa.String(255), primary_key=True)
class ApicAllowedVMNameDBMixin(object):
def get_l3_policy_allowed_vm_names(self, session, l3_policy_id):
rows = (session.query(ApicAllowedVMNameDB).filter_by(
l3_policy_id=l3_policy_id).all())
return rows
def get_l3_policy_allowed_vm_name(self, session, l3_policy_id,
allowed_vm_name):
row = (session.query(ApicAllowedVMNameDB).filter_by(
l3_policy_id=l3_policy_id,
allowed_vm_name=allowed_vm_name).one())
return row
def add_l3_policy_allowed_vm_name(self, session, l3_policy_id,
allowed_vm_name):
row = ApicAllowedVMNameDB(l3_policy_id=l3_policy_id,
allowed_vm_name=allowed_vm_name)
session.add(row)
def delete_l3_policy_allowed_vm_name(self, session, l3_policy_id,
allowed_vm_name):
row = self.get_l3_policy_allowed_vm_name(
session, l3_policy_id, allowed_vm_name)
session.delete(row)

View File

@ -0,0 +1,45 @@
# 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.
"""allowed_vm_names
Revision ID: 5629167be1d1
Revises: 092e4b1aeb0a
Create Date: 2016-10-11 14:14:06.648609
"""
# revision identifiers, used by Alembic.
revision = '5629167be1d1'
down_revision = '092e4b1aeb0a'
from alembic import op
import sqlalchemy as sa
def upgrade():
op.create_table(
'gp_apic_mapping_allowed_vm_names',
sa.Column('l3_policy_id', sa.String(length=36), nullable=False),
sa.Column('allowed_vm_name', sa.String(length=255),
nullable=False),
sa.ForeignKeyConstraint(
['l3_policy_id'], ['gp_l3_policies.id'],
name='gp_apic_mapping_allowed_vm_name_fk_l3pid',
ondelete='CASCADE'),
sa.PrimaryKeyConstraint('l3_policy_id', 'allowed_vm_name')
)
def downgrade():
pass

View File

@ -1 +1 @@
092e4b1aeb0a
5629167be1d1

View File

@ -0,0 +1,55 @@
# 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 neutron.api import extensions
from neutron.api.v2 import attributes as attr
from gbpservice.neutron.extensions import group_policy as gp
CISCO_APIC_GBP_ALLOWED_VM_NAME_EXT = 'cisco_apic_gbp_allowed_vm_name'
EXTENDED_ATTRIBUTES_2_0 = {
gp.L3_POLICIES: {
'allowed_vm_names': {
'allow_post': True, 'allow_put': True, 'default': None,
'validate': {'type:list_of_unique_strings': None},
'convert_to': attr.convert_none_to_empty_list,
'is_visible': True},
},
}
class Apic_allowed_vm_name(extensions.ExtensionDescriptor):
@classmethod
def get_name(cls):
return "APIC GBP Allowed VM Name Extension"
@classmethod
def get_alias(cls):
return CISCO_APIC_GBP_ALLOWED_VM_NAME_EXT
@classmethod
def get_description(cls):
return _("This extension supports a list of allowed VM name regexes "
"that can be applied to the L3 policy resource.")
@classmethod
def get_updated(cls):
return "2016-10-10T10:00:00-00:00"
def get_extended_resources(self, version):
if version == "2.0":
return EXTENDED_ATTRIBUTES_2_0
else:
return {}

View File

@ -14,6 +14,7 @@
# under the License.
import copy
import re
from neutron._i18n import _LW
from neutron.common import constants as n_constants
@ -27,6 +28,8 @@ from oslo_utils import importutils
from gbpservice.neutron.services.grouppolicy.drivers.cisco.apic import (
apic_mapping as amap)
from gbpservice.neutron.services.grouppolicy.drivers.cisco.apic import (
nova_client as nclient)
LOG = log.getLogger(__name__)
@ -42,6 +45,7 @@ class APICMechanismGBPDriver(api.MechanismDriver):
def __init__(self):
super(APICMechanismGBPDriver, self).__init__()
self._dvs_notifier = None
self._apic_allowed_vm_name_driver = None
def _agent_bind_port(self, context, agent_list, bind_strategy):
"""Attempt port binding per agent.
@ -69,22 +73,54 @@ class APICMechanismGBPDriver(api.MechanismDriver):
AgentMechanismDriverBase class, but is modified
to support multiple L2 agent types (DVS and OpFlex).
"""
port = context.current
LOG.debug("Attempting to bind port %(port)s on "
"network %(network)s",
{'port': context.current['id'],
{'port': port['id'],
'network': context.network.current['id']})
vnic_type = context.current.get(portbindings.VNIC_TYPE,
portbindings.VNIC_NORMAL)
vnic_type = port.get(portbindings.VNIC_TYPE,
portbindings.VNIC_NORMAL)
if vnic_type not in [portbindings.VNIC_NORMAL]:
LOG.debug("Refusing to bind due to unsupported vnic_type: %s",
vnic_type)
return
# Attempt to bind ports for DVS agents for nova-compute daemons
# first. This allows having network agents (dhcp, metadata)
# that typically run on a network node using an OpFlex agent to
# co-exist with nova-compute daemons for ESX, which host DVS agents.
if context.current['device_owner'].startswith('compute:'):
if port['device_owner'].startswith('compute:'):
# enforce the allowed_vm_names rules if possible
if (port['device_id'] and self.apic_allowed_vm_name_driver):
ptg, pt = self.apic_gbp._port_id_to_ptg(
context._plugin_context, port['id'])
if ptg is None:
LOG.warning(_LW("PTG for port %s does not exist"),
port['id'])
return
l2p = self.apic_gbp._get_l2_policy(context._plugin_context,
ptg['l2_policy_id'])
l3p = self.apic_gbp.gbp_plugin.get_l3_policy(
context._plugin_context, l2p['l3_policy_id'])
ok_to_bind = True
if l3p.get('allowed_vm_names'):
ok_to_bind = False
vm = nclient.NovaClient().get_server(port['device_id'])
for allowed_vm_name in l3p['allowed_vm_names']:
match = re.search(allowed_vm_name, vm.name)
if match:
ok_to_bind = True
break
if not ok_to_bind:
LOG.warning(_LW("Failed to bind the port due to "
"allowed_vm_names rules %(rules)s "
"for VM: %(vm)s"),
{'rules': l3p['allowed_vm_names'],
'vm': vm.name})
return
# Attempt to bind ports for DVS agents for nova-compute daemons
# first. This allows having network agents (dhcp, metadata)
# that typically run on a network node using an OpFlex agent to
# co-exist with nova-compute daemons for ESX, which host DVS
# agents.
agent_list = context.host_agents(AGENT_TYPE_DVS)
if self._agent_bind_port(context, agent_list, self._bind_dvs_port):
return
@ -205,6 +241,21 @@ class APICMechanismGBPDriver(api.MechanismDriver):
self._dvs_notifier = None
return self._dvs_notifier
@property
def apic_allowed_vm_name_driver(self):
if self._apic_allowed_vm_name_driver is False:
return False
if not self._apic_allowed_vm_name_driver:
ext_drivers = (self.apic_gbp.gbp_plugin.extension_manager.
ordered_ext_drivers)
for driver in ext_drivers:
if 'apic_allowed_vm_name' == driver.name:
self._apic_allowed_vm_name_driver = driver.obj
break
if not self._apic_allowed_vm_name_driver:
self._apic_allowed_vm_name_driver = False
return self._apic_allowed_vm_name_driver
def create_port_postcommit(self, context):
self.apic_gbp.process_port_added(context)

View File

@ -0,0 +1,85 @@
# 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 oslo_log import log as logging
import re
from gbpservice.neutron.db.grouppolicy.extensions import (
apic_allowed_vm_name_db as db)
from gbpservice.neutron.extensions import apic_allowed_vm_name as aavnext
from gbpservice.neutron.services.grouppolicy import (
group_policy_driver_api as api)
from gbpservice.neutron.services.grouppolicy.common import exceptions as gpexc
LOG = logging.getLogger(__name__)
class AllowedVMNameBadRegex(gpexc.GroupPolicyBadRequest):
message = _("Bad regex: %(regex)s is defined for the allowed-vm-names "
"attribute.")
class ApicAllowedVMNameExtensionDriver(api.ExtensionDriver,
db.ApicAllowedVMNameDBMixin):
_supported_extension_alias = aavnext.CISCO_APIC_GBP_ALLOWED_VM_NAME_EXT
_extension_dict = aavnext.EXTENDED_ATTRIBUTES_2_0
def __init__(self):
LOG.debug("APIC Allowed VM Name Extension Driver __init__")
self._policy_driver = None
def initialize(self):
pass
@property
def extension_alias(self):
return self._supported_extension_alias
def process_create_l3_policy(self, session, data, result):
l3p = data['l3_policy']
if 'allowed_vm_names' in l3p:
for vm_name in l3p['allowed_vm_names']:
try:
re.compile(vm_name)
except re.error:
raise AllowedVMNameBadRegex(regex=vm_name)
self.add_l3_policy_allowed_vm_name(
session, l3_policy_id=result['id'],
allowed_vm_name=vm_name)
def process_update_l3_policy(self, session, data, result):
l3p = data['l3_policy']
if not 'allowed_vm_names' in l3p:
return
rows = self.get_l3_policy_allowed_vm_names(
session, l3_policy_id=result['id'])
old_vm_names = [r.allowed_vm_name for r in rows]
add_vm_names = list(set(l3p['allowed_vm_names']) - set(old_vm_names))
for vm_name in add_vm_names:
try:
re.compile(vm_name)
except re.error:
raise AllowedVMNameBadRegex(regex=vm_name)
self.add_l3_policy_allowed_vm_name(
session, l3_policy_id=result['id'],
allowed_vm_name=vm_name)
rm_vm_names = list(set(old_vm_names) - set(l3p['allowed_vm_names']))
for vm_name in rm_vm_names:
self.delete_l3_policy_allowed_vm_name(
session, l3_policy_id=result['id'],
allowed_vm_name=vm_name)
def extend_l3_policy_dict(self, session, result):
rows = self.get_l3_policy_allowed_vm_names(
session, l3_policy_id=result['id'])
allowed_vm_names = [r.allowed_vm_name for r in rows]
result['allowed_vm_names'] = allowed_vm_names

View File

@ -0,0 +1,86 @@
# 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 neutron.db import api as db_api
from gbpservice.neutron.db.grouppolicy.extensions import (
apic_allowed_vm_name_db as db)
from gbpservice.neutron.tests.unit.services.grouppolicy import (
test_extension_driver_api as test_ext_base)
class ExtensionDriverTestCase(test_ext_base.ExtensionDriverTestBase):
_extension_drivers = ['apic_allowed_vm_name']
_extension_path = None
def test_l3p_lifecycle(self):
l3p = self.create_l3_policy(name='myl3')['l3_policy']
self.assertEqual([], l3p['allowed_vm_names'])
l3p = self.show_l3_policy(
l3p['id'], expected_res_status=200)['l3_policy']
self.assertEqual([], l3p['allowed_vm_names'])
self.delete_l3_policy(l3p['id'], tenant_id=l3p['tenant_id'],
expected_res_status=204)
allowed_vm_names = []
l3p = self.create_l3_policy(
name='myl3',
allowed_vm_names=allowed_vm_names)['l3_policy']
self.assertItemsEqual(allowed_vm_names, l3p['allowed_vm_names'])
l3p = self.show_l3_policy(
l3p['id'], expected_res_status=200)['l3_policy']
self.assertItemsEqual([], l3p['allowed_vm_names'])
self.delete_l3_policy(l3p['id'], tenant_id=l3p['tenant_id'],
expected_res_status=204)
allowed_vm_names = ['safe_vm*', '^secure_vm*']
l3p = self.create_l3_policy(
name='myl3',
allowed_vm_names=allowed_vm_names)['l3_policy']
self.assertItemsEqual(allowed_vm_names, l3p['allowed_vm_names'])
l3p = self.show_l3_policy(
l3p['id'], expected_res_status=200)['l3_policy']
self.assertItemsEqual(allowed_vm_names, l3p['allowed_vm_names'])
allowed_vm_names = ['good_vm*', '^ok_vm*', 'safe_vm*']
l3p = self.update_l3_policy(
l3p['id'], allowed_vm_names=allowed_vm_names,
expected_res_status=200)['l3_policy']
self.assertItemsEqual(allowed_vm_names, l3p['allowed_vm_names'])
l3p = self.show_l3_policy(
l3p['id'], expected_res_status=200)['l3_policy']
self.assertItemsEqual(allowed_vm_names, l3p['allowed_vm_names'])
allowed_vm_names = []
l3p = self.update_l3_policy(
l3p['id'], allowed_vm_names=allowed_vm_names,
expected_res_status=200)['l3_policy']
self.assertItemsEqual(allowed_vm_names, l3p['allowed_vm_names'])
l3p = self.show_l3_policy(
l3p['id'], expected_res_status=200)['l3_policy']
self.assertItemsEqual(allowed_vm_names, l3p['allowed_vm_names'])
allowed_vm_names = ['^ok_vm*']
l3p = self.update_l3_policy(
l3p['id'], allowed_vm_names=allowed_vm_names,
expected_res_status=200)['l3_policy']
self.assertItemsEqual(allowed_vm_names, l3p['allowed_vm_names'])
l3p = self.show_l3_policy(
l3p['id'], expected_res_status=200)['l3_policy']
self.assertItemsEqual(allowed_vm_names, l3p['allowed_vm_names'])
self.delete_l3_policy(l3p['id'], tenant_id=l3p['tenant_id'],
expected_res_status=204)
session = db_api.get_session()
rows = (session.query(db.ApicAllowedVMNameDB).filter_by(
l3_policy_id=l3p['id']).all())
self.assertEqual([], rows)

View File

@ -94,7 +94,7 @@ class MockCallRecorder(mock.Mock):
class ApicMappingTestCase(
test_rmd.ResourceMappingTestCase,
mocked.ControllerMixin, mocked.ConfigMixin):
_extension_drivers = ['apic_segmentation_label']
_extension_drivers = ['apic_segmentation_label', 'apic_allowed_vm_name']
_extension_path = None
def setUp(self, sc_plugin=None, nat_enabled=True,
@ -1532,7 +1532,6 @@ class FakePortContext(object):
class TestPolicyTargetDvs(ApicMappingTestCase):
def setUp(self):
super(TestPolicyTargetDvs, self).setUp()
self.driver.apic_manager.app_profile_name = mocked.APIC_AP
@ -1715,6 +1714,38 @@ class TestPolicyTargetDvs(ApicMappingTestCase):
self.ml2.delete_port_postcommit(port_ctx)
self._verify_dvs_notifier('delete_port_call', newp1['port'], 'h1')
def test_bind_port_with_allowed_vm_names(self):
allowed_vm_names = ['safe_vm*', '^secure_vm*']
l3p = self.create_l3_policy(name='myl3',
allowed_vm_names=allowed_vm_names)['l3_policy']
l2p = self.create_l2_policy(
name='myl2', l3_policy_id=l3p['id'])['l2_policy']
ptg = self.create_policy_target_group(
name="ptg1", l2_policy_id=l2p['id'])['policy_target_group']
pt = self.create_policy_target(
policy_target_group_id=ptg['id'])['policy_target']
nova_client = mock.patch(
'gbpservice.neutron.services.grouppolicy.drivers.cisco.'
'apic.nova_client.NovaClient.get_server').start()
vm = mock.Mock()
vm.name = 'secure_vm1'
nova_client.return_value = vm
newp1 = self._bind_port_to_host(pt['port_id'], 'h1')
self.assertEqual(newp1['port']['binding:vif_type'], 'ovs')
# bind again
vm.name = 'bad_vm1'
newp1 = self._bind_port_to_host(pt['port_id'], 'h2')
self.assertEqual(newp1['port']['binding:vif_type'], 'binding_failed')
# update l3p with empty allowed_vm_names
l3p = self.update_l3_policy(l3p['id'], tenant_id=l3p['tenant_id'],
allowed_vm_names=[],
expected_res_status=200)['l3_policy']
newp1 = self._bind_port_to_host(pt['port_id'], 'h3')
self.assertEqual(newp1['port']['binding:vif_type'], 'ovs')
class TestPolicyTargetGroup(ApicMappingTestCase):

View File

@ -60,6 +60,7 @@ gbpservice.neutron.group_policy.extension_drivers =
proxy_group = gbpservice.neutron.services.grouppolicy.drivers.extensions.proxy_group_driver:ProxyGroupDriver
aim_extension = gbpservice.neutron.services.grouppolicy.drivers.extensions.aim_mapping_extension_driver:AIMExtensionDriver
apic_segmentation_label = gbpservice.neutron.services.grouppolicy.drivers.extensions.apic_segmentation_label_driver:ApicSegmentationLabelExtensionDriver
apic_allowed_vm_name = gbpservice.neutron.services.grouppolicy.drivers.extensions.apic_allowed_vm_name_driver:ApicAllowedVMNameExtensionDriver
gbpservice.neutron.group_policy.policy_drivers =
dummy = gbpservice.neutron.services.grouppolicy.drivers.dummy_driver:NoopDriver
implicit_policy = gbpservice.neutron.services.grouppolicy.drivers.implicit_policy:ImplicitPolicyDriver