Statistics
| Branch: | Tag: | Revision:

root / lib / hypervisor / hv_xen.py @ 07b49e41

History | View | Annotate | Download (20.8 kB)

1
#
2
#
3

    
4
# Copyright (C) 2006, 2007, 2008 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
"""Xen hypervisors
23

24
"""
25

    
26
import os
27
import os.path
28
import time
29
import logging
30
from cStringIO import StringIO
31

    
32
from ganeti import constants
33
from ganeti import errors
34
from ganeti import utils
35
from ganeti.hypervisor import hv_base
36

    
37

    
38
class XenHypervisor(hv_base.BaseHypervisor):
39
  """Xen generic hypervisor interface
40

41
  This is the Xen base class used for both Xen PVM and HVM. It contains
42
  all the functionality that is identical for both.
43

44
  """
45
  REBOOT_RETRY_COUNT = 60
46
  REBOOT_RETRY_INTERVAL = 10
47

    
48
  ANCILLARY_FILES = [
49
    '/etc/xen/xend-config.sxp',
50
    '/etc/xen/scripts/vif-bridge',
51
    ]
52

    
53
  @classmethod
54
  def _WriteConfigFile(cls, instance, block_devices):
55
    """Write the Xen config file for the instance.
56

57
    """
58
    raise NotImplementedError
59

    
60
  @staticmethod
61
  def _WriteConfigFileStatic(instance_name, data):
62
    """Write the Xen config file for the instance.
63

64
    This version of the function just writes the config file from static data.
65

66
    """
67
    utils.WriteFile("/etc/xen/%s" % instance_name, data=data)
68

    
69
  @staticmethod
70
  def _ReadConfigFile(instance_name):
71
    """Returns the contents of the instance config file.
72

73
    """
74
    try:
75
      file_content = utils.ReadFile("/etc/xen/%s" % instance_name)
76
    except EnvironmentError, err:
77
      raise errors.HypervisorError("Failed to load Xen config file: %s" % err)
78
    return file_content
79

    
80
  @staticmethod
81
  def _RemoveConfigFile(instance_name):
82
    """Remove the xen configuration file.
83

84
    """
85
    utils.RemoveFile("/etc/xen/%s" % instance_name)
86

    
87
  @staticmethod
88
  def _GetXMList(include_node):
89
    """Return the list of running instances.
90

91
    If the include_node argument is True, then we return information
92
    for dom0 also, otherwise we filter that from the return value.
93

94
    @return: list of (name, id, memory, vcpus, state, time spent)
95

96
    """
97
    for _ in range(5):
98
      result = utils.RunCmd(["xm", "list"])
99
      if not result.failed:
100
        break
101
      logging.error("xm list failed (%s): %s", result.fail_reason,
102
                    result.output)
103
      time.sleep(1)
104

    
105
    if result.failed:
106
      raise errors.HypervisorError("xm list failed, retries"
107
                                   " exceeded (%s): %s" %
108
                                   (result.fail_reason, result.output))
109

    
110
    # skip over the heading
111
    lines = result.stdout.splitlines()[1:]
112
    result = []
113
    for line in lines:
114
      # The format of lines is:
115
      # Name      ID Mem(MiB) VCPUs State  Time(s)
116
      # Domain-0   0  3418     4 r-----    266.2
117
      data = line.split()
118
      if len(data) != 6:
119
        raise errors.HypervisorError("Can't parse output of xm list,"
120
                                     " line: %s" % line)
121
      try:
122
        data[1] = int(data[1])
123
        data[2] = int(data[2])
124
        data[3] = int(data[3])
125
        data[5] = float(data[5])
126
      except ValueError, err:
127
        raise errors.HypervisorError("Can't parse output of xm list,"
128
                                     " line: %s, error: %s" % (line, err))
129

    
130
      # skip the Domain-0 (optional)
131
      if include_node or data[0] != 'Domain-0':
132
        result.append(data)
133

    
134
    return result
135

    
136
  def ListInstances(self):
137
    """Get the list of running instances.
138

139
    """
140
    xm_list = self._GetXMList(False)
141
    names = [info[0] for info in xm_list]
142
    return names
143

    
144
  def GetInstanceInfo(self, instance_name):
