qa_rapi.py 13.8 KB
Newer Older
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
1 2
#

Iustin Pop's avatar
Iustin Pop committed
3
# Copyright (C) 2007, 2008, 2009, 2010, 2011 Google Inc.
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301, USA.


"""Remote API QA tests.

"""

Michael Hanselmann's avatar
Michael Hanselmann committed
25
import tempfile
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
26 27 28 29

from ganeti import utils
from ganeti import constants
from ganeti import errors
Michael Hanselmann's avatar
Michael Hanselmann committed
30 31 32
from ganeti import cli
from ganeti import rapi

Iustin Pop's avatar
Iustin Pop committed
33
import ganeti.rapi.client        # pylint: disable-msg=W0611
Michael Hanselmann's avatar
Michael Hanselmann committed
34
import ganeti.rapi.client_utils
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
35 36 37 38 39

import qa_config
import qa_utils
import qa_error

Iustin Pop's avatar
Iustin Pop committed
40
from qa_utils import (AssertEqual, AssertIn, AssertMatch, StartLocalCommand)
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
41 42


Michael Hanselmann's avatar
Michael Hanselmann committed
43 44
_rapi_ca = None
_rapi_client = None
45 46
_rapi_username = None
_rapi_password = None
47 48


Michael Hanselmann's avatar
Michael Hanselmann committed
49 50
def Setup(username, password):
  """Configures the RAPI client.
51

Michael Hanselmann's avatar
Michael Hanselmann committed
52
  """
Iustin Pop's avatar
Iustin Pop committed
53 54
  # pylint: disable-msg=W0603
  # due to global usage
Michael Hanselmann's avatar
Michael Hanselmann committed
55 56
  global _rapi_ca
  global _rapi_client
57 58 59 60 61
  global _rapi_username
  global _rapi_password

  _rapi_username = username
  _rapi_password = password
62

Michael Hanselmann's avatar
Michael Hanselmann committed
63
  master = qa_config.GetMasterNode()
64

Michael Hanselmann's avatar
Michael Hanselmann committed
65 66
  # Load RAPI certificate from master node
  cmd = ["cat", constants.RAPI_CERT_FILE]
67

Michael Hanselmann's avatar
Michael Hanselmann committed
68 69 70 71 72
  # Write to temporary file
  _rapi_ca = tempfile.NamedTemporaryFile()
  _rapi_ca.write(qa_utils.GetCommandOutput(master["primary"],
                                           utils.ShellQuoteArgs(cmd)))
  _rapi_ca.flush()
73

Michael Hanselmann's avatar
Michael Hanselmann committed
74
  port = qa_config.get("rapi-port", default=constants.DEFAULT_RAPI_PORT)
75 76
  cfg_curl = rapi.client.GenericCurlConfig(cafile=_rapi_ca.name,
                                           proxy="")
77

Michael Hanselmann's avatar
Michael Hanselmann committed
78 79 80
  _rapi_client = rapi.client.GanetiRapiClient(master["primary"], port=port,
                                              username=username,
                                              password=password,
81
                                              curl_config_fn=cfg_curl)
82

Michael Hanselmann's avatar
Michael Hanselmann committed
83
  print "RAPI protocol version: %s" % _rapi_client.GetVersion()
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
84 85 86


INSTANCE_FIELDS = ("name", "os", "pnode", "snodes",
87 88
                   "admin_state",
                   "disk_template", "disk.sizes",
89
                   "nic.ips", "nic.macs", "nic.modes", "nic.links",
90
                   "beparams", "hvparams",
91
                   "oper_state", "oper_ram", "oper_vcpus", "status", "tags")
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
92 93 94 95 96

NODE_FIELDS = ("name", "dtotal", "dfree",
               "mtotal", "mnode", "mfree",
               "pinst_cnt", "sinst_cnt", "tags")

97 98
GROUP_FIELDS = frozenset([
  "name", "uuid",
99
  "alloc_policy",
100 101 102
  "node_cnt", "node_list",
  ])

103 104 105 106 107 108
JOB_FIELDS = frozenset([
  "id", "ops", "status", "summary",
  "opstatus", "opresult", "oplog",
  "received_ts", "start_ts", "end_ts",
  ])

Iustin Pop's avatar
Iustin Pop committed
109
LIST_FIELDS = ("id", "uri")
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
110 111 112 113 114 115


def Enabled():
  """Return whether remote API tests should be run.

  """
116
  return qa_config.TestEnabled('rapi')
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
117 118 119


def _DoTests(uris):
Iustin Pop's avatar
Iustin Pop committed
120 121
  # pylint: disable-msg=W0212
  # due to _SendRequest usage
122
  results = []
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
123

124
  for uri, verify, method, body in uris:
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
125 126
    assert uri.startswith("/")

127
    print "%s %s" % (method, uri)
