Bulk Import vSphere dvPortGroups to phpIPAM

I recently wrote about getting started with VMware's Tanzu Community Edition and deploying phpIPAM as my first real-world Kubernetes workload. Well I've spent much of my time since then working on a script which would help to populate my phpIPAM instance with a list of networks to monitor.

Planning and Exporting

The first step in making this work was to figure out which networks I wanted to import. We've got hundreds of different networks in use across our production vSphere environments. I focused only on those which are portgroups on distributed virtual switches since those configurations are pretty standardized (being vCenter constructs instead of configured on individual hosts). These dvPortGroups bear a naming standard which conveys all sorts of useful information, and it's easy and safe to rename any dvPortGroups which don't fit the standard (unlike renaming portgroups on a standard virtual switch).

The standard naming convention is [Site/Description] [Network Address]{/[Mask]}. So the networks (across two virtual datacenters and two dvSwitches) look something like this:

Production dvPortGroups approximated in my testing lab environment

Some networks have masks in the name, some don't; and some use an underscore (_) rather than a slash (/) to separate the network from the mask . Most networks correctly include the network address with a 0 in the last octet, but some use an x instead. And the VLANs associated with the networks have a varying number of digits. Consistency can be difficult so these are all things that I had to keep in mind as I worked on a solution which would make a true best effort at importing all of these.

As long as the dvPortGroup names stick to this format I can parse the name to come up with a description as well as the IP space of the network. The dvPortGroup also carries information about the associated VLAN, which is useful information to have. And I can easily export this information with a simple PowerCLI query:

 1PS /home/john> get-vdportgroup | select Name, VlanConfiguration
 2
 3Name                           VlanConfiguration
 4----                           -----------------
 5MGT-Home 192.168.1.0
 6MGT-Servers 172.16.10.0        VLAN 1610
 7BOW-Servers 172.16.20.0        VLAN 1620
 8BOW-Servers 172.16.30.0        VLAN 1630
 9BOW-Servers 172.16.40.0        VLAN 1640
10DRE-Servers 172.16.50.0        VLAN 1650
11DRE-Servers 172.16.60.x        VLAN 1660
12VPOT8-Mgmt 172.20.10.0/27      VLAN 20
13VPOT8-Servers 172.20.10.32/27  VLAN 30
14VPOT8-Servers 172.20.10.64_26  VLAN 40

In my homelab, I only have a single vCenter. In production, we've got a handful of vCenters, and each manages the hosts in a given region. So I can use information about which vCenter hosts a dvPortGroup to figure out which region a network is in. When I import this data into phpIPAM, I can use the vCenter name to assign remote scan agents to networks based on the region that they're in. I can also grab information about which virtual datacenter a dvPortGroup lives in, which I'll use for grouping networks into sites or sections.

The vCenter can be found in the Uid property returned by get-vdportgroup:

 1PS /home/john> get-vdportgroup | select Name, VlanConfiguration, Datacenter, Uid
 2
 3Name                     VlanConfiguration   Datacenter Uid
 4----                     -----------------   ---------- ---
 5MGT-Home 192.168.1.0                         Lab        /VIServer=lab\john@vcsa.lab.bowdre.net:443/DistributedPortgroup=DistributedVirtualPortgroup-dvportgroup-27015/
 6MGT-Servers 172.16.10.0  VLAN 1610           Lab        /VIServer=lab\john@vcsa.lab.bowdre.net:443/DistributedPortgroup=DistributedVirtualPortgroup-dvportgroup-27017/
 7BOW-Servers 172.16.20.0  VLAN 1620           Lab        /VIServer=lab\john@vcsa.lab.bowdre.net:443/DistributedPortgroup=DistributedVirtualPortgroup-dvportgroup-28010/
 8BOW-Servers 172.16.30.0  VLAN 1630           Lab        /VIServer=lab\john@vcsa.lab.bowdre.net:443/DistributedPortgroup=DistributedVirtualPortgroup-dvportgroup-28011/
 9BOW-Servers 172.16.40.0  VLAN 1640           Lab        /VIServer=lab\john@vcsa.lab.bowdre.net:443/DistributedPortgroup=DistributedVirtualPortgroup-dvportgroup-28012/
