Skip to content

Commit 6252682

Browse files
committed
Update for NetBox 4.0.7; Add more logging, error checking; Fix netbox-community#134, netbox-community#84 (update)
1 parent dda8ed8 commit 6252682

File tree

5 files changed

+183
-95
lines changed

5 files changed

+183
-95
lines changed

nb-dt-import.py

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
import pynetbox
66
from glob import glob
77
import os
8+
import sys
9+
import time
810

911
import settings
1012
from netbox_api import NetBox
@@ -15,15 +17,19 @@ def main():
1517
args = settings.args
1618

1719
netbox = NetBox(settings)
20+
settings.handle.log("-=-=-=-=- Starting operation -=-=-=-=-")
1821
files, vendors = settings.dtl_repo.get_devices(
1922
f'{settings.dtl_repo.repo_path}/device-types/', args.vendors)
2023

2124
settings.handle.log(f'{len(vendors)} Vendors Found')
2225
device_types = settings.dtl_repo.parse_files(files, slugs=args.slugs)
2326
settings.handle.log(f'{len(device_types)} Device-Types Found')
27+
settings.handle.log("Creating Manufacturers")
2428
netbox.create_manufacturers(vendors)
29+
settings.handle.log("Creating Device Types")
2530
netbox.create_device_types(device_types)
2631

32+
settings.handle.log("-=-=-=-=- Checking Modules -=-=-=-=-")
2733
if netbox.modules:
2834
settings.handle.log("Modules Enabled. Creating Modules...")
2935
files, vendors = settings.dtl_repo.get_devices(
@@ -39,16 +45,24 @@ def main():
3945
f'Script took {(datetime.now() - startTime)} to run')
4046
settings.handle.log(f'{netbox.counter["added"]} devices created')
4147
settings.handle.log(f'{netbox.counter["images"]} images uploaded')
42-
settings.handle.log(
43-
f'{netbox.counter["updated"]} interfaces/ports updated')
44-
settings.handle.log(
45-
f'{netbox.counter["manufacturer"]} manufacturers created')
48+
settings.handle.log(f'{netbox.counter["updated"]} interfaces/ports updated')
49+
settings.handle.log(f'{netbox.counter["manufacturer"]} manufacturers created')
4650
if settings.NETBOX_FEATURES['modules']:
47-
settings.handle.log(
48-
f'{netbox.counter["module_added"]} modules created')
49-
settings.handle.log(
50-
f'{netbox.counter["module_port_added"]} module interface / ports created')
51+
settings.handle.log(f'{netbox.counter["module_added"]} modules created')
52+
settings.handle.log(f'{netbox.counter["module_port_added"]} module interface / ports created')
53+
54+
settings.handle.log(f'{netbox.counter["connection_errors"]} connection errors corrected')
55+
settings.handle.log("-=-=-=-=- Ending operation -=-=-=-=-")
56+
time.sleep(5)
5157

58+
# Uncomment the line below while troubleshooting to pause on completion
59+
#input("Debug pausing to review output. Press RETURN to close.")
60+
61+
def myexcepthook(type, value, traceback, oldhook=sys.excepthook):
62+
oldhook(type, value, traceback)
63+
input("Uncaught exception found. Press RETURN to continue execution.")
5264

5365
if __name__ == "__main__":
66+
# Uncomment the line below while troubleshooting to pause on uncaught exceptions
67+
#sys.excepthook = myexcepthook
5468
main()

netbox_api.py

Lines changed: 147 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
from collections import Counter
2+
import copy
3+
import time
4+
import http
5+
import http.client
26
import pynetbox
37
import requests
48
import os
@@ -17,12 +21,14 @@ def __init__(self, settings):
1721
module_added=0,
1822
module_port_added=0,
1923
images=0,
24+
connection_errors=0,
2025
)
2126
self.url = settings.NETBOX_URL
2227
self.token = settings.NETBOX_TOKEN
2328
self.handle = settings.handle
2429
self.netbox = None
2530
self.ignore_ssl = settings.IGNORE_SSL_ERRORS
31+
self.retry_delay = int(settings.RETRY_DELAY)
2632
self.modules = False
2733
self.connect_api()
2834
self.verify_compatibility()
@@ -80,65 +86,91 @@ def create_manufacturers(self, vendors):
8086
self.handle.verbose_log(f"Error during manufacturer creation. - {request_error.error}")
8187