Michael Hanselmann's avatar
Michael Hanselmann committed
128
    data = _rapi_client._SendRequest(method, uri, None, body)
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
129 130 131 132 133 134 135

    if verify is not None:
      if callable(verify):
        verify(data)
      else:
        AssertEqual(data, verify)

136
    results.append(data)
137 138 139 140 141 142 143

  return results


def _VerifyReturnsJob(data):
  AssertMatch(data, r'^\d+$')

Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
144 145 146 147 148 149

def TestVersion():
  """Testing remote API version.

  """
  _DoTests([
150
    ("/version", constants.RAPI_VERSION, 'GET', None),
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
151 152 153 154 155 156 157
    ])


def TestEmptyCluster():
  """Testing remote API on an empty cluster.

  """
158 159
  master = qa_config.GetMasterNode()
  master_full = qa_utils.ResolveNodeName(master)
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
160 161 162 163

  def _VerifyInfo(data):
    AssertIn("name", data)
    AssertIn("master", data)
164
    AssertEqual(data["master"], master_full)
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
165 166 167

  def _VerifyNodes(data):
    master_entry = {
168 169
      "id": master_full,
      "uri": "/2/nodes/%s" % master_full,
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
170 171 172 173 174 175 176 177
      }
    AssertIn(master_entry, data)

  def _VerifyNodesBulk(data):
    for node in data:
      for entry in NODE_FIELDS:
        AssertIn(entry, node)

178 179
  def _VerifyGroups(data):
    default_group = {
180 181
      "name": constants.INITIAL_NODE_GROUP_NAME,
      "uri": "/2/groups/" + constants.INITIAL_NODE_GROUP_NAME,
182 183 184 185 186 187 188 189
      }
    AssertIn(default_group, data)

  def _VerifyGroupsBulk(data):
    for group in data:
      for field in GROUP_FIELDS:
        AssertIn(field, group)

Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
190
  _DoTests([
191 192 193 194 195
    ("/", None, 'GET', None),
    ("/2/info", _VerifyInfo, 'GET', None),
    ("/2/tags", None, 'GET', None),
    ("/2/nodes", _VerifyNodes, 'GET', None),
    ("/2/nodes?bulk=1", _VerifyNodesBulk, 'GET', None),
196 197
    ("/2/groups", _VerifyGroups, 'GET', None),
    ("/2/groups?bulk=1", _VerifyGroupsBulk, 'GET', None),
198 199 200
    ("/2/instances", [], 'GET', None),
    ("/2/instances?bulk=1", [], 'GET', None),
    ("/2/os", None, 'GET', None),
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
201 202
    ])

203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220
  # Test HTTP Not Found
  for method in ["GET", "PUT", "POST", "DELETE"]:
    try:
      _DoTests([("/99/resource/not/here/99", None, method, None)])
    except rapi.client.GanetiApiError, err:
      AssertEqual(err.code, 404)
    else:
      raise qa_error.Error("Non-existent resource didn't return HTTP 404")

  # Test HTTP Not Implemented
  for method in ["PUT", "POST", "DELETE"]:
    try:
      _DoTests([("/version", None, method, None)])
    except rapi.client.GanetiApiError, err:
      AssertEqual(err.code, 501)
    else:
      raise qa_error.Error("Non-implemented method didn't fail")

Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
221 222 223 224 225 226 227 228

def TestInstance(instance):
  """Testing getting instance(s) info via remote API.

  """
  def _VerifyInstance(data):
    for entry in INSTANCE_FIELDS:
      AssertIn(entry, data)
229

Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
230 231
  def _VerifyInstancesList(data):
    for instance in data:
232
      for entry in LIST_FIELDS:
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
233
        AssertIn(entry, instance)
234

Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
235 236 237 238 239
  def _VerifyInstancesBulk(data):
    for instance_data in data:
      _VerifyInstance(instance_data)

  _DoTests([
240 241 242 243 244 245 246
    ("/2/instances/%s" % instance["name"], _VerifyInstance, 'GET', None),
    ("/2/instances", _VerifyInstancesList, 'GET', None),
    ("/2/instances?bulk=1", _VerifyInstancesBulk, 'GET', None),
    ("/2/instances/%s/activate-disks" % instance["name"],
     _VerifyReturnsJob, 'PUT', None),
    ("/2/instances/%s/deactivate-disks" % instance["name"],
     _VerifyReturnsJob, 'PUT', None),
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
247 248
    ])

249 250 251 252 253 254 255 256 257 258 259 260 261
  # Test OpPrepareExport
  (job_id, ) = _DoTests([
    ("/2/instances/%s/prepare-export?mode=%s" %
     (instance["name"], constants.EXPORT_MODE_REMOTE),
     _VerifyReturnsJob, "PUT", None),
    ])

  result = _WaitForRapiJob(job_id)[0]
  AssertEqual(len(result["handshake"]), 3)
  AssertEqual(result["handshake"][0], constants.RIE_VERSION)
  AssertEqual(len(result["x509_key_name"]), 3)
  AssertIn("-----BEGIN CERTIFICATE-----", result["x509_ca"])

Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
262 263 264 265 266 267 268 269

