Statistics
| Branch: | Tag: | Revision:

root / qa / rapi-workload.py @ 67bd83ae

History | View | Annotate | Download (35.3 kB)

1
#!/usr/bin/python -u
2
#
3

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

    
21

    
22
"""Script for providing a large amount of RAPI calls to Ganeti.
23

24
"""
25

    
26
# pylint: disable=C0103
27
# due to invalid name
28

    
29
import inspect
30
import optparse
31
import sys
32
import types
33

    
34
import ganeti.constants as constants
35
from ganeti.rapi.client import GanetiApiError, NODE_EVAC_PRI, NODE_EVAC_SEC
36

    
37
import qa_config
38
import qa_error
39
import qa_node
40
import qa_rapi
41

    
42

    
43
# The purpose of this file is to provide a stable and extensive RAPI workload
44
# that manipulates the cluster only using RAPI commands, with the assumption
45
# that an empty cluster was set up beforehand. All the nodes that can be added
46
# to the cluster should be a part of it, and no instances should be present.
47
#
48
# Its intended use is in RAPI compatibility tests, where different versions with
49
# possibly vastly different QAs must be compared. Running the QA on both
50
# versions of the cluster will produce RAPI calls, but there is no guarantee
51
# that they will match, or that functions invoked in between will not change the
52
# results.
53
#
54
# By using only RAPI functions, we are sure to be able to capture and log all
55
# the changes in cluster state, and be able to compare them afterwards.
56
#
57
# The functionality of the QA is still used to generate a functioning,
58
# RAPI-enabled cluster, and to set up a C{GanetiRapiClient} capable of issuing
59
# commands to the cluster.
60
#
61
# Due to the fact that not all calls issued as a part of the workload might be
62
# implemented in the different versions of Ganeti, the client does not halt or
63
# produce a non-zero exit code upon encountering a RAPI error. Instead, it
64
# reports it and moves on. Any utility comparing the requests should account for
65
# this.
66

    
67

    
68
def MockMethod(*_args, **_kwargs):
69
  """ Absorbs all arguments, does nothing, returns None.
70

71
  """
72
  return None
73

    
74

    
75
RAPI_USERNAME = "ganeti-qa"
76

    
77

    
78
class GanetiRapiClientWrapper(object):
79
  """ Creates and initializes a GanetiRapiClient, and acts as a wrapper invoking
80
  only the methods that the version of the client actually uses.
81

82
  """
83
  def __init__(self):
84
    self._client = qa_rapi.Setup(RAPI_USERNAME,
85
                                 qa_rapi.LookupRapiSecret(RAPI_USERNAME))
86

    
87
    self._method_invocations = {}
88

    
89
  def _RecordMethodInvocation(self, name, arg_dict):
90
    """ Records the invocation of a C{GanetiRAPIClient} method, noting the
91
    argument and the method names.
92

93
    """
94
    if name not in self._method_invocations:
95
      self._method_invocations[name] = set()
96

    
97
    for named_arg in arg_dict:
98
      self._method_invocations[name].add(named_arg)
99

    
100
  def _InvokerCreator(self, fn, name):
101
    """ Returns an invoker function that will invoke the given function
102
    with any arguments passed to the invoker at a later time, while
103
    catching any specific non-fatal errors we would like to know more
104
    about.
105

106
    @type fn: arbitrary function
107
    @param fn: The function to invoke later.
108
    @type name: string
109
    @param name: The name of the function, for debugging purposes.
110
    @rtype: function
111

112
    """
113
    def decoratedFn(*args, **kwargs):
114
      result = None
115
      try:
116
        print "Using method %s" % name
117
        self._RecordMethodInvocation(name, kwargs)
118
        result = fn(*args, **kwargs)
119
      except GanetiApiError as e:
120
        print "RAPI error while performing function %s : %s" % \
121
              (name, str(e))
122
      return result
123

    
124
    return decoratedFn
125

    
126
  def __getattr__(self, attr):
127
    """ Fetches an attribute from the underlying client if necessary.
128

129
    """
130
    # Assuming that this method exposes no public methods of its own,
131
    # and that any private methods are named according to the style
132
    # guide, this will stop infinite loops in attribute fetches.
133
    if attr.startswith("_"):
134
      return self.__getattribute__(attr)
135

    
136
    # We also want to expose non-methods
137
    if hasattr(self._client, attr) and \
138
       not isinstance(getattr(self._client, attr), types.MethodType):
139
      return getattr(self._client, attr)
140

    
141
    try:
142
      return self._InvokerCreator(self._client.__getattribute__(attr), attr)
143
    except AttributeError:
144
      print "Missing method %s; supplying mock method" % attr
145
      return MockMethod
146

    
147
  def _OutputMethodInvocationDetails(self):
148
    """ Attempts to output as much information as possible about the methods
149
    that have and have not been invoked, including which arguments have not
150
    been used.
151

152
    """
153
    print "\nMethod usage:\n"
154
    for method in [n for n in dir(self._client)
155
                     if not n.startswith('_') and
156
                        isinstance(self.__getattr__(n), types.FunctionType)]:
157
      if method not in self._method_invocations:
158
        print "Method unused: %s" % method
159
      else:
160
        arg_spec, _, _, default_arg_spec = \