145
    """Get instance properties.
146

147
    @param instance_name: the instance name
148

149
    @return: tuple (name, id, memory, vcpus, stat, times)
150

151
    """
152
    xm_list = self._GetXMList(instance_name=="Domain-0")
153
    result = None
154
    for data in xm_list:
155
      if data[0] == instance_name:
156
        result = data
157
        break
158
    return result
159

    
160
  def GetAllInstancesInfo(self):
161
    """Get properties of all instances.
162

163
    @return: list of tuples (name, id, memory, vcpus, stat, times)
164

165
    """
166
    xm_list = self._GetXMList(False)
167
    return xm_list
168

    
169
  def StartInstance(self, instance, block_devices):
170
    """Start an instance.
171

172
    """
173
    self._WriteConfigFile(instance, block_devices)
174
    result = utils.RunCmd(["xm", "create", instance.name])
175

    
176
    if result.failed:
177
      raise errors.HypervisorError("Failed to start instance %s: %s (%s)" %
178
                                   (instance.name, result.fail_reason,
179
                                    result.output))
180

    
181
  def StopInstance(self, instance, force=False, retry=False):
182
    """Stop an instance.
183

184
    """
185
    if retry:
186
      return
187
    self._RemoveConfigFile(instance.name)
188
    if force:
189
      command = ["xm", "destroy", instance.name]
190
    else:
191
      command = ["xm", "shutdown", instance.name]
192
    result = utils.RunCmd(command)
193

    
194
    if result.failed:
195
      raise errors.HypervisorError("Failed to stop instance %s: %s, %s" %
196
                                   (instance.name, result.fail_reason,
197
                                    result.output))
198

    
199
  def RebootInstance(self, instance):
200
    """Reboot an instance.
201

202
    """
203
    ini_info = self.GetInstanceInfo(instance.name)
204
    result = utils.RunCmd(["xm", "reboot", instance.name])
205

    
206
    if result.failed:
207
      raise errors.HypervisorError("Failed to reboot instance %s: %s, %s" %
208
                                   (instance.name, result.fail_reason,
209
                                    result.output))
210
    done = False
211
    retries = self.REBOOT_RETRY_COUNT
212
    while retries > 0:
213
      new_info = self.GetInstanceInfo(instance.name)
214
      # check if the domain ID has changed or the run time has
215
      # decreased
216
      if new_info[1] != ini_info[1] or new_info[5] < ini_info[5]:
217
        done = True
218
        break
219
      time.sleep(self.REBOOT_RETRY_INTERVAL)
220
      retries -= 1
221

    
222
    if not done:
223
      raise errors.HypervisorError("Failed to reboot instance %s: instance"
224
                                   " did not reboot in the expected interval" %
225
                                   (instance.name, ))
226

    
227
  def GetNodeInfo(self):
228
    """Return information about the node.
229

230
    @return: a dict with the following keys (memory values in MiB):
231
          - memory_total: the total memory size on the node
232
          - memory_free: the available memory on the node for instances
233
          - memory_dom0: the memory used by the node itself, if available
234
          - nr_cpus: total number of CPUs
235
          - nr_nodes: in a NUMA system, the number of domains
236
          - nr_sockets: the number of physical CPU sockets in the node
237

238
    """
239
    # note: in xen 3, memory has changed to total_memory
240
    result = utils.RunCmd(["xm", "info"])
241
    if result.failed:
242
      logging.error("Can't run 'xm info' (%s): %s", result.fail_reason,
243
                    result.output)
244
      return None
245

    
246
    xmoutput = result.stdout.splitlines()
247
    result = {}
248
    cores_per_socket = threads_per_core = nr_cpus = None
249
    for line in xmoutput:
250
      splitfields = line.split(":", 1)
251

    
252
      if len(splitfields) > 1:
253
        key = splitfields[0].strip()
254
        val = splitfields[1].strip()
255
        if key == 'memory' or key == 'total_memory':
256
          result['memory_total'] = int(val)
257
        elif key == 'free_memory':
258
          result['memory_free'] = int(val)
259
        elif key == 'nr_cpus':
260
          nr_cpus = result['cpu_total'] = int(val)
