class DescriptorOnboarder(object): """ This class is responsible for onboarding descriptors using Restconf""" DESC_ENDPOINT_MAP = { NsdYang.YangData_Nsd_NsdCatalog_Nsd: "nsd-catalog/nsd", RwNsdYang.YangData_Nsd_NsdCatalog_Nsd: "nsd-catalog/nsd", VnfdYang.YangData_Vnfd_VnfdCatalog_Vnfd: "vnfd-catalog/vnfd", RwVnfdYang.YangData_Vnfd_VnfdCatalog_Vnfd: "vnfd-catalog/vnfd", } DESC_SERIALIZER_MAP = { NsdYang.YangData_Nsd_NsdCatalog_Nsd: convert.NsdSerializer(), RwNsdYang.YangData_Nsd_NsdCatalog_Nsd: convert.RwNsdSerializer(), VnfdYang.YangData_Vnfd_VnfdCatalog_Vnfd: convert.VnfdSerializer(), RwVnfdYang.YangData_Vnfd_VnfdCatalog_Vnfd: convert.RwVnfdSerializer(), } HEADERS = {"content-type": "application/vnd.yang.data+json"} TIMEOUT_SECS = 5 AUTH = ('admin', 'admin') def __init__(self, log, host="127.0.0.1", port=8008, use_ssl=False, ssl_cert=None, ssl_key=None): self._log = log self._host = host self.port = port self._use_ssl = use_ssl self._ssl_cert = ssl_cert self._ssl_key = ssl_key self.timeout = DescriptorOnboarder.TIMEOUT_SECS @classmethod def _get_headers(cls, auth): headers = cls.HEADERS.copy() if auth is not None: headers['authorization'] = auth return headers def _get_url(self, descriptor_msg): if type(descriptor_msg) not in DescriptorOnboarder.DESC_SERIALIZER_MAP: raise TypeError("Invalid descriptor message type") endpoint = DescriptorOnboarder.DESC_ENDPOINT_MAP[type(descriptor_msg)] url = "{}://{}:{}/api/config/{}".format( "https" if self._use_ssl else "http", self._host, self.port, endpoint, ) return url def _make_request_args(self, descriptor_msg, auth=None): if type(descriptor_msg) not in DescriptorOnboarder.DESC_SERIALIZER_MAP: raise TypeError("Invalid descriptor message type") serializer = DescriptorOnboarder.DESC_SERIALIZER_MAP[type( descriptor_msg)] json_data = serializer.to_json_string(descriptor_msg) url = self._get_url(descriptor_msg) request_args = dict( url=url, data=json_data, headers=self._get_headers(auth), auth=DescriptorOnboarder.AUTH, verify=False, cert=(self._ssl_cert, self._ssl_key) if self._use_ssl else None, timeout=self.timeout, ) return request_args def update(self, descriptor_msg, auth=None): """ Update the descriptor config Arguments: descriptor_msg - A descriptor proto-gi msg auth - the authorization header Raises: UpdateError - The descriptor config update failed """ request_args = self._make_request_args(descriptor_msg, auth) try: response = requests.put(**request_args) response.raise_for_status() except requests.exceptions.ConnectionError as e: msg = "Could not connect to restconf endpoint: %s" % str(e) self._log.error(msg) raise UpdateError(msg) from e except requests.exceptions.HTTPError as e: msg = "PUT request to %s error: %s" % (request_args["url"], response.text) self._log.error(msg) raise UpdateError(msg) from e except requests.exceptions.Timeout as e: msg = "Timed out connecting to restconf endpoint: %s", str(e) self._log.error(msg) raise UpdateError(msg) from e def onboard(self, descriptor_msg, auth=None): """ Onboard the descriptor config Arguments: descriptor_msg - A descriptor proto-gi msg auth - the authorization header Raises: OnboardError - The descriptor config update failed """ request_args = self._make_request_args(descriptor_msg, auth) try: response = requests.post(**request_args) response.raise_for_status() except requests.exceptions.ConnectionError as e: msg = "Could not connect to restconf endpoint: %s" % str(e) self._log.error(msg) raise OnboardError(msg) from e except requests.exceptions.HTTPError as e: msg = "POST request to %s error: %s" % (request_args["url"], response.text) self._log.error(msg) raise OnboardError(msg) from e except requests.exceptions.Timeout as e: msg = "Timed out connecting to restconf endpoint: %s", str(e) self._log.error(msg) raise OnboardError(msg) from e
def check_output(self, out_dir, archive=False): prev_dir = os.getcwd() os.chdir(out_dir) # Check the archives or directories are present dirs = os.listdir(out_dir) # The desc dirs are using uuid, so cannot match name # Check there are 3 dirs or files self.assertTrue(len(dirs) >= 3) try: count = 0 for a in dirs: desc = None if archive: if os.path.isfile(a): self.log.debug("Checking archive: {}".format(a)) with tarfile.open(a, 'r') as t: for m in t.getnames(): if m.endswith('.yaml') or m.endswith('.yml'): # Descriptor file t.extract(m) self.log.debug("Extracted file: {}".format(m)) desc = m break else: continue else: if os.path.isdir(a): self.log.debug("Checking directory: {}".format(a)) for m in os.listdir(a): if m.endswith('.yaml') or m.endswith('.yml'): desc = os.path.join(a, m) break if desc: self.log.debug("Checking descriptor: {}".format(desc)) with open(desc, 'r') as d: rest, ext = os.path.splitext(desc) if '_vnfd.y' in desc: vnfd = convert.VnfdSerializer().from_file_hdl(d, ext) gen_desc = vnfd.as_dict() if 'ping_vnfd.y' in desc: exp_desc = self.exp_descs.ping_vnfd.as_dict() elif 'pong_vnfd.y' in desc: exp_desc = self.exp_descs.pong_vnfd.as_dict() else: raise Exception("Unknown VNFD descriptor: {}". format(desc)) elif '_nsd.y' in desc: nsd = convert.NsdSerializer().from_file_hdl(d, ext) gen_desc = nsd.as_dict() exp_desc = self.exp_descs.ping_pong_nsd.as_dict() else: raise Exception("Unknown file: {}".format(desc)) # Compare the descriptors self.compare_dict(gen_desc, exp_desc) # Increment the count of descriptiors found count += 1 if count != 3: raise Exception("Did not find expected number of descriptors: {}". format(count)) except Exception as e: self.log.exception(e) raise e finally: os.chdir(prev_dir)
class RestconfDescriptorHandler(tornado.web.RequestHandler): DESC_SERIALIZER_MAP = { "nsd": convert.NsdSerializer(), "vnfd": convert.VnfdSerializer(), } class AuthError(Exception): pass class ContentTypeError(Exception): pass class RequestBodyError(Exception): pass def initialize(self, log, auth, info): self._auth = auth # The superclass has self._log already defined so use a different name self._logger = log self._info = info self._logger.debug('Created restconf descriptor handler') def _verify_auth(self): if self._auth is None: return None auth_header = self.request.headers.get('Authorization') if auth_header is None or not auth_header.startswith('Basic '): self.set_status(401) self.set_header('WWW-Authenticate', 'Basic realm=Restricted') self._transforms = [] self.finish() msg = "Missing Authorization header" self._logger.error(msg) raise RestconfDescriptorHandler.AuthError(msg) auth_header = auth_header.encode('ascii') auth_decoded = base64.decodebytes(auth_header[6:]).decode() login, password = auth_decoded.split(':', 2) login = login password = password is_auth = ((login, password) == self._auth) if not is_auth: self.set_status(401) self.set_header('WWW-Authenticate', 'Basic realm=Restricted') self._transforms = [] self.finish() msg = "Incorrect username and password in auth header: got {}, expected {}".format( (login, password), self._auth) self._logger.error(msg) raise RestconfDescriptorHandler.AuthError(msg) def _verify_content_type_header(self): content_type_header = self.request.headers.get('content-type') if content_type_header is None: self.set_status(415) self._transforms = [] self.finish() msg = "Missing content-type header" self._logger.error(msg) raise RestconfDescriptorHandler.ContentTypeError(msg) if content_type_header != "application/vnd.yang.data+json": self.set_status(415) self._transforms = [] self.finish() msg = "Unsupported content type: %s" % content_type_header self._logger.error(msg) raise RestconfDescriptorHandler.ContentTypeError(msg) def _verify_headers(self): self._verify_auth() self._verify_content_type_header() def _verify_request_body(self, descriptor_type): if descriptor_type not in RestconfDescriptorHandler.DESC_SERIALIZER_MAP: raise ValueError("Unsupported descriptor type: %s" % descriptor_type) body = self.request.body bytes_hdl = io.BytesIO(body) serializer = RestconfDescriptorHandler.DESC_SERIALIZER_MAP[ descriptor_type] try: message = serializer.from_file_hdl(bytes_hdl, ".json") except convert.SerializationError as e: self.set_status(400) self._transforms = [] self.finish() msg = "Descriptor request body not valid" self._logger.error(msg) raise RestconfDescriptorHandler.RequestBodyError() from e self._info.last_request_message = message self._logger.debug("Received a valid descriptor request") def put(self, descriptor_type): self._info.last_descriptor_type = descriptor_type self._info.last_method = "PUT" try: self._verify_headers() except (RestconfDescriptorHandler.AuthError, RestconfDescriptorHandler.ContentTypeError): return None try: self._verify_request_body(descriptor_type) except RestconfDescriptorHandler.RequestBodyError: return None self.write("Response doesn't matter?") def post(self, descriptor_type): self._info.last_descriptor_type = descriptor_type self._info.last_method = "POST" try: self._verify_headers() except (RestconfDescriptorHandler.AuthError, RestconfDescriptorHandler.ContentTypeError): return None try: self._verify_request_body(descriptor_type) except RestconfDescriptorHandler.RequestBodyError: return None self.write("Response doesn't matter?")
class OnboardTestCase(tornado.testing.AsyncHTTPTestCase): DESC_SERIALIZER_MAP = { "nsd": convert.NsdSerializer(), "vnfd": convert.VnfdSerializer(), } AUTH = ("admin", "admin") def setUp(self): self._log = logging.getLogger(__file__) self._loop = asyncio.get_event_loop() self._handler_info = HandlerInfo() super().setUp() self._port = self.get_http_port() self._onboarder = onboard.DescriptorOnboarder(log=self._log, port=self._port) def get_new_ioloop(self): return tornado.platform.asyncio.AsyncIOMainLoop() def get_app(self): attrs = dict(auth=OnboardTestCase.AUTH, log=self._log, info=self._handler_info) return tornado.web.Application([ (r"/api/config/project/default/.*/(nsd|vnfd)", RestconfDescriptorHandler, attrs), ]) def get_msg(self, desc=None): if desc is None: desc = NsdYang.YangData_Nsd_NsdCatalog_Nsd(id=str(uuid.uuid4()), name="nsd_name") serializer = OnboardTestCase.DESC_SERIALIZER_MAP['nsd'] jstr = serializer.to_json_string(desc, project_ns=False) self._desc = jstr hdl = io.BytesIO(str.encode(jstr)) return serializer.from_file_hdl(hdl, ".json") def get_json(self, msg): serializer = OnboardTestCase.DESC_SERIALIZER_MAP['nsd'] json_data = serializer.to_json_string(msg, project_ns=True) return json.loads(json_data) @rift.test.dts.async_test def test_onboard_nsd(self): nsd_msg = self.get_msg() yield from self._loop.run_in_executor( None, functools.partial(self._onboarder.onboard, descriptor_msg=nsd_msg, auth=OnboardTestCase.AUTH)) self.assertEqual(self._handler_info.last_request_message, self.get_json(nsd_msg)) self.assertEqual(self._handler_info.last_descriptor_type, "nsd") self.assertEqual(self._handler_info.last_method, "POST") @rift.test.dts.async_test def test_update_nsd(self): nsd_msg = self.get_msg() yield from self._loop.run_in_executor( None, functools.partial(self._onboarder.update, descriptor_msg=nsd_msg, auth=OnboardTestCase.AUTH)) self.assertEqual(self._handler_info.last_request_message, self.get_json(nsd_msg)) self.assertEqual(self._handler_info.last_descriptor_type, "nsd") self.assertEqual(self._handler_info.last_method, "PUT") @rift.test.dts.async_test def test_bad_descriptor_type(self): nsd_msg = NsdYang.YangData_Nsd_NsdCatalog_Nsd() with self.assertRaises(TypeError): yield from self._loop.run_in_executor(None, self._onboarder.update, nsd_msg) with self.assertRaises(TypeError): yield from self._loop.run_in_executor(None, self._onboarder.onboard, nsd_msg) @rift.test.dts.async_test def test_bad_port(self): # Use a port not used by the instantiated server new_port = self._port - 1 self._onboarder.port = new_port nsd_msg = self.get_msg() with self.assertRaises(onboard.OnboardError): yield from self._loop.run_in_executor(None, self._onboarder.onboard, nsd_msg) with self.assertRaises(onboard.UpdateError): yield from self._loop.run_in_executor(None, self._onboarder.update, nsd_msg) @rift.test.dts.async_test def test_timeout(self): # Set the timeout to something minimal to speed up test self._onboarder.timeout = .1 nsd_msg = self.get_msg() # Force the request to timeout by running the call synchronously so the with self.assertRaises(onboard.OnboardError): self._onboarder.onboard(nsd_msg) # Force the request to timeout by running the call synchronously so the with self.assertRaises(onboard.UpdateError): self._onboarder.update(nsd_msg)