def TestNode(node):
  """Testing getting node(s) info via remote API.

  """
  def _VerifyNode(data):
    for entry in NODE_FIELDS:
      AssertIn(entry, data)
270

Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
271 272
  def _VerifyNodesList(data):
    for node in data:
273
      for entry in LIST_FIELDS:
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
274
        AssertIn(entry, node)
275

Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
276 277 278 279 280
  def _VerifyNodesBulk(data):
    for node_data in data:
      _VerifyNode(node_data)

  _DoTests([
281 282 283
    ("/2/nodes/%s" % node["primary"], _VerifyNode, 'GET', None),
    ("/2/nodes", _VerifyNodesList, 'GET', None),
    ("/2/nodes?bulk=1", _VerifyNodesBulk, 'GET', None),
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
284 285 286 287 288 289 290 291
    ])


def TestTags(kind, name, tags):
  """Tests .../tags resources.

  """
  if kind == constants.TAG_CLUSTER:
292
    uri = "/2/tags"
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
293
  elif kind == constants.TAG_NODE:
294
    uri = "/2/nodes/%s/tags" % name
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
295
  elif kind == constants.TAG_INSTANCE:
296
    uri = "/2/instances/%s/tags" % name
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
297 298 299 300
  else:
    raise errors.ProgrammerError("Unknown tag kind")

  def _VerifyTags(data):
301
    AssertEqual(sorted(tags), sorted(data))
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
302

303 304 305 306 307 308 309 310 311
  query = "&".join("tag=%s" % i for i in tags)

  # Add tags
  (job_id, ) = _DoTests([
    ("%s?%s" % (uri, query), _VerifyReturnsJob, "PUT", None),
    ])
  _WaitForRapiJob(job_id)

  # Retrieve tags
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
312
  _DoTests([
313
    (uri, _VerifyTags, 'GET', None),
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
314
    ])
315

316 317 318 319 320 321
  # Remove tags
  (job_id, ) = _DoTests([
    ("%s?%s" % (uri, query), _VerifyReturnsJob, "DELETE", None),
    ])
  _WaitForRapiJob(job_id)

322 323 324 325 326 327 328 329 330 331 332 333 334 335

def _WaitForRapiJob(job_id):
  """Waits for a job to finish.

  """
  def _VerifyJob(data):
    AssertEqual(data["id"], job_id)
    for field in JOB_FIELDS:
      AssertIn(field, data)

  _DoTests([
    ("/2/jobs/%s" % job_id, _VerifyJob, "GET", None),
    ])

336 337
  return rapi.client_utils.PollJob(_rapi_client, job_id,
                                   cli.StdioJobPollReportCb())
338 339


340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401
def TestRapiNodeGroups():
  """Test several node group operations using RAPI.

  """
  groups = qa_config.get("groups", {})
  group1, group2, group3 = groups.get("inexistent-groups",
                                      ["group1", "group2", "group3"])[:3]

  # Create a group with no attributes
  body = {
    "name": group1,
    }

  (job_id, ) = _DoTests([
    ("/2/groups", _VerifyReturnsJob, "POST", body),
    ])

  _WaitForRapiJob(job_id)

  # Create a group specifying alloc_policy
  body = {
    "name": group2,
    "alloc_policy": constants.ALLOC_POLICY_UNALLOCABLE,
    }

  (job_id, ) = _DoTests([
    ("/2/groups", _VerifyReturnsJob, "POST", body),
    ])

  _WaitForRapiJob(job_id)

  # Modify alloc_policy
  body = {
    "alloc_policy": constants.ALLOC_POLICY_UNALLOCABLE,
    }

  (job_id, ) = _DoTests([
    ("/2/groups/%s/modify" % group1, _VerifyReturnsJob, "PUT", body),
    ])

  _WaitForRapiJob(job_id)

  # Rename a group
  body = {
    "new_name": group3,
    }

  (job_id, ) = _DoTests([
    ("/2/groups/%s/rename" % group2, _VerifyReturnsJob, "PUT", body),
    ])

  _WaitForRapiJob(job_id)

  # Delete groups
  for group in [group1, group3]:
    (job_id, ) = _DoTests([
      ("/2/groups/%s" % group, _VerifyReturnsJob, "DELETE", None),
      ])

    _WaitForRapiJob(job_id)


402
def TestRapiInstanceAdd(node, use_client):
403 404 405
  """Test adding a new instance via RAPI"""
  instance = qa_config.AcquireInstance()
  try:
