Statistics
| Branch: | Tag: | Revision:

root / lib / rapi / client.py @ 95ab4de9

History | View | Annotate | Download (23.1 kB)

1 95ab4de9 David Knowles
#
2 95ab4de9 David Knowles
#
3 95ab4de9 David Knowles
4 95ab4de9 David Knowles
# Copyright (C) 2010 Google Inc.
5 95ab4de9 David Knowles
#
6 95ab4de9 David Knowles
# This program is free software; you can redistribute it and/or modify
7 95ab4de9 David Knowles
# it under the terms of the GNU General Public License as published by
8 95ab4de9 David Knowles
# the Free Software Foundation; either version 2 of the License, or
9 95ab4de9 David Knowles
# (at your option) any later version.
10 95ab4de9 David Knowles
#
11 95ab4de9 David Knowles
# This program is distributed in the hope that it will be useful, but
12 95ab4de9 David Knowles
# WITHOUT ANY WARRANTY; without even the implied warranty of
13 95ab4de9 David Knowles
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14 95ab4de9 David Knowles
# General Public License for more details.
15 95ab4de9 David Knowles
#
16 95ab4de9 David Knowles
# You should have received a copy of the GNU General Public License
17 95ab4de9 David Knowles
# along with this program; if not, write to the Free Software
18 95ab4de9 David Knowles
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
19 95ab4de9 David Knowles
# 02110-1301, USA.
20 95ab4de9 David Knowles
21 95ab4de9 David Knowles
22 95ab4de9 David Knowles
"""Ganeti RAPI client."""
23 95ab4de9 David Knowles
24 95ab4de9 David Knowles
import httplib
25 95ab4de9 David Knowles
import httplib2
26 95ab4de9 David Knowles
import simplejson
27 95ab4de9 David Knowles
import socket
28 95ab4de9 David Knowles
import urllib
29 95ab4de9 David Knowles
from OpenSSL import SSL
30 95ab4de9 David Knowles
from OpenSSL import crypto
31 95ab4de9 David Knowles
32 95ab4de9 David Knowles
33 95ab4de9 David Knowles
HTTP_DELETE = "DELETE"
34 95ab4de9 David Knowles
HTTP_GET = "GET"
35 95ab4de9 David Knowles
HTTP_PUT = "PUT"
36 95ab4de9 David Knowles
HTTP_POST = "POST"
37 95ab4de9 David Knowles
REPLACE_DISK_PRI = "replace_on_primary"
38 95ab4de9 David Knowles
REPLACE_DISK_SECONDARY = "replace_on_secondary"
39 95ab4de9 David Knowles
REPLACE_DISK_CHG = "replace_new_secondary"
40 95ab4de9 David Knowles
REPLACE_DISK_AUTO = "replace_auto"
41 95ab4de9 David Knowles
VALID_REPLACEMENT_MODES = frozenset([
42 95ab4de9 David Knowles
    REPLACE_DISK_PRI, REPLACE_DISK_SECONDARY, REPLACE_DISK_CHG,
43 95ab4de9 David Knowles
    REPLACE_DISK_AUTO
44 95ab4de9 David Knowles
    ])
45 95ab4de9 David Knowles
VALID_NODE_ROLES = frozenset([
46 95ab4de9 David Knowles
    "drained", "master", "master-candidate", "offline", "regular"
47 95ab4de9 David Knowles
    ])
48 95ab4de9 David Knowles
VALID_STORAGE_TYPES = frozenset(["file", "lvm-pv", "lvm-vg"])
49 95ab4de9 David Knowles
50 95ab4de9 David Knowles
51 95ab4de9 David Knowles
class Error(Exception):
52 95ab4de9 David Knowles
  """Base error class for this module.
53 95ab4de9 David Knowles

54 95ab4de9 David Knowles
  """
55 95ab4de9 David Knowles
  pass
56 95ab4de9 David Knowles
57 95ab4de9 David Knowles
58 95ab4de9 David Knowles
class CertificateError(Error):
59 95ab4de9 David Knowles
  """Raised when a problem is found with the SSL certificate.
60 95ab4de9 David Knowles

61 95ab4de9 David Knowles
  """
62 95ab4de9 David Knowles
  pass
63 95ab4de9 David Knowles
64 95ab4de9 David Knowles
65 95ab4de9 David Knowles
class GanetiApiError(Error):
66 95ab4de9 David Knowles
  """Generic error raised from Ganeti API.
67 95ab4de9 David Knowles

68 95ab4de9 David Knowles
  """
69 95ab4de9 David Knowles
  pass
70 95ab4de9 David Knowles
71 95ab4de9 David Knowles
72 95ab4de9 David Knowles
class InvalidReplacementMode(Error):
73 95ab4de9 David Knowles
  """Raised when an invalid disk replacement mode is attempted.
74 95ab4de9 David Knowles

75 95ab4de9 David Knowles
  """
76 95ab4de9 David Knowles
  pass
77 95ab4de9 David Knowles
78 95ab4de9 David Knowles
79 95ab4de9 David Knowles
class InvalidStorageType(Error):
80 95ab4de9 David Knowles
  """Raised when an invalid storage type is used.
81 95ab4de9 David Knowles

82 95ab4de9 David Knowles
  """
83 95ab4de9 David Knowles
  pass
84 95ab4de9 David Knowles
85 95ab4de9 David Knowles
86 95ab4de9 David Knowles
class InvalidNodeRole(Error):
87 95ab4de9 David Knowles
  """Raised when an invalid node role is used.
88 95ab4de9 David Knowles

89 95ab4de9 David Knowles
  """
90 95ab4de9 David Knowles
  pass
91 95ab4de9 David Knowles
92 95ab4de9 David Knowles
93 95ab4de9 David Knowles
class GanetiRapiClient(object):
94 95ab4de9 David Knowles
  """Ganeti RAPI client.
95 95ab4de9 David Knowles

96 95ab4de9 David Knowles
  """
97 95ab4de9 David Knowles
98 95ab4de9 David Knowles
  USER_AGENT = "Ganeti RAPI Client"
99 95ab4de9 David Knowles
100 95ab4de9 David Knowles
  def __init__(self, master_hostname, port=5080, username=None, password=None,
101 95ab4de9 David Knowles
               ssl_cert=None):
102 95ab4de9 David Knowles
    """Constructor.
103 95ab4de9 David Knowles

104 95ab4de9 David Knowles
    @type master_hostname: str
105 95ab4de9 David Knowles
    @param master_hostname: the ganeti cluster master to interact with
106 95ab4de9 David Knowles
    @type port: int
107 95ab4de9 David Knowles
    @param port: the port on which the RAPI is running. (default is 5080)
108 95ab4de9 David Knowles
    @type username: str
109 95ab4de9 David Knowles
    @param username: the username to connect with
110 95ab4de9 David Knowles
    @type password: str
111 95ab4de9 David Knowles
    @param password: the password to connect with
112 95ab4de9 David Knowles
    @type ssl_cert: str or None
113 95ab4de9 David Knowles
    @param ssl_cert: the expected SSL certificate. if None, SSL certificate
114 95ab4de9 David Knowles
        will not be verified
115 95ab4de9 David Knowles

116 95ab4de9 David Knowles
    """
