Visio Diagrams with Python: Resize to fit

Resize Page to Fit Contents, Set Zoom to Fit Page

When the drawing is ready, the size of the page will be too large or too small – „fit to contents“ might be the solution.

Additionally, I prefer the Zoom of the active Window to be set that it shows the whole drawing.

page.ResizeToFitContents()      #https://docs.microsoft.com/de-de/office/vba/api/visio.page.resizetofitcontents
window = visio.ActiveWindow     #https://docs.microsoft.com/de-de/office/vba/api/visio.application.activewindow
window.ViewFit=1                #https://docs.microsoft.com/de-de/office/vba/api/visio.window.viewfit

Result

Visio Diagrams with Python: Colors

24 Colors to choose from.

„Cells“ contain all „Shape-Format“-Properties

Details: https://docs.microsoft.com/en-us/office/client-developer/visio/cells-visio-shapesheet-reference

# Line-Color
# https://docs.microsoft.com/de-de/office/client-developer/visio/linecolor-cell-line-format-section
oval2.Cells("LineColor").FormulaForce = 3

# Fill-Color
# https://docs.microsoft.com/de-de/office/client-developer/visio/fillforegnd-cell-fill-format-section
rect1.Cells("FillForegnd").FormulaForce=3

# Fill-Pattern "0" - no fill
# https://docs.microsoft.com/de-de/office/client-developer/visio/fillpattern-cell-fill-format-section
rect2.Cells("FillPattern").FormulaForce=0

# Fill-Pattern "3"

# yellow background
# https://docs.microsoft.com/de-de/office/client-developer/visio/fillbkgnd-cell-fill-format-section
#
# light-blue foreground
#
oval1.Cells("FillPattern").FormulaForce = 3
oval1.Cells("FillBkgnd").FormulaForce = 5
oval1.Cells("FillForegnd").FormulaForce = 7

Result

Visio Diagrams with Python: Connectors

Connect two existing Shapes

The API is somehow weird, since it creates an (Shape-)Object, but it doesn’t return it.

  • you’ll have to fetch the last object in the shapes-List
#https://docs.microsoft.com/en-us/office/vba/api/visio.shape.autoconnect
rect1.AutoConnect(rect2,0)
#
#https://docs.microsoft.com/en-us/office/vba/api/visio.shapes.itemu
connector1=shapes.ItemU(len(shapes))
#
connector1.Text = "Connector1"
# 
#https://docs.microsoft.com/de-de/office/client-developer/visio/linecolor-cell-line-format-section
# * color "3" is "light green"
connector1.Cells("LineColor").FormulaForce = 3
#
#https://docs.microsoft.com/de-de/office/client-developer/visio/conlinerouteext-cell-shape-layout-section
# * ConLineRouteExt "2" is "Curved"
connector1.Cells("ConLineRouteExt").FormulaForce = 2

Result

>>> len(shapes)
6

Visio Diagrams with Python: Stencils

Stencils and Stencil-Files

Stencils are stored in „VSSX“-Files

server_stencils_filename = "C:\Program Files (x86)\Microsoft Office\root\Office16\Visio Content\1031\SERVER_M.VSSX"

#64 means “open hidden”
server_stencils = visio.Documents.OpenEx(server_stencils_filename,64)

ftp_stencil=server_stencils.Masters("FTP-Server")

Page-Object

Add a FTP-Server to the page

ftp1 = page.Drop(ftp_stencil,3,3)
ftp1.Text = "FTP-Server#1"

Result

>>> len(shapes)
5

Visio Diagrams with Python using COM

Visualize Data from Python in Visio

I needed to illustrate some data discovered by a python script in Microsoft Visio.

As a starting-point i found two sources of infomation in the Internet:

(1) Python => COM => MS Excel

How to access a Windows COM-Application from Python: https://pbpython.com/windows-com.html

  • MS Excel not Visio
  • but 50% of the information needed

(2) PowerShell => COM => MS Visio

How to access Microsoft Visio using the COM-Interface https://www.powershellstation.com/2016/01/20/powershell-and-visio-1

  • PowerShell
  • the other 50% needed.

Result: „Python => COM => MS Visio“

Putting both pieces together, will allow a Python-Script to control MS Visio.

(3) MS Visio „Object Model“

Without the original documentation provided by Microsoft: https://docs.microsoft.com/en-us/office/vba/api/overview/visio there is _no chance_ to get anything to work.

  • the next 50% 😉

The Basics

(1) Python „PyWin32“