161
          inspect.getargspec(getattr(self._client, method))
162
        default_args = []
163
        if default_arg_spec is not None:
164
          default_args = arg_spec[-len(default_arg_spec):]
165
        used_arg_set = self._method_invocations[method]
166
        unused_args = [arg for arg in default_args if arg not in used_arg_set]
167
        if unused_args:
168
          print "Method %s used, but arguments unused: %s" % \
169
                (method, ", ".join(unused_args))
170

    
171

    
172
def Finish(client, fn, *args, **kwargs):
173
  """ When invoked with a job-starting RAPI client method, it passes along any
174
  additional arguments and waits until its completion.
175

176
  @type client: C{GanetiRapiClientWrapper}
177
  @param client: The client wrapper.
178
  @type fn: function
179
  @param fn: A client method returning a job id.
180

181
  @rtype: tuple of bool, any object
182
  @return: The success status and the result of the operation, if any
183

184
  """
185
  possible_job_id = fn(*args, **kwargs)
186
  try:
187
    # The job ids are returned as both ints and ints represented by strings.
188
    # This is a pythonic check to see if the content is an int.
189
    int(possible_job_id)
190
  except (ValueError, TypeError):
191
    # As a rule of thumb, failures will return None, and other methods are
192
    # expected to return at least something
193
    if possible_job_id is not None:
194
      print ("Finish called with a method not producing a job id, "
195
             "returning %s" % possible_job_id)
196
    return possible_job_id
197

    
198
  success = client.WaitForJobCompletion(possible_job_id)
199

    
200
  result = client.GetJobStatus(possible_job_id)["opresult"][0]
201
  if success:
202
    return success, result
203
  else:
204
    print "Error encountered while performing operation: "
205
    print result
206
    return success, None
207

    
208

    
209
def TestTags(client, get_fn, add_fn, delete_fn, *args):
210
  """ Tests whether tagging works.
211

212
  @type client: C{GanetiRapiClientWrapper}
213
  @param client: The client wrapper.
214
  @type get_fn: function
215
  @param get_fn: A Get*Tags function of the client.
216
  @type add_fn: function
217
  @param add_fn: An Add*Tags function of the client.
218
  @type delete_fn: function
219
  @param delete_fn: A Delete*Tags function of the client.
220

221
  To allow this method to work for all tagging functions of the client, use
222
  named methods.
223

224
  """
225
  get_fn(*args)
226

    
227
  tags = ["tag1", "tag2", "tag3"]
228
  Finish(client, add_fn, *args, tags=tags, dry_run=True)
229
  Finish(client, add_fn, *args, tags=tags)
230

    
231
  get_fn(*args)
232

    
233
  Finish(client, delete_fn, *args, tags=tags[:1], dry_run=True)
234
  Finish(client, delete_fn, *args, tags=tags[:1])
235

    
236
  get_fn(*args)
237

    
238
  Finish(client, delete_fn, *args, tags=tags[1:])
239

    
240
  get_fn(*args)
241

    
242

    
243
def TestGetters(client):
244
  """ Tests the various get functions which only retrieve information about the
245
  cluster.
246

247
  @type client: C{GanetiRapiClientWrapper}
248

249
  """
250
  client.GetVersion()
251
  client.GetFeatures()
252
  client.GetOperatingSystems()
253
  client.GetInfo()
254
  client.GetClusterTags()
255
  client.GetInstances()
256
  client.GetInstances(bulk=True)
257
  client.GetJobs()
258
  client.GetJobs(bulk=True)
259
  client.GetNodes()
260
  client.GetNodes(bulk=True)
261
  client.GetNetworks()
262
  client.GetNetworks(bulk=True)
263
  client.GetGroups()
264
  client.GetGroups(bulk=True)
265

    
266

    
267
def TestQueries(client, resource_name):
268
  """ Finds out which fields are present for a given resource type, and attempts
269
  to retrieve their values for all present resources.
270

271
  @type client: C{GanetiRapiClientWrapper}
272
  @param client: A wrapped RAPI client.
273
  @type resource_name: string
274
  @param resource_name: The name of the resource to use.
275

276
  """
277

    
278
  FIELDS_KEY = "fields"
279

    
280
  query_res = client.QueryFields(resource_name)
281

    
282
  if query_res is None or FIELDS_KEY not in query_res or \
283
    len(query_res[FIELDS_KEY]) == 0:
284
    return
285

    
286
  field_entries = query_res[FIELDS_KEY]
287

    
288
  fields = map(lambda e: e["name"], field_entries)
289

    
290
  client.Query(resource_name, fields)
291

    
292

    
293
def TestQueryFiltering(client, master_name):
294
  """ Performs queries by playing around with the only guaranteed resource, the
295
  master node.
296

297
  @type client: C{GanetiRapiClientWrapper}
298
  @param client: A wrapped RAPI client.
299
  @type master_name: string
300
  @param master_name: The hostname of the master node.
301

302
  """
303
  qfilter = [
304
    "|",
305
    ["=", "name", master_name],
306
    [">", "dtotal", 0],
307
  ]
308
  client.Query("node", ["name"], qfilter)
309

    
310
  qfilter = [
311
    "|",
312
    ["=", "name", "NonexistentInstance"],
313
    [">", "oper_ram", 0],
314
  ]
