Example #1
0
    def _state_overridden(want, have):
        """ The command generator when state is overridden

        :rtype: A list
        :returns: the commands necessary to migrate the current configuration
                  to the desired configuration
        """
        commands = []
        for vlan_id, extant in have.items():
            if vlan_id in want:
                desired = want[vlan_id]
            else:
                desired = dict()

            add_config = dict_diff(extant, desired)
            del_config = dict_diff(desired, extant)

            commands.extend(generate_commands(vlan_id, add_config, del_config))

        # Handle vlans not already in config
        new_vlans = [vlan_id for vlan_id in want if vlan_id not in have]
        for vlan_id in new_vlans:
            desired = want[vlan_id]
            extant = dict(vlan_id=vlan_id)
            add_config = dict_diff(extant, desired)

            commands.extend(generate_commands(vlan_id, add_config, {}))

        return commands
Example #2
0
def get_ace_diff(want_ace, have_ace):
    # gives the diff of the aces passed.
    for h_a in have_ace:
        d = dict_diff(want_ace, h_a)
        if not d:
            break
    return d
Example #3
0
    def _state_merged(self, want, have):
        """ The requests generator when state is merged

        :rtype: A list
        :returns: the requests necessary to merge the provided into
                  the current configuration
        """
        requests = []

        request_patch = deepcopy(self.VLAN_PATCH)

        for w in want:
            if w.get('vlan_id'):
                h = search_obj_in_list(w['vlan_id'], have, 'vlan_id')
                if h:
                    if dict_diff(w, h):
                        request_body = self._update_patch_request(w)
                        request_patch["data"]["openconfig-vlan:vlans"][
                            "vlan"].append(request_body)
                else:
                    request_post = self._update_post_request(w)
                    requests.append(request_post)

        if len(request_patch["data"]["openconfig-vlan:vlans"]["vlan"]):
            request_patch["data"] = json.dumps(request_patch["data"])
            requests.append(request_patch)
        return requests
Example #4
0
    def _set_commands(self, want_ace, have_ace):
        """A helped method that checks if there is
           a delta between the `have_ace` and `want_ace`.
           If there is a delta then it calls `_compute_commands`
           to create the ACE line.

        :rtype: A string
        :returns: An ACE generated from a structured ACE dictionary
                  via a call to `_compute_commands`
        """

        if 'line' in want_ace:
            if want_ace['line'] != have_ace.get('line'):
                return self._compute_commands(want_ace)

        else:
            if ('prefix' in want_ace.get('source', {})) or ('prefix' in want_ace.get('destination', {})):
                self._prepare_for_diff(want_ace)

            protocol_opt_delta = {}
            delta = dict_diff(have_ace, want_ace)

            # `dict_diff` doesn't work properly for `protocol_options` diff,
            # so we need to handle it separately
            if want_ace.get('protocol_options', {}):
                protocol_opt_delta = set(flatten_dict(have_ace.get('protocol_options', {}))) ^ \
                    set(flatten_dict(want_ace.get('protocol_options', {})))

            if delta or protocol_opt_delta:
                want_ace = self._dict_merge(have_ace, want_ace)
                return self._compute_commands(want_ace)
    def _render_updates(self, want, have):
        commands = []
        if have:
            temp_have_legacy_protos = have.pop('legacy_protocols', None)
        else:
            have = {}
            temp_want_legacy_protos = want.pop('legacy_protocols', None)

        updates = dict_diff(have, want)

        if have and temp_have_legacy_protos:
            have['legacy_protocols'] = temp_have_legacy_protos
        if not have and temp_want_legacy_protos:
            want['legacy_protocols'] = temp_want_legacy_protos

        commands.extend(self._add_lldp_protocols(want, have))

        if updates:
            for key, value in iteritems(updates):
                if value:
                    if key == 'enable':
                        commands.append(self._compute_command())
                    elif key == 'address':
                        commands.append(
                            self._compute_command('management-address',
                                                  str(value)))
                    elif key == 'snmp':
                        if value == 'disable':
                            commands.append(
                                self._compute_command(key, remove=True))
                        else:
                            commands.append(
                                self._compute_command(key, str(value)))
        return commands
