Statistics
| Branch: | Tag: | Revision:

root / qa / rapi-workload.py @ dd2bc9b6

History | View | Annotate | Download (34.6 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 sys
31
import types
32

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

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

    
41

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

    
66

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

70
  """
71
  return None
72

    
73

    
74
RAPI_USERNAME = "ganeti-qa"
75

    
76

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

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

    
86
    self._method_invocations = {}
87

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

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

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

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

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

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

    
123
    return decoratedFn
124

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

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

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

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

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

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

    
170

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

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

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

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

    
197
  success = client.WaitForJobCompletion(possible_job_id)
198

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

    
207

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

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

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

223
  """
224
  get_fn(*args)
225

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

    
230
  get_fn(*args)
231

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

    
235
  get_fn(*args)
236

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

    
239
  get_fn(*args)
240

    
241

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

246
  @type client C{GanetiRapiClientWrapper}
247

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

    
265

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

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

275
  """
276

    
277
  FIELDS_KEY = "fields"
278

    
279
  query_res = client.QueryFields(resource_name)
280

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

    
285
  field_entries = query_res[FIELDS_KEY]
286

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

    
289
  client.Query(resource_name, fields)
290

    
291

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

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

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

    
308
  client.Query("instance", ["name"],
309
               ["|",
310
                ["=", "name", "NonexistentInstance"],
311
                [">", "oper_ram", 0],
312
               ])
313

    
314

    
315
def RemoveAllInstances(client):
316
  """ Queries for a list of instances, then removes them all.
317

318
  @type client C{GanetiRapiClientWrapper}
319
  @param client A wrapped RAPI client.
320

321
  """
322
  instances = client.GetInstances()
323
  for inst in instances:
324
    Finish(client, client.DeleteInstance, inst)
325

    
326
  instances = client.GetInstances()
327
  assert len(instances) == 0
328

    
329

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

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

346
  """
347

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

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

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

    
363
  Finish(client, client.DeleteInstance, instance_name)
364

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

    
370
  client.GetInstance(instance_name)
371

    
372
  Finish(client, client.GetInstanceInfo, instance_name)
373

    
374
  Finish(client, client.GetInstanceInfo, instance_name, static=True)
375

    
376
  TestQueries(client, "instance")
377

    
378
  TestTags(client, client.GetInstanceTags, client.AddInstanceTags,
379
           client.DeleteInstanceTags, instance_name)
380

    
381
  Finish(client, client.GrowInstanceDisk,
382
         instance_name, 0, 100, wait_for_sync=True)
383

    
384
  Finish(client, client.RebootInstance,
385
         instance_name, "soft", ignore_secondaries=True, dry_run=True,
386
         reason="Hulk smash gently!")
387

    
388
  Finish(client, client.ShutdownInstance,
389
         instance_name, dry_run=True, no_remember=False,
390
         reason="Hulk smash hard!")
391

    
392
  Finish(client, client.StartupInstance,
393
         instance_name, dry_run=True, no_remember=False,
394
         reason="Not hard enough!")
395

    
396
  Finish(client, client.RebootInstance,
397
         instance_name, "soft", ignore_secondaries=True, dry_run=False)
398

    
399
  Finish(client, client.ShutdownInstance,
400
         instance_name, dry_run=False, no_remember=False)
401

    
402
  Finish(client, client.ModifyInstance,
403
         instance_name, disk_template="drbd", remote_node=node_two)
404

    
405
  Finish(client, client.ModifyInstance,
406
         instance_name, disk_template="plain")
407

    
408
  Finish(client, client.RenameInstance,
409
         instance_name, alternate_name, ip_check=True, name_check=True)
410

    
411
  Finish(client, client.RenameInstance, alternate_name, instance_name)
412

    
413
  Finish(client, client.DeactivateInstanceDisks, instance_name)
414

    
415
  Finish(client, client.ActivateInstanceDisks, instance_name)
416

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

    
423
  Finish(client, client.StartupInstance,
424
         instance_name, dry_run=False, no_remember=False)
425

    
426
  client.GetInstanceConsole(instance_name)
427

    
428
  Finish(client, client.ReinstallInstance,
429
         instance_name, os=None, no_startup=False, osparams={})
430

    
431
  Finish(client, client.DeleteInstance, instance_name, dry_run=True)
432

    
433
  Finish(client, client.DeleteInstance, instance_name)
434

    
435

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

440
  @type client C{GanetiRapiClientWrapper}
441
  @param client A wrapped RAPI client.
442
  @type node string
443
  @type state string
444

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

    
451

    
452
def TestNodeOperations(client, non_master_node):
453
  """ Tests various operations related to nodes only
454

455
  @type client C{GanetiRapiClientWrapper}
456
  @param client A wrapped RAPI client.
457
  @type non_master_node string
458
  @param non_master_node The name of a non-master node in the cluster.
459

460
  """
461

    
462
  client.GetNode(non_master_node)
463

    
464
  old_role = client.GetNodeRole(non_master_node)
465

    
466
  # Should fail
467
  Finish(client, client.SetNodeRole,
468
         non_master_node, "master", False, auto_promote=True)
469

    
470
  Finish(client, client.SetNodeRole,
471
         non_master_node, "regular", False, auto_promote=True)
472

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

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

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

    
482
  Finish(client, client.PowercycleNode,
483
         non_master_node, force=False)
484

    
485
  storage_units_fields = [
486
    "name", "allocatable", "free", "node", "size", "type", "used",
487
  ]
488

    
489
  for storage_type in constants.STS_REPORT:
490
    success, storage_units = Finish(client, client.GetNodeStorageUnits,
491
                                    non_master_node, storage_type,
492
                                    ",".join(storage_units_fields))
493

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

    
506
  MarkUnmarkNode(client, non_master_node, "drained")
507
  MarkUnmarkNode(client, non_master_node, "powered")
508
  MarkUnmarkNode(client, non_master_node, "offline")
509

    
510
  TestQueries(client, "node")
511

    
512

    
513
def TestGroupOperations(client, node, another_node):
514
  """ Tests various operations related to groups only.
515

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

523
  """
524

    
525
  DEFAULT_GROUP_NAME = constants.INITIAL_NODE_GROUP_NAME
526
  TEST_GROUP_NAME = "TestGroup"
527
  ALTERNATE_GROUP_NAME = "RenamedTestGroup"
528

    
529
  Finish(client, client.CreateGroup,
530
         TEST_GROUP_NAME, alloc_policy=constants.ALLOC_POLICY_PREFERRED,
531
         dry_run=True)
532

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

    
536
  client.GetGroup(TEST_GROUP_NAME)
537

    
538
  TestQueries(client, "group")
539

    
540
  TestTags(client, client.GetGroupTags, client.AddGroupTags,
541
           client.DeleteGroupTags, TEST_GROUP_NAME)
542

    
543
  Finish(client, client.ModifyGroup,
544
         TEST_GROUP_NAME, alloc_policy=constants.ALLOC_POLICY_PREFERRED,
545
         depends=None)
546

    
547
  Finish(client, client.AssignGroupNodes,
548
         TEST_GROUP_NAME, [node, another_node], force=False, dry_run=True)
549

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

    
553
  Finish(client, client.RenameGroup,
554
         TEST_GROUP_NAME, ALTERNATE_GROUP_NAME)
555

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

    
559
  Finish(client, client.AssignGroupNodes,
560
         DEFAULT_GROUP_NAME, [another_node], force=False)
561

    
562
  Finish(client, client.DeleteGroup, TEST_GROUP_NAME, dry_run=True)
563

    
564
  Finish(client, client.DeleteGroup, TEST_GROUP_NAME)
565

    
566

    
567
def TestNetworkConnectDisconnect(client, network_name, mode, link):
568
  """ Test connecting and disconnecting the network to a new node group.
569

570
  @type network_name string
571
  @param network_name The name of an existing and unconnected network.
572
  @type mode string
573
  @param mode The network mode.
574
  @type link string
575
  @param link The network link.
576

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

    
583
  Finish(client, client.ConnectNetwork,
584
         network_name, TEST_GROUP_NAME, mode, link, dry_run=True)
585

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

    
589
  Finish(client, client.DisconnectNetwork,
590
         network_name, TEST_GROUP_NAME, dry_run=True)
591

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

    
595
  # Clean up the group
596
  Finish(client, client.DeleteGroup, TEST_GROUP_NAME)
597

    
598

    
599
def TestNetworks(client):
600
  """ Add some networks of different sizes, using RFC5737 addresses like in the
601
  QA.
602

603
  """
604

    
605
  NETWORK_NAME = "SurelyCertainlyNonexistentNetwork"
606

    
607
  Finish(client, client.CreateNetwork,
608
         NETWORK_NAME, "192.0.2.0/30", tags=[], dry_run=True)
609

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

    
613
  client.GetNetwork(NETWORK_NAME)
614

    
615
  TestTags(client, client.GetNetworkTags, client.AddNetworkTags,
616
           client.DeleteNetworkTags, NETWORK_NAME)
617

    
618
  Finish(client, client.ModifyNetwork,
619
         NETWORK_NAME, mac_prefix=None)
620

    
621
  TestQueries(client, "network")
622

    
623
  default_nicparams = qa_config.get("default-nicparams", None)
624

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

    
632
  # Clean up the network
633
  Finish(client, client.DeleteNetwork,
634
         NETWORK_NAME, dry_run=True)
635

    
636
  Finish(client, client.DeleteNetwork, NETWORK_NAME)
637

    
638

    
639
def CreateDRBDInstance(client, node_one, node_two, instance_name):
640
  """ Creates a DRBD-enabled instance on the given nodes.
641

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

    
647

    
648
def TestInstanceMigrations(client, node_one, node_two, node_three,
649
                           instance_name):
650
  """ Test various operations related to migrating instances.
651

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

661
  """
662

    
663
  CreateDRBDInstance(client, node_one, node_two, instance_name)
664
  Finish(client, client.FailoverInstance, instance_name)
665
  Finish(client, client.DeleteInstance, instance_name)
666

    
667
  CreateDRBDInstance(client, node_one, node_two, instance_name)
668
  Finish(client, client.EvacuateNode,
669
         node_two, early_release=False, mode=NODE_EVAC_SEC,
670
         remote_node=node_three)
671
  Finish(client, client.DeleteInstance, instance_name)
672

    
673
  CreateDRBDInstance(client, node_one, node_two, instance_name)
674
  Finish(client, client.EvacuateNode,
675
         node_one, early_release=False, mode=NODE_EVAC_PRI, iallocator="hail")
676
  Finish(client, client.DeleteInstance, instance_name)
677

    
678
  CreateDRBDInstance(client, node_one, node_two, instance_name)
679
  Finish(client, client.MigrateInstance,
680
         instance_name, cleanup=True, target_node=node_two)
681
  Finish(client, client.DeleteInstance, instance_name)
682

    
683
  CreateDRBDInstance(client, node_one, node_two, instance_name)
684
  Finish(client, client.MigrateNode,
685
         node_one, iallocator="hail", mode="non-live")
686
  Finish(client, client.DeleteInstance, instance_name)
687

    
688
  CreateDRBDInstance(client, node_one, node_two, instance_name)
689
  Finish(client, client.MigrateNode,
690
         node_one, target_node=node_two, mode="non-live")
691
  Finish(client, client.DeleteInstance, instance_name)
692

    
693

    
694
def ExtractAllNicInformationPossible(nics, replace_macs=True):
695
  """ Extracts NIC information as a dictionary.
696

697
  @type nics list of tuples of varying structure
698
  @param nics The network interfaces, as received from the instance info RAPI
699
              call.
700

701
  @rtype list of dict
702
  @return Dictionaries of NIC information.
703

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

708
  """
709

    
710
  desired_entries = [
711
    constants.INIC_IP,
712
    constants.INIC_MAC,
713
    constants.INIC_MODE,
714
    constants.INIC_LINK,
715
    constants.INIC_VLAN,
716
    constants.INIC_NETWORK,
717
    constants.INIC_NAME,
718
    ]
719

    
720
  nic_dicts = []
721
  for nic_index in range(len(nics)):
722
    nic_raw_data = nics[nic_index]
723

    
724
    # Fill dictionary with None-s as defaults
725
    nic_dict = dict([(key, None) for key in desired_entries])
726

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

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

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

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

    
752
    nic_dicts.append(nic_dict)
753

    
754
  return nic_dicts
755

    
756

    
757
def MoveInstance(client, src_instance, dst_instance, src_node, dst_node):
758
  """ Moves a single instance, compatible with 2.6.
759

760
  @rtype bool
761
  @return Whether the instance was moved successfully
762

763
  """
764
  success, inst_info_all = Finish(client, client.GetInstanceInfo,
765
                                  src_instance.name, static=True)
766

    
767
  if not success or src_instance.name not in inst_info_all:
768
    raise Exception("Did not find the source instance information!")
769

    
770
  inst_info = inst_info_all[src_instance.name]
771

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

    
779
  NIC_COMPONENTS_26 = [
780
    constants.INIC_IP,
781
    constants.INIC_MAC,
782
    constants.INIC_MODE,
783
    constants.INIC_LINK,
784
    ]
785

    
786
  nic_converter = lambda old: dict((k, old[k]) for k in NIC_COMPONENTS_26)
787
  nics = map(nic_converter, nic_info)
788

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

    
797
    spindles = idisk.get("spindles")
798
    if spindles is not None:
799
      odisk[constants.IDISK_SPINDLES] = spindles
800

    
801
    # Disk name may be present, but must not be supplied in 2.6!
802
    disks.append(odisk)
803

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

    
808
  if not success:
809
    # The instance will still have to be deleted
810
    return False
811

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

    
829
  return success
830

    
831

    
832
def CreateInstanceForMoveTest(client, node, instance):
833
  """ Creates a single shutdown instance to move about in tests.
834

835
  @type node C{_QaNode}
836
  @param node A node configuration object.
837
  @type instance C{_QaInstance}
838
  @param instance An instance configuration object.
839

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

    
845
  Finish(client, client.ShutdownInstance,
846
         instance.name, dry_run=False, no_remember=False)
847

    
848

    
849
def Test26InstanceMove(client, node_one, node_two, instance_to_create,
850
                       new_instance):
851
  """ Tests instance moves using commands that work in 2.6.
852

853
  """
854

    
855
  # First create the instance to move
856
  CreateInstanceForMoveTest(client, node_one, instance_to_create)
857

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

    
864

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

869
  """
870

    
871
  # First create the instance to move
872
  CreateInstanceForMoveTest(client, node_one, instance_to_create)
873

    
874
  instance_to_create.SetDiskTemplate("plain")
875

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

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

    
888

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

894
  @type node_one C{_QaNode}
895
  @param node_one A node configuration object.
896
  @type node_two C{_QaNode}
897
  @param node_two A node configuration object.
898
  @type instance_to_create C{_QaInstance}
899
  @param instance_to_create An instance configuration object.
900
  @type new_instance C{_QaInstance}
901
  @param new_instance An instance configuration object.
902

903
  """
904

    
905
  Test26InstanceMove(client, node_one, node_two, instance_to_create,
906
                     new_instance)
907
  Test211InstanceMove(client, node_one, node_two, instance_to_create,
908
                      new_instance)
909

    
910

    
911
def TestClusterParameterModification(client):
912
  """ Try to modify some of the cluster parameters using RAPI.
913

914
  """
915
  cluster_info = client.GetInfo()
916

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

    
924
  for attribute, options in ATTRIBUTES_TO_MODIFY:
925
    current_value = cluster_info[attribute]
926

    
927
    if current_value in options:
928
      value_to_use = options[1 - options.index(current_value)]
929
    else:
930
      value_to_use = options[0]
931

    
932
    #pylint: disable=W0142
933
    Finish(client, client.ModifyCluster, **{attribute: value_to_use})
934
    Finish(client, client.ModifyCluster, **{attribute: current_value})
935
    #pylint: enable=W0142
936

    
937

    
938
def TestJobCancellation(client, node_one, node_two, instance_one, instance_two):
939
  """ Test if jobs can be cancelled.
940

941
  @type node_one string
942
  @param node_one The name of a node in the cluster.
943
  @type node_two string
944
  @param node_two The name of a node in the cluster.
945
  @type instance_one string
946
  @param instance_one An available instance name.
947
  @type instance_two string
948
  @param instance_two An available instance name.
949

950
  """
951

    
952
  # Just in case, remove all previously present instances
953
  RemoveAllInstances(client)
954

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

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

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

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

    
978
  # And wait for the proper job
979
  client.WaitForJobCompletion(running_job)
980

    
981
  # Remove all the leftover instances, success or no success
982
  RemoveAllInstances(client)
983

    
984

    
985
def Workload(client):
986
  """ The actual RAPI workload used for tests.
987

988
  @type client C{GanetiRapiClientWrapper}
989
  @param client A wrapped RAPI client.
990

991
  """
992

    
993
  # First just the simple information retrievals
994
  TestGetters(client)
995

    
996
  # Then the only remaining function which is parameter-free
997
  Finish(client, client.RedistributeConfig)
998

    
999
  # Try changing the cluster parameters
1000
  TestClusterParameterModification(client)
1001

    
1002
  TestTags(client, client.GetClusterTags, client.AddClusterTags,
1003
           client.DeleteClusterTags)
1004

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

    
1011
  # Instance tests
1012

    
1013
  # First remove all instances the QA might have created
1014
  RemoveAllInstances(client)
1015

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

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

    
1029
  node = qa_config.AcquireNode(exclude=qa_config.GetMasterNode())
1030
  TestNodeOperations(client, node.primary)
1031
  TestQueryFiltering(client, node.primary)
1032
  node.Release()
1033

    
1034
  nodes = qa_config.AcquireManyNodes(2)
1035
  TestGroupOperations(client, nodes[0].primary, nodes[1].primary)
1036
  qa_config.ReleaseManyNodes(nodes)
1037

    
1038
  TestNetworks(client)
1039

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

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

    
1055

    
1056
def Usage():
1057
  sys.stderr.write("Usage:\n\trapi-workload.py qa-config-file")
1058

    
1059

    
1060
def Main():
1061
  if len(sys.argv) < 2:
1062
    Usage()
1063

    
1064
  qa_config.Load(sys.argv[1])
1065

    
1066
  # Only the master will be present after a fresh QA cluster setup, so we have
1067
  # to invoke this to get all the other nodes.
1068
  qa_node.TestNodeAddAll()
1069

    
1070
  client = GanetiRapiClientWrapper()
1071

    
1072
  Workload(client)
1073

    
1074
  qa_node.TestNodeRemoveAll()
1075

    
1076
  # The method invoked has the naming of the protected method, and pylint does
1077
  # not like this. Disabling the warning is healthier than explicitly adding and
1078
  # maintaining an exception for this method in the wrapper.
1079
  # pylint: disable=W0212
1080
  client._OutputMethodInvocationDetails()
1081
  # pylint: enable=W0212
1082

    
1083
if __name__ == "__main__":
1084
  Main()