8288
def create_device_types(self, device_types_to_add):
83-
for device_type in device_types_to_add:
84-
85-
# Remove file base path
86-
src_file = device_type["src"]
87-
del device_type["src"]
88-
89-
# Pre-process front/rear_image flag, remove it if present
90-
saved_images = {}
91-
image_base = os.path.dirname(src_file).replace("device-types","elevation-images")
92-
for i in ["front_image","rear_image"]:
93-
if i in device_type:
94-
if device_type[i]:
95-
image_glob = f"{image_base}/{device_type['slug']}.{i.split('_')[0]}.*"
96-
images = glob.glob(image_glob, recursive=False)
97-
if images:
98-
saved_images[i] = images[0]
99-
else:
100-
self.handle.log(f"Error locating image file using '{image_glob}'")
101-
del device_type[i]
89+
retry_amount = 2
90+
91+
# Treat the original data as immutable in case we encounter a connection error.
92+
for device_type_immutable in device_types_to_add:
93+
# In the event we hit a ConnectionReset error on this item, we want to retry it.
94+
# If it fails twice, assume it's an issue with the device_type
95+
retries = 0
96+
97+
while retries < retry_amount:
98+
device_type = copy.deepcopy(device_type_immutable) # Can this be a copy.copy(device_type_immutable)?
10299

103-
try:
104-
dt = self.device_types.existing_device_types[device_type["model"]]
105-
self.handle.verbose_log(f'Device Type Exists: {dt.manufacturer.name} - '
106-
+ f'{dt.model} - {dt.id}')
107-
except KeyError:
108100
try:
109-
dt = self.netbox.dcim.device_types.create(device_type)
110-
self.counter.update({'added': 1})
111-
self.handle.verbose_log(f'Device Type Created: {dt.manufacturer.name} - '
112-
+ f'{dt.model} - {dt.id}')
113-
except pynetbox.RequestError as e:
114-
self.handle.log(f'Error {e.error} creating device type:'
115-
f' {device_type["manufacturer"]["name"]} {device_type["model"]}')
101+
if retries == 0:
102+
self.handle.verbose_log(f'Processing Source File: {device_type["src"]}')
103+
else:
104+
self.handle.verbose_log(f'(Retry {retries}/{retry_amount}) Processing Source File: {device_type["src"]}')
105+
106+
# Remove file base path
107+
src_file = device_type["src"]
108+
del device_type["src"]
109+
110+
# Pre-process front/rear_image flag, remove it if present
111+
saved_images = {}
112+
image_base = os.path.dirname(src_file).replace("device-types","elevation-images")
113+
for i in ["front_image","rear_image"]:
114+
if i in device_type:
115+
if device_type[i]:
116+
image_glob = f"{image_base}/{device_type['slug']}.{i.split('_')[0]}.*"
117+
images = glob.glob(image_glob, recursive=False)
118+
if images:
119+
saved_images[i] = images[0]
120+
else:
121+
self.handle.log(f"Error locating image file using '{image_glob}'")
122+
del device_type[i]
123+
124+
try:
125+
dt = self.device_types.existing_device_types[device_type["model"]]
126+
self.handle.verbose_log(f'Device Type Exists: {dt.manufacturer.name} - {dt.model} - {dt.id}')
127+
except KeyError:
128+
try:
129+
dt = self.netbox.dcim.device_types.create(device_type)
130+
self.counter.update({'added': 1})
131+
self.handle.verbose_log(f'Device Type Created: {dt.manufacturer.name} - {dt.model} - {dt.id}')
132+
except pynetbox.RequestError as e:
133+
self.handle.log(f'Error {e.error} creating device type: {device_type["manufacturer"]["name"]} {device_type["model"]}')
134+
retries += 1
135+
continue
136+
137+
if "interfaces" in device_type:
138+
self.device_types.create_interfaces(device_type["interfaces"], dt.id)
139+
if "power-ports" in device_type:
140+
self.device_types.create_power_ports(device_type["power-ports"], dt.id)
141+
if "power-port" in device_type:
142+
self.device_types.create_power_ports(device_type["power-port"], dt.id)
143+
if "console-ports" in device_type:
144+
self.device_types.create_console_ports(device_type["console-ports"], dt.id)
145+
if "power-outlets" in device_type:
146+
self.device_types.create_power_outlets(device_type["power-outlets"], dt.id)
147+
if "console-server-ports" in device_type:
148+
self.device_types.create_console_server_ports(device_type["console-server-ports"], dt.id)
149+
if "rear-ports" in device_type:
150+
self.device_types.create_rear_ports(device_type["rear-ports"], dt.id)
151+
if "front-ports" in device_type:
152+
self.device_types.create_front_ports(device_type["front-ports"], dt.id)
153+
if "device-bays" in device_type:
154+
self.device_types.create_device_bays(device_type["device-bays"], dt.id)
155+
if self.modules and 'module-bays' in device_type:
156+
self.device_types.create_module_bays(device_type['module-bays'], dt.id)
157+
158+
# Finally, update images if any
159+
if saved_images:
160+
self.device_types.upload_images(self.url, self.token, saved_images, dt.id)
161+
162+
# We successfully processed the device. Don't retry it.
163+
retries = retry_amount
164+
except (http.client.RemoteDisconnected, requests.exceptions.ConnectionError) as e:
165+
retries += 1
166+
self.counter.update({'connection_errors': 1})
167+
self.handle.log(f'A connection error occurred (Count: {self.counter["connection_errors"]})! Waiting {self.retry_delay} seconds then retrying... Exception: {e}')
168+
169+
# As a connection error has just occurred, we should give the remote end a moment then reconnect.
170+
time.sleep(self.retry_delay)
171+
self.connect_api()
116172
continue
117173

