APIC-EM Data Export

I was at a Cisco DNA customer event on Thursday. Someone in the audience asked a very good question. Basically they wanted to know if there was a way to extrapolate data from the APIC-EM network management tool. At first glance it didn’t seem to be something that was available in the UI. One of the Cisco representatives quickly and correctly stated that it APIC-EM to Excelis all available from the API.

My initial thought was that this was a product weakness. Why can’t we just manually extract this stuff to a CSV and import it wherever we want to? Whether due to intentional omission or strategic direction, an API first approach is better. It is better because it allows systems to be glued together and more of our mindless tasks to be automated. So the counter argument to that really revolves around use cases, initial effort and skills gaps. The examples I’m about to provide should help alleviate some of those concerns.

TL;DR — Looking to get APIC-EM data into an Excel spreadsheet? Python can easily grab Host and Device Data and provide it in a format that is easily consumable such as Text, Tab, CSV or other format of choice.

So after the session, I wanted to just see what level of effort it would take to create code that does the following:

  1. Connect to APIC-EM
  2. Collect the required information
  3. Output the data into a format that can be easily consumed

Obviously step 3 could be to connect the data directly to another system, but I had no interest in going there (others may AND should).

This article will share two Python examples that extract the host and network device inventory and format them into a Tab delimited format. This isn’t meant to demonstrate beautiful or elegant Python coding techniques. My hope is that network engineers use this as a starting point to connect into this useful system, grab useful data and do something important with it. Most should be able make some progress on this without a lot of prior knowledge or investment in time.

For background, I am running this on a MAC with OSX Sierra. The Python installation is the default. I did add the requests library.

PAULMAC:basic-labs paulste$ sudo pip install requests
The directory '/Users/paulste/Library/Caches/pip/http' or its parent directory is not owned by the current user and the cache has been disabled. Please check the permissions and owner of that directory. If executing pip with sudo, you may want sudo's -H flag.
The directory '/Users/paulste/Library/Caches/pip' or its parent directory is not owned by the current user and caching wheels has been disabled. check the permissions and owner of that directory. If executing pip with sudo, you may want sudo's -H flag.
Collecting requests
  Downloading requests-2.13.0-py2.py3-none-any.whl (584kB)
    100% |████████████████████████████████| 593kB 1.4MB/s
Installing collected packages: requests
Successfully installed requests-2.13.0

The examples are set to use an always on sandbox that is hosted by Cisco Devnet. So there could be a point in time that these no longer work against that test destination. I would also say to anyone running this against the Cisco Demonstration (dCloud) instances may see that it is unauthorized. In my testing, I received a 403 Forbidden. So I have to assume that there are IP based restrictions against the API sub directories.

Example 1 – apic-em_dev_tab.py (extracts network devices into Tab format) — Comments Explain the Function

###Written by Paul Stewart @packetu
###http://www.packetu.com
###Use of script constitutes acceptance of risk
###modify and distribute freely
### usage -- python apic-em_dev_txt.py >> tab_text.txt

import requests
import json
import sys
import getpass

####variables####
###default config is devnet sandbox

apic_api = "https://devnetapi.cisco.com/sandbox/apic_em/api/v1/"

ticket_url = apic_api + "ticket"
host_url = apic_api + "network-device"
scope = 'all'

###authenication method-uncomment one of the following###

##uncomment for stored credentials##
username = "devnetuser"
password = "Cisco123!"

##uncomment for prompt for credentials##
#username = raw_input("Please enter username: ")
#password = getpass.getpass(prompt="Please enter password: ")


####
# log in and get a service ticket 
# store in resp

tck_data = "{\n\"username\":\"" + username + "\",\n\"password\":\"" + password + "\"\n}\n"
tck_head = {
    'content-type': "application/json",
    'cache-control': "no-cache",
    }
tck_resp = requests.request("POST", ticket_url, data=tck_data, headers=tck_head)

tck_resp = json.loads(tck_resp.text)
tck_resp = tck_resp["response"]
tck_resp = tck_resp['serviceTicket']

####
#use service ticket in tck_resp to 
#Connect and pull list of hosts


hst_head = {
    'scope': scope,
    'x-auth-token': tck_resp,
    'cache-control': "no-cache",
    }
    
hst_resp = requests.request("GET", host_url, headers=hst_head)

myhosts = json.loads(hst_resp.text)


