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

4
# Copyright (C) 2007, 2008, 2009, 2010, 2011, 2012, 2013 Google Inc.
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#
# 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.

"""

26
import itertools
27
import functools
Hrvoje Ribicic's avatar
Hrvoje Ribicic committed
28
29
30
import random
import re
import tempfile
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
31

Hrvoje Ribicic's avatar
Hrvoje Ribicic committed
32
33
from ganeti import cli
from ganeti import compat
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
34
35
from ganeti import constants
from ganeti import errors
Hrvoje Ribicic's avatar
Hrvoje Ribicic committed
36
37
from ganeti import locking
from ganeti import pathutils
38
from ganeti import objects
39
from ganeti import qlang
Hrvoje Ribicic's avatar
Hrvoje Ribicic committed
40
41
42
from ganeti import query
from ganeti import rapi
from ganeti import utils
Michael Hanselmann's avatar
Michael Hanselmann committed
43

44
from ganeti.http.auth import ParsePasswordFile
45
import ganeti.rapi.client        # pylint: disable=W0611
Michael Hanselmann's avatar
Michael Hanselmann committed
46
import ganeti.rapi.client_utils
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
47
48
49

import qa_config
import qa_error
Hrvoje Ribicic's avatar
Hrvoje Ribicic committed
50
import qa_utils
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
51

Hrvoje Ribicic's avatar
Hrvoje Ribicic committed
52
from qa_instance import IsDiskReplacingSupported
53
54
from qa_instance import IsFailoverSupported
from qa_instance import IsMigrationSupported
55
from qa_job_utils import RunWithLocks
Iustin Pop's avatar
Iustin Pop committed
56
from qa_utils import (AssertEqual, AssertIn, AssertMatch, StartLocalCommand)
57
from qa_utils import InstanceCheck, INST_DOWN, INST_UP, FIRST_ARG
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
58
59


Michael Hanselmann's avatar
Michael Hanselmann committed
60
61
_rapi_ca = None
_rapi_client = None
62
63
_rapi_username = None
_rapi_password = None
64
65


Michael Hanselmann's avatar
Michael Hanselmann committed
66
67
def Setup(username, password):
  """Configures the RAPI client.
68

