Statistics
| Branch: | Tag: | Revision:

root / qa / rapi-workload.py @ 3eea40a0

History | View | Annotate | Download (35.4 kB)

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

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

    
21

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

24
"""
25

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

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

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

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

    
42

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

    
67

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

71
  """
72
  return None
73

    
74

    
75
RAPI_USERNAME = "ganeti-qa"
76

    
77

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

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

    
87
    self._method_invocations = {}
88

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

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

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

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

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

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

    
124
    return decoratedFn
125

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

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

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

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

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

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

    
171

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

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

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

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

    
198
  success = client.WaitForJobCompletion(possible_job_id)
199

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

    
208

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

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

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

224
  """
225
  get_fn(*args)
226

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

    
231
  get_fn(*args)
232

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

    
236
  get_fn(*args)
237

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

    
240
  get_fn(*args)
241

    
242

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

247
  @type client: C{GanetiRapiClientWrapper}
248

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

    
266

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

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

276
  """
277

    
278
  FIELDS_KEY = "fields"
279

    
280
  query_res = client.QueryFields(resource_name)
281

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

    
286
  field_entries = query_res[FIELDS_KEY]
287

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

    
290
  client.Query(resource_name, fields)
291

    
292

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

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

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

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

    
315

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

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

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

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

    
330

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

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

347
  """
348

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

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

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

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

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

    
371
  client.GetInstance(instance_name)
372

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

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

    
377
  TestQueries(client, "instance")
378

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
427
  client.GetInstanceConsole(instance_name)
428

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

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

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

    
436

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

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

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

    
452

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

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

461
  """
462

    
463
  client.GetNode(non_master_node)
464

    
465
  old_role = client.GetNodeRole(non_master_node)
466

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

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

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

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

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

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

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

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

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

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

    
511
  TestQueries(client, "node")
512

    
513

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

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

524
  """
525

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

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

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

    
537
  client.GetGroup(TEST_GROUP_NAME)
538

    
539
  TestQueries(client, "group")
540

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

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

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

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

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

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

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

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

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

    
567

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

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

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

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

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

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

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

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

    
599

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

604
  """
605

    
606
  NETWORK_NAME = "SurelyCertainlyNonexistentNetwork"
607

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

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

    
614
  client.GetNetwork(NETWORK_NAME)
615

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

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

    
622
  TestQueries(client, "network")
623

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

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

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

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

    
639

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

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

    
648

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

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

662
  """
663

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

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

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

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

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

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

    
694

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

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

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

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

709
  """
710

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

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

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

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

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

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

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

    
753
    nic_dicts.append(nic_dict)
754

    
755
  return nic_dicts
756

    
757

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

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

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

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

    
771
  inst_info = inst_info_all[src_instance.name]
772

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

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

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

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

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

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

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

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

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

    
830
  return success
831

    
832

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

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

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

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

    
849

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

854
  """
855

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

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

    
865

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

870
  """
871

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

    
875
  instance_to_create.SetDiskTemplate("plain")
876

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

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

    
889

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

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

904
  """
905

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

    
911

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

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

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

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

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

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

    
938

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

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

951
  """
952

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

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

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

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

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

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

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

    
985

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

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

992
  """
993

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

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

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

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

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

    
1012
  # Instance tests
1013

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

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

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

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

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

    
1039
  TestNetworks(client)
1040

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

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

    
1056

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

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

    
1074
  qa_config.Load(args[0])
1075

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

    
1080
  client = GanetiRapiClientWrapper()
1081

    
1082
  Workload(client)
1083

    
1084
  qa_node.TestNodeRemoveAll()
1085

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

    
1094
if __name__ == "__main__":
1095
  Main()