Statistics
| Branch: | Tag: | Revision:

root / qa / rapi-workload.py @ 82a8bf3e

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

    
30
import sys
31

    
32
import ganeti.constants as constants
33
from ganeti.rapi.client import GanetiApiError
34

    
35
import qa_config
36
import qa_node
37
import qa_rapi
38

    
39

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

    
64

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

68
  """
69
  return None
70

    
71

    
72
def InvokerCreator(fn, name):
73
  """ Returns an invoker function that will invoke the given function
74
  with any arguments passed to the invoker at a later time, while
75
  catching any specific non-fatal errors we would like to know more
76
  about.
77

78
  @type fn arbitrary function
79
  @param fn The function to invoke later.
80
  @type name string
81
  @param name The name of the function, for debugging purposes.
82
  @rtype function
83

84
  """
85
  def decoratedFn(*args, **kwargs):
86
    result = None
87
    try:
88
      print "Using method %s" % name
89
      result = fn(*args, **kwargs)
90
    except GanetiApiError as e:
91
      print "RAPI error while performing function %s : %s" % \
92
            (name, str(e))
93
    return result
94

    
95
  return decoratedFn
96

    
97

    
98
RAPI_USERNAME = "ganeti-qa"
99

    
100

    
101
class GanetiRapiClientWrapper(object):
102
  """ Creates and initializes a GanetiRapiClient, and acts as a wrapper invoking
103
  only the methods that the version of the client actually uses.
104

105
  """
106
  def __init__(self):
107
    self._client = qa_rapi.Setup(RAPI_USERNAME,
108
                                 qa_rapi.LookupRapiSecret(RAPI_USERNAME))
109

    
110
  def __getattr__(self, attr):
111
    """ Fetches an attribute from the underlying client if necessary.
112

113
    """
114
    # Assuming that this method exposes no public methods of its own,
115
    # and that any private methods are named according to the style
116
    # guide, this will stop infinite loops in attribute fetches.
117
    if attr.startswith("_"):
118
      return self.__getattribute__(attr)
119
    try:
120
      return InvokerCreator(self._client.__getattribute__(attr), attr)
121
    except AttributeError:
122
      print "Missing method %s; supplying mock method" % attr
123
      return MockMethod
124

    
125

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

130
  @type client C{GanetiRapiClientWrapper}
131
  @param client The client wrapper.
132
  @type fn function
133
  @param fn A client method returning a job id.
134

135
  """
136
  possible_job_id = fn(*args, **kwargs)
137
  try:
138
    # The job ids are returned as both ints and ints represented by strings.
139
    # This is a pythonic check to see if the content is an int.
140
    int(possible_job_id)
141
  except (ValueError, TypeError):
142
    # As a rule of thumb, failures will return None, and other methods are
143
    # expected to return at least something
144
    if possible_job_id is not None:
145
      print ("Finish called with a method not producing a job id, "
146
             "returning %s" % possible_job_id)
147
    return possible_job_id
148

    
149
  success = client.WaitForJobCompletion(possible_job_id)
150

    
151
  result = client.GetJobStatus(possible_job_id)["opresult"][0]
152
  if success:
153
    return result
154
  else:
155
    print "Error encountered while performing operation: "
156
    print result
157
    return None
158

    
159

    
160
def TestTags(client, get_fn, add_fn, delete_fn, *args):
161
  """ Tests whether tagging works.
162

163
  @type client C{GanetiRapiClientWrapper}
164
  @param client The client wrapper.
165
  @type get_fn function
166
  @param get_fn A Get*Tags function of the client.
167
  @type add_fn function
168
  @param add_fn An Add*Tags function of the client.
169
  @type delete_fn function
170
  @param delete_fn A Delete*Tags function of the client.
171

172
  To allow this method to work for all tagging functions of the client, use
173
  named methods.
174

175
  """
176
  get_fn(*args)
177

    
178
  tags = ["tag1", "tag2", "tag3"]
179
  Finish(client, add_fn, *args, tags=tags, dry_run=True)
180
  Finish(client, add_fn, *args, tags=tags)
181

    
182
  get_fn(*args)
183

    
184
  Finish(client, delete_fn, *args, tags=tags[:1], dry_run=True)
185
  Finish(client, delete_fn, *args, tags=tags[:1])
186

    
187
  get_fn(*args)
188

    
189
  Finish(client, delete_fn, *args, tags=tags[1:])
190

    
191
  get_fn(*args)