Michael Hanselmann's avatar
Michael Hanselmann committed
69
  """
70
  # pylint: disable=W0603
Iustin Pop's avatar
Iustin Pop committed
71
  # due to global usage
Michael Hanselmann's avatar
Michael Hanselmann committed
72
73
  global _rapi_ca
  global _rapi_client
74
75
76
77
78
  global _rapi_username
  global _rapi_password

  _rapi_username = username
  _rapi_password = password
79

Michael Hanselmann's avatar
Michael Hanselmann committed
80
  master = qa_config.GetMasterNode()
81

Michael Hanselmann's avatar
Michael Hanselmann committed
82
  # Load RAPI certificate from master node
83
  cmd = ["cat", qa_utils.MakeNodePath(master, pathutils.RAPI_CERT_FILE)]
84

Michael Hanselmann's avatar
Michael Hanselmann committed
85
86
  # Write to temporary file
  _rapi_ca = tempfile.NamedTemporaryFile()
87
  _rapi_ca.write(qa_utils.GetCommandOutput(master.primary,
Michael Hanselmann's avatar
Michael Hanselmann committed
88
89
                                           utils.ShellQuoteArgs(cmd)))
  _rapi_ca.flush()
90

Michael Hanselmann's avatar
Michael Hanselmann committed
91
  port = qa_config.get("rapi-port", default=constants.DEFAULT_RAPI_PORT)
92
93
  cfg_curl = rapi.client.GenericCurlConfig(cafile=_rapi_ca.name,
                                           proxy="")
94

95
96
97
98
  if qa_config.UseVirtualCluster():
    # TODO: Implement full support for RAPI on virtual clusters
    print qa_utils.FormatWarning("RAPI tests are not yet supported on"
                                 " virtual clusters and will be disabled")
99

100
101
102
103
104
105
106
107
    assert _rapi_client is None
  else:
    _rapi_client = rapi.client.GanetiRapiClient(master.primary, port=port,
                                                username=username,
                                                password=password,
                                                curl_config_fn=cfg_curl)

    print "RAPI protocol version: %s" % _rapi_client.GetVersion()
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
108

109
110
  return _rapi_client

Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
111

112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
def LookupRapiSecret(rapi_user):
  """Find the RAPI secret for the given user.

  @param rapi_user: Login user
  @return: Login secret for the user

  """
  CTEXT = "{CLEARTEXT}"
  master = qa_config.GetMasterNode()
  cmd = ["cat", qa_utils.MakeNodePath(master, pathutils.RAPI_USERS_FILE)]
  file_content = qa_utils.GetCommandOutput(master.primary,
                                           utils.ShellQuoteArgs(cmd))
  users = ParsePasswordFile(file_content)
  entry = users.get(rapi_user)
  if not entry:
    raise qa_error.Error("User %s not found in RAPI users file" % rapi_user)
  secret = entry.password
  if secret.upper().startswith(CTEXT):
    secret = secret[len(CTEXT):]
  elif secret.startswith("{"):
    raise qa_error.Error("Unsupported password schema for RAPI user %s:"
                         " not a clear text password" % rapi_user)
  return secret


Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
137
INSTANCE_FIELDS = ("name", "os", "pnode", "snodes",
138
                   "admin_state",
139
                   "disk_template", "disk.sizes", "disk.spindles",
140
                   "nic.ips", "nic.macs", "nic.modes", "nic.links",
141
                   "beparams", "hvparams",
142
                   "oper_state", "oper_ram", "oper_vcpus", "status", "tags")
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
143

Bernardo Dal Seno's avatar
Bernardo Dal Seno committed
144
NODE_FIELDS = ("name", "dtotal", "dfree", "sptotal", "spfree",
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
145
146
147
               "mtotal", "mnode", "mfree",
               "pinst_cnt", "sinst_cnt", "tags")

148
GROUP_FIELDS = compat.UniqueFrozenset([
149
  "name", "uuid",
150
  "alloc_policy",
151
152
153
  "node_cnt", "node_list",
  ])

154
JOB_FIELDS = compat.UniqueFrozenset([
155
156
157
158
159
  "id", "ops", "status", "summary",
  "opstatus", "opresult", "oplog",
  "received_ts", "start_ts", "end_ts",
  ])

Iustin Pop's avatar
Iustin Pop committed
160
LIST_FIELDS = ("id", "uri")
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
161
162
163
164
165
166


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

  """
167
168
169
  # TODO: Implement RAPI tests for virtual clusters
  return (qa_config.TestEnabled("rapi") and
          not qa_config.UseVirtualCluster())
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
170
171
172


def _DoTests(uris):
173
  # pylint: disable=W0212
Iustin Pop's avatar
Iustin Pop committed
174
  # due to _SendRequest usage
175
  results = []
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
176

177
  for uri, verify, method, body in uris:
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
178
179
    assert uri.startswith("/")

180
    print "%s %s" % (method, uri)
Michael Hanselmann's avatar
Michael Hanselmann committed
181
    data = _rapi_client._SendRequest(method, uri, None, body)
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
182
183
184
185
186
187
188

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

189
    results.append(data)
190
191
192
193
194

  return results


def _VerifyReturnsJob(data):
Iustin Pop's avatar
Iustin Pop committed
195
196
  if not isinstance(data, int):
    AssertMatch(data, r"^\d+$")
197

Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
198
199
200
201
202
203

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

  """
  _DoTests([
Iustin Pop's avatar
Iustin Pop committed
204
    ("/version", constants.RAPI_VERSION, "GET", None),
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
205
206
207
208
209
210
211
    ])


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

  """
212
213
  master = qa_config.GetMasterNode()
  master_full = qa_utils.ResolveNodeName(master)
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
214
215
216
217

  def _VerifyInfo(data):
    AssertIn("name", data)
    AssertIn("master", data)
218
    AssertEqual(data["master"], master_full)
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
219
220
221

  def _VerifyNodes(data):
    master_entry = {
222
223
      "id": master_full,
      "uri": "/2/nodes/%s" % master_full,
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
224
225
226
227
228
229
230
231
      }
    AssertIn(master_entry, data)

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

232
233
  def _VerifyGroups(data):
    default_group = {
234
235
      "name": constants.INITIAL_NODE_GROUP_NAME,
      "uri": "/2/groups/" + constants.INITIAL_NODE_GROUP_NAME,
236
237
238
239
240
241
242
243
      }
    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
