Merge branch 'stable-2.9' into stable-2.10
[ganeti-local] / lib / utils / __init__.py
1 #
2 #
3
4 # Copyright (C) 2006, 2007, 2010, 2011 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 """Ganeti utility module.
23
24 This module holds functions that can be used in both daemons (all) and
25 the command line scripts.
26
27 """
28
29 # Allow wildcard import in pylint: disable=W0401
30
31 import os
32 import re
33 import errno
34 import pwd
35 import time
36 import itertools
37 import select
38 import logging
39 import signal
40
41 from ganeti import errors
42 from ganeti import constants
43 from ganeti import compat
44 from ganeti import pathutils
45
46 from ganeti.utils.algo import *
47 from ganeti.utils.filelock import *
48 from ganeti.utils.hash import *
49 from ganeti.utils.io import *
50 from ganeti.utils.log import *
51 from ganeti.utils.lvm import *
52 from ganeti.utils.mlock import *
53 from ganeti.utils.nodesetup import *
54 from ganeti.utils.process import *
55 from ganeti.utils.retry import *
56 from ganeti.utils.storage import *
57 from ganeti.utils.text import *
58 from ganeti.utils.wrapper import *
59 from ganeti.utils.version import *
60 from ganeti.utils.x509 import *
61
62
63 _VALID_SERVICE_NAME_RE = re.compile("^[-_.a-zA-Z0-9]{1,128}$")
64
65 UUID_RE = re.compile(constants.UUID_REGEX)
66
67
68 def ForceDictType(target, key_types, allowed_values=None):
69   """Force the values of a dict to have certain types.
70
71   @type target: dict
72   @param target: the dict to update
73   @type key_types: dict
74   @param key_types: dict mapping target dict keys to types
75                     in constants.ENFORCEABLE_TYPES
76   @type allowed_values: list
77   @keyword allowed_values: list of specially allowed values
78
79   """
80   if allowed_values is None:
81     allowed_values = []
82
83   if not isinstance(target, dict):
84     msg = "Expected dictionary, got '%s'" % target
85     raise errors.TypeEnforcementError(msg)
86
87   for key in target:
88     if key not in key_types:
89       msg = "Unknown parameter '%s'" % key
90       raise errors.TypeEnforcementError(msg)
91
92     if target[key] in allowed_values:
93       continue
94
95     ktype = key_types[key]
96     if ktype not in constants.ENFORCEABLE_TYPES:
97       msg = "'%s' has non-enforceable type %s" % (key, ktype)
98       raise errors.ProgrammerError(msg)
99
100     if ktype in (constants.VTYPE_STRING, constants.VTYPE_MAYBE_STRING):
101       if target[key] is None and ktype == constants.VTYPE_MAYBE_STRING:
102         msg = ("'None' is not a valid Maybe value for '%s'. "
103                "Use 'VALUE_HS_NOTHING'") % (key, )
104         logging.warning(msg)
105       elif (target[key] == constants.VALUE_HS_NOTHING
106             and ktype == constants.VTYPE_MAYBE_STRING):
107         pass
108       elif not isinstance(target[key], basestring):
109         if isinstance(target[key], bool) and not target[key]:
110           target[key] = ""
111         else:
112           msg = "'%s' (value %s) is not a valid string" % (key, target[key])
113           raise errors.TypeEnforcementError(msg)
114     elif ktype == constants.VTYPE_BOOL:
115       if isinstance(target[key], basestring) and target[key]:
116         if target[key].lower() == constants.VALUE_FALSE:
117           target[key] = False
118         elif target[key].lower() == constants.VALUE_TRUE:
119           target[key] = True
120         else:
121           msg = "'%s' (value %s) is not a valid boolean" % (key, target[key])
122           raise errors.TypeEnforcementError(msg)
123       elif target[key]:
124         target[key] = True
125       else:
126         target[key] = False
127     elif ktype == constants.VTYPE_SIZE:
128       try:
129         target[key] = ParseUnit(target[key])
130       except errors.UnitParseError, err:
131         msg = "'%s' (value %s) is not a valid size. error: %s" % \
132               (key, target[key], err)
133         raise errors.TypeEnforcementError(msg)
134     elif ktype == constants.VTYPE_INT:
135       try:
136         target[key] = int(target[key])
137       except (ValueError, TypeError):
138         msg = "'%s' (value %s) is not a valid integer" % (key, target[key])
139         raise errors.TypeEnforcementError(msg)
140
141
142 def ValidateServiceName(name):
143   """Validate the given service name.
144
145   @type name: number or string
146   @param name: Service name or port specification
147
148   """
149   try:
150     numport = int(name)
151   except (ValueError, TypeError):
152     # Non-numeric service name
153     valid = _VALID_SERVICE_NAME_RE.match(name)
154   else:
155     # Numeric port (protocols other than TCP or UDP might need adjustments
156     # here)
157     valid = (numport >= 0 and numport < (1 << 16))
158
159   if not valid:
160     raise errors.OpPrereqError("Invalid service name '%s'" % name,
161                                errors.ECODE_INVAL)
162
163   return name
164
165
166 def _ComputeMissingKeys(key_path, options, defaults):
167   """Helper functions to compute which keys a invalid.
168
169   @param key_path: The current key path (if any)
170   @param options: The user provided options
171   @param defaults: The default dictionary
172   @return: A list of invalid keys
173
174   """
175   defaults_keys = frozenset(defaults.keys())
176   invalid = []
177   for key, value in options.items():
178     if key_path:
179       new_path = "%s/%s" % (key_path, key)
180     else:
181       new_path = key
182
183     if key not in defaults_keys:
184       invalid.append(new_path)
185     elif isinstance(value, dict):
186       invalid.extend(_ComputeMissingKeys(new_path, value, defaults[key]))
187
188   return invalid
189
190
191 def VerifyDictOptions(options, defaults):
192   """Verify a dict has only keys set which also are in the defaults dict.
193
194   @param options: The user provided options
195   @param defaults: The default dictionary
196   @raise error.OpPrereqError: If one of the keys is not supported
197
198   """
199   invalid = _ComputeMissingKeys("", options, defaults)
200
201   if invalid:
202     raise errors.OpPrereqError("Provided option keys not supported: %s" %
203                                CommaJoin(invalid), errors.ECODE_INVAL)
204
205
206 def ListVolumeGroups():
207   """List volume groups and their size
208
209   @rtype: dict
210   @return:
211        Dictionary with keys volume name and values
212        the size of the volume
213
214   """
215   command = "vgs --noheadings --units m --nosuffix -o name,size"
216   result = RunCmd(command)
217   retval = {}
218   if result.failed:
219     return retval
220
221   for line in result.stdout.splitlines():
222     try:
223       name, size = line.split()
224       size = int(float(size))
225     except (IndexError, ValueError), err:
226       logging.error("Invalid output from vgs (%s): %s", err, line)
227       continue
228
229     retval[name] = size
230
231   return retval
232
233
234 def BridgeExists(bridge):
235   """Check whether the given bridge exists in the system
236
237   @type bridge: str
238   @param bridge: the bridge name to check
239   @rtype: boolean
240   @return: True if it does
241
242   """
243   return os.path.isdir("/sys/class/net/%s/bridge" % bridge)
244
245
246 def TryConvert(fn, val):
247   """Try to convert a value ignoring errors.
248
249   This function tries to apply function I{fn} to I{val}. If no
250   C{ValueError} or C{TypeError} exceptions are raised, it will return
251   the result, else it will return the original value. Any other
252   exceptions are propagated to the caller.
253
254   @type fn: callable
255   @param fn: function to apply to the value
256   @param val: the value to be converted
257   @return: The converted value if the conversion was successful,
258       otherwise the original value.
259
260   """
261   try:
262     nv = fn(val)
263   except (ValueError, TypeError):
264     nv = val
265   return nv
266
267
268 def ParseCpuMask(cpu_mask):
269   """Parse a CPU mask definition and return the list of CPU IDs.
270
271   CPU mask format: comma-separated list of CPU IDs
272   or dash-separated ID ranges
273   Example: "0-2,5" -> "0,1,2,5"
274
275   @type cpu_mask: str
276   @param cpu_mask: CPU mask definition
277   @rtype: list of int
278   @return: list of CPU IDs
279
280   """
281   if not cpu_mask:
282     return []
283   cpu_list = []
284   for range_def in cpu_mask.split(","):
285     boundaries = range_def.split("-")
286     n_elements = len(boundaries)
287     if n_elements > 2:
288       raise errors.ParseError("Invalid CPU ID range definition"
289                               " (only one hyphen allowed): %s" % range_def)
290     try:
291       lower = int(boundaries[0])
292     except (ValueError, TypeError), err:
293       raise errors.ParseError("Invalid CPU ID value for lower boundary of"
294                               " CPU ID range: %s" % str(err))
295     try:
296       higher = int(boundaries[-1])
297     except (ValueError, TypeError), err:
298       raise errors.ParseError("Invalid CPU ID value for higher boundary of"
299                               " CPU ID range: %s" % str(err))
300     if lower > higher:
301       raise errors.ParseError("Invalid CPU ID range definition"
302                               " (%d > %d): %s" % (lower, higher, range_def))
303     cpu_list.extend(range(lower, higher + 1))
304   return cpu_list
305
306
307 def ParseMultiCpuMask(cpu_mask):
308   """Parse a multiple CPU mask definition and return the list of CPU IDs.
309
310   CPU mask format: colon-separated list of comma-separated list of CPU IDs
311   or dash-separated ID ranges, with optional "all" as CPU value
312   Example: "0-2,5:all:1,5,6:2" -> [ [ 0,1,2,5 ], [ -1 ], [ 1, 5, 6 ], [ 2 ] ]
313
314   @type cpu_mask: str
315   @param cpu_mask: multiple CPU mask definition
316   @rtype: list of lists of int
317   @return: list of lists of CPU IDs
318
319   """
320   if not cpu_mask:
321     return []
322   cpu_list = []
323   for range_def in cpu_mask.split(constants.CPU_PINNING_SEP):
324     if range_def == constants.CPU_PINNING_ALL:
325       cpu_list.append([constants.CPU_PINNING_ALL_VAL, ])
326     else:
327       # Uniquify and sort the list before adding
328       cpu_list.append(sorted(set(ParseCpuMask(range_def))))
329
330   return cpu_list
331
332
333 def GetHomeDir(user, default=None):
334   """Try to get the homedir of the given user.
335
336   The user can be passed either as a string (denoting the name) or as
337   an integer (denoting the user id). If the user is not found, the
338   C{default} argument is returned, which defaults to C{None}.
339
340   """
341   try:
342     if isinstance(user, basestring):
343       result = pwd.getpwnam(user)
344     elif isinstance(user, (int, long)):
345       result = pwd.getpwuid(user)
346     else:
347       raise errors.ProgrammerError("Invalid type passed to GetHomeDir (%s)" %
348                                    type(user))
349   except KeyError:
350     return default
351   return result.pw_dir
352
353
354 def FirstFree(seq, base=0):
355   """Returns the first non-existing integer from seq.
356
357   The seq argument should be a sorted list of positive integers. The
358   first time the index of an element is smaller than the element
359   value, the index will be returned.
360
361   The base argument is used to start at a different offset,
362   i.e. C{[3, 4, 6]} with I{offset=3} will return 5.
363
364   Example: C{[0, 1, 3]} will return I{2}.
365
366   @type seq: sequence
367   @param seq: the sequence to be analyzed.
368   @type base: int
369   @param base: use this value as the base index of the sequence
370   @rtype: int
371   @return: the first non-used index in the sequence
372
373   """
374   for idx, elem in enumerate(seq):
375     assert elem >= base, "Passed element is higher than base offset"
376     if elem > idx + base:
377       # idx is not used
378       return idx + base
379   return None
380
381
382 def SingleWaitForFdCondition(fdobj, event, timeout):
383   """Waits for a condition to occur on the socket.
384
385   Immediately returns at the first interruption.
386
387   @type fdobj: integer or object supporting a fileno() method
388   @param fdobj: entity to wait for events on
389   @type event: integer
390   @param event: ORed condition (see select module)
391   @type timeout: float or None
392   @param timeout: Timeout in seconds
393   @rtype: int or None
394   @return: None for timeout, otherwise occured conditions
395
396   """
397   check = (event | select.POLLPRI |
398            select.POLLNVAL | select.POLLHUP | select.POLLERR)
399
400   if timeout is not None:
401     # Poller object expects milliseconds
402     timeout *= 1000
403
404   poller = select.poll()
405   poller.register(fdobj, event)
406   try:
407     # TODO: If the main thread receives a signal and we have no timeout, we
408     # could wait forever. This should check a global "quit" flag or something
409     # every so often.
410     io_events = poller.poll(timeout)
411   except select.error, err:
412     if err[0] != errno.EINTR:
413       raise
414     io_events = []
415   if io_events and io_events[0][1] & check:
416     return io_events[0][1]
417   else:
418     return None
419
420
421 class FdConditionWaiterHelper(object):
422   """Retry helper for WaitForFdCondition.
423
424   This class contains the retried and wait functions that make sure
425   WaitForFdCondition can continue waiting until the timeout is actually
426   expired.
427
428   """
429
430   def __init__(self, timeout):
431     self.timeout = timeout
432
433   def Poll(self, fdobj, event):
434     result = SingleWaitForFdCondition(fdobj, event, self.timeout)
435     if result is None:
436       raise RetryAgain()
437     else:
438       return result
439
440   def UpdateTimeout(self, timeout):
441     self.timeout = timeout
442
443
444 def WaitForFdCondition(fdobj, event, timeout):
445   """Waits for a condition to occur on the socket.
446
447   Retries until the timeout is expired, even if interrupted.
448
449   @type fdobj: integer or object supporting a fileno() method
450   @param fdobj: entity to wait for events on
451   @type event: integer
452   @param event: ORed condition (see select module)
453   @type timeout: float or None
454   @param timeout: Timeout in seconds
455   @rtype: int or None
456   @return: None for timeout, otherwise occured conditions
457
458   """
459   if timeout is not None:
460     retrywaiter = FdConditionWaiterHelper(timeout)
461     try:
462       result = Retry(retrywaiter.Poll, RETRY_REMAINING_TIME, timeout,
463                      args=(fdobj, event), wait_fn=retrywaiter.UpdateTimeout)
464     except RetryTimeout:
465       result = None
466   else:
467     result = None
468     while result is None:
469       result = SingleWaitForFdCondition(fdobj, event, timeout)
470   return result
471
472
473 def EnsureDaemon(name):
474   """Check for and start daemon if not alive.
475
476   """
477   result = RunCmd([pathutils.DAEMON_UTIL, "check-and-start", name])
478   if result.failed:
479     logging.error("Can't start daemon '%s', failure %s, output: %s",
480                   name, result.fail_reason, result.output)
481     return False
482
483   return True
484
485
486 def StopDaemon(name):
487   """Stop daemon
488
489   """
490   result = RunCmd([pathutils.DAEMON_UTIL, "stop", name])
491   if result.failed:
492     logging.error("Can't stop daemon '%s', failure %s, output: %s",
493                   name, result.fail_reason, result.output)
494     return False
495
496   return True
497
498
499 def SplitTime(value):
500   """Splits time as floating point number into a tuple.
501
502   @param value: Time in seconds
503   @type value: int or float
504   @return: Tuple containing (seconds, microseconds)
505
506   """
507   (seconds, microseconds) = divmod(int(value * 1000000), 1000000)
508
509   assert 0 <= seconds, \
510     "Seconds must be larger than or equal to 0, but are %s" % seconds
511   assert 0 <= microseconds <= 999999, \
512     "Microseconds must be 0-999999, but are %s" % microseconds
513
514   return (int(seconds), int(microseconds))
515
516
517 def MergeTime(timetuple):
518   """Merges a tuple into time as a floating point number.
519
520   @param timetuple: Time as tuple, (seconds, microseconds)
521   @type timetuple: tuple
522   @return: Time as a floating point number expressed in seconds
523
524   """
525   (seconds, microseconds) = timetuple
526
527   assert 0 <= seconds, \
528     "Seconds must be larger than or equal to 0, but are %s" % seconds
529   assert 0 <= microseconds <= 999999, \
530     "Microseconds must be 0-999999, but are %s" % microseconds
531
532   return float(seconds) + (float(microseconds) * 0.000001)
533
534
535 def EpochNano():
536   """Return the current timestamp expressed as number of nanoseconds since the
537   unix epoch
538
539   @return: nanoseconds since the Unix epoch
540
541   """
542   return int(time.time() * 1000000000)
543
544
545 def FindMatch(data, name):
546   """Tries to find an item in a dictionary matching a name.
547
548   Callers have to ensure the data names aren't contradictory (e.g. a regexp
549   that matches a string). If the name isn't a direct key, all regular
550   expression objects in the dictionary are matched against it.
551
552   @type data: dict
553   @param data: Dictionary containing data
554   @type name: string
555   @param name: Name to look for
556   @rtype: tuple; (value in dictionary, matched groups as list)
557
558   """
559   if name in data:
560     return (data[name], [])
561
562   for key, value in data.items():
563     # Regex objects
564     if hasattr(key, "match"):
565       m = key.match(name)
566       if m:
567         return (value, list(m.groups()))
568
569   return None
570
571
572 def GetMounts(filename=constants.PROC_MOUNTS):
573   """Returns the list of mounted filesystems.
574
575   This function is Linux-specific.
576
577   @param filename: path of mounts file (/proc/mounts by default)
578   @rtype: list of tuples
579   @return: list of mount entries (device, mountpoint, fstype, options)
580
581   """
582   # TODO(iustin): investigate non-Linux options (e.g. via mount output)
583   data = []
584   mountlines = ReadFile(filename).splitlines()
585   for line in mountlines:
586     device, mountpoint, fstype, options, _ = line.split(None, 4)
587     data.append((device, mountpoint, fstype, options))
588
589   return data
590
591
592 def SignalHandled(signums):
593   """Signal Handled decoration.
594
595   This special decorator installs a signal handler and then calls the target
596   function. The function must accept a 'signal_handlers' keyword argument,
597   which will contain a dict indexed by signal number, with SignalHandler
598   objects as values.
599
600   The decorator can be safely stacked with iself, to handle multiple signals
601   with different handlers.
602
603   @type signums: list
604   @param signums: signals to intercept
605
606   """
607   def wrap(fn):
608     def sig_function(*args, **kwargs):
609       assert "signal_handlers" not in kwargs or \
610              kwargs["signal_handlers"] is None or \
611              isinstance(kwargs["signal_handlers"], dict), \
612              "Wrong signal_handlers parameter in original function call"
613       if "signal_handlers" in kwargs and kwargs["signal_handlers"] is not None:
614         signal_handlers = kwargs["signal_handlers"]
615       else:
616         signal_handlers = {}
617         kwargs["signal_handlers"] = signal_handlers
618       sighandler = SignalHandler(signums)
619       try:
620         for sig in signums:
621           signal_handlers[sig] = sighandler
622         return fn(*args, **kwargs)
623       finally:
624         sighandler.Reset()
625     return sig_function
626   return wrap
627
628
629 def TimeoutExpired(epoch, timeout, _time_fn=time.time):
630   """Checks whether a timeout has expired.
631
632   """
633   return _time_fn() > (epoch + timeout)
634
635
636 class SignalWakeupFd(object):
637   try:
638     # This is only supported in Python 2.5 and above (some distributions
639     # backported it to Python 2.4)
640     _set_wakeup_fd_fn = signal.set_wakeup_fd
641   except AttributeError:
642     # Not supported
643
644     def _SetWakeupFd(self, _): # pylint: disable=R0201
645       return -1
646   else:
647
648     def _SetWakeupFd(self, fd):
649       return self._set_wakeup_fd_fn(fd)
650
651   def __init__(self):
652     """Initializes this class.
653
654     """
655     (read_fd, write_fd) = os.pipe()
656
657     # Once these succeeded, the file descriptors will be closed automatically.
658     # Buffer size 0 is important, otherwise .read() with a specified length
659     # might buffer data and the file descriptors won't be marked readable.
660     self._read_fh = os.fdopen(read_fd, "r", 0)
661     self._write_fh = os.fdopen(write_fd, "w", 0)
662
663     self._previous = self._SetWakeupFd(self._write_fh.fileno())
664
665     # Utility functions
666     self.fileno = self._read_fh.fileno
667     self.read = self._read_fh.read
668
669   def Reset(self):
670     """Restores the previous wakeup file descriptor.
671
672     """
673     if hasattr(self, "_previous") and self._previous is not None:
674       self._SetWakeupFd(self._previous)
675       self._previous = None
676
677   def Notify(self):
678     """Notifies the wakeup file descriptor.
679
680     """
681     self._write_fh.write(chr(0))
682
683   def __del__(self):
684     """Called before object deletion.
685
686     """
687     self.Reset()
688
689
690 class SignalHandler(object):
691   """Generic signal handler class.
692
693   It automatically restores the original handler when deconstructed or
694   when L{Reset} is called. You can either pass your own handler
695   function in or query the L{called} attribute to detect whether the
696   signal was sent.
697
698   @type signum: list
699   @ivar signum: the signals we handle
700   @type called: boolean
701   @ivar called: tracks whether any of the signals have been raised
702
703   """
704   def __init__(self, signum, handler_fn=None, wakeup=None):
705     """Constructs a new SignalHandler instance.
706
707     @type signum: int or list of ints
708     @param signum: Single signal number or set of signal numbers
709     @type handler_fn: callable
710     @param handler_fn: Signal handling function
711
712     """
713     assert handler_fn is None or callable(handler_fn)
714
715     self.signum = set(signum)
716     self.called = False
717
718     self._handler_fn = handler_fn
719     self._wakeup = wakeup
720
721     self._previous = {}
722     try:
723       for signum in self.signum:
724         # Setup handler
725         prev_handler = signal.signal(signum, self._HandleSignal)
726         try:
727           self._previous[signum] = prev_handler
728         except:
729           # Restore previous handler
730           signal.signal(signum, prev_handler)
731           raise
732     except:
733       # Reset all handlers
734       self.Reset()
735       # Here we have a race condition: a handler may have already been called,
736       # but there's not much we can do about it at this point.
737       raise
738
739   def __del__(self):
740     self.Reset()
741
742   def Reset(self):
743     """Restore previous handler.
744
745     This will reset all the signals to their previous handlers.
746
747     """
748     for signum, prev_handler in self._previous.items():
749       signal.signal(signum, prev_handler)
750       # If successful, remove from dict
751       del self._previous[signum]
752
753   def Clear(self):
754     """Unsets the L{called} flag.
755
756     This function can be used in case a signal may arrive several times.
757
758     """
759     self.called = False
760
761   def _HandleSignal(self, signum, frame):
762     """Actual signal handling function.
763
764     """
765     # This is not nice and not absolutely atomic, but it appears to be the only
766     # solution in Python -- there are no atomic types.
767     self.called = True
768
769     if self._wakeup:
770       # Notify whoever is interested in signals
771       self._wakeup.Notify()
772
773     if self._handler_fn:
774       self._handler_fn(signum, frame)
775
776
777 class FieldSet(object):
778   """A simple field set.
779
780   Among the features are:
781     - checking if a string is among a list of static string or regex objects
782     - checking if a whole list of string matches
783     - returning the matching groups from a regex match
784
785   Internally, all fields are held as regular expression objects.
786
787   """
788   def __init__(self, *items):
789     self.items = [re.compile("^%s$" % value) for value in items]
790
791   def Extend(self, other_set):
792     """Extend the field set with the items from another one"""
793     self.items.extend(other_set.items)
794
795   def Matches(self, field):
796     """Checks if a field matches the current set
797
798     @type field: str
799     @param field: the string to match
800     @return: either None or a regular expression match object
801
802     """
803     for m in itertools.ifilter(None, (val.match(field) for val in self.items)):
804       return m
805     return None
806
807   def NonMatching(self, items):
808     """Returns the list of fields not matching the current set
809
810     @type items: list
811     @param items: the list of fields to check
812     @rtype: list
813     @return: list of non-matching fields
814
815     """
816     return [val for val in items if not self.Matches(val)]
817
818
819 def ValidateDeviceNames(kind, container):
820   """Validate instance device names.
821
822   Check that a device container contains only unique and valid names.
823
824   @type kind: string
825   @param kind: One-word item description
826   @type container: list
827   @param container: Container containing the devices
828
829   """
830
831   valid = []
832   for device in container:
833     if isinstance(device, dict):
834       if kind == "NIC":
835         name = device.get(constants.INIC_NAME, None)
836       elif kind == "disk":
837         name = device.get(constants.IDISK_NAME, None)
838       else:
839         raise errors.OpPrereqError("Invalid container kind '%s'" % kind,
840                                    errors.ECODE_INVAL)
841     else:
842       name = device.name
843       # Check that a device name is not the UUID of another device
844       valid.append(device.uuid)
845
846     try:
847       int(name)
848     except (ValueError, TypeError):
849       pass
850     else:
851       raise errors.OpPrereqError("Invalid name '%s'. Purely numeric %s names"
852                                  " are not allowed" % (name, kind),
853                                  errors.ECODE_INVAL)
854
855     if name is not None and name.lower() != constants.VALUE_NONE:
856       if name in valid:
857         raise errors.OpPrereqError("%s name '%s' already used" % (kind, name),
858                                    errors.ECODE_NOTUNIQUE)
859       else:
860         valid.append(name)