315
  client.Query("instance", ["name"], qfilter)
316

    
317

    
318
def RemoveAllInstances(client):
319
  """ Queries for a list of instances, then removes them all.
320

321
  @type client: C{GanetiRapiClientWrapper}
322
  @param client: A wrapped RAPI client.
323

324
  """
325
  instances = client.GetInstances()
326
  for inst in instances:
327
    Finish(client, client.DeleteInstance, inst)
328

    
329
  instances = client.GetInstances()
330
  assert len(instances) == 0
331

    
332

    
333
def TestSingleInstance(client, instance_name, alternate_name, node_one,
334
                       node_two):
335
  """ Creates an instance, performs operations involving it, and then deletes
336
  it.
337

338
  @type client: C{GanetiRapiClientWrapper}
339
  @param client: A wrapped RAPI client.
340
  @type instance_name: string
341
  @param instance_name: The hostname to use.
342
  @type instance_name: string
343
  @param instance_name: Another valid hostname to use.
344
  @type node_one: string
345
  @param node_one: A node on which an instance can be added.
346
  @type node_two: string
347
  @param node_two: Another node on which an instance can be added.
348

349
  """
350

    
351
  # Check that a dry run works, use string with size and unit
352
  Finish(client, client.CreateInstance,
353
         "create", instance_name, "plain", [{"size":"1gb"}], [], dry_run=True,
354
          os="debian-image", pnode=node_one)
355

    
356
  # Another dry run, numeric size, should work, but still a dry run
357
  Finish(client, client.CreateInstance,
358
         "create", instance_name, "plain", [{"size": "1000"}], [{}],
359
         dry_run=True, os="debian-image", pnode=node_one)
360

    
361
  # Create a smaller instance, and delete it immediately
362
  Finish(client, client.CreateInstance,
363
         "create", instance_name, "plain", [{"size":800}], [{}],
364
         os="debian-image", pnode=node_one)
365

    
366
  Finish(client, client.DeleteInstance, instance_name)
367

    
368
  # Create one instance to use in further tests
369
  Finish(client, client.CreateInstance,
370
         "create", instance_name, "plain", [{"size":1200}], [{}],
371
         os="debian-image", pnode=node_one)
372

    
373
  client.GetInstance(instance_name)
374

    
375
  Finish(client, client.GetInstanceInfo, instance_name)
376

    
377
  Finish(client, client.GetInstanceInfo, instance_name, static=True)
378

    
379
  TestQueries(client, "instance")
380

    
381
  TestTags(client, client.GetInstanceTags, client.AddInstanceTags,
382
           client.DeleteInstanceTags, instance_name)
383

    
384
  Finish(client, client.GrowInstanceDisk,
385
         instance_name, 0, 100, wait_for_sync=True)
386

    
387
  Finish(client, client.RebootInstance,
388
         instance_name, "soft", ignore_secondaries=True, dry_run=True,
389
         reason="Hulk smash gently!")
390

    
391
  Finish(client, client.ShutdownInstance,
392
         instance_name, dry_run=True, no_remember=False,
393
         reason="Hulk smash hard!")
394

    
395
  Finish(client, client.StartupInstance,
396
         instance_name, dry_run=True, no_remember=False,
397
         reason="Not hard enough!")
398

    
399
  Finish(client, client.RebootInstance,
400
         instance_name, "soft", ignore_secondaries=True, dry_run=False)
401

    
402
  Finish(client, client.ShutdownInstance,
403
         instance_name, dry_run=False, no_remember=False)
404

    
405
  Finish(client, client.ModifyInstance,
406
         instance_name, disk_template="drbd", remote_node=node_two)
407

    
408
  Finish(client, client.ModifyInstance,
409
         instance_name, disk_template="plain")
410

    
411
  Finish(client, client.RenameInstance,
412
         instance_name, alternate_name, ip_check=True, name_check=True)
413

    
414
  Finish(client, client.RenameInstance, alternate_name, instance_name)
415

    
416
  Finish(client, client.DeactivateInstanceDisks, instance_name)
417

    
418
  Finish(client, client.ActivateInstanceDisks, instance_name)
419

    
420
  # Note that the RecreateInstanceDisks command will always fail, as there is
421
  # no way to induce the necessary prerequisites (removal of LV) via RAPI.
422
  # Keeping it around allows us to at least know that it still exists.
423
  Finish(client, client.RecreateInstanceDisks,
424
         instance_name, [0], [node_one])
425

    
426
  Finish(client, client.StartupInstance,
427
         instance_name, dry_run=False, no_remember=False)
428

    
429
  client.GetInstanceConsole(instance_name)
430

    
431
  Finish(client, client.ReinstallInstance,
432
         instance_name, os=None, no_startup=False, osparams={})
433

    
434
  Finish(client, client.DeleteInstance, instance_name, dry_run=True)
435

    
436
  Finish(client, client.DeleteInstance, instance_name)
437

    
438

    
439
def MarkUnmarkNode(client, node, state):
440
  """ Given a certain node state, marks a node as being in that state, and then
441
  unmarks it.
442

443
  @type client: C{GanetiRapiClientWrapper}
444
  @param client: A wrapped RAPI client.
445
  @type node: string
446
  @type state: string
447

448
  """
