Statistics
| Branch: | Tag: | Revision:

root / qa / qa_rapi.py @ 4fab7cab

History | View | Annotate | Download (18.7 kB)

1
#
2

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

    
20

    
21
"""Remote API QA tests.
22

23
"""
24

    
25
import tempfile
26
import random
27

    
28
from ganeti import utils
29
from ganeti import constants
30
from ganeti import errors
31
from ganeti import cli
32
from ganeti import rapi
33
from ganeti import objects
34
from ganeti import query
35
from ganeti import compat
36
from ganeti import qlang
37

    
38
import ganeti.rapi.client        # pylint: disable-msg=W0611
39
import ganeti.rapi.client_utils
40

    
41
import qa_config
42
import qa_utils
43
import qa_error
44

    
45
from qa_utils import (AssertEqual, AssertIn, AssertMatch, StartLocalCommand)
46

    
47

    
48
_rapi_ca = None
49
_rapi_client = None
50
_rapi_username = None
51
_rapi_password = None
52

    
53

    
54
def Setup(username, password):
55
  """Configures the RAPI client.
56

57
  """
58
  # pylint: disable-msg=W0603
59
  # due to global usage
60
  global _rapi_ca
61
  global _rapi_client
62
  global _rapi_username
63
  global _rapi_password
64

    
65
  _rapi_username = username
66
  _rapi_password = password
67

    
68
  master = qa_config.GetMasterNode()
69

    
70
  # Load RAPI certificate from master node
71
  cmd = ["cat", constants.RAPI_CERT_FILE]
72

    
73
  # Write to temporary file
74
  _rapi_ca = tempfile.NamedTemporaryFile()
75
  _rapi_ca.write(qa_utils.GetCommandOutput(master["primary"],
76
                                           utils.ShellQuoteArgs(cmd)))
77
  _rapi_ca.flush()
78

    
79
  port = qa_config.get("rapi-port", default=constants.DEFAULT_RAPI_PORT)
80
  cfg_curl = rapi.client.GenericCurlConfig(cafile=_rapi_ca.name,
81
                                           proxy="")
82

    
83
  _rapi_client = rapi.client.GanetiRapiClient(master["primary"], port=port,
84
                                              username=username,
85
                                              password=password,
86
                                              curl_config_fn=cfg_curl)
87

    
88
  print "RAPI protocol version: %s" % _rapi_client.GetVersion()
89

    
90

    
91
INSTANCE_FIELDS = ("name", "os", "pnode", "snodes",
92
                   "admin_state",
93
                   "disk_template", "disk.sizes",
94
                   "nic.ips", "nic.macs", "nic.modes", "nic.links",
95
                   "beparams", "hvparams",
96
                   "oper_state", "oper_ram", "oper_vcpus", "status", "tags")
97

    
98
NODE_FIELDS = ("name", "dtotal", "dfree",
99
               "mtotal", "mnode", "mfree",
100
               "pinst_cnt", "sinst_cnt", "tags")
101

    
102
GROUP_FIELDS = frozenset([
103
  "name", "uuid",
104
  "alloc_policy",
105
  "node_cnt", "node_list",
106
  ])
107

    
108
JOB_FIELDS = frozenset([
109
  "id", "ops", "status", "summary",
110
  "opstatus", "opresult", "oplog",
111
  "received_ts", "start_ts", "end_ts",
112
  ])
113

    
114
LIST_FIELDS = ("id", "uri")
115

    
116

    
117
def Enabled():
118
  """Return whether remote API tests should be run.
119

120
  """
121
  return qa_config.TestEnabled('rapi')
122

    
123

    
124
def _DoTests(uris):
125
  # pylint: disable-msg=W0212
126
  # due to _SendRequest usage
127
  results = []
128

    
129
  for uri, verify, method, body in uris:
130
    assert uri.startswith("/")
131

    
132
    print "%s %s" % (method, uri)
133
    data = _rapi_client._SendRequest(method, uri, None, body)
134

    
135
    if verify is not None:
136
      if callable(verify):
137
        verify(data)
138
      else:
139
        AssertEqual(data, verify)
140

    
141
    results.append(data)
142

    
143
  return results
