VirtualBox

source: vbox/trunk/src/VBox/ValidationKit/testmanager/core/restdispatcher.py@ 101505

Last change on this file since 101505 was 98103, checked in by vboxsync, 2 years ago

Copyright year updates by scm.

  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
File size: 16.7 KB
Line 
1# -*- coding: utf-8 -*-
2# $Id: restdispatcher.py 98103 2023-01-17 14:15:46Z vboxsync $
3
4"""
5Test Manager Core - REST cgi handler.
6"""
7
8__copyright__ = \
9"""
10Copyright (C) 2012-2023 Oracle and/or its affiliates.
11
12This file is part of VirtualBox base platform packages, as
13available from https://www.virtualbox.org.
14
15This program is free software; you can redistribute it and/or
16modify it under the terms of the GNU General Public License
17as published by the Free Software Foundation, in version 3 of the
18License.
19
20This program is distributed in the hope that it will be useful, but
21WITHOUT ANY WARRANTY; without even the implied warranty of
22MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
23General Public License for more details.
24
25You should have received a copy of the GNU General Public License
26along with this program; if not, see <https://www.gnu.org/licenses>.
27
28The contents of this file may alternatively be used under the terms
29of the Common Development and Distribution License Version 1.0
30(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
31in the VirtualBox distribution, in which case the provisions of the
32CDDL are applicable instead of those of the GPL.
33
34You may elect to license modified versions of this file under the
35terms and conditions of either the GPL or the CDDL or both.
36
37SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
38"""
39__version__ = "$Revision: 98103 $"
40
41
42# Standard python imports.
43import os;
44import sys;
45
46# Validation Kit imports.
47#from common import constants;
48from common import utils;
49from testmanager import config;
50#from testmanager.core import coreconsts;
51from testmanager.core.db import TMDatabaseConnection;
52from testmanager.core.base import TMExceptionBase, ModelDataBase;
53
54# Python 3 hacks:
55if sys.version_info[0] >= 3:
56 long = int; # pylint: disable=redefined-builtin,invalid-name
57
58
59#
60# Exceptions
61#
62
63class RestDispException(TMExceptionBase):
64 """
65 Exception class for the REST dispatcher.
66 """
67 def __init__(self, sMsg, iStatus):
68 TMExceptionBase.__init__(self, sMsg);
69 self.iStatus = iStatus;
70
71# 400
72class RestDispException400(RestDispException):
73 """ A 400 error """
74 def __init__(self, sMsg):
75 RestDispException.__init__(self, sMsg, 400);
76
77class RestUnknownParameters(RestDispException400):
78 """ Unknown parameter(s). """
79 pass; # pylint: disable=unnecessary-pass
80
81# 404
82class RestDispException404(RestDispException):
83 """ A 404 error """
84 def __init__(self, sMsg):
85 RestDispException.__init__(self, sMsg, 404);
86
87class RestBadPathException(RestDispException404):
88 """ We've got a bad path. """
89 pass; # pylint: disable=unnecessary-pass
90
91class RestBadParameter(RestDispException404):
92 """ Bad parameter. """
93 pass; # pylint: disable=unnecessary-pass
94
95class RestMissingParameter(RestDispException404):
96 """ Missing parameter. """
97 pass; # pylint: disable=unnecessary-pass
98
99
100
101class RestMain(object): # pylint: disable=too-few-public-methods
102 """
103 REST main dispatcher class.
104 """
105
106 ksParam_sPath = 'sPath';
107
108
109 def __init__(self, oSrvGlue):
110 self._oSrvGlue = oSrvGlue;
111 self._oDb = TMDatabaseConnection(oSrvGlue.dprint);
112 self._iFirstHandlerPath = 0;
113 self._iNextHandlerPath = 0;
114 self._sPath = None; # _getStandardParams / dispatchRequest sets this later on.
115 self._asPath = None; # _getStandardParams / dispatchRequest sets this later on.
116 self._sMethod = None; # _getStandardParams / dispatchRequest sets this later on.
117 self._dParams = None; # _getStandardParams / dispatchRequest sets this later on.
118 self._asCheckedParams = [];
119 self._dGetTree = {
120 'vcs': {
121 'changelog': self._handleVcsChangelog_Get,
122 'bugreferences': self._handleVcsBugReferences_Get,
123 },
124 };
125 self._dMethodTrees = {
126 'GET': self._dGetTree,
127 }
128
129 #
130 # Helpers.
131 #
132
133 def _getStringParam(self, sName, asValidValues = None, fStrip = False, sDefValue = None):
134 """
135 Gets a string parameter (stripped).
136
137 Raises exception if not found and no default is provided, or if the
138 value isn't found in asValidValues.
139 """
140 if sName not in self._dParams:
141 if sDefValue is None:
142 raise RestMissingParameter('%s parameter %s is missing' % (self._sPath, sName));
143 return sDefValue;
144 sValue = self._dParams[sName];
145 if isinstance(sValue, list):
146 if len(sValue) == 1:
147 sValue = sValue[0];
148 else:
149 raise RestBadParameter('%s parameter %s value is not a string but list: %s'
150 % (self._sPath, sName, sValue));
151 if fStrip:
152 sValue = sValue.strip();
153
154 if sName not in self._asCheckedParams:
155 self._asCheckedParams.append(sName);
156
157 if asValidValues is not None and sValue not in asValidValues:
158 raise RestBadParameter('%s parameter %s value "%s" not in %s '
159 % (self._sPath, sName, sValue, asValidValues));
160 return sValue;
161
162 def _getBoolParam(self, sName, fDefValue = None):
163 """
164 Gets a boolean parameter.
165
166 Raises exception if not found and no default is provided, or if not a
167 valid boolean.
168 """
169 sValue = self._getStringParam(sName, [ 'True', 'true', '1', 'False', 'false', '0'], sDefValue = str(fDefValue));
170 return sValue in ('True', 'true', '1',);
171
172 def _getIntParam(self, sName, iMin = None, iMax = None):
173 """
174 Gets a string parameter.
175 Raises exception if not found, not a valid integer, or if the value
176 isn't in the range defined by iMin and iMax.
177 """
178 sValue = self._getStringParam(sName);
179 try:
180 iValue = int(sValue, 0);
181 except:
182 raise RestBadParameter('%s parameter %s value "%s" cannot be convert to an integer'
183 % (self._sPath, sName, sValue));
184
185 if (iMin is not None and iValue < iMin) \
186 or (iMax is not None and iValue > iMax):
187 raise RestBadParameter('%s parameter %s value %d is out of range [%s..%s]'
188 % (self._sPath, sName, iValue, iMin, iMax));
189 return iValue;
190
191 def _getLongParam(self, sName, lMin = None, lMax = None, lDefValue = None):
192 """
193 Gets a string parameter.
194 Raises exception if not found, not a valid long integer, or if the value
195 isn't in the range defined by lMin and lMax.
196 """
197 sValue = self._getStringParam(sName, sDefValue = (str(lDefValue) if lDefValue is not None else None));
198 try:
199 lValue = long(sValue, 0);
200 except Exception as oXcpt:
201 raise RestBadParameter('%s parameter %s value "%s" cannot be convert to an integer (%s)'
202 % (self._sPath, sName, sValue, oXcpt));
203
204 if (lMin is not None and lValue < lMin) \
205 or (lMax is not None and lValue > lMax):
206 raise RestBadParameter('%s parameter %s value %d is out of range [%s..%s]'
207 % (self._sPath, sName, lValue, lMin, lMax));
208 return lValue;
209
210 def _checkForUnknownParameters(self):
211 """
212 Check if we've handled all parameters, raises exception if anything
213 unknown was found.
214 """
215
216 if len(self._asCheckedParams) != len(self._dParams):
217 sUnknownParams = '';
218 for sKey in self._dParams:
219 if sKey not in self._asCheckedParams:
220 sUnknownParams += ' ' + sKey + '=' + self._dParams[sKey];
221 raise RestUnknownParameters('Unknown parameters: ' + sUnknownParams);
222
223 return True;
224
225 def writeToMainLog(self, oTestSet, sText, fIgnoreSizeCheck = False):
226 """ Writes the text to the main log file. """
227
228 # Calc the file name and open the file.
229 sFile = os.path.join(config.g_ksFileAreaRootDir, oTestSet.sBaseFilename + '-main.log');
230 if not os.path.exists(os.path.dirname(sFile)):
231 os.makedirs(os.path.dirname(sFile), 0o755);
232
233 with open(sFile, 'ab') as oFile:
234 # Check the size.
235 fSizeOk = True;
236 if not fIgnoreSizeCheck:
237 oStat = os.fstat(oFile.fileno());
238 fSizeOk = oStat.st_size / (1024 * 1024) < config.g_kcMbMaxMainLog;
239
240 # Write the text.
241 if fSizeOk:
242 if sys.version_info[0] >= 3:
243 oFile.write(bytes(sText, 'utf-8'));
244 else:
245 oFile.write(sText);
246
247 return fSizeOk;
248
249 def _getNextPathElementString(self, sName, oDefault = None):
250 """
251 Gets the next handler specific path element.
252 Returns unprocessed string.
253 Throws exception
254 """
255 i = self._iNextHandlerPath;
256 if i < len(self._asPath):
257 self._iNextHandlerPath = i + 1;
258 return self._asPath[i];
259 if oDefault is None:
260 raise RestBadPathException('Requires a "%s" element after "%s"' % (sName, self._sPath,));
261 return oDefault;
262
263 def _getNextPathElementInt(self, sName, iDefault = None, iMin = None, iMax = None):
264 """
265 Gets the next handle specific path element as an integer.
266 Returns integer value.
267 Throws exception if not found or not a valid integer.
268 """
269 sValue = self._getNextPathElementString(sName, oDefault = iDefault);
270 try:
271 iValue = int(sValue);
272 except:
273 raise RestBadPathException('Not an integer "%s" (%s)' % (sValue, sName,));
274 if iMin is not None and iValue < iMin:
275 raise RestBadPathException('Integer "%s" value (%s) is too small, min %s' % (sValue, sName, iMin));
276 if iMax is not None and iValue > iMax:
277 raise RestBadPathException('Integer "%s" value (%s) is too large, max %s' % (sValue, sName, iMax));
278 return iValue;
279
280 def _getNextPathElementLong(self, sName, iDefault = None, iMin = None, iMax = None):
281 """
282 Gets the next handle specific path element as a long integer.
283 Returns integer value.
284 Throws exception if not found or not a valid integer.
285 """
286 sValue = self._getNextPathElementString(sName, oDefault = iDefault);
287 try:
288 iValue = long(sValue);
289 except:
290 raise RestBadPathException('Not an integer "%s" (%s)' % (sValue, sName,));
291 if iMin is not None and iValue < iMin:
292 raise RestBadPathException('Integer "%s" value (%s) is too small, min %s' % (sValue, sName, iMin));
293 if iMax is not None and iValue > iMax:
294 raise RestBadPathException('Integer "%s" value (%s) is too large, max %s' % (sValue, sName, iMax));
295 return iValue;
296
297 def _checkNoMorePathElements(self):
298 """
299 Checks that there are no more path elements.
300 Throws exception if there are.
301 """
302 i = self._iNextHandlerPath;
303 if i < len(self._asPath):
304 raise RestBadPathException('Unknown subpath "%s" below "%s"' %
305 ('/'.join(self._asPath[i:]), '/'.join(self._asPath[:i]),));
306 return True;
307
308 def _doneParsingArguments(self):
309 """
310 Checks that there are no more path elements or unhandled parameters.
311 Throws exception if there are.
312 """
313 self._checkNoMorePathElements();
314 self._checkForUnknownParameters();
315 return True;
316
317 def _dataArrayToJsonReply(self, aoData, sName = 'aoData', dExtraFields = None, iStatus = 200):
318 """
319 Converts aoData into an array objects
320 return True.
321 """
322 self._oSrvGlue.setContentType('application/json');
323 self._oSrvGlue.setStatus(iStatus);
324 self._oSrvGlue.write(u'{\n');
325 if dExtraFields:
326 for sKey in dExtraFields:
327 self._oSrvGlue.write(u' "%s": %s,\n' % (sKey, ModelDataBase.genericToJson(dExtraFields[sKey]),));
328 self._oSrvGlue.write(u' "c%s": %u,\n' % (sName[2:],len(aoData),));
329 self._oSrvGlue.write(u' "%s": [\n' % (sName,));
330 for i, oData in enumerate(aoData):
331 if i > 0:
332 self._oSrvGlue.write(u',\n');
333 self._oSrvGlue.write(ModelDataBase.genericToJson(oData));
334 self._oSrvGlue.write(u' ]\n');
335 ## @todo if config.g_kfWebUiSqlDebug:
336 self._oSrvGlue.write(u'}\n');
337 self._oSrvGlue.flush();
338 return True;
339
340
341 #
342 # Handlers.
343 #
344
345 def _handleVcsChangelog_Get(self):
346 """ GET /vcs/changelog/{sRepository}/{iStartRev}[/{cEntriesBack}] """
347 # Parse arguments
348 sRepository = self._getNextPathElementString('sRepository');
349 iStartRev = self._getNextPathElementInt('iStartRev', iMin = 0);
350 cEntriesBack = self._getNextPathElementInt('cEntriesBack', iDefault = 32, iMin = 0, iMax = 8192);
351 self._checkNoMorePathElements();
352 self._checkForUnknownParameters();
353
354 # Execute it.
355 from testmanager.core.vcsrevisions import VcsRevisionLogic;
356 oLogic = VcsRevisionLogic(self._oDb);
357 return self._dataArrayToJsonReply(oLogic.fetchTimeline(sRepository, iStartRev, cEntriesBack), 'aoCommits',
358 { 'sTracChangesetUrlFmt':
359 config.g_ksTracChangsetUrlFmt.replace('%(sRepository)s', sRepository), } );
360
361 def _handleVcsBugReferences_Get(self):
362 """ GET /vcs/bugreferences/{sTrackerId}/{lBugId} """
363 # Parse arguments
364 sTrackerId = self._getNextPathElementString('sTrackerId');
365 lBugId = self._getNextPathElementLong('lBugId', iMin = 0);
366 self._checkNoMorePathElements();
367 self._checkForUnknownParameters();
368
369 # Execute it.
370 from testmanager.core.vcsbugreference import VcsBugReferenceLogic;
371 oLogic = VcsBugReferenceLogic(self._oDb);
372 oLogic.fetchForBug(sTrackerId, lBugId)
373 return self._dataArrayToJsonReply(oLogic.fetchForBug(sTrackerId, lBugId), 'aoCommits',
374 { 'sTracChangesetUrlFmt': config.g_ksTracChangsetUrlFmt, } );
375
376
377 #
378 # Dispatching.
379 #
380
381 def _dispatchRequestCommon(self):
382 """
383 Dispatches the incoming request after have gotten the path and parameters.
384
385 Will raise RestDispException on failure.
386 """
387
388 #
389 # Split up the path.
390 #
391 asPath = self._sPath.split('/');
392 self._asPath = asPath;
393
394 #
395 # Get the method and the corresponding handler tree.
396 #
397 try:
398 sMethod = self._oSrvGlue.getMethod();
399 except Exception as oXcpt:
400 raise RestDispException('Error retriving request method: %s' % (oXcpt,), 400);
401 self._sMethod = sMethod;
402
403 try:
404 dTree = self._dMethodTrees[sMethod];
405 except KeyError:
406 raise RestDispException('Unsupported method %s' % (sMethod,), 405);
407
408 #
409 # Walk the path till we find a handler for it.
410 #
411 iPath = 0;
412 while iPath < len(asPath):
413 try:
414 oTreeOrHandler = dTree[asPath[iPath]];
415 except KeyError:
416 raise RestBadPathException('Path element #%u "%s" not found (path="%s")' % (iPath, asPath[iPath], self._sPath));
417 iPath += 1;
418 if isinstance(oTreeOrHandler, dict):
419 dTree = oTreeOrHandler;
420 else:
421 #
422 # Call the handler.
423 #
424 self._iFirstHandlerPath = iPath;
425 self._iNextHandlerPath = iPath;
426 return oTreeOrHandler();
427
428 raise RestBadPathException('Empty path (%s)' % (self._sPath,));
429
430 def dispatchRequest(self):
431 """
432 Dispatches the incoming request where the path is given as an argument.
433
434 Will raise RestDispException on failure.
435 """
436
437 #
438 # Get the parameters.
439 #
440 try:
441 dParams = self._oSrvGlue.getParameters();
442 except Exception as oXcpt:
443 raise RestDispException('Error retriving parameters: %s' % (oXcpt,), 500);
444 self._dParams = dParams;
445
446 #
447 # Get the path parameter.
448 #
449 if self.ksParam_sPath not in dParams:
450 raise RestDispException('No "%s" parameter in request (params: %s)' % (self.ksParam_sPath, dParams,), 500);
451 self._sPath = self._getStringParam(self.ksParam_sPath);
452 assert utils.isString(self._sPath);
453
454 return self._dispatchRequestCommon();
455
Note: See TracBrowser for help on using the repository browser.

© 2024 Oracle Support Privacy / Do Not Sell My Info Terms of Use Trademark Policy Automated Access Etiquette