117 95ab4de9 David Knowles
    self._master_hostname = master_hostname
118 95ab4de9 David Knowles
    self._port = port
119 95ab4de9 David Knowles
    if ssl_cert:
120 95ab4de9 David Knowles
      _VerifyCertificate(self._master_hostname, self._port, ssl_cert)
121 95ab4de9 David Knowles
122 95ab4de9 David Knowles
    self._http = httplib2.Http()
123 95ab4de9 David Knowles
    self._headers = {
124 95ab4de9 David Knowles
        "Accept": "text/plain",
125 95ab4de9 David Knowles
        "Content-type": "application/x-www-form-urlencoded",
126 95ab4de9 David Knowles
        "User-Agent": self.USER_AGENT}
127 95ab4de9 David Knowles
    self._version = None
128 95ab4de9 David Knowles
    if username and password:
129 95ab4de9 David Knowles
      self._http.add_credentials(username, password)
130 95ab4de9 David Knowles
131 95ab4de9 David Knowles
  def _MakeUrl(self, path, query=None, prepend_version=True):
132 95ab4de9 David Knowles
    """Constructs the URL to pass to the HTTP client.
133 95ab4de9 David Knowles

134 95ab4de9 David Knowles
    @type path: str
135 95ab4de9 David Knowles
    @param path: HTTP URL path
136 95ab4de9 David Knowles
    @type query: list of two-tuples
137 95ab4de9 David Knowles
    @param query: query arguments to pass to urllib.urlencode
138 95ab4de9 David Knowles
    @type prepend_version: bool
139 95ab4de9 David Knowles
    @param prepend_version: whether to automatically fetch and prepend the
140 95ab4de9 David Knowles
        Ganeti version to the URL path
141 95ab4de9 David Knowles

142 95ab4de9 David Knowles
    @rtype:  str
143 95ab4de9 David Knowles
    @return: URL path
144 95ab4de9 David Knowles

145 95ab4de9 David Knowles
    """
146 95ab4de9 David Knowles
    if prepend_version:
147 95ab4de9 David Knowles
      if not self._version:
148 95ab4de9 David Knowles
        self._GetVersionInternal()
149 95ab4de9 David Knowles
      path = "/%d%s" % (self._version, path)
150 95ab4de9 David Knowles
151 95ab4de9 David Knowles
    return "https://%(host)s:%(port)d%(path)s?%(query)s" % {
152 95ab4de9 David Knowles
        "host": self._master_hostname,
153 95ab4de9 David Knowles
        "port": self._port,
154 95ab4de9 David Knowles
        "path": path,
155 95ab4de9 David Knowles
        "query": urllib.urlencode(query or [])}
156 95ab4de9 David Knowles
157 95ab4de9 David Knowles
  def _SendRequest(self, method, path, query=None, content=None,
158 95ab4de9 David Knowles
                   prepend_version=True):
159 95ab4de9 David Knowles
    """Sends an HTTP request.
160 95ab4de9 David Knowles

161 95ab4de9 David Knowles
    This constructs a full URL, encodes and decodes HTTP bodies, and
162 95ab4de9 David Knowles
    handles invalid responses in a pythonic way.
163 95ab4de9 David Knowles

164 95ab4de9 David Knowles
    @type method: str
165 95ab4de9 David Knowles
    @param method: HTTP method to use
166 95ab4de9 David Knowles
    @type path: str
167 95ab4de9 David Knowles
    @param path: HTTP URL path
168 95ab4de9 David Knowles
    @type query: list of two-tuples
169 95ab4de9 David Knowles
    @param query: query arguments to pass to urllib.urlencode
170 95ab4de9 David Knowles
    @type content: str or None
171 95ab4de9 David Knowles
    @param content: HTTP body content
172 95ab4de9 David Knowles
    @type prepend_version: bool
173 95ab4de9 David Knowles
    @param prepend_version: whether to automatically fetch and prepend the
174 95ab4de9 David Knowles
        Ganeti version to the URL path
175 95ab4de9 David Knowles

176 95ab4de9 David Knowles
    @rtype: str
177 95ab4de9 David Knowles
    @return: JSON-Decoded response
178 95ab4de9 David Knowles

179 95ab4de9 David Knowles
    @raises GanetiApiError: If an invalid response is returned
180 95ab4de9 David Knowles

181 95ab4de9 David Knowles
    """
182 95ab4de9 David Knowles
    if content:
183 95ab4de9 David Knowles
      simplejson.JSONEncoder(sort_keys=True).encode(content)
184 95ab4de9 David Knowles
185 95ab4de9 David Knowles
    url = self._MakeUrl(path, query, prepend_version)
186 95ab4de9 David Knowles
    resp_headers, resp_content = self._http.request(
187 95ab4de9 David Knowles
        url, method, body=content, headers=self._headers)
188 95ab4de9 David Knowles
189 95ab4de9 David Knowles
    if resp_content:
190 95ab4de9 David Knowles
      resp_content = simplejson.loads(resp_content)
191 95ab4de9 David Knowles
192 95ab4de9 David Knowles
    # TODO: Are there other status codes that are valid? (redirect?)
193 95ab4de9 David Knowles
    if resp_headers.status != 200:
194 95ab4de9 David Knowles
      if isinstance(resp_content, dict):
195 95ab4de9 David Knowles
        msg = ("%s %s: %s" %
196 95ab4de9 David Knowles
            (resp_content["code"], resp_content["message"],
197 95ab4de9 David Knowles
             resp_content["explain"]))
198 95ab4de9 David Knowles
      else:
199 95ab4de9 David Knowles
        msg = resp_content
200 95ab4de9 David Knowles
      raise GanetiApiError(msg)
201 95ab4de9 David Knowles
202 95ab4de9 David Knowles
    return resp_content
203 95ab4de9 David Knowles
204 95ab4de9 David Knowles
  def _GetVersionInternal(self):
205 95ab4de9 David Knowles
    """Gets the Remote API version running on the cluster.
206 95ab4de9 David Knowles

207 95ab4de9 David Knowles
    @rtype: int
208 95ab4de9 David Knowles
    @return: Ganeti version
209 95ab4de9 David Knowles

210 95ab4de9 David Knowles
    """
211 95ab4de9 David Knowles
    self._version = self._SendRequest(HTTP_GET, "/version",
212 95ab4de9 David Knowles
                                      prepend_version=False)
213 95ab4de9 David Knowles
    return self._version
214 95ab4de9 David Knowles
215 95ab4de9 David Knowles
  def GetVersion(self):