144

    
145

    
146
def _VerifyReturnsJob(data):
147
  AssertMatch(data, r'^\d+$')
148

    
149

    
150
def TestVersion():
151
  """Testing remote API version.
152

153
  """
154
  _DoTests([
155
    ("/version", constants.RAPI_VERSION, 'GET', None),
156
    ])
157

    
158

    
159
def TestEmptyCluster():
160
  """Testing remote API on an empty cluster.
161

162
  """
163
  master = qa_config.GetMasterNode()
164
  master_full = qa_utils.ResolveNodeName(master)
165

    
166
  def _VerifyInfo(data):
167
    AssertIn("name", data)
168
    AssertIn("master", data)
169
    AssertEqual(data["master"], master_full)
170

    
171
  def _VerifyNodes(data):
172
    master_entry = {
173
      "id": master_full,
174
      "uri": "/2/nodes/%s" % master_full,
175
      }
176
    AssertIn(master_entry, data)
177

    
178
  def _VerifyNodesBulk(data):
179
    for node in data:
180
      for entry in NODE_FIELDS:
181
        AssertIn(entry, node)
182

    
183
  def _VerifyGroups(data):
184
    default_group = {
185
      "name": constants.INITIAL_NODE_GROUP_NAME,
186
      "uri": "/2/groups/" + constants.INITIAL_NODE_GROUP_NAME,
187
      }
188
    AssertIn(default_group, data)
189

    
190
  def _VerifyGroupsBulk(data):
191
    for group in data:
192
      for field in GROUP_FIELDS:
193
        AssertIn(field, group)
194

    
195
  _DoTests([
196
    ("/", None, 'GET', None),
197
    ("/2/info", _VerifyInfo, 'GET', None),
198
    ("/2/tags", None, 'GET', None),
199
    ("/2/nodes", _VerifyNodes, 'GET', None),
200
    ("/2/nodes?bulk=1", _VerifyNodesBulk, 'GET', None),
201
    ("/2/groups", _VerifyGroups, 'GET', None),
202
    ("/2/groups?bulk=1", _VerifyGroupsBulk, 'GET', None),
203
    ("/2/instances", [], 'GET', None),
204
    ("/2/instances?bulk=1", [], 'GET', None),
205
    ("/2/os", None, 'GET', None),
206
    ])
207

    
208
  # Test HTTP Not Found
209
  for method in ["GET", "PUT", "POST", "DELETE"]:
210
    try:
211
      _DoTests([("/99/resource/not/here/99", None, method, None)])
212
    except rapi.client.GanetiApiError, err:
213
      AssertEqual(err.code, 404)
214
    else:
215
      raise qa_error.Error("Non-existent resource didn't return HTTP 404")
216

    
217
  # Test HTTP Not Implemented
218
  for method in ["PUT", "POST", "DELETE"]:
219
    try:
220
      _DoTests([("/version", None, method, None)])
221
    except rapi.client.GanetiApiError, err:
222
      AssertEqual(err.code, 501)
223
    else:
224
      raise qa_error.Error("Non-implemented method didn't fail")
225

    
226

    
227
def TestRapiQuery():
228
  """Testing resource queries via remote API.
229

230
  """
231
  master_name = qa_utils.ResolveNodeName(qa_config.GetMasterNode())
232
  rnd = random.Random(7818)
233

    
234
  for what in constants.QR_VIA_RAPI:
235
    all_fields = query.ALL_FIELDS[what].keys()
236
    rnd.shuffle(all_fields)
237

    
238
    # No fields, should return everything
239
    result = _rapi_client.QueryFields(what)
240
    qresult = objects.QueryFieldsResponse.FromDict(result)
241
    AssertEqual(len(qresult.fields), len(all_fields))
242

    
243
    # One field
244
    result = _rapi_client.QueryFields(what, fields=["name"])
245
    qresult = objects.QueryFieldsResponse.FromDict(result)
246
    AssertEqual(len(qresult.fields), 1)
247

    
248
    # Specify all fields, order must be correct
249
    result = _rapi_client.QueryFields(what, fields=all_fields)
250
    qresult = objects.QueryFieldsResponse.FromDict(result)
251
    AssertEqual(len(qresult.fields), len(all_fields))
