]> review.fuel-infra Code Review - openstack-build/neutron-build.git/commitdiff
Add API mac learning extension for NVP
authorarmando-migliaccio <amigliaccio@nicira.com>
Thu, 23 May 2013 05:43:17 +0000 (22:43 -0700)
committerarmando-migliaccio <amigliaccio@nicira.com>
Thu, 6 Jun 2013 17:14:33 +0000 (10:14 -0700)
This commit adds an API extension for NVP where the
NVP supported mac learning feature can be switched
on/off for a specific port. The attribute can be
True or False or omitted altogether.

Implements blueprint nvp-mac-learning-extension

Change-Id: I9173c7dfe0cf4a9ee7b0605722ce7fa01708f5ba

etc/policy.json
quantum/db/migration/alembic_migrations/versions/3cbf70257c28_nvp_mac_learning.py [new file with mode: 0644]
quantum/plugins/nicira/QuantumPlugin.py
quantum/plugins/nicira/dbexts/__init__.py [new file with mode: 0644]
quantum/plugins/nicira/dbexts/maclearning.py [new file with mode: 0644]
quantum/plugins/nicira/extensions/maclearning.py [new file with mode: 0644]
quantum/plugins/nicira/nvplib.py
quantum/tests/unit/nicira/test_maclearning.py [new file with mode: 0644]

index 6e31a33c5694c199acea615440d9f9bf5a578393..8b0bfec0a686495c34d5e761a168c112c564d80e 100644 (file)
@@ -41,6 +41,7 @@
     "create_port:fixed_ips": "rule:admin_or_network_owner",
     "create_port:port_security_enabled": "rule:admin_or_network_owner",
     "create_port:binding:host_id": "rule:admin_only",
+    "create_port:mac_learning_enabled": "rule:admin_or_network_owner",
     "get_port": "rule:admin_or_owner",
     "get_port:queue_id": "rule:admin_only",
     "get_port:binding:vif_type": "rule:admin_only",
@@ -51,6 +52,7 @@
     "update_port:fixed_ips": "rule:admin_or_network_owner",
     "update_port:port_security_enabled": "rule:admin_or_network_owner",
     "update_port:binding:host_id": "rule:admin_only",
+    "update_port:mac_learning_enabled": "rule:admin_or_network_owner",
     "delete_port": "rule:admin_or_owner",
 
     "create_service_type": "rule:admin_only",
diff --git a/quantum/db/migration/alembic_migrations/versions/3cbf70257c28_nvp_mac_learning.py b/quantum/db/migration/alembic_migrations/versions/3cbf70257c28_nvp_mac_learning.py
new file mode 100644 (file)
index 0000000..078455a
--- /dev/null
@@ -0,0 +1,60 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+#
+# Copyright 2013 OpenStack Foundation
+#
+#    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.
+#
+
+"""nvp_mac_learning
+
+Revision ID: 3cbf70257c28
+Revises: 176a85fc7d79
+Create Date: 2013-05-15 10:15:50.875314
+
+"""
+
+# revision identifiers, used by Alembic.
+revision = '3cbf70257c28'
+down_revision = '176a85fc7d79'
+
+# Change to ['*'] if this migration applies to all plugins
+
+migration_for_plugins = [
+    'quantum.plugins.nicira.QuantumPlugin.NvpPluginV2'
+]
+
+from alembic import op
+import sqlalchemy as sa
+
+
+from quantum.db import migration
+
+
+def upgrade(active_plugin=None, options=None):
+    if not migration.should_run(active_plugin, migration_for_plugins):
+        return
+
+    op.create_table(
+        'maclearningstates',
+        sa.Column('port_id', sa.String(length=36), nullable=False),
+        sa.Column('mac_learning_enabled', sa.Boolean(), nullable=False),
+        sa.ForeignKeyConstraint(
+        ['port_id'], ['ports.id'], ondelete='CASCADE'),
+        sa.PrimaryKeyConstraint('port_id'))
+
+
+def downgrade(active_plugin=None, options=None):
+    if not migration.should_run(active_plugin, migration_for_plugins):
+        return
+
+    op.drop_table('maclearningstates')
index e7631cd78daa7c4590f6c17249998b83dc23e3d9..9722042d5812d6173b4eb8627a16bb069dbc7cdc 100644 (file)
@@ -57,6 +57,8 @@ from quantum.plugins.nicira.common import config  # noqa
 from quantum.plugins.nicira.common import exceptions as nvp_exc
 from quantum.plugins.nicira.common import metadata_access as nvp_meta
 from quantum.plugins.nicira.common import securitygroups as nvp_sec
