Using Tags with Pynetbox in NetBox 2.9+

I wrote earlier about using tags with pynetbox in NetBox 2.8. After that in NetBox 2.9 tag handling was significantly changed: tags must now be separately created before they can be assigned to objects. Previously tags were automatically created if you assigned a nonexisting tag to an object.

Another change is that when returning objects from NetBox the tags are not listed just as strings but as full objects. Let’s see these changes a bit better.

Note: Most of the examples below need pynetbox version 5.1.2 or newer. The older versions have problems saving the modified tags with NetBox 2.9+.

With NetBox 2.8 you could do this:

>>> site = netbox28.dcim.sites.create({"name": "Testsite", "slug": "testsite", "tags": ["Tag 1"]})
>>> pprint.pprint(dict(site))
...
 'tags': ['Tag 1'],
...
>>>

Here the tags were specified as list of tag names (strings).

Now with NetBox 2.9 and later (I’m having the currently latest 2.10.3 here) you will get an error:

>>> site = netbox.dcim.sites.create({"name": "Testsite", "slug": "testsite", "tags": ["Tag 1"]})
Traceback (most recent call last):
...
pynetbox.core.query.RequestError: The request failed with code 400 Bad Request: {'tags': [['Related objects must be referenced by numeric ID or by dictionary of attributes. Received an unrecognized value: Tag 1']]}
>>>

The exception very clearly says that “Tag 1” is an invalid value, and that numeric ID or a dictionary of attributes is needed.

We don’t currently have any tags so let’s create one, and then use it for a new site:

>>> tag = netbox.extras.tags.create({"name": "Tag 1"})
Traceback (most recent call last):
...
pynetbox.core.query.RequestError: The request failed with code 400 Bad Request: {'slug': ['This field is required.']}
>>>
>>> tag = netbox.extras.tags.create({"name": "Tag 1", "slug": "tag-1"})
>>> pprint.pprint(dict(tag))
{'color': '9e9e9e',
 'description': '',
 'id': 4,
 'name': 'Tag 1',
 'slug': 'tag-1',
 'url': 'http://netbox-test.lein.io/api/extras/tags/4/'}
>>>
>>> testsite = netbox.dcim.sites.create({"name": "Testsite", "slug": "testsite", "tags": [{"name": "Tag 1"}]})
>>> testsite.tags
[Tag 1]
>>> pprint.pprint(dict(testsite))
...
 'tags': [{'color': '9e9e9e',
           'id': 4,
           'name': 'Tag 1',
           'slug': 'tag-1',
           'url': 'http://netbox-test.lein.io/api/extras/tags/4/'}],
...
>>>

Notable detail is that when creating a tag, in addition to the tag name you need to specify the tag slug as well, it is not created automatically. The slug is basically an URL-friendly string, so basic rule is that all whitespaces and special characters need to be replaced or removed.

The assigned tags were listed as dictionaries in the tags attribute of the site.

Obviously you will get an error if you try to create a tag that already exists:

>>> same_tag = netbox.extras.tags.create({"name": "Tag 1", "slug": "tag-1"})
Traceback (most recent call last):
...
pynetbox.core.query.RequestError: The request failed with code 400 Bad Request: {'name': ['tag with this name already exists.'], 'slug': ['tag with this slug already exists.']}
>>>

So your tag adding operations may look like this in practice:

>>> def get_or_create_tag(name, slug):
...     tag = netbox.extras.tags.get(name=name)
...     if not tag:
...         tag = netbox.extras.tags.create({"name": name, "slug": slug})
...     return tag
...
>>> t1 = get_or_create_tag("Tag 1", "tag-1")
>>> t1
Tag 1
>>> t2 = get_or_create_tag("Tag 2", "tag-2")
>>> t2
Tag 2
>>>

When creating the site in the earlier example above I entered a list of dictionaries (well, just list of one dictionary in that case, but list anyway) to specify the tags, but as mentioned in an exception earlier, you can also use the tag IDs:

>>> t1.id
4
>>> t2.id
5
>>> testsite2 = netbox.dcim.sites.create({"name": "Testsite 2", "slug": "testsite-2", "tags": [4, 5]})
>>> testsite2.tags
[Tag 1, Tag 2]
>>>

Now that the site has the tags, the tag list modifications are somewhat harder than with the earlier list of strings. As you can see, the tags are now Record objects:

>>> type(testsite2.tags[0])
<class 'pynetbox.core.response.Record'>
>>>

When deleting the tag “Tag 1” from the list this is one way of doing it:

>>> tags = []
>>> for tag in testsite2.tags:
...     if tag.name != "Tag 1":
...             tags.append(tag)
...
>>> testsite2.tags = tags
>>> testsite2.tags
[Tag 2]
>>> # Did not save them to NetBox yet though!
>>>

This is a more pythonic way:

>>> testsite2 = netbox.dcim.sites.get(name="Testsite 2")
>>> testsite2.tags
[Tag 1, Tag 2]
>>> testsite2.tags = [tag for tag in testsite2.tags if tag.name != "Tag 1"]
>>> testsite2.tags
[Tag 2]
>>> testsite2.save()
True
>>>

The complex line here is a list comprehension. It creates a new tag list by running a loop against the original tag list, but only includes the tags that match the condition in the end of the statement (tag.name != "Tag 1"), so effectively it removes that one tag from the list.

Adding a tag is similar to specifying the tags when creating an object: use a tag ID or a tag-describing dictionary:

>>> testsite2.tags.append(4)
>>> testsite2.tags
[Tag 2, 4]
>>> testsite2.save()
True
>>> testsite2.full_details()
True
>>> testsite2.tags
[Tag 1, Tag 2]
>>>

Here I appended the tag ID in the tags list and saved the object successfully.

(full_details() call was here used for updating the testsite2 object data with full data from NetBox.)

Or, a dictionary can be appended:

>>> netbox.extras.tags.create({"name": "Last Tag", "slug": "last-tag"})
Last Tag
>>> testsite2.tags.append({"name": "Last Tag"})
>>> testsite2.tags
[Tag 1, Tag 2, {'name': 'Last Tag'}]
>>> testsite2.save()
True
>>> testsite2.full_details()
True
>>> testsite2.tags
[Last Tag, Tag 1, Tag 2]
>>>

It can be somewhat confusing that the object’s tag list can contain objects of different types when modifying the tags: there are the original tags as Record objects, and then there can be integers (tag IDs) or dictionaries (describing tags) for the newly added tags. But you need to remember how pynetbox actually works here: it does not process the tag data much itself, but it just sends it in the NetBox API call when saving the object, and it is NetBox that accepts integers or dictionaries as tag inputs.

Checking if a tag is present:

>>> testsite2.tags
[Last Tag, Tag 1, Tag 2]
>>> if "Tag 1" in [tag.name for tag in testsite2.tags]:
...     print("Yes, it's there")
...
Yes, it's there
>>>

This again uses list comprehension to generate a list of all names of the tags, and then checks the membership of “Tag 1” in that list.

The final example is about filtering based on tags. It hasn’t been changed with NetBox version 2.9+, so it still works with tag slugs (and using tag as the filter argument name, not tags):

>>> netbox.dcim.sites.all()
[Testsite, Testsite 2]
>>> netbox.dcim.sites.filter(tag="last-tag")
[Testsite 2]
>>>

Leave a Reply