252
    AssertEqual([fdef.name for fdef in qresult.fields], all_fields)
253

    
254
    # Unknown field
255
    result = _rapi_client.QueryFields(what, fields=["_unknown!"])
256
    qresult = objects.QueryFieldsResponse.FromDict(result)
257
    AssertEqual(len(qresult.fields), 1)
258
    AssertEqual(qresult.fields[0].name, "_unknown!")
259
    AssertEqual(qresult.fields[0].kind, constants.QFT_UNKNOWN)
260

    
261
    # Try once more, this time without the client
262
    _DoTests([
263
      ("/2/query/%s/fields" % what, None, "GET", None),
264
      ("/2/query/%s/fields?fields=name,name,%s" % (what, all_fields[0]),
265
       None, "GET", None),
266
      ])
267

    
268
    # Try missing query argument
269
    try:
270
      _DoTests([
271
        ("/2/query/%s" % what, None, "GET", None),
272
        ])
273
    except rapi.client.GanetiApiError, err:
274
      AssertEqual(err.code, 400)
275
    else:
276
      raise qa_error.Error("Request missing 'fields' parameter didn't fail")
277

    
278
    def _Check(exp_fields, data):
279
      qresult = objects.QueryResponse.FromDict(data)
280
      AssertEqual([fdef.name for fdef in qresult.fields], exp_fields)
281
      if not isinstance(qresult.data, list):
282
        raise qa_error.Error("Query did not return a list")
283

    
284
    _DoTests([
285
      # Specify fields in query
286
      ("/2/query/%s?fields=%s" % (what, ",".join(all_fields)),
287
       compat.partial(_Check, all_fields), "GET", None),
288

    
289
      ("/2/query/%s?fields=name" % what,
290
       compat.partial(_Check, ["name"]), "GET", None),
291

    
292
      # Note the spaces
293
      ("/2/query/%s?fields=name,%%20name%%09,name%%20" % what,
294
       compat.partial(_Check, ["name"] * 3), "GET", None),
295

    
296
      # PUT with fields in query
297
      ("/2/query/%s?fields=name" % what,
298
       compat.partial(_Check, ["name"]), "PUT", {}),
299

    
300
      # Fields in body
301
      ("/2/query/%s" % what, compat.partial(_Check, all_fields), "PUT", {
302
         "fields": all_fields,
303
         }),
304

    
305
      ("/2/query/%s" % what, compat.partial(_Check, ["name"] * 4), "PUT", {
306
         "fields": ["name"] * 4,
307
         }),
308
      ])
309

    
310
    def _CheckFilter():
311
      _DoTests([
312
        # With filter
313
        ("/2/query/%s" % what, compat.partial(_Check, all_fields), "PUT", {
314
           "fields": all_fields,
315
           "filter": [qlang.OP_TRUE, "name"],
316
           }),
317
        ])
318

    
319
    if what == constants.QR_LOCK:
320
      # Locks can't be filtered
321
      try:
322
        _CheckFilter()
323
      except rapi.client.GanetiApiError, err:
324
        AssertEqual(err.code, 500)
325
      else:
326
        raise qa_error.Error("Filtering locks didn't fail")
327
    else:
328
      _CheckFilter()
329

    
330
    if what == constants.QR_NODE:
331
      # Test with filter
332
      (nodes, ) = _DoTests([("/2/query/%s" % what,
333
        compat.partial(_Check, ["name", "master"]), "PUT", {
334
        "fields": ["name", "master"],
335
        "filter": [qlang.OP_TRUE, "master"],
336
        })])
337
      qresult = objects.QueryResponse.FromDict(nodes)
338
      AssertEqual(qresult.data, [
339
        [[constants.RS_NORMAL, master_name], [constants.RS_NORMAL, True]],
340
        ])
341

    
342

    
343
def TestInstance(instance):
344
  """Testing getting instance(s) info via remote API.
345

346
  """
347
  def _VerifyInstance(data):
348
    for entry in INSTANCE_FIELDS:
349
      AssertIn(entry, data)
350

    
351
  def _VerifyInstancesList(data):
352
    for instance in data:
353
      for entry in LIST_FIELDS:
354
        AssertIn(entry, instance)