####
#print myhosts as
#comma delimited 
#using .get to fill any null 
#values and prevent key errors
#modify to your required output

####
# 'errorDescription' causing issue--omitting

####print the header
print "macAddress\tupTime\tbootDateTime\tlastUpdateTime\tsnmpContact\tplatformId\tseries\terrorCode\tinterfaceCount\tid\tlocationName\tinstanceUuid\tfamily\treachabilityFailureReason\treachabilityStatus\thostname\tmemorySize\troleSource\tlineCardCount\tcollectionStatus\trole\tlocation\ttype\tlineCardId\tapManagerInterfaceIp\tlastUpdated\tmanagementIpAddress\tserialNumber\tinventoryStatusDetail\tsoftwareVersion\tsnmpLocation\ttagCount"

####print the data
if myhosts['response'] == []:
    print 'No Data!'
else:
	for rows in myhosts['response']:
		print str(rows.get('macAddress', '')) + "\t" + str(rows.get('upTime', '')) + "\t" + str(rows.get('bootDateTime', '')) + "\t" + str(rows.get('lastUpdateTime', '')) + "\t" + str(rows.get('snmpContact', '')) + "\t" + str(rows.get('platformId', '')) + "\t" + str(rows.get('series', '')) + "\t" + str(rows.get('errorCode', '')) + "\t" + str(rows.get('interfaceCount', '')) + "\t" + str(rows.get('id', '')) + "\t" + str(rows.get('locationName', '')) + "\t" + str(rows.get('instanceUuid', '')) + "\t" + str(rows.get('family', '')) + "\t" + str(rows.get('reachabilityFailureReason', '')) + "\t" + str(rows.get('reachabilityStatus', '')) + "\t" + str(rows.get('hostname', '')) + "\t" + str(rows.get('memorySize', '')) + "\t" + str(rows.get('roleSource', '')) + "\t" + str(rows.get('lineCardCount', '')) + "\t" + str(rows.get('collectionStatus', '')) + "\t" + str(rows.get('role', '')) + "\t" + str(rows.get('location', '')) + "\t" + str(rows.get('type', '')) + "\t" + str(rows.get('lineCardId', '')) + "\t" + str(rows.get('apManagerInterfaceIp', '')) + "\t" + str(rows.get('lastUpdated', '')) + "\t" + str(rows.get('managementIpAddress', '')) + "\t" + str(rows.get('serialNumber', '')) + "\t" + str(rows.get('inventoryStatusDetail', '')) + "\t" + str(rows.get('softwareVersion', '')) + "\t" + str(rows.get('snmpLocation', '')) + "\t" + str(rows.get('tagCount', ''))		

Example 1 Output — Ugly, but it will pull into Excel in a clean way