216 95ab4de9 David Knowles
    """Gets the ganeti version running on the cluster.
217 95ab4de9 David Knowles

218 95ab4de9 David Knowles
    @rtype: int
219 95ab4de9 David Knowles
    @return: Ganeti version
220 95ab4de9 David Knowles

221 95ab4de9 David Knowles
    """
222 95ab4de9 David Knowles
    if not self._version:
223 95ab4de9 David Knowles
      self._GetVersionInternal()
224 95ab4de9 David Knowles
    return self._version
225 95ab4de9 David Knowles
226 95ab4de9 David Knowles
  def GetOperatingSystems(self):
227 95ab4de9 David Knowles
    """Gets the Operating Systems running in the Ganeti cluster.
228 95ab4de9 David Knowles

229 95ab4de9 David Knowles
    @rtype: list of str
230 95ab4de9 David Knowles
    @return: operating systems
231 95ab4de9 David Knowles

232 95ab4de9 David Knowles
    """
233 95ab4de9 David Knowles
    return self._SendRequest(HTTP_GET, "/os")
234 95ab4de9 David Knowles
235 95ab4de9 David Knowles
  def GetInfo(self):
236 95ab4de9 David Knowles
    """Gets info about the cluster.
237 95ab4de9 David Knowles

238 95ab4de9 David Knowles
    @rtype: dict
239 95ab4de9 David Knowles
    @return: information about the cluster
240 95ab4de9 David Knowles

241 95ab4de9 David Knowles
    """
242 95ab4de9 David Knowles
    return self._SendRequest(HTTP_GET, "/info")
243 95ab4de9 David Knowles
244 95ab4de9 David Knowles
  def GetClusterTags(self):
245 95ab4de9 David Knowles
    """Gets the cluster tags.
246 95ab4de9 David Knowles

247 95ab4de9 David Knowles
    @rtype: list of str
248 95ab4de9 David Knowles
    @return: cluster tags
249 95ab4de9 David Knowles

250 95ab4de9 David Knowles
    """
251 95ab4de9 David Knowles
    return self._SendRequest(HTTP_GET, "/tags")
252 95ab4de9 David Knowles
253 95ab4de9 David Knowles
  def AddClusterTags(self, tags, dry_run=False):
254 95ab4de9 David Knowles
    """Adds tags to the cluster.
255 95ab4de9 David Knowles

256 95ab4de9 David Knowles
    @type tags: list of str
257 95ab4de9 David Knowles
    @param tags: tags to add to the cluster
258 95ab4de9 David Knowles
    @type dry_run: bool
259 95ab4de9 David Knowles
    @param dry_run: whether to perform a dry run
260 95ab4de9 David Knowles

261 95ab4de9 David Knowles
    @rtype: int
262 95ab4de9 David Knowles
    @return: job id
263 95ab4de9 David Knowles

264 95ab4de9 David Knowles
    """
265 95ab4de9 David Knowles
    query = [("tag", t) for t in tags]
266 95ab4de9 David Knowles
    if dry_run:
267 95ab4de9 David Knowles
      query.append(("dry-run", 1))
268 95ab4de9 David Knowles
269 95ab4de9 David Knowles
    self._SendRequest(HTTP_PUT, "/tags", query)
270 95ab4de9 David Knowles
271 95ab4de9 David Knowles
  def DeleteClusterTags(self, tags, dry_run=False):
272 95ab4de9 David Knowles
    """Deletes tags from the cluster.
273 95ab4de9 David Knowles

274 95ab4de9 David Knowles
    @type tags: list of str
275 95ab4de9 David Knowles
    @param tags: tags to delete
276 95ab4de9 David Knowles
    @type dry_run: bool
277 95ab4de9 David Knowles
    @param dry_run: whether to perform a dry run
278 95ab4de9 David Knowles

279 95ab4de9 David Knowles
    """
280 95ab4de9 David Knowles
    query = [("tag", t) for t in tags]
281 95ab4de9 David Knowles
    if dry_run:
282 95ab4de9 David Knowles
      query.append(("dry-run", 1))
283 95ab4de9 David Knowles
284 95ab4de9 David Knowles
    self._SendRequest(HTTP_DELETE, "/tags", query)
285 95ab4de9 David Knowles
286 95ab4de9 David Knowles
  def GetInstances(self, bulk=False):
287 95ab4de9 David Knowles
    """Gets information about instances on the cluster.
288 95ab4de9 David Knowles

289 95ab4de9 David Knowles
    @type bulk: bool
290 95ab4de9 David Knowles
    @param bulk: whether to return all information about all instances
291 95ab4de9 David Knowles

292 95ab4de9 David Knowles
    @rtype: list of dict or list of str
293 95ab4de9 David Knowles
    @return: if bulk is True, info about the instances, else a list of instances
294 95ab4de9 David Knowles

295 95ab4de9 David Knowles
    """
296 95ab4de9 David Knowles
    query = []
297 95ab4de9 David Knowles
    if bulk:
298 95ab4de9 David Knowles
      query.append(("bulk", 1))
299 95ab4de9 David Knowles
300 95ab4de9 David Knowles
    instances = self._SendRequest(HTTP_GET, "/instances", query)
301 95ab4de9 David Knowles
    if bulk:
302 95ab4de9 David Knowles
      return instances
303 95ab4de9 David Knowles
    else:
304 95ab4de9 David Knowles
      return [i["id"] for i in instances]
305 95ab4de9 David Knowles
306 95ab4de9 David Knowles
307 95ab4de9 David Knowles
  def GetInstanceInfo(self, instance):
308 95ab4de9 David Knowles
    """Gets information about an instance.
309 95ab4de9 David Knowles

310 95ab4de9 David Knowles
    @type instance: str
311 95ab4de9 David Knowles
    @param instance: instance whose info to return
312 95ab4de9 David Knowles

313 95ab4de9 David Knowles
    @rtype: dict
314 95ab4de9 David Knowles
    @return: info about the instance
315 95ab4de9 David Knowles

316 95ab4de9 David Knowles
    """
317 95ab4de9 David Knowles
    return self._SendRequest(HTTP_GET, "/instances/%s" % instance)
318 95ab4de9 David Knowles
319 95ab4de9 David Knowles
  def CreateInstance(self, dry_run=False):
320 95ab4de9 David Knowles
    """Creates a new instance.
321 95ab4de9 David Knowles

322 95ab4de9 David Knowles
    @type dry_run: bool
323 95ab4de9 David Knowles
    @param dry_run: whether to perform a dry run
324 95ab4de9 David Knowles

325 95ab4de9 David Knowles
    @rtype: int
326 95ab4de9 David Knowles
    @return: job id
327 95ab4de9 David Knowles

328 95ab4de9 David Knowles
    """
329 95ab4de9 David Knowles
    # TODO: Pass arguments needed to actually create an instance.
330 95ab4de9 David Knowles
    query = []
331 95ab4de9 David Knowles
    if dry_run:
332 95ab4de9 David Knowles
      query.append(("dry-run", 1))
333 95ab4de9 David Knowles
334 95ab4de9 David Knowles
    return self._SendRequest(HTTP_POST, "/instances", query)