355

    
356
  def _VerifyInstancesBulk(data):
357
    for instance_data in data:
358
      _VerifyInstance(instance_data)
359

    
360
  _DoTests([
361
    ("/2/instances/%s" % instance["name"], _VerifyInstance, 'GET', None),
362
    ("/2/instances", _VerifyInstancesList, 'GET', None),
363
    ("/2/instances?bulk=1", _VerifyInstancesBulk, 'GET', None),
364
    ("/2/instances/%s/activate-disks" % instance["name"],
365
     _VerifyReturnsJob, 'PUT', None),
366
    ("/2/instances/%s/deactivate-disks" % instance["name"],
367
     _VerifyReturnsJob, 'PUT', None),
368
    ])
369

    
370
  # Test OpBackupPrepare
371
  (job_id, ) = _DoTests([
372
    ("/2/instances/%s/prepare-export?mode=%s" %
373
     (instance["name"], constants.EXPORT_MODE_REMOTE),
374
     _VerifyReturnsJob, "PUT", None),
375
    ])
376

    
377
  result = _WaitForRapiJob(job_id)[0]
378
  AssertEqual(len(result["handshake"]), 3)
379
  AssertEqual(result["handshake"][0], constants.RIE_VERSION)
380
  AssertEqual(len(result["x509_key_name"]), 3)
381
  AssertIn("-----BEGIN CERTIFICATE-----", result["x509_ca"])
382

    
383

    
384
def TestNode(node):
385
  """Testing getting node(s) info via remote API.
386

387
  """
388
  def _VerifyNode(data):
389
    for entry in NODE_FIELDS:
390
      AssertIn(entry, data)
391

    
392
  def _VerifyNodesList(data):
393
    for node in data:
394
      for entry in LIST_FIELDS:
395
        AssertIn(entry, node)
396

    
397
  def _VerifyNodesBulk(data):
398
    for node_data in data:
399
      _VerifyNode(node_data)
400

    
401
  _DoTests([
402
    ("/2/nodes/%s" % node["primary"], _VerifyNode, 'GET', None),
403
    ("/2/nodes", _VerifyNodesList, 'GET', None),
404
    ("/2/nodes?bulk=1", _VerifyNodesBulk, 'GET', None),
405
    ])
406

    
407

    
408
def TestTags(kind, name, tags):
409
  """Tests .../tags resources.
410

411
  """
412
  if kind == constants.TAG_CLUSTER:
413
    uri = "/2/tags"
414
  elif kind == constants.TAG_NODE:
415
    uri = "/2/nodes/%s/tags" % name
416
  elif kind == constants.TAG_INSTANCE:
417
    uri = "/2/instances/%s/tags" % name
418
  else:
419
    raise errors.ProgrammerError("Unknown tag kind")
420

    
421
  def _VerifyTags(data):
422
    AssertEqual(sorted(tags), sorted(data))
423

    
424
  queryargs = "&".join("tag=%s" % i for i in tags)
425

    
426
  # Add tags
427
  (job_id, ) = _DoTests([
428
    ("%s?%s" % (uri, queryargs), _VerifyReturnsJob, "PUT", None),
429
    ])
430
  _WaitForRapiJob(job_id)
431

    
432
  # Retrieve tags
433
  _DoTests([
434
    (uri, _VerifyTags, 'GET', None),
435
    ])
436

    
437
  # Remove tags
438
  (job_id, ) = _DoTests([
439
    ("%s?%s" % (uri, queryargs), _VerifyReturnsJob, "DELETE", None),
440
    ])
441
  _WaitForRapiJob(job_id)
442

    
443

    
444
def _WaitForRapiJob(job_id):
445
  """Waits for a job to finish.
446

447
  """
448
  def _VerifyJob(data):
449
    AssertEqual(data["id"], job_id)
450
    for field in JOB_FIELDS:
451
      AssertIn(field, data)
452

    
453
  _DoTests([
454
    ("/2/jobs/%s" % job_id, _VerifyJob, "GET", None),
455
    ])
456

    
457
  return rapi.client_utils.PollJob(_rapi_client, job_id,
458
                                   cli.StdioJobPollReportCb())