192

    
193

    
194
def TestGetters(client):
195
  """ Tests the various get functions which only retrieve information about the
196
  cluster.
197

198
  @type client C{GanetiRapiClientWrapper}
199

200
  """
201
  client.GetVersion()
202
  client.GetFeatures()
203
  client.GetOperatingSystems()
204
  client.GetInfo()
205
  client.GetClusterTags()
206
  client.GetInstances()
207
  client.GetInstances(bulk=True)
208
  client.GetJobs()
209
  client.GetJobs(bulk=True)
210
  client.GetNodes()
211
  client.GetNodes(bulk=True)
212
  client.GetNetworks()
213
  client.GetNetworks(bulk=True)
214
  client.GetGroups()
215
  client.GetGroups(bulk=True)
216

    
217

    
218
def TestQueries(client, resource_name):
219
  """ Finds out which fields are present for a given resource type, and attempts
220
  to retrieve their values for all present resources.
221

222
  @type client C{GanetiRapiClientWrapper}
223
  @param client A wrapped RAPI client.
224
  @type resource_name string
225
  @param resource_name The name of the resource to use.
226

227
  """
228

    
229
  FIELDS_KEY = "fields"
230

    
231
  query_res = client.QueryFields(resource_name)
232

    
233
  if query_res is None or FIELDS_KEY not in query_res or \
234
    len(query_res[FIELDS_KEY]) == 0:
235
    return
236

    
237
  field_entries = query_res[FIELDS_KEY]
238

    
239
  fields = map(lambda e: e["name"], field_entries)
240

    
241
  client.Query(resource_name, fields)
242

    
243

    
244
def TestQueryFiltering(client, master_name):
245
  """ Performs queries by playing around with the only guaranteed resource, the
246
  master node.
247

248
  @type client C{GanetiRapiClientWrapper}
249
  @param client A wrapped RAPI client.
250
  @type master_name string
251
  @param master_name The hostname of the master node.
252

253
  """
254
  client.Query("node", ["name"],
255
               ["|",
256
                ["=", "name", master_name],
257
                [">", "dtotal", 0],
258
               ])
259

    
260
  client.Query("instance", ["name"],
261
               ["|",
262
                ["=", "name", "NonexistentInstance"],
263
                [">", "oper_ram", 0],
264
               ])
265

    
266

    
267
def RemoveAllInstances(client):
268
  """ Queries for a list of instances, then removes them all.
269

270
  @type client C{GanetiRapiClientWrapper}
271
  @param client A wrapped RAPI client.
272

273
  """
274
  instances = client.GetInstances()
275
  for inst in instances:
276
    Finish(client, client.DeleteInstance, inst)
277

    
278
  instances = client.GetInstances()
279
  assert len(instances) == 0
280

    
281

    
282
def TestSingleInstance(client, instance_name, alternate_name, node_one,
283
                       node_two):
284
  """ Creates an instance, performs operations involving it, and then deletes
285
  it.
286

287
  @type client C{GanetiRapiClientWrapper}
288
  @param client A wrapped RAPI client.
289
  @type instance_name string
290
  @param instance_name The hostname to use.
291
  @type instance_name string
292
  @param instance_name Another valid hostname to use.
293
  @type node_one string
294
  @param node_one A node on which an instance can be added.
295
  @type node_two string
296
  @param node_two A node on which an instance can be added.
297

298
  """
299

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

    
305
  # Another dry run, numeric size, should work, but still a dry run
306
  Finish(client, client.CreateInstance,
307
         "create", instance_name, "plain", [{"size": "1000"}], [{}],
308
         dry_run=True, os="debian-image", pnode=node_one)
309

    
310
  # Create a smaller instance, and delete it immediately
311
  Finish(client, client.CreateInstance,
312
         "create", instance_name, "plain", [{"size":800}], [{}],
313
         os="debian-image", pnode=node_one)
314

    
315
  Finish(client, client.DeleteInstance, instance_name)
316

    
317
  # Create one instance to use in further tests
318
  Finish(client, client.CreateInstance,
319
         "create", instance_name, "plain", [{"size":1200}], [{}],
320
         os="debian-image", pnode=node_one)
321

    
322
  client.GetInstance(instance_name)
323

    
324
  Finish(client, client.GetInstanceInfo, instance_name)
325

    
326
  Finish(client, client.GetInstanceInfo, instance_name, static=True)
327

    
328
  TestQueries(client, "instance")
