Statistics
| Branch: | Tag: | Revision:

root / qa / rapi-workload.py @ 9578de1c

History | View | Annotate | Download (27.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_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
  @rtype tuple of bool, any object
180
  @return The success status and the result of the operation, if any
181

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

    
196
  success = client.WaitForJobCompletion(possible_job_id)
197

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

    
206

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

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

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

222
  """
223
  get_fn(*args)
224

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

    
229
  get_fn(*args)
230

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

    
234
  get_fn(*args)
235

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

    
238
  get_fn(*args)
239

    
240

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

245
  @type client C{GanetiRapiClientWrapper}
246

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

    
264

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

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

274
  """
275

    
276
  FIELDS_KEY = "fields"
277

    
278
  query_res = client.QueryFields(resource_name)
279

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

    
284
  field_entries = query_res[FIELDS_KEY]
285

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

    
288
  client.Query(resource_name, fields)
289

    
290

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

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

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

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

    
313

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

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

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

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

    
328

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

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

345
  """
346

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

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

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

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

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

    
369
  client.GetInstance(instance_name)
370

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

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

    
375
  TestQueries(client, "instance")
376

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
425
  client.GetInstanceConsole(instance_name)
426

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

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

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

    
434

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

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

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

    
450

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

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

459
  """
460

    
461
  client.GetNode(non_master_node)
462

    
463
  old_role = client.GetNodeRole(non_master_node)
464

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

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

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

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

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

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

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

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

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

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

    
509
  TestQueries(client, "node")
510

    
511

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

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

522
  """
523

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

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

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

    
535
  client.GetGroup(TEST_GROUP_NAME)
536

    
537
  TestQueries(client, "group")
538

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

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

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

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

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

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

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

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

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

    
565

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

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

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

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

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

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

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

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

    
597

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

602
  """
603

    
604
  NETWORK_NAME = "SurelyCertainlyNonexistentNetwork"
605

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

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

    
612
  client.GetNetwork(NETWORK_NAME)
613

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

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

    
620
  TestQueries(client, "network")
621

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

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

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

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

    
637

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

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

    
646

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

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

660
  """
661

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

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

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

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

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

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

    
692

    
693
def TestJobCancellation(client, node_one, node_two, instance_one, instance_two):
694
  """ Test if jobs can be cancelled.
695

696
  @type node_one string
697
  @param node_one The name of a node in the cluster.
698
  @type node_two string
699
  @param node_two The name of a node in the cluster.
700
  @type instance_one string
701
  @param instance_one An available instance name.
702
  @type instance_two string
703
  @param instance_two An available instance name.
704

705
  """
706

    
707
  # Just in case, remove all previously present instances
708
  RemoveAllInstances(client)
709

    
710
  # Let us issue a job that is sure to both succeed and last for a while
711
  running_job = client.CreateInstance("create", instance_one, "drbd",
712
                                      [{"size": "5000"}], [{}],
713
                                      os="debian-image", pnode=node_one,
714
                                      snode=node_two)
715

    
716
  # And immediately afterwards, another very similar one
717
  job_to_cancel = client.CreateInstance("create", instance_two, "drbd",
718
                                        [{"size": "5000"}], [{}],
719
                                        os="debian-image", pnode=node_one,
720
                                        snode=node_two)
721

    
722
  # Try to cancel, which should fail as the job is already running
723
  success, msg = client.CancelJob(running_job)
724
  if success:
725
    print "Job succeeded: this should not have happened as it is running!"
726
    print "Message: %s" % msg
727

    
728
  success, msg = client.CancelJob(job_to_cancel)
729
  if not success:
730
    print "Job failed: this was unexpected as it was not a dry run"
731
    print "Message: %s" % msg
732

    
733
  # And wait for the proper job
734
  client.WaitForJobCompletion(running_job)
735

    
736
  # Remove all the leftover instances, success or no success
737
  RemoveAllInstances(client)
738

    
739

    
740
def TestInstanceMoves(client, node_one, node_two, instance_to_create,
741
                      new_instance):
742
  """ Reuses a part of the QA to test instance moves.
743

744
  @type node_one C{_QaNode}
745
  @param node_one A node configuration object.
746
  @type node_two C{_QaNode}
747
  @param node_two A node configuration object.
748
  @type instance_to_create C{_QaInstance}
749
  @param instance_to_create An instance configuration object.
750
  @type new_instance C{_QaInstance}
751
  @param new_instance An instance configuration object.
752

753
  """
754

    
755
  # First create the instance to move
756
  Finish(client, client.CreateInstance,
757
         "create", instance_to_create.name, "plain", [{"size": "2000"}], [{}],
758
         os="debian-image", pnode=node_one.primary)
759

    
760
  Finish(client, client.ShutdownInstance,
761
         instance_to_create.name, dry_run=False, no_remember=False)
762

    
763
  instance_to_create.SetDiskTemplate("plain")
764

    
765
  qa_rapi.TestInterClusterInstanceMove(instance_to_create, new_instance,
766
                                       [node_one], node_two,
767
                                       perform_checks=False)
768

    
769
  # Finally, cleanup
770
  RemoveAllInstances(client)
771

    
772

    
773
def Workload(client):
774
  """ The actual RAPI workload used for tests.
775

776
  @type client C{GanetiRapiClientWrapper}
777
  @param client A wrapped RAPI client.
778

779
  """
780

    
781
  # First just the simple information retrievals
782
  TestGetters(client)
783

    
784
  # Then the only remaining function which is parameter-free
785
  Finish(client, client.RedistributeConfig)
786

    
787
  TestTags(client, client.GetClusterTags, client.AddClusterTags,
788
           client.DeleteClusterTags)
789

    
790
  # Generously assume the master is present
791
  node = qa_config.AcquireNode()
792
  TestTags(client, client.GetNodeTags, client.AddNodeTags,
793
           client.DeleteNodeTags, node.primary)
794
  node.Release()
795

    
796
  # Instance tests
797

    
798
  # First remove all instances the QA might have created
799
  RemoveAllInstances(client)
800

    
801
  nodes = qa_config.AcquireManyNodes(2)
802
  instances = qa_config.AcquireManyInstances(2)
803
  TestSingleInstance(client, instances[0].name, instances[1].name,
804
                     nodes[0].primary, nodes[1].primary)
805
  qa_config.ReleaseManyInstances(instances)
806
  qa_config.ReleaseManyNodes(nodes)
807

    
808
  # Test all the queries which involve resources that do not have functions
809
  # of their own
810
  TestQueries(client, "lock")
811
  TestQueries(client, "job")
812
  TestQueries(client, "export")
813

    
814
  node = qa_config.AcquireNode(exclude=qa_config.GetMasterNode())
815
  TestNodeOperations(client, node.primary)
816
  TestQueryFiltering(client, node.primary)
817
  node.Release()
818

    
819
  nodes = qa_config.AcquireManyNodes(2)
820
  TestGroupOperations(client, nodes[0].primary, nodes[1].primary)
821
  qa_config.ReleaseManyNodes(nodes)
822

    
823
  TestNetworks(client)
824

    
825
  nodes = qa_config.AcquireManyNodes(3)
826
  instance = qa_config.AcquireInstance()
827
  TestInstanceMigrations(client, nodes[0].primary, nodes[1].primary,
828
                         nodes[2].primary, instance.name)
829
  instance.Release()
830
  qa_config.ReleaseManyNodes(nodes)
831

    
832
  nodes = qa_config.AcquireManyNodes(2)
833
  instances = qa_config.AcquireManyInstances(2)
834
  TestInstanceMoves(client, nodes[0], nodes[1], instances[0], instances[1])
835
  qa_config.ReleaseManyInstances(instances)
836
  qa_config.ReleaseManyNodes(nodes)
837

    
838

    
839
def Usage():
840
  sys.stderr.write("Usage:\n\trapi-workload.py qa-config-file")
841

    
842

    
843
def Main():
844
  if len(sys.argv) < 2:
845
    Usage()
846

    
847
  qa_config.Load(sys.argv[1])
848

    
849
  # Only the master will be present after a fresh QA cluster setup, so we have
850
  # to invoke this to get all the other nodes.
851
  qa_node.TestNodeAddAll()
852

    
853
  client = GanetiRapiClientWrapper()
854

    
855
  Workload(client)
856

    
857
  qa_node.TestNodeRemoveAll()
858

    
859
  # The method invoked has the naming of the protected method, and pylint does
860
  # not like this. Disabling the warning is healthier than explicitly adding and
861
  # maintaining an exception for this method in the wrapper.
862
  # pylint: disable=W0212
863
  client._OutputMethodInvocationDetails()
864
  # pylint: enable=W0212
865

    
866
if __name__ == "__main__":
867
  Main()