449
  # pylint: disable=W0142
450
  Finish(client, client.ModifyNode, node, **{state: True})
451
  Finish(client, client.ModifyNode, node, **{state: False})
452
  # pylint: enable=W0142
453

    
454

    
455
def TestNodeOperations(client, non_master_node):
456
  """ Tests various operations related to nodes only
457

458
  @type client: C{GanetiRapiClientWrapper}
459
  @param client: A wrapped RAPI client.
460
  @type non_master_node: string
461
  @param non_master_node: The name of a non-master node in the cluster.
462

463
  """
464

    
465
  client.GetNode(non_master_node)
466

    
467
  old_role = client.GetNodeRole(non_master_node)
468

    
469
  # Should fail
470
  Finish(client, client.SetNodeRole,
471
         non_master_node, "master", False, auto_promote=True)
472

    
473
  Finish(client, client.SetNodeRole,
474
         non_master_node, "regular", False, auto_promote=True)
475

    
476
  Finish(client, client.SetNodeRole,
477
         non_master_node, "master-candidate", False, auto_promote=True)
478

    
479
  Finish(client, client.SetNodeRole,
480
         non_master_node, "drained", False, auto_promote=True)
481

    
482
  Finish(client, client.SetNodeRole,
483
         non_master_node, old_role, False, auto_promote=True)
484

    
485
  Finish(client, client.PowercycleNode,
486
         non_master_node, force=False)
487

    
488
  storage_units_fields = [
489
    "name", "allocatable", "free", "node", "size", "type", "used",
490
  ]
491

    
492
  for storage_type in constants.STS_REPORT:
493
    success, storage_units = Finish(client, client.GetNodeStorageUnits,
494
                                    non_master_node, storage_type,
495
                                    ",".join(storage_units_fields))
496

    
497
    if success and len(storage_units) > 0 and len(storage_units[0]) > 0:
498
      # Name is the first entry of the first result, allocatable the other
499
      unit_name = storage_units[0][0]
500
      Finish(client, client.ModifyNodeStorageUnits,
501
             non_master_node, storage_type, unit_name,
502
             allocatable=not storage_units[0][1])
503
      Finish(client, client.ModifyNodeStorageUnits,
504
             non_master_node, storage_type, unit_name,
505
             allocatable=storage_units[0][1])
506
      Finish(client, client.RepairNodeStorageUnits,
507
             non_master_node, storage_type, unit_name)
508

    
509
  MarkUnmarkNode(client, non_master_node, "drained")
510
  MarkUnmarkNode(client, non_master_node, "powered")
511
  MarkUnmarkNode(client, non_master_node, "offline")
512

    
513
  TestQueries(client, "node")
514

    
515

    
516
def TestGroupOperations(client, node, another_node):
517
  """ Tests various operations related to groups only.
518

519
  @type client: C{GanetiRapiClientWrapper}
520
  @param client: A Ganeti RAPI client to use.
521
  @type node: string
522
  @param node: The name of a node in the cluster.
523
  @type another_node: string
524
  @param another_node: The name of another node in the cluster.
525

526
  """
527

    
528
  DEFAULT_GROUP_NAME = constants.INITIAL_NODE_GROUP_NAME
529
  TEST_GROUP_NAME = "TestGroup"
530
  ALTERNATE_GROUP_NAME = "RenamedTestGroup"
531

    
532
  Finish(client, client.CreateGroup,
533
         TEST_GROUP_NAME, alloc_policy=constants.ALLOC_POLICY_PREFERRED,
534
         dry_run=True)
535

    
536
  Finish(client, client.CreateGroup,
537
         TEST_GROUP_NAME, alloc_policy=constants.ALLOC_POLICY_PREFERRED)
538

    
539
  client.GetGroup(TEST_GROUP_NAME)
540

    
541
  TestQueries(client, "group")
542

    
543
  TestTags(client, client.GetGroupTags, client.AddGroupTags,
544
           client.DeleteGroupTags, TEST_GROUP_NAME)
545

    
546
  Finish(client, client.ModifyGroup,
547
         TEST_GROUP_NAME, alloc_policy=constants.ALLOC_POLICY_PREFERRED,
548
         depends=None)
549

    
550
  Finish(client, client.AssignGroupNodes,
551
         TEST_GROUP_NAME, [node, another_node], force=False, dry_run=True)
552

    
553
  Finish(client, client.AssignGroupNodes,
554
         TEST_GROUP_NAME, [another_node], force=False)
555

    
556
  Finish(client, client.RenameGroup,
557
         TEST_GROUP_NAME, ALTERNATE_GROUP_NAME)
558

    
559
  Finish(client, client.RenameGroup,
560
         ALTERNATE_GROUP_NAME, TEST_GROUP_NAME)
561

    
562
  Finish(client, client.AssignGroupNodes,
563
         DEFAULT_GROUP_NAME, [another_node], force=False)
564

    
565
  Finish(client, client.DeleteGroup, TEST_GROUP_NAME, dry_run=True)
566

    
567
  Finish(client, client.DeleteGroup, TEST_GROUP_NAME)