261
        elif key == 'nr_nodes':
262
          result['cpu_nodes'] = int(val)
263
        elif key == 'cores_per_socket':
264
          cores_per_socket = int(val)
265
        elif key == 'threads_per_core':
266
          threads_per_core = int(val)
267

    
268
    if (cores_per_socket is not None and
269
        threads_per_core is not None and nr_cpus is not None):
270
      result['cpu_sockets'] = nr_cpus / (cores_per_socket * threads_per_core)
271

    
272
    dom0_info = self.GetInstanceInfo("Domain-0")
273
    if dom0_info is not None:
274
      result['memory_dom0'] = dom0_info[2]
275

    
276
    return result
277

    
278
  @classmethod
279
  def GetShellCommandForConsole(cls, instance, hvparams, beparams):
280
    """Return a command for connecting to the console of an instance.
281

282
    """
283
    return "xm console %s" % instance.name
284

    
285

    
286
  def Verify(self):
287
    """Verify the hypervisor.
288

289
    For Xen, this verifies that the xend process is running.
290

291
    """
292
    result = utils.RunCmd(["xm", "info"])
293
    if result.failed:
294
      return "'xm info' failed: %s, %s" % (result.fail_reason, result.output)
295

    
296
  @staticmethod
297
  def _GetConfigFileDiskData(disk_template, block_devices):
298
    """Get disk directive for xen config file.
299

300
    This method builds the xen config disk directive according to the
301
    given disk_template and block_devices.
302

303
    @param disk_template: string containing instance disk template
304
    @param block_devices: list of tuples (cfdev, rldev):
305
        - cfdev: dict containing ganeti config disk part
306
        - rldev: ganeti.bdev.BlockDev object
307

308
    @return: string containing disk directive for xen instance config file
309

310
    """
311
    FILE_DRIVER_MAP = {
312
      constants.FD_LOOP: "file",
313
      constants.FD_BLKTAP: "tap:aio",
314
      }
315
    disk_data = []
316
    if len(block_devices) > 24:
317
      # 'z' - 'a' = 24
318
      raise errors.HypervisorError("Too many disks")
319
    # FIXME: instead of this hardcoding here, each of PVM/HVM should
320
    # directly export their info (currently HVM will just sed this info)
321
    namespace = ["sd" + chr(i + ord('a')) for i in range(24)]
322
    for sd_name, (cfdev, dev_path) in zip(namespace, block_devices):
323
      if cfdev.mode == constants.DISK_RDWR:
324
        mode = "w"
325
      else:
326
        mode = "r"
327
      if cfdev.dev_type == constants.LD_FILE:
328
        line = "'%s:%s,%s,%s'" % (FILE_DRIVER_MAP[cfdev.physical_id[0]],
329
                                  dev_path, sd_name, mode)
330
      else:
331
        line = "'phy:%s,%s,%s'" % (dev_path, sd_name, mode)
332
      disk_data.append(line)
333

    
334
    return disk_data
335

    
336
  def MigrationInfo(self, instance):
337
    """Get instance information to perform a migration.
338

339
    @type instance: L{objects.Instance}
340
    @param instance: instance to be migrated
341
    @rtype: string
342
    @return: content of the xen config file
343

344
    """
345
    return self._ReadConfigFile(instance.name)
346

    
347
  def AcceptInstance(self, instance, info, target):
348
    """Prepare to accept an instance.
349

350
    @type instance: L{objects.Instance}
351
    @param instance: instance to be accepted
352
    @type info: string
353
    @param info: content of the xen config file on the source node
354
    @type target: string
355
    @param target: target host (usually ip), on this node
356

357
    """
358
    pass
359

    
360
  def FinalizeMigration(self, instance, info, success):
361
    """Finalize an instance migration.
362

363
    After a successful migration we write the xen config file.
364
    We do nothing on a failure, as we did not change anything at accept time.
365

366
    @type instance: L{objects.Instance}
367
    @param instance: instance whose migration is being aborted
368
    @type info: string
369
    @param info: content of the xen config file on the source node
370
    @type success: boolean
371
    @param success: whether the migration was a success or a failure
372

373
    """
374
    if success:
375
      self._WriteConfigFileStatic(instance.name, info)
