blob: 4e39a6aaf890fce34f4f6696e71e6666c49169ae [file] [log] [blame]
Chip Bolingf5af85d2019-02-12 15:36:17 -06001# Copyright 2017-present Adtran, Inc.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15import structlog
16from lxml import etree
17from ncclient import manager
18from ncclient.operations import RPCError
19from ncclient.transport.errors import SSHError
20from twisted.internet import defer, threads
21from twisted.internet.defer import inlineCallbacks, returnValue
22
23log = structlog.get_logger('ncclient')
24
25ADTRAN_NS = 'http://www.adtran.com/ns/yang'
26
27
28def adtran_module_url(module):
29 return '{}/{}'.format(ADTRAN_NS, module)
30
31
32def phys_entities_rpc():
33 return """
34 <filter xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
35 <physical-entities-state xmlns="{}">
36 <physical-entity/>
37 </physical-entities-state>
38 </filter>
39 """.format(adtran_module_url('adtran-physical-entities'))
40
41
42class AdtranNetconfClient(object):
43 """
44 Performs NETCONF requests
45 """
46 def __init__(self, host_ip, port=830, username='', password='', timeout=10):
47 self._ip = host_ip
48 self._port = port
49 self._username = username
50 self._password = password
51 self._timeout = timeout
52 self._session = None
53
54 def __str__(self):
55 return "AdtranNetconfClient {}@{}:{}".format(self._username, self._ip, self._port)
56
57 @property
58 def capabilities(self):
59 """
60 Get the server's NETCONF capabilities
61
62 :return: (ncclient.capabilities.Capabilities) object representing the server's capabilities.
63 """
64 return self._session.server_capabilities if self._session else None
65
66 @property
67 def connected(self):
68 """
69 Is this client connected to a NETCONF server
70 :return: (boolean) True if connected
71 """
72 return self._session is not None and self._session.connected
73
74 def connect(self, connect_timeout=None):
75 """
76 Connect to the NETCONF server
77
78 o To disable attempting publickey authentication altogether, call with
79 allow_agent and look_for_keys as False.
80
81 o hostkey_verify enables hostkey verification from ~/.ssh/known_hosts
82
83 :return: (deferred) Deferred request
84 """
85 timeout = connect_timeout or self._timeout
86
87 return threads.deferToThread(self._do_connect, timeout)
88
89 def _do_connect(self, timeout):
90 try:
91 self._session = manager.connect(host=self._ip,
92 port=self._port,
93 username=self._username,
94 password=self._password,
95 allow_agent=False,
96 look_for_keys=False,
97 hostkey_verify=False,
98 timeout=timeout)
99
100 except SSHError as e:
101 # Log and rethrow exception so any errBack is called
102 log.warn('SSHError-during-connect', e=e)
103 raise e
104
105 except Exception as e:
106 # Log and rethrow exception so any errBack is called
107 log.exception('Connect-failed: {}', e=e)
108 raise e
109
110 # If debug logging is enabled, decrease the level, DEBUG is a significant
111 # performance hit during response XML decode
112
113 if log.isEnabledFor('DEBUG'):
114 log.setLevel('INFO')
115
116 # TODO: ncclient also supports RaiseMode:NONE to limit exceptions. To set use:
117 #
118 # self._session.raise_mode = RaiseMode:NONE
119 #
120 # and the when you get a response back, you can check 'response.ok' to
121 # see if it is 'True' if it is not, you can enumerate the 'response.errors'
122 # list for more information
123
124 return self._session
125
126 def close(self):
127 """
128 Close the connection to the NETCONF server
129 :return: (deferred) Deferred request
130 """
131 s, self._session = self._session, None
132
133 if s is None or not s.connected:
134 return defer.returnValue(True)
135
136 return threads.deferToThread(self._do_close, s)
137
138 def _do_close(self, old_session):
139 return old_session.close_session()
140
141 @inlineCallbacks
142 def _reconnect(self):
143 try:
144 yield self.close()
145 except:
146 pass
147
148 try:
149 yield self.connect()
150 except:
151 pass
152
153 def get_config(self, source='running'):
154 """
155 Get the configuration from the specified source
156
157 :param source: (string) Configuration source, 'running', 'candidate', ...
158
159 :return: (deferred) Deferred request that wraps the GetReply class
160 """
161 if not self._session:
162 raise NotImplemented('No SSH Session')
163
164 if not self._session.connected:
165 self._reconnect()
166
167 return threads.deferToThread(self._do_get_config, source)
168
169 def _do_get_config(self, source):
170 """
171 Get the configuration from the specified source
172
173 :param source: (string) Configuration source, 'running', 'candidate', ...
174
175 :return: (GetReply) The configuration.
176 """
177 return self._session.get_config(source)
178
179 def get(self, payload):
180 """
181 Get the requested data from the server
182
183 :param payload: Payload/filter
184 :return: (deferred) for GetReply
185 """
186 log.debug('get', filter=payload)
187
188 if not self._session:
189 raise NotImplemented('No SSH Session')
190
191 if not self._session.connected:
192 self._reconnect()
193
194 return threads.deferToThread(self._do_get, payload)
195
196 def _do_get(self, payload):
197 """
198 Get the requested data from the server
199
200 :param payload: Payload/filter
201 :return: (GetReply) response
202 """
203 try:
204 log.debug('get', payload=payload)
205 response = self._session.get(payload)
206 # To get XML, use response.xml
207 log.debug('response', response=response)
208
209 except RPCError as e:
210 log.exception('get', e=e)
211 raise
212
213 return response
214
215 def lock(self, source, lock_timeout):
216 """
217 Lock the configuration system
218 :return: (deferred) for RpcReply
219 """
220 log.info('lock', source=source, timeout=lock_timeout)
221
222 if not self._session or not self._session.connected:
223 raise NotImplemented('TODO: Support auto-connect if needed')
224
225 return threads.deferToThread(self._do_lock, source, lock_timeout)
226
227 def _do_lock(self, source, lock_timeout):
228 """
229 Lock the configuration system
230 """
231 try:
232 response = self._session.lock(source, timeout=lock_timeout)
233 # To get XML, use response.xml
234
235 except RPCError as e:
236 log.exception('lock', e=e)
237 raise
238
239 return response
240
241 def unlock(self, source):
242 """
243 Get the requested data from the server
244 :param source: RPC request
245
246 :return: (deferred) for RpcReply
247 """
248 log.info('unlock', source=source)
249
250 if not self._session or not self._session.connected:
251 raise NotImplemented('TODO: Support auto-connect if needed')
252
253 return threads.deferToThread(self._do_unlock, source)
254
255 def _do_unlock(self, source):
256 """
257 Lock the configuration system
258 """
259 try:
260 response = self._session.unlock(source)
261 # To get XML, use response.xml
262
263 except RPCError as e:
264 log.exception('unlock', e=e)
265 raise
266
267 return response
268
269 @inlineCallbacks
270 def edit_config(self, config, target='running', default_operation='none',
271 test_option=None, error_option=None, ignore_delete_error=False):
272 """
273 Loads all or part of the specified config to the target configuration datastore
274 with the ability to lock the datastore during the edit.
275
276 :param config is the configuration, which must be rooted in the config element.
277 It can be specified either as a string or an Element.format="xml"
278 :param target is the name of the configuration datastore being edited
279 :param default_operation if specified must be one of { 'merge', 'replace', or 'none' }
280 :param test_option if specified must be one of { 'test_then_set', 'set' }
281 :param error_option if specified must be one of { 'stop-on-error',
282 'continue-on-error', 'rollback-on-error' } The
283 'rollback-on-error' error_option depends on the
284 :rollback-on-error capability.
285 :param ignore_delete_error: (bool) For some startup deletes/clean-ups, we do a
286 delete high up in the config to get whole lists. If
287 these lists are empty, this helps suppress any error
288 message from NETConf on failure to delete an empty list
289
290 :return: (deferred) for RpcReply
291 """
292 if not self._session:
293 raise NotImplemented('No SSH Session')
294
295 if not self._session.connected:
296 try:
297 yield self._reconnect()
298
299 except Exception as e:
300 log.exception('edit-config-connect', e=e)
301
302 try:
303 if config[:7] != '<config':
304 config = '<config xmlns:xc="urn:ietf:params:xml:ns:netconf:base:1.0"' + \
305 ' xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">' + \
306 config + '</config>'
307
308 log.debug('netconf-request', config=config, target=target,
309 default_operation=default_operation)
310
311 rpc_reply = yield threads.deferToThread(self._do_edit_config, target,
312 config, default_operation,
313 test_option, error_option)
314 except Exception as e:
315 if ignore_delete_error and 'operation="delete"' in config.lower():
316 returnValue('ignoring-delete-error')
317 log.exception('edit_config', e=e, config=config, target=target)
318 raise
319
320 returnValue(rpc_reply)
321
322 def _do_edit_config(self, target, config, default_operation, test_option, error_option,
323 ignore_delete_error=False):
324 """
325 Perform actual edit-config operation
326 """
327 try:
328 log.debug('edit-config', target=target, config=config)
329
330 response = self._session.edit_config(target=target, config=config
331 # TODO: Support additional options later
332 # ,default_operation=default_operation,
333 # test_option=test_option,
334 # error_option=error_option
335 )
336
337 log.debug('netconf-response', response=response)
338 # To get XML, use response.xml
339 # To check status, use response.ok (boolean)
340
341 except RPCError as e:
342 if not ignore_delete_error or 'operation="delete"' not in config.lower():
343 log.exception('do_edit_config', e=e, config=config, target=target)
344 raise
345
346 return response
347
348 def rpc(self, rpc_string):
349 """
350 Custom RPC request
351 :param rpc_string: (string) RPC request
352 :return: (deferred) for GetReply
353 """
354 log.debug('rpc', rpc=rpc_string)
355
356 if not self._session:
357 raise NotImplemented('No SSH Session')
358
359 if not self._session.connected:
360 self._reconnect()
361
362 return threads.deferToThread(self._do_rpc, rpc_string)
363
364 def _do_rpc(self, rpc_string):
365 try:
366 response = self._session.dispatch(etree.fromstring(rpc_string))
367 # To get XML, use response.xml
368
369 except RPCError as e:
370 log.exception('rpc', e=e)
371 raise
372
373 return response