335 95ab4de9 David Knowles
336 95ab4de9 David Knowles
  def DeleteInstance(self, instance, dry_run=False):
337 95ab4de9 David Knowles
    """Deletes an instance.
338 95ab4de9 David Knowles

339 95ab4de9 David Knowles
    @type instance: str
340 95ab4de9 David Knowles
    @param instance: the instance to delete
341 95ab4de9 David Knowles

342 95ab4de9 David Knowles
    """
343 95ab4de9 David Knowles
    query = []
344 95ab4de9 David Knowles
    if dry_run:
345 95ab4de9 David Knowles
      query.append(("dry-run", 1))
346 95ab4de9 David Knowles
347 95ab4de9 David Knowles
    self._SendRequest(HTTP_DELETE, "/instances/%s" % instance, query)
348 95ab4de9 David Knowles
349 95ab4de9 David Knowles
  def GetInstanceTags(self, instance):
350 95ab4de9 David Knowles
    """Gets tags for an instance.
351 95ab4de9 David Knowles

352 95ab4de9 David Knowles
    @type instance: str
353 95ab4de9 David Knowles
    @param instance: instance whose tags to return
354 95ab4de9 David Knowles

355 95ab4de9 David Knowles
    @rtype: list of str
356 95ab4de9 David Knowles
    @return: tags for the instance
357 95ab4de9 David Knowles

358 95ab4de9 David Knowles
    """
359 95ab4de9 David Knowles
    return self._SendRequest(HTTP_GET, "/instances/%s/tags" % instance)
360 95ab4de9 David Knowles
361 95ab4de9 David Knowles
  def AddInstanceTags(self, instance, tags, dry_run=False):
362 95ab4de9 David Knowles
    """Adds tags to an instance.
363 95ab4de9 David Knowles

364 95ab4de9 David Knowles
    @type instance: str
365 95ab4de9 David Knowles
    @param instance: instance to add tags to
366 95ab4de9 David Knowles
    @type tags: list of str
367 95ab4de9 David Knowles
    @param tags: tags to add to the instance
368 95ab4de9 David Knowles
    @type dry_run: bool
369 95ab4de9 David Knowles
    @param dry_run: whether to perform a dry run
370 95ab4de9 David Knowles

371 95ab4de9 David Knowles
    @rtype: int
372 95ab4de9 David Knowles
    @return: job id
373 95ab4de9 David Knowles

374 95ab4de9 David Knowles
    """
375 95ab4de9 David Knowles
    query = [("tag", t) for t in tags]
376 95ab4de9 David Knowles
    if dry_run:
377 95ab4de9 David Knowles
      query.append(("dry-run", 1))
378 95ab4de9 David Knowles
379 95ab4de9 David Knowles
    self._SendRequest(HTTP_PUT, "/instances/%s/tags" % instance, query)
380 95ab4de9 David Knowles
381 95ab4de9 David Knowles
  def DeleteInstanceTags(self, instance, tags, dry_run=False):
382 95ab4de9 David Knowles
    """Deletes tags from an instance.
383 95ab4de9 David Knowles

384 95ab4de9 David Knowles
    @type instance: str
385 95ab4de9 David Knowles
    @param instance: instance to delete tags from
386 95ab4de9 David Knowles
    @type tags: list of str
387 95ab4de9 David Knowles
    @param tags: tags to delete
388 95ab4de9 David Knowles
    @type dry_run: bool
389 95ab4de9 David Knowles
    @param dry_run: whether to perform a dry run
390 95ab4de9 David Knowles

391 95ab4de9 David Knowles
    """
392 95ab4de9 David Knowles
    query = [("tag", t) for t in tags]
393 95ab4de9 David Knowles
    if dry_run:
394 95ab4de9 David Knowles
      query.append(("dry-run", 1))
395 95ab4de9 David Knowles
396 95ab4de9 David Knowles
    self._SendRequest(HTTP_DELETE, "/instances/%s/tags" % instance, query)
397 95ab4de9 David Knowles
398 95ab4de9 David Knowles
  def RebootInstance(self, instance, reboot_type=None, ignore_secondaries=None,
399 95ab4de9 David Knowles
                     dry_run=False):
400 95ab4de9 David Knowles
    """Reboots an instance.
401 95ab4de9 David Knowles

402 95ab4de9 David Knowles
    @type instance: str
403 95ab4de9 David Knowles
    @param instance: instance to rebot
404 95ab4de9 David Knowles
    @type reboot_type: str
405 95ab4de9 David Knowles
    @param reboot_type: one of: hard, soft, full
406 95ab4de9 David Knowles
    @type ignore_secondaries: bool
407 95ab4de9 David Knowles
    @param ignore_secondaries: if True, ignores errors for the secondary node
408 95ab4de9 David Knowles
        while re-assembling disks (in hard-reboot mode only)
409 95ab4de9 David Knowles
    @type dry_run: bool
410 95ab4de9 David Knowles
    @param dry_run: whether to perform a dry run
411 95ab4de9 David Knowles

412 95ab4de9 David Knowles
    """
413 95ab4de9 David Knowles
    query = []
414 95ab4de9 David Knowles
    if reboot_type:
415 95ab4de9 David Knowles
      query.append(("type", reboot_type))
416 95ab4de9 David Knowles
    if ignore_secondaries is not None:
417 95ab4de9 David Knowles
      query.append(("ignore_secondaries", ignore_secondaries))
418 95ab4de9 David Knowles
    if dry_run:
419 95ab4de9 David Knowles
      query.append(("dry-run", 1))
420 95ab4de9 David Knowles
421 95ab4de9 David Knowles
    self._SendRequest(HTTP_POST, "/instances/%s/reboot" % instance, query)
422 95ab4de9 David Knowles
423 95ab4de9 David Knowles
  def ShutdownInstance(self, instance, dry_run=False):
424 95ab4de9 David Knowles
    """Shuts down an instance.
425 95ab4de9 David Knowles

426 95ab4de9 David Knowles
    @type instance: str
427 95ab4de9 David Knowles
    @param instance: the instance to shut down
428 95ab4de9 David Knowles
    @type dry_run: bool
429 95ab4de9 David Knowles
    @param dry_run: whether to perform a dry run
430 95ab4de9 David Knowles

431 95ab4de9 David Knowles
    """
432 95ab4de9 David Knowles
    query = []
433 95ab4de9 David Knowles
    if dry_run:
434 95ab4de9 David Knowles
      query.append(("dry-run", 1))
435 95ab4de9 David Knowles
436 95ab4de9 David Knowles
    self._SendRequest(HTTP_PUT, "/instances/%s/shutdown" % instance, query)
437 95ab4de9 David Knowles
438 95ab4de9 David Knowles
  def StartupInstance(self, instance, dry_run=False):
