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
"""
@@ -65,10 +84,30 @@ def post(self, **json):
65
84
66
85
:returns: child_mapper: Mapper containing the created object
67
86
"""
87
+ for k , v in json .items ():
88
+ if isinstance (v , NetboxMapper ):
89
+ try :
90
+ json [k ] = v .id
91
+ except AttributeError :
92
+ raise ValueError ("Mapper {} has no id" .format (k ))
68
93
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 )
94
+ try :
95
+ return next (self .get (new_mapper_dict ["id" ]))
96
+ except requests .exceptions .HTTPError as e :
97
+ if e .response .status_code == 404 :
98
+ logger .debug (
99
+ "Rare case of weird endpoint where object cannot be fetch "
100
+ "after POST by using the same endpoint. Returning a "
101
+ "mapper based on this answer instead of fetching the "
102
+ "entire object."
103
+ ""
104
+ "Do not try to put this mapper as it will fail."
105
+ )
106
+ return self ._build_new_mapper_from (
107
+ new_mapper_dict ,
108
+ self ._route + "{}/" .format (new_mapper_dict ["id" ]),
109
+ passive_mapper = True
110
+ )
72
111
73
112
def put (self ):
74
113
"""
@@ -83,17 +122,42 @@ def put(self):
83
122
>>> child_mapper = netbox_mapper.get(1)
84
123
>>> child_mapper.name = "another name"
85
124
>>> child_mapper.put()
86
- <child_mapper>
87
125
88
- :returns: child_mapper: Mapper containing the updated object
126
+ :returns: request_reponse: requests object containing the netbox
127
+ response
89
128
"""
90
129
assert getattr (self , "id" , None ) is not None , "self.id does not exist"
91
130
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 )
131
+ return self .netbox_api .put (self ._route , json = self .to_dict ())
94
132
95
133
def to_dict (self ):
96
- return {a : getattr (self , a , None ) for a in self .__upstream_attrs__ }
134
+ serialize = {}
135
+ foreign_keys = self .__foreign_keys__ .copy ()
136
+ for a in self .__upstream_attrs__ :
137
+ val = getattr (self , a , None )
138
+ if isinstance (val , dict ):
139
+ if "value" in val and "label" in val :
140
+ val = val ["value" ]
141
+ elif isinstance (val , NetboxMapper ):
142
+ foreign_keys .append (a )
143
+ continue
144
+
145
+ serialize [a ] = val
146
+
147
+ for fk in foreign_keys :
148
+ attr = getattr (self , fk , None )
149
+ if isinstance (attr , int ):
150
+ serialize [fk ] = attr
151
+ continue
152
+
153
+ try :
154
+ # check that attr is iterable
155
+ iter (attr )
156
+ serialize [fk ] = [getattr (i , "id" , None ) for i in attr ]
157
+ except TypeError :
158
+ serialize [fk ] = getattr (attr , "id" , None )
159
+
160
+ return serialize
97
161
98
162
def delete (self , id = None ):
99
163
"""
@@ -128,12 +192,76 @@ def delete(self, id=None):
128
192
delete_route = self ._route + "{}/" .format (id ) if id else self ._route
129
193
return self .netbox_api .delete (delete_route )
130
194
131
- def _build_new_mapper_from (self , mapper_attributes , new_route ):
132
- mapper = type (self )(
195
+ def _build_new_mapper_from (
196
+ self , mapper_attributes , new_route , passive_mapper = False
197
+ ):
198
+ cls = NetboxPassiveMapper if passive_mapper else NetboxMapper
199
+ mapper_class = type (
200
+ "NetboxMapper_{}_{}" .format (
201
+ re .sub ("_|-" , "" , self .__model__ .title ()),
202
+ re .sub ("_|-" , "" , "" .join (
203
+ s .title () for s in new_route .split ("/" )
204
+ ))
205
+ ), (cls ,), {}
206
+ )
207
+
208
+ mapper = mapper_class (
133
209
self .netbox_api , self .__app_name__ , self .__model__ , new_route
134
210
)
135
- mapper .__upstream_attrs__ = list (mapper_attributes .keys ())
211
+ mapper .__upstream_attrs__ = []
212
+ mapper .__foreign_keys__ = []
136
213
for attr , val in mapper_attributes .items ():
137
- setattr (mapper , attr , val )
214
+ if isinstance (val , dict ) and "id" in val :
215
+ mapper .__foreign_keys__ .append (attr )
216
+ mapper ._set_property_foreign_key (attr , val )
217
+ else :
218
+ mapper .__upstream_attrs__ .append (attr )
219
+ setattr (mapper , attr , val )
138
220
139
221
return mapper
222
+
223
+ def _set_property_foreign_key (self , attr , value ):
224
+ def get_foreign_object (* args ):
225
+ if hasattr (self , "_{}" .format (attr )):
226
+ return getattr (self , "_{}" .format (attr ))
227
+
228
+ if attr in self ._fk_cache :
229
+ return self ._fk_cache [attr ]
230
+
231
+ url = value ["url" ]
232
+ route = url .replace (self .netbox_api .url , "" , 1 ).lstrip ("/" )
233
+ model , app_name , * params = route .split ("/" )
234
+
235
+ fk = list (NetboxMapper (self .netbox_api , model , app_name ).get (
236
+ * [p for p in params if p ]
237
+ ))
238
+ if not fk :
239
+ fk = None
240
+ elif len (fk ) == 1 :
241
+ fk = fk [0 ]
242
+
243
+ self ._fk_cache [attr ] = fk
244
+ return fk
245
+
246
+ def setter (cls , value ):
247
+ setattr (self , "_{}" .format (attr ), value )
248
+
249
+ try :
250
+ self ._fk_cache .pop (attr )
251
+ except KeyError :
252
+ pass
253
+ setattr (type (self ), attr , property (get_foreign_object , setter ))
254
+
255
+
256
+ class NetboxPassiveMapper (NetboxMapper ):
257
+ def get (self , * args , ** kwargs ):
258
+ raise ForbiddenAsPassiveMapperError ()
259
+
260
+ def post (self , * args , ** kwargs ):
261
+ raise ForbiddenAsPassiveMapperError ()
262
+
263
+ def put (self , * args , ** kwargs ):
264
+ raise ForbiddenAsPassiveMapperError ()
265
+
266
+ def delete (self , * args , ** kwargs ):
267
+ raise ForbiddenAsPassiveMapperError ()
0 commit comments