329

    
330
  TestTags(client, client.GetInstanceTags, client.AddInstanceTags,
331
           client.DeleteInstanceTags, instance_name)
332

    
333
  Finish(client, client.GrowInstanceDisk,
334
         instance_name, 0, 100, wait_for_sync=True)
335

    
336
  Finish(client, client.RebootInstance,
337
         instance_name, "soft", ignore_secondaries=True, dry_run=True,
338
         reason="Hulk smash gently!")
339

    
340
  Finish(client, client.ShutdownInstance,
341
         instance_name, dry_run=True, no_remember=False,
342
         reason="Hulk smash hard!")
343

    
344
  Finish(client, client.StartupInstance,
345
         instance_name, dry_run=True, no_remember=False,
346
         reason="Not hard enough!")
347

    
348
  Finish(client, client.RebootInstance,
349
         instance_name, "soft", ignore_secondaries=True, dry_run=False)
350

    
351
  Finish(client, client.ShutdownInstance,
352
         instance_name, dry_run=False, no_remember=False)
353

    
354
  Finish(client, client.ModifyInstance,
355
         instance_name, disk_template="drbd", remote_node=node_two)
356

    
357
  Finish(client, client.ModifyInstance,
358
         instance_name, disk_template="plain")
359

    
360
  Finish(client, client.RenameInstance,
361
         instance_name, alternate_name, ip_check=True, name_check=True)
362

    
363
  Finish(client, client.RenameInstance, alternate_name, instance_name)
364

    
365
  Finish(client, client.DeactivateInstanceDisks, instance_name)
366

    
367
  Finish(client, client.ActivateInstanceDisks, instance_name)
368

    
369
  # Note that the RecreateInstanceDisks command will always fail, as there is
370
  # no way to induce the necessary prerequisites (removal of LV) via RAPI.
371
  # Keeping it around allows us to at least know that it still exists.
372
  Finish(client, client.RecreateInstanceDisks,
373
         instance_name, [0], [node_one])
374

    
375
  Finish(client, client.StartupInstance,
376
         instance_name, dry_run=False, no_remember=False)
377

    
378
  client.GetInstanceConsole(instance_name)
379

    
380
  Finish(client, client.ReinstallInstance,
381
         instance_name, os=None, no_startup=False, osparams={})
382

    
383
  Finish(client, client.DeleteInstance, instance_name, dry_run=True)
384

    
385
  Finish(client, client.DeleteInstance, instance_name)
386

    
387

    
388
def MarkUnmarkNode(client, node, state):
389
  """ Given a certain node state, marks a node as being in that state, and then
390
  unmarks it.
391

392
  @type client C{GanetiRapiClientWrapper}
393
  @param client A wrapped RAPI client.
394
  @type node string
395
  @type state string
396

397
  """
398
  # pylint: disable=W0142
399
  Finish(client, client.ModifyNode, node, **{state: True})
400
  Finish(client, client.ModifyNode, node, **{state: False})
401
  # pylint: enable=W0142
402

    
403

    
404
def TestNodeOperations(client, non_master_node):
405
  """ Tests various operations related to nodes only
406

407
  @type client C{GanetiRapiClientWrapper}
408
  @param client A wrapped RAPI client.
409
  @type non_master_node string
410
  @param non_master_node The name of a non-master node in the cluster.
411

412
  """
413

    
414
  client.GetNode(non_master_node)
415

    
416
  old_role = client.GetNodeRole(non_master_node)
417

    
418
  # Should fail
419
  Finish(client, client.SetNodeRole,
420
         non_master_node, "master", False, auto_promote=True)
421

    
422
  Finish(client, client.SetNodeRole,
423
         non_master_node, "regular", False, auto_promote=True)
424

    
425
  Finish(client, client.SetNodeRole,
426
         non_master_node, "master-candidate", False, auto_promote=True)
427

    
428
  Finish(client, client.SetNodeRole,
429
         non_master_node, "drained", False, auto_promote=True)
430

    
431
  Finish(client, client.SetNodeRole,
432
         non_master_node, old_role, False, auto_promote=True)
433

    
434
  Finish(client, client.PowercycleNode,
435
         non_master_node, force=False)
436

    
437
  storage_units_fields = [
438
    "name", "allocatable", "free", "node", "size", "type", "used",
439
  ]
440

    
441
  for storage_type in constants.STS_REPORT:
442
    storage_units = Finish(client, client.GetNodeStorageUnits,
443
                           non_master_node, storage_type,
444
                           ",".join(storage_units_fields))