10DRE-Servers 172.16.50.0  VLAN 1650           Lab        /VIServer=lab\john@vcsa.lab.bowdre.net:443/DistributedPortgroup=DistributedVirtualPortgroup-dvportgroup-28013/
11DRE-Servers 172.16.60.x  VLAN 1660           Lab        /VIServer=lab\john@vcsa.lab.bowdre.net:443/DistributedPortgroup=DistributedVirtualPortgroup-dvportgroup-28014/
12VPOT8-Mgmt 172.20.10.0/ VLAN 20             Other Lab  /VIServer=lab\john@vcsa.lab.bowdre.net:443/DistributedPortgroup=DistributedVirtualPortgroup-dvportgroup-35018/
13VPOT8-Servers 172.20.10 VLAN 30             Other Lab  /VIServer=lab\john@vcsa.lab.bowdre.net:443/DistributedPortgroup=DistributedVirtualPortgroup-dvportgroup-35019/
14VPOT8-Servers 172.20.10 VLAN 40             Other Lab  /VIServer=lab\john@vcsa.lab.bowdre.net:443/DistributedPortgroup=DistributedVirtualPortgroup-dvportgroup-35020/

It's not pretty, but it'll do the trick. All that's left is to export this data into a handy-dandy CSV-formatted file that I can easily parse for import:

1get-vdportgroup | select Name, VlanConfiguration, Datacenter, Uid | export-csv -NoTypeInformation ./networks.csv

My networks.csv export, including the networks which don't match the naming criteria and will be skipped by the import process.

Setting up phpIPAM

After deploying a fresh phpIPAM instance on my Tanzu Community Edition Kubernetes cluster, there are a few additional steps needed to enable API access. To start, I log in to my phpIPAM instance and navigate to the Administration > Server Management > phpIPAM Settings page, where I enabled both the Prettify links and API feature settings - making sure to hit the Save button at the bottom of the page once I do so.

Enabling the API

Then I need to head to the User Management page to create a new user that will be used to authenticate against the API:

New user creation

And finally, I head to the API section to create a new API key with Read/Write permissions:

API key creation

I'm also going to head in to Administration > IP Related Management > Sections and delete the default sample sections so that the inventory will be nice and empty:

We don't need no stinkin' sections!

Script time

