Add a new NodeGroup config object
[ganeti-local] / lib / hypervisor / hv_xen.py
1 #
2 #
3
4 # Copyright (C) 2006, 2007, 2008, 2009, 2010 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 logging
27 from cStringIO import StringIO
28
29 from ganeti import constants
30 from ganeti import errors
31 from ganeti import utils
32 from ganeti.hypervisor import hv_base
33 from ganeti import netutils
34
35
36 class XenHypervisor(hv_base.BaseHypervisor):
37   """Xen generic hypervisor interface
38
39   This is the Xen base class used for both Xen PVM and HVM. It contains
40   all the functionality that is identical for both.
41
42   """
43   CAN_MIGRATE = True
44   REBOOT_RETRY_COUNT = 60
45   REBOOT_RETRY_INTERVAL = 10
46
47   ANCILLARY_FILES = [
48     '/etc/xen/xend-config.sxp',
49     '/etc/xen/scripts/vif-bridge',
50     ]
51
52   @classmethod
53   def _WriteConfigFile(cls, instance, block_devices):
54     """Write the Xen config file for the instance.
55
56     """
57     raise NotImplementedError
58
59   @staticmethod
60   def _WriteConfigFileStatic(instance_name, data):
61     """Write the Xen config file for the instance.
62
63     This version of the function just writes the config file from static data.
64
65     """
66     utils.WriteFile("/etc/xen/%s" % instance_name, data=data)
67
68   @staticmethod
69   def _ReadConfigFile(instance_name):
70     """Returns the contents of the instance config file.
71
72     """
73     try:
74       file_content = utils.ReadFile("/etc/xen/%s" % instance_name)
75     except EnvironmentError, err:
76       raise errors.HypervisorError("Failed to load Xen config file: %s" % err)
77     return file_content
78
79   @staticmethod
80   def _RemoveConfigFile(instance_name):
81     """Remove the xen configuration file.
82
83     """
84     utils.RemoveFile("/etc/xen/%s" % instance_name)
85
86   @staticmethod
87   def _RunXmList(xmlist_errors):
88     """Helper function for L{_GetXMList} to run "xm list".
89
90     """
91     result = utils.RunCmd(["xm", "list"])
92     if result.failed:
93       logging.error("xm list failed (%s): %s", result.fail_reason,
94                     result.output)
95       xmlist_errors.append(result)
96       raise utils.RetryAgain()
97
98     # skip over the heading
99     return result.stdout.splitlines()[1:]
100
101   @classmethod
102   def _GetXMList(cls, include_node):
103     """Return the list of running instances.
104
105     If the include_node argument is True, then we return information
106     for dom0 also, otherwise we filter that from the return value.
107
108     @return: list of (name, id, memory, vcpus, state, time spent)
109
110     """
111     xmlist_errors = []
112     try:
113       lines = utils.Retry(cls._RunXmList, 1, 5, args=(xmlist_errors, ))
114     except utils.RetryTimeout:
115       if xmlist_errors:
116         xmlist_result = xmlist_errors.pop()
117
118         errmsg = ("xm list failed, timeout exceeded (%s): %s" %
119                   (xmlist_result.fail_reason, xmlist_result.output))
120       else:
121         errmsg = "xm list failed"
122
123       raise errors.HypervisorError(errmsg)
124
125     result = []
126     for line in lines:
127       # The format of lines is:
128       # Name      ID Mem(MiB) VCPUs State  Time(s)
129       # Domain-0   0  3418     4 r-----    266.2
130       data = line.split()
131       if len(data) != 6:
132         raise errors.HypervisorError("Can't parse output of xm list,"
133                                      " line: %s" % line)
134       try:
135         data[1] = int(data[1])
136         data[2] = int(data[2])
137         data[3] = int(data[3])
138         data[5] = float(data[5])
139       except (TypeError, ValueError), err:
140         raise errors.HypervisorError("Can't parse output of xm list,"
141                                      " line: %s, error: %s" % (line, err))
142
143       # skip the Domain-0 (optional)
144       if include_node or data[0] != 'Domain-0':
145         result.append(data)
146
147     return result
148
149   def ListInstances(self):
150     """Get the list of running instances.
151
152     """
153     xm_list = self._GetXMList(False)
154     names = [info[0] for info in xm_list]
155     return names
156
157   def GetInstanceInfo(self, instance_name):
158     """Get instance properties.
159
160     @param instance_name: the instance name
161
162     @return: tuple (name, id, memory, vcpus, stat, times)
163
164     """
165     xm_list = self._GetXMList(instance_name=="Domain-0")
166     result = None
167     for data in xm_list:
168       if data[0] == instance_name:
169         result = data
170         break
171     return result
172
173   def GetAllInstancesInfo(self):
174     """Get properties of all instances.
175
176     @return: list of tuples (name, id, memory, vcpus, stat, times)
177
178     """
179     xm_list = self._GetXMList(False)
180     return xm_list
181
182   def StartInstance(self, instance, block_devices):
183     """Start an instance.
184
185     """
186     self._WriteConfigFile(instance, block_devices)
187     result = utils.RunCmd(["xm", "create", instance.name])
188
189     if result.failed:
190       raise errors.HypervisorError("Failed to start instance %s: %s (%s)" %
191                                    (instance.name, result.fail_reason,
192                                     result.output))
193
194   def StopInstance(self, instance, force=False, retry=False, name=None):
195     """Stop an instance.
196
197     """
198     if name is None:
199       name = instance.name
200     self._RemoveConfigFile(name)
201     if force:
202       command = ["xm", "destroy", name]
203     else:
204       command = ["xm", "shutdown", name]
205     result = utils.RunCmd(command)
206
207     if result.failed:
208       raise errors.HypervisorError("Failed to stop instance %s: %s, %s" %
209                                    (name, result.fail_reason, result.output))
210
211   def RebootInstance(self, instance):
212     """Reboot an instance.
213
214     """
215     ini_info = self.GetInstanceInfo(instance.name)
216
217     if ini_info is None:
218       raise errors.HypervisorError("Failed to reboot instance %s,"
219                                    " not running" % instance.name)
220
221     result = utils.RunCmd(["xm", "reboot", instance.name])
222     if result.failed:
223       raise errors.HypervisorError("Failed to reboot instance %s: %s, %s" %
224                                    (instance.name, result.fail_reason,
225                                     result.output))
226
227     def _CheckInstance():
228       new_info = self.GetInstanceInfo(instance.name)
229
230       # check if the domain ID has changed or the run time has decreased
231       if (new_info is not None and
232           (new_info[1] != ini_info[1] or new_info[5] < ini_info[5])):
233         return
234
235       raise utils.RetryAgain()
236
237     try:
238       utils.Retry(_CheckInstance, self.REBOOT_RETRY_INTERVAL,
239                   self.REBOOT_RETRY_INTERVAL * self.REBOOT_RETRY_COUNT)
240     except utils.RetryTimeout:
241       raise errors.HypervisorError("Failed to reboot instance %s: instance"
242                                    " did not reboot in the expected interval" %
243                                    (instance.name, ))
244
245   def GetNodeInfo(self):
246     """Return information about the node.
247
248     @return: a dict with the following keys (memory values in MiB):
249           - memory_total: the total memory size on the node
250           - memory_free: the available memory on the node for instances
251           - memory_dom0: the memory used by the node itself, if available
252           - nr_cpus: total number of CPUs
253           - nr_nodes: in a NUMA system, the number of domains
254           - nr_sockets: the number of physical CPU sockets in the node
255
256     """
257     # note: in xen 3, memory has changed to total_memory
258     result = utils.RunCmd(["xm", "info"])
259     if result.failed:
260       logging.error("Can't run 'xm info' (%s): %s", result.fail_reason,
261                     result.output)
262       return None
263
264     xmoutput = result.stdout.splitlines()
265     result = {}
266     cores_per_socket = threads_per_core = nr_cpus = None
267     for line in xmoutput:
268       splitfields = line.split(":", 1)
269
270       if len(splitfields) > 1:
271         key = splitfields[0].strip()
272         val = splitfields[1].strip()
273         if key == 'memory' or key == 'total_memory':
274           result['memory_total'] = int(val)
275         elif key == 'free_memory':
276           result['memory_free'] = int(val)
277         elif key == 'nr_cpus':
278           nr_cpus = result['cpu_total'] = int(val)
279         elif key == 'nr_nodes':
280           result['cpu_nodes'] = int(val)
281         elif key == 'cores_per_socket':
282           cores_per_socket = int(val)
283         elif key == 'threads_per_core':
284           threads_per_core = int(val)
285
286     if (cores_per_socket is not None and
287         threads_per_core is not None and nr_cpus is not None):
288       result['cpu_sockets'] = nr_cpus / (cores_per_socket * threads_per_core)
289
290     dom0_info = self.GetInstanceInfo("Domain-0")
291     if dom0_info is not None:
292       result['memory_dom0'] = dom0_info[2]
293
294     return result
295
296   @classmethod
297   def GetShellCommandForConsole(cls, instance, hvparams, beparams):
298     """Return a command for connecting to the console of an instance.
299
300     """
301     return "xm console %s" % instance.name
302
303
304   def Verify(self):
305     """Verify the hypervisor.
306
307     For Xen, this verifies that the xend process is running.
308
309     """
310     result = utils.RunCmd(["xm", "info"])
311     if result.failed:
312       return "'xm info' failed: %s, %s" % (result.fail_reason, result.output)
313
314   @staticmethod
315   def _GetConfigFileDiskData(block_devices):
316     """Get disk directive for xen config file.
317
318     This method builds the xen config disk directive according to the
319     given disk_template and block_devices.
320
321     @param block_devices: list of tuples (cfdev, rldev):
322         - cfdev: dict containing ganeti config disk part
323         - rldev: ganeti.bdev.BlockDev object
324
325     @return: string containing disk directive for xen instance config file
326
327     """
328     FILE_DRIVER_MAP = {
329       constants.FD_LOOP: "file",
330       constants.FD_BLKTAP: "tap:aio",
331       }
332     disk_data = []
333     if len(block_devices) > 24:
334       # 'z' - 'a' = 24
335       raise errors.HypervisorError("Too many disks")
336     # FIXME: instead of this hardcoding here, each of PVM/HVM should
337     # directly export their info (currently HVM will just sed this info)
338     namespace = ["sd" + chr(i + ord('a')) for i in range(24)]
339     for sd_name, (cfdev, dev_path) in zip(namespace, block_devices):
340       if cfdev.mode == constants.DISK_RDWR:
341         mode = "w"
342       else:
343         mode = "r"
344       if cfdev.dev_type == constants.LD_FILE:
345         line = "'%s:%s,%s,%s'" % (FILE_DRIVER_MAP[cfdev.physical_id[0]],
346                                   dev_path, sd_name, mode)
347       else:
348         line = "'phy:%s,%s,%s'" % (dev_path, sd_name, mode)
349       disk_data.append(line)
350
351     return disk_data
352
353   def MigrationInfo(self, instance):
354     """Get instance information to perform a migration.
355
356     @type instance: L{objects.Instance}
357     @param instance: instance to be migrated
358     @rtype: string
359     @return: content of the xen config file
360
361     """
362     return self._ReadConfigFile(instance.name)
363
364   def AcceptInstance(self, instance, info, target):
365     """Prepare to accept an instance.
366
367     @type instance: L{objects.Instance}
368     @param instance: instance to be accepted
369     @type info: string
370     @param info: content of the xen config file on the source node
371     @type target: string
372     @param target: target host (usually ip), on this node
373
374     """
375     pass
376
377   def FinalizeMigration(self, instance, info, success):
378     """Finalize an instance migration.
379
380     After a successful migration we write the xen config file.
381     We do nothing on a failure, as we did not change anything at accept time.
382
383     @type instance: L{objects.Instance}
384     @param instance: instance whose migration is being finalized
385     @type info: string
386     @param info: content of the xen config file on the source node
387     @type success: boolean
388     @param success: whether the migration was a success or a failure
389
390     """
391     if success:
392       self._WriteConfigFileStatic(instance.name, info)
393
394   def MigrateInstance(self, instance, target, live):
395     """Migrate an instance to a target node.
396
397     The migration will not be attempted if the instance is not
398     currently running.
399
400     @type instance: L{objects.Instance}
401     @param instance: the instance to be migrated
402     @type target: string
403     @param target: ip address of the target node
404     @type live: boolean
405     @param live: perform a live migration
406
407     """
408     if self.GetInstanceInfo(instance.name) is None:
409       raise errors.HypervisorError("Instance not running, cannot migrate")
410
411     port = instance.hvparams[constants.HV_MIGRATION_PORT]
412
413     if not netutils.TcpPing(target, port, live_port_needed=True):
414       raise errors.HypervisorError("Remote host %s not listening on port"
415                                    " %s, cannot migrate" % (target, port))
416
417     args = ["xm", "migrate", "-p", "%d" % port]
418     if live:
419       args.append("-l")
420     args.extend([instance.name, target])
421     result = utils.RunCmd(args)
422     if result.failed:
423       raise errors.HypervisorError("Failed to migrate instance %s: %s" %
424                                    (instance.name, result.output))
425     # remove old xen file after migration succeeded
426     try:
427       self._RemoveConfigFile(instance.name)
428     except EnvironmentError:
429       logging.exception("Failure while removing instance config file")
430
431   @classmethod
432   def PowercycleNode(cls):
433     """Xen-specific powercycle.
434
435     This first does a Linux reboot (which triggers automatically a Xen
436     reboot), and if that fails it tries to do a Xen reboot. The reason
437     we don't try a Xen reboot first is that the xen reboot launches an
438     external command which connects to the Xen hypervisor, and that
439     won't work in case the root filesystem is broken and/or the xend
440     daemon is not working.
441
442     """
443     try:
444       cls.LinuxPowercycle()
445     finally:
446       utils.RunCmd(["xm", "debug", "R"])
447
448
449 class XenPvmHypervisor(XenHypervisor):
450   """Xen PVM hypervisor interface"""
451
452   PARAMETERS = {
453     constants.HV_USE_BOOTLOADER: hv_base.NO_CHECK,
454     constants.HV_BOOTLOADER_PATH: hv_base.OPT_FILE_CHECK,
455     constants.HV_BOOTLOADER_ARGS: hv_base.NO_CHECK,
456     constants.HV_KERNEL_PATH: hv_base.REQ_FILE_CHECK,
457     constants.HV_INITRD_PATH: hv_base.OPT_FILE_CHECK,
458     constants.HV_ROOT_PATH: hv_base.REQUIRED_CHECK,
459     constants.HV_KERNEL_ARGS: hv_base.NO_CHECK,
460     constants.HV_MIGRATION_PORT: hv_base.NET_PORT_CHECK,
461     constants.HV_MIGRATION_MODE: hv_base.MIGRATION_MODE_CHECK,
462     }
463
464   @classmethod
465   def _WriteConfigFile(cls, instance, block_devices):
466     """Write the Xen config file for the instance.
467
468     """
469     hvp = instance.hvparams
470     config = StringIO()
471     config.write("# this is autogenerated by Ganeti, please do not edit\n#\n")
472
473     # if bootloader is True, use bootloader instead of kernel and ramdisk
474     # parameters.
475     if hvp[constants.HV_USE_BOOTLOADER]:
476       # bootloader handling
477       bootloader_path = hvp[constants.HV_BOOTLOADER_PATH]
478       if bootloader_path:
479         config.write("bootloader = '%s'\n" % bootloader_path)
480       else:
481         raise errors.HypervisorError("Bootloader enabled, but missing"
482                                      " bootloader path")
483
484       bootloader_args = hvp[constants.HV_BOOTLOADER_ARGS]
485       if bootloader_args:
486         config.write("bootargs = '%s'\n" % bootloader_args)
487     else:
488       # kernel handling
489       kpath = hvp[constants.HV_KERNEL_PATH]
490       config.write("kernel = '%s'\n" % kpath)
491
492       # initrd handling
493       initrd_path = hvp[constants.HV_INITRD_PATH]
494       if initrd_path:
495         config.write("ramdisk = '%s'\n" % initrd_path)
496
497     # rest of the settings
498     config.write("memory = %d\n" % instance.beparams[constants.BE_MEMORY])
499     config.write("vcpus = %d\n" % instance.beparams[constants.BE_VCPUS])
500     config.write("name = '%s'\n" % instance.name)
501
502     vif_data = []
503     for nic in instance.nics:
504       nic_str = "mac=%s" % (nic.mac)
505       ip = getattr(nic, "ip", None)
506       if ip is not None:
507         nic_str += ", ip=%s" % ip
508       if nic.nicparams[constants.NIC_MODE] == constants.NIC_MODE_BRIDGED:
509         nic_str += ", bridge=%s" % nic.nicparams[constants.NIC_LINK]
510       vif_data.append("'%s'" % nic_str)
511
512     disk_data = cls._GetConfigFileDiskData(block_devices)
513
514     config.write("vif = [%s]\n" % ",".join(vif_data))
515     config.write("disk = [%s]\n" % ",".join(disk_data))
516
517     config.write("root = '%s'\n" % hvp[constants.HV_ROOT_PATH])
518     config.write("on_poweroff = 'destroy'\n")
519     config.write("on_reboot = 'restart'\n")
520     config.write("on_crash = 'restart'\n")
521     config.write("extra = '%s'\n" % hvp[constants.HV_KERNEL_ARGS])
522     # just in case it exists
523     utils.RemoveFile("/etc/xen/auto/%s" % instance.name)
524     try:
525       utils.WriteFile("/etc/xen/%s" % instance.name, data=config.getvalue())
526     except EnvironmentError, err:
527       raise errors.HypervisorError("Cannot write Xen instance confile"
528                                    " file /etc/xen/%s: %s" %
529                                    (instance.name, err))
530
531     return True
532
533
534 class XenHvmHypervisor(XenHypervisor):
535   """Xen HVM hypervisor interface"""
536
537   ANCILLARY_FILES = XenHypervisor.ANCILLARY_FILES + [
538     constants.VNC_PASSWORD_FILE,
539     ]
540
541   PARAMETERS = {
542     constants.HV_ACPI: hv_base.NO_CHECK,
543     constants.HV_BOOT_ORDER: (True, ) +
544       (lambda x: x and len(x.strip("acdn")) == 0,
545        "Invalid boot order specified, must be one or more of [acdn]",
546        None, None),
547     constants.HV_CDROM_IMAGE_PATH: hv_base.OPT_FILE_CHECK,
548     constants.HV_DISK_TYPE:
549       hv_base.ParamInSet(True, constants.HT_HVM_VALID_DISK_TYPES),
550     constants.HV_NIC_TYPE:
551       hv_base.ParamInSet(True, constants.HT_HVM_VALID_NIC_TYPES),
552     constants.HV_PAE: hv_base.NO_CHECK,
553     constants.HV_VNC_BIND_ADDRESS:
554       (False, netutils.IP4Address.IsValid,
555        "VNC bind address is not a valid IP address", None, None),
556     constants.HV_KERNEL_PATH: hv_base.REQ_FILE_CHECK,
557     constants.HV_DEVICE_MODEL: hv_base.REQ_FILE_CHECK,
558     constants.HV_VNC_PASSWORD_FILE: hv_base.REQ_FILE_CHECK,
559     constants.HV_MIGRATION_PORT: hv_base.NET_PORT_CHECK,
560     constants.HV_MIGRATION_MODE: hv_base.MIGRATION_MODE_CHECK,
561     constants.HV_USE_LOCALTIME: hv_base.NO_CHECK,
562     }
563
564   @classmethod
565   def _WriteConfigFile(cls, instance, block_devices):
566     """Create a Xen 3.1 HVM config file.
567
568     """
569     hvp = instance.hvparams
570
571     config = StringIO()
572     config.write("# this is autogenerated by Ganeti, please do not edit\n#\n")
573
574     # kernel handling
575     kpath = hvp[constants.HV_KERNEL_PATH]
576     config.write("kernel = '%s'\n" % kpath)
577
578     config.write("builder = 'hvm'\n")
579     config.write("memory = %d\n" % instance.beparams[constants.BE_MEMORY])
580     config.write("vcpus = %d\n" % instance.beparams[constants.BE_VCPUS])
581     config.write("name = '%s'\n" % instance.name)
582     if hvp[constants.HV_PAE]:
583       config.write("pae = 1\n")
584     else:
585       config.write("pae = 0\n")
586     if hvp[constants.HV_ACPI]:
587       config.write("acpi = 1\n")
588     else:
589       config.write("acpi = 0\n")
590     config.write("apic = 1\n")
591     config.write("device_model = '%s'\n" % hvp[constants.HV_DEVICE_MODEL])
592     config.write("boot = '%s'\n" % hvp[constants.HV_BOOT_ORDER])
593     config.write("sdl = 0\n")
594     config.write("usb = 1\n")
595     config.write("usbdevice = 'tablet'\n")
596     config.write("vnc = 1\n")
597     if hvp[constants.HV_VNC_BIND_ADDRESS] is None:
598       config.write("vnclisten = '%s'\n" % constants.VNC_DEFAULT_BIND_ADDRESS)
599     else:
600       config.write("vnclisten = '%s'\n" % hvp[constants.HV_VNC_BIND_ADDRESS])
601
602     if instance.network_port > constants.VNC_BASE_PORT:
603       display = instance.network_port - constants.VNC_BASE_PORT
604       config.write("vncdisplay = %s\n" % display)
605       config.write("vncunused = 0\n")
606     else:
607       config.write("# vncdisplay = 1\n")
608       config.write("vncunused = 1\n")
609
610     vnc_pwd_file = hvp[constants.HV_VNC_PASSWORD_FILE]
611     try:
612       password = utils.ReadFile(vnc_pwd_file)
613     except EnvironmentError, err:
614       raise errors.HypervisorError("Failed to open VNC password file %s: %s" %
615                                    (vnc_pwd_file, err))
616
617     config.write("vncpasswd = '%s'\n" % password.rstrip())
618
619     config.write("serial = 'pty'\n")
620     if hvp[constants.HV_USE_LOCALTIME]:
621       config.write("localtime = 1\n")
622
623     vif_data = []
624     nic_type = hvp[constants.HV_NIC_TYPE]
625     if nic_type is None:
626       # ensure old instances don't change
627       nic_type_str = ", type=ioemu"
628     elif nic_type == constants.HT_NIC_PARAVIRTUAL:
629       nic_type_str = ", type=paravirtualized"
630     else:
631       nic_type_str = ", model=%s, type=ioemu" % nic_type
632     for nic in instance.nics:
633       nic_str = "mac=%s%s" % (nic.mac, nic_type_str)
634       ip = getattr(nic, "ip", None)
635       if ip is not None:
636         nic_str += ", ip=%s" % ip
637       if nic.nicparams[constants.NIC_MODE] == constants.NIC_MODE_BRIDGED:
638         nic_str += ", bridge=%s" % nic.nicparams[constants.NIC_LINK]
639       vif_data.append("'%s'" % nic_str)
640
641     config.write("vif = [%s]\n" % ",".join(vif_data))
642     disk_data = cls._GetConfigFileDiskData(block_devices)
643     disk_type = hvp[constants.HV_DISK_TYPE]
644     if disk_type in (None, constants.HT_DISK_IOEMU):
645       replacement = ",ioemu:hd"
646     else:
647       replacement = ",hd"
648     disk_data = [line.replace(",sd", replacement) for line in disk_data]
649     iso_path = hvp[constants.HV_CDROM_IMAGE_PATH]
650     if iso_path:
651       iso = "'file:%s,hdc:cdrom,r'" % iso_path
652       disk_data.append(iso)
653
654     config.write("disk = [%s]\n" % (",".join(disk_data)))
655
656     config.write("on_poweroff = 'destroy'\n")
657     config.write("on_reboot = 'restart'\n")
658     config.write("on_crash = 'restart'\n")
659     # just in case it exists
660     utils.RemoveFile("/etc/xen/auto/%s" % instance.name)
661     try:
662       utils.WriteFile("/etc/xen/%s" % instance.name,
663                       data=config.getvalue())
664     except EnvironmentError, err:
665       raise errors.HypervisorError("Cannot write Xen instance confile"
666                                    " file /etc/xen/%s: %s" %
667                                    (instance.name, err))
668
669     return True