445

    
446
    if len(storage_units) > 0 and len(storage_units[0]) > 0:
447
      # Name is the first entry of the first result, allocatable the other
448
      unit_name = storage_units[0][0]
449
      Finish(client, client.ModifyNodeStorageUnits,
450
             non_master_node, storage_type, unit_name,
451
             allocatable=not storage_units[0][1])
452
      Finish(client, client.ModifyNodeStorageUnits,
453
             non_master_node, storage_type, unit_name,
454
             allocatable=storage_units[0][1])
455
      Finish(client, client.RepairNodeStorageUnits,
456
             non_master_node, storage_type, unit_name)
457

    
458
  MarkUnmarkNode(client, non_master_node, "drained")
459
  MarkUnmarkNode(client, non_master_node, "powered")
460
  MarkUnmarkNode(client, non_master_node, "offline")
461

    
462
  TestQueries(client, "node")
463

    
464

    
465
def TestGroupOperations(client, node, another_node):
466
  """ Tests various operations related to groups only.
467

468
  @type client C{GanetiRapiClientWrapper}
469
  @param client A Ganeti RAPI client to use.
470
  @type node string
471
  @param node The name of a node in the cluster.
472
  @type another_node string
473
  @param another_node The name of another node in the cluster.
474

475
  """
476

    
477
  DEFAULT_GROUP_NAME = constants.INITIAL_NODE_GROUP_NAME
478
  TEST_GROUP_NAME = "TestGroup"
479
  ALTERNATE_GROUP_NAME = "RenamedTestGroup"
480

    
481
  Finish(client, client.CreateGroup,
482
         TEST_GROUP_NAME, alloc_policy=constants.ALLOC_POLICY_PREFERRED,
483
         dry_run=True)
484

    
485
  Finish(client, client.CreateGroup,
486
         TEST_GROUP_NAME, alloc_policy=constants.ALLOC_POLICY_PREFERRED)
487

    
488
  client.GetGroup(TEST_GROUP_NAME)
489

    
490
  TestQueries(client, "group")
491

    
492
  TestTags(client, client.GetGroupTags, client.AddGroupTags,
493
           client.DeleteGroupTags, TEST_GROUP_NAME)
494

    
495
  Finish(client, client.ModifyGroup,
496
         TEST_GROUP_NAME, alloc_policy=constants.ALLOC_POLICY_PREFERRED,
497
         depends=None)
498

    
499
  Finish(client, client.AssignGroupNodes,
500
         TEST_GROUP_NAME, [node, another_node], force=False, dry_run=True)
501

    
502
  Finish(client, client.AssignGroupNodes,
503
         TEST_GROUP_NAME, [another_node], force=False)
504

    
505
  Finish(client, client.RenameGroup,
506
         TEST_GROUP_NAME, ALTERNATE_GROUP_NAME)
507

    
508
  Finish(client, client.RenameGroup,
509
         ALTERNATE_GROUP_NAME, TEST_GROUP_NAME)
510

    
511
  Finish(client, client.AssignGroupNodes,
512
         DEFAULT_GROUP_NAME, [another_node], force=False)
513

    
514
  Finish(client, client.DeleteGroup, TEST_GROUP_NAME, dry_run=True)
515

    
516
  Finish(client, client.DeleteGroup, TEST_GROUP_NAME)
517

    
518

    
519
def TestNetworkConnectDisconnect(client, network_name, mode, link):
520
  """ Test connecting and disconnecting the network to a new node group.
521

522
  @type network_name string
523
  @param network_name The name of an existing and unconnected network.
524
  @type mode string
525
  @param mode The network mode.
526
  @type link string
527
  @param link The network link.
528

529
  """
530
  # For testing the connect/disconnect calls, a group is needed
531
  TEST_GROUP_NAME = "TestGroup"
532
  Finish(client, client.CreateGroup,
533
         TEST_GROUP_NAME, alloc_policy=constants.ALLOC_POLICY_PREFERRED)
534

    
535
  Finish(client, client.ConnectNetwork,
536
         network_name, TEST_GROUP_NAME, mode, link, dry_run=True)
537

    
538
  Finish(client, client.ConnectNetwork,
539
         network_name, TEST_GROUP_NAME, mode, link)
540

    
541
  Finish(client, client.DisconnectNetwork,
542
         network_name, TEST_GROUP_NAME, dry_run=True)
543

    
544
  Finish(client, client.DisconnectNetwork,
545
         network_name, TEST_GROUP_NAME)