Well that's enough prep work; now it's time for the Python3 script:

  1# The latest version of this script can be found on Github:
  2# https://github.com/jbowdre/misc-scripts/blob/main/Python/phpipam-bulk-import.py
  3
  4import requests
  5from collections import namedtuple
  6
  7check_cert = True
  8created = 0
  9remote_agent = False
 10name_to_id = namedtuple('name_to_id', ['name', 'id'])
 11
 12## for testing only:
 13# from requests.packages.urllib3.exceptions import InsecureRequestWarning
 14# requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
 15# check_cert = False
 16
 17## Makes sure input fields aren't blank.
 18def validate_input_is_not_empty(field, prompt):
 19  while True:
 20    user_input = input(f'\n{prompt}:\n')
 21    if len(user_input) == 0:
 22      print(f'[ERROR] {field} cannot be empty!')
 23      continue
 24    else:
 25      return user_input
 26
 27
 28## Takes in a list of dictionary items, extracts all the unique values for a given key,
 29#  and returns a sorted list of those.
 30def get_sorted_list_of_unique_values(key, list_of_dict):
 31  valueSet = set(sub[key] for sub in list_of_dict)
 32  valueList = list(valueSet)
 33  valueList.sort()
 34  return valueList
 35
 36
 37## Match names and IDs
 38def get_id_from_sets(name, sets):
 39  return [item.id for item in sets if name == item.name][0]
 40
 41
 42## Authenticate to phpIPAM endpoint and return an auth token
 43def auth_session(uri, auth):
 44  print(f'Authenticating to {uri}...')
 45  try:
 46    req = requests.post(f'{uri}/user/', auth=auth, verify=check_cert)
 47  except:
 48    raise requests.exceptions.RequestException
 49  if req.status_code != 200:
 50    print(f'[ERROR] Authentication failure: {req.json()}')
 51    raise requests.exceptions.RequestException
 52  token = {"token": req.json()['data']['token']}
 53  print('\n[AUTH_SUCCESS] Authenticated successfully!')
 54  return token
 55
 56
 57## Find or create a remote scan agent for each region (vcenter)
 58def get_agent_sets(uri, token, regions):
 59  agent_sets = []
 60
 61  def create_agent_set(uri, token, name):
 62    import secrets
 63    # generate a random secret to be used for identifying this agent
 64    payload = {
 65      'name': name,
 66      'type': 'mysql',
 67      'code': secrets.base64.urlsafe_b64encode(secrets.token_bytes(24)).decode("utf-8"),
 68      'description': f'Remote scan agent for region {name}'
 69    }
 70    req = requests.post(f'{uri}/tools/scanagents/', data=payload, headers=token, verify=check_cert)
 71    id = req.json()['id']
 72    agent_set = name_to_id(name, id)
 73    print(f'[AGENT_CREATE] {name} created.')
 74    return agent_set
 75
 76  for region in regions:
 77    name = regions[region]['name']
 78    req = requests.get(f'{uri}/tools/scanagents/?filter_by=name&filter_value={name}', headers=token, verify=check_cert)
 79    if req.status_code == 200:
 80      id = req.json()['data'][0]['id']
 81      agent_set = name_to_id(name, id)
 82    else:
 83      agent_set = create_agent_set(uri, token, name)
 84    agent_sets.append(agent_set)
 85  return agent_sets
 86
 87
 88## Find or create a section for each virtual datacenter
 89def get_section(uri, token, section, parentSectionId):
 90
 91  def create_section(uri, token, section, parentSectionId):
 92    payload = {
 93      'name': section,
 94      'masterSection': parentSectionId,
 95      'permissions': '{"2":"2"}',
 96      'showVLAN': '1'
 97    }
 98    req = requests.post(f'{uri}/sections/', data=payload, headers=token, verify=check_cert)
 99    id = req.json()['id']