Example #6
0
    def _state_overridden(self, want, have):
        """ The request generator when state is overridden

        :rtype: A list
        :returns: the requests necessary to migrate the current configuration
                  to the desired configuration
        """
        requests = []
        request_patch = deepcopy(self.VLAN_PATCH)

        have_copy = []
        for w in want:
            if w.get('vlan_id'):
                h = search_obj_in_list(w['vlan_id'], have, 'vlan_id')
                if h:
                    if dict_diff(w, h):
                        request_body = self._update_patch_request(w)
                        request_patch["data"]["openconfig-vlan:vlans"][
                            "vlan"].append(request_body)
                    have_copy.append(h)
                else:
                    request_post = self._update_post_request(w)
                    requests.append(request_post)

        for h in have:
            if h not in have_copy and h['vlan_id'] != 1:
                request_delete = self._update_delete_request(h)
                requests.append(request_delete)

        if len(request_patch["data"]["openconfig-vlan:vlans"]["vlan"]):
            request_patch["data"] = json.dumps(request_patch["data"])
            requests.append(request_patch)

        return requests
    def _render_updates(self, want, have):
        commands = []

        temp_have_members = have.pop("members", None)
        temp_want_members = want.pop("members", None)

        updates = dict_diff(have, want)

        if temp_have_members:
            have["members"] = temp_have_members
        if temp_want_members:
            want["members"] = temp_want_members

        commands.extend(self._add_bond_members(want, have))

        if updates:
            for key, value in iteritems(updates):
                if value:
                    if key == "arp_monitor":
                        commands.extend(
                            self._add_arp_monitor(updates, key, want, have))
                    else:
                        commands.append(
                            self._compute_command(have["name"], key,
                                                  str(value)))
        return commands
Example #8
0
    def _state_replaced(self, want, have):
        """ The command generator when state is replaced

        :rtype: A list
        :returns: the commands necessary to migrate the current configuration
                  to the desired configuration
        """
        cmds = []
        obj_in_have = search_obj_in_list(want['name'], have, 'name')
        if obj_in_have:
            diff = dict_diff(want, obj_in_have)
        else:
            diff = want
        merged_cmds = self.set_commands(want, have)
        if 'name' not in diff:
            diff['name'] = want['name']

        replaced_cmds = []
        if obj_in_have:
            replaced_cmds = self.del_attribs(diff)
        if replaced_cmds or merged_cmds:
            for cmd in set(replaced_cmds).intersection(set(merged_cmds)):
                merged_cmds.remove(cmd)
            cmds.extend(replaced_cmds)
            cmds.extend(merged_cmds)
        return cmds
Example #9
0
 def get_diff(self, comparable, base):
     diff = {}
     if not base:
         diff = comparable
     else:
         diff = dict_diff(base, comparable)
     return diff
Example #10
0
    def _state_replaced(self, want, have):
        """ The command generator when state is replaced.
        Scope is limited to vlan objects defined in the playbook.
        :rtype: A list
        :returns: The minimum command set required to migrate the current
                  configuration to the desired configuration.
        """
        obj_in_have = search_obj_in_list(want["vlan_id"], have, "vlan_id")
        if obj_in_have:
            # ignore states that are already reset, then diff what's left
            obj_in_have = self.remove_default_states(obj_in_have)
            diff = dict_diff(want, obj_in_have)
            # Remove merge items from diff; what's left will be used to
            # remove states not specified in the playbook
            for k in dict(set(want.items()) - set(obj_in_have.items())).keys():
                diff.pop(k, None)
        else:
            diff = want

        # merged_cmds: 'want' cmds to update 'have' states that don't match
        # replaced_cmds: remaining 'have' cmds that need to be reset to default
        merged_cmds = self.set_commands(want, have)
        replaced_cmds = []
        if obj_in_have:
            # Remaining diff items are used to reset states to default
            replaced_cmds = self.del_attribs(diff)
        cmds = []
        if replaced_cmds or merged_cmds:
            cmds += ["vlan %s" % str(want["vlan_id"])]
            cmds += merged_cmds + replaced_cmds
        return cmds
    def _add_location(self, name, want_item, have_item):
        commands = []
        have_dict = {}
        have_ca = {}
        set_cmd = name + " location "
        want_location_type = want_item.get("location") or {}
        have_location_type = have_item.get("location") or {}

        if want_location_type["coordinate_based"]:
            want_dict = want_location_type.get("coordinate_based") or {}
            if is_dict_element_present(have_location_type, "coordinate_based"):
                have_dict = have_location_type.get("coordinate_based") or {}
            location_type = "coordinate-based"
            updates = dict_diff(have_dict, want_dict)
            for key, value in iteritems(updates):
                if value:
                    commands.append(
                        self._compute_command(set_cmd + location_type, key,
                                              str(value)))

        elif want_location_type["civic_based"]:
            location_type = "civic-based"
            want_dict = want_location_type.get("civic_based") or {}
            want_ca = want_dict.get("ca_info") or []
            if is_dict_element_present(have_location_type, "civic_based"):
                have_dict = have_location_type.get("civic_based") or {}
                have_ca = have_dict.get("ca_info") or []
                if want_dict["country_code"] != have_dict["country_code"]:
                    commands.append(
                        self._compute_command(
                            set_cmd + location_type,
                            "country-code",
                            str(want_dict["country_code"]),
                        ))
            else:
                commands.append(
                    self._compute_command(
                        set_cmd + location_type,
                        "country-code",
                        str(want_dict["country_code"]),
                    ))
            commands.extend(self._add_civic_address(name, want_ca, have_ca))

        elif want_location_type["elin"]:
            location_type = "elin"
            if is_dict_element_present(have_location_type, "elin"):
                if want_location_type.get("elin") != have_location_type.get(
                        "elin"):
                    commands.append(
                        self._compute_command(
                            set_cmd + location_type,
                            value=str(want_location_type["elin"]),
                        ))
            else:
                commands.append(
                    self._compute_command(
                        set_cmd + location_type,
                        value=str(want_location_type["elin"]),
                    ))
        return commands