244
  _DoTests([
Iustin Pop's avatar
Iustin Pop committed
245
246
247
248
249
250
251
252
253
254
    ("/", 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),
    ("/2/groups", _VerifyGroups, "GET", None),
    ("/2/groups?bulk=1", _VerifyGroupsBulk, "GET", None),
    ("/2/instances", [], "GET", None),
    ("/2/instances?bulk=1", [], "GET", None),
    ("/2/os", None, "GET", None),
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
255
256
    ])

257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
  # 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
275

276
277
278
279
def TestRapiQuery():
  """Testing resource queries via remote API.

  """
280
281
282
283
284
  # FIXME: the tests are failing if no LVM is enabled, investigate
  # if it is a bug in the QA or in the code
  if not qa_config.IsStorageTypeSupported(constants.ST_LVM_VG):
    return

285
286
287
288
  master_name = qa_utils.ResolveNodeName(qa_config.GetMasterNode())
  rnd = random.Random(7818)

  for what in constants.QR_VIA_RAPI:
289
290
    if what == constants.QR_JOB:
      namefield = "id"
Klaus Aehlig's avatar
Klaus Aehlig committed
291
      trivial_filter = [qlang.OP_GE, namefield, 0]
292
293
    elif what == constants.QR_EXPORT:
      namefield = "export"
Klaus Aehlig's avatar
Klaus Aehlig committed
294
      trivial_filter = [qlang.OP_REGEXP, ".*", namefield]
295
296
    else:
      namefield = "name"
Klaus Aehlig's avatar
Klaus Aehlig committed
297
      trivial_filter = [qlang.OP_REGEXP, ".*", namefield]
298

299
300
301
302
303
304
305
306
307
    all_fields = query.ALL_FIELDS[what].keys()
    rnd.shuffle(all_fields)

    # No fields, should return everything
    result = _rapi_client.QueryFields(what)
    qresult = objects.QueryFieldsResponse.FromDict(result)
    AssertEqual(len(qresult.fields), len(all_fields))

    # One field
308
    result = _rapi_client.QueryFields(what, fields=[namefield])
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
    qresult = objects.QueryFieldsResponse.FromDict(result)
    AssertEqual(len(qresult.fields), 1)

    # Specify all fields, order must be correct
    result = _rapi_client.QueryFields(what, fields=all_fields)
    qresult = objects.QueryFieldsResponse.FromDict(result)
    AssertEqual(len(qresult.fields), len(all_fields))
    AssertEqual([fdef.name for fdef in qresult.fields], all_fields)

    # Unknown field
    result = _rapi_client.QueryFields(what, fields=["_unknown!"])
    qresult = objects.QueryFieldsResponse.FromDict(result)
    AssertEqual(len(qresult.fields), 1)
    AssertEqual(qresult.fields[0].name, "_unknown!")
    AssertEqual(qresult.fields[0].kind, constants.QFT_UNKNOWN)

    # Try once more, this time without the client
    _DoTests([
      ("/2/query/%s/fields" % what, None, "GET", None),
      ("/2/query/%s/fields?fields=name,name,%s" % (what, all_fields[0]),
       None, "GET", None),
      ])

    # Try missing query argument
    try:
      _DoTests([
        ("/2/query/%s" % what, None, "GET", None),
        ])
    except rapi.client.GanetiApiError, err:
      AssertEqual(err.code, 400)
    else:
      raise qa_error.Error("Request missing 'fields' parameter didn't fail")

    def _Check(exp_fields, data):
      qresult = objects.QueryResponse.FromDict(data)
      AssertEqual([fdef.name for fdef in qresult.fields], exp_fields)
      if not isinstance(qresult.data, list):
        raise qa_error.Error("Query did not return a list")

    _DoTests([
      # Specify fields in query
      ("/2/query/%s?fields=%s" % (what, ",".join(all_fields)),
       compat.partial(_Check, all_fields), "GET", None),

353
354
      ("/2/query/%s?fields=%s" % (what, namefield),
       compat.partial(_Check, [namefield]), "GET", None),
355
356

      # Note the spaces
357
358
      ("/2/query/%s?fields=%s,%%20%s%%09,%s%%20" %
       (what, namefield, namefield, namefield),
359
       compat.partial(_Check, [namefield] * 3), "GET", None)])
360

