def test_resource_merge_simple_dict(): """ Test simple dictionary merging """ test_data = [ # desired, existing, merged ({}, { 'a': 1 }, { 'a': 1 }), ({ 'a': 1 }, {}, { 'a': 1 }), ({ 'a': 1 }, { 'a': 2 }, { 'a': 1 }), ({ 'a': 1 }, { 'b': 2 }, { 'a': 1, 'b': 2 }), ({ 'a': 1, 'b': 3 }, { 'b': 2 }, { 'a': 1, 'b': 3 }), ({ 'a': 1, 'b': 2 }, { 'c': 3, 'd': 4 }, { 'a': 1, 'b': 2, 'c': 3, 'd': 4 }) ] for test in test_data: desired = test[0] existing = test[1] expected = test[2] assert merge(existing, desired) == expected
def test_resource_merge_list_of_named_dict(): """ Test merge lists of named dictionary objects This is unique for Big-IP resources that are lists of named objects (the resources must have a unique property 'name'). """ test_data = [ # desired, existing, merged ( [], [], [] ), ( [], [{'name': 'resource-a', 'value': 1}], [{'name': 'resource-a', 'value': 1}] ), ( [{'name': 'resource-a', 'value': 1}], [], [{'name': 'resource-a', 'value': 1}]), ( [{'name': 'resource-a', 'value': 1}], [{'name': 'resource-a', 'value': 3}], [{'name': 'resource-a', 'value': 1}] ), ( [{'name': 'resource-a', 'value1': 1, 'value2': 2}], [{'name': 'resource-a', 'value2': 0, 'value3': 3}], [{'name': 'resource-a', 'value1': 1, 'value2': 2}] ), ( [ {'name': 'resource-a', 'value1': 1, 'value2': 2}, {'name': 'resource-b', 'valueB': 'b'} ], [ {'name': 'resource-a', 'value2': 0, 'value3': 3}, {'name': 'resource-c', 'valueC': 'c'}, {'name': 'resource-b', 'valueB': 'b'} ], [ {'name': 'resource-a', 'value1': 1, 'value2': 2}, {'name': 'resource-b', 'valueB': 'b'}, {'name': 'resource-c', 'valueC': 'c'} ] ) ] for test in test_data: desired = test[0] existing = test[1] expected = test[2] assert merge(existing, desired) == expected
def test_resource_merge_scalars(): """ Test simple scalar merging (replacing) """ test_data = [ # desired, existing, merged (4, 3, 4), ('a', 'b', 'a'), (True, False, True) ] for test in test_data: desired = test[0] existing = test[1] expected = test[2] assert merge(existing, desired) == expected
def test_resource_merge_scalars(): """ Test simple scalar merging (replacing) """ test_data = [ # desired, existing, merged (4, 3, 4), ('a', 'b', 'a'), (True, False, True) ] for test in test_data: desired = test[0] existing = test[1] expected = test[2] assert merge(existing, desired) == expected
def test_resource_merge_simple_dict(): """ Test simple dictionary merging """ test_data = [ # desired, existing, merged ({}, {'a': 1}, {'a': 1}), ({'a': 1}, {}, {'a': 1}), ({'a': 1}, {'a': 2}, {'a': 1}), ({'a': 1}, {'b': 2}, {'a': 1, 'b': 2}), ({'a': 1, 'b': 3}, {'b': 2}, {'a': 1, 'b': 3}), ({'a': 1, 'b': 2}, {'c': 3, 'd': 4}, {'a': 1, 'b': 2, 'c': 3, 'd': 4}) ] for test in test_data: desired = test[0] existing = test[1] expected = test[2] assert merge(existing, desired) == expected
def test_resource_merge_simple_arrays(): """ Test simple list merging (replacing) """ test_data = [ # desired, existing, merged ([], [], []), ([], [1], [1]), ([1], [], [1]), ([1], [2], [1, 2]), ([2], [1], [2, 1]), ([1, 2], [1], [1, 2]), ([1], [1, 2], [1, 2]), ([1, 3], [1, 2], [1, 3, 2]), (['apple', 'orange'], ['apple', 'pear'], ['apple', 'orange', 'pear']) ] for test in test_data: desired = test[0] existing = test[1] expected = test[2] assert merge(existing, desired) == expected
def test_resource_merge_simple_arrays(): """ Test simple list merging (replacing) """ test_data = [ # desired, existing, merged ([], [], []), ([], [1], [1]), ([1], [], [1]), ([1], [2], [1, 2]), ([2], [1], [2, 1]), ([1, 2], [1], [1, 2]), ([1], [1, 2], [1, 2]), ([1, 3], [1, 2], [1, 3, 2]), (['apple', 'orange'], ['apple', 'pear'], ['apple', 'orange', 'pear']) ] for test in test_data: desired = test[0] existing = test[1] expected = test[2] assert merge(existing, desired) == expected
def merge(self, desired_data): u"""Merge in properties from controller instead of replacing""" # 1. stop processing if no merging is needed prev_updates = self._retrieve_whitelist_updates() if desired_data == {} and prev_updates is None: # nothing needs to be done (cccl has not and will not make changes # to this resource) return False prev_data = copy.deepcopy(self._data) # 2. remove old CCCL updates pospatch.convert_to_positional_patch(self._data, prev_updates) try: # This actually backs out the previous updates # to get back to the original F5 resource state. if prev_updates: self._data = prev_updates.apply(self._data) except Exception as e: # pylint: disable=broad-except LOGGER.warning("Failed removing updates to resource %s: %s", self.name, e) # 3. perform new merge with latest CCCL specific config original_resource = copy.deepcopy(self) self._data = merge(self._data, desired_data) self.post_merge_adjustments() # 4. compute the new updates so we can back out next go-around cur_updates = jsonpatch.make_patch(self._data, original_resource.data) # 5. remove move / adjust indexes per resource specific pospatch.convert_from_positional_patch(self._data, cur_updates) changed = self._data != prev_data # 6. update metadata with new CCCL updates self._save_whitelist_updates(cur_updates) # 7. determine if there was a needed change return changed
def merge(self, desired_data): u"""Merge in properties from controller instead of replacing""" # 1. stop processing if no merging is needed prev_updates = self._retrieve_whitelist_updates() if desired_data == {} and prev_updates is None: # nothing needs to be done (cccl has not and will not make changes # to this resource) return False prev_data = copy.deepcopy(self._data) # 2. remove old CCCL updates pospatch.convert_to_positional_patch(self._data, prev_updates) try: # This actually backs out the previous updates # to get back to the original F5 resource state. if prev_updates: self._data = prev_updates.apply(self._data) except Exception as e: # pylint: disable=broad-except LOGGER.warning("Failed removing updates to resource %s: %s", self.name, e) # 3. perform new merge with latest CCCL specific config original_resource = copy.deepcopy(self) self._data = merge(self._data, desired_data) self.post_merge_adjustments() # 4. compute the new updates so we can back out next go-around cur_updates = jsonpatch.make_patch(self._data, original_resource.data) # 5. remove move / adjust indexes per resource specific pospatch.convert_from_positional_patch(self._data, cur_updates) changed = self._data != prev_data # 6. update metadata with new CCCL updates self._save_whitelist_updates(cur_updates) # 7. determine if there was a needed change return changed
def test_resource_merge_sample_resource(): """ Test actual Big-IP resource """ desired = { 'destination': '/test/172.16.3.59%0:80', 'name': 'ingress_172-16-3-59_80', 'rules': [], 'vlansDisabled': True, 'enabled': True, 'sourceAddressTranslation': { 'type': 'automap' }, 'partition': 'test', 'source': '0.0.0.0%0/0', 'profiles': [{ 'partition': 'Common', 'name': 'http', 'context': 'all' }, { 'partition': 'Common', 'name': 'tcp', 'context': 'all' }], 'connectionLimit': 0, 'ipProtocol': 'tcp', 'vlans': [], 'policies': [{ 'partition': 'test', 'name': 'ingress_172-16-3-59_80' }] } existing = { 'destination': '/test/172.16.3.59%0:80', 'name': 'ingress_172-16-3-59_80', 'rules': [], 'vlansDisabled': False, # should change 'enabled': True, 'sourceAddressTranslation': { 'type': 'snat' }, # should change 'partition': 'test', 'source': '0.0.0.0%0/0', 'profiles': [ # html profile should be kept, but added after CCCL entries { 'partition': 'Common', 'name': 'html', 'context': 'all' }, { 'partition': 'Common', 'name': 'http', 'context': 'all' }, { 'partition': 'Common', 'name': 'tcp', 'context': 'all' } ], 'connectionLimit': 1, # should change 'ipProtocol': 'tcp', 'vlans': [{ 'name': 'vlan', 'a': '1', 'b': '2' } # should be kept ], 'policies': [] # will be added to } expected = { 'destination': '/test/172.16.3.59%0:80', 'name': 'ingress_172-16-3-59_80', 'rules': [], 'vlansDisabled': True, 'enabled': True, 'sourceAddressTranslation': { 'type': 'automap' }, 'partition': 'test', 'source': '0.0.0.0%0/0', 'profiles': [{ 'partition': 'Common', 'name': 'http', 'context': 'all' }, { 'partition': 'Common', 'name': 'tcp', 'context': 'all' }, { 'partition': 'Common', 'name': 'html', 'context': 'all' }], 'connectionLimit': 0, 'ipProtocol': 'tcp', 'vlans': [{ 'name': 'vlan', 'a': '1', 'b': '2' }], 'policies': [{ 'partition': 'test', 'name': 'ingress_172-16-3-59_80' }] } assert merge(existing, desired) == expected
def test_resource_merge_list_of_named_dict(): """ Test merge lists of named dictionary objects This is unique for Big-IP resources that are lists of named objects (the resources must have a unique property 'name'). """ test_data = [ # desired, existing, merged ([], [], []), ([], [{ 'name': 'resource-a', 'value': 1 }], [{ 'name': 'resource-a', 'value': 1 }]), ([{ 'name': 'resource-a', 'value': 1 }], [], [{ 'name': 'resource-a', 'value': 1 }]), ([{ 'name': 'resource-a', 'value': 1 }], [{ 'name': 'resource-a', 'value': 3 }], [{ 'name': 'resource-a', 'value': 1 }]), ([{ 'name': 'resource-a', 'value1': 1, 'value2': 2 }], [{ 'name': 'resource-a', 'value2': 0, 'value3': 3 }], [{ 'name': 'resource-a', 'value1': 1, 'value2': 2 }]), ([{ 'name': 'resource-a', 'value1': 1, 'value2': 2 }, { 'name': 'resource-b', 'valueB': 'b' }], [{ 'name': 'resource-a', 'value2': 0, 'value3': 3 }, { 'name': 'resource-c', 'valueC': 'c' }, { 'name': 'resource-b', 'valueB': 'b' }], [{ 'name': 'resource-a', 'value1': 1, 'value2': 2 }, { 'name': 'resource-b', 'valueB': 'b' }, { 'name': 'resource-c', 'valueC': 'c' }]) ] for test in test_data: desired = test[0] existing = test[1] expected = test[2] assert merge(existing, desired) == expected
def test_resource_merge_sample_resource(): """ Test actual Big-IP resource """ desired = { 'destination': '/test/172.16.3.59%0:80', 'name': 'ingress_172-16-3-59_80', 'rules': [], 'vlansDisabled': True, 'enabled': True, 'sourceAddressTranslation': {'type': 'automap'}, 'partition': 'test', 'source': '0.0.0.0%0/0', 'profiles': [ {'partition': 'Common', 'name': 'http', 'context': 'all'}, {'partition': 'Common', 'name': 'tcp', 'context': 'all'} ], 'connectionLimit': 0, 'ipProtocol': 'tcp', 'vlans': [], 'policies': [ {'partition': 'test', 'name': 'ingress_172-16-3-59_80'} ] } existing = { 'destination': '/test/172.16.3.59%0:80', 'name': 'ingress_172-16-3-59_80', 'rules': [], 'vlansDisabled': False, # should change 'enabled': True, 'sourceAddressTranslation': {'type': 'snat'}, # should change 'partition': 'test', 'source': '0.0.0.0%0/0', 'profiles': [ # html profile should be kept, but added after CCCL entries {'partition': 'Common', 'name': 'html', 'context': 'all'}, {'partition': 'Common', 'name': 'http', 'context': 'all'}, {'partition': 'Common', 'name': 'tcp', 'context': 'all'} ], 'connectionLimit': 1, # should change 'ipProtocol': 'tcp', 'vlans': [ {'name': 'vlan', 'a': '1', 'b': '2'} # should be kept ], 'policies': [] # will be added to } expected = { 'destination': '/test/172.16.3.59%0:80', 'name': 'ingress_172-16-3-59_80', 'rules': [], 'vlansDisabled': True, 'enabled': True, 'sourceAddressTranslation': {'type': 'automap'}, 'partition': 'test', 'source': '0.0.0.0%0/0', 'profiles': [ {'partition': 'Common', 'name': 'http', 'context': 'all'}, {'partition': 'Common', 'name': 'tcp', 'context': 'all'}, {'partition': 'Common', 'name': 'html', 'context': 'all'} ], 'connectionLimit': 0, 'ipProtocol': 'tcp', 'vlans': [ {'name': 'vlan', 'a': '1', 'b': '2'} ], 'policies': [ {'partition': 'test', 'name': 'ingress_172-16-3-59_80'} ] } assert merge(existing, desired) == expected