Remove offloading modes from data model

Change-Id: I4869f3a08bb1fd60cceb324d2b66a31553e90bd4
Implements: blueprint nics-and-nodes-attributes-via-plugin
This commit is contained in:
Andriy Popovych 2016-08-23 11:32:17 +03:00
parent 4ed6b4570a
commit 95321932af
14 changed files with 122 additions and 395 deletions

View File

@ -16,5 +16,3 @@ from nailgun.api.v1.validators.json_schema \
import cluster as cluster_schema
from nailgun.api.v1.validators.json_schema \
import node as node_schema
from nailgun.api.v1.validators.json_schema \
import interface as iface_schema

View File

@ -199,7 +199,6 @@ def upload_fixture(fileobj, loader=None):
# UGLY HACK for testing
if new_obj.__class__.__name__ == 'Node':
objects.Node.update_interfaces(new_obj)
objects.Node.update_interfaces_offloading_modes(new_obj)
fire_callback_on_node_create(new_obj)
db().commit()

View File

@ -768,8 +768,6 @@ class NetworkManager(object):
in iface['assigned_networks']]
objects.NIC.assign_networks(current_iface, nets_to_assign)
update = {}
if 'offloading_modes' in iface:
update['offloading_modes'] = iface['offloading_modes']
if 'attributes' in iface:
update['attributes'] = nailgun_utils.dict_merge(
current_iface.attributes,
@ -814,10 +812,8 @@ class NetworkManager(object):
objects.Bond.get_attributes(bond_db),
bond_attributes
)
update = {
'slaves': slaves,
'offloading_modes': bond.get('offloading_modes', {}),
'attributes': bond_attributes
}
objects.Bond.update(bond_db, update)
@ -1400,27 +1396,14 @@ class NetworkManager(object):
iface.attributes['offloading']['disable']['value']
}
# TODO(apopovych): rewrite to get offloading data from attributes
if iface.offloading_modes:
modified_offloading_modes = \
cls._get_modified_offloading_modes(iface.offloading_modes)
if modified_offloading_modes:
properties['ethtool'] = {}
properties['ethtool']['offload'] = \
modified_offloading_modes
if iface.attributes.get('offloading', {}).get(
'modes', {}).get('value'):
properties['ethtool'] = {
'offload': iface.attributes['offloading']['modes']['value']
}
return properties
@classmethod
def _get_modified_offloading_modes(cls, offloading_modes):
result = dict()
for mode in offloading_modes:
if mode['state'] is not None:
result[mode['name']] = mode['state']
if mode['sub'] and mode['state'] is not False:
result.update(cls._get_modified_offloading_modes(mode['sub']))
return result
@classmethod
def find_nic_assoc_with_ng(cls, node, network_group):
"""Will find iface on node that is associated with network_group.

View File

@ -13,8 +13,6 @@
# 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 copy
from sqlalchemy import Boolean
from sqlalchemy import Column
from sqlalchemy.dialects import postgresql as psql
@ -29,7 +27,6 @@ from nailgun import consts
from nailgun.db.sqlalchemy.models.base import Base
from nailgun.db.sqlalchemy.models.fields import JSON
from nailgun.db.sqlalchemy.models.mutable import MutableDict
from nailgun.db.sqlalchemy.models.mutable import MutableList
class IPAddr(Base):
@ -152,10 +149,6 @@ class NodeNICInterface(Base):
driver = Column(Text)
bus_info = Column(Text)
pxe = Column(Boolean, default=False, nullable=False)
offloading_modes = Column(MutableList.as_mutable(JSON),
default=[], nullable=False,
server_default='[]')
attributes = Column(
MutableDict.as_mutable(JSON),
default={}, server_default='{}', nullable=False)
@ -178,24 +171,6 @@ class NodeNICInterface(Base):
def assigned_networks(self, value):
self.assigned_networks_list = value
# TODO(fzhadaev): move to object
@classmethod
def offloading_modes_as_flat_dict(cls, modes):
"""Represents multilevel structure of offloading modes as flat dict
This is done to ease merging
:param modes: list of offloading modes
:return: flat dictionary {mode['name']: mode['state']}
"""
result = dict()
if modes is None:
return result
for mode in modes:
result[mode["name"]] = mode["state"]
if mode["sub"]:
result.update(cls.offloading_modes_as_flat_dict(mode["sub"]))
return result
class NodeBondInterface(Base):
__tablename__ = 'node_bond_interfaces'
@ -252,57 +227,3 @@ class NodeBondInterface(Base):
@assigned_networks.setter
def assigned_networks(self, value):
self.assigned_networks_list = value
@property
def offloading_modes(self):
tmp = None
intersection_dict = {}
for interface in self.slaves:
modes = interface.offloading_modes
if tmp is None:
tmp = modes
intersection_dict = \
interface.offloading_modes_as_flat_dict(tmp)
continue
intersection_dict = self._intersect_offloading_dicts(
intersection_dict,
interface.offloading_modes_as_flat_dict(modes)
)
return self._apply_intersection(tmp, intersection_dict)
@offloading_modes.setter
def offloading_modes(self, new_modes):
new_modes_dict = \
NodeNICInterface.offloading_modes_as_flat_dict(new_modes)
for interface in self.slaves:
self._update_modes(interface.offloading_modes, new_modes_dict)
interface.offloading_modes.changed()
def _update_modes(self, modes, update_dict):
for mode in modes:
if mode['name'] in update_dict:
mode['state'] = update_dict[mode['name']]
if mode['sub']:
self._update_modes(mode['sub'], update_dict)
def _intersect_offloading_dicts(self, dict1, dict2):
result = dict()
for mode in dict1:
if mode in dict2:
result[mode] = dict1[mode] and dict2[mode]
return result
def _apply_intersection(self, modes, intersection_dict):
result = list()
if modes is None:
return result
for mode in copy.deepcopy(modes):
if mode["name"] not in intersection_dict:
continue
mode["state"] = intersection_dict[mode["name"]]
if mode["sub"]:
mode["sub"] = \
self._apply_intersection(mode["sub"], intersection_dict)
result.append(mode)
return result

View File

@ -43,7 +43,6 @@ class DPDKMixin(object):
@classmethod
def refresh_interface_dpdk_properties(cls, interface, dpdk_drivers):
attributes = interface.attributes
meta = interface.meta
dpdk_attributes = copy.deepcopy(attributes.get('dpdk', {}))
dpdk_available = cls.dpdk_available(interface, dpdk_drivers)
@ -53,8 +52,10 @@ class DPDKMixin(object):
# update attributes in DB only if something was changed
if attributes.get('dpdk', {}) != dpdk_attributes:
attributes['dpdk'] = dpdk_attributes
if meta.get('dpdk', {}) != {'available': dpdk_available}:
meta['dpdk'] = {'available': dpdk_available}
if 'meta' in interface:
meta = interface.meta
if meta.get('dpdk', {}) != {'available': dpdk_available}:
meta['dpdk'] = {'available': dpdk_available}
class NIC(DPDKMixin, NailgunObject):
@ -107,32 +108,6 @@ class NIC(DPDKMixin, NailgunObject):
enabled = instance.attributes.get('sriov', {}).get('enabled')
return enabled and enabled['value']
@classmethod
def update_offloading_modes(cls, instance, new_modes, keep_states=False):
"""Update information about offloading modes for the interface.
:param instance: Interface object
:param new_modes: New offloading modes
:param keep_states: If True, information about available modes will be
updated, but states configured by user will not be overwritten.
"""
def set_old_states(modes):
"""Set old state for offloading modes
:param modes: List of offloading modes
"""
for mode in modes:
if mode['name'] in old_modes_states:
mode['state'] = old_modes_states[mode['name']]
if mode.get('sub'):
set_old_states(mode['sub'])
if keep_states:
old_modes_states = instance.offloading_modes_as_flat_dict(
instance.offloading_modes)
set_old_states(new_modes)
instance.offloading_modes = new_modes
@classmethod
def get_nic_interfaces_for_all_nodes(cls, cluster, networks=None):
nic_interfaces_query = db().query(

View File

@ -36,7 +36,6 @@ class NodeInterfacesSerializer(BasicSerializer):
'driver',
'bus_info',
'meta',
'offloading_modes',
'pxe'
)
bond_fields = (
@ -45,8 +44,7 @@ class NodeInterfacesSerializer(BasicSerializer):
'type',
'mode',
'state',
'assigned_networks',
'offloading_modes'
'assigned_networks'
)
nic_fields_60 = (

View File

@ -477,7 +477,8 @@ class TestHandlers(BaseIntegrationTest):
self.assertEqual(len(resp.json_body), 1)
resp_nic = resp.json_body[0]
nic = new_meta['interfaces'][0]
self.assertEqual(resp_nic['offloading_modes'], nic['offloading_modes'])
self.assertEqual(
nic['offloading_modes'], resp_nic['meta']['offloading_modes'])
def test_NIC_change_offloading_modes(self):
meta = self.env.default_metadata()
@ -518,7 +519,8 @@ class TestHandlers(BaseIntegrationTest):
self.assertEqual(len(resp.json_body), 1)
resp_nic = resp.json_body[0]
nic = new_meta['interfaces'][0]
self.assertEqual(resp_nic['offloading_modes'], nic['offloading_modes'])
self.assertEqual(
nic['offloading_modes'], resp_nic['meta']['offloading_modes'])
resp = self.app.get(
reverse('NodeCollectionHandler', kwargs={'node_id': node['id']}),
@ -548,8 +550,7 @@ class TestHandlers(BaseIntegrationTest):
}
]
}
self.env.set_interfaces_in_meta(resp_node["meta"], [
new_nic])
self.env.set_interfaces_in_meta(resp_node["meta"], [new_nic])
resp_node.pop('group_id')
@ -566,8 +567,7 @@ class TestHandlers(BaseIntegrationTest):
self.assertEqual(len(resp.json_body), 1)
resp_nic = resp.json_body[0]
self.assertEqual(
resp_nic['offloading_modes'],
new_nic['offloading_modes'])
new_nic['offloading_modes'], resp_nic['meta']['offloading_modes'])
def test_NIC_locking_on_update_by_agent(self):
lock_vs_status = (

View File

@ -168,7 +168,8 @@ class TestNodeNICsBonding(BaseIntegrationTest):
'xmit_hash_policy': {
'value': {'value': BOND_XMIT_HASH_POLICY.layer2_3}},
'lacp_rate': {'value': {'value': 'slow'}},
'type__': {'value': bond_type}
'type__': {'value': bond_type},
'offloading': {'modes': {'value': {'mode_common': None}}}
}
attributes.update(iface_props)
@ -195,13 +196,9 @@ class TestNodeNICsBonding(BaseIntegrationTest):
resp.json_body)
self.assertEqual(len(bonds), 1)
self.assertEqual(bonds[0]["name"], bond_name)
bond_offloading_modes = bonds[0]['offloading_modes']
self.assertEqual(len(bond_offloading_modes), 1)
modes = bonds[0]['attributes']['offloading']['modes']['value']
self.assertDictEqual(
bond_offloading_modes[0],
{'name': 'mode_common',
'state': None,
'sub': []})
modes, {'mode_common': None})
def nics_bond_remove(self, put_func):
resp = self.env.node_nics_get(self.env.nodes[0]["id"])
@ -716,15 +713,14 @@ class TestNodeNICsBonding(BaseIntegrationTest):
body)
self.assertEqual(1, len(bonds))
bond_offloading_modes = bonds[0]['offloading_modes']
bond_offloading_modes = bonds[0]['attributes'][
'offloading']['modes']['value']
self.assertEqual(len(bond_offloading_modes), 1)
slaves = bonds[0]['slaves']
self.assertEqual(2, len(slaves))
self.assertIsNone(bond_offloading_modes[0]['state'])
bond_offloading_modes[0]['state'] = True
self.assertTrue(bond_offloading_modes[0]['state'])
self.assertIsNone(bond_offloading_modes['mode_common'])
bond_offloading_modes['mode_common'] = True
resp = self.env.node_nics_put(
self.env.nodes[0]["id"],
@ -736,12 +732,13 @@ class TestNodeNICsBonding(BaseIntegrationTest):
body)
self.assertEqual(1, len(bonds))
bond_offloading_modes = bonds[0]['offloading_modes']
bond_offloading_modes = bonds[0]['attributes'][
'offloading']['modes']['value']
self.assertEqual(len(bond_offloading_modes), 1)
slaves = bonds[0]['slaves']
self.assertEqual(2, len(slaves))
self.assertTrue(bond_offloading_modes[0]['state'])
self.assertTrue(bond_offloading_modes['mode_common'])
def test_nics_bond_cannot_contain_sriov_enabled_interfaces(self):
self.data.append({

View File

@ -14,6 +14,7 @@
# License for the specific language governing permissions and limitations
# under the License.
import copy
import mock
from netaddr import IPNetwork
@ -96,16 +97,19 @@ class TestBondObject(BaseTestCase):
'name': 'bond0',
'slaves': self.node.nic_interfaces,
'node': self.node,
'attributes': {
'offloading': {
'modes': {'value': {'test_mode': 'mode'}}}}
}
bond = objects.Bond.create(data)
offloading_modes = bond.offloading_modes
offloading_modes[0]['state'] = 'test'
data = {
'offloading_modes': offloading_modes
new_data = {
'attributes': {
'offloading': {
'modes': {'value': {'test_mode': 'test'}}}}
}
objects.Bond.update(bond, data)
self.assertEqual(data['offloading_modes'], bond.offloading_modes)
objects.Bond.update(bond, copy.deepcopy(new_data))
self.assertEqual(new_data['attributes'], bond['attributes'])
def test_get_bond_interfaces_for_all_nodes(self):
node = self.env.nodes[0]
@ -120,6 +124,71 @@ class TestBondObject(BaseTestCase):
class TestNICObject(BaseTestCase):
changed_modes = [
{
'name': 'mode_1',
'state': True,
'sub': [
{
'name': 'sub_mode_1',
'state': None,
'sub': []
}
]
},
{
'name': 'mode_2',
'state': None,
'sub': [
{
'name': 'sub_mode_2',
'state': False,
'sub': []
}
]
}
]
expected_result = {
'mode_1': True,
'sub_mode_1': None,
'mode_2': None,
'sub_mode_2': False
}
deep_structure = [
{
'name': 'level_1',
'state': True,
'sub': [
{
'name': 'level_2',
'state': None,
'sub': [
{
'name': 'level_3',
'state': None,
'sub': [
{
'name': 'level_4',
'state': False,
'sub': []
}
]
}
]
}
]
}
]
expected_result_deep = {
'level_1': True,
'level_2': None,
'level_3': None,
'level_4': False
}
def setUp(self):
super(TestNICObject, self).setUp()
@ -127,6 +196,16 @@ class TestNICObject(BaseTestCase):
cluster_kwargs={'api': False},
nodes_kwargs=[{'role': 'controller'}])
def test_offloading_modes_as_flat_dict(self):
self.assertDictEqual(
self.expected_result,
objects.NIC.offloading_modes_as_flat_dict(
self.changed_modes))
self.assertDictEqual(
self.expected_result_deep,
objects.NIC.offloading_modes_as_flat_dict(
self.deep_structure))
def test_replace_assigned_networks(self):
node = self.env.nodes[0]
nic_1 = node.interfaces[0]
@ -166,44 +245,6 @@ class TestNICObject(BaseTestCase):
self.env.clusters[0])
self.assertEqual(len(nic_interfaces), len(interfaces))
def test_update_offloading_modes(self):
node = self.env.nodes[0]
new_modes = [
{'state': True, 'name': 'tx-checksumming', 'sub': [
{'state': False, 'name': 'tx-checksum-sctp', 'sub': []},
{'state': True, 'name': 'tx-checksum-ipv6', 'sub': []},
{'state': None, 'name': 'tx-checksum-ipv4', 'sub': []}]},
{'state': None, 'name': 'rx-checksumming', 'sub': []},
{'state': True, 'name': 'new_offloading_mode', 'sub': []}]
objects.NIC.update_offloading_modes(node.interfaces[0], new_modes)
self.assertListEqual(node.interfaces[0].offloading_modes, new_modes)
def test_update_offloading_modes_keep_states(self):
node = self.env.nodes[0]
old_modes = [
{'state': True, 'name': 'tx-checksumming', 'sub': [
{'state': False, 'name': 'tx-checksum-sctp', 'sub': []},
{'state': True, 'name': 'tx-checksum-ipv6', 'sub':
[{'state': None, 'name': 'tx-checksum-ipv4', 'sub': []}]}]
}]
node.interfaces[0].offloading_modes = old_modes
new_mode = {'state': True, 'name': 'new_offloading_mode', 'sub': []}
new_modes = [
{'state': True, 'name': 'tx-checksumming', 'sub': [
{'state': True, 'name': 'tx-checksum-sctp', 'sub': []},
{'state': True, 'name': 'tx-checksum-ipv6', 'sub':
[{'state': False, 'name': 'tx-checksum-ipv4', 'sub': []}]}]
},
new_mode]
objects.NIC.update_offloading_modes(node.interfaces[0], new_modes,
keep_states=True)
old_modes.append(new_mode)
# States for old offloading modes should be preserved
self.assertListEqual(node.interfaces[0].offloading_modes,
old_modes)
class TestIPAddrObject(BaseTestCase):

View File

@ -32,19 +32,6 @@ INTERFACES = {
"name": {"type": "string"},
"driver": base_types.NULLABLE_STRING,
"bus_info": base_types.NULLABLE_STRING,
"offloading_modes": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {"type": "string"},
"state": base_types.NULLABLE_BOOL,
"sub": {
"$ref": "#/items/properties/offloading_modes"
}
}
}
},
"pxe": {"type": "boolean"},
"attributes": {
"type": "object",

View File

@ -18,7 +18,6 @@ from oslo_serialization import jsonutils
import six
from nailgun.api.v1.validators.base import BasicValidator
from nailgun.api.v1.validators.json_schema import iface_schema
from nailgun import consts
from nailgun.db import db
from nailgun.db.sqlalchemy.models import Cluster
@ -27,6 +26,8 @@ from nailgun.db.sqlalchemy.models import NetworkGroup
from nailgun.db.sqlalchemy.models import Node
from nailgun.db.sqlalchemy.models import NodeGroup
from nailgun import errors
from nailgun.extensions.network_manager.validators.json_schema import \
interface
from nailgun.extensions.network_manager.validators.json_schema import \
network_template
from nailgun.extensions.network_manager.validators.json_schema import networks
@ -320,7 +321,7 @@ class NeutronNetworkConfigurationValidator(NetworkConfigurationValidator):
class NetAssignmentValidator(BasicValidator):
single_schema = iface_schema.INTERFACES
single_schema = interface.INTERFACES
@classmethod
def validate(cls, node):

View File

@ -344,7 +344,6 @@ class Node(NailgunObject):
# Add interfaces for node from 'meta'.
if new_node.meta and new_node.meta.get('interfaces'):
cls.update_interfaces(new_node)
cls.update_interfaces_offloading_modes(new_node)
# role cannot be assigned if cluster_id is not set
if new_node_cluster_id:
@ -637,9 +636,6 @@ class Node(NailgunObject):
instance.mac = data.pop("mac", None) or instance.mac
db().flush()
cls.update_interfaces(instance)
cls.update_interfaces_offloading_modes(
instance,
is_agent)
cluster_changed = False
add_to_cluster = False
@ -831,27 +827,6 @@ class Node(NailgunObject):
data.pop('status', None)
return cls.update(instance, data)
@classmethod
def update_interfaces_offloading_modes(cls, instance, keep_states=False):
"""Update information about offloading modes for node interfaces.
:param instance: Node object
:param keep_states: If True, information about available modes will be
updated, but states configured by user will not be overwritten.
"""
for interface in instance.meta["interfaces"]:
new_offloading_modes = interface.get('offloading_modes')
if new_offloading_modes:
NIC.update_offloading_modes(
cls.get_interface_by_mac_or_name(
instance,
interface['mac'],
interface['name']
),
new_offloading_modes,
keep_states
)
@classmethod
def update_roles(cls, instance, new_roles):
"""Update roles for Node instance.