406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436
    memory = utils.ParseUnit(qa_config.get("mem"))
    disk_sizes = [utils.ParseUnit(size) for size in qa_config.get("disk")]

    if use_client:
      disks = [{"size": size} for size in disk_sizes]
      nics = [{}]

      beparams = {
        constants.BE_MEMORY: memory,
        }

      job_id = _rapi_client.CreateInstance(constants.INSTANCE_CREATE,
                                           instance["name"],
                                           constants.DT_PLAIN,
                                           disks, nics,
                                           os=qa_config.get("os"),
                                           pnode=node["primary"],
                                           beparams=beparams)
    else:
      body = {
        "name": instance["name"],
        "os": qa_config.get("os"),
        "disk_template": constants.DT_PLAIN,
        "pnode": node["primary"],
        "memory": memory,
        "disks": disk_sizes,
        }

      (job_id, ) = _DoTests([
        ("/2/instances", _VerifyReturnsJob, "POST", body),
        ])
437 438 439 440 441 442 443 444 445

    _WaitForRapiJob(job_id)

    return instance
  except:
    qa_config.ReleaseInstance(instance)
    raise


446
def TestRapiInstanceRemove(instance, use_client):
447
  """Test removing instance via RAPI"""
448 449 450 451 452 453
  if use_client:
    job_id = _rapi_client.DeleteInstance(instance["name"])
  else:
    (job_id, ) = _DoTests([
      ("/2/instances/%s" % instance["name"], _VerifyReturnsJob, "DELETE", None),
      ])
454 455 456 457

  _WaitForRapiJob(job_id)

  qa_config.ReleaseInstance(instance)
458 459


460 461 462 463 464 465 466 467
def TestRapiInstanceMigrate(instance):
  """Test migrating instance via RAPI"""
  # Move to secondary node
  _WaitForRapiJob(_rapi_client.MigrateInstance(instance["name"]))
  # And back to previous primary
  _WaitForRapiJob(_rapi_client.MigrateInstance(instance["name"]))


468
def TestRapiInstanceRename(rename_source, rename_target):
469
  """Test renaming instance via RAPI"""
470
  _WaitForRapiJob(_rapi_client.RenameInstance(rename_source, rename_target))
471 472


473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494
def TestRapiInstanceModify(instance):
  """Test modifying instance via RAPI"""
  def _ModifyInstance(**kwargs):
    _WaitForRapiJob(_rapi_client.ModifyInstance(instance["name"], **kwargs))

  _ModifyInstance(hvparams={
    constants.HV_KERNEL_ARGS: "single",
    })

  _ModifyInstance(beparams={
    constants.BE_VCPUS: 3,
    })

  _ModifyInstance(beparams={
    constants.BE_VCPUS: constants.VALUE_DEFAULT,
    })

  _ModifyInstance(hvparams={
    constants.HV_KERNEL_ARGS: constants.VALUE_DEFAULT,
    })


495 496
def TestInterClusterInstanceMove(src_instance, dest_instance,
                                 pnode, snode, tnode):
497 498 499 500 501 502 503 504
  """Test tools/move-instance"""
  master = qa_config.GetMasterNode()

  rapi_pw_file = tempfile.NamedTemporaryFile()
  rapi_pw_file.write(_rapi_password)
  rapi_pw_file.flush()

  # TODO: Run some instance tests before moving back
Iustin Pop's avatar
Iustin Pop committed
505

506 507 508 509 510 511 512 513
  if snode is None:
    # instance is not redundant, but we still need to pass a node
    # (which will be ignored)
    fsec = tnode
  else:
    fsec = snode
  # note: pnode:snode are the *current* nodes, so we move it first to
  # tnode:pnode, then back to pnode:snode
Iustin Pop's avatar
Iustin Pop committed
514
  for si, di, pn, sn in [(src_instance["name"], dest_instance["name"],
515
                          tnode["primary"], pnode["primary"]),
Iustin Pop's avatar
Iustin Pop committed
516
                         (dest_instance["name"], src_instance["name"],
517
                          pnode["primary"], fsec["primary"])]:
518 519 520 521 522 523
    cmd = [
      "../tools/move-instance",
      "--verbose",
      "--src-ca-file=%s" % _rapi_ca.name,
      "--src-username=%s" % _rapi_username,
      "--src-password-file=%s" % rapi_pw_file.name,
Iustin Pop's avatar
Iustin Pop committed
524 525 526
      "--dest-instance-name=%s" % di,
      "--dest-primary-node=%s" % pn,
      "--dest-secondary-node=%s" % sn,
527
      "--net=0:mac=%s" % constants.VALUE_GENERATE,
528 529
      master["primary"],
      master["primary"],
Iustin Pop's avatar
Iustin Pop committed
530
      si,
531 532 533
      ]

    AssertEqual(StartLocalCommand(cmd).wait(), 0)