118-
if "interfaces" in device_type:
119-
self.device_types.create_interfaces(device_type["interfaces"], dt.id)
120-
if "power-ports" in device_type:
121-
self.device_types.create_power_ports(device_type["power-ports"], dt.id)
122-
if "power-port" in device_type:
123-
self.device_types.create_power_ports(device_type["power-port"], dt.id)
124-
if "console-ports" in device_type:
125-
self.device_types.create_console_ports(device_type["console-ports"], dt.id)
126-
if "power-outlets" in device_type:
127-
self.device_types.create_power_outlets(device_type["power-outlets"], dt.id)
128-
if "console-server-ports" in device_type:
129-
self.device_types.create_console_server_ports(device_type["console-server-ports"], dt.id)
130-
if "rear-ports" in device_type:
131-
self.device_types.create_rear_ports(device_type["rear-ports"], dt.id)
132-
if "front-ports" in device_type:
133-
self.device_types.create_front_ports(device_type["front-ports"], dt.id)
134-
if "device-bays" in device_type:
135-
self.device_types.create_device_bays(device_type["device-bays"], dt.id)
136-
if self.modules and 'module-bays' in device_type:
137-
self.device_types.create_module_bays(device_type['module-bays'], dt.id)
138-
139-
# Finally, update images if any
140-
if saved_images:
141-
self.device_types.upload_images(self.url, self.token, saved_images, dt.id)
142174

143175
def create_module_types(self, module_types):
144176
all_module_types = {}
@@ -147,37 +179,63 @@ def create_module_types(self, module_types):
147179
all_module_types[curr_nb_mt.manufacturer.slug] = {}
148180

149181
all_module_types[curr_nb_mt.manufacturer.slug][curr_nb_mt.model] = curr_nb_mt
182+
183+
retry_amount = 2
184+
# Treat the original data as immutable in case we encounter a connection error.
185+
for curr_mt_immutable in module_types:
186+
# In the event we hit a ConnectionReset error on this item, we want to retry it.
187+
# If it fails twice, assume it's an issue with the device_type
188+
retries = 0
150189

190+
while retries < retry_amount:
191+
curr_mt = copy.deepcopy(curr_mt_immutable) # Can this be a copy.copy(curr_mt_immutable)?
151192

