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