Install „PyWin32“-Library

C:\RH>pip install pywin32
Collecting pywin32
  Downloading pywin32-300-cp38-cp38-win_amd64.whl (9.3 MB)
     |████████████████████████████████| 9.3 MB 2.2 MB/s
Installing collected packages: pywin32
Successfully installed pywin32-300

Access Windows/COM from Python

  • open the Visio-Application
    • „visio“-object: stores a pointer to access application-Level functions
  • add a document
    • „document“-object
  • fetch the active Visio-Page
    • „page“-object
  • fetch the list of all Shapes at this page
    • „shapes“-object
import win32com.client as win32
#
visio = win32.gencache.EnsureDispatch('Visio.Application')
document = visio.Documents.Add("")
#
page = visio.ActivePage
#
shapes = page.Shapes

(2) Explore the Visio-Object-Model

Methods

Object-Methods could get discovered using the Python „dir“, notice for example

  • DrawOval
  • DrawRectangle
>>> dir(page)
['AddDataVisualization', 'AddGuide', 'AutoConnectMany', 'AutoSizeDrawing', 'AvoidPageBreaks', 'BoundingBox', 'CLSID', 'CenterDrawing', 'CreateDataVisualizerDiagram', 'CreateSelection', 'Delete', 'DrawArcByThreePoints', 'DrawBezier', 'DrawCircularArc', 'DrawLine', 'DrawNURBS', 'DrawOval', 'DrawPolyline', 'DrawQuarterArc', 'DrawRectangle', 'DrawSpline', 'Drop', 'DropCallout', 'DropConnected', 'DropContainer', 'DropIntoList', 'DropLegend', 'DropLinked', 'DropMany', 'DropManyLinkedU', 'DropManyU', 'Duplicate', 'Export', 'GetCallouts', 'GetContainers', 'GetFormulas', 'GetFormulasU', 'GetResults', 'GetShapesLinkedToData', 'GetShapesLinkedToDataRow', 'GetTheme', 'GetThemeVariant', 'Import', 'InsertFromFile', 'InsertObject', 'Layout', 'LayoutChangeDirection', 'LayoutIncremental', 'LinkShapesToDataRows', 'OpenDrawWindow', 'Paste', 'PasteSpecial', 'PasteToLocation', 'Print', 'PrintTile', 'ResizeToFitContents', 'SetFormulas', 'SetResults', 'SetTheme', 'SetThemeVariant', 'ShapeIDsToUniqueIDs', 'SpatialSearch', 'SplitConnector', 'UniqueIDsToShapeIDs', 'VisualBoundingBox', '_ApplyTypes_', '__call__', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattr__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__int__', '__iter__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', '_get_good_object_', '_get_good_single_object_', '_oleobj_', '_prop_map_get_', '_prop_map_put_', 'coclass_clsid', 'old_Paste', 'old_PasteSpecial']

Properties

But Object-Parameters won’t and if you don’t want to guess required parameters of Object-Methods, it’s time to bookmark the original documentation provided by Microsoft: https://docs.microsoft.com/en-us/office/vba/api/overview/visio

Start drawing

Page-Object

A good starting point might be the „Page“-Object: https://docs.microsoft.com/en-us/office/vba/api/visio.page

Well add „Shapes“ to the „Page“(-Object) using Methods of the Page-Object:

https://docs.microsoft.com/en-us/office/vba/api/visio.page.drawrectangle or https://docs.microsoft.com/en-us/office/vba/api/visio.page.drawoval – both Methods will return an „Shape“-Object.

#https://docs.microsoft.com/en-us/office/vba/api/visio.page.drawrectangle
rect1 = page.DrawRectangle(1,1,2,2)
rect2 = page.DrawRectangle(4,4,5,5)
#
#https://docs.microsoft.com/en-us/office/vba/api/visio.page.drawoval
oval1 = page.DrawOval(1,4,2,5)
oval2 = page.DrawOval(4,1,5,2)

Shape Object

Look at https://docs.microsoft.com/en-us/office/vba/api/visio.shape – there’s a „Text“-Property:

#https://docs.microsoft.com/en-us/office/vba/api/visio.shape
#https://docs.microsoft.com/en-us/office/vba/api/visio.shape.text
rect1.Text="Rect1"
rect2.Text="Rect2"
oval1.Text="Oval1"
oval2.Text="Oval2"

Result

>>> len(shapes)
4

WaitForCDPNeighbor.py enhanced: Wait for CDP Hostname with usable connected IP-Address.

The following version adds a „-c“ option: „Connected IP-Address“-Check.

  • the script exits, when the Router-VM is up and running and has an usable IP-Address

The Script still checks all CDP-Packets received at the specified Interface:

  • If the Sender has the correct „Hostname“ it reads the CDP-Management-IP-Address announced.
  • This IP-Address has to be within the IP-Range of any connected IP-Network at the specified Interface.

So it’ll be possible to establish a SSH-Session to the router-VM.

C:\> echo %VM%
CSX

C:\> python WaitForCDPNeighbor.py -n %VM% -c -i "VirtualBox Host-Only Ethernet Adapter"
192.168.56.101
#! /usr/bin/env python

# Ronald Heitmann
#
import argparse

from scapy.all import *
load_contrib("cdp")

from netaddr import IPNetwork, IPAddress

from netifaces import AF_INET, AF_INET6, AF_LINK
import netifaces

# returns a list of all IP-Adresses bound to the specified Interface "if_name"
def get_connected(if_name):
  if_id = ""
  networks = []

  for i in ifaces.data.keys():
    iface = ifaces.data[i]
    wname = iface.data['name']
    if wname == if_name:
      if_id = i
      addresses = netifaces.ifaddresses(i)
      if AF_INET in addresses:
        for addr in netifaces.ifaddresses(i)[AF_INET]:
          #print(addr)
          ipnetwork = IPNetwork(addr["addr"]+"/"+addr["netmask"]).cidr
          networks.append(ipnetwork)
  #print(networks)
  return networks

# checks, if the IP-Address "ip" is within the subnet-range of any network contained in the list "networks"

def is_IP_connected(ip, networks):
  found = False
  ipnetwork = IPNetwork(ip).cidr

  for n in networks:
    if (ipnetwork in n):
      found = True

  return found

  
def main():

    # Parse CLI-Arguments
    parser = argparse.ArgumentParser(description='Wait for a CDP-Neighbor.')
    parser.add_argument("-i", "--interface", help="monitored interface", default="VirtualBox Host-Only Ethernet Adapter")
    parser.add_argument("-n", "--hostname", help="Neighbor to wait for", required=True)
    parser.add_argument("-c", "--connected", help="wait, until CDP-Neighbor announces an directly-connected IP-Address", action='store_true')
    args = parser.parse_args()

    #looking for a specific hostname
    #
    hostname = ""
    wait_for_hostname = args.hostname

    #watching for CDP-Packets from this host on a specific interface
    #the router-IP should use a directly-connected IP-Address
    ip = "0.0.0.0"
    wait_for_connected = args.connected
    
    interface=args.interface
    networks = get_connected(interface)
    #print(networks) 

    #CDP
    capturefilter="ether dst 01:00:0c:cc:cc:cc"

    while not((hostname == wait_for_hostname) and (not(wait_for_connected) or is_IP_connected(ip,networks))):
      p=sniff(iface=interface, count=1, filter=capturefilter)
      pkt=p[0]
      #print("Packet received",pkt.show())

      #is this a CDP-Packet containing a hostname?
      if (CDPMsgDeviceID in pkt):
        #is this the CDP-Neighbor we're looking for?
        device=pkt["CDPMsgDeviceID"].val.decode()
        hostname=device.split(".")[0]
        #print("Hostname:",hostname)

        if (hostname == wait_for_hostname):
          #is this a CDP-Packet containing a management-IP-Address?
          if (CDPAddrRecordIPv4 in pkt):
            ip=pkt["CDPAddrRecordIPv4"].addr
            #print("IP-Address found:",ip)

    #return the IP-Address to the calling application or the CLI
    return ip

if __name__ == "__main__":
    print(main())

Python: Combine NetIfaces, Scapy and IPAddress to find local connected network

You got the task to generate a list of IP-Networks connected to LAN „VirtualBox Host-Only Network“. There might be more than one IP-Network.

Use Scapy to crawl through all Interfaces and get the human-readable interface name [only required for windows users]. Use NetIfaces to get a list of IP-Addresses connected to this interface. Use IPAddress to calculate the IP-Network(s) directly connected.

! multiple IPs per Interface supporte
!
from netifaces import AF_INET, AF_INET6, AF_LINK
import netifaces

from scapy.all import *

import ipaddress


if_name = "VirtualBox Host-Only Network"
if_id = ""
if_inet = []

for i in ifaces.data.keys():
  iface = ifaces.data[i]
  wname = iface.data['netid']
  if wname == if_name:
    if_id = i
    addresses = netifaces.ifaddresses(i)
    if AF_INET in addresses:
      for addr in netifaces.ifaddresses(i)[AF_INET]:
        print(addr)
        ipaddr = ipaddress.ip_interface(addr["addr"]+"/"+addr["netmask"])
        ipnetwork = ipaddr.network
        print(ipaddr,ipnetwork)
        if_inet.append(ipnetwork)

print("NAME: {0}\nIP: {1}\nID: {2}".format(if_name,if_inet,if_id))

In my case, only one subnet is directly connected:

  • 192.168.56.0/24
...
{'addr': '192.168.56.1', 'netmask': '255.255.255.0', 'broadcast': '192.168.56.255'}
192.168.56.1/24 192.168.56.0/24
>>> print("NAME: {0}\nIP: {1}\nID: {2}".format(if_name,if_inet,if_id))
NAME: VirtualBox Host-Only Network
IP: [IPv4Network('192.168.56.0/24')]
ID: {D30DEC05-D495-4DA1-81F1-42B07885B0EB}
>>>

Python netifaces and scapy: Getting meaningful Interface-names and full IP-Adresses-/Netmask-Information

Using Windows:

  • scapy reads:
    • meaningful interface-names
    • ip-address
    • but no netmask
  • netifaces reads:
    • full Ethernet/IP/IPv6-Information
    • but no meaningful interface-names

Mixing both, provides everything needed.

  1. Netifaces only
  2. from netifaces import AF_INET, AF_INET6, AF_LINK
    import netifaces
    
    for i in netifaces.interfaces():
       niif=netifaces.ifaddresses(i)
       print("i",i)
       for k,v in niif.items():
         print("Key",k)
         if k==AF_LINK:
           print("LINK:",v)
         if k==AF_INET:
           print("IPv4",v)
         if k==AF_INET6:
           print("IPv6",v)
       print()
    
    >>> for i in netifaces.interfaces():
    ...    niif=netifaces.ifaddresses(i)
    ...    print("i",i)
    ...    for k,v in niif.items():
    ...      print("Key",k)
    ...      if k==AF_LINK:
    ...        print("LINK:",v)
    ...      if k==AF_INET:
    ...        print("IPv4",v)
    ...      if k==AF_INET6:
    ...        print("IPv6",v)
    ...    print()
    ...
    i {07E9D8A4-E167-4FFF-B851-61A20C49AE6E}
    Key -1000
    LINK: [{'addr': '00:00:81:00:de:11'}]
    Key 23
    IPv6 [{'addr': 'fe80::8400:abb0:2a62:a173%4', 'netmask': 'ffff:ffff:ffff:ffff::/64', 'broadcast': 'fe80::ffff:ffff:ffff:ffff%4'}]
    
    i {D30DEC05-D495-4DA1-81F1-42B07885B0EB}
    Key -1000
    LINK: [{'addr': '0a:00:27:00:00:14'}]
    Key 23
    IPv6 [{'addr': 'fe80::1e0:4a4:8afc:90f7%20', 'netmask': 'ffff:ffff:ffff:ffff::/64', 'broadcast': 'fe80::ffff:ffff:ffff:ffff%20'}]
    Key 2
    IPv4 [{'addr': '192.168.56.1', 'netmask': '255.255.255.0', 'broadcast': '192.168.56.255'}]
    
    i {402C453F-4B74-4883-9257-BD31FAB7AB57}
    Key -1000
    LINK: [{'addr': '74:70:fd:bd:10:38'}]
    Key 23
    IPv6 [{'addr': 'fe80::2599:368f:e80d:94b6%8', 'netmask': 'ffff:ffff:ffff:ffff::/64', 'broadcast': 'fe80::ffff:ffff:ffff:ffff%8'}]
    
    i {2120C2CA-E7D4-45DF-8090-A8D7F48EFF42}
    Key -1000
    LINK: [{'addr': '76:70:fd:bd:10:37'}]
    Key 23
    IPv6 [{'addr': 'fe80::34d6:9683:4af5:afad%6', 'netmask': 'ffff:ffff:ffff:ffff::/64', 'broadcast': 'fe80::ffff:ffff:ffff:ffff%6'}]
    
    i {F931F123-70A4-4CFB-BD9C-C0A509080286}
    Key -1000
    LINK: [{'addr': '74:70:fd:bd:10:37'}]
    Key 23
    IPv6 [{'addr': 'fe80::1926:3c29:1b34:e1f6%22', 'netmask': 'ffff:ffff:ffff:ffff::/64', 'broadcast': 'fe80::ffff:ffff:ffff:ffff%22'}]
    Key 2
    IPv4 [{'addr': '192.168.1.163', 'netmask': '255.255.255.0', 'broadcast': '192.168.1.255'}]
    
    i {693A5869-6A31-11E8-85D1-806E6F6E6963}
    Key -1000
    LINK: [{'addr': ''}]
    Key 23
    IPv6 [{'addr': '::1', 'netmask': 'ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff/128', 'broadcast': '::1'}]
    Key 2
    IPv4 [{'addr': '127.0.0.1', 'netmask': '255.0.0.0', 'broadcast': '127.255.255.255'}]
    
  3. Adding Scapy to get meaningful interface names
  4. from scapy.all import *
    
    for i in ifaces.data.keys():
      print(i)
      iface = ifaces.data[i]
      wname = iface.data['netid']
      addresses = netifaces.ifaddresses(i)
      af_inet = [{"addr": "0.0.0.0", "netmask": "0.0.0.0", "broadcast": "0.0.0.0"}]
      if AF_INET in addresses:
        af_inet = netifaces.ifaddresses(i)[AF_INET]
      print(wname,af_inet)
    
    >>> for i in ifaces.data.keys():
    ...   print(i)
    ...   iface = ifaces.data[i]
    ...   wname = iface.data['netid']
    ...   addresses = netifaces.ifaddresses(i)
    ...   af_inet = [{"addr": "0.0.0.0", "netmask": "0.0.0.0", "broadcast": "0.0.0.0"}]
    ...   if AF_INET in addresses:
    ...     af_inet = netifaces.ifaddresses(i)[AF_INET]
    ...   print(wname,af_inet)
    ...
    {F931F123-70A4-4CFB-BD9C-C0A509080286}
    Wi-Fi [{'addr': '192.168.1.163', 'netmask': '255.255.255.0', 'broadcast': '192.168.1.255'}]
    {D30DEC05-D495-4DA1-81F1-42B07885B0EB}
    VirtualBox Host-Only Network [{'addr': '192.168.56.1', 'netmask': '255.255.255.0', 'broadcast': '192.168.56.255'}]
    {07E9D8A4-E167-4FFF-B851-61A20C49AE6E}
    ETH_DELL [{'addr': '0.0.0.0', 'netmask': '0.0.0.0', 'broadcast': '0.0.0.0'}]
    
    

Python „netifaces“ installation: „Microsoft Visual C++ 14.0 is required“

I expected a nobrainer:

C:\RH>pip install netifaces
Collecting netifaces
  Downloading https://files.pythonhosted.org/packages/81/39/4e9a026265ba944ddf1fea176dbb29e0fe50c43717ba4fcf3646d099fe38/netifaces-0.10.7.tar.gz
Installing collected packages: netifaces
  Running setup.py install for netifaces ... error
    Complete output from command c:\users\rh\appdata\local\programs\python\python37\python.exe -u -c "import setuptools, tokenize;__file__='C:\\Users\\RH\\AppData\\Local\\Temp\\pip-install-wbfanly3\\netifaces\\setup.py';f=getattr(tokenize, 'open', open)(__file__);code=f.read().replace('\r\n', '\n');f.close();exec(compile(code, __file__, 'exec'))" install --record C:\Users\RONALD~1.HEI\AppData\Local\Temp\pip-record-m26yfbyt\install-record.txt --single-version-externally-managed --compile:
    running install
    running build
    running build_ext
    building 'netifaces' extension
    error: Microsoft Visual C++ 14.0 is required. Get it with "Microsoft Visual C++ Build Tools": http://landinghub.visualstudio.com/visual-cpp-build-tools

The suggested URL to download the missing software is wrong (HTTP 404).

  1. Go to the Microsoft-Repository Tools for Visual Studio 2017 or use the direct link to vs_buildtools.exe

    • …it’s about 1.2MB
  2. run „vs_buildtools.exe“
    • …it downloads ~ 70 MB
  3. Select „Workloads => Windows => [x] Visual C++ Build Tools“ => [Install]
    • …it downloads 1.12 GB
    • …and installs
  4. and … unlucky…

  5. reboot required

Now netifaces can get installed:

C:\RH>pip install netifaces
Collecting netifaces
  Using cached https://files.pythonhosted.org/packages/81/39/4e9a026265ba944ddf1fea176dbb29e0fe50c43717ba4fcf3646d099fe38/netifaces-0.10.7.tar.gz
Installing collected packages: netifaces
  Running setup.py install for netifaces ... done
Successfully installed netifaces-0.10.7

Python Scapy: Wait for CDP-Neighbor

After provisioning a new Cisco CSR1000V-Router this script

  • waits until the router is fully deployed
  • got it’s Management-IP-Address assigned via DHCP.

and returns this Management IP-Address.

  1. Prepare
  2. First, install „netaddr“-package.

    • needed to check, if the management-IP of the CDP-Neighbor is in the correct IP-Subnet
    C:\RH>pip install netaddr
    Collecting netaddr
      Downloading https://files.pythonhosted.org/packages/ba/97/ce14451a9fd7bdb5a397abf99b24a1a6bb7a1a440b019bebd2e9a0dbec74/netaddr-0.7.19-py2.py3-none-any.whl (1.6MB)
        100% |████████████████████████████████| 1.6MB 4.0MB/s
    Installing collected packages: netaddr
    Successfully installed netaddr-0.7.19
    
  3. the „wait-for-CDP“-Script
  4. Obviously far to much hard-coded stuff, just as an example 😉 waiting for a CDP-Neighbor to appear with the following properties:

    • hostname „CSR-A“
    • an IP-Address within the network „192.168.56.0 /24“
    • at the interface

    • VirtualBox Host-Only-Network
    #! /usr/bin/env python
    
    from scapy.all import *
    load_contrib("cdp")
    
    from netaddr import IPNetwork, IPAddress
    
    # run it for max. 99 Packets
    ip = "0.0.0.0"
    wait_for_hostname = "CSR-A"
    wait_for_host_in_network = "192.168.56.0/24"
    
    interface="VirtualBox Host-Only Ethernet Adapter"
    capturefilter="ether dst 01:00:0c:cc:cc:cc"
    
    while not(IPAddress(ip) in IPNetwork(wait_for_host_in_network)):
      p=sniff(iface=interface, count=1, filter=capturefilter)
      pkt=p[0]
      #print("Packet received",pkt.show())
    
      #is this a CDP-Packet containing a hostname?
      if (CDPMsgDeviceID in pkt):
        #is this the CDP-Neighbor we're looking for?
        device=pkt["CDPMsgDeviceID"].val.decode()
        hostname=device.split(".")[0]
        #print("Hostname:",hostname)
    
        if (hostname == wait_for_hostname):
          #is this a CDP-Packet containing a management-IP-Address?
          if (CDPAddrRecordIPv4 in pkt):
            ip=pkt["CDPAddrRecordIPv4"].addr
            #print("IP-Address found:",ip)
    
    #return the IP-Address to the calling application or the CLI
    print(ip)
    
  5. Provision a new CSR1000V Router named „CSR-A“
  6. C:\RH>CSR1000v-Virtual-Box.cmd
    Enter CSR1000v Hostname:CSR-A
    Virtual machine 'CSR-A' is created and registered.
    UUID: 8a9c969e-3895-4a7c-9cbc-5f5551bf1b7b
    Settings file: 'c:\RH\LAB\VM\CSR-A\CSR-A.vbox'
    Creating ISO image at c:\RH\LAB\VM\CSR-A\config.iso, 1 Files, Size: 8,00 KB
    100%
    0%...10%...20%...30%...40%...50%...60%...70%...80%...90%...100%
    Medium created. UUID: 55ea8df5-7e3c-4485-8e08-9302cb61a09d
    Waiting for VM "CSR-A" to power on...
    VM "CSR-A" has been successfully started.
    
  7. Run the „wait-for-CDP-Neighbor“-Script
  8. In a second CMD-Box, and… Be patient, of course.

    C:\RH>python wait-for-cdpneighbor.py
    192.168.56.101
    
  9. Or store the returned IP-Address in an Shell-Variable „ROUTER-IP“/li>
    C:\RH>for /f %I in ('python wait-for-cdpneighbor.py 2^> nul') do @(set ROUTER-IP=%I)
    
  10. and pass it to putty – using public-key authentication
  11. The Public-Key for user „labuser“ is part of my baseline-config used to provision the virtual-router.

    C:\RH>putty -ssh -i C:\RH\work\putty-rsa-key\labuser.ppk labuser@%ROUTER-IP%
    

    Works, this is a nice preparation for an automated virtual ansible-lab, too!