100    print(f'[SECTION_CREATE] Section {section} created.')
101    return id
102
103  req = requests.get(f'{uri}/sections/{section}/', headers=token, verify=check_cert)
104  if req.status_code == 200:
105    id = req.json()['data']['id']
106  else:
107    id = create_section(uri, token, section, parentSectionId)
108  return id
109
110
111## Find or create VLANs
112def get_vlan_sets(uri, token, vlans):
113  vlan_sets = []
114
115  def create_vlan_set(uri, token, vlan):
116    payload = {
117      'name': f'VLAN {vlan}',
118      'number': vlan
119    }
120    req = requests.post(f'{uri}/vlan/', data=payload, headers=token, verify=check_cert)
121    id = req.json()['id']
122    vlan_set = name_to_id(vlan, id)
123    print(f'[VLAN_CREATE] VLAN {vlan} created.')
124    return vlan_set
125
126  for vlan in vlans:
127    if vlan != 0:
128      req = requests.get(f'{uri}/vlan/?filter_by=number&filter_value={vlan}', headers=token, verify=check_cert)
129      if req.status_code == 200:
130        id = req.json()['data'][0]['vlanId']
131        vlan_set = name_to_id(vlan, id)
132      else:
133        vlan_set = create_vlan_set(uri, token, vlan)
134      vlan_sets.append(vlan_set)
135  return vlan_sets
136
137
138## Find or create nameserver configurations for each region
139def get_nameserver_sets(uri, token, regions):
140
141  nameserver_sets = []
142
143  def create_nameserver_set(uri, token, name, nameservers):
144    payload = {
145      'name': name,
146      'namesrv1': nameservers,
147      'description': f'Nameserver created for region {name}'
148    }
149    req = requests.post(f'{uri}/tools/nameservers/', data=payload, headers=token, verify=check_cert)
150    id = req.json()['id']
151    nameserver_set = name_to_id(name, id)
152    print(f'[NAMESERVER_CREATE] Nameserver {name} created.')
153    return nameserver_set
154
155  for region in regions:
156    name = regions[region]['name']
157    req = requests.get(f'{uri}/tools/nameservers/?filter_by=name&filter_value={name}', headers=token, verify=check_cert)
158    if req.status_code == 200:
159      id = req.json()['data'][0]['id']
160      nameserver_set = name_to_id(name, id)
161    else:
162      nameserver_set = create_nameserver_set(uri, token, name, regions[region]['nameservers'])
163    nameserver_sets.append(nameserver_set)
164  return nameserver_sets
165
166
167## Find or create subnet for each dvPortGroup
168def create_subnet(uri, token, network):
169
170  def update_nameserver_permissions(uri, token, network):
171    nameserverId = network['nameserverId']
172    sectionId = network['sectionId']
173    req = requests.get(f'{uri}/tools/nameservers/{nameserverId}/', headers=token, verify=check_cert)
174    permissions = req.json()['data']['permissions']
175    permissions = str(permissions).split(';')
176    if not sectionId in permissions:
177      permissions.append(sectionId)
178      if 'None' in permissions:
179        permissions.remove('None')
180      permissions = ';'.join(permissions)
181      payload = {
182        'permissions': permissions
183      }
184      req = requests.patch(f'{uri}/tools/nameservers/{nameserverId}/', data=payload, headers=token, verify=check_cert)
185
186  payload = {
187    'subnet': network['subnet'],
188    'mask': network['mask'],
189    'description': network['name'],
190    'sectionId': network['sectionId'],
191    'scanAgent': network['agentId'],
192    'nameserverId': network['nameserverId'],
193    'vlanId': network['vlanId'],
194    'pingSubnet': '1',
195    'discoverSubnet': '1',
196    'resolveDNS': '1',
197    'DNSrecords': '1'
198  }
199  req = requests.post(f'{uri}/subnets/', data=payload, headers=token, verify=check_cert)
200  if req.status_code == 201:
201    network['subnetId'] = req.json()['id']
202    update_nameserver_permissions(uri, token, network)
203    print(f"[SUBNET_CREATE] Created subnet {req.json()['data']}")
204    global created
205    created += 1
206  elif req.status_code == 409:
207    print(f"[SUBNET_EXISTS] Subnet {network['subnet']}/{network['mask']} already exists.")
208  else:
209    print(f"[ERROR] Problem creating subnet {network['name']}: {req.json()}")
210
211
212## Import list of networks from the specified CSV file
213def import_networks(filepath):
214  print(f'Importing networks from {filepath}...')
215  import csv
216  import re
217  ipPattern = re.compile('\d{1,3}\.\d{1,3}\.\d{1,3}\.[0-9xX]{1,3}')
218  networks = []
219  with open(filepath) as csv_file:
220    reader = csv.DictReader(csv_file)
221    line_count = 0
222    for row in reader:
223      network = {}
224      if line_count > 0:
225        if(re.search(ipPattern, row['Name'])):
226          network['subnet'] = re.findall(ipPattern, row['Name'])[0]
227          if network['subnet'].split('.')[-1].lower() == 'x':
228            network['subnet'] = network['subnet'].lower().replace('x', '0')
229          network['name'] = row['Name']
230          if '/' in row['Name'][-3]:
231            network['mask'] = row['Name'].split('/')[-1]
232          elif '_' in row['Name'][-3]:
233            network['mask'] = row['Name'].split('_')[-1]
234          else:
235            network['mask'] = '24'
236          network['section'] = row['Datacenter']
237          try:
238            network['vlan'] = int(row['VlanConfiguration'].split('VLAN ')[1])
239          except:
240            network['vlan'] = 0
241          network['vcenter'] = f"{(row['Uid'].split('@'))[1].split(':')[0].split('.')[0]}"
242          networks.append(network)
243      line_count += 1
244    print(f'Processed {line_count} lines and found:')
245  return networks
246
247
248def main():
249  import socket
250  import getpass
251  import argparse
252  from pathlib import Path
253
254  parser = argparse.ArgumentParser()
255  parser.add_argument("filepath", type=Path)
256
257  # Accept CSV file as an argument to the script or prompt for input if necessary
258  try:
259    p = parser.parse_args()
260    filepath = p.filepath
261  except:
262    # make sure filepath is a path to an actual file
263    print("""\n\n
264    This script helps to add vSphere networks to phpIPAM for IP address management. It is expected
265    that the vSphere networks are configured as portgroups on distributed virtual switches and 
266    named like '[Description] [Subnet IP]{/[mask]}' (ex: 'LAB-Servers 192.168.1.0'). The following PowerCLI
267    command can be used to export the networks from vSphere:
268
269      Get-VDPortgroup | Select Name, Datacenter, VlanConfiguration, Uid | Export-Csv -NoTypeInformation ./networks.csv
270
271    Subnets added to phpIPAM will be automatically configured for monitoring either using the built-in
272    scan agent (default) or a new remote scan agent for each vCenter.
273    """)
274    while True:
275      filepath = Path(validate_input_is_not_empty('Filepath', 'Path to CSV-formatted export from vCenter'))
276      if filepath.exists():
277        break
278      else:
279        print(f'[ERROR] Unable to find file at {filepath.name}.')
280        continue
281  
282  # get collection of networks to import
283  networks = import_networks(filepath)
284  networkNames = get_sorted_list_of_unique_values('name', networks)
285  print(f'\n- {len(networkNames)} networks:\n\t{networkNames}')
286  vcenters = get_sorted_list_of_unique_values('vcenter', networks)
287  print(f'\n- {len(vcenters)} vCenter servers:\n\t{vcenters}')
288  vlans = get_sorted_list_of_unique_values('vlan', networks)
289  print(f'\n- {len(vlans)} VLANs:\n\t{vlans}')
290  sections = get_sorted_list_of_unique_values('section', networks)
291  print(f'\n- {len(sections)} Datacenters:\n\t{sections}')
292
293  regions = {}
294  for vcenter in vcenters:
295    nameservers = None
296    name = validate_input_is_not_empty('Region Name', f'Region name for vCenter {vcenter}')
297    for region in regions:
298      if name in regions[region]['name']:
299        nameservers = regions[region]['nameservers']
300    if not nameservers:
301      nameservers = validate_input_is_not_empty('Nameserver IPs', f"Comma-separated list of nameserver IPs in {name}")
302      nameservers = nameservers.replace(',',';').replace(' ','')
303    regions[vcenter] = {'name': name, 'nameservers': nameservers}
304
305  # make sure hostname resolves
306  while True:
307    hostname = input('\nFully-qualified domain name of the phpIPAM host:\n')
308    if len(hostname) == 0:
309      print('[ERROR] Hostname cannot be empty.')
310      continue
311    try:
312      test = socket.gethostbyname(hostname)
313    except:
314      print(f'[ERROR] Unable to resolve {hostname}.')
315      continue
316    else:
317      del test
318      break
319  
320  username = validate_input_is_not_empty('Username', f'Username with read/write access to {hostname}')
321  password = getpass.getpass(f'Password for {username}:\n')
322  apiAppId = validate_input_is_not_empty('App ID', f'App ID for API key (from https://{hostname}/administration/api/)')
323
324  agent = input('\nUse per-region remote scan agents instead of a single local scanner? (y/N):\n')
325  try:
326    if agent.lower()[0] == 'y':
327      global remote_agent
328      remote_agent = True
329  except:
330    pass
331
332  proceed = input(f'\n\nProceed with importing {len(networkNames)} networks to {hostname}? (y/N):\n')
333  try:
334    if proceed.lower()[0] == 'y':
335      pass
336    else:
337      import sys
338      sys.exit("Operation aborted.")
339  except:
340    import sys
341    sys.exit("Operation aborted.")
342  del proceed
343
344  # assemble variables
345  uri = f'https://{hostname}/api/{apiAppId}'
346  auth = (username, password)
347
348  # auth to phpIPAM
349  token = auth_session(uri, auth)
350
351  # create nameserver entries
352  nameserver_sets = get_nameserver_sets(uri, token, regions)
353  vlan_sets = get_vlan_sets(uri, token, vlans)
354  if remote_agent:
355    agent_sets = get_agent_sets(uri, token, regions)
356  
357  # create the networks
358  for network in networks:
359    network['region'] = regions[network['vcenter']]['name']
360    network['regionId'] = get_section(uri, token, network['region'], None)
361    network['nameserverId'] = get_id_from_sets(network['region'], nameserver_sets)
362    network['sectionId'] = get_section(uri, token, network['section'], network['regionId'])
363    if network['vlan'] == 0:
364      network['vlanId'] = None
365    else:
366      network['vlanId'] = get_id_from_sets(network['vlan'], vlan_sets) 
367    if remote_agent:
368      network['agentId'] = get_id_from_sets(network['region'], agent_sets)
369    else:
370      network['agentId'] = '1'
371    create_subnet(uri, token, network)
372
373  print(f'\n[FINISH] Created {created} of {len(networks)} networks.')
374
375
376if __name__ == "__main__":
377  main()