macAddress	upTime	bootDateTime	lastUpdateTime	snmpContact	platformId	series	errorCode	interfaceCount	id	locationName	instanceUuid	family	reachabilityFailureReason	reachabilityStatus	hostname	memorySize	roleSource	lineCardCount	collectionStatus	role	location	type	lineCardId	apManagerInterfaceIp	lastUpdated	managementIpAddress	serialNumber	inventoryStatusDetail	softwareVersion	snmpLocation	tagCount
54:78:1a:8e:28:c0	16:11:38.75	2016-11-18 07:54:31	1485632961022		WS-C2960C-8PC-L	Cisco Catalyst 2960-C Series Switches	DEV-UNREACHED	11	8dbd8068-1091-4cde-8cf5-d1b58dc5c9c7	None	8dbd8068-1091-4cde-8cf5-d1b58dc5c9c7	Switches and Hubs	SNMP Connectivity Failed	Unreachable	AHEC-2960C1	59556224	AUTO	1	Partial Collection FailureACCESS	None	Cisco Catalyst 2960C-8PC-L Switch	b77af8c6-bfa2-4388-9cbc-5e36f72090bf		2017-01-28 19:49:21	165.10.1.39	FOC1637Y3FJ		15.2(2.5.72)E4		0
68:bc:0c:63:4a:b0	None	None	1485632966239		AIR-CAP3502I-A-K9	Cisco 3500I Series Unified Access Points	null	None	cd6d9b24-839b-4d58-adfe-3fdf781e1782	None	cd6d9b24-839b-4d58-adfe-3fdf781e1782	Unified AP	NA	Reachable	AP7081.059f.19ca	NA	AUTO	None	Managed	ACCESS	None	Cisco 3500I Unified Access Point	None	10.1.14.2	2017-01-28 19:49:26	10.1.14.3	FGL1548S2YF	NA	8.1.14.16	default location	0
64:a0:e7:d4:9b:c1	219 days, 21:09:28.84	2016-04-13 03:02:46	1485632966387		WS-C2960S-48LPS-L	Cisco Catalyst 2960 Series Switches	DEV-UNREACHED	55	26450a30-57d8-4b56-b8f1-6fc535d67645	None	26450a30-57d8-4b56-b8f1-6fc535d67645	Switches and Hubs	SNMP Connectivity Failed	Unreachable	Branch-Access1	73969000	AUTO	1	Partial Collection Failure	ACCESS	None	Cisco Catalyst 29xx Stack-able Ethernet Switch	cbfd87b9-a8a2-48a8-8296-2947dbf81de8		2017-01-28 19:49:26	10.2.1.17	FOC1537W1ZY		12.2(55)SE3		0
7c:0e:ce:9f:3c:d9	174 days, 23:37:05.56	2016-05-28 00:31:46	1485632959545		CISCO2911/K9	Cisco 2900 Series Integrated Services Routers G2	DEV-UNREACHED	7	0dd240fd-5cca-4774-a801-9f1c04edcc70	None	0dd240fd-5cca-4774-a801-9f1c04edcc70	Routers	SNMP Connectivity Failed	Unreachable	Branch-Router1	300214800	AUTO	1	Partial Collection Failure	BORDER ROUTER	None	Cisco 2911 Integrated Services Router G2	b913fc7b-ecda-4e8d-b96b-c46cc82cd6a8		2017-01-28 19:49:19	10.2.2.1	FTX1840ALC1		15.2(4)M6a		1
f0:7f:06:bb:dc:81	174 days, 23:49:53.28	2016-05-28 00:29:46	1485632960137		CISCO2911/K9	Cisco 2900 Series Integrated Services Routers G2	DEV-UNREACHED	7	6ce631db-9212-4587-867f-b8f3aed1702d	None	6ce631db-9212-4587-867f-b8f3aed1702d	Routers	SNMP Connectivity Failed	Unreachable	Branch-Router2	300246160	AUTO	1	Partial Collection Failure	BORDER ROUTER	None	Cisco 2911 Integrated Services Router G2	94da60d6-5fc3-4613-bce7-7bc03e517647		2017-01-28 19:49:20	10.2.2.2	FTX1840ALBY		15.2(4)M6a		1
None	354 days, 0:18:52.75	2015-11-30 23:53:04	1485633627531		CISCO2911/K9	Cisco 2900 Series Integrated Services Routers G2	DEV-UNREACHED	8	d337811b-d371-444c-a49f-9e2791f955b4	None	d337811b-d371-444c-a49f-9e2791f955b4	Routers	SNMP Connectivity Failed	Unreachable	Branch2-Router.yourdomain.com	300217616	AUTO	1	Partial Collection Failure	BORDER ROUTER	None	Cisco 2911 Integrated Services Router G2	41a03d20-585c-4158-b5da-89125e592148		2017-01-28 20:00:27	218.1.100.100	FTX1840ALC0		15.2(4)M6a		0
f0:29:29:5c:30:e2	175 days, 0:00:54.84	2016-05-28 00:04:15	1485632966302		WS-C3850-48U	Cisco Catalyst 3850 Series Ethernet Stackable Switch	DEV-UNREACHED	62	5b5ea8da-8c23-486a-b95e-7429684d25fc	None	5b5ea8da-8c23-486a-b95e-7429684d25fc	Switches and Hubs	SNMP Connectivity Failed	Unreachable	CAMPUS-Access1	536870912	AUTO	2	Partial Collection Failure	ACCESS	None	Cisco Catalyst 3850-48U-E Switch	4e3ad63f-36ac-4807-b2a9-e377ffc3b503, 2afe376a-b613-4695-82ba-3ed0030c37e6		2017-01-28 19:49:26	10.1.12.1	FOC1703V36B		03.06.04.E		1
24:e9:b3:3f:b1:80	109 days, 8:08:47.24	2016-08-01 15:55:13	1485632961077		WS-C6503-E	Cisco Catalyst 6500 Series Switches	DEV-UNREACHED	56	30d39b18-9ada-4148-ad6c-2ee20975b845	None	30d39b18-9ada-4148-ad6c-2ee20975b845	Switches and Hubs	SNMP Connectivity Failed	Unreachable	CAMPUS-Core1	1397215264	MANUAL	14	Partial Collection Failure	CORE	None	Cisco Catalyst 6503 Switch	99629b7d-b558-4776-8258-c9059507e6ba, 98ba3828-2464-4c02-b64f-c9106fe15574, d1e8a963-9851-4afb-a044-884343c870a3, d606ceea-b457-4f3f-8b70-c2f528387df6, cd773cbf-68fc-4ffb-8a12-3efb96ece3e9, 9e721ea9-2e17-47d4-8f8f-666653ae5998, d609b8e3-0409-4139-9075-7420b4a955f0, ce5bcbca-2cfc-4185-9f9d-8f3b59b0d49d, ee0865ba-b064-4b80-a59c-85d8d3959981, 0e074792-676d-42b8-aca3-dcbc40a0f349, 3578a12a-0746-4052-9380-3bea11fc6563, 5e5ade87-3f2b-4738-847f-920420ec8f0d, 2e1ee55f-7f7a-437e-b0c9-9eafeefc71f8, 129d546d-f482-4af0-83a9-8ddfd670c75d		2017-01-28 19:49:21	10.1.7.1	FXS1825Q1PA		15.2(1)SY1		0
24:e9:b3:3f:b1:c0	226 days, 22:38:02.60	2016-04-06 01:25:14	1485632960257		WS-C6503-E	Cisco Catalyst 6500 Series Switches	DEV-UNREACHED	56	1b329f52-95eb-44ad-9314-55932162ab86	None	1b329f52-95eb-44ad-9314-55932162ab86	Switches and Hubs	SNMP Connectivity Failed	Unreachable	CAMPUS-Core2	1397215264	MANUAL	13	Partial Collection Failure	CORE	None	Cisco Catalyst 6503 Switch	e9c05ad5-7927-47ed-a7bb-00dca4c6bc3a, 282a822c-2429-45fe-93c8-95471e2235a0, 9e4bd7b4-d275-427d-8939-8b777f473201, 4559fa5e-4014-4968-b836-5e57da42e401, fd0ef08b-1e15-4b4f-b2e5-e4c1c0e0359b, a1ac1cf6-6cb3-4705-b2d3-60413eea2f0f, e7d236a7-c4d9-45da-9bda-d8af714495f2, da5cf7c8-ee9e-44d4-afb9-b78a51c30292, 1872a219-ad03-47a4-971b-d200ef1aae66, 7bdf020a-8a5b-41c3-af97-7eafdd6f88fb, bcda7746-cd09-4a28-8c6b-74e1551f423d, 6c83d6e1-c2b4-41a6-9aab-b9c8714b81b5, 6ce7447b-b310-472d-9307-7696ba81c7c8		2017-01-28 19:49:20	10.1.10.1	FXS1825Q1P8	15.2(1)SY1		0
None	115 days, 19:22:08.43	2016-07-26 04:42:13	1485632960980		WS-C4507R+E	Cisco Catalyst 4500 Series Switches	DEV-UNREACHED	60	c8ed3e49-5eeb-4dee-b120-edeb179c8394	None	c8ed3e49-5eeb-4dee-b120-edeb179c8394	Switches and Hubs	SNMP Connectivity Failed	Unreachable	CAMPUS-Dist1	805306368	AUTO	2	Partial Collection Failure	DISTRIBUTION	None	Cisco Catalyst 4507R plus E Switch	6ed4dd13-e223-4296-9ac2-5dc1a6e676ae, c1eb9026-abde-4461-abb3-7d1cda5b0927		2017-01-28 19:49:20	10.255.1.5	FOX1524GV2Z	03.02.00.XO		0
30:e4:db:25:75:3f	367 days, 9:12:46.03	2015-11-17 14:52:13	1485632959565		WS-C4507R+E	Cisco Catalyst 4500 Series Switches	DEV-UNREACHED	56	4af8bf34-295f-46f4-97b7-0a2d2ea4cf22	None	4af8bf34-295f-46f4-97b7-0a2d2ea4cf22	Switches and Hubs	SNMP Connectivity Failed	Unreachable	CAMPUS-Dist2	805306368	AUTO	2	Partial Collection Failure	DISTRIBUTION	None	Cisco Catalyst 4507R plus E Switch	775a85cf-923a-4848-b608-a20684a67e9d, 2e7fb0e0-ca4a-4c6d-b492-06437878d6e1		2017-01-28 19:49:19	10.1.11.1	FOX1525G5S1		03.04.00.SG		0
f4:4e:05:cf:2e:30	220 days, 0:03:46.17	2016-04-13 00:03:13	1485632960944		ISR4451-X/K9	Cisco 4400 Series Integrated Services Routers	DEV-UNREACHED	6	9712ab62-6140-43fd-b1ee-1b07d1fb67d7	None	9712ab62-6140-43fd-b1ee-1b07d1fb67d7	Routers	SNMP Connectivity Failed	Unreachable	CAMPUS-Router1	1728629512	AUTO	6	Partial Collection FailureBORDER ROUTER	None	Cisco 4451 Series Integrated Services Router	f84f1b1f-7341-417e-8ab9-6037cb2dff09, daa0ceb6-0383-4a22-829a-8337d4811456, 382dcfe9-971d-4109-bf59-6f9788c4333e, 03390efc-103d-4220-a540-2074c6829485, 4de117cc-badd-46ac-a747-5b14fd83d59e, 596fd2a8-7e3d-4b3c-ad0a-ffbb82122648		2017-01-28 19:49:20	10.1.2.1	FTX1842AHM2		15.4(3)S		0
f4:4e:05:cf:2f:e0	445 days, 18:58:51.74	2015-08-31 05:08:13	1485632959637		ISR4451-X/K9	Cisco 4400 Series Integrated Services Routers	DEV-UNREACHED	6	55450140-de19-47b5-ae80-bfd741b23fd9	None	55450140-de19-47b5-ae80-bfd741b23fd9	Routers	SNMP Connectivity Failed	Unreachable	CAMPUS-Router2	1728629512	AUTO	6	Partial Collection FailureBORDER ROUTER	None	Cisco 4451 Series Integrated Services Router	5160d894-c2b8-4839-9600-d380867e5419, 85fb9da6-b2c0-4442-881e-d92c64fc091e, 6431ea3b-7707-4c36-a06d-4ca318d27ae0, ac440c1e-be74-491a-99d1-2dcf97f5053e, a030671f-8489-4e78-9145-f2c67873642d, 1cf7b911-1ee3-463a-a0aa-a1d42ed3aeb0		2017-01-28 19:49:19	10.1.4.2	FTX1842AHM1		15.4(3)S		0
a4:93:4c:fb:a2:e0	497 days, 2:27:52.95	2015-07-10 04:27:22	1485632966239		AIR-CT5508-K9	Cisco 5500 Series Wireless LAN Controllers	DEV-UNREACHED	12	ae19cd21-1b26-4f58-8ccd-d265deabb6c3	None	ae19cd21-1b26-4f58-8ccd-d265deabb6c3	Wireless Controller	SNMP Connectivity Failed	Unreachable	Campus-WLC-5508	1025646592	AUTO	None	Partial Collection Failure	ACCESS	None	Cisco 5508 Wireless LAN Controller	None		2017-01-28 19:49:26	10.1.14.2	FCW1630L0JG		8.1.14.16		0