439 95ab4de9 David Knowles
    """Starts up an instance.
440 95ab4de9 David Knowles

441 95ab4de9 David Knowles
    @type instance: str
442 95ab4de9 David Knowles
    @param instance: the instance to start up
443 95ab4de9 David Knowles
    @type dry_run: bool
444 95ab4de9 David Knowles
    @param dry_run: whether to perform a dry run
445 95ab4de9 David Knowles

446 95ab4de9 David Knowles
    """
447 95ab4de9 David Knowles
    query = []
448 95ab4de9 David Knowles
    if dry_run:
449 95ab4de9 David Knowles
      query.append(("dry-run", 1))
450 95ab4de9 David Knowles
451 95ab4de9 David Knowles
    self._SendRequest(HTTP_PUT, "/instances/%s/startup" % instance, query)
452 95ab4de9 David Knowles
453 95ab4de9 David Knowles
  def ReinstallInstance(self, instance, os, no_startup=False):
454 95ab4de9 David Knowles
    """Reinstalls an instance.
455 95ab4de9 David Knowles

456 95ab4de9 David Knowles
    @type instance: str
457 95ab4de9 David Knowles
    @param instance: the instance to reinstall
458 95ab4de9 David Knowles
    @type os: str
459 95ab4de9 David Knowles
    @param os: the os to reinstall
460 95ab4de9 David Knowles
    @type no_startup: bool
461 95ab4de9 David Knowles
    @param no_startup: whether to start the instance automatically
462 95ab4de9 David Knowles

463 95ab4de9 David Knowles
    """
464 95ab4de9 David Knowles
    query = [("os", os)]
465 95ab4de9 David Knowles
    if no_startup:
466 95ab4de9 David Knowles
      query.append(("nostartup", 1))
467 95ab4de9 David Knowles
    self._SendRequest(HTTP_POST, "/instances/%s/reinstall" % instance, query)
468 95ab4de9 David Knowles
469 95ab4de9 David Knowles
  def ReplaceInstanceDisks(self, instance, disks, mode="replace_auto",
470 95ab4de9 David Knowles
                           remote_node=None, iallocator="hail", dry_run=False):
471 95ab4de9 David Knowles
    """Replaces disks on an instance.
472 95ab4de9 David Knowles

473 95ab4de9 David Knowles
    @type instance: str
474 95ab4de9 David Knowles
    @param instance: instance whose disks to replace
475 95ab4de9 David Knowles
    @type disks: list of str
476 95ab4de9 David Knowles
    @param disks: disks to replace
477 95ab4de9 David Knowles
    @type mode: str
478 95ab4de9 David Knowles
    @param mode: replacement mode to use. defaults to replace_auto
479 95ab4de9 David Knowles
    @type remote_node: str or None
480 95ab4de9 David Knowles
    @param remote_node: new secondary node to use (for use with
481 95ab4de9 David Knowles
        replace_new_secondary mdoe)
482 95ab4de9 David Knowles
    @type iallocator: str or None
483 95ab4de9 David Knowles
    @param iallocator: instance allocator plugin to use (for use with
484 95ab4de9 David Knowles
        replace_auto mdoe).  default is hail
485 95ab4de9 David Knowles
    @type dry_run: bool
486 95ab4de9 David Knowles
    @param dry_run: whether to perform a dry run
487 95ab4de9 David Knowles

488 95ab4de9 David Knowles
    @rtype: int
489 95ab4de9 David Knowles
    @return: job id
490 95ab4de9 David Knowles

491 95ab4de9 David Knowles
    @raises InvalidReplacementMode: If an invalid disk replacement mode is given
492 95ab4de9 David Knowles
    @raises GanetiApiError: If no secondary node is given with a non-auto
493 95ab4de9 David Knowles
        replacement mode is requested.
494 95ab4de9 David Knowles

495 95ab4de9 David Knowles
    """
496 95ab4de9 David Knowles
    if mode not in VALID_REPLACEMENT_MODES:
497 95ab4de9 David Knowles
      raise InvalidReplacementMode("%s is not a valid disk replacement mode.",
498 95ab4de9 David Knowles
                                   mode)
499 95ab4de9 David Knowles
500 95ab4de9 David Knowles
    query = [("mode", mode), ("disks", ",".join(disks))]
501 95ab4de9 David Knowles
502 95ab4de9 David Knowles
    if mode is REPLACE_DISK_AUTO:
503 95ab4de9 David Knowles
      query.append(("iallocator", iallocator))
504 95ab4de9 David Knowles
    elif mode is REPLACE_DISK_SECONDARY:
505 95ab4de9 David Knowles
      if remote_node is None:
506 95ab4de9 David Knowles
        raise GanetiApiError("You must supply a new secondary node.")
507 95ab4de9 David Knowles
      query.append(("remote_node", remote_node))
508 95ab4de9 David Knowles
509 95ab4de9 David Knowles
    if dry_run:
510 95ab4de9 David Knowles
      query.append(("dry-run", 1))
511 95ab4de9 David Knowles
512 95ab4de9 David Knowles
    return self._SendRequest(HTTP_POST,
513 95ab4de9 David Knowles
                             "/instances/%s/replace-disks" % instance, query)
514 95ab4de9 David Knowles
515 95ab4de9 David Knowles
  def GetJobs(self):
516 95ab4de9 David Knowles
    """Gets all jobs for the cluster.
517 95ab4de9 David Knowles

518 95ab4de9 David Knowles
    @rtype: list of int
519 95ab4de9 David Knowles
    @return: job ids for the cluster
520 95ab4de9 David Knowles

521 95ab4de9 David Knowles
    """
522 95ab4de9 David Knowles
    return [int(j["id"]) for j in self._SendRequest(HTTP_GET, "/jobs")]
523 95ab4de9 David Knowles
524 95ab4de9 David Knowles
  def GetJobStatus(self, job_id):
525 95ab4de9 David Knowles
    """Gets the status of a job.
526 95ab4de9 David Knowles

527 95ab4de9 David Knowles
    @type job_id: int
528 95ab4de9 David Knowles
    @param job_id: job id whose status to query
529 95ab4de9 David Knowles

530 95ab4de9 David Knowles
    @rtype: dict
531 95ab4de9 David Knowles
    @return: job status
532 95ab4de9 David Knowles

533 95ab4de9 David Knowles
    """
534 95ab4de9 David Knowles
    return self._SendRequest(HTTP_GET, "/jobs/%d" % job_id)
535 95ab4de9 David Knowles
536 95ab4de9 David Knowles
  def DeleteJob(self, job_id, dry_run=False):
537 95ab4de9 David Knowles
    """Deletes a job.
538 95ab4de9 David Knowles

539 95ab4de9 David Knowles
    @type job_id: int
540 95ab4de9 David Knowles
    @param job_id: id of the job to delete
541 95ab4de9 David Knowles
    @type dry_run: bool
542 95ab4de9 David Knowles
    @param dry_run: whether to perform a dry run
543 95ab4de9 David Knowles

544 95ab4de9 David Knowles
    """
545 95ab4de9 David Knowles
    query = []
