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