546

    
547
  # Clean up the group
548
  Finish(client, client.DeleteGroup, TEST_GROUP_NAME)
549

    
550

    
551
def TestNetworks(client):
552
  """ Add some networks of different sizes, using RFC5737 addresses like in the
553
  QA.
554

555
  """
556

    
557
  NETWORK_NAME = "SurelyCertainlyNonexistentNetwork"
558

    
559
  Finish(client, client.CreateNetwork,
560
         NETWORK_NAME, "192.0.2.0/30", tags=[], dry_run=True)
561

    
562
  Finish(client, client.CreateNetwork,
563
         NETWORK_NAME, "192.0.2.0/30", tags=[])
564

    
565
  client.GetNetwork(NETWORK_NAME)
566

    
567
  TestTags(client, client.GetNetworkTags, client.AddNetworkTags,
568
           client.DeleteNetworkTags, NETWORK_NAME)
569

    
570
  Finish(client, client.ModifyNetwork,
571
         NETWORK_NAME, mac_prefix=None)
572

    
573
  TestQueries(client, "network")
574

    
575
  default_nicparams = qa_config.get("default-nicparams", None)
576

    
577
  # The entry might not be present in the QA config
578
  if default_nicparams is not None:
579
    mode = default_nicparams.get("mode", None)
580
    link = default_nicparams.get("link", None)
581
    if mode is not None and link is not None:
582
      TestNetworkConnectDisconnect(client, NETWORK_NAME, mode, link)
583

    
584
  # Clean up the network
585
  Finish(client, client.DeleteNetwork,
586
         NETWORK_NAME, dry_run=True)
587

    
588
  Finish(client, client.DeleteNetwork, NETWORK_NAME)
589

    
590

    
591
def Workload(client):
592
  """ The actual RAPI workload used for tests.
593

594
  @type client C{GanetiRapiClientWrapper}
595
  @param client A wrapped RAPI client.
596

597
  """
598

    
599
  # First just the simple information retrievals
600
  TestGetters(client)
601

    
602
  # Then the only remaining function which is parameter-free
603
  Finish(client, client.RedistributeConfig)
604

    
605
  TestTags(client, client.GetClusterTags, client.AddClusterTags,
606
           client.DeleteClusterTags)
607

    
608
  # Generously assume the master is present
609
  node = qa_config.AcquireNode()
610
  TestTags(client, client.GetNodeTags, client.AddNodeTags,
611
           client.DeleteNodeTags, node.primary)
612
  node.Release()
613

    
614
  # Instance tests
615

    
616
  # First remove all instances the QA might have created
617
  RemoveAllInstances(client)
618

    
619
  nodes = qa_config.AcquireManyNodes(2)
620
  instance_one = qa_config.AcquireInstance()
621
  instance_two = qa_config.AcquireInstance()
622
  TestSingleInstance(client, instance_one.name, instance_two.name,
623
                     nodes[0].primary, nodes[1].primary)
624
  instance_two.Release()
625
  instance_one.Release()
626
  qa_config.ReleaseManyNodes(nodes)
627

    
628
  # Test all the queries which involve resources that do not have functions
629
  # of their own
630
  TestQueries(client, "lock")
631
  TestQueries(client, "job")
632
  TestQueries(client, "export")
633

    
634
  node = qa_config.AcquireNode(exclude=qa_config.GetMasterNode())
635
  TestNodeOperations(client, node.primary)
636
  TestQueryFiltering(client, node.primary)
637
  node.Release()
638

    
639
  nodes = qa_config.AcquireManyNodes(2)
640
  TestGroupOperations(client, nodes[0].primary, nodes[1].primary)
641
  qa_config.ReleaseManyNodes(nodes)
642

    
643
  TestNetworks(client)
644

    
645

    
646
def Usage():
647
  sys.stderr.write("Usage:\n\trapi-workload.py qa-config-file")
648

    
649

    
650
def Main():
651
  if len(sys.argv) < 2:
652
    Usage()
653

    
654
  qa_config.Load(sys.argv[1])
655

    
656
  # Only the master will be present after a fresh QA cluster setup, so we have
657
  # to invoke this to get all the other nodes.
658
  qa_node.TestNodeAddAll()
659

    
660
  client = GanetiRapiClientWrapper()
661

    
662
  Workload(client)
663

    
664
  qa_node.TestNodeRemoveAll()
665

    
666

    
667
if __name__ == "__main__":
668
  Main()