546 95ab4de9 David Knowles
    if dry_run:
547 95ab4de9 David Knowles
      query.append(("dry-run", 1))
548 95ab4de9 David Knowles
549 95ab4de9 David Knowles
    self._SendRequest(HTTP_DELETE, "/jobs/%d" % job_id, query)
550 95ab4de9 David Knowles
551 95ab4de9 David Knowles
  def GetNodes(self, bulk=False):
552 95ab4de9 David Knowles
    """Gets all nodes in the cluster.
553 95ab4de9 David Knowles

554 95ab4de9 David Knowles
    @type bulk: bool
555 95ab4de9 David Knowles
    @param bulk: whether to return all information about all instances
556 95ab4de9 David Knowles

557 95ab4de9 David Knowles
    @rtype: list of dict or str
558 95ab4de9 David Knowles
    @return: if bulk is true, info about nodes in the cluster,
559 95ab4de9 David Knowles
        else list of nodes in the cluster
560 95ab4de9 David Knowles

561 95ab4de9 David Knowles
    """
562 95ab4de9 David Knowles
    query = []
563 95ab4de9 David Knowles
    if bulk:
564 95ab4de9 David Knowles
      query.append(("bulk", 1))
565 95ab4de9 David Knowles
566 95ab4de9 David Knowles
    nodes = self._SendRequest(HTTP_GET, "/nodes", query)
567 95ab4de9 David Knowles
    if bulk:
568 95ab4de9 David Knowles
      return nodes
569 95ab4de9 David Knowles
    else:
570 95ab4de9 David Knowles
      return [n["id"] for n in nodes]
571 95ab4de9 David Knowles
572 95ab4de9 David Knowles
  def GetNodeInfo(self, node):
573 95ab4de9 David Knowles
    """Gets information about a node.
574 95ab4de9 David Knowles

575 95ab4de9 David Knowles
    @type node: str
576 95ab4de9 David Knowles
    @param node: node whose info to return
577 95ab4de9 David Knowles

578 95ab4de9 David Knowles
    @rtype: dict
579 95ab4de9 David Knowles
    @return: info about the node
580 95ab4de9 David Knowles

581 95ab4de9 David Knowles
    """
582 95ab4de9 David Knowles
    return self._SendRequest(HTTP_GET, "/nodes/%s" % node)
583 95ab4de9 David Knowles
584 95ab4de9 David Knowles
  def EvacuateNode(self, node, iallocator=None, remote_node=None,
585 95ab4de9 David Knowles
                   dry_run=False):
586 95ab4de9 David Knowles
    """Evacuates instances from a Ganeti node.
587 95ab4de9 David Knowles

588 95ab4de9 David Knowles
    @type node: str
589 95ab4de9 David Knowles
    @param node: node to evacuate
590 95ab4de9 David Knowles
    @type iallocator: str or None
591 95ab4de9 David Knowles
    @param iallocator: instance allocator to use
592 95ab4de9 David Knowles
    @type remote_node: str
593 95ab4de9 David Knowles
    @param remote_node: node to evaucate to
594 95ab4de9 David Knowles
    @type dry_run: bool
595 95ab4de9 David Knowles
    @param dry_run: whether to perform a dry run
596 95ab4de9 David Knowles

597 95ab4de9 David Knowles
    @rtype: int
598 95ab4de9 David Knowles
    @return: job id
599 95ab4de9 David Knowles

600 95ab4de9 David Knowles
    @raises GanetiApiError: if an iallocator and remote_node are both specified
601 95ab4de9 David Knowles

602 95ab4de9 David Knowles
    """
603 95ab4de9 David Knowles
    query = []
604 95ab4de9 David Knowles
    if iallocator and remote_node:
605 95ab4de9 David Knowles
      raise GanetiApiError("Only one of iallocator or remote_node can be used.")
606 95ab4de9 David Knowles
607 95ab4de9 David Knowles
    if iallocator:
608 95ab4de9 David Knowles
      query.append(("iallocator", iallocator))
609 95ab4de9 David Knowles
    if remote_node:
610 95ab4de9 David Knowles
      query.append(("remote_node", remote_node))
611 95ab4de9 David Knowles
    if dry_run:
612 95ab4de9 David Knowles
      query.append(("dry-run", 1))
613 95ab4de9 David Knowles
614 95ab4de9 David Knowles
    return self._SendRequest(HTTP_POST, "/nodes/%s/evacuate" % node, query)
615 95ab4de9 David Knowles
616 95ab4de9 David Knowles
  def MigrateNode(self, node, live=True, dry_run=False):
617 95ab4de9 David Knowles
    """Migrates all primary instances from a node.
618 95ab4de9 David Knowles

619 95ab4de9 David Knowles
    @type node: str
620 95ab4de9 David Knowles
    @param node: node to migrate
621 95ab4de9 David Knowles
    @type live: bool
622 95ab4de9 David Knowles
    @param live: whether to use live migration
623 95ab4de9 David Knowles
    @type dry_run: bool
624 95ab4de9 David Knowles
    @param dry_run: whether to perform a dry run
625 95ab4de9 David Knowles

626 95ab4de9 David Knowles
    @rtype: int
627 95ab4de9 David Knowles
    @return: job id
628 95ab4de9 David Knowles

629 95ab4de9 David Knowles
    """
630 95ab4de9 David Knowles
    query = []
631 95ab4de9 David Knowles
    if live:
632 95ab4de9 David Knowles
      query.append(("live", 1))
633 95ab4de9 David Knowles
    if dry_run:
634 95ab4de9 David Knowles
      query.append(("dry-run", 1))
635 95ab4de9 David Knowles
636 95ab4de9 David Knowles
    return self._SendRequest(HTTP_POST, "/nodes/%s/migrate" % node, query)
637 95ab4de9 David Knowles
638 95ab4de9 David Knowles
  def GetNodeRole(self, node):
639 95ab4de9 David Knowles
    """Gets the current role for a node.
640 95ab4de9 David Knowles

641 95ab4de9 David Knowles
    @type node: str
642 95ab4de9 David Knowles
    @param node: node whose role to return
643 95ab4de9 David Knowles

644 95ab4de9 David Knowles
    @rtype: str
645 95ab4de9 David Knowles
    @return: the current role for a node
646 95ab4de9 David Knowles

647 95ab4de9 David Knowles
    """
648 95ab4de9 David Knowles
    return self._SendRequest(HTTP_GET, "/nodes/%s/role" % node)
649 95ab4de9 David Knowles
650 95ab4de9 David Knowles
  def SetNodeRole(self, node, role, force=False):
