blob: d0f323d07c87a9afa768d638b048f2f2b17beeeb [file] [log] [blame]
Jeremy Mordkoff6f07e6f2016-09-07 18:56:51 -04001#!/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
20import argparse
21import asyncio
22import base64
23import concurrent.futures
24import io
Philip Josephba63fbf2017-04-04 15:46:10 +053025import json
Jeremy Mordkoff6f07e6f2016-09-07 18:56:51 -040026import logging
27import os
28import sys
29import tornado.testing
30import tornado.web
31import unittest
32import uuid
33import xmlrunner
34
35from rift.package import convert
36from rift.tasklets.rwlaunchpad import onboard
37import rift.test.dts
38
39import gi
Philip Josephba63fbf2017-04-04 15:46:10 +053040gi.require_version('NsdYang', '1.0')
41gi.require_version('VnfdYang', '1.0')
Philip Joseph4f810f22017-03-07 23:09:10 +053042gi.require_version('ProjectNsdYang', '1.0')
43gi.require_version('ProjectVnfdYang', '1.0')
Jeremy Mordkoff6f07e6f2016-09-07 18:56:51 -040044
45from gi.repository import (
Philip Josephba63fbf2017-04-04 15:46:10 +053046 NsdYang,
47 VnfdYang,
48 ProjectNsdYang,
49 ProjectVnfdYang,
Jeremy Mordkoff6f07e6f2016-09-07 18:56:51 -040050 )
51
52
53class RestconfDescriptorHandler(tornado.web.RequestHandler):
Jeremy Mordkoff6f07e6f2016-09-07 18:56:51 -040054 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 Josephba63fbf2017-04-04 15:46:10 +0530132 if descriptor_type not in ['nsd', 'vnfd']:
Jeremy Mordkoff6f07e6f2016-09-07 18:56:51 -0400133 raise ValueError("Unsupported descriptor type: %s" % descriptor_type)
134
Philip Josephba63fbf2017-04-04 15:46:10 +0530135 body = convert.decode(self.request.body)
136 self._logger.debug("Received msg: {}".format(body))
Jeremy Mordkoff6f07e6f2016-09-07 18:56:51 -0400137
138 try:
Philip Josephba63fbf2017-04-04 15:46:10 +0530139 message = json.loads(body)
Jeremy Mordkoff6f07e6f2016-09-07 18:56:51 -0400140 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 Josephba63fbf2017-04-04 15:46:10 +0530151 self._logger.debug("Received a valid descriptor request: {}".format(message))
Jeremy Mordkoff6f07e6f2016-09-07 18:56:51 -0400152
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
188class 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
195class OnboardTestCase(tornado.testing.AsyncHTTPTestCase):
Philip Josephba63fbf2017-04-04 15:46:10 +0530196 DESC_SERIALIZER_MAP = {
197 "nsd": convert.NsdSerializer(),
198 "vnfd": convert.VnfdSerializer(),
199 }
200
Jeremy Mordkoff6f07e6f2016-09-07 18:56:51 -0400201 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 Josephba63fbf2017-04-04 15:46:10 +0530219 (r"/api/config/project/default/.*/(nsd|vnfd)",
220 RestconfDescriptorHandler, attrs),
Jeremy Mordkoff6f07e6f2016-09-07 18:56:51 -0400221 ])
222
Philip Josephba63fbf2017-04-04 15:46:10 +0530223
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 Mordkoff6f07e6f2016-09-07 18:56:51 -0400238 @rift.test.dts.async_test
239 def test_onboard_nsd(self):
Philip Josephba63fbf2017-04-04 15:46:10 +0530240 nsd_msg = self.get_msg()
Jeremy Mordkoff6f07e6f2016-09-07 18:56:51 -0400241 yield from self._loop.run_in_executor(None, self._onboarder.onboard, nsd_msg)
Philip Josephba63fbf2017-04-04 15:46:10 +0530242 self.assertEqual(self._handler_info.last_request_message, self.get_json(nsd_msg))
Jeremy Mordkoff6f07e6f2016-09-07 18:56:51 -0400243 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 Josephba63fbf2017-04-04 15:46:10 +0530248 nsd_msg = self.get_msg()
Jeremy Mordkoff6f07e6f2016-09-07 18:56:51 -0400249 yield from self._loop.run_in_executor(None, self._onboarder.update, nsd_msg)
Philip Josephba63fbf2017-04-04 15:46:10 +0530250 self.assertEqual(self._handler_info.last_request_message, self.get_json(nsd_msg))
Jeremy Mordkoff6f07e6f2016-09-07 18:56:51 -0400251 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 Josephba63fbf2017-04-04 15:46:10 +0530256 nsd_msg = NsdYang.YangData_Nsd_NsdCatalog_Nsd()
Jeremy Mordkoff6f07e6f2016-09-07 18:56:51 -0400257 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 Josephba63fbf2017-04-04 15:46:10 +0530268 nsd_msg = self.get_msg()
Jeremy Mordkoff6f07e6f2016-09-07 18:56:51 -0400269
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 Josephba63fbf2017-04-04 15:46:10 +0530281 nsd_msg = self.get_msg()
Jeremy Mordkoff6f07e6f2016-09-07 18:56:51 -0400282
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
292def 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
312if __name__ == '__main__':
313 main()