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