1# Copyright (c) 2013 The Chromium OS Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4"""Machine Manager module."""
5
6from __future__ import print_function
7
8import collections
9import file_lock_machine
10import hashlib
11import image_chromeos
12import math
13import os.path
14import re
15import sys
16import threading
17import time
18
19import test_flag
20from cros_utils import command_executer
21from cros_utils import logger
22
23CHECKSUM_FILE = '/usr/local/osimage_checksum_file'
24
25
26class BadChecksum(Exception):
27  """Raised if all machines for a label don't have the same checksum."""
28  pass
29
30
31class BadChecksumString(Exception):
32  """Raised if all machines for a label don't have the same checksum string."""
33  pass
34
35
36class MissingLocksDirectory(Exception):
37  """Raised when cannot find/access the machine locks directory."""
38
39
40class CrosCommandError(Exception):
41  """Raised when an error occurs running command on DUT."""
42
43
44class CrosMachine(object):
45  """The machine class."""
46
47  def __init__(self, name, chromeos_root, log_level, cmd_exec=None):
48    self.name = name
49    self.image = None
50    # We relate a dut with a label if we reimage the dut using label or we
51    # detect at the very beginning that the dut is running this label.
52    self.label = None
53    self.checksum = None
54    self.locked = False
55    self.released_time = time.time()
56    self.test_run = None
57    self.chromeos_root = chromeos_root
58    self.log_level = log_level
59    self.cpuinfo = None
60    self.machine_id = None
61    self.checksum_string = None
62    self.meminfo = None
63    self.phys_kbytes = None
64    self.ce = cmd_exec or command_executer.GetCommandExecuter(
65        log_level=self.log_level)
66    self.SetUpChecksumInfo()
67
68  def SetUpChecksumInfo(self):
69    if not self.IsReachable():
70      self.machine_checksum = None
71      return
72    self._GetMemoryInfo()
73    self._GetCPUInfo()
74    self._ComputeMachineChecksumString()
75    self._GetMachineID()
76    self.machine_checksum = self._GetMD5Checksum(self.checksum_string)
77    self.machine_id_checksum = self._GetMD5Checksum(self.machine_id)
78
79  def IsReachable(self):
80    command = 'ls'
81    ret = self.ce.CrosRunCommand(
82        command, machine=self.name, chromeos_root=self.chromeos_root)
83    if ret:
84      return False
85    return True
86
87  def _ParseMemoryInfo(self):
88    line = self.meminfo.splitlines()[0]
89    usable_kbytes = int(line.split()[1])
90    # This code is from src/third_party/test/files/client/bin/base_utils.py
91    # usable_kbytes is system's usable DRAM in kbytes,
92    #   as reported by memtotal() from device /proc/meminfo memtotal
93    #   after Linux deducts 1.5% to 9.5% for system table overhead
94    # Undo the unknown actual deduction by rounding up
95    #   to next small multiple of a big power-of-two
96    #   eg  12GB - 5.1% gets rounded back up to 12GB
97    mindeduct = 0.005  # 0.5 percent
98    maxdeduct = 0.095  # 9.5 percent
99    # deduction range 1.5% .. 9.5% supports physical mem sizes
100    #    6GB .. 12GB in steps of .5GB
101    #   12GB .. 24GB in steps of 1 GB
102    #   24GB .. 48GB in steps of 2 GB ...
103    # Finer granularity in physical mem sizes would require
104    #   tighter spread between min and max possible deductions
105
106    # increase mem size by at least min deduction, without rounding
107    min_kbytes = int(usable_kbytes / (1.0 - mindeduct))
108    # increase mem size further by 2**n rounding, by 0..roundKb or more
109    round_kbytes = int(usable_kbytes / (1.0 - maxdeduct)) - min_kbytes
110    # find least binary roundup 2**n that covers worst-cast roundKb
111    mod2n = 1 << int(math.ceil(math.log(round_kbytes, 2)))
112    # have round_kbytes <= mod2n < round_kbytes*2
113    # round min_kbytes up to next multiple of mod2n
114    phys_kbytes = min_kbytes + mod2n - 1
115    phys_kbytes -= phys_kbytes % mod2n  # clear low bits
116    self.phys_kbytes = phys_kbytes
117
118  def _GetMemoryInfo(self):
119    #TODO yunlian: when the machine in rebooting, it will not return
120    #meminfo, the assert does not catch it either
121    command = 'cat /proc/meminfo'
122    ret, self.meminfo, _ = self.ce.CrosRunCommandWOutput(
123        command, machine=self.name, chromeos_root=self.chromeos_root)
124    assert ret == 0, 'Could not get meminfo from machine: %s' % self.name
125    if ret == 0:
126      self._ParseMemoryInfo()
127
128  def _GetCPUInfo(self):
129    command = 'cat /proc/cpuinfo'
130    ret, self.cpuinfo, _ = self.ce.CrosRunCommandWOutput(
131        command, machine=self.name, chromeos_root=self.chromeos_root)
132    assert ret == 0, 'Could not get cpuinfo from machine: %s' % self.name
133
134  def _ComputeMachineChecksumString(self):
135    self.checksum_string = ''
136    exclude_lines_list = ['MHz', 'BogoMIPS', 'bogomips']
137    for line in self.cpuinfo.splitlines():
138      if not any(e in line for e in exclude_lines_list):
139        self.checksum_string += line
140    self.checksum_string += ' ' + str(self.phys_kbytes)
141
142  def _GetMD5Checksum(self, ss):
143    if ss:
144      return hashlib.md5(ss).hexdigest()
145    else:
146      return ''
147
148  def _GetMachineID(self):
149    command = 'dump_vpd_log --full --stdout'
150    _, if_out, _ = self.ce.CrosRunCommandWOutput(
151        command, machine=self.name, chromeos_root=self.chromeos_root)
152    b = if_out.splitlines()
153    a = [l for l in b if 'Product' in l]
154    if len(a):
155      self.machine_id = a[0]
156      return
157    command = 'ifconfig'
158    _, if_out, _ = self.ce.CrosRunCommandWOutput(
159        command, machine=self.name, chromeos_root=self.chromeos_root)
160    b = if_out.splitlines()
161    a = [l for l in b if 'HWaddr' in l]
162    if len(a):
163      self.machine_id = '_'.join(a)
164      return
165    a = [l for l in b if 'ether' in l]
166    if len(a):
167      self.machine_id = '_'.join(a)
168      return
169    assert 0, 'Could not get machine_id from machine: %s' % self.name
170
171  def __str__(self):
172    l = []
173    l.append(self.name)
174    l.append(str(self.image))
175    l.append(str(self.checksum))
176    l.append(str(self.locked))
177    l.append(str(self.released_time))
178    return ', '.join(l)
179
180
181class MachineManager(object):
182  """Lock, image and unlock machines locally for benchmark runs.
183
184  This class contains methods and calls to lock, unlock and image
185  machines and distribute machines to each benchmark run.  The assumption is
186  that all of the machines for the experiment have been globally locked
187  (using an AFE server) in the ExperimentRunner, but the machines still need
188  to be locally locked/unlocked (allocated to benchmark runs) to prevent
189  multiple benchmark runs within the same experiment from trying to use the
190  same machine at the same time.
191  """
192
193  def __init__(self,
194               chromeos_root,
195               acquire_timeout,
196               log_level,
197               locks_dir,
198               cmd_exec=None,
199               lgr=None):
200    self._lock = threading.RLock()
201    self._all_machines = []
202    self._machines = []
203    self.image_lock = threading.Lock()
204    self.num_reimages = 0
205    self.chromeos_root = None
206    self.machine_checksum = {}
207    self.machine_checksum_string = {}
208    self.acquire_timeout = acquire_timeout
209    self.log_level = log_level
210    self.locks_dir = locks_dir
211    self.ce = cmd_exec or command_executer.GetCommandExecuter(
212        log_level=self.log_level)
213    self.logger = lgr or logger.GetLogger()
214
215    if self.locks_dir and not os.path.isdir(self.locks_dir):
216      raise MissingLocksDirectory(
217          'Cannot access locks directory: %s' % self.locks_dir)
218
219    self._initialized_machines = []
220    self.chromeos_root = chromeos_root
221
222  def RemoveNonLockedMachines(self, locked_machines):
223    for m in self._all_machines:
224      if m.name not in locked_machines:
225        self._all_machines.remove(m)
226
227    for m in self._machines:
228      if m.name not in locked_machines:
229        self._machines.remove(m)
230
231  def GetChromeVersion(self, machine):
232    """Get the version of Chrome running on the DUT."""
233
234    cmd = '/opt/google/chrome/chrome --version'
235    ret, version, _ = self.ce.CrosRunCommandWOutput(
236        cmd, machine=machine.name, chromeos_root=self.chromeos_root)
237    if ret != 0:
238      raise CrosCommandError(
239          "Couldn't get Chrome version from %s." % machine.name)
240
241    if ret != 0:
242      version = ''
243    return version.rstrip()
244
245  def ImageMachine(self, machine, label):
246    checksum = label.checksum
247
248    if checksum and (machine.checksum == checksum):
249      return
250    chromeos_root = label.chromeos_root
251    if not chromeos_root:
252      chromeos_root = self.chromeos_root
253    image_chromeos_args = [
254        image_chromeos.__file__, '--no_lock',
255        '--chromeos_root=%s' % chromeos_root,
256        '--image=%s' % label.chromeos_image,
257        '--image_args=%s' % label.image_args, '--remote=%s' % machine.name,
258        '--logging_level=%s' % self.log_level
259    ]
260    if label.board:
261      image_chromeos_args.append('--board=%s' % label.board)
262
263    # Currently can't image two machines at once.
264    # So have to serialized on this lock.
265    save_ce_log_level = self.ce.log_level
266    if self.log_level != 'verbose':
267      self.ce.log_level = 'average'
268
269    with self.image_lock:
270      if self.log_level != 'verbose':
271        self.logger.LogOutput('Pushing image onto machine.')
272        self.logger.LogOutput('Running image_chromeos.DoImage with %s' %
273                              ' '.join(image_chromeos_args))
274      retval = 0
275      if not test_flag.GetTestMode():
276        retval = image_chromeos.DoImage(image_chromeos_args)
277      if retval:
278        cmd = 'reboot && exit'
279        if self.log_level != 'verbose':
280          self.logger.LogOutput('reboot & exit.')
281        self.ce.CrosRunCommand(
282            cmd, machine=machine.name, chromeos_root=self.chromeos_root)
283        time.sleep(60)
284        if self.log_level != 'verbose':
285          self.logger.LogOutput('Pushing image onto machine.')
286          self.logger.LogOutput('Running image_chromeos.DoImage with %s' %
287                                ' '.join(image_chromeos_args))
288        retval = image_chromeos.DoImage(image_chromeos_args)
289      if retval:
290        raise RuntimeError("Could not image machine: '%s'." % machine.name)
291      else:
292        self.num_reimages += 1
293      machine.checksum = checksum
294      machine.image = label.chromeos_image
295      machine.label = label
296
297    if not label.chrome_version:
298      label.chrome_version = self.GetChromeVersion(machine)
299
300    self.ce.log_level = save_ce_log_level
301    return retval
302
303  def ComputeCommonCheckSum(self, label):
304    # Since this is used for cache lookups before the machines have been
305    # compared/verified, check here to make sure they all have the same
306    # checksum (otherwise the cache lookup may not be valid).
307    common_checksum = None
308    for machine in self.GetMachines(label):
309      # Make sure the machine's checksums are calculated.
310      if not machine.machine_checksum:
311        machine.SetUpChecksumInfo()
312      cs = machine.machine_checksum
313      # If this is the first machine we've examined, initialize
314      # common_checksum.
315      if not common_checksum:
316        common_checksum = cs
317      # Make sure this machine's checksum matches our 'common' checksum.
318      if cs != common_checksum:
319        raise BadChecksum('Machine checksums do not match!')
320    self.machine_checksum[label.name] = common_checksum
321
322  def ComputeCommonCheckSumString(self, label):
323    # The assumption is that this function is only called AFTER
324    # ComputeCommonCheckSum, so there is no need to verify the machines
325    # are the same here.  If this is ever changed, this function should be
326    # modified to verify that all the machines for a given label are the
327    # same.
328    for machine in self.GetMachines(label):
329      if machine.checksum_string:
330        self.machine_checksum_string[label.name] = machine.checksum_string
331        break
332
333  def _TryToLockMachine(self, cros_machine):
334    with self._lock:
335      assert cros_machine, "Machine can't be None"
336      for m in self._machines:
337        if m.name == cros_machine.name:
338          return
339      locked = True
340      if self.locks_dir:
341        locked = file_lock_machine.Machine(cros_machine.name,
342                                           self.locks_dir).Lock(
343                                               True, sys.argv[0])
344      if locked:
345        self._machines.append(cros_machine)
346        command = 'cat %s' % CHECKSUM_FILE
347        ret, out, _ = self.ce.CrosRunCommandWOutput(
348            command,
349            chromeos_root=self.chromeos_root,
350            machine=cros_machine.name)
351        if ret == 0:
352          cros_machine.checksum = out.strip()
353      elif self.locks_dir:
354        self.logger.LogOutput("Couldn't lock: %s" % cros_machine.name)
355
356  # This is called from single threaded mode.
357  def AddMachine(self, machine_name):
358    with self._lock:
359      for m in self._all_machines:
360        assert m.name != machine_name, 'Tried to double-add %s' % machine_name
361
362      if self.log_level != 'verbose':
363        self.logger.LogOutput('Setting up remote access to %s' % machine_name)
364        self.logger.LogOutput(
365            'Checking machine characteristics for %s' % machine_name)
366      cm = CrosMachine(machine_name, self.chromeos_root, self.log_level)
367      if cm.machine_checksum:
368        self._all_machines.append(cm)
369
370  def RemoveMachine(self, machine_name):
371    with self._lock:
372      self._machines = [m for m in self._machines if m.name != machine_name]
373      if self.locks_dir:
374        res = file_lock_machine.Machine(machine_name,
375                                        self.locks_dir).Unlock(True)
376        if not res:
377          self.logger.LogError("Could not unlock machine: '%s'." % machine_name)
378
379  def ForceSameImageToAllMachines(self, label):
380    machines = self.GetMachines(label)
381    for m in machines:
382      self.ImageMachine(m, label)
383      m.SetUpChecksumInfo()
384
385  def AcquireMachine(self, label):
386    image_checksum = label.checksum
387    machines = self.GetMachines(label)
388    check_interval_time = 120
389    with self._lock:
390      # Lazily external lock machines
391      while self.acquire_timeout >= 0:
392        for m in machines:
393          new_machine = m not in self._all_machines
394          self._TryToLockMachine(m)
395          if new_machine:
396            m.released_time = time.time()
397        if self.GetAvailableMachines(label):
398          break
399        sleep_time = max(1, min(self.acquire_timeout, check_interval_time))
400        time.sleep(sleep_time)
401        self.acquire_timeout -= sleep_time
402
403      if self.acquire_timeout < 0:
404        self.logger.LogFatal(
405            'Could not acquire any of the '
406            "following machines: '%s'" % ', '.join(machine.name
407                                                   for machine in machines))
408
409###      for m in self._machines:
410###        if (m.locked and time.time() - m.released_time < 10 and
411###            m.checksum == image_checksum):
412###          return None
413      unlocked_machines = [
414          machine for machine in self.GetAvailableMachines(label)
415          if not machine.locked
416      ]
417      for m in unlocked_machines:
418        if image_checksum and m.checksum == image_checksum:
419          m.locked = True
420          m.test_run = threading.current_thread()
421          return m
422      for m in unlocked_machines:
423        if not m.checksum:
424          m.locked = True
425          m.test_run = threading.current_thread()
426          return m
427      # This logic ensures that threads waiting on a machine will get a machine
428      # with a checksum equal to their image over other threads. This saves time
429      # when crosperf initially assigns the machines to threads by minimizing
430      # the number of re-images.
431      # TODO(asharif): If we centralize the thread-scheduler, we wont need this
432      # code and can implement minimal reimaging code more cleanly.
433      for m in unlocked_machines:
434        if time.time() - m.released_time > 15:
435          # The release time gap is too large, so it is probably in the start
436          # stage, we need to reset the released_time.
437          m.released_time = time.time()
438        elif time.time() - m.released_time > 8:
439          m.locked = True
440          m.test_run = threading.current_thread()
441          return m
442    return None
443
444  def GetAvailableMachines(self, label=None):
445    if not label:
446      return self._machines
447    return [m for m in self._machines if m.name in label.remote]
448
449  def GetMachines(self, label=None):
450    if not label:
451      return self._all_machines
452    return [m for m in self._all_machines if m.name in label.remote]
453
454  def ReleaseMachine(self, machine):
455    with self._lock:
456      for m in self._machines:
457        if machine.name == m.name:
458          assert m.locked, 'Tried to double-release %s' % m.name
459          m.released_time = time.time()
460          m.locked = False
461          m.status = 'Available'
462          break
463
464  def Cleanup(self):
465    with self._lock:
466      # Unlock all machines (via file lock)
467      for m in self._machines:
468        res = file_lock_machine.Machine(m.name, self.locks_dir).Unlock(True)
469
470        if not res:
471          self.logger.LogError("Could not unlock machine: '%s'." % m.name)
472
473  def __str__(self):
474    with self._lock:
475      l = ['MachineManager Status:'] + [str(m) for m in self._machines]
476      return '\n'.join(l)
477
478  def AsString(self):
479    with self._lock:
480      stringify_fmt = '%-30s %-10s %-4s %-25s %-32s'
481      header = stringify_fmt % ('Machine', 'Thread', 'Lock', 'Status',
482                                'Checksum')
483      table = [header]
484      for m in self._machines:
485        if m.test_run:
486          test_name = m.test_run.name
487          test_status = m.test_run.timeline.GetLastEvent()
488        else:
489          test_name = ''
490          test_status = ''
491
492        try:
493          machine_string = stringify_fmt % (m.name, test_name, m.locked,
494                                            test_status, m.checksum)
495        except ValueError:
496          machine_string = ''
497        table.append(machine_string)
498      return 'Machine Status:\n%s' % '\n'.join(table)
499
500  def GetAllCPUInfo(self, labels):
501    """Get cpuinfo for labels, merge them if their cpuinfo are the same."""
502    dic = collections.defaultdict(list)
503    for label in labels:
504      for machine in self._all_machines:
505        if machine.name in label.remote:
506          dic[machine.cpuinfo].append(label.name)
507          break
508    output_segs = []
509    for key, v in dic.iteritems():
510      output = ' '.join(v)
511      output += '\n-------------------\n'
512      output += key
513      output += '\n\n\n'
514      output_segs.append(output)
515    return ''.join(output_segs)
516
517  def GetAllMachines(self):
518    return self._all_machines
519
520
521class MockCrosMachine(CrosMachine):
522  """Mock cros machine class."""
523  # pylint: disable=super-init-not-called
524
525  MEMINFO_STRING = """MemTotal:        3990332 kB
526MemFree:         2608396 kB
527Buffers:          147168 kB
528Cached:           811560 kB
529SwapCached:            0 kB
530Active:           503480 kB
531Inactive:         628572 kB
532Active(anon):     174532 kB
533Inactive(anon):    88576 kB
534Active(file):     328948 kB
535Inactive(file):   539996 kB
536Unevictable:           0 kB
537Mlocked:               0 kB
538SwapTotal:       5845212 kB
539SwapFree:        5845212 kB
540Dirty:              9384 kB
541Writeback:             0 kB
542AnonPages:        173408 kB
543Mapped:           146268 kB
544Shmem:             89676 kB
545Slab:             188260 kB
546SReclaimable:     169208 kB
547SUnreclaim:        19052 kB
548KernelStack:        2032 kB
549PageTables:         7120 kB
550NFS_Unstable:          0 kB
551Bounce:                0 kB
552WritebackTmp:          0 kB
553CommitLimit:     7840376 kB
554Committed_AS:    1082032 kB
555VmallocTotal:   34359738367 kB
556VmallocUsed:      364980 kB
557VmallocChunk:   34359369407 kB
558DirectMap4k:       45824 kB
559DirectMap2M:     4096000 kB
560"""
561
562  CPUINFO_STRING = """processor: 0
563vendor_id: GenuineIntel
564cpu family: 6
565model: 42
566model name: Intel(R) Celeron(R) CPU 867 @ 1.30GHz
567stepping: 7
568microcode: 0x25
569cpu MHz: 1300.000
570cache size: 2048 KB
571physical id: 0
572siblings: 2
573core id: 0
574cpu cores: 2
575apicid: 0
576initial apicid: 0
577fpu: yes
578fpu_exception: yes
579cpuid level: 13
580wp: yes
581flags: fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx rdtscp lm constant_tsc arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc aperfmperf pni pclmulqdq dtes64 monitor ds_cpl vmx est tm2 ssse3 cx16 xtpr pdcm pcid sse4_1 sse4_2 x2apic popcnt tsc_deadline_timer xsave lahf_lm arat epb xsaveopt pln pts dts tpr_shadow vnmi flexpriority ept vpid
582bogomips: 2594.17
583clflush size: 64
584cache_alignment: 64
585address sizes: 36 bits physical, 48 bits virtual
586power management:
587
588processor: 1
589vendor_id: GenuineIntel
590cpu family: 6
591model: 42
592model name: Intel(R) Celeron(R) CPU 867 @ 1.30GHz
593stepping: 7
594microcode: 0x25
595cpu MHz: 1300.000
596cache size: 2048 KB
597physical id: 0
598siblings: 2
599core id: 1
600cpu cores: 2
601apicid: 2
602initial apicid: 2
603fpu: yes
604fpu_exception: yes
605cpuid level: 13
606wp: yes
607flags: fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx rdtscp lm constant_tsc arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc aperfmperf pni pclmulqdq dtes64 monitor ds_cpl vmx est tm2 ssse3 cx16 xtpr pdcm pcid sse4_1 sse4_2 x2apic popcnt tsc_deadline_timer xsave lahf_lm arat epb xsaveopt pln pts dts tpr_shadow vnmi flexpriority ept vpid
608bogomips: 2594.17
609clflush size: 64
610cache_alignment: 64
611address sizes: 36 bits physical, 48 bits virtual
612power management:
613"""
614
615  def __init__(self, name, chromeos_root, log_level):
616    self.name = name
617    self.image = None
618    self.checksum = None
619    self.locked = False
620    self.released_time = time.time()
621    self.test_run = None
622    self.chromeos_root = chromeos_root
623    self.checksum_string = re.sub(r'\d', '', name)
624    #In test, we assume "lumpy1", "lumpy2" are the same machine.
625    self.machine_checksum = self._GetMD5Checksum(self.checksum_string)
626    self.log_level = log_level
627    self.label = None
628    self.ce = command_executer.GetCommandExecuter(log_level=self.log_level)
629    self._GetCPUInfo()
630
631  def IsReachable(self):
632    return True
633
634  def _GetMemoryInfo(self):
635    self.meminfo = self.MEMINFO_STRING
636    self._ParseMemoryInfo()
637
638  def _GetCPUInfo(self):
639    self.cpuinfo = self.CPUINFO_STRING
640
641
642class MockMachineManager(MachineManager):
643  """Mock machine manager class."""
644
645  def __init__(self, chromeos_root, acquire_timeout, log_level, locks_dir):
646    super(MockMachineManager, self).__init__(chromeos_root, acquire_timeout,
647                                             log_level, locks_dir)
648
649  def _TryToLockMachine(self, cros_machine):
650    self._machines.append(cros_machine)
651    cros_machine.checksum = ''
652
653  def AddMachine(self, machine_name):
654    with self._lock:
655      for m in self._all_machines:
656        assert m.name != machine_name, 'Tried to double-add %s' % machine_name
657      cm = MockCrosMachine(machine_name, self.chromeos_root, self.log_level)
658      assert cm.machine_checksum, (
659          'Could not find checksum for machine %s' % machine_name)
660      # In Original MachineManager, the test is 'if cm.machine_checksum:' - if a
661      # machine is unreachable, then its machine_checksum is None. Here we
662      # cannot do this, because machine_checksum is always faked, so we directly
663      # test cm.IsReachable, which is properly mocked.
664      if cm.IsReachable():
665        self._all_machines.append(cm)
666
667  def GetChromeVersion(self, machine):
668    return 'Mock Chrome Version R50'
669
670  def AcquireMachine(self, label):
671    for machine in self._all_machines:
672      if not machine.locked:
673        machine.locked = True
674        return machine
675    return None
676
677  def ImageMachine(self, machine_name, label):
678    if machine_name or label:
679      return 0
680    return 1
681
682  def ReleaseMachine(self, machine):
683    machine.locked = False
684
685  def GetMachines(self, label=None):
686    return self._all_machines
687
688  def GetAvailableMachines(self, label=None):
689    return self._all_machines
690
691  def ForceSameImageToAllMachines(self, label=None):
692    return 0
693
694  def ComputeCommonCheckSum(self, label=None):
695    common_checksum = 12345
696    for machine in self.GetMachines(label):
697      machine.machine_checksum = common_checksum
698    self.machine_checksum[label.name] = common_checksum
699
700  def GetAllMachines(self):
701    return self._all_machines
702