I'll run it and provide the path to the network export CSV file:

1python3 phpipam-bulk-import.py ~/networks.csv

The script will print out a little descriptive bit about what sort of networks it's going to try to import and then will straight away start processing the file to identify the networks, vCenters, VLANs, and datacenters which will be imported:

 1Importing networks from /home/john/networks.csv...
 2Processed 17 lines and found:
 3
 4- 10 networks:
 5        ['BOW-Servers 172.16.20.0', 'BOW-Servers 172.16.30.0', 'BOW-Servers 172.16.40.0', 'DRE-Servers 172.16.50.0', 'DRE-Servers 172.16.60.x', 'MGT-Home 192.168.1.0', 'MGT-Servers 172.16.10.0', 'VPOT8-Mgmt 172.20.10.0/27', 'VPOT8-Servers 172.20.10.32/27', 'VPOT8-Servers 172.20.10.64_26']
 6
 7- 1 vCenter servers:
 8        ['vcsa']
 9
10- 10 VLANs:
11        [0, 20, 30, 40, 1610, 1620, 1630, 1640, 1650, 1660]
12
13- 2 Datacenters:
14        ['Lab', 'Other Lab']

It then starts prompting for the additional details which will be needed:

 1Region name for vCenter vcsa:
 2Labby
 3
 4Comma-separated list of nameserver IPs in Lab vCenter:
 5192.168.1.5
 6
 7Fully-qualified domain name of the phpIPAM host:
 8ipam-k8s.lab.bowdre.net
 9