Example 2 – apic-em_host_tab.py (extracts host devices into Tab format)–Comments Explain Function

###Written by Paul Stewart @packetu
###http://www.packetu.com
###Use of script constitutes acceptance of risk
###modify and distribute freely
### usage -- python apic-em_dev_txt.py >> tab_text.txt

import requests
import json
import sys
import getpass

####variables####
###default config is devnet sandbox

apic_base = "https://devnetapi.cisco.com/sandbox/apic_em"

ticket_url = apic_base + "/api/v1/ticket"
host_url = apic_base + "/api/v1/host"
scope = 'all'

###authenication method-uncomment one of the following###

###stored credentials####
username = "devnetuser"
password = "Cisco123!"

###prompt for credentials####
#username = raw_input("Please enter username: ")
#password = getpass.getpass(prompt="Please enter password: ")


#########
# log in and get a service ticket 
# store in resp

tck_data = "{\n\"username\":\"" + username + "\",\n\"password\":\"" + password + "\"\n}\n"
tck_head = {
    'content-type': "application/json",
    'cache-control': "no-cache",
    }
tck_resp = requests.request("POST", ticket_url, data=tck_data, headers=tck_head)

tck_resp = json.loads(tck_resp.text)
tck_resp = tck_resp["response"]
tck_resp = tck_resp['serviceTicket']

