Statistics
| Branch: | Tag: | Revision:

root / qa / rapi-workload.py @ 396c5dfb

History | View | Annotate | Download (22.1 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
35

    
36
import qa_config
37
import qa_node
38
import qa_rapi
39

    
40

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

    
65

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

69
  """
70
  return None
71

    
72

    
73
RAPI_USERNAME = "ganeti-qa"
74

    
75

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

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

    
85
    self._method_invocations = {}
86

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

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

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

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

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

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

    
122
    return decoratedFn
123

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

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

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

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

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

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

    
169

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

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

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

    
193
  success = client.WaitForJobCompletion(possible_job_id)
194

    
195
  result = client.GetJobStatus(possible_job_id)["opresult"][0]
196
  if success:
197
    return result
198
  else:
199
    print "Error encountered while performing operation: "
200
    print result
201
    return None
202

    
203

    
204
def TestTags(client, get_fn, add_fn, delete_fn, *args):
205
  """ Tests whether tagging works.
206

207
  @type client C{GanetiRapiClientWrapper}
208
  @param client The client wrapper.
209
  @type get_fn function
210
  @param get_fn A Get*Tags function of the client.
211
  @type add_fn function
212
  @param add_fn An Add*Tags function of the client.
213
  @type delete_fn function
214
  @param delete_fn A Delete*Tags function of the client.
215

216
  To allow this method to work for all tagging functions of the client, use
217
  named methods.
218

219
  """
220
  get_fn(*args)
221

    
222
  tags = ["tag1", "tag2", "tag3"]
223
  Finish(client, add_fn, *args, tags=tags, dry_run=True)
224
  Finish(client, add_fn, *args, tags=tags)
225

    
226
  get_fn(*args)
227

    
228
  Finish(client, delete_fn, *args, tags=tags[:1], dry_run=True)
229
  Finish(client, delete_fn, *args, tags=tags[:1])
230

    
231
  get_fn(*args)
232

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

    
235
  get_fn(*args)
236

    
237

    
238
def TestGetters(client):
239
  """ Tests the various get functions which only retrieve information about the
240
  cluster.
241

242
  @type client C{GanetiRapiClientWrapper}
243

244
  """
245
  client.GetVersion()
246
  client.GetFeatures()
247
  client.GetOperatingSystems()
248
  client.GetInfo()
249
  client.GetClusterTags()
250
  client.GetInstances()
251
  client.GetInstances(bulk=True)
252
  client.GetJobs()
253
  client.GetJobs(bulk=True)
254
  client.GetNodes()
255
  client.GetNodes(bulk=True)
256
  client.GetNetworks()
257
  client.GetNetworks(bulk=True)
258
  client.GetGroups()
259
  client.GetGroups(bulk=True)
260

    
261

    
262
def TestQueries(client, resource_name):
263
  """ Finds out which fields are present for a given resource type, and attempts
264
  to retrieve their values for all present resources.
265

266
  @type client C{GanetiRapiClientWrapper}
267
  @param client A wrapped RAPI client.
268
  @type resource_name string
269
  @param resource_name The name of the resource to use.
270

271
  """
272

    
273
  FIELDS_KEY = "fields"
274

    
275
  query_res = client.QueryFields(resource_name)
276

    
277
  if query_res is None or FIELDS_KEY not in query_res or \
278
    len(query_res[FIELDS_KEY]) == 0:
279
    return
280

    
281
  field_entries = query_res[FIELDS_KEY]
282

    
283
  fields = map(lambda e: e["name"], field_entries)
284

    
285
  client.Query(resource_name, fields)
286

    
287

    
288
def TestQueryFiltering(client, master_name):
289
  """ Performs queries by playing around with the only guaranteed resource, the
290
  master node.
291

292
  @type client C{GanetiRapiClientWrapper}
293
  @param client A wrapped RAPI client.
294
  @type master_name string
295
  @param master_name The hostname of the master node.
296

297
  """
298
  client.Query("node", ["name"],
299
               ["|",
300
                ["=", "name", master_name],
301
                [">", "dtotal", 0],
302
               ])
303

    
304
  client.Query("instance", ["name"],
305
               ["|",
306
                ["=", "name", "NonexistentInstance"],
307
                [">", "oper_ram", 0],
308
               ])
309

    
310

    
311
def RemoveAllInstances(client):
312
  """ Queries for a list of instances, then removes them all.
313

314
  @type client C{GanetiRapiClientWrapper}
315
  @param client A wrapped RAPI client.
316

317
  """
318
  instances = client.GetInstances()
319
  for inst in instances:
320
    Finish(client, client.DeleteInstance, inst)
321

    
322
  instances = client.GetInstances()
323
  assert len(instances) == 0
324

    
325

    
326
def TestSingleInstance(client, instance_name, alternate_name, node_one,
327
                       node_two):
328
  """ Creates an instance, performs operations involving it, and then deletes
329
  it.
330

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

342
  """
343

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

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

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

    
359
  Finish(client, client.DeleteInstance, instance_name)
360

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

    
366
  client.GetInstance(instance_name)
367

    
368
  Finish(client, client.GetInstanceInfo, instance_name)
369

    
370
  Finish(client, client.GetInstanceInfo, instance_name, static=True)
371

    
372
  TestQueries(client, "instance")
373

    
374
  TestTags(client, client.GetInstanceTags, client.AddInstanceTags,
375
           client.DeleteInstanceTags, instance_name)
376

    
377
  Finish(client, client.GrowInstanceDisk,
378
         instance_name, 0, 100, wait_for_sync=True)
379

    
380
  Finish(client, client.RebootInstance,
381
         instance_name, "soft", ignore_secondaries=True, dry_run=True,
382
         reason="Hulk smash gently!")
383

    
384
  Finish(client, client.ShutdownInstance,
385
         instance_name, dry_run=True, no_remember=False,
386
         reason="Hulk smash hard!")
387

    
388
  Finish(client, client.StartupInstance,
389
         instance_name, dry_run=True, no_remember=False,
390
         reason="Not hard enough!")
391

    
392
  Finish(client, client.RebootInstance,
393
         instance_name, "soft", ignore_secondaries=True, dry_run=False)
394

    
395
  Finish(client, client.ShutdownInstance,
396
         instance_name, dry_run=False, no_remember=False)
397

    
398
  Finish(client, client.ModifyInstance,
399
         instance_name, disk_template="drbd", remote_node=node_two)
400

    
401
  Finish(client, client.ModifyInstance,
402
         instance_name, disk_template="plain")
403

    
404
  Finish(client, client.RenameInstance,
405
         instance_name, alternate_name, ip_check=True, name_check=True)
406

    
407
  Finish(client, client.RenameInstance, alternate_name, instance_name)
408

    
409
  Finish(client, client.DeactivateInstanceDisks, instance_name)
410

    
411
  Finish(client, client.ActivateInstanceDisks, instance_name)
412

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

    
419
  Finish(client, client.StartupInstance,
420
         instance_name, dry_run=False, no_remember=False)
421

    
422
  client.GetInstanceConsole(instance_name)
423

    
424
  Finish(client, client.ReinstallInstance,
425
         instance_name, os=None, no_startup=False, osparams={})
426

    
427
  Finish(client, client.DeleteInstance, instance_name, dry_run=True)
428

    
429
  Finish(client, client.DeleteInstance, instance_name)
430

    
431

    
432
def MarkUnmarkNode(client, node, state):
433
  """ Given a certain node state, marks a node as being in that state, and then
434
  unmarks it.
435

436
  @type client C{GanetiRapiClientWrapper}
437
  @param client A wrapped RAPI client.
438
  @type node string
439
  @type state string
440

441
  """
442
  # pylint: disable=W0142
443
  Finish(client, client.ModifyNode, node, **{state: True})
444
  Finish(client, client.ModifyNode, node, **{state: False})
445
  # pylint: enable=W0142
446

    
447

    
448
def TestNodeOperations(client, non_master_node):
449
  """ Tests various operations related to nodes only
450

451
  @type client C{GanetiRapiClientWrapper}
452
  @param client A wrapped RAPI client.
453
  @type non_master_node string
454
  @param non_master_node The name of a non-master node in the cluster.
455

456
  """
457

    
458
  client.GetNode(non_master_node)
459

    
460
  old_role = client.GetNodeRole(non_master_node)
461

    
462
  # Should fail
463
  Finish(client, client.SetNodeRole,
464
         non_master_node, "master", False, auto_promote=True)
465

    
466
  Finish(client, client.SetNodeRole,
467
         non_master_node, "regular", False, auto_promote=True)
468

    
469
  Finish(client, client.SetNodeRole,
470
         non_master_node, "master-candidate", False, auto_promote=True)
471

    
472
  Finish(client, client.SetNodeRole,
473
         non_master_node, "drained", False, auto_promote=True)
474

    
475
  Finish(client, client.SetNodeRole,
476
         non_master_node, old_role, False, auto_promote=True)
477

    
478
  Finish(client, client.PowercycleNode,
479
         non_master_node, force=False)
480

    
481
  storage_units_fields = [
482
    "name", "allocatable", "free", "node", "size", "type", "used",
483
  ]
484

    
485
  for storage_type in constants.STS_REPORT:
486
    storage_units = Finish(client, client.GetNodeStorageUnits,
487
                           non_master_node, storage_type,
488
                           ",".join(storage_units_fields))
489

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

    
502
  MarkUnmarkNode(client, non_master_node, "drained")
503
  MarkUnmarkNode(client, non_master_node, "powered")
504
  MarkUnmarkNode(client, non_master_node, "offline")
505

    
506
  TestQueries(client, "node")
507

    
508

    
509
def TestGroupOperations(client, node, another_node):
510
  """ Tests various operations related to groups only.
511

512
  @type client C{GanetiRapiClientWrapper}
513
  @param client A Ganeti RAPI client to use.
514
  @type node string
515
  @param node The name of a node in the cluster.
516
  @type another_node string
517
  @param another_node The name of another node in the cluster.
518

519
  """
520

    
521
  DEFAULT_GROUP_NAME = constants.INITIAL_NODE_GROUP_NAME
522
  TEST_GROUP_NAME = "TestGroup"
523
  ALTERNATE_GROUP_NAME = "RenamedTestGroup"
524

    
525
  Finish(client, client.CreateGroup,
526
         TEST_GROUP_NAME, alloc_policy=constants.ALLOC_POLICY_PREFERRED,
527
         dry_run=True)
528

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

    
532
  client.GetGroup(TEST_GROUP_NAME)
533

    
534
  TestQueries(client, "group")
535

    
536
  TestTags(client, client.GetGroupTags, client.AddGroupTags,
537
           client.DeleteGroupTags, TEST_GROUP_NAME)
538

    
539
  Finish(client, client.ModifyGroup,
540
         TEST_GROUP_NAME, alloc_policy=constants.ALLOC_POLICY_PREFERRED,
541
         depends=None)
542

    
543
  Finish(client, client.AssignGroupNodes,
544
         TEST_GROUP_NAME, [node, another_node], force=False, dry_run=True)
545

    
546
  Finish(client, client.AssignGroupNodes,
547
         TEST_GROUP_NAME, [another_node], force=False)
548

    
549
  Finish(client, client.RenameGroup,
550
         TEST_GROUP_NAME, ALTERNATE_GROUP_NAME)
551

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

    
555
  Finish(client, client.AssignGroupNodes,
556
         DEFAULT_GROUP_NAME, [another_node], force=False)
557

    
558
  Finish(client, client.DeleteGroup, TEST_GROUP_NAME, dry_run=True)
559

    
560
  Finish(client, client.DeleteGroup, TEST_GROUP_NAME)
561

    
562

    
563
def TestNetworkConnectDisconnect(client, network_name, mode, link):
564
  """ Test connecting and disconnecting the network to a new node group.
565

566
  @type network_name string
567
  @param network_name The name of an existing and unconnected network.
568
  @type mode string
569
  @param mode The network mode.
570
  @type link string
571
  @param link The network link.
572

573
  """
574
  # For testing the connect/disconnect calls, a group is needed
575
  TEST_GROUP_NAME = "TestGroup"
576
  Finish(client, client.CreateGroup,
577
         TEST_GROUP_NAME, alloc_policy=constants.ALLOC_POLICY_PREFERRED)
578

    
579
  Finish(client, client.ConnectNetwork,
580
         network_name, TEST_GROUP_NAME, mode, link, dry_run=True)
581

    
582
  Finish(client, client.ConnectNetwork,
583
         network_name, TEST_GROUP_NAME, mode, link)
584

    
585
  Finish(client, client.DisconnectNetwork,
586
         network_name, TEST_GROUP_NAME, dry_run=True)
587

    
588
  Finish(client, client.DisconnectNetwork,
589
         network_name, TEST_GROUP_NAME)
590

    
591
  # Clean up the group
592
  Finish(client, client.DeleteGroup, TEST_GROUP_NAME)
593

    
594

    
595
def TestNetworks(client):
596
  """ Add some networks of different sizes, using RFC5737 addresses like in the
597
  QA.
598

599
  """
600

    
601
  NETWORK_NAME = "SurelyCertainlyNonexistentNetwork"
602

    
603
  Finish(client, client.CreateNetwork,
604
         NETWORK_NAME, "192.0.2.0/30", tags=[], dry_run=True)
605

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

    
609
  client.GetNetwork(NETWORK_NAME)
610

    
611
  TestTags(client, client.GetNetworkTags, client.AddNetworkTags,
612
           client.DeleteNetworkTags, NETWORK_NAME)
613

    
614
  Finish(client, client.ModifyNetwork,
615
         NETWORK_NAME, mac_prefix=None)
616

    
617
  TestQueries(client, "network")
618

    
619
  default_nicparams = qa_config.get("default-nicparams", None)
620

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

    
628
  # Clean up the network
629
  Finish(client, client.DeleteNetwork,
630
         NETWORK_NAME, dry_run=True)
631

    
632
  Finish(client, client.DeleteNetwork, NETWORK_NAME)
633

    
634

    
635
def Workload(client):
636
  """ The actual RAPI workload used for tests.
637

638
  @type client C{GanetiRapiClientWrapper}
639
  @param client A wrapped RAPI client.
640

641
  """
642

    
643
  # First just the simple information retrievals
644
  TestGetters(client)
645

    
646
  # Then the only remaining function which is parameter-free
647
  Finish(client, client.RedistributeConfig)
648

    
649
  TestTags(client, client.GetClusterTags, client.AddClusterTags,
650
           client.DeleteClusterTags)
651

    
652
  # Generously assume the master is present
653
  node = qa_config.AcquireNode()
654
  TestTags(client, client.GetNodeTags, client.AddNodeTags,
655
           client.DeleteNodeTags, node.primary)
656
  node.Release()
657

    
658
  # Instance tests
659

    
660
  # First remove all instances the QA might have created
661
  RemoveAllInstances(client)
662

    
663
  nodes = qa_config.AcquireManyNodes(2)
664
  instance_one = qa_config.AcquireInstance()
665
  instance_two = qa_config.AcquireInstance()
666
  TestSingleInstance(client, instance_one.name, instance_two.name,
667
                     nodes[0].primary, nodes[1].primary)
668
  instance_two.Release()
669
  instance_one.Release()
670
  qa_config.ReleaseManyNodes(nodes)
671

    
672
  # Test all the queries which involve resources that do not have functions
673
  # of their own
674
  TestQueries(client, "lock")
675
  TestQueries(client, "job")
676
  TestQueries(client, "export")
677

    
678
  node = qa_config.AcquireNode(exclude=qa_config.GetMasterNode())
679
  TestNodeOperations(client, node.primary)
680
  TestQueryFiltering(client, node.primary)
681
  node.Release()
682

    
683
  nodes = qa_config.AcquireManyNodes(2)
684
  TestGroupOperations(client, nodes[0].primary, nodes[1].primary)
685
  qa_config.ReleaseManyNodes(nodes)
686

    
687
  TestNetworks(client)
688

    
689

    
690
def Usage():
691
  sys.stderr.write("Usage:\n\trapi-workload.py qa-config-file")
692

    
693

    
694
def Main():
695
  if len(sys.argv) < 2:
696
    Usage()
697

    
698
  qa_config.Load(sys.argv[1])
699

    
700
  # Only the master will be present after a fresh QA cluster setup, so we have
701
  # to invoke this to get all the other nodes.
702
  qa_node.TestNodeAddAll()
703

    
704
  client = GanetiRapiClientWrapper()
705

    
706
  Workload(client)
707

    
708
  qa_node.TestNodeRemoveAll()
709

    
710
  # The method invoked has the naming of the protected method, and pylint does
711
  # not like this. Disabling the warning is healthier than explicitly adding and
712
  # maintaining an exception for this method in the wrapper.
713
  # pylint: disable=W0212
714
  client._OutputMethodInvocationDetails()
715
  # pylint: enable=W0212
716

    
717
if __name__ == "__main__":
718
  Main()