VirtualBox

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

Last change on this file since 95512 was 93115, checked in by vboxsync, 3 years ago

scm --update-copyright-year

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