361
362
363
364
365
    if what in constants.QR_VIA_RAPI_PUT:
      _DoTests([
        # PUT with fields in query
        ("/2/query/%s?fields=%s" % (what, namefield),
         compat.partial(_Check, [namefield]), "PUT", {}),
366

367
368
369
        ("/2/query/%s" % what, compat.partial(_Check, [namefield] * 4), "PUT", {
           "fields": [namefield] * 4,
           }),
370

371
372
373
        ("/2/query/%s" % what, compat.partial(_Check, all_fields), "PUT", {
           "fields": all_fields,
           }),
374

375
376
        ("/2/query/%s" % what, compat.partial(_Check, [namefield] * 4), "PUT", {
           "fields": [namefield] * 4
377
         })])
378
379
380
381
382
383

    def _CheckFilter():
      _DoTests([
        # With filter
        ("/2/query/%s" % what, compat.partial(_Check, all_fields), "PUT", {
           "fields": all_fields,
Klaus Aehlig's avatar
Klaus Aehlig committed
384
           "filter": trivial_filter
385
386
387
388
389
390
391
392
393
394
395
396
           }),
        ])

    if what == constants.QR_LOCK:
      # Locks can't be filtered
      try:
        _CheckFilter()
      except rapi.client.GanetiApiError, err:
        AssertEqual(err.code, 500)
      else:
        raise qa_error.Error("Filtering locks didn't fail")
    else:
397
398
      if what in constants.QR_VIA_RAPI_PUT:
        _CheckFilter()
399
400
401

    if what == constants.QR_NODE:
      # Test with filter
Iustin Pop's avatar
Iustin Pop committed
402
403
404
405
406
407
      (nodes, ) = _DoTests(
        [("/2/query/%s" % what,
          compat.partial(_Check, ["name", "master"]), "PUT",
          {"fields": ["name", "master"],
           "filter": [qlang.OP_TRUE, "master"],
           })])
408
409
410
411
412
413
      qresult = objects.QueryResponse.FromDict(nodes)
      AssertEqual(qresult.data, [
        [[constants.RS_NORMAL, master_name], [constants.RS_NORMAL, True]],
        ])