376

    
377
  def MigrateInstance(self, instance, target, live):
378
    """Migrate an instance to a target node.
379

380
    The migration will not be attempted if the instance is not
381
    currently running.
382

383
    @type instance: string
384
    @param instance: instance name
385
    @type target: string
386
    @param target: ip address of the target node
387
    @type live: boolean
388
    @param live: perform a live migration
389

390
    """
391
    if self.GetInstanceInfo(instance) is None:
392
      raise errors.HypervisorError("Instance not running, cannot migrate")
393
    args = ["xm", "migrate"]
394
    if live:
395
      args.append("-l")
396
    args.extend([instance, target])
397
    result = utils.RunCmd(args)
398
    if result.failed:
399
      raise errors.HypervisorError("Failed to migrate instance %s: %s" %
400
                                   (instance, result.output))
401
    # remove old xen file after migration succeeded
402
    try:
403
      self._RemoveConfigFile(instance)
404
    except EnvironmentError:
405
      logging.exception("Failure while removing instance config file")
406

    
407
  @classmethod
408
  def PowercycleNode(cls):
409
    """Xen-specific powercycle.
410

411
    This first does a Linux reboot (which triggers automatically a Xen
412
    reboot), and if that fails it tries to do a Xen reboot. The reason
413
    we don't try a Xen reboot first is that the xen reboot launches an
414
    external command which connects to the Xen hypervisor, and that
415
    won't work in case the root filesystem is broken and/or the xend
416
    daemon is not working.
417

418
    """
419
    try:
420
      cls.LinuxPowercycle()
421
    finally:
422
      utils.RunCmd(["xm", "debug", "R"])
423

    
424

    
425
class XenPvmHypervisor(XenHypervisor):
426
  """Xen PVM hypervisor interface"""
427

    
428
  PARAMETERS = {
429
    constants.HV_USE_BOOTLOADER: hv_base.NO_CHECK,
430
    constants.HV_BOOTLOADER_PATH: hv_base.OPT_FILE_CHECK,
431
    constants.HV_BOOTLOADER_ARGS: hv_base.NO_CHECK,
432
    constants.HV_KERNEL_PATH: hv_base.REQ_FILE_CHECK,
433
    constants.HV_INITRD_PATH: hv_base.OPT_FILE_CHECK,
434
    constants.HV_ROOT_PATH: hv_base.REQUIRED_CHECK,
435
    constants.HV_KERNEL_ARGS: hv_base.NO_CHECK,
436
    }
437

    
438
  @classmethod
439
  def _WriteConfigFile(cls, instance, block_devices):
440
    """Write the Xen config file for the instance.
441

442
    """
443
    hvp = instance.hvparams
444
    config = StringIO()
445
    config.write("# this is autogenerated by Ganeti, please do not edit\n#\n")
446

    
447
    # if bootloader is True, use bootloader instead of kernel and ramdisk
448
    # parameters.
449
    if hvp[constants.HV_USE_BOOTLOADER]:
450
      # bootloader handling
451
      bootloader_path = hvp[constants.HV_BOOTLOADER_PATH]
452
      if bootloader_path:
453
        config.write("bootloader = '%s'\n" % bootloader_path)
454
      else:
455
        raise errors.HypervisorError("Bootloader enabled, but missing"
456
                                     " bootloader path")
457

    
458
      bootloader_args = hvp[constants.HV_BOOTLOADER_ARGS]
459
      if bootloader_args:
460
        config.write("bootargs = '%s'\n" % bootloader_args)
461
    else:
462
      # kernel handling
463
      kpath = hvp[constants.HV_KERNEL_PATH]
464
      config.write("kernel = '%s'\n" % kpath)
465

    
466
      # initrd handling
467
      initrd_path = hvp[constants.HV_INITRD_PATH]
468
      if initrd_path:
469
        config.write("ramdisk = '%s'\n" % initrd_path)
470

    
471
    # rest of the settings
472
    config.write("memory = %d\n" % instance.beparams[constants.BE_MEMORY])
473
    config.write("vcpus = %d\n" % instance.beparams[constants.BE_VCPUS])
474
    config.write("name = '%s'\n" % instance.name)