########
#use service ticket in tck_resp to 
#Connect and pull list of hosts


hst_head = {
    'scope': scope,
    'x-auth-token': tck_resp,
    'cache-control': "no-cache",
    }
    
hst_resp = requests.request("GET", host_url, headers=hst_head)

myhosts = json.loads(hst_resp.text)


####
#print myhosts as
#comma delimited 
#using .get to fill any null 
#values and prevent key errors
#modify to your required output

print "hostMac\tlastUpdated\tsource\tconnectedAPName\tconnectedNetworkDeviceIpAddress\thostType\tid\tconnectedAPMacAddress\tsubType\tvlanId\thostIp\tconnectedNetworkDeviceId\tpointOfAttachment\tpointOfPresence"
if myhosts['response'] == []:
    print 'No Data!'
else:
    for rows in myhosts['response']:
        print str(rows.get('hostMac', '')) + "\t" + str(rows.get('lastUpdated', '')) + "\t" + str(rows.get('source', '')) + "\t" + str(rows.get('connectedAPName', '')) + "\t" + str(rows.get('connectedNetworkDeviceIpAddress', '')) + "\t" + str(rows.get('hostType', '')) + "\t" + str(rows.get('id', '')) + "\t" + str(rows.get('connectedAPMacAddress', '')) + "\t" + str(rows.get('subType', '')) + "\t" + str(rows.get('vlanId', '')) + "\t" + str(rows.get('hostIp', '')) + "\t" + str(rows.get('connectedNetworkDeviceId', '')) + "\t" + str(rows.get('pointOfAttachment', '')) + "\t" + str(rows.get('pointOfPresence', ''))
          