459

    
460

    
461
def TestRapiNodeGroups():
462
  """Test several node group operations using RAPI.
463

464
  """
465
  groups = qa_config.get("groups", {})
466
  group1, group2, group3 = groups.get("inexistent-groups",
467
                                      ["group1", "group2", "group3"])[:3]
468

    
469
  # Create a group with no attributes
470
  body = {
471
    "name": group1,
472
    }
473

    
474
  (job_id, ) = _DoTests([
475
    ("/2/groups", _VerifyReturnsJob, "POST", body),
476
    ])
477

    
478
  _WaitForRapiJob(job_id)
479

    
480
  # Create a group specifying alloc_policy
481
  body = {
482
    "name": group2,
483
    "alloc_policy": constants.ALLOC_POLICY_UNALLOCABLE,
484
    }
485

    
486
  (job_id, ) = _DoTests([
487
    ("/2/groups", _VerifyReturnsJob, "POST", body),
488
    ])
489

    
490
  _WaitForRapiJob(job_id)
491

    
492
  # Modify alloc_policy
493
  body = {
494
    "alloc_policy": constants.ALLOC_POLICY_UNALLOCABLE,
495
    }
496

    
497
  (job_id, ) = _DoTests([
498
    ("/2/groups/%s/modify" % group1, _VerifyReturnsJob, "PUT", body),
499
    ])
500

    
501
  _WaitForRapiJob(job_id)
502

    
503
  # Rename a group
504
  body = {
505
    "new_name": group3,
506
    }
507

    
508
  (job_id, ) = _DoTests([
509
    ("/2/groups/%s/rename" % group2, _VerifyReturnsJob, "PUT", body),
510
    ])
511

    
512
  _WaitForRapiJob(job_id)
513

    
514
  # Delete groups
515
  for group in [group1, group3]:
516
    (job_id, ) = _DoTests([
517
      ("/2/groups/%s" % group, _VerifyReturnsJob, "DELETE", None),
518
      ])
519

    
520
    _WaitForRapiJob(job_id)
521

    
522

    
523
def TestRapiInstanceAdd(node, use_client):
524
  """Test adding a new instance via RAPI"""
525
  instance = qa_config.AcquireInstance()
526
  try:
527
    memory = utils.ParseUnit(qa_config.get("mem"))
528
    disk_sizes = [utils.ParseUnit(size) for size in qa_config.get("disk")]
529

    
530
    if use_client:
531
      disks = [{"size": size} for size in disk_sizes]
532
      nics = [{}]
533

    
534
      beparams = {
535
        constants.BE_MEMORY: memory,
536
        }
537

    
538
      job_id = _rapi_client.CreateInstance(constants.INSTANCE_CREATE,
539
                                           instance["name"],
540
                                           constants.DT_PLAIN,
541
                                           disks, nics,
542
                                           os=qa_config.get("os"),
543
                                           pnode=node["primary"],
544
                                           beparams=beparams)
545
    else:
546
      body = {
547
        "name": instance["name"],
548
        "os": qa_config.get("os"),
549
        "disk_template": constants.DT_PLAIN,
550
        "pnode": node["primary"],
551
        "memory": memory,
552
        "disks": disk_sizes,
553
        }
554

    
555
      (job_id, ) = _DoTests([
556
        ("/2/instances", _VerifyReturnsJob, "POST", body),
557
        ])
558

    
559
    _WaitForRapiJob(job_id)
560

    
561
    return instance
562
  except:
563
    qa_config.ReleaseInstance(instance)
564
    raise
565

    
566

    
567
def TestRapiInstanceRemove(instance, use_client):
568
  """Test removing instance via RAPI"""
569
  if use_client:
570
    job_id = _rapi_client.DeleteInstance(instance["name"])
571
  else:
572
    (job_id, ) = _DoTests([
573
      ("/2/instances/%s" % instance["name"], _VerifyReturnsJob, "DELETE", None),
574
      ])
575

    
576
  _WaitForRapiJob(job_id)
577

    
578
  qa_config.ReleaseInstance(instance)
579

    
580

    
581
def TestRapiInstanceMigrate(instance):
582
  """Test migrating instance via RAPI"""
583
  # Move to secondary node