414
@InstanceCheck(INST_UP, INST_UP, FIRST_ARG)
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
415
416
417
418
419
420
421
def TestInstance(instance):
  """Testing getting instance(s) info via remote API.

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

Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
423
424
  def _VerifyInstancesList(data):
    for instance in data:
425
      for entry in LIST_FIELDS:
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
426
        AssertIn(entry, instance)
427

Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
428
429
430
431
432
  def _VerifyInstancesBulk(data):
    for instance_data in data:
      _VerifyInstance(instance_data)

  _DoTests([
433
    ("/2/instances/%s" % instance.name, _VerifyInstance, "GET", None),
Iustin Pop's avatar
Iustin Pop committed
434
435
    ("/2/instances", _VerifyInstancesList, "GET", None),
    ("/2/instances?bulk=1", _VerifyInstancesBulk, "GET", None),
436
    ("/2/instances/%s/activate-disks" % instance.name,
Iustin Pop's avatar
Iustin Pop committed
437
     _VerifyReturnsJob, "PUT", None),
438
    ("/2/instances/%s/deactivate-disks" % instance.name,
Iustin Pop's avatar
Iustin Pop committed
439
     _VerifyReturnsJob, "PUT", None),
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
440
441
    ])

442
  # Test OpBackupPrepare
443
444
  (job_id, ) = _DoTests([
    ("/2/instances/%s/prepare-export?mode=%s" %
445
     (instance.name, constants.EXPORT_MODE_REMOTE),
446
447
448
449
450
451
452
453
454
     _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
455
456
457
458
459
460
461
462

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

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

Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
464
465
  def _VerifyNodesList(data):
    for node in data:
466
      for entry in LIST_FIELDS:
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
467
        AssertIn(entry, node)
468

Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
469
470
471
472
473
  def _VerifyNodesBulk(data):
    for node_data in data:
      _VerifyNode(node_data)

  _DoTests([
474
    ("/2/nodes/%s" % node.primary, _VerifyNode, "GET", None),
Iustin Pop's avatar
Iustin Pop committed
475
476
    ("/2/nodes", _VerifyNodesList, "GET", None),
    ("/2/nodes?bulk=1", _VerifyNodesBulk, "GET", None),
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
477
478
479
    ])


480
481
482
483
484
485
486
487
488
489
490
491
def _FilterTags(seq):
  """Removes unwanted tags from a sequence.

  """
  ignore_re = qa_config.get("ignore-tags-re", None)

  if ignore_re:
    return itertools.ifilterfalse(re.compile(ignore_re).match, seq)
  else:
    return seq


Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
492
493
494
495
496
def TestTags(kind, name, tags):
  """Tests .../tags resources.

  """
  if kind == constants.TAG_CLUSTER:
497
    uri = "/2/tags"
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
498
  elif kind == constants.TAG_NODE:
499
    uri = "/2/nodes/%s/tags" % name
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
500
  elif kind == constants.TAG_INSTANCE:
501
    uri = "/2/instances/%s/tags" % name
502
503
  elif kind == constants.TAG_NODEGROUP:
    uri = "/2/groups/%s/tags" % name
Hrvoje Ribicic's avatar
Hrvoje Ribicic committed
504
505
  elif kind == constants.TAG_NETWORK:
    uri = "/2/networks/%s/tags" % name
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
506
507
508
509
  else:
    raise errors.ProgrammerError("Unknown tag kind")

  def _VerifyTags(data):
510
    AssertEqual(sorted(tags), sorted(_FilterTags(data)))
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
511

512
  queryargs = "&".join("tag=%s" % i for i in tags)
513
514
515

  # Add tags
  (job_id, ) = _DoTests([
516
    ("%s?%s" % (uri, queryargs), _VerifyReturnsJob, "PUT", None),
517
518
519
520
    ])
  _WaitForRapiJob(job_id)

  # Retrieve tags
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
521
  _DoTests([
Iustin Pop's avatar
Iustin Pop committed
522
    (uri, _VerifyTags, "GET", None),
Oleksiy Mishchenko's avatar
Oleksiy Mishchenko committed
523
    ])
524

525
526
  # Remove tags
  (job_id, ) = _DoTests([
527
    ("%s?%s" % (uri, queryargs), _VerifyReturnsJob, "DELETE", None),
528
529
530
    ])
  _WaitForRapiJob(job_id)

531
532
533
534
535
536
537
538
539
540
541
542
543
544

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),
    ])

545
546
  return rapi.client_utils.PollJob(_rapi_client, job_id,
                                   cli.StdioJobPollReportCb())
547
548


549
550
551
552
def TestRapiNodeGroups():
  """Test several node group operations using RAPI.

  """
553
  (group1, group2, group3) = qa_utils.GetNonexistentGroups(3)
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608

  # 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)


609
def TestRapiInstanceAdd(node, use_client):
610
  """Test adding a new instance via RAPI"""
611
612
  if not qa_config.IsTemplateSupported(constants.DT_PLAIN):
    return
613
  instance = qa_config.AcquireInstance()
614
  instance.SetDiskTemplate(constants.DT_PLAIN)
615
  try:
616
617
618
    disks = [{"size": utils.ParseUnit(d.get("size")),
              "name": str(d.get("name"))}
             for d in qa_config.GetDiskOptions()]
619
    nic0_mac = instance.GetNicMacAddr(0, constants.VALUE_GENERATE)
620
621
622
    nics = [{
      constants.INIC_MAC: nic0_mac,
      }]
623

624
    beparams = {
625
626
      constants.BE_MAXMEM: utils.ParseUnit(qa_config.get(constants.BE_MAXMEM)),
      constants.BE_MINMEM: utils.ParseUnit(qa_config.get(constants.BE_MINMEM)),
627
      }
628

629
    if use_client:
630
      job_id = _rapi_client.CreateInstance(constants.INSTANCE_CREATE,
631
                                           instance.name,
632
633
634
                                           constants.DT_PLAIN,
                                           disks, nics,
                                           os=qa_config.get("os"),
635
                                           pnode=node.primary,
636
637
638
                                           beparams=beparams)
    else:
      body = {
639
640
        "__version__": 1,
        "mode": constants.INSTANCE_CREATE,
641
        "name": instance.name,
642
        "os_type": qa_config.get("os"),
643
        "disk_template": constants.DT_PLAIN,
644
        "pnode": node.primary,
645
646
647
        "beparams": beparams,
        "disks": disks,
        "nics": nics,
648
649
650
651
652
        }

      (job_id, ) = _DoTests([
        ("/2/instances", _VerifyReturnsJob, "POST", body),
        ])
653
654
655
656
657

    _WaitForRapiJob(job_id)

    return instance
  except:
658
    instance.Release()
659
660
661
    raise


662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
def _GenInstanceAllocationDict(node, instance):
  """Creates an instance allocation dict to be used with the RAPI"""
  instance.SetDiskTemplate(constants.DT_PLAIN)

  disks = [{"size": utils.ParseUnit(d.get("size")),
              "name": str(d.get("name"))}
             for d in qa_config.GetDiskOptions()]

  nic0_mac = instance.GetNicMacAddr(0, constants.VALUE_GENERATE)
  nics = [{
    constants.INIC_MAC: nic0_mac,
    }]

  beparams = {
    constants.BE_MAXMEM: utils.ParseUnit(qa_config.get(constants.BE_MAXMEM)),
    constants.BE_MINMEM: utils.ParseUnit(qa_config.get(constants.BE_MINMEM)),
    }

  return _rapi_client.InstanceAllocation(constants.INSTANCE_CREATE,
                                         instance.name,
                                         constants.DT_PLAIN,
                                         disks, nics,
                                         os=qa_config.get("os"),
                                         pnode=node.primary,
                                         beparams=beparams)


def TestRapiInstanceMultiAlloc(node):
  """Test adding two new instances via the RAPI instance-multi-alloc method"""
  if not qa_config.IsTemplateSupported(constants.DT_PLAIN):
    return

  JOBS_KEY = "jobs"

  instance_one = qa_config.AcquireInstance()
  instance_two = qa_config.AcquireInstance()
  instance_list = [instance_one, instance_two]
  try:
    rapi_dicts = map(functools.partial(_GenInstanceAllocationDict, node),
                     instance_list)

    job_id = _rapi_client.InstancesMultiAlloc(rapi_dicts)

    results, = _WaitForRapiJob(job_id)

    if JOBS_KEY not in results:
      raise qa_error.Error("RAPI instance-multi-alloc did not deliver "
                           "information about created jobs")

    if len(results[JOBS_KEY]) != len(instance_list):
      raise qa_error.Error("RAPI instance-multi-alloc failed to return the "
                           "desired number of jobs!")

    for success, job in results[JOBS_KEY]:
      if success:
        _WaitForRapiJob(job)
      else:
        raise qa_error.Error("Failed to create instance in "
                             "instance-multi-alloc call")
  except:
    # Note that although released, it may be that some of the instance creations
    # have in fact succeeded. Handling this in a better way may be possible, but
    # is not necessary as the QA has already failed at this point.
    for instance in instance_list:
      instance.Release()
    raise

  return (instance_one, instance_two)


732
@InstanceCheck(None, INST_DOWN, FIRST_ARG)
733
def TestRapiInstanceRemove(instance, use_client):
734
  """Test removing instance via RAPI"""
735
736
737
738
739
  # FIXME: this does not work if LVM is not enabled. Find out if this is a bug
  # in RAPI or in the test
  if not qa_config.IsStorageTypeSupported(constants.ST_LVM_VG):
    return

740
  if use_client:
741
    job_id = _rapi_client.DeleteInstance(instance.name)
742
743
  else:
    (job_id, ) = _DoTests([
744
      ("/2/instances/%s" % instance.name, _VerifyReturnsJob, "DELETE", None),
745
      ])
746
747
748

  _WaitForRapiJob(job_id)

749

750
@InstanceCheck(INST_UP, INST_UP, FIRST_ARG)
751
752
def TestRapiInstanceMigrate(instance):
  """Test migrating instance via RAPI"""
753
754
755
756
  if not IsMigrationSupported(instance):
    print qa_utils.FormatInfo("Instance doesn't support migration, skipping"
                              " test")
    return
757
  # Move to secondary node
758
  _WaitForRapiJob(_rapi_client.MigrateInstance(instance.name))
759
  qa_utils.RunInstanceCheck(instance, True)
760
  # And back to previous primary
761
  _WaitForRapiJob(_rapi_client.MigrateInstance(instance.name))
762
763


764
@InstanceCheck(INST_UP, INST_UP, FIRST_ARG)
765
766
def TestRapiInstanceFailover(instance):
  """Test failing over instance via RAPI"""
767
768
769
770
  if not IsFailoverSupported(instance):
    print qa_utils.FormatInfo("Instance doesn't support failover, skipping"
                              " test")
    return
771
  # Move to secondary node
772
  _WaitForRapiJob(_rapi_client.FailoverInstance(instance.name))
773
  qa_utils.RunInstanceCheck(instance, True)
774
  # And back to previous primary
775
  _WaitForRapiJob(_rapi_client.FailoverInstance(instance.name))
776
777


778
@InstanceCheck(INST_UP, INST_DOWN, FIRST_ARG)
779
780
def TestRapiInstanceShutdown(instance):
  """Test stopping an instance via RAPI"""
781
  _WaitForRapiJob(_rapi_client.ShutdownInstance(instance.name))
782
783


784
@InstanceCheck(INST_DOWN, INST_UP, FIRST_ARG)
785
786
def TestRapiInstanceStartup(instance):
  """Test starting an instance via RAPI"""
787
  _WaitForRapiJob(_rapi_client.StartupInstance(instance.name))
788
789


790
@InstanceCheck(INST_DOWN, INST_DOWN, FIRST_ARG)
Iustin Pop's avatar
Iustin Pop committed
791
792
793
794
795
796
797
def TestRapiInstanceRenameAndBack(rename_source, rename_target):
  """Test renaming instance via RAPI

  This must leave the instance with the original name (in the
  non-failure case).

  """
798
  _WaitForRapiJob(_rapi_client.RenameInstance(rename_source, rename_target))
799
  qa_utils.RunInstanceCheck(rename_source, False)
800
  qa_utils.RunInstanceCheck(rename_target, False)
Iustin Pop's avatar
Iustin Pop committed
801
  _WaitForRapiJob(_rapi_client.RenameInstance(rename_target, rename_source))
802
  qa_utils.RunInstanceCheck(rename_target, False)
803
804


805
@InstanceCheck(INST_DOWN, INST_DOWN, FIRST_ARG)
806
807
def TestRapiInstanceReinstall(instance):
  """Test reinstalling an instance via RAPI"""
808
809
810
811
  if instance.disk_template == constants.DT_DISKLESS:
    print qa_utils.FormatInfo("Test not supported for diskless instances")
    return

812
  _WaitForRapiJob(_rapi_client.ReinstallInstance(instance.name))
813
814
815
816
  # By default, the instance is started again
  qa_utils.RunInstanceCheck(instance, True)

  # Reinstall again without starting
817
  _WaitForRapiJob(_rapi_client.ReinstallInstance(instance.name,
818
                                                 no_startup=True))
819
820


821
@InstanceCheck(INST_UP, INST_UP, FIRST_ARG)
822
823
def TestRapiInstanceReplaceDisks(instance):
  """Test replacing instance disks via RAPI"""
824
825
826
827
  if not IsDiskReplacingSupported(instance):
    print qa_utils.FormatInfo("Instance doesn't support disk replacing,"
                              " skipping test")
    return
Iustin Pop's avatar
Iustin Pop committed
828
  fn = _rapi_client.ReplaceInstanceDisks
829
  _WaitForRapiJob(fn(instance.name,
Iustin Pop's avatar
Iustin Pop committed
830
                     mode=constants.REPLACE_DISK_AUTO, disks=[]))
831
  _WaitForRapiJob(fn(instance.name,
Iustin Pop's avatar
Iustin Pop committed
832
                     mode=constants.REPLACE_DISK_SEC, disks="0"))
833
834


835
@InstanceCheck(INST_UP, INST_UP, FIRST_ARG)
836
837
def TestRapiInstanceModify(instance):
  """Test modifying instance via RAPI"""
838
839
  default_hv = qa_config.GetDefaultHypervisor()

840
  def _ModifyInstance(**kwargs):
841
    _WaitForRapiJob(_rapi_client.ModifyInstance(instance.name, **kwargs))
842
843
844
845
846
847
848
849
850

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

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

851
852
853
854
855
856
857
858
859
860
861
862
863
864
  if default_hv == constants.HT_XEN_PVM:
    _ModifyInstance(hvparams={
      constants.HV_KERNEL_ARGS: "single",
      })
    _ModifyInstance(hvparams={
      constants.HV_KERNEL_ARGS: constants.VALUE_DEFAULT,
      })
  elif default_hv == constants.HT_XEN_HVM:
    _ModifyInstance(hvparams={
      constants.HV_BOOT_ORDER: "acn",
      })
    _ModifyInstance(hvparams={
      constants.HV_BOOT_ORDER: constants.VALUE_DEFAULT,
      })
865
866


867
@InstanceCheck(INST_UP, INST_UP, FIRST_ARG)
868
869
def TestRapiInstanceConsole(instance):
  """Test getting instance console information via RAPI"""
870
  result = _rapi_client.GetInstanceConsole(instance.name)
871
  console = objects.InstanceConsole.FromDict(result)
Jose A. Lopes's avatar
Jose A. Lopes committed
872
  AssertEqual(console.Validate(), None)
873
  AssertEqual(console.instance, qa_utils.ResolveInstanceName(instance.name))
874
875


876
@InstanceCheck(INST_DOWN, INST_DOWN, FIRST_ARG)
877
878
879
def TestRapiStoppedInstanceConsole(instance):
  """Test getting stopped instance's console information via RAPI"""
  try:
880
    _rapi_client.GetInstanceConsole(instance.name)
881
882
883
884
885
886
887
  except rapi.client.GanetiApiError, err:
    AssertEqual(err.code, 503)
  else:
    raise qa_error.Error("Getting console for stopped instance didn't"
                         " return HTTP 503")


888
889
890
891
892
893
894
def GetOperatingSystems():
  """Retrieves a list of all available operating systems.

  """
  return _rapi_client.GetOperatingSystems()


895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
def _InvokeMoveInstance(current_dest_inst, current_src_inst, rapi_pw_filename,
                        joint_master, perform_checks, target_nodes=None):
  """ Invokes the move-instance tool for testing purposes.

  """
  # Some uses of this test might require that RAPI-only commands are used,
  # and the checks are command-line based.
  if perform_checks:
    qa_utils.RunInstanceCheck(current_dest_inst, False)

  cmd = [
      "../tools/move-instance",
      "--verbose",
      "--src-ca-file=%s" % _rapi_ca.name,
      "--src-username=%s" % _rapi_username,
      "--src-password-file=%s" % rapi_pw_filename,
      "--dest-instance-name=%s" % current_dest_inst,
      ]

  if target_nodes:
    pnode, snode = target_nodes
    cmd.extend([
      "--dest-primary-node=%s" % pnode,
      "--dest-secondary-node=%s" % snode,
      ])
  else:
921
922
923
924
    cmd.extend([
      "--iallocator=%s" % constants.IALLOC_HAIL,
      "--opportunistic-tries=1",
      ])
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939

  cmd.extend([
    "--net=0:mac=%s" % constants.VALUE_GENERATE,
    joint_master,
    joint_master,
    current_src_inst,
    ])

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

  if perform_checks:
    qa_utils.RunInstanceCheck(current_src_inst, False)
    qa_utils.RunInstanceCheck(current_dest_inst, True)


940
def TestInterClusterInstanceMove(src_instance, dest_instance,
941
                                 inodes, tnode, perform_checks=True):
942
943
944
945
946
947
948
  """Test tools/move-instance"""
  master = qa_config.GetMasterNode()

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

949
950
951
  # Needed only if checks are to be performed
  if perform_checks:
    dest_instance.SetDiskTemplate(src_instance.disk_template)
952

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

955
956
957
958
959
960
  if len(inodes) > 1:
    # No disk template currently requires more than 1 secondary node. If this
    # changes, either this test must be skipped or the script must be updated.
    assert len(inodes) == 2
    snode = inodes[1]
  else:
961
    # Instance is not redundant, but we still need to pass a node
962
    # (which will be ignored)
963
964
    snode = tnode
  pnode = inodes[0]
965

966
967
968
969
970
  # pnode:snode are the *current* nodes, and the first move is an
  # iallocator-guided move outside of pnode. The node lock for the pnode
  # assures that this happens, and while we cannot be sure where the instance
  # will land, it is a real move.
  locks = {locking.LEVEL_NODE: [pnode.primary]}
971
  RunWithLocks(_InvokeMoveInstance, locks, 600.0, False,
972
973
               dest_instance.name, src_instance.name, rapi_pw_file.name,
               master.primary, perform_checks)
974

975
976
977
978
  # And then back to pnode:snode
  _InvokeMoveInstance(src_instance.name, dest_instance.name, rapi_pw_file.name,
                      master.primary, perform_checks,
                      target_nodes=(pnode.primary, snode.primary))