475

    
476
    vif_data = []
477
    for nic in instance.nics:
478
      nic_str = "mac=%s" % (nic.mac)
479
      ip = getattr(nic, "ip", None)
480
      if ip is not None:
481
        nic_str += ", ip=%s" % ip
482
      vif_data.append("'%s'" % nic_str)
483
      if nic.nicparams[constants.NIC_MODE] == constants.NIC_MODE_BRIDGED:
484
        nic_str += ", bridge=%s" % nic.nicparams[constants.NIC_LINK]
485

    
486
    config.write("vif = [%s]\n" % ",".join(vif_data))
487
    config.write("disk = [%s]\n" % ",".join(
488
                 cls._GetConfigFileDiskData(instance.disk_template,
489
                                            block_devices)))
490

    
491
    config.write("root = '%s'\n" % hvp[constants.HV_ROOT_PATH])
492
    config.write("on_poweroff = 'destroy'\n")
493
    config.write("on_reboot = 'restart'\n")
494
    config.write("on_crash = 'restart'\n")
495
    config.write("extra = '%s'\n" % hvp[constants.HV_KERNEL_ARGS])
496
    # just in case it exists
497
    utils.RemoveFile("/etc/xen/auto/%s" % instance.name)
498
    try:
499
      utils.WriteFile("/etc/xen/%s" % instance.name, data=config.getvalue())
500
    except EnvironmentError, err:
501
      raise errors.HypervisorError("Cannot write Xen instance confile"
502
                                   " file /etc/xen/%s: %s" %
503
                                   (instance.name, err))
504

    
505
    return True
506

    
507

    
508
class XenHvmHypervisor(XenHypervisor):
509
  """Xen HVM hypervisor interface"""
510

    
511
  ANCILLARY_FILES = XenHypervisor.ANCILLARY_FILES + \
512
    [constants.VNC_PASSWORD_FILE]
513

    
514
  PARAMETERS = {
515
    constants.HV_ACPI: hv_base.NO_CHECK,
516
    constants.HV_BOOT_ORDER: (True, ) +
517
      (lambda x: x and len(x.strip("acdn")) == 0,
518
       "Invalid boot order specified, must be one or more of [acdn]",
519
       None, None),
520
    constants.HV_CDROM_IMAGE_PATH: hv_base.OPT_FILE_CHECK,
521
    constants.HV_DISK_TYPE:
522
      hv_base.ParamInSet(True, constants.HT_HVM_VALID_DISK_TYPES),
523
    constants.HV_NIC_TYPE:
524
      hv_base.ParamInSet(True, constants.HT_HVM_VALID_NIC_TYPES),
525
    constants.HV_PAE: hv_base.NO_CHECK,
526
    constants.HV_VNC_BIND_ADDRESS:
527
      (False, utils.IsValidIP,
528
       "VNC bind address is not a valid IP address", None, None),
529
    constants.HV_KERNEL_PATH: hv_base.REQ_FILE_CHECK,
530
    constants.HV_DEVICE_MODEL: hv_base.REQ_FILE_CHECK,
531
    constants.HV_VNC_PASSWORD_FILE: hv_base.REQ_FILE_CHECK,
532
    }
533

    
534
  @classmethod
535
  def _WriteConfigFile(cls, instance, block_devices):
536
    """Create a Xen 3.1 HVM config file.
537

538
    """
539
    hvp = instance.hvparams
540

    
541
    config = StringIO()
542
    config.write("# this is autogenerated by Ganeti, please do not edit\n#\n")
543

    
544
    # kernel handling
545
    kpath = hvp[constants.HV_KERNEL_PATH]
546
    config.write("kernel = '%s'\n" % kpath)
547

    
548
    config.write("builder = 'hvm'\n")
549
    config.write("memory = %d\n" % instance.beparams[constants.BE_MEMORY])
550
    config.write("vcpus = %d\n" % instance.beparams[constants.BE_VCPUS])
551
    config.write("name = '%s'\n" % instance.name)
552
    if hvp[constants.HV_PAE]:
553
      config.write("pae = 1\n")
554
    else:
555
      config.write("pae = 0\n")
556
    if hvp[constants.HV_ACPI]:
557
      config.write("acpi = 1\n")
558
    else:
559
      config.write("acpi = 0\n")
560
    config.write("apic = 1\n")
561
    config.write("device_model = '%s'\n" % hvp[constants.HV_DEVICE_MODEL])
562
    config.write("boot = '%s'\n" % hvp[constants.HV_BOOT_ORDER])
563
    config.write("sdl = 0\n")
564
    config.write("usb = 1\n")
565
    config.write("usbdevice = 'tablet'\n")
566
    config.write("vnc = 1\n")
567
    if hvp[constants.HV_VNC_BIND_ADDRESS] is None:
568
      config.write("vnclisten = '%s'\n" % constants.VNC_DEFAULT_BIND_ADDRESS)
569
    else:
570
      config.write("vnclisten = '%s'\n" % hvp[constants.HV_VNC_BIND_ADDRESS])
571

    
572
    if instance.network_port > constants.VNC_BASE_PORT:
573
      display = instance.network_port - constants.VNC_BASE_PORT
574
      config.write("vncdisplay = %s\n" % display)
575
      config.write("vncunused = 0\n")
576
    else:
577
      config.write("# vncdisplay = 1\n")
578
      config.write("vncunused = 1\n")
579

    
580
    vnc_pwd_file = hvp[constants.HV_VNC_PASSWORD_FILE]
581
    try:
582
      password = utils.ReadFile(vnc_pwd_file)
583
    except EnvironmentError, err:
584
      raise errors.HypervisorError("Failed to open VNC password file %s: %s" %
585
                                   (vnc_pwd_file, err))
586

    
587
    config.write("vncpasswd = '%s'\n" % password.rstrip())
588

    
589
    config.write("serial = 'pty'\n")
590
    config.write("localtime = 1\n")
591

    
592
    vif_data = []
593
    nic_type = hvp[constants.HV_NIC_TYPE]
594
    if nic_type is None:
595
      # ensure old instances don't change
596
      nic_type_str = ", type=ioemu"
597
    elif nic_type == constants.HT_NIC_PARAVIRTUAL:
598
      nic_type_str = ", type=paravirtualized"
599
    else:
600
      nic_type_str = ", model=%s, type=ioemu" % nic_type
601
    for nic in instance.nics:
602
      nic_str = "mac=%s%s" % (nic.mac, nic_type_str)
603
      ip = getattr(nic, "ip", None)
604
      if ip is not None:
605
        nic_str += ", ip=%s" % ip
606
      vif_data.append("'%s'" % nic_str)
607
      if nic.nicparams[constants.NIC_MODE] == constants.NIC_MODE_BRIDGED:
608
        nic_str += ", bridge=%s" % nic.nicparams[constants.NIC_LINK]
609

    
610
    config.write("vif = [%s]\n" % ",".join(vif_data))
611
    disk_data = cls._GetConfigFileDiskData(instance.disk_template,
612
                                            block_devices)
613
    disk_type = hvp[constants.HV_DISK_TYPE]
614
    if disk_type in (None, constants.HT_DISK_IOEMU):
615
      replacement = ",ioemu:hd"
616
    else:
617
      replacement = ",hd"
618
    disk_data = [line.replace(",sd", replacement) for line in disk_data]
619
    iso_path = hvp[constants.HV_CDROM_IMAGE_PATH]
620
    if iso_path:
621
      iso = "'file:%s,hdc:cdrom,r'" % iso_path
622
      disk_data.append(iso)
623

    
624
    config.write("disk = [%s]\n" % (",".join(disk_data)))
625

    
626
    config.write("on_poweroff = 'destroy'\n")
627
    config.write("on_reboot = 'restart'\n")
628
    config.write("on_crash = 'restart'\n")
629
    # just in case it exists
630
    utils.RemoveFile("/etc/xen/auto/%s" % instance.name)
631
    try:
632
      utils.WriteFile("/etc/xen/%s" % instance.name,
633
                      data=config.getvalue())
634
    except EnvironmentError, err:
635
      raise errors.HypervisorError("Cannot write Xen instance confile"
636
                                   " file /etc/xen/%s: %s" %
637
                                   (instance.name, err))
638

    
639
    return True