568

    
569

    
570
def TestNetworkConnectDisconnect(client, network_name, mode, link):
571
  """ Test connecting and disconnecting the network to a new node group.
572

573
  @type network_name: string
574
  @param network_name: The name of an existing and unconnected network.
575
  @type mode: string
576
  @param mode: The network mode.
577
  @type link: string
578
  @param link: The network link.
579

580
  """
581
  # For testing the connect/disconnect calls, a group is needed
582
  TEST_GROUP_NAME = "TestGroup"
583
  Finish(client, client.CreateGroup,
584
         TEST_GROUP_NAME, alloc_policy=constants.ALLOC_POLICY_PREFERRED)
585

    
586
  Finish(client, client.ConnectNetwork,
587
         network_name, TEST_GROUP_NAME, mode, link, dry_run=True)
588

    
589
  Finish(client, client.ConnectNetwork,
590
         network_name, TEST_GROUP_NAME, mode, link)
591

    
592
  Finish(client, client.DisconnectNetwork,
593
         network_name, TEST_GROUP_NAME, dry_run=True)
594

    
595
  Finish(client, client.DisconnectNetwork,
596
         network_name, TEST_GROUP_NAME)
597

    
598
  # Clean up the group
599
  Finish(client, client.DeleteGroup, TEST_GROUP_NAME)
600

    
601

    
602
def TestNetworks(client):
603
  """ Add some networks of different sizes, using RFC5737 addresses like in the
604
  QA.
605

606
  """
607

    
608
  NETWORK_NAME = "SurelyCertainlyNonexistentNetwork"
609

    
610
  Finish(client, client.CreateNetwork,
611
         NETWORK_NAME, "192.0.2.0/30", tags=[], dry_run=True)
612

    
613
  Finish(client, client.CreateNetwork,
614
         NETWORK_NAME, "192.0.2.0/30", tags=[])
615

    
616
  client.GetNetwork(NETWORK_NAME)
617

    
618
  TestTags(client, client.GetNetworkTags, client.AddNetworkTags,
619
           client.DeleteNetworkTags, NETWORK_NAME)
620

    
621
  Finish(client, client.ModifyNetwork,
622
         NETWORK_NAME, mac_prefix=None)
623

    
624
  TestQueries(client, "network")
625

    
626
  default_nicparams = qa_config.get("default-nicparams", None)
627

    
628
  # The entry might not be present in the QA config
629
  if default_nicparams is not None:
630
    mode = default_nicparams.get("mode", None)
631
    link = default_nicparams.get("link", None)
632
    if mode is not None and link is not None:
633
      TestNetworkConnectDisconnect(client, NETWORK_NAME, mode, link)
634

    
635
  # Clean up the network
636
  Finish(client, client.DeleteNetwork,
637
         NETWORK_NAME, dry_run=True)
638

    
639
  Finish(client, client.DeleteNetwork, NETWORK_NAME)
640

    
641

    
642
def CreateDRBDInstance(client, node_one, node_two, instance_name):
643
  """ Creates a DRBD-enabled instance on the given nodes.
644

645
  """
646
  Finish(client, client.CreateInstance,
647
         "create", instance_name, "drbd", [{"size": "1000"}], [{}],
648
         os="debian-image", pnode=node_one, snode=node_two)
649

    
650

    
651
def TestInstanceMigrations(client, node_one, node_two, node_three,
652
                           instance_name):
653
  """ Test various operations related to migrating instances.
654

655
  @type node_one: string
656
  @param node_one: The name of a node in the cluster.
657
  @type node_two: string
658
  @param node_two: The name of another node in the cluster.
659
  @type node_three: string
660
  @param node_three: The name of yet another node in the cluster.
661
  @type instance_name: string
662
  @param instance_name: An instance name that can be used.
663

664
  """
665

    
666
  CreateDRBDInstance(client, node_one, node_two, instance_name)
667
  Finish(client, client.FailoverInstance, instance_name)
668
  Finish(client, client.DeleteInstance, instance_name)
669

    
670
  CreateDRBDInstance(client, node_one, node_two, instance_name)
671
  Finish(client, client.EvacuateNode,
672
         node_two, early_release=False, mode=NODE_EVAC_SEC,
673
         remote_node=node_three)
674
  Finish(client, client.DeleteInstance, instance_name)
675

    
676
  CreateDRBDInstance(client, node_one, node_two, instance_name)
677
  Finish(client, client.EvacuateNode,
678
         node_one, early_release=False, mode=NODE_EVAC_PRI, iallocator="hail")
679
  Finish(client, client.DeleteInstance, instance_name)
680

    
681
  CreateDRBDInstance(client, node_one, node_two, instance_name)
682
  Finish(client, client.MigrateInstance,
683
         instance_name, cleanup=True, target_node=node_two)
684
  Finish(client, client.DeleteInstance, instance_name)
685

    
686
  CreateDRBDInstance(client, node_one, node_two, instance_name)
687
  Finish(client, client.MigrateNode,
688
         node_one, iallocator="hail", mode="non-live")
689
  Finish(client, client.DeleteInstance, instance_name)
690

    
691
  CreateDRBDInstance(client, node_one, node_two, instance_name)
692
  Finish(client, client.MigrateNode,
693
         node_one, target_node=node_two, mode="non-live")
694
  Finish(client, client.DeleteInstance, instance_name)
