VirtualBox

source: vbox/trunk/src/VBox/ValidationKit/testboxscript/testboxconnection.py

Last change on this file was 106061, checked in by vboxsync, 3 days ago

Copyright year updates by scm.

  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
File size: 12.0 KB
Line 
1# -*- coding: utf-8 -*-
2# $Id: testboxconnection.py 106061 2024-09-16 14:03:52Z vboxsync $
3
4"""
5TestBox Script - HTTP Connection Handling.
6"""
7
8__copyright__ = \
9"""
10Copyright (C) 2012-2024 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: 106061 $"
40
41
42# Standard python imports.
43import sys;
44if sys.version_info[0] >= 3:
45 import http.client as httplib; # pylint: disable=import-error,no-name-in-module
46 import urllib.parse as urlparse; # pylint: disable=import-error,no-name-in-module
47 from urllib.parse import urlencode as urllib_urlencode; # pylint: disable=import-error,no-name-in-module
48else:
49 import httplib; # pylint: disable=import-error,no-name-in-module
50 import urlparse; # pylint: disable=import-error,no-name-in-module
51 from urllib import urlencode as urllib_urlencode; # pylint: disable=import-error,no-name-in-module
52
53# Validation Kit imports.
54from common import constants
55from common import utils
56import testboxcommons
57
58
59
60class TestBoxResponse(object):
61 """
62 Response object return by TestBoxConnection.request().
63 """
64 def __init__(self, oResponse):
65 """
66 Convert the HTTPResponse to a dictionary, raising TestBoxException on
67 malformed response.
68 """
69 if oResponse is not None:
70 # Read the whole response (so we can log it).
71 sBody = oResponse.read();
72 sBody = sBody.decode('utf-8');
73
74 # Check the content type.
75 sContentType = oResponse.getheader('Content-Type');
76 if sContentType is None or sContentType != 'application/x-www-form-urlencoded; charset=utf-8':
77 testboxcommons.log('SERVER RESPONSE: Content-Type: %s' % (sContentType,));
78 testboxcommons.log('SERVER RESPONSE: %s' % (sBody.rstrip(),))
79 raise testboxcommons.TestBoxException('Invalid server response type: "%s"' % (sContentType,));
80
81 # Parse the body (this should be the exact reverse of what
82 # TestBoxConnection.postRequestRaw).
83 ##testboxcommons.log2('SERVER RESPONSE: "%s"' % (sBody,))
84 self._dResponse = urlparse.parse_qs(sBody, strict_parsing=True);
85
86 # Convert the dictionary from 'field:values' to 'field:value'. Fail
87 # if a field has more than one value (i.e. given more than once).
88 for sField in self._dResponse:
89 if len(self._dResponse[sField]) != 1:
90 raise testboxcommons.TestBoxException('The field "%s" appears more than once in the server response' \
91 % (sField,));
92 self._dResponse[sField] = self._dResponse[sField][0]
93 else:
94 # Special case, dummy response object.
95 self._dResponse = {};
96 # Done.
97
98 def getStringChecked(self, sField):
99 """
100 Check if specified field is present in server response and returns it as string.
101 If not present, a fitting exception will be raised.
102 """
103 if not sField in self._dResponse:
104 raise testboxcommons.TestBoxException('Required data (' + str(sField) + ') was not found in server response');
105 return str(self._dResponse[sField]).strip();
106
107 def getIntChecked(self, sField, iMin = None, iMax = None):
108 """
109 Check if specified field is present in server response and returns it as integer.
110 If not present, a fitting exception will be raised.
111
112 The iMin and iMax values are inclusive.
113 """
114 if not sField in self._dResponse:
115 raise testboxcommons.TestBoxException('Required data (' + str(sField) + ') was not found in server response')
116 try:
117 iValue = int(self._dResponse[sField]);
118 except:
119 raise testboxcommons.TestBoxException('Malformed integer field %s: "%s"' % (sField, self._dResponse[sField]));
120
121 if (iMin is not None and iValue < iMin) \
122 or (iMax is not None and iValue > iMax):
123 raise testboxcommons.TestBoxException('Value (%d) of field %s is out of range [%s..%s]' \
124 % (iValue, sField, iMin, iMax));
125 return iValue;
126
127 def checkParameterCount(self, cExpected):
128 """
129 Checks the parameter count, raise TestBoxException if it doesn't meet
130 the expectations.
131 """
132 if len(self._dResponse) != cExpected:
133 raise testboxcommons.TestBoxException('Expected %d parameters, server sent %d' % (cExpected, len(self._dResponse)));
134 return True;
135
136 def toString(self):
137 """
138 Convers the response to a string (for debugging purposes).
139 """
140 return str(self._dResponse);
141
142
143class TestBoxConnection(object):
144 """
145 Wrapper around HTTPConnection.
146 """
147
148 def __init__(self, sTestManagerUrl, sTestBoxId, sTestBoxUuid, fLongTimeout = False):
149 """
150 Constructor.
151 """
152 self._oConn = None;
153 self._oParsedUrl = urlparse.urlparse(sTestManagerUrl);
154 self._sTestBoxId = sTestBoxId;
155 self._sTestBoxUuid = sTestBoxUuid;
156
157 #
158 # Connect to it - may raise exception on failure.
159 # When connecting we're using a 15 second timeout, we increase it later.
160 #
161 if self._oParsedUrl.scheme == 'https': # pylint: disable=no-member
162 fnCtor = httplib.HTTPSConnection;
163 else:
164 fnCtor = httplib.HTTPConnection;
165 if sys.version_info[0] >= 3 \
166 or (sys.version_info[0] == 2 and sys.version_info[1] >= 6):
167
168 self._oConn = fnCtor(self._oParsedUrl.hostname, timeout=15);
169 else:
170 self._oConn = fnCtor(self._oParsedUrl.hostname);
171
172 if self._oConn.sock is None:
173 self._oConn.connect();
174
175 #
176 # Increase the timeout for the non-connect operations.
177 #
178 try:
179 self._oConn.sock.settimeout(5*60 if fLongTimeout else 1 * 60);
180 except:
181 pass;
182
183 ##testboxcommons.log2('hostname=%s timeout=%u' % (self._oParsedUrl.hostname, self._oConn.sock.gettimeout()));
184
185 def __del__(self):
186 """ Makes sure the connection is really closed on destruction """
187 self.close()
188
189 def close(self):
190 """ Closes the connection """
191 if self._oConn is not None:
192 self._oConn.close();
193 self._oConn = None;
194
195 def postRequestRaw(self, sAction, dParams):
196 """
197 Posts a request to the test manager and gets the response. The dParams
198 argument is a dictionary of unencoded key-value pairs (will be
199 modified).
200 Raises exception on failure.
201 """
202 dHeader = \
203 {
204 'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8',
205 'User-Agent': 'TestBoxScript/%s.0 (%s, %s)' % (__version__, utils.getHostOs(), utils.getHostArch()),
206 'Accept': 'text/plain,application/x-www-form-urlencoded',
207 'Accept-Encoding': 'identity',
208 'Cache-Control': 'max-age=0',
209 'Connection': 'keep-alive',
210 };
211 sServerPath = '/%s/testboxdisp.py' % (self._oParsedUrl.path.strip('/'),); # pylint: disable=no-member
212 dParams[constants.tbreq.ALL_PARAM_ACTION] = sAction;
213 sBody = urllib_urlencode(dParams);
214 ##testboxcommons.log2('sServerPath=%s' % (sServerPath,));
215 try:
216 self._oConn.request('POST', sServerPath, sBody, dHeader);
217 oResponse = self._oConn.getresponse();
218 oResponse2 = TestBoxResponse(oResponse);
219 except:
220 testboxcommons.log2Xcpt();
221 raise
222 return oResponse2;
223
224 def postRequest(self, sAction, dParams = None):
225 """
226 Posts a request to the test manager, prepending the testbox ID and
227 UUID to the arguments, and gets the response. The dParams argument is a
228 is a dictionary of unencoded key-value pairs (will be modified).
229 Raises exception on failure.
230 """
231 if dParams is None:
232 dParams = {};
233 dParams[constants.tbreq.ALL_PARAM_TESTBOX_ID] = self._sTestBoxId;
234 dParams[constants.tbreq.ALL_PARAM_TESTBOX_UUID] = self._sTestBoxUuid;
235 return self.postRequestRaw(sAction, dParams);
236
237 def sendReply(self, sReplyAction, sCmdName):
238 """
239 Sends a reply to a test manager command.
240 Raises exception on failure.
241 """
242 return self.postRequest(sReplyAction, { constants.tbreq.COMMAND_ACK_PARAM_CMD_NAME: sCmdName });
243
244 def sendReplyAndClose(self, sReplyAction, sCmdName):
245 """
246 Sends a reply to a test manager command and closes the connection.
247 Raises exception on failure.
248 """
249 self.sendReply(sReplyAction, sCmdName);
250 self.close();
251 return True;
252
253 def sendAckAndClose(self, sCmdName):
254 """
255 Acks a command and closes the connection to the test manager.
256 Raises exception on failure.
257 """
258 return self.sendReplyAndClose(constants.tbreq.COMMAND_ACK, sCmdName);
259
260 def sendAck(self, sCmdName):
261 """
262 Acks a command.
263 Raises exception on failure.
264 """
265 return self.sendReply(constants.tbreq.COMMAND_ACK, sCmdName);
266
267 @staticmethod
268 def sendSignOn(sTestManagerUrl, dParams):
269 """
270 Sends a sign-on request to the server, returns the response (TestBoxResponse).
271 No exceptions will be raised.
272 """
273 oConnection = None;
274 try:
275 oConnection = TestBoxConnection(sTestManagerUrl, None, None);
276 return oConnection.postRequestRaw(constants.tbreq.SIGNON, dParams);
277 except:
278 testboxcommons.log2Xcpt();
279 if oConnection is not None: # Be kind to apache.
280 try: oConnection.close();
281 except: pass;
282
283 return TestBoxResponse(None);
284
285 @staticmethod
286 def requestCommandWithConnection(sTestManagerUrl, sTestBoxId, sTestBoxUuid, fBusy):
287 """
288 Queries the test manager for a command and returns its respons + an open
289 connection for acking/nack the command (and maybe more).
290
291 No exceptions will be raised. On failure (None, None) will be returned.
292 """
293 oConnection = None;
294 try:
295 oConnection = TestBoxConnection(sTestManagerUrl, sTestBoxId, sTestBoxUuid, fLongTimeout = not fBusy);
296 if fBusy:
297 oResponse = oConnection.postRequest(constants.tbreq.REQUEST_COMMAND_BUSY);
298 else:
299 oResponse = oConnection.postRequest(constants.tbreq.REQUEST_COMMAND_IDLE);
300 return (oResponse, oConnection);
301 except:
302 testboxcommons.log2Xcpt();
303 if oConnection is not None: # Be kind to apache.
304 try: oConnection.close();
305 except: pass;
306 return (None, None);
307
308 def isConnected(self):
309 """
310 Checks if we are still connected.
311 """
312 return self._oConn is not None;
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