Skip to content

Commit fce1d64

Browse files
author
DanielDraganov
authored
Merge pull request #735 from karaatanassov/topic/macmonitor
Sample monitoring for changes in the MAC addresses of VMs.
2 parents 7020598 + a5e4f82 commit fce1d64

File tree

2 files changed

+310
-1
lines changed

2 files changed

+310
-1
lines changed

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@ argparse
22
pyvmomi>=v6.5.0.2017.5-1
33
requests
44
suds>=0.4,<0.7 ; python_version < '3'
5-
suds-jurko ; python_version >= '3.0'
5+
suds>=1.0 ; python_version >= '3.0'
66
vcrpy>=1.1.1

samples/monitor_mac_addresses.py

Lines changed: 309 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,309 @@
1+
#!/usr/bin/env python
2+
"""
3+
Copyright (c) 2023 VMware, Inc. All Rights Reserved.
4+
5+
Sample monitoring for changes in the MAC addresses of VMs.
6+
7+
The monitor uses the `PropertyCollector` API to detect changes in the MAC
8+
addresses of VMs. The `PropertyCollector` is initialized with a `ContainerView`
9+
of all VMs. As we can observe MAC addresses either from the virtual ethernet
10+
cards or through the guest the property spec includes all VM virtual devices -
11+
`config.hardware.device` and guest networks - `guest.net`. As ethernet cards
12+
are only one of many possible virtual devices, we will receive spurious updates
13+
related to other hardware. Also DHCP changes will update the `guest.net` values
14+
even when the MAC and IP addresses do not change. To mitigate the spurious
15+
updates the property collector output is filtered through a cache of currently
16+
known values that only lets through real changes to MAC and IP addresses.
17+
18+
The code can be enhanced to run in a background executor and provide query
19+
capability to query the cache as necessary.
20+
"""
21+
22+
23+
import time
24+
from pyVmomi import vim, vmodl
25+
from pyVim.connect import Disconnect
26+
from tools import cli, service_instance
27+
28+
DEVICES_PROP_PATH = "config.hardware.device"
29+
GUEST_NET_PROP_PATH = "guest.net"
30+
NAME_PROP_PATH = "name"
31+
32+
33+
class VMDetails:
34+
"""
35+
Physical Networks Address details of a VM. Contains vm name and a map
36+
of device key to MAC address.
37+
"""
38+
def __init__(self, vm_name: str, vnic: dict[int:str], guest_net: dict[str:list[str]]):
39+
""" Create a new VM Details
40+
vm_name: The name of the VM
41+
vm_mac_addresses: A map of device key to MAC address
42+
vnic: A map of device key to mac address
43+
guest_net: A map of mac address to ip addresses
44+
"""
45+
self.vm_name = vm_name
46+
self.vnic = vnic
47+
self.guest_net = guest_net
48+
49+
50+
class VmMacChangeListener:
51+
""" Listens for changes in the mac addresses of VMs """
52+
def update_vm(self, vm_id: str, vm_name: str,
53+
vnic: dict[int:str], guest_net: dict[str:list[str]]):
54+
""" Update the VM details
55+
vm_id: The id of the VM
56+
vm_name: The name of the VM
57+
vnic: A map of device key to mac address
58+
guest_net: A map of mac address to ip addresses
59+
"""
60+
def remove_vm(self, vm_id: str):
61+
""" Remove a VM from the list of VMs
62+
vm_id: The id of the VM
63+
"""
64+
65+
66+
class VmMacChangePrinter:
67+
""" Prints changes in the mac addresses of VMs """
68+
def update_vm(self, vm_id: str, vm_name: str,
69+
vnic: dict[int:str], guest_net: dict[str:list[str]]):
70+
print(f"VM '{vm_name}' ({vm_id}) has mac addresses\
71+
\n\tVNICS: {vnic}\n\tGUEST_NET: {guest_net}")
72+
73+
def remove_vm(self, vm_id: str):
74+
print(f"VM {vm_id} has been removed")
75+
76+
77+
class VmMacCache(VmMacChangeListener):
78+
"""
79+
This cache removes spurious updates to MAC and IP addresses. It listens for
80+
changes in the MAC addresses of VMs, updates the cache of VM Mac addresses
81+
when a real change occurs and notifies a nested VmMacChangeListener. The
82+
cache is a map of VM id to VMDetails.
83+
"""
84+
def __init__(self, nested: VmMacChangeListener):
85+
""" Create a new VM Mac Change Cache
86+
nested: The next listener to notify of changes
87+
"""
88+
self.vm_cache: dict[str, VMDetails] = {}
89+
self.nested = nested
90+
91+
def update_vm(self, vm_id: str, vm_name: str,
92+
vnic: dict[int:str], guest_net: dict[str:list[str]]):
93+
""" Update the VM details.
94+
vm_id: The id of the VM
95+
vm_name: The name of the VM
96+
vnic: A map of device key to mac address
97+
guest_net: A map of mac address to ip addresses
98+
"""
99+
if vm_id in self.vm_cache:
100+
updated = False
101+
if vm_name and self.vm_cache[vm_id].vm_name != vm_name:
102+
self.vm_cache[vm_id].vm_name = vm_name
103+
updated = True
104+
if vnic and self.vm_cache[vm_id].vnic != vnic:
105+
self.vm_cache[vm_id].vnic = vnic
106+
updated = True
107+
if guest_net and self.vm_cache[vm_id].guest_net != guest_net:
108+
self.vm_cache[vm_id].guest_net = guest_net
109+
updated = True
110+
if updated:
111+
cached = self.vm_cache[vm_id]
112+
self.nested.update_vm(vm_id, cached.vm_name, cached.vnic, cached.guest_net)
113+
else:
114+
self.vm_cache[vm_id] = VMDetails(vm_name, vnic, guest_net)
115+
self.nested.update_vm(vm_id, vm_name, vnic, guest_net)
116+
117+
def remove_vm(self, vm_id: str):
118+
""" Remove a VM from the list of VMs
119+
vm_id: The id of the VM
120+
"""
121+
if vm_id in self.vm_cache:
122+
del self.vm_cache[vm_id]
123+
self.nested.remove_vm(vm_id)
124+
125+
126+
def make_wait_options(max_wait_seconds: int = None, max_object_updates: int = None) -> \
127+
vmodl.query.PropertyCollector.WaitOptions:
128+
"""
129+
Creates property collector wait options needed for WaitForUpdatesEx API.
130+
"""
131+
wait_opts = vmodl.query.PropertyCollector.WaitOptions()
132+
133+
if max_object_updates is not None:
134+
wait_opts.maxObjectUpdates = max_object_updates
135+
136+
if max_wait_seconds is not None:
137+
wait_opts.maxWaitSeconds = max_wait_seconds
138+
139+
return wait_opts
140+
141+
142+
def create_view_filter(view: vim.view.View,
143+
prop_spec: vmodl.query.PropertyCollector.PropertySpec) -> \
144+
vmodl.query.PropertyCollector.FilterSpec:
145+
"""
146+
Create a property collector filter spec based on a view object and a set of
147+
properties the caller wants to monitor.
148+
"""
149+
traversal_spec = vmodl.query.PropertyCollector.TraversalSpec()
150+
traversal_spec.name = "traverseEntities"
151+
traversal_spec.path = "view"
152+
traversal_spec.skip = False
153+
traversal_spec.type = vim.view.ContainerView
154+
155+
objectSpec = vmodl.query.PropertyCollector.ObjectSpec()
156+
objectSpec.obj = view
157+
objectSpec.skip = True
158+
objectSpec.selectSet = [traversal_spec]
159+
160+
filter_spec = vmodl.query.PropertyCollector.FilterSpec()
161+
filter_spec.propSet = [prop_spec]
162+
filter_spec.objectSet = [objectSpec]
163+
164+
return filter_spec
165+
166+
167+
class VmMacChangeDetector:
168+
""" Detects changes in the MAC addresses of VMs from PropertyCollector updates. """
169+
def __init__(self, si: vim.ServiceInstance, listener: VmMacChangeListener,
170+
max_wait_seconds: int = 10, max_object_updates: int = 100) -> None:
171+
"""
172+
Create a new VM Mac Change Detector
173+
si: The Service Instance
174+
listener: The listener to notify of changes
175+
"""
176+
self.si = si
177+
self.listener = listener
178+
self.max_wait_seconds = max_wait_seconds
179+
self.max_object_updates = max_object_updates
180+
self.version = ""
181+
self.pc = None
182+
self.view = None
183+
self.filter = None
184+
185+
def monitor(self, seconds: int):
186+
"""
187+
Monitor for changes in the mac addresses of VMs.
188+
seconds: number of seconds to monitor changes. 0 monitors indefinitely
189+
"""
190+
if not self.pc:
191+
self._init_property_collector()
192+
193+
wait_opts = make_wait_options(self.max_wait_seconds, self.max_object_updates)
194+
195+
start = time.time()
196+
while seconds == 0 or time.time() - start < seconds:
197+
res = self.pc.WaitForUpdatesEx(self.version, wait_opts)
198+
if res is None:
199+
continue
200+
self.version = res.version
201+
for filter_set in res.filterSet:
202+
if filter_set.filter == self.filter:
203+
self._process_updates(filter_set.objectSet)
204+
205+
def close(self):
206+
""" Close the active objects """
207+
self.filter.DestroyPropertyFilter()
208+
self.view.DestroyView()
209+
self.pc.DestroyPropertyCollector()
210+
self.filter = None
211+
self.view = None
212+
self.pc = None
213+
214+
def _init_property_collector(self):
215+
""" Initialise the PropertyCollector """
216+
self.pc = self.si.content.propertyCollector.CreatePropertyCollector()
217+
view_mgr = self.si.content.viewManager
218+
root_folder = self.si.content.rootFolder
219+
self.view = view_mgr.CreateContainerView(root_folder, [vim.VirtualMachine], True)
220+
221+
prop_spec = vmodl.query.PropertyCollector.PropertySpec()
222+
prop_spec.type = vim.VirtualMachine
223+
prop_spec.pathSet = [NAME_PROP_PATH, GUEST_NET_PROP_PATH, DEVICES_PROP_PATH]
224+
225+
filter_spec = create_view_filter(self.view, prop_spec)
226+
self.filter = self.pc.CreateFilter(filter_spec, False)
227+
self.version = ""
228+
229+
def _process_updates(self, objects: list[vmodl.query.PropertyCollector.ObjectUpdate]):
230+
for obj_update in objects:
231+
# pylint: disable=W0212
232+
mo_id = obj_update.obj._GetMoId()
233+
if obj_update.kind == "leave":
234+
self.listener.remove_vm(mo_id)
235+
continue
236+
# 'enter' or 'modify'
237+
name = None
238+
vnic = None
239+
guest_net = None
240+
for change in obj_update.changeSet:
241+
if change.name == NAME_PROP_PATH:
242+
if change.op != "assign":
243+
print(f"WARN: Unexpected name change in {mo_id} \
244+
{obj_update.obj.name}: {change.op}")
245+
continue
246+
name = change.val
247+
if change.name == GUEST_NET_PROP_PATH:
248+
if change.op != "assign":
249+
print(f"WARN: Unexpected net change in {mo_id} \
250+
{obj_update.obj.name}: {change.op}")
251+
continue
252+
guest_net = self._get_guest_addresses(change.val)
253+
if change.name == DEVICES_PROP_PATH:
254+
if change.op != "assign":
255+
print(f"WARN: Unexpected device change in {mo_id} \
256+
{obj_update.obj.name}: {change.op}")
257+
continue
258+
vnic = self._get_vnic_addresses(change.val)
259+
self.listener.update_vm(mo_id, name, vnic, guest_net)
260+
261+
def _get_vnic_addresses(self, devices):
262+
vnic = {}
263+
for device in devices:
264+
if isinstance(device, vim.vm.device.VirtualEthernetCard) and \
265+
device.key and device.macAddress:
266+
vnic[device.key] = device.macAddress
267+
return vnic
268+
269+
def _get_guest_addresses(self, nics):
270+
guest_net = {}
271+
for nic in nics:
272+
if isinstance(nic, vim.vm.GuestInfo.NicInfo) and nic.macAddress:
273+
guest_net[nic.macAddress] = [ip.ipAddress for ip in nic.ipConfig.ipAddress] \
274+
if nic.ipConfig else []
275+
return guest_net
276+
277+
def __enter__(self):
278+
return self
279+
280+
def __exit__(self, exc_type, exc_value, traceback):
281+
self.close()
282+
283+
284+
def main():
285+
"""
286+
Sample monitoring for changes in the MAC addresses of VMs.
287+
"""
288+
parser = cli.Parser()
289+
parser.add_optional_arguments(cli.Argument.MINUTES)
290+
parser.add_custom_argument('--no_filter',
291+
action="store_true",
292+
default=False,
293+
help='Remove the filtering cache.')
294+
args = parser.get_args()
295+
si = service_instance.connect(args)
296+
wait_seconds = int(args.minutes) * 60 if args.minutes else 60
297+
try:
298+
printer = VmMacChangePrinter()
299+
300+
listener = printer if args.no_filter else VmMacCache(printer)
301+
302+
with VmMacChangeDetector(si, listener) as detector:
303+
detector.monitor(wait_seconds)
304+
finally:
305+
Disconnect(si)
306+
307+
308+
if __name__ == "__main__":
309+
main()

0 commit comments

Comments
 (0)