Example #12
0
    def _state_replaced(self, w, have):
        """ The command generator when state is replaced

        :rtype: A list
        :returns: the commands necessary to migrate the current configuration
                  to the desired configuration
        """
        commands = []
        obj_in_have = search_obj_in_list(w['name'], have, 'name')
        if obj_in_have:
            diff = dict_diff(w, obj_in_have)
        else:
            diff = w
        merged_commands = self.set_commands(w, have)
        if 'name' not in diff:
            diff['name'] = w['name']
        wkeys = w.keys()
        dkeys = diff.keys()
        for k in wkeys:
            if k in self.exclude_params and k in dkeys:
                del diff[k]
        replaced_commands = self.del_attribs(diff)

        if merged_commands:
            cmds = set(replaced_commands).intersection(set(merged_commands))
            for cmd in cmds:
                merged_commands.remove(cmd)
            commands.extend(replaced_commands)
            commands.extend(merged_commands)
        return commands
Example #13
0
    def _render_updates(self, want, have, opr=True):
        """
        This function takes the diff between want and have and
        invokes the appropriate functions to create the commands
        to update the attributes.
        :param want:
        :param have:
        :return: list of commands
        """
        commands = []
        want_nh = want.get('next_hops') or []
        # delete static route operation per destination
        if not opr and not want_nh:
            commands.append(
                self._compute_command(dest=want['dest'], remove=True))

        else:
            temp_have_next_hops = have.pop('next_hops', None)
            temp_want_next_hops = want.pop('next_hops', None)
            updates = dict_diff(have, want)
            if temp_have_next_hops:
                have['next_hops'] = temp_have_next_hops
            if temp_want_next_hops:
                want['next_hops'] = temp_want_next_hops
            commands.extend(self._add_next_hop(want, have, opr=opr))

            if opr and updates:
                for key, value in iteritems(updates):
                    if value:
                        if key == 'blackhole_config':
                            commands.extend(
                                self._add_blackhole(key, want, have))
        return commands
Example #14
0
    def _state_overridden(self, want, have):
        """ The request generator when state is overridden

        :rtype: A list
        :returns: the requests necessary to migrate the current configuration
                  to the desired configuration
        """
        requests = []
        have_copy = []
        for w in want:
            for h in have:
                if w["name"] == h["name"]:
                    if dict_diff(w, h):
                        l2_request = self._update_patch_request(w, h)
                        l2_request["data"] = json.dumps(l2_request["data"])
                        requests.append(l2_request)
                    have_copy.append(h)
                    break

        for h in have:
            if h not in have_copy:
                l2_delete = self._update_delete_request(h)
                if l2_delete["path"]:
                    l2_delete["data"] = json.dumps(l2_delete["data"])
                    requests.append(l2_delete)

        return requests