152-
for curr_mt in module_types:
153-
try:
154-
module_type_res = all_module_types[curr_mt['manufacturer']['slug']][curr_mt["model"]]
155-
self.handle.verbose_log(f'Module Type Exists: {module_type_res.manufacturer.name} - '
156-
+ f'{module_type_res.model} - {module_type_res.id}')
157-
except KeyError:
158193
try:
159-
module_type_res = self.netbox.dcim.module_types.create(curr_mt)
160-
self.counter.update({'module_added': 1})
161-
self.handle.verbose_log(f'Module Type Created: {module_type_res.manufacturer.name} - '
162-
+ f'{module_type_res.model} - {module_type_res.id}')
163-
except pynetbox.RequestError as exce:
164-
self.handle.log(f"Error '{exce.error}' creating module type: " +
165-
f"{curr_mt}")
166-
167-
if "interfaces" in curr_mt:
168-
self.device_types.create_module_interfaces(curr_mt["interfaces"], module_type_res.id)
169-
if "power-ports" in curr_mt:
170-
self.device_types.create_module_power_ports(curr_mt["power-ports"], module_type_res.id)
171-
if "console-ports" in curr_mt:
172-
self.device_types.create_module_console_ports(curr_mt["console-ports"], module_type_res.id)
173-
if "power-outlets" in curr_mt:
174-
self.device_types.create_module_power_outlets(curr_mt["power-outlets"], module_type_res.id)
175-
if "console-server-ports" in curr_mt:
176-
self.device_types.create_module_console_server_ports(curr_mt["console-server-ports"], module_type_res.id)
177-
if "rear-ports" in curr_mt:
178-
self.device_types.create_module_rear_ports(curr_mt["rear-ports"], module_type_res.id)
179-
if "front-ports" in curr_mt:
180-
self.device_types.create_module_front_ports(curr_mt["front-ports"], module_type_res.id)
194+
if retries == 0:
195+
self.handle.verbose_log(f'Processing Source File: {curr_mt["src"]}')
196+
else:
197+
self.handle.verbose_log(f'(Retry {retries}/{retry_amount}) Processing Source File: {curr_mt["src"]}')
198+
199+
try:
200+
module_type_res = all_module_types[curr_mt['manufacturer']['slug']][curr_mt["model"]]
201+
self.handle.verbose_log(f'Module Type Exists: {module_type_res.manufacturer.name} - {module_type_res.model} - {module_type_res.id}')
202+
except KeyError:
203+
try:
204+
module_type_res = self.netbox.dcim.module_types.create(curr_mt)
205+
self.counter.update({'module_added': 1})
206+
self.handle.verbose_log(f'Module Type Created: {module_type_res.manufacturer.name} - {module_type_res.model} - {module_type_res.id}')
207+
except pynetbox.RequestError as exce:
208+
self.handle.log(f"Error '{exce.error}' creating module type: {curr_mt["manufacturer"]} {curr_mt["model"]} {curr_mt["part_number"]}")
209+
retries += 1
210+
continue
211+
212+
if "interfaces" in curr_mt:
213+
self.device_types.create_module_interfaces(curr_mt["interfaces"], module_type_res.id)
214+
if "power-ports" in curr_mt:
215+
self.device_types.create_module_power_ports(curr_mt["power-ports"], module_type_res.id)
216+
if "console-ports" in curr_mt:
217+
self.device_types.create_module_console_ports(curr_mt["console-ports"], module_type_res.id)
218+
if "power-outlets" in curr_mt:
219+
self.device_types.create_module_power_outlets(curr_mt["power-outlets"], module_type_res.id)
220+
if "console-server-ports" in curr_mt:
221+
self.device_types.create_module_console_server_ports(curr_mt["console-server-ports"], module_type_res.id)
222+
if "rear-ports" in curr_mt:
223+
self.device_types.create_module_rear_ports(curr_mt["rear-ports"], module_type_res.id)
224+
if "front-ports" in curr_mt:
225+
self.device_types.create_module_front_ports(curr_mt["front-ports"], module_type_res.id)
226+
227+
# We successfully processed the device. Don't retry it.
228+
retries = retry_amount
229+
230+
except (http.client.RemoteDisconnected, requests.exceptions.ConnectionError) as e:
231+
retries += 1
232+
self.counter.update({'connection_errors': 1})
233+
self.handle.log(f'A connection error occurred (Count: {self.counter["connection_errors"]})! Waiting {self.retry_delay} seconds then retrying... Exception: {e}')
234+
235+
# As a connection error has just occurred, we should give the remote end a moment then reconnect.
236+
time.sleep(self.retry_delay)
237+
self.connect_api()
238+
continue
181239

182240
class DeviceTypes:
183241
def __new__(cls, *args, **kwargs):
@@ -480,6 +538,10 @@ def upload_images(self,baseurl,token,images,device_type):
480538

481539
files = { i: (os.path.basename(f), open(f,"rb") ) for i,f in images.items() }
482540
response = requests.patch(url, headers=headers, files=files, verify=(not self.ignore_ssl))
483-
484-
self.handle.log( f'Images {images} updated at {url}: {response}' )
541+
542+
if response.status_code == 500:
543+
raise Exception(f"Remote server failed to write images. Ensure your media directory exists and is writable! - {response}")
544+
else:
545+
self.handle.log( f'Images {images} updated at {url}: {response} (Code {response.status_code})' )
546+
485547
self.counter["images"] += len(images)

repo.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ def get_modules_path(self):
3636
return os.path.join(self.get_absolute_path(), 'module-types')
3737

3838
def slug_format(self, name):
39-
return re_sub('\W+', '-', name.lower())
39+
return re_sub(r'\W+', '-', name.lower()) # Fix #139
4040

4141
def pull_repo(self):
4242
try:
@@ -85,12 +85,17 @@ def get_devices(self, base_path, vendors: list = None):
8585
def parse_files(self, files: list, slugs: list = None):
8686
deviceTypes = []
8787
for file in files:
88+
self.handle.verbose_log(f"Parsing file {file}")
8889
with open(file, 'r') as stream:
8990
try:
9091
data = yaml.safe_load(stream)
9192
except yaml.YAMLError as excep:
9293
self.handle.verbose_log(excep)
9394
continue
95+
except UnicodeDecodeError as excep:
96+
self.handle.verbose_log(excep)
97+
continue
98+
9499
manufacturer = data['manufacturer']
95100
data['manufacturer'] = {
96101
'name': manufacturer, 'slug': self.slug_format(manufacturer)}

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
GitPython==3.1.32
2-
pynetbox==7.0.1
2+
pynetbox==7.3.4
33
python-dotenv==1.0.0
44
PyYAML==6.0.1

0 commit comments

Comments
 (0)