Harness and network mocks

Often, charms need to access network information; for example:

self.model.get_binding('juju-info').network.bind_address

However, those of you who have tried unittesting this kind of charms will know that this doesn’t work out of the box. So, fresh out of the press, we present to you a networking harness extension.

charmcraft fetch-lib charms.harness_extensions.v0.networking (source)

DISCLAIMER: This is a very early release, a preview of a preview, in alpha version. Take a look, give feedback, but keep in mind that things might still dramatically change soon-ish.

For the above use case, it will be sufficient to add to the top of your unittest file (or conftest.py if you’re into that) the following lines:

from charms.harness_extensions.v0 import networking
networking.enable()

For the more complicated use cases, the networking extension offers a few utilities:

# let's pretend 'foo' is a relation:
foo = charm.model.get_relation('foo', 1)
with networking(networks={foo: Network(private_address="42.42.42.42")}):
    # the juju-info network is present by default unless you pass None
    assert c.model.get_binding("juju-info").network.bind_address == IPv4Address("1.1.1.1")
    # the custom foo endpoint is mocked:
    assert c.model.get_binding(foo).network.bind_address == IPv4Address("42.42.42.42")
    assert c.model.get_binding('foo').network.bind_address == IPv4Address("42.42.42.42")

This is very condensed; what is going on behind the scenes is in fact:


networking.enable()
networking.add_network('foo', 1, 
{
        "bind-addresses": [
            {
                "mac-address": "",
                "interface-name": "",
                "interfacename": "",
                "addresses": [
                    {"hostname": "", "value": "1.1.1.1", "cidr": ""}
                ],
            }
        ],
        "bind-address": "1.1.1.1",
        "egress-subnets": ["1.1.1.2/32"],
        "ingress-addresses": ["1.1.1.2"],
    })

# ... do your tests

networking.disable()

So you can do this whenever you need lower-level control over the network data.

This allows you to simulate juju relate via situations.

Disclaimer

This is not a full mockup of Juju networking. Therefore, consistency is not guaranteed. In order to accurately mock real-world juju networking behaviour, you will still need to understand real-world juju networking behaviour.

This library will make it easier for you to implement some basic scenarios and some common use cases, but if you start wondering ‘how does my CMR fit into this’? Or ‘where are my spaces’? Then you probably need something more advanced. (In which case, do get in touch! We’re eager to understand and support your use case too!)

2 Likes

How about the __init__ for the networking lib just enables things by default. And your functions (being namespaced under “networking”) could just be named “enable” and “disable” - e.g. networking.enable(). Seems better to keep implementation details out of names (“harness_patch”). Also looks like you are missing quotes around one of your foos in the code snippet with the with ....

I did consider side-effecting on import, but I wanted to give more fine-grained control over when things are enabled and when they are not. Also seems to violate the principle of least surprise.

About the naming, I can relate to that. networking.enable() does sound better.

And the foo is supposed to be unquoted: it references the Relation instance.

Released 0.3 with networking.enable() and networking.disable() as suggested by @rwcarlsen

1 Like