Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions module/sources/vmware/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,17 @@ def __init__(self):
ConfigOption("host_tag_source", str),
ConfigOption("vm_tag_source", str)
]),
ConfigOption("tag_name_include_category",
bool,
description="""\
If enabled, vCenter tag names synced to NetBox will include the vCenter category as a
prefix in the format 'CategoryName:TagName'. Useful if TagName and CategoryName is used
as key/value pairs in vCenter.
When changed, existing synced tags are replaced on
the next run. Note: vm_exclude_by_tag_filter entries must use 'CategoryName:TagName'
format when this option is enabled.
""",
default_value=False),
ConfigOption("sync_custom_attributes",
bool,
description="""sync custom attributes defined for hosts and VMs
Expand Down
22 changes: 17 additions & 5 deletions module/sources/vmware/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -806,19 +806,30 @@ def get_vmware_object_tags(self, obj):

# noinspection PyBroadException
try:
tag_name = self.tag_session.tagging.Tag.get(tag_id).name
tag_description = self.tag_session.tagging.Tag.get(tag_id).description
tag = self.tag_session.tagging.Tag.get(tag_id) # store the object
tag_name = tag.name
tag_description = tag.description
except Exception as e:
log.error(f"Unable to retrieve vCenter tag '{tag_id}' for '{obj.name}': {e}")
continue
continue # skip tag entirely if basic fetch fails

if tag_name is not None:
category_name = None
if bool(self.settings.tag_name_include_category) is True:
# noinspection PyBroadException
try:
category_name = self.tag_session.tagging.Category.get(tag.category_id).name
except Exception as e:
log.debug(f"Unable to retrieve category of vCenter tag '{tag_name}': {e}")

if tag_name is not None:
if tag_description is not None and len(f"{tag_description}") > 0:
tag_description = f"{primary_tag_name}: {tag_description}"
else:
tag_description = primary_tag_name

if category_name is not None:
tag_name = f"{category_name}:{tag_name}"

tag_list.append(self.inventory.add_update_object(NBTag, data={
"name": tag_name,
"description": tag_description
Expand Down Expand Up @@ -2452,8 +2463,9 @@ def add_virtual_machine(self, obj):
vcenter_tags = self.collect_object_tags(obj)

# check if VM tag excludes VM from being synced to NetBox
vcenter_tag_names = [NetBoxObject.extract_tag_name(t) for t in vcenter_tags]
for sync_exclude_tag in self.settings.vm_exclude_by_tag_filter or list():
if sync_exclude_tag in vcenter_tags:
if sync_exclude_tag in vcenter_tag_names:
log.debug(f"Virtual machine vCenter tag '{sync_exclude_tag}' in matches 'vm_exclude_by_tag_filter'. "
f"Skipping")
return
Expand Down
8 changes: 8 additions & 0 deletions settings-example.ini
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,14 @@ password = super-secret
;host_tag_source =
;vm_tag_source =

; If enabled, vCenter tag names synced to NetBox will include the vCenter category as a
; prefix in the format 'CategoryName:TagName'. Useful if TagName and CategoryName is used
; as key/value pairs in vCenter.
; When changed, existing synced tags are replaced on
; the next run. Note: vm_exclude_by_tag_filter entries must use 'CategoryName:TagName'
; format when this option is enabled.
;tag_name_include_category = False

; sync custom attributes defined for hosts and VMs in vCenter to NetBox as custom fields
;sync_custom_attributes = False

Expand Down
94 changes: 94 additions & 0 deletions tests/test_vmware_tag_category.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
"""
vCenter tag handling: the exclude filter has to compare tag names, and putting the
category into the tag name is opt-in and must not change anything else (PR #518).

vcsim serves no tag API, so the tag session is faked where the tags themselves matter.
"""
from types import SimpleNamespace

import pytest

from module.netbox.object_classes import NBTag, NBVM
from module.sources import instantiate_sources
from module.sources.vmware import connection as vmware_connection
from module.sources.vmware.connection import VMWareHandler


@pytest.fixture(autouse=True)
def dynamic_id(monkeypatch):
"""The vSphere automation SDK is optional and absent here, the tag call needs the type."""
monkeypatch.setattr(vmware_connection, "DynamicID",
lambda **kwargs: SimpleNamespace(**kwargs), raising=False)


class _FakeTagging:
"""The parts of the vSphere tagging API get_vmware_object_tags() uses."""

def __init__(self, name="prod", description="", category="env"):
self.category_lookups = 0
tag = SimpleNamespace(name=name, description=description, category_id="cat-1")
outer = self

class _Tag:
@staticmethod
def get(_tag_id):
return tag

class _Category:
@staticmethod
def get(_category_id):
outer.category_lookups += 1
return SimpleNamespace(name=category)

class _TagAssociation:
@staticmethod
def list_attached_tags(_dynamic_id):
return ["tag-1"]

self.Tag, self.Category, self.TagAssociation = _Tag, _Category, _TagAssociation


def _handler_with_tags(inventory, tagging, include_category=False):
handler = object.__new__(VMWareHandler)
handler.inventory = inventory
handler.name = "test"
handler.source_tag = "Source: test"
handler.tag_session = SimpleNamespace(tagging=tagging)
handler.settings = SimpleNamespace(tag_name_include_category=include_category)
return handler


def _collect(handler):
obj = SimpleNamespace(name="vm1", _wsdlName="VirtualMachine", _moId="vm-1")
return handler.get_vmware_object_tags(obj)


def test_tag_name_and_description_are_untouched_by_default(inventory):
tagging = _FakeTagging(description="production")
tags = _collect(_handler_with_tags(inventory, tagging))

assert [tag.get_display_name() for tag in tags] == ["prod"]
assert tags[0].data.get("description") == "NetBox-synced: production"
assert tagging.category_lookups == 0, "the category was looked up although the option is off"


def test_tag_name_includes_the_category_when_enabled(inventory):
tagging = _FakeTagging(description="production")
tags = _collect(_handler_with_tags(inventory, tagging, include_category=True))

assert [tag.get_display_name() for tag in tags] == ["env:prod"]
assert tags[0].data.get("description") == "NetBox-synced: production"


def test_vm_exclude_by_tag_filter_skips_matching_vms(vcsim, inventory, load_config, vmware_settings, monkeypatch):
load_config(vmware_settings + "vm_exclude_by_tag_filter = no-sync\n")
source = instantiate_sources()[0]
assert source.init_successful

excluded = inventory.add_update_object(NBTag, data={"name": "no-sync"})
monkeypatch.setattr(source, "collect_object_tags", lambda _obj: [excluded])

inventory.resolve_relations()
source.apply()

assert list(inventory.get_all_items(NBVM)) == [], "vm_exclude_by_tag_filter did not exclude anything"