+from quantum.plugins.nicira.dbexts import maclearning as mac_db
+from quantum.plugins.nicira.extensions import maclearning as mac_ext
 from quantum.plugins.nicira.extensions import nvp_networkgw as networkgw
 from quantum.plugins.nicira.extensions import nvp_qos as ext_qos
 from quantum.plugins.nicira import nicira_db
@@ -125,6 +127,7 @@ class NvpPluginV2(db_base_plugin_v2.QuantumDbPluginV2,
                   l3_db.L3_NAT_db_mixin,
                   portsecurity_db.PortSecurityDbMixin,
                   securitygroups_db.SecurityGroupDbMixin,
+                  mac_db.MacLearningDbMixin,
                   networkgw_db.NetworkGatewayMixin,
                   qos_db.NVPQoSDbMixin,
                   nvp_sec.NVPSecurityGroups,
@@ -136,7 +139,8 @@ class NvpPluginV2(db_base_plugin_v2.QuantumDbPluginV2,
     functionality using NVP.
     """
 
-    supported_extension_aliases = ["network-gateway",
+    supported_extension_aliases = ["mac-learning",
+                                   "network-gateway",
                                    "nvp-qos",
                                    "port-security",
                                    "provider",
@@ -363,7 +367,8 @@ class NvpPluginV2(db_base_plugin_v2.QuantumDbPluginV2,
                                    port_data['fixed_ips'],
                                    port_data[psec.PORTSECURITY],
                                    port_data[ext_sg.SECURITYGROUPS],
-                                   port_data[ext_qos.QUEUE])
+                                   port_data[ext_qos.QUEUE],
+                                   port_data.get(mac_ext.MAC_LEARNING))
 
     def _nvp_create_port(self, context, port_data):
         """Driver for creating a logical switch port on NVP platform."""
@@ -1056,6 +1061,7 @@ class NvpPluginV2(db_base_plugin_v2.QuantumDbPluginV2,
                 context, filters)
             for quantum_lport in quantum_lports:
                 self._extend_port_port_security_dict(context, quantum_lport)
+                self._extend_port_mac_learning_state(context, quantum_lport)
         if (filters.get('network_id') and len(filters.get('network_id')) and
             self._network_is_external(context, filters['network_id'][0])):
             # Do not perform check on NVP platform
@@ -1192,6 +1198,10 @@ class NvpPluginV2(db_base_plugin_v2.QuantumDbPluginV2,
             port_data[ext_qos.QUEUE] = self._check_for_queue_and_create(
                 context, port_data)
             self._process_port_queue_mapping(context, port_data)
+            if (isinstance(port_data.get(mac_ext.MAC_LEARNING), bool)):
+                self._create_mac_learning_state(context, port_data)
+            elif mac_ext.MAC_LEARNING in port_data:
+                port_data.pop(mac_ext.MAC_LEARNING)
             # provider networking extension checks
             # Fetch the network and network binding from Quantum db
             try:
@@ -1274,6 +1284,17 @@ class NvpPluginV2(db_base_plugin_v2.QuantumDbPluginV2,
 
             ret_port[ext_qos.QUEUE] = self._check_for_queue_and_create(
                 context, ret_port)
+            # Populate the mac learning attribute
+            new_mac_learning_state = port['port'].get(mac_ext.MAC_LEARNING)
+            old_mac_learning_state = self._get_mac_learning_state(context, id)
+            if (new_mac_learning_state is not None and
+                old_mac_learning_state != new_mac_learning_state):
+                self._update_mac_learning_state(context, id,
+                                                new_mac_learning_state)
+                ret_port[mac_ext.MAC_LEARNING] = new_mac_learning_state
+            elif (new_mac_learning_state is None and
+                  old_mac_learning_state is not None):
+                ret_port[mac_ext.MAC_LEARNING] = old_mac_learning_state
             self._delete_port_queue_mapping(context, ret_port['id'])
             self._process_port_queue_mapping(context, ret_port)
             self._extend_port_port_security_dict(context, ret_port)
@@ -1291,7 +1312,8 @@ class NvpPluginV2(db_base_plugin_v2.QuantumDbPluginV2,
                                        ret_port['fixed_ips'],
                                        ret_port[psec.PORTSECURITY],
                                        ret_port[ext_sg.SECURITYGROUPS],
-                                       ret_port[ext_qos.QUEUE])
+                                       ret_port[ext_qos.QUEUE],
+                                       ret_port.get(mac_ext.MAC_LEARNING))
 
                     # Update the port status from nvp. If we fail here hide it
                     # since the port was successfully updated but we were not
@@ -1365,6 +1387,7 @@ class NvpPluginV2(db_base_plugin_v2.QuantumDbPluginV2,
                                                                 id, fields)
             self._extend_port_port_security_dict(context, quantum_db_port)
             self._extend_port_qos_queue(context, quantum_db_port)
+            self._extend_port_mac_learning_state(context, quantum_db_port)
 
             if self._network_is_external(context,
                                          quantum_db_port['network_id']):
diff --git a/quantum/plugins/nicira/dbexts/__init__.py b/quantum/plugins/nicira/dbexts/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/quantum/plugins/nicira/dbexts/maclearning.py b/quantum/plugins/nicira/dbexts/maclearning.py
new file mode 100644 (file)
index 0000000..44af13e
--- /dev/null
@@ -0,0 +1,73 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+#
+# Copyright 2013 VMware, Inc.  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 sqlalchemy as sa
+from sqlalchemy.orm import exc
+
+from quantum.db import model_base
+from quantum.openstack.common import log as logging
+from quantum.plugins.nicira.extensions import maclearning as mac
+
+LOG = logging.getLogger(__name__)
+
+
+class MacLearningState(model_base.BASEV2):
+
+    port_id = sa.Column(sa.String(36),
+                        sa.ForeignKey('ports.id', ondelete="CASCADE"),
+                        primary_key=True)
+    mac_learning_enabled = sa.Column(sa.Boolean(), nullable=False)
+
+
+class MacLearningDbMixin(object):
+    """Mixin class for mac learning."""
+
+    def _make_mac_learning_state_dict(self, port, fields=None):
+        res = {'port_id': port['port_id'],
+               mac.MAC_LEARNING: port[mac.MAC_LEARNING]}
+        return self._fields(res, fields)
+
+    def _get_mac_learning_state(self, context, port_id):
+        try:
+            query = self._model_query(context, MacLearningState)
+            state = query.filter(MacLearningState.port_id == port_id).one()
+        except exc.NoResultFound:
+            return None
+        return state[mac.MAC_LEARNING]
+
+    def _extend_port_mac_learning_state(self, context, port):
+        state = self._get_mac_learning_state(context, port['id'])
+        if state:
+            port[mac.MAC_LEARNING] = state
+
+    def _update_mac_learning_state(self, context, port_id, enabled):
+        try:
+            query = self._model_query(context, MacLearningState)
+            state = query.filter(MacLearningState.port_id == port_id).one()
+            state.update({mac.MAC_LEARNING: enabled})
+        except exc.NoResultFound:
+            self._create_mac_learning_state(context,
+                                            {'id': port_id,
+                                             mac.MAC_LEARNING: enabled})
+
+    def _create_mac_learning_state(self, context, port):
+        with context.session.begin(subtransactions=True):
+            enabled = port[mac.MAC_LEARNING]
+            state = MacLearningState(port_id=port['id'],
+                                     mac_learning_enabled=enabled)
+            context.session.add(state)
+        return self._make_mac_learning_state_dict(state)
diff --git a/quantum/plugins/nicira/extensions/maclearning.py b/quantum/plugins/nicira/extensions/maclearning.py
new file mode 100644 (file)
index 0000000..7fa1034
--- /dev/null
@@ -0,0 +1,65 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+#
+# Copyright 2013 Nicira Networks, Inc.  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 quantum.api.v2 import attributes
+
+
+MAC_LEARNING = 'mac_learning_enabled'
+EXTENDED_ATTRIBUTES_2_0 = {
+    'ports': {
+        MAC_LEARNING: {'allow_post': True, 'allow_put': True,
+                       'convert_to': attributes.convert_to_boolean,
+                       'default': attributes.ATTR_NOT_SPECIFIED,
+                       'is_visible': True},
+    }
+}
+
+
+class Maclearning(object):
+    """Extension class supporting port security."""
+
+    @classmethod
+    def get_name(cls):
+        return "MAC Learning"
+
+    @classmethod
+    def get_alias(cls):
+        return "mac-learning"
+
+    @classmethod
+    def get_description(cls):
+        return "Provides mac learning capabilities"
+
+    @classmethod
+    def get_namespace(cls):
+        return "http://docs.openstack.org/ext/maclearning/api/v1.0"
+
+    @classmethod
+    def get_updated(cls):
+        return "2013-05-1T10:00:00-00:00"
+
+    @classmethod
+    def get_resources(cls):
+        """Returns Ext Resources."""
+        return []
+
+    def get_extended_resources(self, version):
+        if version == "2.0":
+            return EXTENDED_ATTRIBUTES_2_0
+        else:
+            return {}
index e6f6ff0d6f43417298aee1e378c2c6cb48a82264..2072f64418f83e50ad5e432eed4d4cfa86aa1792 100644 (file)
@@ -738,7 +738,8 @@ def get_port(cluster, network, port, relations=None):
 
 
 def _configure_extensions(lport_obj, mac_address, fixed_ips,
-                          port_security_enabled, security_profiles, queue_id):
+                          port_security_enabled, security_profiles,
+                          queue_id, mac_learning_enabled):
     lport_obj['allowed_address_pairs'] = []
     if port_security_enabled:
         for fixed_ip in fixed_ips:
@@ -753,12 +754,16 @@ def _configure_extensions(lport_obj, mac_address, fixed_ips,
              "ip_address": "0.0.0.0"})
     lport_obj['security_profiles'] = list(security_profiles or [])
     lport_obj['queue_uuid'] = queue_id
+    if mac_learning_enabled is not None:
+        lport_obj["mac_learning"] = mac_learning_enabled
+        lport_obj["type"] = "LogicalSwitchPortConfig"
 
 
 def update_port(cluster, lswitch_uuid, lport_uuid, quantum_port_id, tenant_id,
                 display_name, device_id, admin_status_enabled,
                 mac_address=None, fixed_ips=None, port_security_enabled=None,
-                security_profiles=None, queue_id=None):
+                security_profiles=None, queue_id=None,
+                mac_learning_enabled=None):
     # device_id can be longer than 40 so we rehash it
     hashed_device_id = hashlib.sha1(device_id).hexdigest()
     lport_obj = dict(
@@ -771,7 +776,7 @@ def update_port(cluster, lswitch_uuid, lport_uuid, quantum_port_id, tenant_id,
 
     _configure_extensions(lport_obj, mac_address, fixed_ips,
                           port_security_enabled, security_profiles,
-                          queue_id)
+                          queue_id, mac_learning_enabled)
 
     path = "/ws.v1/lswitch/" + lswitch_uuid + "/lport/" + lport_uuid
     try:
@@ -791,7 +796,8 @@ def update_port(cluster, lswitch_uuid, lport_uuid, quantum_port_id, tenant_id,
 def create_lport(cluster, lswitch_uuid, tenant_id, quantum_port_id,
                  display_name, device_id, admin_status_enabled,
                  mac_address=None, fixed_ips=None, port_security_enabled=None,
-                 security_profiles=None, queue_id=None):
+                 security_profiles=None, queue_id=None,
+                 mac_learning_enabled=None):
     """Creates a logical port on the assigned logical switch."""
     # device_id can be longer than 40 so we rehash it
     hashed_device_id = hashlib.sha1(device_id).hexdigest()
@@ -807,7 +813,7 @@ def create_lport(cluster, lswitch_uuid, tenant_id, quantum_port_id,
 
     _configure_extensions(lport_obj, mac_address, fixed_ips,
                           port_security_enabled, security_profiles,
-                          queue_id)
+                          queue_id, mac_learning_enabled)
 
     path = _build_uri_path(LSWITCHPORT_RESOURCE,
                            parent_resource_id=lswitch_uuid)
diff --git a/quantum/tests/unit/nicira/test_maclearning.py b/quantum/tests/unit/nicira/test_maclearning.py
new file mode 100644 (file)
index 0000000..8a7f563
--- /dev/null
@@ -0,0 +1,141 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright (c) 2013 OpenStack Foundation.
+# 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 contextlib
+import mock
+import os
+
+from oslo.config import cfg
+
+from quantum.api.v2 import attributes
+from quantum.common.test_lib import test_config
+from quantum import context
+from quantum.extensions import agent
+from quantum.openstack.common import log as logging
+import quantum.plugins.nicira as nvp_plugin
+from quantum.tests.unit.nicira import fake_nvpapiclient
+from quantum.tests.unit import test_db_plugin
+
+
+LOG = logging.getLogger(__name__)
+NVP_MODULE_PATH = nvp_plugin.__name__
+NVP_FAKE_RESPS_PATH = os.path.join(os.path.dirname(__file__), 'etc')
+NVP_INI_CONFIG_PATH = os.path.join(os.path.dirname(__file__),
+                                   'etc/nvp.ini.full.test')
+NVP_EXTENSIONS_PATH = os.path.join(os.path.dirname(__file__),
+                                   '../../../plugins/nicira/extensions')
+
+
+class MacLearningExtensionManager(object):
+
+    def get_resources(self):
+        # Add the resources to the global attribute map
+        # This is done here as the setup process won't
+        # initialize the main API router which extends
+        # the global attribute map
+        attributes.RESOURCE_ATTRIBUTE_MAP.update(
+            agent.RESOURCE_ATTRIBUTE_MAP)
+        return agent.Agent.get_resources()
+
+    def get_actions(self):
+        return []
+
+    def get_request_extensions(self):
+        return []
+
+
+class MacLearningDBTestCase(test_db_plugin.QuantumDbPluginV2TestCase):
+    fmt = 'json'
+
+    def setUp(self):
+        self.adminContext = context.get_admin_context()
+        test_config['config_files'] = [NVP_INI_CONFIG_PATH]
+        test_config['plugin_name_v2'] = (
+            'quantum.plugins.nicira.QuantumPlugin.NvpPluginV2')
+        cfg.CONF.set_override('api_extensions_path',
+                              NVP_EXTENSIONS_PATH)
+        # Save the original RESOURCE_ATTRIBUTE_MAP
+        self.saved_attr_map = {}
+        for resource, attrs in attributes.RESOURCE_ATTRIBUTE_MAP.iteritems():
+            self.saved_attr_map[resource] = attrs.copy()
+        ext_mgr = MacLearningExtensionManager()
+        test_config['extension_manager'] = ext_mgr
+        # mock nvp api client
+        self.fc = fake_nvpapiclient.FakeClient(NVP_FAKE_RESPS_PATH)
+        self.mock_nvpapi = mock.patch('%s.NvpApiClient.NVPApiHelper'
+                                      % NVP_MODULE_PATH, autospec=True)
+        instance = self.mock_nvpapi.start()
+
+        def _fake_request(*args, **kwargs):
+            return self.fc.fake_request(*args, **kwargs)
+
+        # Emulate tests against NVP 2.x
+        instance.return_value.get_nvp_version.return_value = "2.999"
+        instance.return_value.request.side_effect = _fake_request
+        cfg.CONF.set_override('metadata_mode', None, 'NVP')
+        self.addCleanup(self.fc.reset_all)
+        self.addCleanup(self.mock_nvpapi.stop)
+        self.addCleanup(self.restore_resource_attribute_map)
+        self.addCleanup(cfg.CONF.reset)
+        super(MacLearningDBTestCase, self).setUp()
+
+    def restore_resource_attribute_map(self):
+        # Restore the original RESOURCE_ATTRIBUTE_MAP
+        attributes.RESOURCE_ATTRIBUTE_MAP = self.saved_attr_map
+
+    def test_create_with_mac_learning(self):
+        with self.port(arg_list=('mac_learning_enabled',),
+                       mac_learning_enabled=True) as port:
+            req = self.new_show_request('ports', port['port']['id'], self.fmt)
+            sport = self.deserialize(self.fmt, req.get_response(self.api))
+            self.assertTrue(sport['port']['mac_learning_enabled'])
+
+    def test_create_port_without_mac_learning(self):
+        with self.port() as port:
+            req = self.new_show_request('ports', port['port']['id'], self.fmt)
+            sport = self.deserialize(self.fmt, req.get_response(self.api))
+            self.assertNotIn('mac_learning', sport['port'])
+
+    def test_update_port_with_mac_learning(self):
+        with self.port(arg_list=('mac_learning_enabled',),
+                       mac_learning_enabled=False) as port:
+            data = {'port': {'mac_learning_enabled': True}}
+            req = self.new_update_request('ports', data, port['port']['id'])
+            res = self.deserialize(self.fmt, req.get_response(self.api))
+            self.assertTrue(res['port']['mac_learning_enabled'])
+
+    def test_update_preexisting_port_with_mac_learning(self):
+        with self.port() as port:
+            req = self.new_show_request('ports', port['port']['id'], self.fmt)
+            sport = self.deserialize(self.fmt, req.get_response(self.api))
+            self.assertNotIn('mac_learning_enabled', sport['port'])
+            data = {'port': {'mac_learning_enabled': True}}
+            req = self.new_update_request('ports', data, port['port']['id'])
+            res = self.deserialize(self.fmt, req.get_response(self.api))
+            self.assertTrue(res['port']['mac_learning_enabled'])
+
+    def test_list_ports(self):
+        # for this test we need to enable overlapping ips
+        cfg.CONF.set_default('allow_overlapping_ips', True)
+        with contextlib.nested(self.port(arg_list=('mac_learning_enabled',),
+                                         mac_learning_enabled=True),
+                               self.port(arg_list=('mac_learning_enabled',),
+                                         mac_learning_enabled=True),
+                               self.port(arg_list=('mac_learning_enabled',),
+                                         mac_learning_enabled=True)):
+            for port in self._list('ports')['ports']:
+                self.assertTrue(port['mac_learning_enabled'])