10Username with read/write access to ipam-k8s.lab.bowdre.net:
11api-user
12Password for api-user:
13
14
15App ID for API key (from https://ipam-k8s.lab.bowdre.net/administration/api/):
16api-user
17
18Use per-region remote scan agents instead of a single local scanner? (y/N):
19y

Up to this point, the script has only been processing data locally, getting things ready for talking to the phpIPAM API. But now, it prompts to confirm that we actually want to do the thing (yes please) and then gets to work:

 1Proceed with importing 10 networks to ipam-k8s.lab.bowdre.net? (y/N):
 2y
 3Authenticating to https://ipam-k8s.lab.bowdre.net/api/api-user...
 4
 5[AUTH_SUCCESS] Authenticated successfully!
 6[VLAN_CREATE] VLAN 20 created.
 7[VLAN_CREATE] VLAN 30 created.
 8[VLAN_CREATE] VLAN 40 created.
 9[VLAN_CREATE] VLAN 1610 created.
10[VLAN_CREATE] VLAN 1620 created.
11[VLAN_CREATE] VLAN 1630 created.
12[VLAN_CREATE] VLAN 1640 created.
13[VLAN_CREATE] VLAN 1650 created.
14[VLAN_CREATE] VLAN 1660 created.
15[SECTION_CREATE] Section Labby created.
16[SECTION_CREATE] Section Lab created.
17[SUBNET_CREATE] Created subnet 192.168.1.0/24
18[SUBNET_CREATE] Created subnet 172.16.10.0/24
19[SUBNET_CREATE] Created subnet 172.16.20.0/24
20[SUBNET_CREATE] Created subnet 172.16.30.0/24
21[SUBNET_CREATE] Created subnet 172.16.40.0/24
22[SUBNET_CREATE] Created subnet 172.16.50.0/24
23[SUBNET_CREATE] Created subnet 172.16.60.0/24
24[SECTION_CREATE] Section Other Lab created.
25[SUBNET_CREATE] Created subnet 172.20.10.0/27
26[SUBNET_CREATE] Created subnet 172.20.10.32/27
27[SUBNET_CREATE] Created subnet 172.20.10.64/26
28
29[FINISH] Created 10 of 10 networks.

Success! Now I can log in to my phpIPAM instance and check out my newly-imported subnets:

New subnets!

Even the one with the weird name formatting was parsed and imported correctly:

Subnet details

So now phpIPAM knows about the vSphere networks I care about, and it can keep track of which vLAN and nameservers go with which networks. Great! But it still isn't scanning or monitoring those networks, even though I told the script that I wanted to use a remote scan agent. And I can check in the Administration > Server management > Scan agents section of the phpIPAM interface to see my newly-created agent configuration.

New agent config

... but I haven't actually deployed an agent yet. I'll do that by following the same basic steps described here to spin up my phpipam-agent on Kubernetes, and I'll plug in that automagically-generated code for the IPAM_AGENT_KEY environment variable:

 1---
 2apiVersion: apps/v1
 3kind: Deployment
 4metadata:
 5  name: phpipam-agent
 6spec:
 7  selector:
 8    matchLabels:
 9      app: phpipam-agent
10  replicas: 1
11  template:
12    metadata:
13      labels:
14        app: phpipam-agent
15    spec:
16      containers:
17      - name: phpipam-agent
18        image: ghcr.io/jbowdre/phpipam-agent:latest
19        env:
20        - name: IPAM_DATABASE_HOST
21          value: "ipam-k8s.lab.bowdre.net"
22        - name: IPAM_DATABASE_NAME
23          value: "phpipam"
24        - name: IPAM_DATABASE_USER
25          value: "phpipam"
26        - name: IPAM_DATABASE_PASS
27          value: "VMware1!"
28        - name: IPAM_DATABASE_PORT
29          value: "3306"
30        - name: IPAM_AGENT_KEY
31          value: "CxtRbR81r1ojVL2epG90JaShxIUBl0bT"
32        - name: IPAM_SCAN_INTERVAL
33          value: "15m"
34        - name: IPAM_RESET_AUTODISCOVER
35          value: "false"
36        - name: IPAM_REMOVE_DHCP
37          value: "false"
38        - name: TZ
39          value: "UTC"

I kick it off with a kubectl apply command and check back a few minutes later (after the 15-minute interval defined in the above YAML) to see that it worked, the remote agent scanned like it was supposed to and is reporting IP status back to the phpIPAM database server:

Newly-discovered IPs

I think I've got some more tweaks to do with this environment (why isn't phpIPAM resolving hostnames despite the correct DNS servers getting configured?) but this at least demonstrates a successful proof-of-concept import thanks to my Python script. Sure, I only imported 10 networks here, but I feel like I'm ready to process the several hundred which are available in our production environment now.

And who knows, maybe this script will come in handy for someone else. Until next time!

More Scripts