651 95ab4de9 David Knowles
    """Sets the role for a node.
652 95ab4de9 David Knowles

653 95ab4de9 David Knowles
    @type node: str
654 95ab4de9 David Knowles
    @param node: the node whose role to set
655 95ab4de9 David Knowles
    @type role: str
656 95ab4de9 David Knowles
    @param role: the role to set for the node
657 95ab4de9 David Knowles
    @type force: bool
658 95ab4de9 David Knowles
    @param force: whether to force the role change
659 95ab4de9 David Knowles

660 95ab4de9 David Knowles
    @rtype: int
661 95ab4de9 David Knowles
    @return: job id
662 95ab4de9 David Knowles

663 95ab4de9 David Knowles
    @raise InvalidNodeRole: If an invalid node role is specified
664 95ab4de9 David Knowles

665 95ab4de9 David Knowles
    """
666 95ab4de9 David Knowles
    if role not in VALID_NODE_ROLES:
667 95ab4de9 David Knowles
      raise InvalidNodeRole("%s is not a valid node role.", role)
668 95ab4de9 David Knowles
669 95ab4de9 David Knowles
    query = [("force", force)]
670 95ab4de9 David Knowles
    return self._SendRequest(HTTP_PUT, "/nodes/%s/role" % node, query,
671 95ab4de9 David Knowles
                             content=role)
672 95ab4de9 David Knowles
673 95ab4de9 David Knowles
  def GetNodeStorageUnits(self, node, storage_type, output_fields):
674 95ab4de9 David Knowles
    """Gets the storage units for a node.
675 95ab4de9 David Knowles

676 95ab4de9 David Knowles
    @type node: str
677 95ab4de9 David Knowles
    @param node: the node whose storage units to return
678 95ab4de9 David Knowles
    @type storage_type: str
679 95ab4de9 David Knowles
    @param storage_type: storage type whose units to return
680 95ab4de9 David Knowles
    @type output_fields: str
681 95ab4de9 David Knowles
    @param output_fields: storage type fields to return
682 95ab4de9 David Knowles

683 95ab4de9 David Knowles
    @rtype: int
684 95ab4de9 David Knowles
    @return: job id where results can be retrieved
685 95ab4de9 David Knowles

686 95ab4de9 David Knowles
    @raise InvalidStorageType: If an invalid storage type is specified
687 95ab4de9 David Knowles

688 95ab4de9 David Knowles
    """
689 95ab4de9 David Knowles
    # TODO: Add default for storage_type & output_fields
690 95ab4de9 David Knowles
    if storage_type not in VALID_STORAGE_TYPES:
691 95ab4de9 David Knowles
      raise InvalidStorageType("%s is an invalid storage type.", storage_type)
692 95ab4de9 David Knowles
693 95ab4de9 David Knowles
    query = [("storage_type", storage_type), ("output_fields", output_fields)]
694 95ab4de9 David Knowles
    return self._SendRequest(HTTP_GET, "/nodes/%s/storage" % node, query)
695 95ab4de9 David Knowles
696 95ab4de9 David Knowles
  def ModifyNodeStorageUnits(self, node, storage_type, name, allocatable=True):
697 95ab4de9 David Knowles
    """Modifies parameters of storage units on the node.
698 95ab4de9 David Knowles

699 95ab4de9 David Knowles
    @type node: str
700 95ab4de9 David Knowles
    @param node: node whose storage units to modify
701 95ab4de9 David Knowles
    @type storage_type: str
702 95ab4de9 David Knowles
    @param storage_type: storage type whose units to modify
703 95ab4de9 David Knowles
    @type name: str
704 95ab4de9 David Knowles
    @param name: name of the storage unit
705 95ab4de9 David Knowles
    @type allocatable: bool
706 95ab4de9 David Knowles
    @param allocatable: TODO: Document me
707 95ab4de9 David Knowles

708 95ab4de9 David Knowles
    @rtype: int
709 95ab4de9 David Knowles
    @return: job id
710 95ab4de9 David Knowles

711 95ab4de9 David Knowles
    @raise InvalidStorageType: If an invalid storage type is specified
712 95ab4de9 David Knowles

713 95ab4de9 David Knowles
    """
714 95ab4de9 David Knowles
    if storage_type not in VALID_STORAGE_TYPES:
715 95ab4de9 David Knowles
      raise InvalidStorageType("%s is an invalid storage type.", storage_type)
716 95ab4de9 David Knowles
717 95ab4de9 David Knowles
    query = [
718 95ab4de9 David Knowles
        ("storage_type", storage_type), ("name", name),
719 95ab4de9 David Knowles
        ("allocatable", allocatable)
720 95ab4de9 David Knowles
        ]
721 95ab4de9 David Knowles
    return self._SendRequest(HTTP_PUT, "/nodes/%s/storage/modify" % node, query)
722 95ab4de9 David Knowles
723 95ab4de9 David Knowles
  def RepairNodeStorageUnits(self, node, storage_type, name):
724 95ab4de9 David Knowles
    """Repairs a storage unit on the node.
725 95ab4de9 David Knowles

726 95ab4de9 David Knowles
    @type node: str
727 95ab4de9 David Knowles
    @param node: node whose storage units to repair
728 95ab4de9 David Knowles
    @type storage_type: str
729 95ab4de9 David Knowles
    @param storage_type: storage type to repair
730 95ab4de9 David Knowles
    @type name: str
731 95ab4de9 David Knowles
    @param name: name of the storage unit to repair
732 95ab4de9 David Knowles

733 95ab4de9 David Knowles
    @rtype: int
734 95ab4de9 David Knowles
    @return: job id
735 95ab4de9 David Knowles

736 95ab4de9 David Knowles
    @raise InvalidStorageType: If an invalid storage type is specified
737 95ab4de9 David Knowles

738 95ab4de9 David Knowles
    """
739 95ab4de9 David Knowles
    if storage_type not in VALID_STORAGE_TYPES:
740 95ab4de9 David Knowles
      raise InvalidStorageType("%s is an invalid storage type.", storage_type)
741 95ab4de9 David Knowles
742 95ab4de9 David Knowles
    query = [("storage_type", storage_type), ("name", name)]
743 95ab4de9 David Knowles
    return self._SendRequest(HTTP_PUT, "/nodes/%s/storage/repair" % node, query)
744 95ab4de9 David Knowles
745 95ab4de9 David Knowles
  def GetNodeTags(self, node):
746 95ab4de9 David Knowles
    """Gets the tags for a node.
747 95ab4de9 David Knowles

748 95ab4de9 David Knowles
    @type node: str
749 95ab4de9 David Knowles
    @param node: node whose tags to return
750 95ab4de9 David Knowles

751 95ab4de9 David Knowles
    @rtype: list of str
752 95ab4de9 David Knowles
    @return: tags for the node
753 95ab4de9 David Knowles

754 95ab4de9 David Knowles
    """
755 95ab4de9 David Knowles
    return self._SendRequest(HTTP_GET, "/nodes/%s/tags" % node)
756 95ab4de9 David Knowles
757 95ab4de9 David Knowles
  def AddNodeTags(self, node, tags, dry_run=False):