Example #15
0
    def _state_merged(self, want, have):
        """ The command generator when state is merged

        :rtype: A list
        :returns: the commands necessary to merge the provided into
                  the current configuration
        """
        commands = []
        want_copy = deepcopy(remove_empties(want))
        have_copy = deepcopy(have)

        want_vifs = want_copy.pop("vifs", [])
        have_vifs = have_copy.pop("vifs", [])

        updates = dict_diff(have_copy, want_copy)

        if updates:
            for key, value in iteritems(updates):
                commands.append(
                    self._compute_commands(key=key,
                                           value=value,
                                           interface=want_copy["name"]))

        if want_vifs:
            for want_vif in want_vifs:
                have_vif = search_obj_in_list(want_vif["vlan_id"],
                                              have_vifs,
                                              key="vlan_id")
                if not have_vif:
                    have_vif = {
                        "vlan_id": want_vif["vlan_id"],
                        "enabled": True,
                    }

                vif_updates = dict_diff(have_vif, want_vif)
                if vif_updates:
                    for key, value in iteritems(vif_updates):
                        commands.append(
                            self._compute_commands(
                                key=key,
                                value=value,
                                interface=want_copy["name"],
                                vif=want_vif["vlan_id"],
                            ))

        return commands
    def _add_location(self, name, want_item, have_item):
        commands = []
        have_dict = {}
        have_ca = {}
        set_cmd = name + ' location '
        want_location_type = want_item.get('location') or {}
        have_location_type = have_item.get('location') or {}

        if want_location_type['coordinate_based']:
            want_dict = want_location_type.get('coordinate_based') or {}
            if is_dict_element_present(have_location_type, 'coordinate_based'):
                have_dict = have_location_type.get('coordinate_based') or {}
            location_type = 'coordinate-based'
            updates = dict_diff(have_dict, want_dict)
            for key, value in iteritems(updates):
                if value:
                    commands.append(
                        self._compute_command(set_cmd + location_type, key, str(value))
                    )

        elif want_location_type['civic_based']:
            location_type = 'civic-based'
            want_dict = want_location_type.get('civic_based') or {}
            want_ca = want_dict.get('ca_info') or []
            if is_dict_element_present(have_location_type, 'civic_based'):
                have_dict = have_location_type.get('civic_based') or {}
                have_ca = have_dict.get('ca_info') or []
                if want_dict['country_code'] != have_dict['country_code']:
                    commands.append(
                        self._compute_command(
                            set_cmd + location_type, 'country-code', str(want_dict['country_code'])
                        )
                    )
            else:
                commands.append(
                    self._compute_command(
                        set_cmd + location_type, 'country-code', str(want_dict['country_code'])
                    )
                )
            commands.extend(self._add_civic_address(name, want_ca, have_ca))

        elif want_location_type['elin']:
            location_type = 'elin'
            if is_dict_element_present(have_location_type, 'elin'):
                if want_location_type.get('elin') != have_location_type.get('elin'):
                    commands.append(
                        self._compute_command(
                            set_cmd + location_type, value=str(want_location_type['elin'])
                        )
                    )
            else:
                commands.append(
                    self._compute_command(
                        set_cmd + location_type, value=str(want_location_type['elin'])
                    )
                )
        return commands
Example #17
0
    def test_dict_diff(self):
        base = dict(obj2=dict(),
                    b1=True,
                    b2=False,
                    b3=False,
                    one=1,
                    two=2,
                    three=3,
                    obj1=dict(key1=1, key2=2),
                    l1=[1, 3],
                    l2=[1, 2, 3],
                    l4=[4],
                    nested=dict(n1=dict(n2=2)))

        other = dict(b1=True,
                     b2=False,
                     b3=True,
                     b4=True,
                     one=1,
                     three=4,
                     four=4,
                     obj1=dict(key1=2),
                     l1=[2, 1],
                     l2=[3, 2, 1],
                     l3=[1],
                     nested=dict(n1=dict(n2=2, n3=3)))

        result = dict_diff(base, other)

        # string assertions
        self.assertNotIn('one', result)
        self.assertNotIn('two', result)
        self.assertEqual(result['three'], 4)
        self.assertEqual(result['four'], 4)

        # dict assertions
        self.assertIn('obj1', result)
        self.assertIn('key1', result['obj1'])
        self.assertNotIn('key2', result['obj1'])

        # list assertions
        self.assertEqual(result['l1'], [2, 1])
        self.assertNotIn('l2', result)
        self.assertEqual(result['l3'], [1])
        self.assertNotIn('l4', result)

        # nested assertions
        self.assertIn('obj1', result)
        self.assertEqual(result['obj1']['key1'], 2)
        self.assertNotIn('key2', result['obj1'])

        # bool assertions
        self.assertNotIn('b1', result)
        self.assertNotIn('b2', result)
        self.assertTrue(result['b3'])
        self.assertTrue(result['b4'])