695

    
696

    
697
def ExtractAllNicInformationPossible(nics, replace_macs=True):
698
  """ Extracts NIC information as a dictionary.
699

700
  @type nics: list of tuples of varying structure
701
  @param nics: The network interfaces, as received from the instance info RAPI
702
               call.
703

704
  @rtype: list of dict
705
  @return: Dictionaries of NIC information.
706

707
  The NIC information is returned in a different format across versions, and to
708
  try and see if the execution of commands is still compatible, this function
709
  attempts to grab all the info that it can.
710

711
  """
712

    
713
  desired_entries = [
714
    constants.INIC_IP,
715
    constants.INIC_MAC,
716
    constants.INIC_MODE,
717
    constants.INIC_LINK,
718
    constants.INIC_VLAN,
719
    constants.INIC_NETWORK,
720
    constants.INIC_NAME,
721
    ]
722

    
723
  nic_dicts = []
724
  for nic_index in range(len(nics)):
725
    nic_raw_data = nics[nic_index]
726

    
727
    # Fill dictionary with None-s as defaults
728
    nic_dict = dict([(key, None) for key in desired_entries])
729

    
730
    try:
731
      # The 2.6 format
732
      ip, mac, mode, link = nic_raw_data
733
    except ValueError:
734
      # If there is yet another ValueError here, let it go through as it is
735
      # legitimate - we are out of versions
736

    
737
      # The 2.11 format
738
      nic_name, _, ip, mac, mode, link, vlan, network, _ = nic_raw_data
739
      nic_dict[constants.INIC_VLAN] = vlan
740
      nic_dict[constants.INIC_NETWORK] = network
741
      nic_dict[constants.INIC_NAME] = nic_name
742

    
743
    # These attributes will be present in either version
744
    nic_dict[constants.INIC_IP] = ip
745
    nic_dict[constants.INIC_MAC] = mac
746
    nic_dict[constants.INIC_MODE] = mode
747
    nic_dict[constants.INIC_LINK] = link
748

    
749
    # Very simple mac generation, which should work as the setup cluster should
750
    # have no mac prefix restrictions in the default network, and there is a
751
    # hard and reasonable limit of only 8 NICs
752
    if replace_macs:
753
      nic_dict[constants.INIC_MAC] = "00:00:00:00:00:%02x" % nic_index
754

    
755
    nic_dicts.append(nic_dict)
756

    
757
  return nic_dicts
758

    
759

    
760
def MoveInstance(client, src_instance, dst_instance, src_node, dst_node):
761
  """ Moves a single instance, compatible with 2.6.
762

763
  @rtype: bool
764
  @return: Whether the instance was moved successfully
765

766
  """
767
  success, inst_info_all = Finish(client, client.GetInstanceInfo,
768
                                  src_instance.name, static=True)
769

    
770
  if not success or src_instance.name not in inst_info_all:
771
    raise Exception("Did not find the source instance information!")
772

    
773
  inst_info = inst_info_all[src_instance.name]
774

    
775
  # Try to extract NICs first, as this is the operation most likely to fail
776
  try:
777
    nic_info = ExtractAllNicInformationPossible(inst_info["nics"])
778
  except ValueError:
779
    # Without the NIC info, there is very little we can do
780
    return False
781

    
782
  NIC_COMPONENTS_26 = [
783
    constants.INIC_IP,
784
    constants.INIC_MAC,
785
    constants.INIC_MODE,
786
    constants.INIC_LINK,
787
    ]
788

    
789
  nic_converter = lambda old: dict((k, old[k]) for k in NIC_COMPONENTS_26)
790
  nics = map(nic_converter, nic_info)
791

    
792
  # Prepare the parameters
793
  disks = []
794
  for idisk in inst_info["disks"]:
795
    odisk = {
796
      constants.IDISK_SIZE: idisk["size"],
797
      constants.IDISK_MODE: idisk["mode"],
798
      }
799

    
800
    spindles = idisk.get("spindles")
801
    if spindles is not None:
802
      odisk[constants.IDISK_SPINDLES] = spindles
803

    
804
    # Disk name may be present, but must not be supplied in 2.6!
805
    disks.append(odisk)
806

    
807
  # With all the parameters properly prepared, try the export
808
  success, exp_info = Finish(client, client.PrepareExport,
809
                             src_instance.name, constants.EXPORT_MODE_REMOTE)
810

    
811
  if not success:
812
    # The instance will still have to be deleted
813
    return False
814

    
815
  success, _ = Finish(client, client.CreateInstance,
816
                      constants.INSTANCE_REMOTE_IMPORT, dst_instance.name,
817
                      inst_info["disk_template"], disks, nics,
818
                      os=inst_info["os"],
819
                      pnode=dst_node.primary,
820
                      snode=src_node.primary, # Ignored as no DRBD
821
                      start=(inst_info["config_state"] == "up"),
822
                      ip_check=False,
823
                      iallocator=inst_info.get("iallocator", None),
824
                      hypervisor=inst_info["hypervisor"],
825
                      source_handshake=exp_info["handshake"],
826
                      source_x509_ca=exp_info["x509_ca"],
827
                      source_instance_name=inst_info["name"],
828
                      beparams=inst_info["be_instance"],
829
                      hvparams=inst_info["hv_instance"],
830
                      osparams=inst_info["os_instance"])