Example 2 Output — Ugly, but it will pull into Excel in a clean way

hostMac	lastUpdated	source	connectedAPName	connectedNetworkDeviceIpAddress	hostType	id	connectedAPMacAddress	subType	vlanId	hostIp	connectedNetworkDeviceId	pointOfAttachment	pointOfPresence
00:24:d7:43:59:d8	1479514114932	200	AP7081.059f.19ca	10.1.14.3	wireless	48cdeb9b-b412-491e-a80c-7ec5bbe98167	68:bc:0c:63:4a:b0	UNKNOWN	600	10.1.15.117	cd6d9b24-839b-4d58-adfe-3fdf781e1782	ae19cd21-1b26-4f58-8ccd-d265deabb6c3	ae19cd21-1b26-4f58-8ccd-d265deabb6c3
5c:f9:dd:52:07:78	1479514299803	200		10.2.1.17	wired	f624d4f3-0ab9-4ae3-b09d-62051edbd8f3		UNKNOWN	200	10.2.1.22	26450a30-57d8-4b56-b8f1-6fc535d67645
e8:9a:8f:7a:22:99	1479513914455	200		10.1.12.1	wired	572d4065-abd8-4b97-bfc3-ab5ee13f6c08		UNKNOWN	200	10.1.12.20	5b5ea8da-8c23-486a-b95e-7429684d25fc

All of this, as well as an XLSX example, is downloadable and is free to use and share. My recommendation would be to use this as a starting point and not a final product. Tweak it as necessary to make it better and more useful for your specific use cases.

Conclusion

API’s are the future and an API first approach is really advantageous for all of us. Most of this stuff isn’t complex and can be understood in a short amount of time. There’s a ton of Python examples on the internet. By combining those examples with a little bit of conceptual understanding, one can easily get to the data necessary. Moreover, the process will lead to a path where automation and orchestration can be the norm.

Disclaimer: This article includes the independent thoughts, opinions, commentary or technical detail of Paul Stewart. This may or may does not reflect the position of past, present or future employers.

No related content found.

About Paul Stewart, CCIE 26009 (Security)

Paul is a Network and Security Engineer, Trainer and Blogger who enjoys understanding how things really work. With over 15 years of experience in the technology industry, Paul has helped many organizations build, maintain and secure their networks and systems.
This entry was posted in Uncategorized. Bookmark the permalink.

Leave a Reply