def test_dict_diff():
    base = dict(obj2=dict(),
                b1=True,
                b2=False,
                b3=False,
                one=1,
                two=2,
                three=3,
                obj1=dict(key1=1, key2=2),
                l1=[1, 3],
                l2=[1, 2, 3],
                l4=[4],
                nested=dict(n1=dict(n2=2)))

    other = dict(b1=True,
                 b2=False,
                 b3=True,
                 b4=True,
                 one=1,
                 three=4,
                 four=4,
                 obj1=dict(key1=2),
                 l1=[2, 1],
                 l2=[3, 2, 1],
                 l3=[1],
                 nested=dict(n1=dict(n2=2, n3=3)))

    result = dict_diff(base, other)

    # string assertions
    assert 'one' not in result
    assert 'two' not in result
    assert result['three'] == 4
    assert result['four'] == 4

    # dict assertions
    assert 'obj1' in result
    assert 'key1' in result['obj1']
    assert 'key2' not in result['obj1']

    # list assertions
    assert result['l1'] == [2, 1]
    assert 'l2' not in result
    assert result['l3'] == [1]
    assert 'l4' not in result

    # nested assertions
    assert 'obj1' in result
    assert result['obj1']['key1'] == 2
    assert 'key2' not in result['obj1']

    # bool assertions
    assert 'b1' not in result
    assert 'b2' not in result
    assert result['b3']
    assert result['b4']
    def _state_overridden(want, have):
        """ The command generator when state is overridden

        :rtype: A list
        :returns: the commands necessary to migrate the current configuration
                  to the desired configuration
        """
        commands = []
        for key, extant in have.items():
            if key in want:
                desired = want[key]
            else:
                desired = dict()

            add_config = dict_diff(extant, desired)
            del_config = dict_diff(desired, extant)

            commands.extend(generate_commands(key, add_config, del_config))

        return commands
Example #20
0
def set_config(want, have):
    commands = []
    to_set = dict_diff(have, want)
    for member in to_set.get("members", []):
        channel_id = want["name"][12:]
        commands.extend([
            "interface {0}".format(member["member"]),
            "channel-group {0} mode {1}".format(channel_id, member["mode"]),
        ])

    return commands
Example #21
0
    def _state_replaced(want, have):
        """ The command generator when state is replaced

        :rtype: A list
        :returns: the commands necessary to migrate the current configuration
                  to the desired configuration
        """
        commands = []
        for vlan_id, desired in want.items():
            if vlan_id in have:
                extant = have[vlan_id]
            else:
                extant = dict()

            add_config = dict_diff(extant, desired)
            del_config = dict_diff(desired, extant)

            commands.extend(generate_commands(vlan_id, add_config, del_config))

        return commands
Example #22
0
    def _state_merged(self, want, have):
        """ The command generator when state is merged

        :rtype: A list
        :returns: the commands necessary to merge the provided into
                  the current configuration
        """
        commands = []
        diff = dict_diff(have, want)
        commands.extend(self.set_commands(diff))
        return commands
Example #23
0
 def set_commands(self, want, have):
     commands = []
     obj_in_have = flatten_dict(
         search_obj_in_list(want['name'], have, 'name'))
     if not obj_in_have:
         commands = self.add_commands(flatten_dict(want))
     else:
         diff = dict_diff(obj_in_have, want)
         if diff:
             diff.update({'name': want['name']})
             commands = self.add_commands(diff)
     return commands
    def _state_replaced(want, have):
        """ The command generator when state is replaced

        :rtype: A list
        :returns: the commands necessary to migrate the current configuration
                  to the desired configuration
        """
        commands = []
        for key, desired in want.items():
            interface_name = normalize_interface(key)
            if interface_name in have:
                extant = have[interface_name]
            else:
                extant = dict()

            add_config = dict_diff(extant, desired)
            del_config = dict_diff(desired, extant)

            commands.extend(generate_commands(key, add_config, del_config))

        return commands
Example #25
0
def remove_config(want, have):
    commands = []
    if not want.get("members"):
        return ["no interface {0}".format(want["name"])]

    to_remove = dict_diff(want, have)
    for member in to_remove.get("members", []):
        commands.extend([
            "interface {0}".format(member["member"]),
            "no channel-group",
        ])

    return commands
Example #26
0
    def _state_replaced(want, have):
        """ The command generator when state is replaced

        :rtype: A list
        :returns: the commands necessary to migrate the current configuration
                  to the desired configuration
        """
        commands = []
        to_set = dict_diff(have, want)
        if 'system' in to_set:
            system = to_set['system']
            if 'priority' in system:
                commands.append('lacp system-priority {0}'.format(system['priority']))

        to_del = dict_diff(want, have)
        if 'system' in to_del:
            system = to_del['system']
            system_set = to_set.get('system', {})
            if 'priority' in system and 'priority' not in system_set:
                commands.append('no lacp system-priority')

        return commands
