| Jeremy Mordkoff | 6f07e6f | 2016-09-07 18:56:51 -0400 | [diff] [blame] | 1 | #!/usr/bin/env python3 |
| 2 | |
| 3 | # |
| 4 | # Copyright 2016 RIFT.IO Inc |
| 5 | # |
| 6 | # Licensed under the Apache License, Version 2.0 (the "License"); |
| 7 | # you may not use this file except in compliance with the License. |
| 8 | # You may obtain a copy of the License at |
| 9 | # |
| 10 | # http://www.apache.org/licenses/LICENSE-2.0 |
| 11 | # |
| 12 | # Unless required by applicable law or agreed to in writing, software |
| 13 | # distributed under the License is distributed on an "AS IS" BASIS, |
| 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 15 | # See the License for the specific language governing permissions and |
| 16 | # limitations under the License. |
| 17 | # |
| 18 | |
| 19 | |
| 20 | import argparse |
| 21 | import asyncio |
| 22 | import base64 |
| 23 | import concurrent.futures |
| 24 | import io |
| Philip Joseph | ba63fbf | 2017-04-04 15:46:10 +0530 | [diff] [blame] | 25 | import json |
| Jeremy Mordkoff | 6f07e6f | 2016-09-07 18:56:51 -0400 | [diff] [blame] | 26 | import logging |
| 27 | import os |
| 28 | import sys |
| 29 | import tornado.testing |
| 30 | import tornado.web |
| 31 | import unittest |
| 32 | import uuid |
| 33 | import xmlrunner |
| 34 | |
| 35 | from rift.package import convert |
| 36 | from rift.tasklets.rwlaunchpad import onboard |
| 37 | import rift.test.dts |
| 38 | |
| 39 | import gi |
| Philip Joseph | ba63fbf | 2017-04-04 15:46:10 +0530 | [diff] [blame] | 40 | gi.require_version('NsdYang', '1.0') |
| 41 | gi.require_version('VnfdYang', '1.0') |
| Philip Joseph | 4f810f2 | 2017-03-07 23:09:10 +0530 | [diff] [blame] | 42 | gi.require_version('ProjectNsdYang', '1.0') |
| 43 | gi.require_version('ProjectVnfdYang', '1.0') |
| Jeremy Mordkoff | 6f07e6f | 2016-09-07 18:56:51 -0400 | [diff] [blame] | 44 | |
| 45 | from gi.repository import ( |
| Philip Joseph | ba63fbf | 2017-04-04 15:46:10 +0530 | [diff] [blame] | 46 | NsdYang, |
| 47 | VnfdYang, |
| 48 | ProjectNsdYang, |
| 49 | ProjectVnfdYang, |
| Jeremy Mordkoff | 6f07e6f | 2016-09-07 18:56:51 -0400 | [diff] [blame] | 50 | ) |
| 51 | |
| 52 | |
| 53 | class RestconfDescriptorHandler(tornado.web.RequestHandler): |
| Jeremy Mordkoff | 6f07e6f | 2016-09-07 18:56:51 -0400 | [diff] [blame] | 54 | class AuthError(Exception): |
| 55 | pass |
| 56 | |
| 57 | |
| 58 | class ContentTypeError(Exception): |
| 59 | pass |
| 60 | |
| 61 | |
| 62 | class RequestBodyError(Exception): |
| 63 | pass |
| 64 | |
| 65 | |
| 66 | def initialize(self, log, auth, info): |
| 67 | self._auth = auth |
| 68 | # The superclass has self._log already defined so use a different name |
| 69 | self._logger = log |
| 70 | self._info = info |
| 71 | self._logger.debug('Created restconf descriptor handler') |
| 72 | |
| 73 | def _verify_auth(self): |
| 74 | if self._auth is None: |
| 75 | return None |
| 76 | |
| 77 | auth_header = self.request.headers.get('Authorization') |
| 78 | if auth_header is None or not auth_header.startswith('Basic '): |
| 79 | self.set_status(401) |
| 80 | self.set_header('WWW-Authenticate', 'Basic realm=Restricted') |
| 81 | self._transforms = [] |
| 82 | self.finish() |
| 83 | |
| 84 | msg = "Missing Authorization header" |
| 85 | self._logger.error(msg) |
| 86 | raise RestconfDescriptorHandler.AuthError(msg) |
| 87 | |
| 88 | auth_header = auth_header.encode('ascii') |
| 89 | auth_decoded = base64.decodebytes(auth_header[6:]).decode() |
| 90 | login, password = auth_decoded.split(':', 2) |
| 91 | login = login |
| 92 | password = password |
| 93 | is_auth = ((login, password) == self._auth) |
| 94 | |
| 95 | if not is_auth: |
| 96 | self.set_status(401) |
| 97 | self.set_header('WWW-Authenticate', 'Basic realm=Restricted') |
| 98 | self._transforms = [] |
| 99 | self.finish() |
| 100 | |
| 101 | msg = "Incorrect username and password in auth header: got {}, expected {}".format( |
| 102 | (login, password), self._auth |
| 103 | ) |
| 104 | self._logger.error(msg) |
| 105 | raise RestconfDescriptorHandler.AuthError(msg) |
| 106 | |
| 107 | def _verify_content_type_header(self): |
| 108 | content_type_header = self.request.headers.get('content-type') |
| 109 | if content_type_header is None: |
| 110 | self.set_status(415) |
| 111 | self._transforms = [] |
| 112 | self.finish() |
| 113 | |
| 114 | msg = "Missing content-type header" |
| 115 | self._logger.error(msg) |
| 116 | raise RestconfDescriptorHandler.ContentTypeError(msg) |
| 117 | |
| 118 | if content_type_header != "application/vnd.yang.data+json": |
| 119 | self.set_status(415) |
| 120 | self._transforms = [] |
| 121 | self.finish() |
| 122 | |
| 123 | msg = "Unsupported content type: %s" % content_type_header |
| 124 | self._logger.error(msg) |
| 125 | raise RestconfDescriptorHandler.ContentTypeError(msg) |
| 126 | |
| 127 | def _verify_headers(self): |
| 128 | self._verify_auth() |
| 129 | self._verify_content_type_header() |
| 130 | |
| 131 | def _verify_request_body(self, descriptor_type): |
| Philip Joseph | ba63fbf | 2017-04-04 15:46:10 +0530 | [diff] [blame] | 132 | if descriptor_type not in ['nsd', 'vnfd']: |
| Jeremy Mordkoff | 6f07e6f | 2016-09-07 18:56:51 -0400 | [diff] [blame] | 133 | raise ValueError("Unsupported descriptor type: %s" % descriptor_type) |
| 134 | |
| Philip Joseph | ba63fbf | 2017-04-04 15:46:10 +0530 | [diff] [blame] | 135 | body = convert.decode(self.request.body) |
| 136 | self._logger.debug("Received msg: {}".format(body)) |
| Jeremy Mordkoff | 6f07e6f | 2016-09-07 18:56:51 -0400 | [diff] [blame] | 137 | |
| 138 | try: |
| Philip Joseph | ba63fbf | 2017-04-04 15:46:10 +0530 | [diff] [blame] | 139 | message = json.loads(body) |
| Jeremy Mordkoff | 6f07e6f | 2016-09-07 18:56:51 -0400 | [diff] [blame] | 140 | except convert.SerializationError as e: |
| 141 | self.set_status(400) |
| 142 | self._transforms = [] |
| 143 | self.finish() |
| 144 | |
| 145 | msg = "Descriptor request body not valid" |
| 146 | self._logger.error(msg) |
| 147 | raise RestconfDescriptorHandler.RequestBodyError() from e |
| 148 | |
| 149 | self._info.last_request_message = message |
| 150 | |
| Philip Joseph | ba63fbf | 2017-04-04 15:46:10 +0530 | [diff] [blame] | 151 | self._logger.debug("Received a valid descriptor request: {}".format(message)) |
| Jeremy Mordkoff | 6f07e6f | 2016-09-07 18:56:51 -0400 | [diff] [blame] | 152 | |
| 153 | def put(self, descriptor_type): |
| 154 | self._info.last_descriptor_type = descriptor_type |
| 155 | self._info.last_method = "PUT" |
| 156 | |
| 157 | try: |
| 158 | self._verify_headers() |
| 159 | except (RestconfDescriptorHandler.AuthError, |
| 160 | RestconfDescriptorHandler.ContentTypeError): |
| 161 | return None |
| 162 | |
| 163 | try: |
| 164 | self._verify_request_body(descriptor_type) |
| 165 | except RestconfDescriptorHandler.RequestBodyError: |
| 166 | return None |
| 167 | |
| 168 | self.write("Response doesn't matter?") |
| 169 | |
| 170 | def post(self, descriptor_type): |
| 171 | self._info.last_descriptor_type = descriptor_type |
| 172 | self._info.last_method = "POST" |
| 173 | |
| 174 | try: |
| 175 | self._verify_headers() |
| 176 | except (RestconfDescriptorHandler.AuthError, |
| 177 | RestconfDescriptorHandler.ContentTypeError): |
| 178 | return None |
| 179 | |
| 180 | try: |
| 181 | self._verify_request_body(descriptor_type) |
| 182 | except RestconfDescriptorHandler.RequestBodyError: |
| 183 | return None |
| 184 | |
| 185 | self.write("Response doesn't matter?") |
| 186 | |
| 187 | |
| 188 | class HandlerInfo(object): |
| 189 | def __init__(self): |
| 190 | self.last_request_message = None |
| 191 | self.last_descriptor_type = None |
| 192 | self.last_method = None |
| 193 | |
| 194 | |
| 195 | class OnboardTestCase(tornado.testing.AsyncHTTPTestCase): |
| Philip Joseph | ba63fbf | 2017-04-04 15:46:10 +0530 | [diff] [blame] | 196 | DESC_SERIALIZER_MAP = { |
| 197 | "nsd": convert.NsdSerializer(), |
| 198 | "vnfd": convert.VnfdSerializer(), |
| 199 | } |
| 200 | |
| Jeremy Mordkoff | 6f07e6f | 2016-09-07 18:56:51 -0400 | [diff] [blame] | 201 | AUTH = ("admin", "admin") |
| 202 | def setUp(self): |
| 203 | self._log = logging.getLogger(__file__) |
| 204 | self._loop = asyncio.get_event_loop() |
| 205 | |
| 206 | self._handler_info = HandlerInfo() |
| 207 | super().setUp() |
| 208 | self._port = self.get_http_port() |
| 209 | self._onboarder = onboard.DescriptorOnboarder( |
| 210 | log=self._log, port=self._port |
| 211 | ) |
| 212 | |
| 213 | def get_new_ioloop(self): |
| 214 | return tornado.platform.asyncio.AsyncIOMainLoop() |
| 215 | |
| 216 | def get_app(self): |
| 217 | attrs = dict(auth=OnboardTestCase.AUTH, log=self._log, info=self._handler_info) |
| 218 | return tornado.web.Application([ |
| Philip Joseph | ba63fbf | 2017-04-04 15:46:10 +0530 | [diff] [blame] | 219 | (r"/api/config/project/default/.*/(nsd|vnfd)", |
| 220 | RestconfDescriptorHandler, attrs), |
| Jeremy Mordkoff | 6f07e6f | 2016-09-07 18:56:51 -0400 | [diff] [blame] | 221 | ]) |
| 222 | |
| Philip Joseph | ba63fbf | 2017-04-04 15:46:10 +0530 | [diff] [blame] | 223 | |
| 224 | def get_msg(self, desc=None): |
| 225 | if desc is None: |
| 226 | desc = NsdYang.YangData_Nsd_NsdCatalog_Nsd(id=str(uuid.uuid4()), name="nsd_name") |
| 227 | serializer = OnboardTestCase.DESC_SERIALIZER_MAP['nsd'] |
| 228 | jstr = serializer.to_json_string(desc, project_ns=False) |
| 229 | self._desc = jstr |
| 230 | hdl = io.BytesIO(str.encode(jstr)) |
| 231 | return serializer.from_file_hdl(hdl, ".json") |
| 232 | |
| 233 | def get_json(self, msg): |
| 234 | serializer = OnboardTestCase.DESC_SERIALIZER_MAP['nsd'] |
| 235 | json_data = serializer.to_json_string(msg, project_ns=True) |
| 236 | return json.loads(json_data) |
| 237 | |
| Jeremy Mordkoff | 6f07e6f | 2016-09-07 18:56:51 -0400 | [diff] [blame] | 238 | @rift.test.dts.async_test |
| 239 | def test_onboard_nsd(self): |
| Philip Joseph | ba63fbf | 2017-04-04 15:46:10 +0530 | [diff] [blame] | 240 | nsd_msg = self.get_msg() |
| Jeremy Mordkoff | 6f07e6f | 2016-09-07 18:56:51 -0400 | [diff] [blame] | 241 | yield from self._loop.run_in_executor(None, self._onboarder.onboard, nsd_msg) |
| Philip Joseph | ba63fbf | 2017-04-04 15:46:10 +0530 | [diff] [blame] | 242 | self.assertEqual(self._handler_info.last_request_message, self.get_json(nsd_msg)) |
| Jeremy Mordkoff | 6f07e6f | 2016-09-07 18:56:51 -0400 | [diff] [blame] | 243 | self.assertEqual(self._handler_info.last_descriptor_type, "nsd") |
| 244 | self.assertEqual(self._handler_info.last_method, "POST") |
| 245 | |
| 246 | @rift.test.dts.async_test |
| 247 | def test_update_nsd(self): |
| Philip Joseph | ba63fbf | 2017-04-04 15:46:10 +0530 | [diff] [blame] | 248 | nsd_msg = self.get_msg() |
| Jeremy Mordkoff | 6f07e6f | 2016-09-07 18:56:51 -0400 | [diff] [blame] | 249 | yield from self._loop.run_in_executor(None, self._onboarder.update, nsd_msg) |
| Philip Joseph | ba63fbf | 2017-04-04 15:46:10 +0530 | [diff] [blame] | 250 | self.assertEqual(self._handler_info.last_request_message, self.get_json(nsd_msg)) |
| Jeremy Mordkoff | 6f07e6f | 2016-09-07 18:56:51 -0400 | [diff] [blame] | 251 | self.assertEqual(self._handler_info.last_descriptor_type, "nsd") |
| 252 | self.assertEqual(self._handler_info.last_method, "PUT") |
| 253 | |
| 254 | @rift.test.dts.async_test |
| 255 | def test_bad_descriptor_type(self): |
| Philip Joseph | ba63fbf | 2017-04-04 15:46:10 +0530 | [diff] [blame] | 256 | nsd_msg = NsdYang.YangData_Nsd_NsdCatalog_Nsd() |
| Jeremy Mordkoff | 6f07e6f | 2016-09-07 18:56:51 -0400 | [diff] [blame] | 257 | with self.assertRaises(TypeError): |
| 258 | yield from self._loop.run_in_executor(None, self._onboarder.update, nsd_msg) |
| 259 | |
| 260 | with self.assertRaises(TypeError): |
| 261 | yield from self._loop.run_in_executor(None, self._onboarder.onboard, nsd_msg) |
| 262 | |
| 263 | @rift.test.dts.async_test |
| 264 | def test_bad_port(self): |
| 265 | # Use a port not used by the instantiated server |
| 266 | new_port = self._port - 1 |
| 267 | self._onboarder.port = new_port |
| Philip Joseph | ba63fbf | 2017-04-04 15:46:10 +0530 | [diff] [blame] | 268 | nsd_msg = self.get_msg() |
| Jeremy Mordkoff | 6f07e6f | 2016-09-07 18:56:51 -0400 | [diff] [blame] | 269 | |
| 270 | with self.assertRaises(onboard.OnboardError): |
| 271 | yield from self._loop.run_in_executor(None, self._onboarder.onboard, nsd_msg) |
| 272 | |
| 273 | with self.assertRaises(onboard.UpdateError): |
| 274 | yield from self._loop.run_in_executor(None, self._onboarder.update, nsd_msg) |
| 275 | |
| 276 | @rift.test.dts.async_test |
| 277 | def test_timeout(self): |
| 278 | # Set the timeout to something minimal to speed up test |
| 279 | self._onboarder.timeout = .1 |
| 280 | |
| Philip Joseph | ba63fbf | 2017-04-04 15:46:10 +0530 | [diff] [blame] | 281 | nsd_msg = self.get_msg() |
| Jeremy Mordkoff | 6f07e6f | 2016-09-07 18:56:51 -0400 | [diff] [blame] | 282 | |
| 283 | # Force the request to timeout by running the call synchronously so the |
| 284 | with self.assertRaises(onboard.OnboardError): |
| 285 | self._onboarder.onboard(nsd_msg) |
| 286 | |
| 287 | # Force the request to timeout by running the call synchronously so the |
| 288 | with self.assertRaises(onboard.UpdateError): |
| 289 | self._onboarder.update(nsd_msg) |
| 290 | |
| 291 | |
| 292 | def main(argv=sys.argv[1:]): |
| 293 | logging.basicConfig(format='TEST %(message)s') |
| 294 | |
| 295 | runner = xmlrunner.XMLTestRunner(output=os.environ["RIFT_MODULE_TEST"]) |
| 296 | parser = argparse.ArgumentParser() |
| 297 | parser.add_argument('-v', '--verbose', action='store_true') |
| 298 | parser.add_argument('-n', '--no-runner', action='store_true') |
| 299 | |
| 300 | args, unknown = parser.parse_known_args(argv) |
| 301 | if args.no_runner: |
| 302 | runner = None |
| 303 | |
| 304 | # Set the global logging level |
| 305 | logging.getLogger().setLevel(logging.DEBUG if args.verbose else logging.ERROR) |
| 306 | |
| 307 | # The unittest framework requires a program name, so use the name of this |
| 308 | # file instead (we do not want to have to pass a fake program name to main |
| 309 | # when this is called from the interpreter). |
| 310 | unittest.main(argv=[__file__] + unknown + ["-v"], testRunner=runner) |
| 311 | |
| 312 | if __name__ == '__main__': |
| 313 | main() |