1
+ import logging
2
+ import re
3
+ import requests
1
4
2
5
from .api import NetboxAPI
3
- from .exceptions import ForbiddenAsChildError
6
+ from .exceptions import ForbiddenAsChildError , ForbiddenAsPassiveMapperError
7
+
8
+
9
+ logger = logging .getLogger ("netboxapi" )
4
10
5
11
6
12
class NetboxMapper ():
@@ -9,6 +15,10 @@ def __init__(self, netbox_api, app_name, model, route=None):
9
15
self .__app_name__ = app_name
10
16
self .__model__ = model
11
17
self .__upstream_attrs__ = []
18
+ self .__foreign_keys__ = []
19
+
20
+ #: cache for foreign keys properties
21
+ self ._fk_cache = {}
12
22
13
23
self ._route = (
14
24
route or
@@ -31,6 +41,10 @@ def get(self, *args, **kwargs):
31
41
>>> netbox_mapper.get("1", "racks", q="name_to_filter")
32
42
33
43
Will do a request to "/dcim/sites/1/racks/?q=name_to_filter"
44
+
45
+ Some specific routes will not return objects with ID (this one for
46
+ example: `/ipam/prefixes/{id}/available-prefixes/`). In this case, no
47
+ mapper will be built from the result and it will be yield as received.
34
48
"""
35
49
if args :
36
50
route = self ._route + "/" .join (str (a ) for a in args ) + "/"
@@ -43,10 +57,15 @@ def get(self, *args, **kwargs):
43
57
44
58
if isinstance (new_mappers_dict , dict ):
45
59
new_mappers_dict = [new_mappers_dict ]
46
- for d in new_mappers_dict :
47
- yield self ._build_new_mapper_from (
48
- d , route + "{}/" .format (d ["id" ])
49
- )
60
+ try :
61
+ for d in new_mappers_dict :
62
+ yield self ._build_new_mapper_from (
63
+ d , route + "{}/" .format (d ["id" ])
64
+ )
65
+ except KeyError :
66
+ # Result objects have no id, cannot build a mapper from them,
67
+ # yield them as received
68
+ yield from new_mappers_dict
50
69
51
70
def post (self , ** json ):
52
71
"""
@@ -66,9 +85,23 @@ def post(self, **json):
66
85
:returns: child_mapper: Mapper containing the created object
67
86
"""
68
87
new_mapper_dict = self .netbox_api .post (self ._route , json = json )
69
- route = self ._route + "{}/" .format (new_mapper_dict ["id" ])
70
-
71
- return self ._build_new_mapper_from (new_mapper_dict , route )
88
+ try :
89
+ return next (self .get (new_mapper_dict ["id" ]))
90
+ except requests .exceptions .HTTPError as e :
91
+ if e .response .status_code == 404 :
92
+ logger .debug (
93
+ "Rare case of weird endpoint where object cannot be fetch "
94
+ "after POST by using the same endpoint. Returning a "
95
+ "mapper based on this answer instead of fetching the "
96
+ "entire object."
97
+ ""
98
+ "Do not try to put this mapper as it will fail."
99
+ )
100
+ return self ._build_new_mapper_from (
101
+ new_mapper_dict ,
102
+ self ._route + "{}/" .format (new_mapper_dict ["id" ]),
103
+ passive_mapper = True
104
+ )
72
105
73
106
def put (self ):
74
107
"""
@@ -83,17 +116,34 @@ def put(self):
83
116
>>> child_mapper = netbox_mapper.get(1)
84
117
>>> child_mapper.name = "another name"
85
118
>>> child_mapper.put()
86
- <child_mapper>
87
119
88
- :returns: child_mapper: Mapper containing the updated object
120
+ :returns: request_reponse: requests object containing the netbox
121
+ response
89
122
"""
90
123
assert getattr (self , "id" , None ) is not None , "self.id does not exist"
91
124
92
- new_mapper_dict = self .netbox_api .put (self ._route , json = self .to_dict ())
93
- return self ._build_new_mapper_from (new_mapper_dict , self ._route )
125
+ return self .netbox_api .put (self ._route , json = self .to_dict ())
94
126
95
127
def to_dict (self ):
96
- return {a : getattr (self , a , None ) for a in self .__upstream_attrs__ }
128
+ serialize = {}
129
+ for a in self .__upstream_attrs__ :
130
+ val = getattr (self , a , None )
131
+ if isinstance (val , dict ):
132
+ if "value" in val and "label" in val :
133
+ val = val ["value" ]
134
+
135
+ serialize [a ] = val
136
+
137
+ for fk in self .__foreign_keys__ :
138
+ attr = getattr (self , fk , None )
139
+ try :
140
+ # check that attr is iterable
141
+ iter (attr )
142
+ serialize [fk ] = [getattr (i , "id" , None ) for i in attr ]
143
+ except TypeError :
144
+ serialize [fk ] = getattr (attr , "id" , None )
145
+
146
+ return serialize
97
147
98
148
def delete (self , id = None ):
99
149
"""
@@ -128,12 +178,73 @@ def delete(self, id=None):
128
178
delete_route = self ._route + "{}/" .format (id ) if id else self ._route
129
179
return self .netbox_api .delete (delete_route )
130
180
131
- def _build_new_mapper_from (self , mapper_attributes , new_route ):
132
- mapper = type (self )(
181
+ def _build_new_mapper_from (
182
+ self , mapper_attributes , new_route , passive_mapper = False
183
+ ):
184
+ if not passive_mapper :
185
+ cls = type (self )
186
+ else :
187
+ cls = NetboxPassiveMapper
188
+ mapper_class = type (
189
+ "NetboxMapper_{}_{}" .format (
190
+ re .sub ("_|-" , "" , self .__model__ .title ()),
191
+ re .sub ("_|-" , "" , "" .join (
192
+ s .title () for s in new_route .split ("/" )
193
+ ))
194
+ ), (cls ,), {}
195
+ )
196
+
197
+ mapper = mapper_class (
133
198
self .netbox_api , self .__app_name__ , self .__model__ , new_route
134
199
)
135
- mapper .__upstream_attrs__ = list (mapper_attributes .keys ())
200
+ mapper .__upstream_attrs__ = []
201
+ mapper .__foreign_keys__ = []
136
202
for attr , val in mapper_attributes .items ():
137
- setattr (mapper , attr , val )
203
+ if isinstance (val , dict ) and "id" in val :
204
+ mapper .__foreign_keys__ .append (attr )
205
+ mapper ._set_property_foreign_key (attr , val )
206
+ else :
207
+ mapper .__upstream_attrs__ .append (attr )
208
+ setattr (mapper , attr , val )
138
209
139
210
return mapper
211
+
212
+ def _set_property_foreign_key (self , attr , value ):
213
+ def get_foreign_object (* args ):
214
+ if attr in self ._fk_cache :
215
+ return self ._fk_cache [attr ]
216
+
217
+ url = value ["url" ]
218
+ route = url .replace (self .netbox_api .url , "" , 1 ).lstrip ("/" )
219
+ model , app_name , * params = route .split ("/" )
220
+
221
+ fk = list (NetboxMapper (self .netbox_api , model , app_name ).get (
222
+ * [p for p in params if p ]
223
+ ))
224
+ if not fk :
225
+ fk = None
226
+ elif len (fk ) == 1 :
227
+ fk = fk [0 ]
228
+
229
+ self ._fk_cache [attr ] = fk
230
+ return fk
231
+
232
+ try :
233
+ self ._fk_cache .pop (attr )
234
+ except KeyError :
235
+ pass
236
+ setattr (type (self ), attr , property (get_foreign_object ))
237
+
238
+
239
+ class NetboxPassiveMapper (NetboxMapper ):
240
+ def get (self , * args , ** kwargs ):
241
+ raise ForbiddenAsPassiveMapperError ()
242
+
243
+ def post (self , * args , ** kwargs ):
244
+ raise ForbiddenAsPassiveMapperError ()
245
+
246
+ def put (self , * args , ** kwargs ):
247
+ raise ForbiddenAsPassiveMapperError ()
248
+
249
+ def delete (self , * args , ** kwargs ):
250
+ raise ForbiddenAsPassiveMapperError ()
0 commit comments