Example #27
0
    def _state_merged(self, want, have):
        """ The command generator when state is merged

        :rtype: A list
        :returns: the commands necessary to merge the provided into
                  the current configuration
        """
        commands = []
        updates = dict_diff(have, want)
        if updates:
            for key, value in iteritems(flatten_dict(remove_empties(updates))):
                commands.append(self._compute_commands(key, value))

        return commands
Example #28
0
    def _state_replaced(self, w, have):
        """ The command generator when state is replaced

        :rtype: A list
        :returns: the commands necessary to migrate the current configuration
                  to the desired configuration
        """
        commands = []
        name = w['name']
        obj_in_have = search_obj_in_list(name, have, 'name')
        if obj_in_have:
            # If 'w' does not specify mode then intf may need to change to its
            # default mode, however default mode may depend on sysdef.
            if not w.get('mode') and re.search('Ethernet|port-channel', name):
                sysdefs = self.intf_defs['sysdefs']
                sysdef_mode = sysdefs['mode']
                if obj_in_have.get('mode') != sysdef_mode:
                    w['mode'] = sysdef_mode
            diff = dict_diff(w, obj_in_have)
        else:
            diff = w

        merged_commands = self.set_commands(w, have)
        if merged_commands:
            # merged_commands:
            #   - These commands are changes specified by the playbook.
            #   - merged_commands apply to both existing and new objects
            # replaced_commands:
            #   - These are the unspecified commands, used to reset any params
            #     that are not already set to default states
            #   - replaced_commands should only be used on 'have' objects
            #     (interfaces that already exist)
            if obj_in_have:
                if 'name' not in diff:
                    diff['name'] = name
                wkeys = w.keys()
                dkeys = diff.keys()
                for k in wkeys:
                    if k in self.exclude_params and k in dkeys:
                        del diff[k]
                replaced_commands = self.del_attribs(diff)
                cmds = set(replaced_commands).intersection(
                    set(merged_commands))
                for cmd in cmds:
                    merged_commands.remove(cmd)
                commands.extend(replaced_commands)

            commands.extend(merged_commands)
        return commands
Example #29
0
    def _state_merged(want, have):
        """ The command generator when state is merged

        :rtype: A list
        :returns: the commands necessary to merge the provided into
                  the current configuration
        """
        commands = []
        to_set = dict_diff(have, want)
        if 'system' in to_set:
            system = to_set['system']
            if 'priority' in system:
                commands.append('lacp system-priority {0}'.format(system['priority']))

        return commands
Example #30
0
    def _state_deleted(want, have):
        """ The command generator when state is deleted

        :rtype: A list
        :returns: the commands necessary to remove the current configuration
                  of the provided objects
        """
        commands = []
        to_del = dict_diff(want, have)
        if 'system' in to_del:
            system = to_del['system']
            if 'priority' in system:
                commands.append('no lacp system-priority')

        return commands
Example #31
0
def test_dict_diff():
    base = dict(obj2=dict(), b1=True, b2=False, b3=False,
                one=1, two=2, three=3, obj1=dict(key1=1, key2=2),
                l1=[1, 3], l2=[1, 2, 3], l4=[4],
                nested=dict(n1=dict(n2=2)))

    other = dict(b1=True, b2=False, b3=True, b4=True,
                 one=1, three=4, four=4, obj1=dict(key1=2),
                 l1=[2, 1], l2=[3, 2, 1], l3=[1],
                 nested=dict(n1=dict(n2=2, n3=3)))

    result = dict_diff(base, other)

    # string assertions
    assert 'one' not in result
    assert 'two' not in result
    assert result['three'] == 4
    assert result['four'] == 4

    # dict assertions
    assert 'obj1' in result
    assert 'key1' in result['obj1']
    assert 'key2' not in result['obj1']

    # list assertions
    assert result['l1'] == [2, 1]
    assert 'l2' not in result
    assert result['l3'] == [1]
    assert 'l4' not in result

    # nested assertions
    assert 'obj1' in result
    assert result['obj1']['key1'] == 2
    assert 'key2' not in result['obj1']

    # bool assertions
    assert 'b1' not in result
    assert 'b2' not in result
    assert result['b3']
    assert result['b4']