VOL-1397: Adtran-OLT - Initial containerization commit
- Need to move VERSION to base directory
Change-Id: I9d62d0607a011ce642e379fd92b35ec48b300070
diff --git a/adapters/adtran_common/net/adtran_rest.py b/adapters/adtran_common/net/adtran_rest.py
new file mode 100644
index 0000000..9020e82
--- /dev/null
+++ b/adapters/adtran_common/net/adtran_rest.py
@@ -0,0 +1,189 @@
+# Copyright 2017-present Adtran, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import json
+
+import structlog
+import treq
+from twisted.internet.defer import inlineCallbacks, returnValue
+from twisted.internet.error import ConnectionClosed, ConnectionDone, ConnectionLost
+
+log = structlog.get_logger()
+
+
+class RestInvalidResponseCode(Exception):
+ def __init__(self, message, url, code):
+ super(RestInvalidResponseCode, self).__init__(message)
+ self.url = url
+ self.code = code
+
+
+class AdtranRestClient(object):
+ """
+ Performs Adtran RESTCONF requests
+ """
+ # HTTP shortcuts
+ HELLO_URI = '/restconf/adtran-hello:hello'
+
+ REST_GET_REQUEST_HEADER = {'User-Agent': 'Adtran RESTConf',
+ 'Accept': ['application/json']}
+
+ REST_POST_REQUEST_HEADER = {'User-Agent': 'Adtran RESTConf',
+ 'Content-Type': 'application/json',
+ 'Accept': ['application/json']}
+
+ REST_PATCH_REQUEST_HEADER = REST_POST_REQUEST_HEADER
+ REST_PUT_REQUEST_HEADER = REST_POST_REQUEST_HEADER
+ REST_DELETE_REQUEST_HEADER = REST_GET_REQUEST_HEADER
+
+ HTTP_OK = 200
+ HTTP_CREATED = 201
+ HTTP_ACCEPTED = 202
+ HTTP_NON_AUTHORITATIVE_INFORMATION = 203
+ HTTP_NO_CONTENT = 204
+ HTTP_RESET_CONTENT = 205
+ HTTP_PARTIAL_CONTENT = 206
+ HTTP_NOT_FOUND = 404
+
+ _valid_methods = {'GET', 'POST', 'PATCH', 'DELETE'}
+ _valid_results = {'GET': [HTTP_OK, HTTP_NO_CONTENT],
+ 'POST': [HTTP_OK, HTTP_CREATED, HTTP_NO_CONTENT],
+ 'PUT': [HTTP_OK, HTTP_CREATED, HTTP_NO_CONTENT],
+ 'PATCH': [HTTP_OK],
+ 'DELETE': [HTTP_OK, HTTP_ACCEPTED, HTTP_NO_CONTENT, HTTP_NOT_FOUND]
+ }
+
+ for _method in _valid_methods:
+ assert _method in _valid_results # Make sure we have a results entry for each supported method
+
+ def __init__(self, host_ip, port, username='', password='', timeout=10):
+ """
+ REST Client initialization
+
+ :param host_ip: (string) IP Address of Adtran Device
+ :param port: (int) Port number
+ :param username: (string) Username for credentials
+ :param password: (string) Password for credentials
+ :param timeout: (int) Number of seconds to wait for a response before timing out
+ """
+ self._ip = host_ip
+ self._port = port
+ self._username = username
+ self._password = password
+ self._timeout = timeout
+
+ def __str__(self):
+ return "AdtranRestClient {}@{}:{}".format(self._username, self._ip, self._port)
+
+ @inlineCallbacks
+ def request(self, method, uri, data=None, name='', timeout=None, is_retry=False,
+ suppress_error=False):
+ """
+ Send a REST request to the Adtran device
+
+ :param method: (string) HTTP method
+ :param uri: (string) fully URL to perform method on
+ :param data: (string) optional data for the request body
+ :param name: (string) optional name of the request, useful for logging purposes
+ :param timeout: (int) Number of seconds to wait for a response before timing out
+ :param is_retry: (boolean) True if this method called recursively in order to recover
+ from a connection loss. Can happen sometimes in debug sessions
+ and in the real world.
+ :param suppress_error: (boolean) If true, do not output ERROR message on REST request failure
+ :return: (dict) On success with the proper results
+ """
+ log.debug('request', method=method, uri=uri, data=data, retry=is_retry)
+
+ if method.upper() not in self._valid_methods:
+ raise NotImplementedError("REST method '{}' is not supported".format(method))
+
+ url = 'http://{}:{}{}{}'.format(self._ip, self._port,
+ '/' if uri[0] != '/' else '',
+ uri)
+ response = None
+ timeout = timeout or self._timeout
+
+ try:
+ if method.upper() == 'GET':
+ response = yield treq.get(url,
+ auth=(self._username, self._password),
+ timeout=timeout,
+ headers=self.REST_GET_REQUEST_HEADER)
+ elif method.upper() == 'POST' or method.upper() == 'PUT':
+ response = yield treq.post(url,
+ data=data,
+ auth=(self._username, self._password),
+ timeout=timeout,
+ headers=self.REST_POST_REQUEST_HEADER)
+ elif method.upper() == 'PATCH':
+ response = yield treq.patch(url,
+ data=data,
+ auth=(self._username, self._password),
+ timeout=timeout,
+ headers=self.REST_PATCH_REQUEST_HEADER)
+ elif method.upper() == 'DELETE':
+ response = yield treq.delete(url,
+ auth=(self._username, self._password),
+ timeout=timeout,
+ headers=self.REST_DELETE_REQUEST_HEADER)
+ else:
+ raise NotImplementedError("REST method '{}' is not supported".format(method))
+
+ except NotImplementedError:
+ raise
+
+ except (ConnectionDone, ConnectionLost) as e:
+ if is_retry:
+ raise
+ returnValue(self.request(method, uri, data=data, name=name,
+ timeout=timeout, is_retry=True))
+
+ except ConnectionClosed:
+ returnValue(ConnectionClosed)
+
+ except Exception as e:
+ log.exception("rest-request", method=method, url=url, name=name, e=e)
+ raise
+
+ if response.code not in self._valid_results[method.upper()]:
+ message = "REST {} '{}' request to '{}' failed with status code {}".format(method, name,
+ url, response.code)
+ if not suppress_error:
+ log.error(message)
+ raise RestInvalidResponseCode(message, url, response.code)
+
+ if response.code in {self.HTTP_NO_CONTENT, self.HTTP_NOT_FOUND}:
+ returnValue(None)
+
+ else:
+ # TODO: May want to support multiple body encodings in the future
+
+ headers = response.headers
+ type_key = 'content-type'
+ type_val = 'application/json'
+
+ if not headers.hasHeader(type_key) or type_val not in headers.getRawHeaders(type_key, []):
+ raise Exception("REST {} '{}' request response from '{}' was not JSON",
+ method, name, url)
+
+ content = yield response.content()
+ try:
+ result = json.loads(content)
+
+ except Exception as e:
+ log.exception("json-decode", method=method, url=url, name=name,
+ content=content, e=e)
+ raise
+
+ returnValue(result)