Keeping the CMDB the Single Source of Truth
Automating tagging out of ServiceNow into Azure and vCenter.
You should probably have a CMDB.
Why? Because your organization practices ITIL of course.
In all seriousness, having a Configuration Management Database (CMDB) is a fundamental practice in IT Service Management best practices like ITIL. By implementing a CMDB, you'll be able to ensure that your organization has a single source of truth (SSOT) for all IT assets and services, making it easier to manage changes, track configurations, and optimize operations. This, in turn, will help you improve overall service quality in your organization.
I am not here to teach or convince you to adopt ITIL, as it also has its drawbacks. But I am here to say you have a SSOT for the assets (servers, workstations, etc.) inside of your organization. If you have a CMDB, I hope this blog post can help.
You should probably tag your resources too.
Organizations that fail to properly tag their resources, whether they're located in the cloud or on-premises, are missing out on a crucial aspect of effective IT management. By implementing a tagging system, you can gain greater visibility and control over your resources. Tagging allows organizations to implement grouping or resources, attribute-based access control (ABAC), financial management (cost allocation), automation (e.g., patching schedules), and more.
Why does any of that matter?
You could pay anywhere from $30K-$1M on the ITOM module from ServiceNow to automate tagging out of the CMDB into Azure and/or vCenter (plus a whole lot more functionality). Or you could roll your own. I am going to show you how it can be done for no additional cost.
Prerequisites
ServiceNow + Midservers.
AWX or Ansible Tower.
Service account with vSphere tagging permissions.
Azure subscription.
Azure Function with Tagging Contributor/Reader over your specified scope.
Flow Design
The automation is simple, on creation or update of a server record in the CMDB, the specified fields will be grabbed and posted to Ansible or Azure to tag the VMs.
vCenter Tagging
In ServiceNow, Flow Designer can be used to create the first half of both Azure and vCenter tagging. For vCenter you should have the trigger of:
Create or update a server in the cmdb_ci_server table.
Run trigger of “For each unique change”.
Run the flow on current and extended tables.
**Additional conditions are based on your organization.**
Once triggered, you should talk to your AWX or Ansible Tower instance (via a midserver) and run a job via the template ID.
Extra arguments can be sent to the playbook as such:
var vm_name_sys = fd_data.trigger.current.name;
var business_unit_sys = fd_data.trigger.current.company;
var cost_center_sys = fd_data.trigger.current.cost_center;
var owned_by_sys = fd_data.trigger.current.owned_by;
var install_status_sys = fd_data.trigger.current.install_status;
var environment_sys = fd_data.trigger.current.classification;
var location_sys = fd_data.trigger.current.location;
var os_sys = fd_data.trigger.current.os;
var payload = {
vm_name: vm_name_sys.getDisplayValue().toUpperCase(),
business_unit: business_unit_sys.getDisplayValue(),
cost_center: cost_center_sys.getDisplayValue(),
owned_by: owned_by_sys.getDisplayValue(),
install_status: install_status_sys.getDisplayValue(),
classification: environment_sys.getDisplayValue(),
location: location_sys.getDisplayValue(),
os: os_sys.getDisplayValue()
};
return JSON.stringify(payload);
This will post to Ansible, create categories, and assign tags to the VM via an Ansible playbook formatted as such:
---
- name: Create a tag for each category
community.vmware.vmware_tag:
hostname: '<vcenter-hostname>'
username: "{{ service_account }}"
password: "{{ service_password }}"
category_name: "{{ item.category_name }}"
tag_name: "{{ item.tag_name }}"
state: present
loop:
- { category_name: "{{ cost_center_cat }}", tag_name: "{{ cost_center }}" }
- { category_name: "{{ owned_by_cat }}", tag_name: "{{ owned_by }}" }
- { category_name: "{{ classification_cat }}", tag_name: "{{ classification }}" }
- { category_name: "{{ install_stat_cat }}", tag_name: "{{ install_status }}" }
- { category_name: "{{ bu_cat }}", tag_name: "{{ business_unit }}" }
- { category_name: "{{ loc_cat }}", tag_name: "{{ location }}" }
- { category_name: "{{ os_cat }}", tag_name: "{{ os }}" }
delegate_to: localhost
register: tag_result
- name: Assign Tags to VM
community.vmware.vmware_tag_manager:
hostname: '<vcenter-hostname>'
username: "{{ service_account }}"
password: "{{ service_password }}"
tag_names:
- "{{ cost_center_cat }}:{{ cost_center }}"
- "{{ owned_by_cat }}:{{ owned_by }}"
- "{{ classification_cat }}:{{ classification }}"
- "{{ install_stat_cat }}:{{ install_status }}"
- "{{ bu_cat }}:{{ business_unit }}"
- "{{ loc_cat }}:{{ location }}"
- "{{ os_cat }}:{{ os }}"
object_name: "{{ vm_name }}"
object_type: VirtualMachine
state: set
Keep in mind this file would be the ‘task.yml’ in the directory structure of template in AWX/Ansible Tower:
roles/
│
├───call-rolename.yml
│
└───role-name/
│
├───tasks/
│ ├───main.yml
│ └───task.yml
│
└───vars/
└───main.yml
Azure Tagging
Trigger:
Create or update to a server in the cmdb_ci_server table.
Run trigger of “For each unique change”.
Run the flow on current and extended tables.
**Additional conditions are based on your organization.**
Action:
An action must be created in ServiceNow with a script step and a REST step. The script is used to extract the values from the server record passed in by the trigger, and the REST step to pass the output JSON into the function app in Azure.
(function execute(inputs, outputs) {
var outputObject = {
vm_name: inputs.serverGR.name.getDisplayValue(),
business_unit: inputs.serverGR.company.getDisplayValue(),
cost_center: inputs.serverGR.cost_center.getDisplayValue(),
owned_by: inputs.serverGR.owned_by.email.getDisplayValue(),
install_status: inputs.serverGR.install_status.getDisplayValue(),
classification: inputs.serverGR.classification.getDisplayValue(),
managed_by_group: inputs.serverGR.managed_by_group.getDisplayValue(),
support_group: inputs.serverGR.support_group.getDisplayValue(),
os: inputs.serverGR.os.getDisplayValue(),
purpose: inputs.serverGR.short_description.getDisplayValue(),
location: inputs.serverGR.location.getDisplayValue()
};
outputs.outputjson = JSON.stringify(outputObject);
})(inputs, outputs);
The function app allows for tagging of virtual machines with the tagging contributor and reader role assigned to its managed identity.
import os
import azure.functions as func
from azure.identity import ManagedIdentityCredential
from azure.mgmt.resource import ResourceManagementClient
from azure.mgmt.resource.resources.models import TagsResource, TagsPatchResource
from azure.mgmt.compute import ComputeManagementClient
import logging
A map is created to tag the objects in the JSON body and convert them to the correct tag names wanted into Azure:
# A dictionary to map the keys from request body to the required tag names
tag_key_map = {
"business_unit": "BU",
"cost_center": "Cost Center",
"owned_by": "Owner",
"install_status": "Install Status",
"classification": "Classification",
"managed_by_group": "Management Group",
"support_group": "Support Group",
"os": "Operating System",
"purpose": "Purpose",
"location": "Location"
}
The function is used to get the resource group name of the virtual machine passed in the request ‘vm_name’ field:
def get_resource_group(vm_name):
credential = ManagedIdentityCredential()
subscription_id = os.environ["AZURE_SUBSCRIPTION_ID"]
compute_client = ComputeManagementClient(credential, subscription_id)
try:
vms = compute_client.virtual_machines.list_all()
for vm in vms:
if vm.name.lower() == vm_name.lower():
return vm.id.split('/')[4]
logging.info(f"{vm_name} not found.")
raise Exception(f"VM {vm_name} not found in any resource group")
except Exception as e:
logging.error(f"Error getting resource group for VM {vm_name}: {str(e)}")
return None
A function to apply the tags to the specified VM is used:
def apply_tags_to_vm(vm_name, tags):
try:
credential = ManagedIdentityCredential()
subscription_id = os.environ["AZURE_SUBSCRIPTION_ID"]
# Retrieve VM's resource group dynamically
resource_group = get_resource_group(vm_name)
if resource_group is None:
raise Exception(f"Resource group for VM {vm_name} not found")
resource_client = ResourceManagementClient(credential, subscription_id)
# Construct VM's resource ID
vm_resource_id = f"/subscriptions/{subscription_id}/resourceGroups/{resource_group}/providers/Microsoft.Compute/virtualMachines/{vm_name}"
# Create TagsPatchResource for merge operation
tag_patch_resource = TagsPatchResource(
operation="Merge",
properties={'tags': tags}
)
# Update tags for the VM with merge operation
resource_client.tags.begin_update_at_scope(vm_resource_id, tag_patch_resource)
return True, f"Tags {tags} were added to VM {vm_name} in resource group {resource_group}"
except Exception as e:
logging.info(f"Failed to apply tags to VM {str(e)}")
return False, f"Failed to apply tags to VM {vm_name}: {str(e)}"
Lastly, the main function processes the request received from ServiceNow and utilizes the first two functions to assign the tags to the virtual machine.
def main(req: func.HttpRequest) -> func.HttpResponse:
try:
req_body = req.get_json()
# Translate the tags using tag_key_map
tags_to_apply = {tag_key_map[k]: v for k, v in req_body.items() if k in tag_key_map and v is not None}
# Check if the required 'vm_name' key exists
vm_name = req_body.get('vm_name')
if not vm_name:
raise ValueError("Key 'vm_name' not found in request")
success, message = apply_tags_to_vm(vm_name, tags_to_apply)
if success:
return func.HttpResponse(message, status_code=200)
else:
return func.HttpResponse(message, status_code=500)
except ValueError as ve:
# This captures the missing 'vm_name' key situation and returns a 400 Bad Request
return func.HttpResponse(f"Error: {str(ve)}", status_code=400)
except Exception as e:
return func.HttpResponse(f"Error: {str(e)}", status_code=500)
End Results
In the end, when server records inside of the CMDB are created or updated they will either call Ansible and update tags in vCenter or post to a function app that will update tags in Azure. This allows your organization to make changes only inside of the CMDB which will be replicated in different environments without the need for manual intervention or inconsistent data across these systems.
Again, tagging allows for great governance over the cloud and on-prem resources. Implementing an effective tagging strategy allows organizations to have better visibility and control over group, ABAC, cost management, and automation over tagged resources. This post showed the implementation of automating that tagging strategy out of a CMDB and using/maintaining that CMDB as the single source of truth over your organization’s infrastructure.
Code
All code from this blog post can be found on my GitHub.