584
  _WaitForRapiJob(_rapi_client.MigrateInstance(instance["name"]))
585
  # And back to previous primary
586
  _WaitForRapiJob(_rapi_client.MigrateInstance(instance["name"]))
587

    
588

    
589
def TestRapiInstanceRename(rename_source, rename_target):
590
  """Test renaming instance via RAPI"""
591
  _WaitForRapiJob(_rapi_client.RenameInstance(rename_source, rename_target))
592

    
593

    
594
def TestRapiInstanceReinstall(instance):
595
  """Test reinstalling an instance via RAPI"""
596
  _WaitForRapiJob(_rapi_client.ReinstallInstance(instance["name"]))
597

    
598

    
599
def TestRapiInstanceModify(instance):
600
  """Test modifying instance via RAPI"""
601
  def _ModifyInstance(**kwargs):
602
    _WaitForRapiJob(_rapi_client.ModifyInstance(instance["name"], **kwargs))
603

    
604
  _ModifyInstance(hvparams={
605
    constants.HV_KERNEL_ARGS: "single",
606
    })
607

    
608
  _ModifyInstance(beparams={
609
    constants.BE_VCPUS: 3,
610
    })
611

    
612
  _ModifyInstance(beparams={
613
    constants.BE_VCPUS: constants.VALUE_DEFAULT,
614
    })
615

    
616
  _ModifyInstance(hvparams={
617
    constants.HV_KERNEL_ARGS: constants.VALUE_DEFAULT,
618
    })
619

    
620

    
621
def TestRapiInstanceConsole(instance):
622
  """Test getting instance console information via RAPI"""
623
  result = _rapi_client.GetInstanceConsole(instance["name"])
624
  console = objects.InstanceConsole.FromDict(result)
625
  AssertEqual(console.Validate(), True)
626
  AssertEqual(console.instance, qa_utils.ResolveInstanceName(instance["name"]))
627

    
628

    
629
def TestRapiStoppedInstanceConsole(instance):
630
  """Test getting stopped instance's console information via RAPI"""
631
  try:
632
    _rapi_client.GetInstanceConsole(instance["name"])
633
  except rapi.client.GanetiApiError, err:
634
    AssertEqual(err.code, 503)
635
  else:
636
    raise qa_error.Error("Getting console for stopped instance didn't"
637
                         " return HTTP 503")
638

    
639

    
640
def GetOperatingSystems():
641
  """Retrieves a list of all available operating systems.
642

643
  """
644
  return _rapi_client.GetOperatingSystems()
645

    
646

    
647
def TestInterClusterInstanceMove(src_instance, dest_instance,
648
                                 pnode, snode, tnode):
649
  """Test tools/move-instance"""
650
  master = qa_config.GetMasterNode()
651

    
652
  rapi_pw_file = tempfile.NamedTemporaryFile()
653
  rapi_pw_file.write(_rapi_password)
654
  rapi_pw_file.flush()
655

    
656
  # TODO: Run some instance tests before moving back
657

    
658
  if snode is None:
659
    # instance is not redundant, but we still need to pass a node
660
    # (which will be ignored)
661
    fsec = tnode
662
  else:
663
    fsec = snode
664
  # note: pnode:snode are the *current* nodes, so we move it first to
665
  # tnode:pnode, then back to pnode:snode
666
  for si, di, pn, sn in [(src_instance["name"], dest_instance["name"],
667
                          tnode["primary"], pnode["primary"]),
668
                         (dest_instance["name"], src_instance["name"],
669
                          pnode["primary"], fsec["primary"])]:
670
    cmd = [
671
      "../tools/move-instance",
672
      "--verbose",
673
      "--src-ca-file=%s" % _rapi_ca.name,
674
      "--src-username=%s" % _rapi_username,
675
      "--src-password-file=%s" % rapi_pw_file.name,
676
      "--dest-instance-name=%s" % di,
677
      "--dest-primary-node=%s" % pn,
678
      "--dest-secondary-node=%s" % sn,
679
      "--net=0:mac=%s" % constants.VALUE_GENERATE,
680
      master["primary"],
681
      master["primary"],
682
      si,
683
      ]
684

    
685
    AssertEqual(StartLocalCommand(cmd).wait(), 0)