758 95ab4de9 David Knowles
    """Adds tags to a node.
759 95ab4de9 David Knowles

760 95ab4de9 David Knowles
    @type node: str
761 95ab4de9 David Knowles
    @param node: node to add tags to
762 95ab4de9 David Knowles
    @type tags: list of str
763 95ab4de9 David Knowles
    @param tags: tags to add to the node
764 95ab4de9 David Knowles
    @type dry_run: bool
765 95ab4de9 David Knowles
    @param dry_run: whether to perform a dry run
766 95ab4de9 David Knowles

767 95ab4de9 David Knowles
    @rtype: int
768 95ab4de9 David Knowles
    @return: job id
769 95ab4de9 David Knowles

770 95ab4de9 David Knowles
    """
771 95ab4de9 David Knowles
    query = [("tag", t) for t in tags]
772 95ab4de9 David Knowles
    if dry_run:
773 95ab4de9 David Knowles
      query.append(("dry-run", 1))
774 95ab4de9 David Knowles
775 95ab4de9 David Knowles
    return self._SendRequest(HTTP_PUT, "/nodes/%s/tags" % node, query,
776 95ab4de9 David Knowles
                             content=tags)
777 95ab4de9 David Knowles
778 95ab4de9 David Knowles
  def DeleteNodeTags(self, node, tags, dry_run=False):
779 95ab4de9 David Knowles
    """Delete tags from a node.
780 95ab4de9 David Knowles

781 95ab4de9 David Knowles
    @type node: str
782 95ab4de9 David Knowles
    @param node: node to remove tags from
783 95ab4de9 David Knowles
    @type tags: list of str
784 95ab4de9 David Knowles
    @param tags: tags to remove from the node
785 95ab4de9 David Knowles
    @type dry_run: bool
786 95ab4de9 David Knowles
    @param dry_run: whether to perform a dry run
787 95ab4de9 David Knowles

788 95ab4de9 David Knowles
    @rtype: int
789 95ab4de9 David Knowles
    @return: job id
790 95ab4de9 David Knowles

791 95ab4de9 David Knowles
    """
792 95ab4de9 David Knowles
    query = [("tag", t) for t in tags]
793 95ab4de9 David Knowles
    if dry_run:
794 95ab4de9 David Knowles
      query.append(("dry-run", 1))
795 95ab4de9 David Knowles
796 95ab4de9 David Knowles
    return self._SendRequest(HTTP_DELETE, "/nodes/%s/tags" % node, query)
797 95ab4de9 David Knowles
798 95ab4de9 David Knowles
799 95ab4de9 David Knowles
class HTTPSConnectionOpenSSL(httplib.HTTPSConnection):
800 95ab4de9 David Knowles
  """HTTPS Connection handler that verifies the SSL certificate.
801 95ab4de9 David Knowles

802 95ab4de9 David Knowles
  """
803 95ab4de9 David Knowles
804 95ab4de9 David Knowles
  # pylint: disable-msg=W0142
805 95ab4de9 David Knowles
  def __init__(self, *args, **kwargs):
806 95ab4de9 David Knowles
    """Constructor.
807 95ab4de9 David Knowles

808 95ab4de9 David Knowles
    """
809 95ab4de9 David Knowles
    httplib.HTTPSConnection.__init__(self, *args, **kwargs)
810 95ab4de9 David Knowles
811 95ab4de9 David Knowles
    self._ssl_cert = None
812 95ab4de9 David Knowles
    if self.cert_file:
813 95ab4de9 David Knowles
      f = open(self.cert_file, "r")
814 95ab4de9 David Knowles
      self._ssl_cert = crypto.load_certificate(crypto.FILETYPE_PEM, f.read())
815 95ab4de9 David Knowles
      f.close()
816 95ab4de9 David Knowles
817 95ab4de9 David Knowles
  # pylint: disable-msg=W0613
818 95ab4de9 David Knowles
  def _VerifySSLCertCallback(self, conn, cert, errnum, errdepth, ok):
819 95ab4de9 David Knowles
    """Verifies the SSL certificate provided by the peer.
820 95ab4de9 David Knowles

821 95ab4de9 David Knowles
    """
822 95ab4de9 David Knowles
    return (self._ssl_cert.digest("sha1") == cert.digest("sha1") and
823 95ab4de9 David Knowles
            self._ssl_cert.digest("md5") == cert.digest("md5"))
824 95ab4de9 David Knowles
825 95ab4de9 David Knowles
  def connect(self):
826 95ab4de9 David Knowles
    """Connect to the server specified when the object was created.
827 95ab4de9 David Knowles

828 95ab4de9 David Knowles
    This ensures that SSL certificates are verified.
829 95ab4de9 David Knowles

830 95ab4de9 David Knowles
    """
831 95ab4de9 David Knowles
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
832 95ab4de9 David Knowles
    ctx = SSL.Context(SSL.SSLv23_METHOD)
833 95ab4de9 David Knowles
    ctx.set_options(SSL.OP_NO_SSLv2)
834 95ab4de9 David Knowles
    ctx.use_certificate(self._ssl_cert)
835 95ab4de9 David Knowles
    ctx.set_verify(SSL.VERIFY_PEER | SSL.VERIFY_FAIL_IF_NO_PEER_CERT,
836 95ab4de9 David Knowles
                   self._VerifySSLCertCallback)
837 95ab4de9 David Knowles
838 95ab4de9 David Knowles
    ssl = SSL.Connection(ctx, sock)
839 95ab4de9 David Knowles
    ssl.connect((self.host, self.port))
840 95ab4de9 David Knowles
    self.sock = httplib.FakeSocket(sock, ssl)
841 95ab4de9 David Knowles
842 95ab4de9 David Knowles
843 95ab4de9 David Knowles
def _VerifyCertificate(hostname, port, cert_file):
844 95ab4de9 David Knowles
  """Verifies the SSL certificate for the given host/port.
845 95ab4de9 David Knowles

846 95ab4de9 David Knowles
  @type hostname: str
847 95ab4de9 David Knowles
  @param hostname: the ganeti cluster master whose certificate to verify
848 95ab4de9 David Knowles
  @type port: int
849 95ab4de9 David Knowles
  @param port: the port on which the RAPI is running
850 95ab4de9 David Knowles
  @type cert_file: str
851 95ab4de9 David Knowles
  @param cert_file: filename of the expected SSL certificate
852 95ab4de9 David Knowles

853 95ab4de9 David Knowles
  @raises CertificateError: If an invalid SSL certificate is found
854 95ab4de9 David Knowles

855 95ab4de9 David Knowles
  """
856 95ab4de9 David Knowles
  https = HTTPSConnectionOpenSSL(hostname, port, cert_file=cert_file)
857 95ab4de9 David Knowles
  try:
858 95ab4de9 David Knowles
    try:
859 95ab4de9 David Knowles
      https.request(HTTP_GET, "/version")
860 95ab4de9 David Knowles
    except (crypto.Error, SSL.Error):
861 95ab4de9 David Knowles
      raise CertificateError("Invalid SSL certificate.")
862 95ab4de9 David Knowles
  finally:
863 95ab4de9 David Knowles
    https.close()