831

    
832
  return success
833

    
834

    
835
def CreateInstanceForMoveTest(client, node, instance):
836
  """ Creates a single shutdown instance to move about in tests.
837

838
  @type node: C{_QaNode}
839
  @param node: A node configuration object.
840
  @type instance: C{_QaInstance}
841
  @param instance: An instance configuration object.
842

843
  """
844
  Finish(client, client.CreateInstance,
845
         "create", instance.name, "plain", [{"size": "2000"}], [{}],
846
         os="debian-image", pnode=node.primary)
847

    
848
  Finish(client, client.ShutdownInstance,
849
         instance.name, dry_run=False, no_remember=False)
850

    
851

    
852
def Test26InstanceMove(client, node_one, node_two, instance_to_create,
853
                       new_instance):
854
  """ Tests instance moves using commands that work in 2.6.
855

856
  """
857

    
858
  # First create the instance to move
859
  CreateInstanceForMoveTest(client, node_one, instance_to_create)
860

    
861
  # The cleanup should be conditional on operation success
862
  if MoveInstance(client, instance_to_create, new_instance, node_one, node_two):
863
    Finish(client, client.DeleteInstance, new_instance.name)
864
  else:
865
    Finish(client, client.DeleteInstance, instance_to_create.name)
866

    
867

    
868
def Test211InstanceMove(client, node_one, node_two, instance_to_create,
869
                        new_instance):
870
  """ Tests instance moves using the QA-provided move test.
871

872
  """
873

    
874
  # First create the instance to move
875
  CreateInstanceForMoveTest(client, node_one, instance_to_create)
876

    
877
  instance_to_create.SetDiskTemplate("plain")
878

    
879
  try:
880
    qa_rapi.TestInterClusterInstanceMove(instance_to_create, new_instance,
881
                                         [node_one], node_two,
882
                                         perform_checks=False)
883
  except qa_error.Error:
884
    # A failure is sad, but requires no special actions to be undertaken
885
    pass
886

    
887
  # Try to delete the instance when done - either the move has failed, or
888
  # a double move was performed - the instance to delete is one and the same
889
  Finish(client, client.DeleteInstance, instance_to_create.name)
890

    
891

    
892
def TestInstanceMoves(client, node_one, node_two, instance_to_create,
893
                      new_instance):
894
  """ Performs two types of instance moves, one compatible with 2.6, the other
895
  with 2.11.
896

897
  @type node_one: C{_QaNode}
898
  @param node_one: A node configuration object.
899
  @type node_two: C{_QaNode}
900
  @param node_two: A node configuration object.
901
  @type instance_to_create: C{_QaInstance}
902
  @param instance_to_create: An instance configuration object.
903
  @type new_instance: C{_QaInstance}
904
  @param new_instance: An instance configuration object.
905

906
  """
907

    
908
  Test26InstanceMove(client, node_one, node_two, instance_to_create,
909
                     new_instance)
910
  Test211InstanceMove(client, node_one, node_two, instance_to_create,
911
                      new_instance)
912

    
913

    
914
def TestClusterParameterModification(client):
915
  """ Try to modify some of the cluster parameters using RAPI.
916

917
  """
918
  cluster_info = client.GetInfo()
919

    
920
  # Each attribute has several safe choices we can use
921
  ATTRIBUTES_TO_MODIFY = [
922
    ("default_iallocator", ["hail", ""]), # Use "" to reset
923
    ("candidate_pool_size", [1, 5]),
924
    ("maintain_node_health", [True, False]),
925
    ]
926

    
927
  for attribute, options in ATTRIBUTES_TO_MODIFY:
928
    current_value = cluster_info[attribute]
929

    
930
    if current_value in options:
931
      value_to_use = options[1 - options.index(current_value)]
932
    else:
933
      value_to_use = options[0]
934

    
935
    #pylint: disable=W0142
936
    Finish(client, client.ModifyCluster, **{attribute: value_to_use})
937
    Finish(client, client.ModifyCluster, **{attribute: current_value})
938
    #pylint: enable=W0142
939

    
940

    
941
def TestJobCancellation(client, node_one, node_two, instance_one, instance_two):
942
  """ Test if jobs can be cancelled.
943

944
  @type node_one: string
945
  @param node_one: The name of a node in the cluster.
946
  @type node_two: string
947
  @param node_two: The name of another node in the cluster.
948
  @type instance_one: string
949
  @param instance_one: An available instance name.
950
  @type instance_two: string
951
  @param instance_two: An available instance name.
952

953
  """
954

    
955
  # Just in case, remove all previously present instances
956
  RemoveAllInstances(client)
957

    
958
  # Let us issue a job that is sure to both succeed and last for a while
959
  running_job = client.CreateInstance("create", instance_one, "drbd",
960
                                      [{"size": "5000"}], [{}],
961
                                      os="debian-image", pnode=node_one,
962
                                      snode=node_two)
963

    
964
  # And immediately afterwards, another very similar one
965
  job_to_cancel = client.CreateInstance("create", instance_two, "drbd",
966
                                        [{"size": "5000"}], [{}],
967
                                        os="debian-image", pnode=node_one,
968
                                        snode=node_two)
969

    
970
  # Try to cancel, which should fail as the job is already running
