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