Revision 2a7c3583
b/INSTALL | ||
---|---|---|
29 | 29 |
- `simplejson Python module <http://code.google.com/p/simplejson/>`_ |
30 | 30 |
- `pyparsing Python module <http://pyparsing.wikispaces.com/>`_ |
31 | 31 |
- `pyinotify Python module <http://trac.dbzteam.org/pyinotify/>`_ |
32 |
- `PycURL Python module <http://pycurl.sourceforge.net/>`_ |
|
32 | 33 |
- `socat <http://www.dest-unreach.org/socat/>`_ |
33 | 34 |
|
34 | 35 |
These programs are supplied as part of most Linux distributions, so |
... | ... | |
39 | 40 |
|
40 | 41 |
$ apt-get install lvm2 ssh bridge-utils iproute iputils-arping \ |
41 | 42 |
python python-pyopenssl openssl python-pyparsing \ |
42 |
python-simplejson python-pyinotify socat |
|
43 |
python-simplejson python-pyinotify python-pycurl \ |
|
44 |
socat |
|
43 | 45 |
|
44 | 46 |
If you want to build from source, please see doc/devnotes.rst for more |
45 | 47 |
dependencies. |
b/daemons/ganeti-watcher | ||
---|---|---|
610 | 610 |
@return: Whether RAPI is working properly |
611 | 611 |
|
612 | 612 |
""" |
613 |
ssl_config = rapi.client.CertAuthorityVerify(constants.RAPI_CERT_FILE) |
|
614 |
rapi_client = \ |
|
615 |
rapi.client.GanetiRapiClient(hostname, |
|
616 |
config_ssl_verification=ssl_config) |
|
613 |
curl_config = rapi.client.GenericCurlConfig(cafile=constants.RAPI_CERT_FILE) |
|
614 |
rapi_client = rapi.client.GanetiRapiClient(hostname, |
|
615 |
curl_config_fn=curl_config) |
|
617 | 616 |
try: |
618 | 617 |
master_version = rapi_client.GetVersion() |
619 | 618 |
except rapi.client.CertificateError, err: |
... | ... | |
646 | 645 |
return options, args |
647 | 646 |
|
648 | 647 |
|
648 |
@rapi.client.UsesRapiClient |
|
649 | 649 |
def main(): |
650 | 650 |
"""Main function. |
651 | 651 |
|
b/lib/rapi/client.py | ||
---|---|---|
19 | 19 |
# 02110-1301, USA. |
20 | 20 |
|
21 | 21 |
|
22 |
"""Ganeti RAPI client.""" |
|
22 |
"""Ganeti RAPI client. |
|
23 |
|
|
24 |
@attention: To use the RAPI client, the application B{must} call |
|
25 |
C{pycurl.global_init} during initialization and |
|
26 |
C{pycurl.global_cleanup} before exiting the process. This is very |
|
27 |
important in multi-threaded programs. See curl_global_init(3) and |
|
28 |
curl_global_cleanup(3) for details. The decorator L{UsesRapiClient} |
|
29 |
can be used. |
|
30 |
|
|
31 |
""" |
|
23 | 32 |
|
24 | 33 |
# No Ganeti-specific modules should be imported. The RAPI client is supposed to |
25 | 34 |
# be standalone. |
26 | 35 |
|
27 |
import sys |
|
28 |
import httplib |
|
29 |
import urllib2 |
|
30 | 36 |
import logging |
31 | 37 |
import simplejson |
32 |
import socket |
|
33 | 38 |
import urllib |
34 |
import OpenSSL |
|
35 |
import distutils.version |
|
39 |
import threading |
|
40 |
import pycurl |
|
41 |
|
|
42 |
try: |
|
43 |
from cStringIO import StringIO |
|
44 |
except ImportError: |
|
45 |
from StringIO import StringIO |
|
36 | 46 |
|
37 | 47 |
|
38 | 48 |
GANETI_RAPI_PORT = 5080 |
... | ... | |
61 | 71 |
_REQ_DATA_VERSION_FIELD = "__version__" |
62 | 72 |
_INST_CREATE_REQV1 = "instance-create-reqv1" |
63 | 73 |
|
74 |
# Older pycURL versions don't have all error constants |
|
75 |
try: |
|
76 |
_CURLE_SSL_CACERT = pycurl.E_SSL_CACERT |
|
77 |
_CURLE_SSL_CACERT_BADFILE = pycurl.E_SSL_CACERT_BADFILE |
|
78 |
except AttributeError: |
|
79 |
_CURLE_SSL_CACERT = 60 |
|
80 |
_CURLE_SSL_CACERT_BADFILE = 77 |
|
81 |
|
|
82 |
_CURL_SSL_CERT_ERRORS = frozenset([ |
|
83 |
_CURLE_SSL_CACERT, |
|
84 |
_CURLE_SSL_CACERT_BADFILE, |
|
85 |
]) |
|
86 |
|
|
64 | 87 |
|
65 | 88 |
class Error(Exception): |
66 | 89 |
"""Base error class for this module. |
... | ... | |
85 | 108 |
self.code = code |
86 | 109 |
|
87 | 110 |
|
88 |
def FormatX509Name(x509_name): |
|
89 |
"""Formats an X509 name. |
|
90 |
|
|
91 |
@type x509_name: OpenSSL.crypto.X509Name |
|
111 |
def UsesRapiClient(fn): |
|
112 |
"""Decorator for code using RAPI client to initialize pycURL. |
|
92 | 113 |
|
93 | 114 |
""" |
94 |
try: |
|
95 |
# Only supported in pyOpenSSL 0.7 and above |
|
96 |
get_components_fn = x509_name.get_components |
|
97 |
except AttributeError: |
|
98 |
return repr(x509_name) |
|
99 |
else: |
|
100 |
return "".join("/%s=%s" % (name, value) |
|
101 |
for name, value in get_components_fn()) |
|
102 |
|
|
103 |
|
|
104 |
class CertAuthorityVerify: |
|
105 |
"""Certificate verificator for SSL context. |
|
106 |
|
|
107 |
Configures SSL context to verify server's certificate. |
|
115 |
def wrapper(*args, **kwargs): |
|
116 |
# curl_global_init(3) and curl_global_cleanup(3) must be called with only |
|
117 |
# one thread running. This check is just a safety measure -- it doesn't |
|
118 |
# cover all cases. |
|
119 |
assert threading.activeCount() == 1, \ |
|
120 |
"Found active threads when initializing pycURL" |
|
121 |
|
|
122 |
pycurl.global_init(pycurl.GLOBAL_ALL) |
|
123 |
try: |
|
124 |
return fn(*args, **kwargs) |
|
125 |
finally: |
|
126 |
pycurl.global_cleanup() |
|
127 |
|
|
128 |
return wrapper |
|
129 |
|
|
130 |
|
|
131 |
def GenericCurlConfig(verbose=False, use_signal=False, |
|
132 |
use_curl_cabundle=False, cafile=None, capath=None, |
|
133 |
proxy=None, verify_hostname=False, |
|
134 |
connect_timeout=None, timeout=None, |
|
135 |
_pycurl_version_fn=pycurl.version_info): |
|
136 |
"""Curl configuration function generator. |
|
137 |
|
|
138 |
@type verbose: bool |
|
139 |
@param verbose: Whether to set cURL to verbose mode |
|
140 |
@type use_signal: bool |
|
141 |
@param use_signal: Whether to allow cURL to use signals |
|
142 |
@type use_curl_cabundle: bool |
|
143 |
@param use_curl_cabundle: Whether to use cURL's default CA bundle |
|
144 |
@type cafile: string |
|
145 |
@param cafile: In which file we can find the certificates |
|
146 |
@type capath: string |
|
147 |
@param capath: In which directory we can find the certificates |
|
148 |
@type proxy: string |
|
149 |
@param proxy: Proxy to use, None for default behaviour and empty string for |
|
150 |
disabling proxies (see curl_easy_setopt(3)) |
|
151 |
@type verify_hostname: bool |
|
152 |
@param verify_hostname: Whether to verify the remote peer certificate's |
|
153 |
commonName |
|
154 |
@type connect_timeout: number |
|
155 |
@param connect_timeout: Timeout for establishing connection in seconds |
|
156 |
@type timeout: number |
|
157 |
@param timeout: Timeout for complete transfer in seconds (see |
|
158 |
curl_easy_setopt(3)). |
|
108 | 159 |
|
109 | 160 |
""" |
110 |
_CAPATH_MINVERSION = "0.9"
|
|
111 |
_DEFVFYPATHS_MINVERSION = "0.9"
|
|
161 |
if use_curl_cabundle and (cafile or capath):
|
|
162 |
raise Error("Can not use default CA bundle when CA file or path is set")
|
|
112 | 163 |
|
113 |
_PYOPENSSL_VERSION = OpenSSL.__version__ |
|
114 |
_PARSED_PYOPENSSL_VERSION = distutils.version.LooseVersion(_PYOPENSSL_VERSION) |
|
115 |
|
|
116 |
_SUPPORT_CAPATH = (_PARSED_PYOPENSSL_VERSION >= _CAPATH_MINVERSION) |
|
117 |
_SUPPORT_DEFVFYPATHS = (_PARSED_PYOPENSSL_VERSION >= _DEFVFYPATHS_MINVERSION) |
|
118 |
|
|
119 |
def __init__(self, cafile=None, capath=None, use_default_verify_paths=False): |
|
120 |
"""Initializes this class. |
|
164 |
def _ConfigCurl(curl, logger): |
|
165 |
"""Configures a cURL object |
|
121 | 166 |
|
122 |
@type cafile: string |
|
123 |
@param cafile: In which file we can find the certificates |
|
124 |
@type capath: string |
|
125 |
@param capath: In which directory we can find the certificates |
|
126 |
@type use_default_verify_paths: bool |
|
127 |
@param use_default_verify_paths: Whether the platform provided CA |
|
128 |
certificates are to be used for |
|
129 |
verification purposes |
|
167 |
@type curl: pycurl.Curl |
|
168 |
@param curl: cURL object |
|
130 | 169 |
|
131 | 170 |
""" |
132 |
self._cafile = cafile |
|
133 |
self._capath = capath |
|
134 |
self._use_default_verify_paths = use_default_verify_paths |
|
135 |
|
|
136 |
if self._capath is not None and not self._SUPPORT_CAPATH: |
|
137 |
raise Error(("PyOpenSSL %s has no support for a CA directory," |
|
138 |
" version %s or above is required") % |
|
139 |
(self._PYOPENSSL_VERSION, self._CAPATH_MINVERSION)) |
|
140 |
|
|
141 |
if self._use_default_verify_paths and not self._SUPPORT_DEFVFYPATHS: |
|
142 |
raise Error(("PyOpenSSL %s has no support for using default verification" |
|
143 |
" paths, version %s or above is required") % |
|
144 |
(self._PYOPENSSL_VERSION, self._DEFVFYPATHS_MINVERSION)) |
|
145 |
|
|
146 |
@staticmethod |
|
147 |
def _VerifySslCertCb(logger, _, cert, errnum, errdepth, ok): |
|
148 |
"""Callback for SSL certificate verification. |
|
149 |
|
|
150 |
@param logger: Logging object |
|
151 |
|
|
152 |
""" |
|
153 |
if ok: |
|
154 |
log_fn = logger.debug |
|
171 |
logger.debug("Using cURL version %s", pycurl.version) |
|
172 |
|
|
173 |
# pycurl.version_info returns a tuple with information about the used |
|
174 |
# version of libcurl. Item 5 is the SSL library linked to it. |
|
175 |
# e.g.: (3, '7.18.0', 463360, 'x86_64-pc-linux-gnu', 1581, 'GnuTLS/2.0.4', |
|
176 |
# 0, '1.2.3.3', ...) |
|
177 |
sslver = _pycurl_version_fn()[5] |
|
178 |
if not sslver: |
|
179 |
raise Error("No SSL support in cURL") |
|
180 |
|
|
181 |
lcsslver = sslver.lower() |
|
182 |
if lcsslver.startswith("openssl/"): |
|
183 |
pass |
|
184 |
elif lcsslver.startswith("gnutls/"): |
|
185 |
if capath: |
|
186 |
raise Error("cURL linked against GnuTLS has no support for a" |
|
187 |
" CA path (%s)" % (pycurl.version, )) |
|
155 | 188 |
else: |
156 |
log_fn = logger.error |
|
157 |
|
|
158 |
log_fn("Verifying SSL certificate at depth %s, subject '%s', issuer '%s'", |
|
159 |
errdepth, FormatX509Name(cert.get_subject()), |
|
160 |
FormatX509Name(cert.get_issuer())) |
|
161 |
|
|
162 |
if not ok: |
|
163 |
try: |
|
164 |
# Only supported in pyOpenSSL 0.7 and above |
|
165 |
# pylint: disable-msg=E1101 |
|
166 |
fn = OpenSSL.crypto.X509_verify_cert_error_string |
|
167 |
except AttributeError: |
|
168 |
errmsg = "" |
|
169 |
else: |
|
170 |
errmsg = ":%s" % fn(errnum) |
|
171 |
|
|
172 |
logger.error("verify error:num=%s%s", errnum, errmsg) |
|
173 |
|
|
174 |
return ok |
|
175 |
|
|
176 |
def __call__(self, ctx, logger): |
|
177 |
"""Configures an SSL context to verify certificates. |
|
178 |
|
|
179 |
@type ctx: OpenSSL.SSL.Context |
|
180 |
@param ctx: SSL context |
|
181 |
|
|
182 |
""" |
|
183 |
if self._use_default_verify_paths: |
|
184 |
ctx.set_default_verify_paths() |
|
185 |
|
|
186 |
if self._cafile or self._capath: |
|
187 |
if self._SUPPORT_CAPATH: |
|
188 |
ctx.load_verify_locations(self._cafile, self._capath) |
|
189 |
else: |
|
190 |
ctx.load_verify_locations(self._cafile) |
|
191 |
|
|
192 |
ctx.set_verify(OpenSSL.SSL.VERIFY_PEER, |
|
193 |
lambda conn, cert, errnum, errdepth, ok: \ |
|
194 |
self._VerifySslCertCb(logger, conn, cert, |
|
195 |
errnum, errdepth, ok)) |
|
196 |
|
|
197 |
|
|
198 |
class _HTTPSConnectionOpenSSL(httplib.HTTPSConnection): |
|
199 |
"""HTTPS Connection handler that verifies the SSL certificate. |
|
200 |
|
|
201 |
""" |
|
202 |
# Python before version 2.6 had its own httplib.FakeSocket wrapper for |
|
203 |
# sockets |
|
204 |
_SUPPORT_FAKESOCKET = (sys.hexversion < 0x2060000) |
|
205 |
|
|
206 |
def __init__(self, *args, **kwargs): |
|
207 |
"""Initializes this class. |
|
208 |
|
|
209 |
""" |
|
210 |
httplib.HTTPSConnection.__init__(self, *args, **kwargs) |
|
211 |
self._logger = None |
|
212 |
self._config_ssl_verification = None |
|
213 |
|
|
214 |
def Setup(self, logger, config_ssl_verification): |
|
215 |
"""Sets the SSL verification config function. |
|
216 |
|
|
217 |
@param logger: Logging object |
|
218 |
@type config_ssl_verification: callable |
|
219 |
|
|
220 |
""" |
|
221 |
assert self._logger is None |
|
222 |
assert self._config_ssl_verification is None |
|
223 |
|
|
224 |
self._logger = logger |
|
225 |
self._config_ssl_verification = config_ssl_verification |
|
226 |
|
|
227 |
def connect(self): |
|
228 |
"""Connect to the server specified when the object was created. |
|
229 |
|
|
230 |
This ensures that SSL certificates are verified. |
|
231 |
|
|
232 |
""" |
|
233 |
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) |
|
234 |
|
|
235 |
ctx = OpenSSL.SSL.Context(OpenSSL.SSL.TLSv1_METHOD) |
|
236 |
ctx.set_options(OpenSSL.SSL.OP_NO_SSLv2) |
|
237 |
|
|
238 |
if self._config_ssl_verification: |
|
239 |
self._config_ssl_verification(ctx, self._logger) |
|
240 |
|
|
241 |
ssl = OpenSSL.SSL.Connection(ctx, sock) |
|
242 |
ssl.connect((self.host, self.port)) |
|
243 |
|
|
244 |
if self._SUPPORT_FAKESOCKET: |
|
245 |
self.sock = httplib.FakeSocket(sock, ssl) |
|
189 |
raise NotImplementedError("cURL uses unsupported SSL version '%s'" % |
|
190 |
sslver) |
|
191 |
|
|
192 |
curl.setopt(pycurl.VERBOSE, verbose) |
|
193 |
curl.setopt(pycurl.NOSIGNAL, not use_signal) |
|
194 |
|
|
195 |
# Whether to verify remote peer's CN |
|
196 |
if verify_hostname: |
|
197 |
# curl_easy_setopt(3): "When CURLOPT_SSL_VERIFYHOST is 2, that |
|
198 |
# certificate must indicate that the server is the server to which you |
|
199 |
# meant to connect, or the connection fails. [...] When the value is 1, |
|
200 |
# the certificate must contain a Common Name field, but it doesn't matter |
|
201 |
# what name it says. [...]" |
|
202 |
curl.setopt(pycurl.SSL_VERIFYHOST, 2) |
|
246 | 203 |
else: |
247 |
self.sock = _SslSocketWrapper(ssl) |
|
248 |
|
|
249 |
|
|
250 |
class _SslSocketWrapper(object): |
|
251 |
def __init__(self, sock): |
|
252 |
"""Initializes this class. |
|
253 |
|
|
254 |
""" |
|
255 |
self._sock = sock |
|
256 |
|
|
257 |
def __getattr__(self, name): |
|
258 |
"""Forward everything to underlying socket. |
|
259 |
|
|
260 |
""" |
|
261 |
return getattr(self._sock, name) |
|
262 |
|
|
263 |
def makefile(self, mode, bufsize): |
|
264 |
"""Fake makefile method. |
|
265 |
|
|
266 |
makefile() on normal file descriptors uses dup2(2), which doesn't work with |
|
267 |
SSL sockets and therefore is not implemented by pyOpenSSL. This fake method |
|
268 |
works with the httplib module, but might not work for other modules. |
|
269 |
|
|
270 |
""" |
|
271 |
# pylint: disable-msg=W0212 |
|
272 |
return socket._fileobject(self._sock, mode, bufsize) |
|
273 |
|
|
274 |
|
|
275 |
class _HTTPSHandler(urllib2.HTTPSHandler): |
|
276 |
def __init__(self, logger, config_ssl_verification): |
|
277 |
"""Initializes this class. |
|
278 |
|
|
279 |
@param logger: Logging object |
|
280 |
@type config_ssl_verification: callable |
|
281 |
@param config_ssl_verification: Function to configure SSL context for |
|
282 |
certificate verification |
|
283 |
|
|
284 |
""" |
|
285 |
urllib2.HTTPSHandler.__init__(self) |
|
286 |
self._logger = logger |
|
287 |
self._config_ssl_verification = config_ssl_verification |
|
288 |
|
|
289 |
def _CreateHttpsConnection(self, *args, **kwargs): |
|
290 |
"""Wrapper around L{_HTTPSConnectionOpenSSL} to add SSL verification. |
|
291 |
|
|
292 |
This wrapper is necessary provide a compatible API to urllib2. |
|
293 |
|
|
294 |
""" |
|
295 |
conn = _HTTPSConnectionOpenSSL(*args, **kwargs) |
|
296 |
conn.Setup(self._logger, self._config_ssl_verification) |
|
297 |
return conn |
|
298 |
|
|
299 |
def https_open(self, req): |
|
300 |
"""Creates HTTPS connection. |
|
301 |
|
|
302 |
Called by urllib2. |
|
303 |
|
|
304 |
""" |
|
305 |
return self.do_open(self._CreateHttpsConnection, req) |
|
306 |
|
|
307 |
|
|
308 |
class _RapiRequest(urllib2.Request): |
|
309 |
def __init__(self, method, url, headers, data): |
|
310 |
"""Initializes this class. |
|
204 |
curl.setopt(pycurl.SSL_VERIFYHOST, 0) |
|
205 |
|
|
206 |
if cafile or capath or use_curl_cabundle: |
|
207 |
# Require certificates to be checked |
|
208 |
curl.setopt(pycurl.SSL_VERIFYPEER, True) |
|
209 |
if cafile: |
|
210 |
curl.setopt(pycurl.CAINFO, str(cafile)) |
|
211 |
if capath: |
|
212 |
curl.setopt(pycurl.CAPATH, str(capath)) |
|
213 |
# Not changing anything for using default CA bundle |
|
214 |
else: |
|
215 |
# Disable SSL certificate verification |
|
216 |
curl.setopt(pycurl.SSL_VERIFYPEER, False) |
|
311 | 217 |
|
312 |
""" |
|
313 |
urllib2.Request.__init__(self, url, data=data, headers=headers) |
|
314 |
self._method = method |
|
218 |
if proxy is not None: |
|
219 |
curl.setopt(pycurl.PROXY, str(proxy)) |
|
315 | 220 |
|
316 |
def get_method(self): |
|
317 |
"""Returns the HTTP request method. |
|
221 |
# Timeouts |
|
222 |
if connect_timeout is not None: |
|
223 |
curl.setopt(pycurl.CONNECTTIMEOUT, connect_timeout) |
|
224 |
if timeout is not None: |
|
225 |
curl.setopt(pycurl.TIMEOUT, timeout) |
|
318 | 226 |
|
319 |
""" |
|
320 |
return self._method |
|
227 |
return _ConfigCurl |
|
321 | 228 |
|
322 | 229 |
|
323 | 230 |
class GanetiRapiClient(object): |
... | ... | |
328 | 235 |
_json_encoder = simplejson.JSONEncoder(sort_keys=True) |
329 | 236 |
|
330 | 237 |
def __init__(self, host, port=GANETI_RAPI_PORT, |
331 |
username=None, password=None, |
|
332 |
config_ssl_verification=None, ignore_proxy=False, |
|
333 |
logger=logging): |
|
334 |
"""Constructor. |
|
238 |
username=None, password=None, logger=logging, |
|
239 |
curl_config_fn=None, curl=None): |
|
240 |
"""Initializes this class. |
|
335 | 241 |
|
336 | 242 |
@type host: string |
337 | 243 |
@param host: the ganeti cluster master to interact with |
... | ... | |
341 | 247 |
@param username: the username to connect with |
342 | 248 |
@type password: string |
343 | 249 |
@param password: the password to connect with |
344 |
@type config_ssl_verification: callable |
|
345 |
@param config_ssl_verification: Function to configure SSL context for |
|
346 |
certificate verification |
|
347 |
@type ignore_proxy: bool |
|
348 |
@param ignore_proxy: Whether to ignore proxy settings |
|
250 |
@type curl_config_fn: callable |
|
251 |
@param curl_config_fn: Function to configure C{pycurl.Curl} object |
|
349 | 252 |
@param logger: Logging object |
350 | 253 |
|
351 | 254 |
""" |
... | ... | |
355 | 258 |
|
356 | 259 |
self._base_url = "https://%s:%s" % (host, port) |
357 | 260 |
|
358 |
handlers = [_HTTPSHandler(self._logger, config_ssl_verification)] |
|
359 |
|
|
261 |
# Create pycURL object if not supplied |
|
262 |
if not curl: |
|
263 |
curl = pycurl.Curl() |
|
264 |
|
|
265 |
# Default cURL settings |
|
266 |
curl.setopt(pycurl.VERBOSE, False) |
|
267 |
curl.setopt(pycurl.FOLLOWLOCATION, False) |
|
268 |
curl.setopt(pycurl.MAXREDIRS, 5) |
|
269 |
curl.setopt(pycurl.NOSIGNAL, True) |
|
270 |
curl.setopt(pycurl.USERAGENT, self.USER_AGENT) |
|
271 |
curl.setopt(pycurl.SSL_VERIFYHOST, 0) |
|
272 |
curl.setopt(pycurl.SSL_VERIFYPEER, False) |
|
273 |
curl.setopt(pycurl.HTTPHEADER, [ |
|
274 |
"Accept: %s" % HTTP_APP_JSON, |
|
275 |
"Content-type: %s" % HTTP_APP_JSON, |
|
276 |
]) |
|
277 |
|
|
278 |
# Setup authentication |
|
360 | 279 |
if username is not None: |
361 |
pwmgr = urllib2.HTTPPasswordMgrWithDefaultRealm() |
|
362 |
pwmgr.add_password(None, self._base_url, username, password) |
|
363 |
handlers.append(urllib2.HTTPBasicAuthHandler(pwmgr)) |
|
280 |
if password is None: |
|
281 |
raise Error("Password not specified") |
|
282 |
curl.setopt(pycurl.HTTPAUTH, pycurl.HTTPAUTH_BASIC) |
|
283 |
curl.setopt(pycurl.USERPWD, str("%s:%s" % (username, password))) |
|
364 | 284 |
elif password: |
365 | 285 |
raise Error("Specified password without username") |
366 | 286 |
|
367 |
if ignore_proxy: |
|
368 |
handlers.append(urllib2.ProxyHandler({})) |
|
369 |
|
|
370 |
self._http = urllib2.build_opener(*handlers) # pylint: disable-msg=W0142 |
|
287 |
# Call external configuration function |
|
288 |
if curl_config_fn: |
|
289 |
curl_config_fn(curl, logger) |
|
371 | 290 |
|
372 |
self._headers = { |
|
373 |
"Accept": HTTP_APP_JSON, |
|
374 |
"Content-type": HTTP_APP_JSON, |
|
375 |
"User-Agent": self.USER_AGENT, |
|
376 |
} |
|
291 |
self._curl = curl |
|
377 | 292 |
|
378 | 293 |
@staticmethod |
379 | 294 |
def _EncodeQuery(query): |
... | ... | |
427 | 342 |
""" |
428 | 343 |
assert path.startswith("/") |
429 | 344 |
|
345 |
curl = self._curl |
|
346 |
|
|
430 | 347 |
if content: |
431 | 348 |
encoded_content = self._json_encoder.encode(content) |
432 | 349 |
else: |
433 |
encoded_content = None
|
|
350 |
encoded_content = ""
|
|
434 | 351 |
|
435 | 352 |
# Build URL |
436 | 353 |
urlparts = [self._base_url, path] |
... | ... | |
440 | 357 |
|
441 | 358 |
url = "".join(urlparts) |
442 | 359 |
|
443 |
self._logger.debug("Sending request %s %s to %s:%s" |
|
444 |
" (headers=%r, content=%r)", |
|
445 |
method, url, self._host, self._port, self._headers, |
|
446 |
encoded_content) |
|
360 |
self._logger.debug("Sending request %s %s to %s:%s (content=%r)", |
|
361 |
method, url, self._host, self._port, encoded_content) |
|
362 |
|
|
363 |
# Buffer for response |
|
364 |
encoded_resp_body = StringIO() |
|
447 | 365 |
|
448 |
req = _RapiRequest(method, url, self._headers, encoded_content) |
|
366 |
# Configure cURL |
|
367 |
curl.setopt(pycurl.CUSTOMREQUEST, str(method)) |
|
368 |
curl.setopt(pycurl.URL, str(url)) |
|
369 |
curl.setopt(pycurl.POSTFIELDS, str(encoded_content)) |
|
370 |
curl.setopt(pycurl.WRITEFUNCTION, encoded_resp_body.write) |
|
449 | 371 |
|
450 | 372 |
try: |
451 |
resp = self._http.open(req) |
|
452 |
encoded_response_content = resp.read() |
|
453 |
except (OpenSSL.SSL.Error, OpenSSL.crypto.Error), err: |
|
454 |
raise CertificateError("SSL issue: %s (%r)" % (err, err)) |
|
455 |
except urllib2.HTTPError, err: |
|
456 |
raise GanetiApiError(str(err), code=err.code) |
|
457 |
except urllib2.URLError, err: |
|
458 |
raise GanetiApiError(str(err)) |
|
459 |
|
|
460 |
if encoded_response_content: |
|
461 |
response_content = simplejson.loads(encoded_response_content) |
|
373 |
# Send request and wait for response |
|
374 |
try: |
|
375 |
curl.perform() |
|
376 |
except pycurl.error, err: |
|
377 |
if err.args[0] in _CURL_SSL_CERT_ERRORS: |
|
378 |
raise CertificateError("SSL certificate error %s" % err) |
|
379 |
|
|
380 |
raise GanetiApiError(str(err)) |
|
381 |
finally: |
|
382 |
# Reset settings to not keep references to large objects in memory |
|
383 |
# between requests |
|
384 |
curl.setopt(pycurl.POSTFIELDS, "") |
|
385 |
curl.setopt(pycurl.WRITEFUNCTION, lambda _: None) |
|
386 |
|
|
387 |
# Get HTTP response code |
|
388 |
http_code = curl.getinfo(pycurl.RESPONSE_CODE) |
|
389 |
|
|
390 |
# Was anything written to the response buffer? |
|
391 |
if encoded_resp_body.tell(): |
|
392 |
response_content = simplejson.loads(encoded_resp_body.getvalue()) |
|
462 | 393 |
else: |
463 | 394 |
response_content = None |
464 | 395 |
|
465 |
# TODO: Are there other status codes that are valid? (redirect?) |
|
466 |
if resp.code != HTTP_OK: |
|
396 |
if http_code != HTTP_OK: |
|
467 | 397 |
if isinstance(response_content, dict): |
468 | 398 |
msg = ("%s %s: %s" % |
469 | 399 |
(response_content["code"], |
... | ... | |
472 | 402 |
else: |
473 | 403 |
msg = str(response_content) |
474 | 404 |
|
475 |
raise GanetiApiError(msg, code=resp.code)
|
|
405 |
raise GanetiApiError(msg, code=http_code)
|
|
476 | 406 |
|
477 | 407 |
return response_content |
478 | 408 |
|
b/qa/ganeti-qa.py | ||
---|---|---|
39 | 39 |
import qa_utils |
40 | 40 |
|
41 | 41 |
from ganeti import utils |
42 |
from ganeti import rapi |
|
43 |
|
|
44 |
import ganeti.rapi.client |
|
42 | 45 |
|
43 | 46 |
|
44 | 47 |
def RunTest(fn, *args): |
... | ... | |
269 | 272 |
instance, pnode, snode) |
270 | 273 |
|
271 | 274 |
|
275 |
@rapi.client.UsesRapiClient |
|
272 | 276 |
def main(): |
273 | 277 |
"""Main program. |
274 | 278 |
|
b/qa/qa_rapi.py | ||
---|---|---|
72 | 72 |
_rapi_ca.flush() |
73 | 73 |
|
74 | 74 |
port = qa_config.get("rapi-port", default=constants.DEFAULT_RAPI_PORT) |
75 |
cfg_ssl = rapi.client.CertAuthorityVerify(cafile=_rapi_ca.name) |
|
75 |
cfg_curl = rapi.client.GenericCurlConfig(cafile=_rapi_ca.name, |
|
76 |
proxy="") |
|
76 | 77 |
|
77 | 78 |
_rapi_client = rapi.client.GanetiRapiClient(master["primary"], port=port, |
78 | 79 |
username=username, |
79 | 80 |
password=password, |
80 |
config_ssl_verification=cfg_ssl, |
|
81 |
ignore_proxy=True) |
|
81 |
curl_config_fn=cfg_curl) |
|
82 | 82 |
|
83 | 83 |
print "RAPI protocol version: %s" % _rapi_client.GetVersion() |
84 | 84 |
|
b/test/ganeti.rapi.client_unittest.py | ||
---|---|---|
25 | 25 |
import re |
26 | 26 |
import unittest |
27 | 27 |
import warnings |
28 |
import pycurl |
|
28 | 29 |
|
30 |
from ganeti import constants |
|
29 | 31 |
from ganeti import http |
30 | 32 |
from ganeti import serializer |
31 | 33 |
|
... | ... | |
50 | 52 |
return None |
51 | 53 |
|
52 | 54 |
|
53 |
class HttpResponseMock: |
|
54 |
"""Dumb mock of httplib.HTTPResponse. |
|
55 |
|
|
56 |
""" |
|
57 |
|
|
58 |
def __init__(self, code, data): |
|
59 |
self.code = code |
|
60 |
self._data = data |
|
55 |
class FakeCurl: |
|
56 |
def __init__(self, rapi): |
|
57 |
self._rapi = rapi |
|
58 |
self._opts = {} |
|
59 |
self._info = {} |
|
61 | 60 |
|
62 |
def read(self):
|
|
63 |
return self._data
|
|
61 |
def setopt(self, opt, value):
|
|
62 |
self._opts[opt] = value
|
|
64 | 63 |
|
64 |
def getopt(self, opt): |
|
65 |
return self._opts.get(opt) |
|
65 | 66 |
|
66 |
class OpenerDirectorMock:
|
|
67 |
"""Mock for urllib.OpenerDirector.
|
|
67 |
def unsetopt(self, opt):
|
|
68 |
self._opts.pop(opt, None)
|
|
68 | 69 |
|
69 |
""" |
|
70 |
def getinfo(self, info): |
|
71 |
return self._info[info] |
|
70 | 72 |
|
71 |
def __init__(self, rapi): |
|
72 |
self._rapi = rapi |
|
73 |
self.last_request = None |
|
73 |
def perform(self): |
|
74 |
method = self._opts[pycurl.CUSTOMREQUEST] |
|
75 |
url = self._opts[pycurl.URL] |
|
76 |
request_body = self._opts[pycurl.POSTFIELDS] |
|
77 |
writefn = self._opts[pycurl.WRITEFUNCTION] |
|
74 | 78 |
|
75 |
def open(self, req):
|
|
76 |
self.last_request = req
|
|
79 |
path = _GetPathFromUri(url)
|
|
80 |
(code, resp_body) = self._rapi.FetchResponse(path, method, request_body)
|
|
77 | 81 |
|
78 |
path = _GetPathFromUri(req.get_full_url())
|
|
79 |
code, resp_body = self._rapi.FetchResponse(path, req.get_method())
|
|
80 |
return HttpResponseMock(code, resp_body)
|
|
82 |
self._info[pycurl.RESPONSE_CODE] = code
|
|
83 |
if resp_body is not None:
|
|
84 |
writefn(resp_body)
|
|
81 | 85 |
|
82 | 86 |
|
83 | 87 |
class RapiMock(object): |
... | ... | |
85 | 89 |
self._mapper = connector.Mapper() |
86 | 90 |
self._responses = [] |
87 | 91 |
self._last_handler = None |
92 |
self._last_req_data = None |
|
88 | 93 |
|
89 | 94 |
def AddResponse(self, response, code=200): |
90 | 95 |
self._responses.insert(0, (code, response)) |
... | ... | |
92 | 97 |
def GetLastHandler(self): |
93 | 98 |
return self._last_handler |
94 | 99 |
|
95 |
def FetchResponse(self, path, method): |
|
100 |
def GetLastRequestData(self): |
|
101 |
return self._last_req_data |
|
102 |
|
|
103 |
def FetchResponse(self, path, method, request_body): |
|
104 |
self._last_req_data = request_body |
|
105 |
|
|
96 | 106 |
try: |
97 | 107 |
HandlerClass, items, args = self._mapper.getController(path) |
98 | 108 |
self._last_handler = HandlerClass(items, args, None) |
... | ... | |
111 | 121 |
return code, response |
112 | 122 |
|
113 | 123 |
|
124 |
class TestConstants(unittest.TestCase): |
|
125 |
def test(self): |
|
126 |
self.assertEqual(client.GANETI_RAPI_PORT, constants.DEFAULT_RAPI_PORT) |
|
127 |
self.assertEqual(client.GANETI_RAPI_VERSION, constants.RAPI_VERSION) |
|
128 |
self.assertEqual(client.HTTP_APP_JSON, http.HTTP_APP_JSON) |
|
129 |
self.assertEqual(client._REQ_DATA_VERSION_FIELD, rlib2._REQ_DATA_VERSION) |
|
130 |
self.assertEqual(client._INST_CREATE_REQV1, rlib2._INST_CREATE_REQV1) |
|
131 |
|
|
132 |
|
|
114 | 133 |
class RapiMockTest(unittest.TestCase): |
115 | 134 |
def test(self): |
116 | 135 |
rapi = RapiMock() |
117 | 136 |
path = "/version" |
118 |
self.assertEqual((404, None), rapi.FetchResponse("/foo", "GET")) |
|
137 |
self.assertEqual((404, None), rapi.FetchResponse("/foo", "GET", None))
|
|
119 | 138 |
self.assertEqual((501, "Method not implemented"), |
120 |
rapi.FetchResponse("/version", "POST")) |
|
139 |
rapi.FetchResponse("/version", "POST", None))
|
|
121 | 140 |
rapi.AddResponse("2") |
122 |
code, response = rapi.FetchResponse("/version", "GET") |
|
141 |
code, response = rapi.FetchResponse("/version", "GET", None)
|
|
123 | 142 |
self.assertEqual(200, code) |
124 | 143 |
self.assertEqual("2", response) |
125 | 144 |
self.failUnless(isinstance(rapi.GetLastHandler(), rlib2.R_version)) |
126 | 145 |
|
127 | 146 |
|
147 |
def _FakeNoSslPycurlVersion(): |
|
148 |
# Note: incomplete version tuple |
|
149 |
return (3, "7.16.0", 462848, "mysystem", 1581, None, 0) |
|
150 |
|
|
151 |
|
|
152 |
def _FakeFancySslPycurlVersion(): |
|
153 |
# Note: incomplete version tuple |
|
154 |
return (3, "7.16.0", 462848, "mysystem", 1581, "FancySSL/1.2.3", 0) |
|
155 |
|
|
156 |
|
|
157 |
def _FakeOpenSslPycurlVersion(): |
|
158 |
# Note: incomplete version tuple |
|
159 |
return (2, "7.15.5", 462597, "othersystem", 668, "OpenSSL/0.9.8c", 0) |
|
160 |
|
|
161 |
|
|
162 |
def _FakeGnuTlsPycurlVersion(): |
|
163 |
# Note: incomplete version tuple |
|
164 |
return (3, "7.18.0", 463360, "somesystem", 1581, "GnuTLS/2.0.4", 0) |
|
165 |
|
|
166 |
|
|
167 |
class TestExtendedConfig(unittest.TestCase): |
|
168 |
def testAuth(self): |
|
169 |
curl = FakeCurl(RapiMock()) |
|
170 |
cl = client.GanetiRapiClient("master.example.com", |
|
171 |
username="user", password="pw", |
|
172 |
curl=curl) |
|
173 |
|
|
174 |
self.assertEqual(curl.getopt(pycurl.HTTPAUTH), pycurl.HTTPAUTH_BASIC) |
|
175 |
self.assertEqual(curl.getopt(pycurl.USERPWD), "user:pw") |
|
176 |
|
|
177 |
def testInvalidAuth(self): |
|
178 |
# No username |
|
179 |
self.assertRaises(client.Error, client.GanetiRapiClient, |
|
180 |
"master-a.example.com", password="pw") |
|
181 |
# No password |
|
182 |
self.assertRaises(client.Error, client.GanetiRapiClient, |
|
183 |
"master-b.example.com", username="user") |
|
184 |
|
|
185 |
def testCertVerifyInvalidCombinations(self): |
|
186 |
self.assertRaises(client.Error, client.GenericCurlConfig, |
|
187 |
use_curl_cabundle=True, cafile="cert1.pem") |
|
188 |
self.assertRaises(client.Error, client.GenericCurlConfig, |
|
189 |
use_curl_cabundle=True, capath="certs/") |
|
190 |
self.assertRaises(client.Error, client.GenericCurlConfig, |
|
191 |
use_curl_cabundle=True, |
|
192 |
cafile="cert1.pem", capath="certs/") |
|
193 |
|
|
194 |
def testProxySignalVerifyHostname(self): |
|
195 |
for use_gnutls in [False, True]: |
|
196 |
if use_gnutls: |
|
197 |
pcverfn = _FakeGnuTlsPycurlVersion |
|
198 |
else: |
|
199 |
pcverfn = _FakeOpenSslPycurlVersion |
|
200 |
|
|
201 |
for proxy in ["", "http://127.0.0.1:1234"]: |
|
202 |
for use_signal in [False, True]: |
|
203 |
for verify_hostname in [False, True]: |
|
204 |
cfgfn = client.GenericCurlConfig(proxy=proxy, use_signal=use_signal, |
|
205 |
verify_hostname=verify_hostname, |
|
206 |
_pycurl_version_fn=pcverfn) |
|
207 |
|
|
208 |
curl = FakeCurl(RapiMock()) |
|
209 |
cl = client.GanetiRapiClient("master.example.com", |
|
210 |
curl_config_fn=cfgfn, curl=curl) |
|
211 |
|
|
212 |
self.assertEqual(curl.getopt(pycurl.PROXY), proxy) |
|
213 |
self.assertEqual(curl.getopt(pycurl.NOSIGNAL), not use_signal) |
|
214 |
|
|
215 |
if verify_hostname: |
|
216 |
self.assertEqual(curl.getopt(pycurl.SSL_VERIFYHOST), 2) |
|
217 |
else: |
|
218 |
self.assertEqual(curl.getopt(pycurl.SSL_VERIFYHOST), 0) |
|
219 |
|
|
220 |
def testNoCertVerify(self): |
|
221 |
cfgfn = client.GenericCurlConfig() |
|
222 |
|
|
223 |
curl = FakeCurl(RapiMock()) |
|
224 |
cl = client.GanetiRapiClient("master.example.com", curl_config_fn=cfgfn, |
|
225 |
curl=curl) |
|
226 |
|
|
227 |
self.assertFalse(curl.getopt(pycurl.SSL_VERIFYPEER)) |
|
228 |
self.assertFalse(curl.getopt(pycurl.CAINFO)) |
|
229 |
self.assertFalse(curl.getopt(pycurl.CAPATH)) |
|
230 |
|
|
231 |
def testCertVerifyCurlBundle(self): |
|
232 |
cfgfn = client.GenericCurlConfig(use_curl_cabundle=True) |
|
233 |
|
|
234 |
curl = FakeCurl(RapiMock()) |
|
235 |
cl = client.GanetiRapiClient("master.example.com", curl_config_fn=cfgfn, |
|
236 |
curl=curl) |
|
237 |
|
|
238 |
self.assert_(curl.getopt(pycurl.SSL_VERIFYPEER)) |
|
239 |
self.assertFalse(curl.getopt(pycurl.CAINFO)) |
|
240 |
self.assertFalse(curl.getopt(pycurl.CAPATH)) |
|
241 |
|
|
242 |
def testCertVerifyCafile(self): |
|
243 |
mycert = "/tmp/some/UNUSED/cert/file.pem" |
|
244 |
cfgfn = client.GenericCurlConfig(cafile=mycert) |
|
245 |
|
|
246 |
curl = FakeCurl(RapiMock()) |
|
247 |
cl = client.GanetiRapiClient("master.example.com", curl_config_fn=cfgfn, |
|
248 |
curl=curl) |
|
249 |
|
|
250 |
self.assert_(curl.getopt(pycurl.SSL_VERIFYPEER)) |
|
251 |
self.assertEqual(curl.getopt(pycurl.CAINFO), mycert) |
|
252 |
self.assertFalse(curl.getopt(pycurl.CAPATH)) |
|
253 |
|
|
254 |
def testCertVerifyCapath(self): |
|
255 |
certdir = "/tmp/some/UNUSED/cert/directory" |
|
256 |
pcverfn = _FakeOpenSslPycurlVersion |
|
257 |
cfgfn = client.GenericCurlConfig(capath=certdir, |
|
258 |
_pycurl_version_fn=pcverfn) |
|
259 |
|
|
260 |
curl = FakeCurl(RapiMock()) |
|
261 |
cl = client.GanetiRapiClient("master.example.com", curl_config_fn=cfgfn, |
|
262 |
curl=curl) |
|
263 |
|
|
264 |
self.assert_(curl.getopt(pycurl.SSL_VERIFYPEER)) |
|
265 |
self.assertEqual(curl.getopt(pycurl.CAPATH), certdir) |
|
266 |
self.assertFalse(curl.getopt(pycurl.CAINFO)) |
|
267 |
|
|
268 |
def testCertVerifyCapathGnuTls(self): |
|
269 |
certdir = "/tmp/some/UNUSED/cert/directory" |
|
270 |
pcverfn = _FakeGnuTlsPycurlVersion |
|
271 |
cfgfn = client.GenericCurlConfig(capath=certdir, |
|
272 |
_pycurl_version_fn=pcverfn) |
|
273 |
|
|
274 |
curl = FakeCurl(RapiMock()) |
|
275 |
self.assertRaises(client.Error, client.GanetiRapiClient, |
|
276 |
"master.example.com", curl_config_fn=cfgfn, curl=curl) |
|
277 |
|
|
278 |
def testCertVerifyNoSsl(self): |
|
279 |
certdir = "/tmp/some/UNUSED/cert/directory" |
|
280 |
pcverfn = _FakeNoSslPycurlVersion |
|
281 |
cfgfn = client.GenericCurlConfig(capath=certdir, |
|
282 |
_pycurl_version_fn=pcverfn) |
|
283 |
|
|
284 |
curl = FakeCurl(RapiMock()) |
|
285 |
self.assertRaises(client.Error, client.GanetiRapiClient, |
|
286 |
"master.example.com", curl_config_fn=cfgfn, curl=curl) |
|
287 |
|
|
288 |
def testCertVerifyFancySsl(self): |
|
289 |
certdir = "/tmp/some/UNUSED/cert/directory" |
|
290 |
pcverfn = _FakeFancySslPycurlVersion |
|
291 |
cfgfn = client.GenericCurlConfig(capath=certdir, |
|
292 |
_pycurl_version_fn=pcverfn) |
|
293 |
|
|
294 |
curl = FakeCurl(RapiMock()) |
|
295 |
self.assertRaises(NotImplementedError, client.GanetiRapiClient, |
|
296 |
"master.example.com", curl_config_fn=cfgfn, curl=curl) |
|
297 |
|
|
298 |
def testCertVerifyCapath(self): |
|
299 |
for connect_timeout in [None, 1, 5, 10, 30, 60, 300]: |
|
300 |
for timeout in [None, 1, 30, 60, 3600, 24 * 3600]: |
|
301 |
cfgfn = client.GenericCurlConfig(connect_timeout=connect_timeout, |
|
302 |
timeout=timeout) |
|
303 |
|
|
304 |
curl = FakeCurl(RapiMock()) |
|
305 |
cl = client.GanetiRapiClient("master.example.com", curl_config_fn=cfgfn, |
|
306 |
curl=curl) |
|
307 |
|
|
308 |
self.assertEqual(curl.getopt(pycurl.CONNECTTIMEOUT), connect_timeout) |
|
309 |
self.assertEqual(curl.getopt(pycurl.TIMEOUT), timeout) |
|
310 |
|
|
311 |
|
|
128 | 312 |
class GanetiRapiClientTests(testutils.GanetiTestCase): |
129 | 313 |
def setUp(self): |
130 | 314 |
testutils.GanetiTestCase.setUp(self) |
131 | 315 |
|
132 | 316 |
self.rapi = RapiMock() |
133 |
self.http = OpenerDirectorMock(self.rapi) |
|
134 |
self.client = client.GanetiRapiClient('master.foo.com') |
|
135 |
self.client._http = self.http |
|
136 |
# Hard-code the version for easier testing. |
|
137 |
self.client._version = 2 |
|
317 |
self.curl = FakeCurl(self.rapi) |
|
318 |
self.client = client.GanetiRapiClient("master.example.com", |
|
319 |
curl=self.curl) |
|
320 |
|
|
321 |
# Signals should be disabled by default |
|
322 |
self.assert_(self.curl.getopt(pycurl.NOSIGNAL)) |
|
323 |
|
|
324 |
# No auth and no proxy |
|
325 |
self.assertFalse(self.curl.getopt(pycurl.USERPWD)) |
|
326 |
self.assert_(self.curl.getopt(pycurl.PROXY) is None) |
|
327 |
|
|
328 |
# Content-type is required for requests |
|
329 |
headers = self.curl.getopt(pycurl.HTTPHEADER) |
|
330 |
self.assert_("Content-type: application/json" in headers) |
|
138 | 331 |
|
139 | 332 |
def assertHandler(self, handler_cls): |
140 | 333 |
self.failUnless(isinstance(self.rapi.GetLastHandler(), handler_cls)) |
... | ... | |
273 | 466 |
self.assertHandler(rlib2.R_2_instances) |
274 | 467 |
self.assertDryRun() |
275 | 468 |
|
276 |
data = serializer.LoadJson(self.http.last_request.data)
|
|
469 |
data = serializer.LoadJson(self.rapi.GetLastRequestData())
|
|
277 | 470 |
|
278 | 471 |
for field in ["dry_run", "beparams", "hvparams", "start"]: |
279 | 472 |
self.assertFalse(field in data) |
... | ... | |
293 | 486 |
self.assertEqual(job_id, 24740) |
294 | 487 |
self.assertHandler(rlib2.R_2_instances) |
295 | 488 |
|
296 |
data = serializer.LoadJson(self.http.last_request.data)
|
|
489 |
data = serializer.LoadJson(self.rapi.GetLastRequestData())
|
|
297 | 490 |
self.assertEqual(data[rlib2._REQ_DATA_VERSION], 1) |
298 | 491 |
self.assertEqual(data["name"], "inst2.example.com") |
299 | 492 |
self.assertEqual(data["disk_template"], "drbd8") |
... | ... | |
411 | 604 |
self.assertHandler(rlib2.R_2_instances_name_export) |
412 | 605 |
self.assertItems(["inst2"]) |
413 | 606 |
|
414 |
data = serializer.LoadJson(self.http.last_request.data)
|
|
607 |
data = serializer.LoadJson(self.rapi.GetLastRequestData())
|
|
415 | 608 |
self.assertEqual(data["mode"], "local") |
416 | 609 |
self.assertEqual(data["destination"], "nodeX") |
417 | 610 |
self.assertEqual(data["shutdown"], True) |
... | ... | |
509 | 702 |
self.assertHandler(rlib2.R_2_nodes_name_role) |
510 | 703 |
self.assertItems(["node-foo"]) |
511 | 704 |
self.assertQuery("force", ["1"]) |
512 |
self.assertEqual("\"master-candidate\"", self.http.last_request.data)
|
|
705 |
self.assertEqual("\"master-candidate\"", self.rapi.GetLastRequestData())
|
|
513 | 706 |
|
514 | 707 |
def testGetNodeStorageUnits(self): |
515 | 708 |
self.rapi.AddResponse("42") |
... | ... | |
576 | 769 |
|
577 | 770 |
|
578 | 771 |
if __name__ == '__main__': |
579 |
testutils.GanetiTestProgram() |
|
772 |
client.UsesRapiClient(testutils.GanetiTestProgram)() |
b/tools/move-instance | ||
---|---|---|
148 | 148 |
self.src_cluster_name = src_cluster_name |
149 | 149 |
self.dest_cluster_name = dest_cluster_name |
150 | 150 |
|
151 |
# TODO: Implement timeouts for RAPI connections |
|
151 | 152 |
# TODO: Support for using system default paths for verifying SSL certificate |
152 |
# (already implemented in CertAuthorityVerify) |
|
153 | 153 |
logging.debug("Using '%s' as source CA", options.src_ca_file) |
154 |
src_ssl_config = rapi.client.CertAuthorityVerify(cafile=options.src_ca_file)
|
|
154 |
src_curl_config = rapi.client.GenericCurlConfig(cafile=options.src_ca_file)
|
|
155 | 155 |
|
156 | 156 |
if options.dest_ca_file: |
157 | 157 |
logging.debug("Using '%s' as destination CA", options.dest_ca_file) |
158 |
dest_ssl_config = \
|
|
159 |
rapi.client.CertAuthorityVerify(cafile=options.dest_ca_file)
|
|
158 |
dest_curl_config = \
|
|
159 |
rapi.client.GenericCurlConfig(cafile=options.dest_ca_file)
|
|
160 | 160 |
else: |
161 | 161 |
logging.debug("Using source CA for destination") |
162 |
dest_ssl_config = src_ssl_config
|
|
162 |
dest_curl_config = src_curl_config
|
|
163 | 163 |
|
164 | 164 |
logging.debug("Source RAPI server is %s:%s", |
165 | 165 |
src_cluster_name, options.src_rapi_port) |
... | ... | |
182 | 182 |
self.GetSourceClient = lambda: \ |
183 | 183 |
rapi.client.GanetiRapiClient(src_cluster_name, |
184 | 184 |
port=options.src_rapi_port, |
185 |
config_ssl_verification=src_ssl_config,
|
|
185 |
curl_config_fn=src_curl_config,
|
|
186 | 186 |
username=src_username, |
187 | 187 |
password=src_password) |
188 | 188 |
|
... | ... | |
212 | 212 |
self.GetDestClient = lambda: \ |
213 | 213 |
rapi.client.GanetiRapiClient(dest_cluster_name, |
214 | 214 |
port=dest_rapi_port, |
215 |
config_ssl_verification=dest_ssl_config,
|
|
215 |
curl_config_fn=dest_curl_config,
|
|
216 | 216 |
username=dest_username, |
217 | 217 |
password=dest_password) |
218 | 218 |
|
... | ... | |
771 | 771 |
return (src_cluster_name, dest_cluster_name, instance_names) |
772 | 772 |
|
773 | 773 |
|
774 |
@rapi.client.UsesRapiClient |
|
774 | 775 |
def main(): |
775 | 776 |
"""Main routine. |
776 | 777 |
|
Also available in: Unified diff