971
  success, msg = client.CancelJob(running_job)
972
  if success:
973
    print "Job succeeded: this should not have happened as it is running!"
974
    print "Message: %s" % msg
975

    
976
  success, msg = client.CancelJob(job_to_cancel)
977
  if not success:
978
    print "Job failed: this was unexpected as it was not a dry run"
979
    print "Message: %s" % msg
980

    
981
  # And wait for the proper job
982
  client.WaitForJobCompletion(running_job)
983

    
984
  # Remove all the leftover instances, success or no success
985
  RemoveAllInstances(client)
986

    
987

    
988
def Workload(client):
989
  """ The actual RAPI workload used for tests.
990

991
  @type client: C{GanetiRapiClientWrapper}
992
  @param client: A wrapped RAPI client.
993

994
  """
995

    
996
  # First just the simple information retrievals
997
  TestGetters(client)
998

    
999
  # Then the only remaining function which is parameter-free
1000
  Finish(client, client.RedistributeConfig)
1001

    
1002
  # Try changing the cluster parameters
1003
  TestClusterParameterModification(client)
1004

    
1005
  TestTags(client, client.GetClusterTags, client.AddClusterTags,
1006
           client.DeleteClusterTags)
1007

    
1008
  # Generously assume the master is present
1009
  node = qa_config.AcquireNode()
1010
  TestTags(client, client.GetNodeTags, client.AddNodeTags,
1011
           client.DeleteNodeTags, node.primary)
1012
  node.Release()
1013

    
1014
  # Instance tests
1015

    
1016
  # First remove all instances the QA might have created
1017
  RemoveAllInstances(client)
1018

    
1019
  nodes = qa_config.AcquireManyNodes(2)
1020
  instances = qa_config.AcquireManyInstances(2)
1021
  TestSingleInstance(client, instances[0].name, instances[1].name,
1022
                     nodes[0].primary, nodes[1].primary)
1023
  qa_config.ReleaseManyInstances(instances)
1024
  qa_config.ReleaseManyNodes(nodes)
1025

    
1026
  # Test all the queries which involve resources that do not have functions
1027
  # of their own
1028
  TestQueries(client, "lock")
1029
  TestQueries(client, "job")
1030
  TestQueries(client, "export")
1031

    
1032
  node = qa_config.AcquireNode(exclude=qa_config.GetMasterNode())
1033
  TestNodeOperations(client, node.primary)
1034
  TestQueryFiltering(client, node.primary)
1035
  node.Release()
1036

    
1037
  nodes = qa_config.AcquireManyNodes(2)
1038
  TestGroupOperations(client, nodes[0].primary, nodes[1].primary)
1039
  qa_config.ReleaseManyNodes(nodes)
1040

    
1041
  TestNetworks(client)
1042

    
1043
  nodes = qa_config.AcquireManyNodes(3)
1044
  instance = qa_config.AcquireInstance()
1045
  TestInstanceMigrations(client, nodes[0].primary, nodes[1].primary,
1046
                         nodes[2].primary, instance.name)
1047
  instance.Release()
1048
  qa_config.ReleaseManyNodes(nodes)
1049

    
1050
  nodes = qa_config.AcquireManyNodes(2)
1051
  instances = qa_config.AcquireManyInstances(2)
1052
  TestInstanceMoves(client, nodes[0], nodes[1], instances[0], instances[1])
1053
  TestJobCancellation(client, nodes[0].primary, nodes[1].primary,
1054
                      instances[0].name, instances[1].name)
1055
  qa_config.ReleaseManyInstances(instances)
1056
  qa_config.ReleaseManyNodes(nodes)
1057

    
1058

    
1059
def Main():
1060
  parser = optparse.OptionParser(usage="%prog [options] <config-file>")
1061
  parser.add_option("--yes-do-it", dest="yes_do_it",
1062
                    action="store_true",
1063
                    help="Really execute the tests")
1064
  parser.add_option("--show-invocations", dest="show_invocations",
1065
                    action="store_true",
1066
                    help="Show which client methods have and have not been "
1067
                         "called")
1068
  (opts, args) = parser.parse_args()
1069

    
1070
  if not opts.yes_do_it:
1071
    print ("Executing this script irreversibly destroys any Ganeti\n"
1072
           "configuration on all nodes involved. If you really want\n"
1073
           "to start testing, supply the --yes-do-it option.")
1074
    sys.exit(1)
1075

    
1076
  qa_config.Load(args[0])
1077

    
1078
  # Only the master will be present after a fresh QA cluster setup, so we have
1079
  # to invoke this to get all the other nodes.
1080
  qa_node.TestNodeAddAll()
1081

    
1082
  client = GanetiRapiClientWrapper()
1083

    
1084
  Workload(client)
1085

    
1086
  qa_node.TestNodeRemoveAll()
1087

    
1088
  # The method invoked has the naming of the protected method, and pylint does
1089
  # not like this. Disabling the warning is healthier than explicitly adding and
1090
  # maintaining an exception for this method in the wrapper.
1091
  if opts.show_invocations:
1092
    # pylint: disable=W0212
1093
    client._OutputMethodInvocationDetails()
1094
    # pylint: enable=W0212
1095

    
1096
if __name__ == "__main__":
1097
  Main()