Perform port update if security group changed

If only the security groups for a network changed in a profile, do not
delete and recreate the network during a profile update.  Instead apply
the security group change by an update to the existing port.

Closes-Bug: #1905490
Change-Id: I43f1181c1592144d49784ba0d3c51c01bf2f1060
This commit is contained in:
Duc Truong 2020-11-24 21:51:18 +00:00
parent 028af9be62
commit 6dff71e13f
2 changed files with 173 additions and 5 deletions

View File

@ -1332,6 +1332,43 @@ class ServerProfile(base.Profile):
obj.data['internal_ports'] = internal_ports
node_obj.Node.update(self.context, obj.id, {'data': obj.data})
def _nw_compare(self, n1, n2, property):
return n1.get(property, None) == n2.get(property, None)
def _update_network_update_port(self, obj, networks):
"""Update existing port in network from the node.
Currently only update to security group is supported.
:param obj: The node object to operate.
:param networks: A list networks that contain updated security groups.
:returns: ``None``
:raises: ``EResourceUpdate``
"""
nc = self.network(obj)
internal_ports = obj.data.get('internal_ports', [])
# process each network to be updated
for n in networks:
# verify network properties and resolve names into ids
net = self._validate_network(obj, n, 'update')
# find existing port that matches network
candidate_ports = self._find_port_by_net_spec(
obj, net, internal_ports)
port = candidate_ports[0]
try:
# set updated security groups for port
port_attr = {
'security_groups': net.get(self.PORT_SECURITY_GROUPS, []),
}
LOG.debug("Setting security groups %s for port %s",
port_attr, port['id'])
nc.port_update(port['id'], **port_attr)
except exc.InternalError as ex:
raise exc.EResourceUpdate(type='server', id=obj.physical_id,
message=str(ex))
def _update_network(self, obj, new_profile):
"""Updating server network interfaces.
@ -1344,10 +1381,36 @@ class ServerProfile(base.Profile):
networks_current = self.properties[self.NETWORKS]
networks_create = new_profile.properties[self.NETWORKS]
networks_delete = copy.deepcopy(networks_current)
for network in networks_current:
if network in networks_create:
networks_create.remove(network)
networks_delete.remove(network)
networks_update = []
for nw in networks_current:
if nw in networks_create:
# network already exist. no need to create or delete it.
LOG.debug("Network %s already exists, skip create/delete", nw)
networks_create.remove(nw)
networks_delete.remove(nw)
# find networks for which only security group changed
for nw in networks_current:
# networks to be created with only sg changes
sg_create_nw = [n for n in networks_create
if (self._nw_compare(n, nw, 'network') and
self._nw_compare(n, nw, 'port') and
self._nw_compare(n, nw, 'fixed_ip') and
self._nw_compare(n, nw, 'floating_network') and
self._nw_compare(n, nw, 'floating_ip'))]
for n in sg_create_nw:
# don't create networks with only security group changes
LOG.debug("Network %s only has security group changes, "
"don't create/delete it. Only update it.", n)
networks_create.remove(n)
networks_update.append(n)
if nw in networks_delete:
networks_delete.remove(nw)
# update network
if networks_update:
self._update_network_update_port(obj, networks_update)
# Detach some existing interfaces
if networks_delete:

View File

@ -1195,9 +1195,91 @@ class TestNovaServerUpdate(base.SenlinTestCase):
str(ex))
cc.server_interface_delete.assert_called_once_with('port1', 'NOVA_ID')
def test_update_port(self):
cc = mock.Mock()
cc.server_get.return_value = mock.Mock(status=consts.VS_ACTIVE)
nc = mock.Mock()
net1 = mock.Mock(id='net1')
nc.network_get.return_value = net1
nc.port_find.return_value = mock.Mock(id='port3', status='DOWN')
profile = server.ServerProfile('t', self.spec)
profile.stop_timeout = 232
profile._computeclient = cc
profile._networkclient = nc
validation_results = [
{'network': 'net1_id', 'fixed_ip': 'ip1',
'security_groups': ['sg1_id']},
{'network': 'net1_id', 'fixed_ip': 'ip1',
'security_groups': ['sg1_id', 'sg2_id']},
{'network': 'net1_id', 'fixed_ip': 'ip1'}
]
mock_validate = self.patchobject(profile, '_validate_network',
side_effect=validation_results)
candidate_ports = [
[{'id': 'port1_id', 'network_id': 'net1_id',
'fixed_ips': [{'ip_address': 'ip1'}]}],
[{'id': 'port2_id', 'network_id': 'net1_id',
'fixed_ips': [{'ip_address': 'ip1'}]}],
[{'id': 'port3_id', 'network_id': 'net1_id',
'fixed_ips': [{'ip_address': 'ip1'}]}]
]
self.patchobject(profile, '_find_port_by_net_spec',
side_effect=candidate_ports)
obj = mock.Mock(physical_id='NOVA_ID', data={'internal_ports': [
{'id': 'port1', 'network_id': 'net1',
'fixed_ips': [{'ip_address': 'ip1'}]},
{'id': 'port2', 'network_id': 'net1', 'remove': True,
'fixed_ips': [{'ip_address': 'ip-random2'}],
'security_groups': ['default']},
{'id': 'port3', 'network_id': 'net1', 'remove': True,
'fixed_ips': [{'ip_address': 'ip3'}],
'security_groups': ['default']}
]})
networks = [
{'network': 'net1', 'port': None, 'fixed_ip': 'ip1',
'security_groups': ['default'], 'floating_network': None,
'floating_ip': None},
{'network': 'net1', 'port': None, 'fixed_ip': 'ip1',
'security_groups': ['default', 'blah'], 'floating_network': None,
'floating_ip': None},
{'network': 'net1', 'port': None, 'fixed_ip': 'ip1',
'security_groups': None, 'floating_network': None,
'floating_ip': None},
]
res = profile._update_network_update_port(obj, networks)
self.assertIsNone(res)
validation_calls = [
mock.call(obj,
{'network': 'net1', 'port': None, 'fixed_ip': 'ip1',
'security_groups': ['default'],
'floating_network': None, 'floating_ip': None},
'update'),
mock.call(obj,
{'network': 'net1', 'port': None, 'fixed_ip': 'ip1',
'security_groups': ['default', 'blah'],
'floating_network': None, 'floating_ip': None},
'update'),
mock.call(obj,
{'network': 'net1', 'port': None, 'fixed_ip': 'ip1',
'security_groups': None, 'floating_network': None,
'floating_ip': None},
'update')
]
mock_validate.assert_has_calls(validation_calls)
update_calls = [
mock.call('port1_id', security_groups=['sg1_id']),
mock.call('port2_id', security_groups=['sg1_id', 'sg2_id']),
mock.call('port3_id', security_groups=[]),
]
nc.port_update.assert_has_calls(update_calls)
@mock.patch.object(server.ServerProfile, '_update_network_update_port')
@mock.patch.object(server.ServerProfile, '_update_network_remove_port')
@mock.patch.object(server.ServerProfile, '_update_network_add_port')
def test_update_network(self, mock_create, mock_delete):
def test_update_network(self, mock_create, mock_delete, mock_update):
obj = mock.Mock(physical_id='FAKE_ID')
old_spec = copy.deepcopy(self.spec)
@ -1205,6 +1287,11 @@ class TestNovaServerUpdate(base.SenlinTestCase):
{'network': 'net1', 'fixed_ip': 'ip1'},
{'network': 'net1'},
{'port': 'port3'},
# sg only changes:
{'network': 'net3', 'fixed_ip': 'ip1'},
{'network': 'net4', 'fixed_ip': 'ip1',
'security_groups': ['blah']},
{'port': 'port5', 'security_groups': ['default']},
]
profile = server.ServerProfile('t', old_spec)
new_spec = copy.deepcopy(self.spec)
@ -1212,6 +1299,12 @@ class TestNovaServerUpdate(base.SenlinTestCase):
{'network': 'net1', 'fixed_ip': 'ip2'},
{'network': 'net2'},
{'port': 'port4'},
# sg only changes:
{'network': 'net3', 'fixed_ip': 'ip1',
'security_groups': ['default']},
{'network': 'net4', 'fixed_ip': 'ip1',
'security_groups': ['default']},
{'port': 'port5', 'security_groups': ['default', 'blah']},
]
new_profile = server.ServerProfile('t1', new_spec)
@ -1239,6 +1332,18 @@ class TestNovaServerUpdate(base.SenlinTestCase):
'floating_ip': None, 'port': 'port3', 'security_groups': None}
]
mock_delete.assert_called_once_with(obj, networks_delete)
networks_update = [
{'network': 'net3', 'port': None, 'fixed_ip': 'ip1',
'security_groups': ['default'], 'floating_network': None,
'floating_ip': None},
{'network': 'net4', 'port': None, 'fixed_ip': 'ip1',
'security_groups': ['default'], 'floating_network': None,
'floating_ip': None},
{'network': None, 'port': 'port5', 'fixed_ip': None,
'security_groups': ['default', 'blah'], 'floating_network': None,
'floating_ip': None}
]
mock_update.assert_called_once_with(obj, networks_update)
@mock.patch.object(server.ServerProfile, '_update_password')
@mock.patch.object(server.ServerProfile, '_check_password')