View File

@ -26,7 +26,6 @@ from oslo_serialization import jsonutils
from nailgun import consts
from nailgun.db import db
from nailgun.db.sqlalchemy import models
from nailgun.extensions.network_manager.manager import NetworkManager
from nailgun import objects
from nailgun.orchestrator import stages
from nailgun.test import base
@ -462,28 +461,6 @@ class TestDeploymentAttributesSerialization70(
self.assertEqual(roles, dict(expected_roles))
def test_offloading_modes_serialize(self):
meta = self.env.default_metadata()
changed_offloading_modes = {}
for interface in meta['interfaces']:
changed_offloading_modes[interface['name']] = \
NetworkManager._get_modified_offloading_modes(
interface.get('offloading_modes'))
for node in self.serialized_for_astute:
interfaces = node['network_scheme']['interfaces']
for iface_name in interfaces:
ethtool_blk = interfaces[iface_name].get('ethtool', None)
self.assertIsNotNone(
ethtool_blk,
"There is no 'ethtool' block in deployment data")
offload_blk = ethtool_blk.get('offload', None)
self.assertIsNotNone(
offload_blk,
"There is no 'offload' block in deployment data")
self.assertDictEqual(offload_blk,
changed_offloading_modes[iface_name])
def test_network_metadata(self):
neutron_serializer = self.serializer.get_net_provider_serializer(
self.cluster_db)

View File

@ -74,7 +74,7 @@ class TestNodeInterfacesDbModels(BaseTestCase):
'ip_addr': '10.20.0.2',
'netmask': '255.255.255.0',
'state': 'test_state',
'interface_properties': {'test_property': 'test_value'},
'attributes': {'test_property': 'test_value'},
'parent_id': 1,
'driver': 'test_driver',
'bus_info': 'some_test_info'
@ -105,143 +105,18 @@ class TestNodeInterfacesDbModels(BaseTestCase):
}
]
changed_modes = [
{
'name': 'mode_1',
'state': True,
'sub': [
{
'name': 'sub_mode_1',
'state': None,
'sub': []
}
]
},
{
'name': 'mode_2',
'state': None,
'sub': [
{
'name': 'sub_mode_2',
'state': False,
'sub': []
}
]
}
]
expected_result = {
'mode_1': True,
'sub_mode_1': None,
'mode_2': None,
'sub_mode_2': False
}
deep_structure = [
{
'name': 'level_1',
'state': True,
'sub': [
{
'name': 'level_2',
'state': None,
'sub': [
{
'name': 'level_3',
'state': None,
'sub': [
{
'name': 'level_4',
'state': False,
'sub': []
}
]
}
]
}
]
}
]
expected_result_deep = {
'level_1': True,
'level_2': None,
'level_3': None,
'level_4': False
}
def test_offloading_modes_as_flat_dict(self):
self.assertDictEqual(
self.expected_result,
NodeNICInterface.offloading_modes_as_flat_dict(
self.changed_modes))
self.assertDictEqual(
self.expected_result_deep,
NodeNICInterface.offloading_modes_as_flat_dict(
self.deep_structure))
def test_update_offloading_modes_for_bond_interface(self):
different_modes = [
[{
'name': 'mode_for_nic1',
'state': None,
'sub': [
{
'name': 'sub_mode_for_nic1',
'state': None,
'sub': []
}
]
}],
[{
'name': 'mode_for_nic2',
'state': None,
'sub': []
}],
]
nics = []
for i in range(2):
nic_data = copy.deepcopy(self.sample_nic_interface_data)
nic_data['offloading_modes'] = \
self.unchanged_modes + different_modes[i]
nics.append(NodeNICInterface(**nic_data))
sample_bond_data = {
'node_id': 1,
'name': 'test_bond_interface',
'mode': 'active-backup',
'bond_properties': {'test_property': 'test_value'}
}
bond = NodeBondInterface(**sample_bond_data)
bond.slaves = nics
bond_modes = bond.offloading_modes
self.assertListEqual(self.unchanged_modes, bond_modes)
bond.offloading_modes = self.changed_modes
bond_modes = bond.offloading_modes
self.assertListEqual(self.changed_modes, bond_modes)
for i in range(2):
self.assertListEqual(self.changed_modes + different_modes[i],
nics[i].offloading_modes)
def test_interface_properties_str_type_failure(self):
def test_interface_attributes_str_type_failure(self):
nic_data = copy.deepcopy(self.sample_nic_interface_data)
nic_data['interface_properties'] = jsonutils.dumps(
nic_data['interface_properties']) # str type cause ValueError
nic_data['attributes'] = jsonutils.dumps(
nic_data['attributes']) # str type cause ValueError
self.assertRaises(ValueError, NodeNICInterface, **nic_data)
def test_bond_properties_str_type_failure(self):
def test_bond_attributes_str_type_failure(self):
sample_bond_data = {
'node_id': 1,
'name': 'test_bond_interface',
'mode': 'active-backup',
'bond_properties': jsonutils.dumps(
'attributes': jsonutils.dumps(
{'test_property': 'test_value'}) # str type cause ValueError
}
self.assertRaises(ValueError, NodeBondInterface, **sample_bond_data)