Merge "Change pickup_hosts() for affinity=True/None"

This commit is contained in:
Zuul 2019-03-08 18:48:08 +00:00 committed by Gerrit Code Review
commit 7b16eb7e65
4 changed files with 374 additions and 82 deletions

View File

@ -12,6 +12,7 @@
# License for the specific language governing permissions and limitations
# under the License.
import collections
import datetime
import retrying
@ -60,6 +61,15 @@ class VirtualInstancePlugin(base.BasePlugin, nova.NovaClientWrapper):
self.monitor.register_healing_handler(self.heal_reservations)
self.placement_client = placement.BlazarPlacementClient()
# TODO(tetsuro) Remove this with a release note when all the support
# for True/None affinity is ready
def _check_affinity(self, affinity):
# TODO(masahito) the instance reservation plugin only supports
# anti-affinity rule in short-term goal.
if bool_from_string(affinity):
raise mgr_exceptions.MalformedParameter(
param='affinity (only affinity = False is supported)')
def filter_hosts_by_reservation(self, hosts, start_date, end_date,
excludes):
free = []
@ -75,7 +85,7 @@ class VirtualInstancePlugin(base.BasePlugin, nova.NovaClientWrapper):
if r['id'] not in excludes]
if reservations == []:
free.append({'host': host, 'reservations': None})
free.append({'host': host, 'reservations': []})
elif not [r for r in reservations
if r['resource_type'] == oshosts.RESOURCE_TYPE]:
non_free.append({'host': host, 'reservations': reservations})
@ -117,13 +127,37 @@ class VirtualInstancePlugin(base.BasePlugin, nova.NovaClientWrapper):
return max_vcpus, max_memory, max_disk
def get_hosts_list(self, host_info, cpus, memory, disk):
hosts_list = []
host = host_info['host']
reservations = host_info['reservations']
max_cpus, max_memory, max_disk = self.max_usages(host,
reservations)
used_cpus, used_memory, used_disk = (cpus, memory, disk)
while (max_cpus + used_cpus <= host['vcpus'] and
max_memory + used_memory <= host['memory_mb'] and
max_disk + used_disk <= host['local_gb']):
hosts_list.append(host)
used_cpus += cpus
used_memory += memory
used_disk += disk
return hosts_list
def query_available_hosts(self, cpus=None, memory=None, disk=None,
resource_properties=None,
start_date=None, end_date=None,
excludes_res=None):
"""Query hosts that are available for a reservation.
"""Returns a list of available hosts for a reservation.
Its return value is in the order of reserved hosts to free hosts now.
The list is in the order of reserved hosts to free hosts.
1. filter hosts that have a spec enough to accommodate the flavor
2. categorize hosts into hosts with and without allocation
at the reservation time frame
3. filter out hosts used by physical host reservation from
allocate_host
4. filter out hosts that can't accommodate the flavor at the
time frame because of other reservations
"""
flavor_definitions = [
'and',
@ -145,34 +179,28 @@ class VirtualInstancePlugin(base.BasePlugin, nova.NovaClientWrapper):
excludes_res)
available_hosts = []
for host_info in reserved_hosts:
host = host_info['host']
reservations = host_info['reservations']
max_cpus, max_memory, max_disk = self.max_usages(host,
reservations)
for host_info in (reserved_hosts + free_hosts):
hosts_list = self.get_hosts_list(host_info, cpus, memory, disk)
available_hosts.extend(hosts_list)
if not (max_cpus + cpus > host['vcpus'] or
max_memory + memory > host['memory_mb'] or
max_disk + disk > host['local_gb']):
available_hosts.append(host)
available_hosts.extend([h['host'] for h in free_hosts])
return available_hosts
def pickup_hosts(self, reservation_id, values):
"""Checks whether Blazar can accommodate the request.
"""Returns lists of host ids to add/remove.
This method filters and pick up hosts for this reservation
with following steps.
This function picks up available hosts, calculates the difference from
old reservations and returns a dict of a list of host ids to add
and remove keyed by "added" or "removed".
1. filter hosts that have a spec enough to accommodate the flavor
2. categorize hosts allocated_hosts and not_allocated_hosts
at the reservation time frame
3. filter out hosts used by physical host reservation from
allocate_host
4. filter out hosts that can't accommodate the flavor at the
time frame because of others reservations
Note that the lists allow duplicated host ids for `affinity=True`
cases.
:raises: NotEnoughHostsAvailable exception if there are not enough
hosts available for the request
"""
req_amount = values['amount']
affinity = bool_from_string(values['affinity'], default=None)
query_params = {
'cpus': values['vcpus'],
'memory': values['memory_mb'],
@ -182,32 +210,69 @@ class VirtualInstancePlugin(base.BasePlugin, nova.NovaClientWrapper):
'end_date': values['end_date']
}
# add the specific query param for reservation update
old_allocs = db_api.host_allocation_get_all_by_values(
reservation_id=reservation_id)
if old_allocs:
# This is a path for *update* reservation. Add the specific
# query param not to consider resources reserved by existing
# reservations to update
query_params['excludes_res'] = [reservation_id]
new_hosts = self.query_available_hosts(**query_params)
old_host_ids = {h['compute_host_id'] for h in old_allocs}
candidate_ids = {h['id'] for h in new_hosts}
old_host_id_list = [h['compute_host_id'] for h in old_allocs]
candidate_id_list = [h['id'] for h in new_hosts]
kept_host_ids = old_host_ids & candidate_ids
removed_host_ids = old_host_ids - candidate_ids
extra_host_ids = candidate_ids - old_host_ids
added_host_ids = set([])
# Build `new_host_id_list`. Note that we'd like to pick up hosts in
# the following order of priority:
# 1. hosts reserved by the reservation to update
# 2. hosts with reservations followed by hosts without reservations
# Note that the `candidate_id_list` has already been ordered
# satisfying the second requirement.
if affinity:
host_id_map = collections.Counter(candidate_id_list)
available = {k for k, v in host_id_map.items() if v >= req_amount}
if not available:
raise mgr_exceptions.NotEnoughHostsAvailable()
new_host_ids = set(old_host_id_list) & available
if new_host_ids:
# (priority 1) This is a path for update reservation. We pick
# up a host from hosts reserved by the reservation to update.
new_host_id = new_host_ids.pop()
else:
# (priority 2) This is a path both for update and for new
# reservation. We pick up hosts with some other reservations
# if possible and otherwise pick up hosts without any
# reservation. We can do so by considering the order of the
# `candidate_id_list`.
for host_id in candidate_id_list:
if host_id in available:
new_host_id = host_id
break
new_host_id_list = [new_host_id] * req_amount
else:
# Hosts that can accommodate but don't satisfy priority 1
_, possible_host_list = plugins_utils.list_difference(
old_host_id_list, candidate_id_list)
# Hosts that satisfy priority 1
new_host_id_list, _ = plugins_utils.list_difference(
candidate_id_list, possible_host_list)
if affinity is False:
# Eliminate the duplication
new_host_id_list = list(set(new_host_id_list))
for host_id in possible_host_list:
if (affinity is False) and (host_id in new_host_id_list):
# Eliminate the duplication
continue
new_host_id_list.append(host_id)
if len(new_host_id_list) < req_amount:
raise mgr_exceptions.NotEnoughHostsAvailable()
while len(new_host_id_list) > req_amount:
new_host_id_list.pop()
if len(kept_host_ids) > values['amount']:
extra = len(kept_host_ids) - values['amount']
for i in range(extra):
removed_host_ids.add(kept_host_ids.pop())
elif len(kept_host_ids) < values['amount']:
less = values['amount'] - len(kept_host_ids)
ordered_extra_host_ids = [h['id'] for h in new_hosts
if h['id'] in extra_host_ids]
for i in range(min(less, len(extra_host_ids))):
added_host_ids.add(ordered_extra_host_ids[i])
# Calculate the difference from the existing reserved host
removed_host_ids, added_host_ids = plugins_utils.list_difference(
old_host_id_list, new_host_id_list)
return {'added': added_host_ids, 'removed': removed_host_ids}
@ -328,26 +393,17 @@ class VirtualInstancePlugin(base.BasePlugin, nova.NovaClientWrapper):
def reserve_resource(self, reservation_id, values):
self.validate_reservation_param(values)
# TODO(masahito) the instance reservation plugin only supports
# anti-affinity rule in short-term goal.
if bool_from_string(values['affinity']):
raise mgr_exceptions.MalformedParameter(
param='affinity (only affinity = False is supported)')
self._check_affinity(values['affinity'])
hosts = self.pickup_hosts(reservation_id, values)
if len(hosts['added']) < values['amount']:
raise mgr_exceptions.HostNotFound("The reservation can't be "
"accommodate because of less "
"capacity.")
instance_reservation_val = {
'reservation_id': reservation_id,
'vcpus': values['vcpus'],
'memory_mb': values['memory_mb'],
'disk_gb': values['disk_gb'],
'amount': values['amount'],
'affinity': bool_from_string(values['affinity']),
'affinity': bool_from_string(values['affinity'], default=None),
'resource_properties': values['resource_properties']
}
instance_reservation = db_api.instance_reservation_create(
@ -396,11 +452,8 @@ class VirtualInstancePlugin(base.BasePlugin, nova.NovaClientWrapper):
- If an instance reservation has already started
- only amount is increasable.
"""
# TODO(masahito) the instance reservation plugin only supports
# anti-affinity rule in short-term goal.
if bool_from_string(new_values.get('affinity', None)):
raise mgr_exceptions.MalformedParameter(
param='affinity (only affinity = False is supported)')
affinity = new_values.get('affinity', None)
self._check_affinity(affinity)
reservation = db_api.reservation_get(reservation_id)
lease = db_api.lease_get(reservation['lease_id'])
@ -422,6 +475,10 @@ class VirtualInstancePlugin(base.BasePlugin, nova.NovaClientWrapper):
msg = "An error reservation doesn't accept an updating request."
raise mgr_exceptions.InvalidStateUpdate(msg)
if new_values.get('affinity', None):
new_values['affinity'] = bool_from_string(new_values['affinity'],
default=None)
for key in updatable:
if key not in new_values:
new_values[key] = reservation[key]
@ -435,10 +492,6 @@ class VirtualInstancePlugin(base.BasePlugin, nova.NovaClientWrapper):
"active status.")
raise mgr_exceptions.CantUpdateParameter(err_msg)
if (new_values['amount'] - reservation['amount'] !=
(len(changed_hosts['added']) - len(changed_hosts['removed']))):
raise mgr_exceptions.NotEnoughHostsAvailable()
db_api.instance_reservation_update(
reservation['resource_id'],
{key: new_values[key] for key in updatable})

View File

@ -165,9 +165,9 @@ class TestVirtualInstancePlugin(tests.TestCase):
'get_reservations_by_host_id')
mock_get_reservations.side_effect = fake_get_reservation_by_host
free = [{'host': hosts_list[0], 'reservations': None},
{'host': hosts_list[1], 'reservations': None},
{'host': hosts_list[2], 'reservations': None}]
free = [{'host': hosts_list[0], 'reservations': []},
{'host': hosts_list[1], 'reservations': []},
{'host': hosts_list[2], 'reservations': []}]
non_free = []
plugin = instance_plugin.VirtualInstancePlugin()
@ -218,11 +218,12 @@ class TestVirtualInstancePlugin(tests.TestCase):
'memory_mb': 1024,
'disk_gb': 20,
'amount': 2,
'affinity': False,
'resource_properties': '',
'start_date': datetime.datetime(2030, 1, 1, 8, 00),
'end_date': datetime.datetime(2030, 1, 1, 12, 00)
}
expected = {'added': set(['host-2', 'host-3']), 'removed': set([])}
expected = {'added': ['host-2', 'host-3'], 'removed': []}
ret = plugin.pickup_hosts('reservation-id1', values)
self.assertEqual(expected, ret)
@ -260,11 +261,12 @@ class TestVirtualInstancePlugin(tests.TestCase):
'memory_mb': 1024,
'disk_gb': 20,
'amount': 2,
'affinity': False,
'resource_properties': '',
'start_date': datetime.datetime(2030, 1, 1, 8, 00),
'end_date': datetime.datetime(2030, 1, 1, 12, 00),
}
expected = {'added': set(['host-1', 'host-2']), 'removed': set([])}
expected = {'added': ['host-1', 'host-2'], 'removed': []}
ret = plugin.pickup_hosts('reservation-id1', values)
self.assertEqual(expected, ret)
@ -314,18 +316,123 @@ class TestVirtualInstancePlugin(tests.TestCase):
'memory_mb': 1024,
'disk_gb': 20,
'amount': 2,
'affinity': False,
'resource_properties': '',
'start_date': datetime.datetime(2030, 1, 1, 8, 00),
'end_date': datetime.datetime(2030, 1, 1, 12, 00)
}
expected = {'added': set(['host-1', 'host-3']), 'removed': set([])}
expected = {'added': ['host-1', 'host-3'], 'removed': []}
ret = plugin.pickup_hosts('reservation-id1', params)
self.assertEqual(expected, ret)
expected_query = ['vcpus >= 1', 'memory_mb >= 1024', 'local_gb >= 20']
mock_host_get_query.assert_called_once_with(expected_query)
def test_pickup_host_with_affinity(self):
def fake_get_reservation_by_host(host_id, start, end):
if host_id in ['host-1', 'host-3']:
return [
{'id': '1',
'resource_type': instances.RESOURCE_TYPE},
{'id': '2',
'resource_type': instances.RESOURCE_TYPE}
]
else:
return []
plugin = instance_plugin.VirtualInstancePlugin()
mock_host_allocation_get = self.patch(
db_api, 'host_allocation_get_all_by_values')
mock_host_allocation_get.return_value = []
mock_host_get_query = self.patch(db_api,
'reservable_host_get_all_by_queries')
hosts_list = [self.generate_host_info('host-1', 8, 8192, 1000),
self.generate_host_info('host-2', 2, 2048, 500),
self.generate_host_info('host-3', 2, 2048, 500)]
mock_host_get_query.return_value = hosts_list
mock_get_reservations = self.patch(db_utils,
'get_reservations_by_host_id')
mock_get_reservations.side_effect = fake_get_reservation_by_host
mock_max_usages = self.patch(plugin, 'max_usages')
mock_max_usages.return_value = (0, 0, 0)
mock_reservation_get = self.patch(db_api, 'reservation_get')
mock_reservation_get.return_value = {
'status': 'pending'
}
params = {
'vcpus': 2,
'memory_mb': 2048,
'disk_gb': 100,
'amount': 2,
'affinity': True,
'resource_properties': '',
'start_date': datetime.datetime(2030, 1, 1, 8, 00),
'end_date': datetime.datetime(2030, 1, 1, 12, 00)
}
expected = {'added': ['host-1', 'host-1'], 'removed': []}
ret = plugin.pickup_hosts('reservation-id1', params)
self.assertEqual(expected, ret)
expected_query = ['vcpus >= 2', 'memory_mb >= 2048', 'local_gb >= 100']
mock_host_get_query.assert_called_once_with(expected_query)
def test_pickup_host_with_no_affinity(self):
def fake_get_reservation_by_host(host_id, start, end):
return []
plugin = instance_plugin.VirtualInstancePlugin()
mock_host_allocation_get = self.patch(
db_api, 'host_allocation_get_all_by_values')
mock_host_allocation_get.return_value = []
mock_host_get_query = self.patch(db_api,
'reservable_host_get_all_by_queries')
hosts_list = [self.generate_host_info('host-1', 8, 8192, 1000),
self.generate_host_info('host-2', 2, 2048, 500),
self.generate_host_info('host-3', 2, 2048, 500)]
mock_host_get_query.return_value = hosts_list
mock_get_reservations = self.patch(db_utils,
'get_reservations_by_host_id')
mock_get_reservations.side_effect = fake_get_reservation_by_host
mock_max_usages = self.patch(plugin, 'max_usages')
mock_max_usages.return_value = (0, 0, 0)
mock_reservation_get = self.patch(db_api, 'reservation_get')
mock_reservation_get.return_value = {
'status': 'pending'
}
params = {
'vcpus': 4,
'memory_mb': 4096,
'disk_gb': 200,
'amount': 2,
'affinity': None,
'resource_properties': '',
'start_date': datetime.datetime(2030, 1, 1, 8, 00),
'end_date': datetime.datetime(2030, 1, 1, 12, 00)
}
expected = {'added': ['host-1', 'host-1'], 'removed': []}
ret = plugin.pickup_hosts('reservation-id1', params)
self.assertEqual(expected, ret)
expected_query = ['vcpus >= 4', 'memory_mb >= 4096', 'local_gb >= 200']
mock_host_get_query.assert_called_once_with(expected_query)
def test_pickup_host_from_less_hosts(self):
def fake_get_reservation_by_host(host_id, start, end):
if host_id in ['host-1', 'host-3']:
@ -360,6 +467,21 @@ class TestVirtualInstancePlugin(tests.TestCase):
db_api, 'host_allocation_get_all_by_values')
mock_host_allocation_get.return_value = []
old_reservation = {
'id': 'reservation-id1',
'status': 'pending',
'lease_id': 'lease-id1',
'resource_id': 'instance-reservation-id1',
'vcpus': 2, 'memory_mb': 1024, 'disk_gb': 100,
'amount': 2, 'affinity': False,
'resource_properties': ''}
mock_reservation_get = self.patch(db_api, 'reservation_get')
mock_reservation_get.return_value = old_reservation
mock_lease_get = self.patch(db_api, 'lease_get')
mock_lease_get.return_value = {'start_date': '2030-01-01 8:00',
'end_date': '2030-01-01 12:00'}
mock_max_usages = self.patch(plugin, 'max_usages')
mock_max_usages.return_value = (1, 1024, 100)
@ -368,14 +490,15 @@ class TestVirtualInstancePlugin(tests.TestCase):
'memory_mb': 1024,
'disk_gb': 20,
'amount': 2,
'affinity': False,
'resource_properties': '',
'start_date': datetime.datetime(2030, 1, 1, 8, 00),
'end_date': datetime.datetime(2030, 1, 1, 12, 00)
}
hosts = plugin.pickup_hosts('reservation-id1', values)
self.assertTrue((len(hosts['added']) - len(hosts['removed']))
< values['amount'])
self.assertRaises(mgr_exceptions.NotEnoughHostsAvailable,
plugin.update_reservation, 'reservation-id1',
values)
def test_max_usage_with_serial_reservation(self):
def fake_event_get(sort_key, sort_dir, filters):
@ -573,6 +696,32 @@ class TestVirtualInstancePlugin(tests.TestCase):
mock_create_reservation_class.assert_called_once_with(
'reservation-id1')
def test_query_available_hosts(self):
mock_host_get_query = self.patch(db_api,
'reservable_host_get_all_by_queries')
host1, host2, host3 = (self.generate_host_info(host_id, 4, 4096, 1000)
for host_id in ['host-1', 'host-2', 'host-3'])
hosts_list = [host1, host2, host3]
mock_host_get_query.return_value = hosts_list
get_reservations = self.patch(db_utils,
'get_reservations_by_host_id')
get_reservations.return_value = []
plugin = instance_plugin.VirtualInstancePlugin()
query_params = {
'cpus': 1, 'memory': 1024, 'disk': 10,
'resource_properties': '',
'start_date': datetime.datetime(2020, 7, 7, 18, 0),
'end_date': datetime.datetime(2020, 7, 7, 19, 0)
}
ret = plugin.query_available_hosts(**query_params)
expected = [host1] * 4 + [host2] * 4 + [host3] * 4
self.assertEqual(expected, ret)
def test_pickup_hosts_for_update(self):
reservation = {'id': 'reservation-id1', 'status': 'pending'}
plugin = instance_plugin.VirtualInstancePlugin()
@ -595,8 +744,8 @@ class TestVirtualInstancePlugin(tests.TestCase):
values = self.get_input_values(1, 1024, 10, 1, False,
'2020-07-01 10:00', '2020-07-01 11:00',
'lease-1', '')
expect = {'added': set([]),
'removed': set(['host-id1', 'host-id2', 'host-id3'])}
expect = {'added': [],
'removed': ['host-id1', 'host-id2', 'host-id3']}
ret = plugin.pickup_hosts(reservation['id'], values)
self.assertEqual(expect['added'], ret['added'])
self.assertEqual(2, len(ret['removed']))
@ -614,7 +763,7 @@ class TestVirtualInstancePlugin(tests.TestCase):
values = self.get_input_values(1, 1024, 10, 3, False,
'2020-07-01 10:00', '2020-07-01 11:00',
'lease-1', '["==", "key1", "value1"]')
expect = {'added': set(['host-id4']), 'removed': set(['host-id1'])}
expect = {'added': ['host-id4'], 'removed': ['host-id1']}
ret = plugin.pickup_hosts(reservation['id'], values)
self.assertEqual(expect['added'], ret['added'])
self.assertEqual(expect['removed'], ret['removed'])
@ -628,16 +777,15 @@ class TestVirtualInstancePlugin(tests.TestCase):
mock_query_available.assert_called_with(**query_params)
# case: new amount is greater than old amount
host_ids = ('host-id1', 'host-id2', 'host-id3', 'host-id4')
mock_query_available.return_value = [
self.generate_host_info('host-id1', 2, 2024, 1000),
self.generate_host_info('host-id2', 2, 2024, 1000),
self.generate_host_info('host-id3', 2, 2024, 1000),
self.generate_host_info('host-id4', 2, 2024, 1000)]
self.generate_host_info(host_id, 2, 2024, 1000)
for host_id in host_ids]
values = self.get_input_values(1, 1024, 10, 4, False,
'2020-07-01 10:00', '2020-07-01 11:00',
'lease-1', '')
expect = {'added': set(['host-id4']), 'removed': set([])}
expect = {'added': ['host-id4'], 'removed': []}
ret = plugin.pickup_hosts(reservation['id'], values)
self.assertEqual(expect['added'], ret['added'])
self.assertEqual(expect['removed'], ret['removed'])
@ -650,6 +798,40 @@ class TestVirtualInstancePlugin(tests.TestCase):
}
mock_query_available.assert_called_with(**query_params)
# case: affinity is changed to True
mock_query_available.return_value = [
self.generate_host_info(host_id, 8, 8192, 1000)
for host_id in host_ids * 8]
values = self.get_input_values(1, 1024, 10, 4, True,
'2020-07-01 10:00', '2020-07-01 11:00',
'lease-1', '')
ret = plugin.pickup_hosts(reservation['id'], values)
# We don't care which host id (1-3) is picked up
# Just make sure the same host is returned three times in "added"
added = ret['added']
self.assertEqual(3, len(added))
self.assertEqual(1, len(set(added)))
self.assertIn(added[0], ('host-id1', 'host-id2', 'host-id3'))
# and make sure the other two hosts are removed
removed = ret['removed']
self.assertEqual(2, len(removed))
self.assertEqual(2, len(set(removed)))
expect_removed = set(host_ids) - set(added)
for host_id in removed:
self.assertIn(host_id, expect_removed)
query_params = {
'cpus': 1, 'memory': 1024, 'disk': 10,
'resource_properties': '',
'start_date': '2020-07-01 10:00',
'end_date': '2020-07-01 11:00',
'excludes_res': ['reservation-id1']
}
mock_query_available.assert_called_with(**query_params)
def test_update_resources(self):
reservation = {
'id': 'reservation-id1',
@ -790,11 +972,21 @@ class TestVirtualInstancePlugin(tests.TestCase):
mock_lease_get.return_value = {'start_date': '2020-07-07 18:00',
'end_date': '2020-07-07 19:00'}
mock_pickup_hosts = self.patch(plugin, 'pickup_hosts')
mock_pickup_hosts.return_value = {
'added': set(), 'removed': set(['host-id2'])}
# Mock that we have at least two hosts for (2 vcpus + 100 disk_gb),
# but we have only one for (4 vcpus and 200 disk_gb)
mock_alloc_get = self.patch(db_api,
'host_allocation_get_all_by_values')
mock_alloc_get.return_value = [{'compute_host_id': 'host-id1'},
{'compute_host_id': 'host-id2'}]
new_values = {'vcpus': 4, 'disk_gb': 200}
mock_query_available = self.patch(plugin, 'query_available_hosts')
mock_query_available.return_value = [
self.generate_host_info('host-id1', 4, 2048, 1000)]
new_values = {'vcpus': 4, 'disk_gb': 200,
'start_date': datetime.datetime(2020, 7, 7, 18, 0),
'end_date': datetime.datetime(2020, 7, 7, 19, 0),
'id': '00ee4f12-77c8-44d5-abca-06a543210a50'}
self.assertRaises(mgr_exceptions.NotEnoughHostsAvailable,
plugin.update_reservation, 'reservation-id1',
new_values)

View File

@ -77,3 +77,25 @@ class TestPluginsUtils(tests.TestCase):
self.assertRaises(
manager_exceptions.MalformedRequirements,
plugins_utils.convert_requirements, 'something')
def test_list_difference(self):
old_list = [1, 1, 2, 3, 4, 4, 4, 5]
new_list = [1, 2, 3, 4, 7, 8, 8]
result = plugins_utils.list_difference(old_list, new_list)
to_remove = [1, 4, 4, 5]
to_add = [7, 8, 8]
self.assertEqual((to_remove, to_add), result)
def test_list_difference_empty(self):
old_list = []
new_list = [1, 2, 2, 2, 3, 4, 7, 8, 8]
result = plugins_utils.list_difference(old_list, new_list)
to_remove = []
to_add = [1, 2, 2, 2, 3, 4, 7, 8, 8]
self.assertEqual((to_remove, to_add), result)

View File

@ -13,6 +13,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import copy
from oslo_serialization import jsonutils
import six
@ -76,3 +77,27 @@ def _requirements_with_and_keyword(requirements):
isinstance(requirements[0], six.string_types) and
requirements[0] == 'and' and
all(convert_requirements(x) for x in requirements[1:]))
def list_difference(list1, list2):
"""Return a tuple that shows the differences between lists.
Example:
list1 = [1, 1, 2, 3, 4, 4, 4, 5] # old list
list2 = [1, 2, 3, 4, 7, 8, 8] # new list
-> ([1, 4, 4, 5], [7, 8, 8]) # (to_remove, to_add)
"""
def list_subtract(list_a, list_b):
result = copy.copy(list_a)
for value in list_b:
if value in result:
try:
result.remove(value)
except ValueError:
pass
return result
result1 = list_subtract(list1, list2)
result2 = list_subtract(list2, list1)
return result1, result2