blob: a273a79562a1e4a0627d5f227173aca5a506dc25 [file] [log] [blame]
Caroline Ticef6ef4392017-04-06 17:16:05 -07001#!/usr/bin/env python2
Zhizhou Yangcdd9e342019-09-19 20:56:32 -07002# -*- coding: utf-8 -*-
3# Copyright 2019 The Chromium OS Authors. All rights reserved.
4# Use of this source code is governed by a BSD-style license that can be
5# found in the LICENSE file.
6
Caroline Ticea4486452015-12-08 13:43:23 -08007"""This module controls locking and unlocking of test machines."""
8
9from __future__ import print_function
10
cmticee5bc63b2015-05-27 16:59:37 -070011import argparse
12import getpass
13import os
14import sys
15import traceback
16
Zhizhou Yangcdd9e342019-09-19 20:56:32 -070017import file_lock_machine
18
19from cros_utils import command_executer
Caroline Ticea8af9a72016-07-20 12:52:59 -070020from cros_utils import logger
21from cros_utils import machines
cmticee5bc63b2015-05-27 16:59:37 -070022
Luis Lozanof2a3ef42015-12-15 13:49:30 -080023
cmticee5bc63b2015-05-27 16:59:37 -070024class AFELockException(Exception):
25 """Base class for exceptions in this module."""
26
27
28class MachineNotPingable(AFELockException):
29 """Raised when machine does not respond to ping."""
30
31
32class MissingHostInfo(AFELockException):
33 """Raised when cannot find info about machine on machine servers."""
34
35
36class UpdateNonLocalMachine(AFELockException):
37 """Raised when user requests to add/remove a ChromeOS HW Lab machine.."""
38
39
40class DuplicateAdd(AFELockException):
41 """Raised when user requests to add a machine that's already on the server."""
42
43
44class UpdateServerError(AFELockException):
45 """Raised when attempt to add/remove a machine from local server fails."""
46
47
48class LockingError(AFELockException):
49 """Raised when server fails to lock/unlock machine as requested."""
50
51
cmticee5bc63b2015-05-27 16:59:37 -070052class DontOwnLock(AFELockException):
53 """Raised when user attmepts to unlock machine locked by someone else."""
54 # This should not be raised if the user specified '--force'
55
56
57class NoAFEServer(AFELockException):
58 """Raised when cannot find/access the autotest server."""
59
60
61class AFEAccessError(AFELockException):
62 """Raised when cannot get information about lab machine from lab server."""
63
64
65class AFELockManager(object):
66 """Class for locking/unlocking machines vie Autotest Front End servers.
67
68 This class contains methods for checking the locked status of machines
69 on both the ChromeOS HW Lab AFE server and a local AFE server. It also
70 has methods for adding/removing machines from the local server, and for
71 changing the lock status of machines on either server. For the ChromeOS
72 HW Lab, it only allows access to the toolchain team lab machines, as
73 defined in toolchain-utils/crosperf/default_remotes. By default it will
Manoj Gupta4ed81bc2017-04-20 13:06:33 -070074 look for a local server on chrotomation2.svl.corp.google.com, but an
cmticee5bc63b2015-05-27 16:59:37 -070075 alternative local AFE server can be supplied, if desired.
76
77 !!!IMPORTANT NOTE!!! The AFE server can only be called from the main
78 thread/process of a program. If you launch threads and try to call it
79 from a thread, you will get an error. This has to do with restrictions
80 in the Python virtual machine (and signal handling) and cannot be changed.
81 """
82
Manoj Gupta4ed81bc2017-04-20 13:06:33 -070083 LOCAL_SERVER = 'chrotomation2.svl.corp.google.com'
Zhizhou Yangcdd9e342019-09-19 20:56:32 -070084 SKYLAB_PATH = '/usr/local/bin/skylab'
85 LEASE_MINS = 600
86 SKYLAB_CREDENTIAL = '/usr/local/google/home/mobiletc-prebuild/' \
87 'chromeos-swarming-1adbe355c97c.json'
88 SWARMING = 'chromite/third_party/swarming.client/swarming.py'
89 SUCCESS = 0
cmticee5bc63b2015-05-27 16:59:37 -070090
Luis Lozanof2a3ef42015-12-15 13:49:30 -080091 def __init__(self,
92 remotes,
93 force_option,
94 chromeos_root,
95 local_server,
Zhizhou Yangcdd9e342019-09-19 20:56:32 -070096 locks_dir='',
97 use_local=False,
Luis Lozanof2a3ef42015-12-15 13:49:30 -080098 log=None):
cmticee5bc63b2015-05-27 16:59:37 -070099 """Initializes an AFELockManager object.
100
101 Args:
102 remotes: A list of machine names or ip addresses to be managed. Names
Caroline Ticea4486452015-12-08 13:43:23 -0800103 and ip addresses should be represented as strings. If the list is
104 empty, the lock manager will get all known machines.
105 force_option: A Boolean indicating whether or not to force an unlock of
cmticee5bc63b2015-05-27 16:59:37 -0700106 a machine that was locked by someone else.
107 chromeos_root: The ChromeOS chroot to use for the autotest scripts.
Caroline Ticea4486452015-12-08 13:43:23 -0800108 local_server: A string containing the name or ip address of the machine
cmticee5bc63b2015-05-27 16:59:37 -0700109 that is running an AFE server, which is to be used for managing
110 machines that are not in the ChromeOS HW lab.
Zhizhou Yangcdd9e342019-09-19 20:56:32 -0700111 locks_dir: A directory used for file locking local devices.
cmticee5bc63b2015-05-27 16:59:37 -0700112 local: A Boolean indicating whether or not to use/allow a local AFE
113 server to be used (see local_server argument).
Caroline Ticef6ef4392017-04-06 17:16:05 -0700114 use_local: Use the local server instead of the official one.
cmticee5bc63b2015-05-27 16:59:37 -0700115 log: If not None, this is the logger object to be used for writing out
116 informational output messages. It is expected to be an instance of
Caroline Ticea8af9a72016-07-20 12:52:59 -0700117 Logger class from cros_utils/logger.py.
cmticee5bc63b2015-05-27 16:59:37 -0700118 """
119 self.chromeos_root = chromeos_root
120 self.user = getpass.getuser()
121 self.logger = log or logger.GetLogger()
Zhizhou Yangcdd9e342019-09-19 20:56:32 -0700122 self.ce = command_executer.GetCommandExecuter(self.logger)
cmticee5bc63b2015-05-27 16:59:37 -0700123 autotest_path = os.path.join(chromeos_root,
124 'src/third_party/autotest/files')
125
cmticed1172b42015-06-12 15:14:09 -0700126 sys.path.append(chromeos_root)
cmticee5bc63b2015-05-27 16:59:37 -0700127 sys.path.append(autotest_path)
128 sys.path.append(os.path.join(autotest_path, 'server', 'cros'))
129
Zhizhou Yangcdd9e342019-09-19 20:56:32 -0700130 self.locks_dir = locks_dir
131
cmticee5bc63b2015-05-27 16:59:37 -0700132 # We have to wait to do these imports until the paths above have
133 # been fixed.
Yunlian Jiangd97422a2015-12-16 11:06:13 -0800134 # pylint: disable=import-error
cmticee5bc63b2015-05-27 16:59:37 -0700135 from client import setup_modules
Caroline Tice6b161382016-09-15 15:03:46 -0700136 setup_modules.setup(
137 base_path=autotest_path, root_module_name='autotest_lib')
cmticee5bc63b2015-05-27 16:59:37 -0700138
139 from dynamic_suite import frontend_wrappers
140
Caroline Tice6b161382016-09-15 15:03:46 -0700141 self.afe = frontend_wrappers.RetryingAFE(
142 timeout_min=30, delay_sec=10, debug=False, server='cautotest')
143
144 self.local = use_local
145 self.machines = list(set(remotes)) or []
146 self.toolchain_lab_machines = self.GetAllToolchainLabMachines()
147 if self.machines and self.AllLabMachines():
148 self.local = False
149
150 if not self.local:
cmticee5bc63b2015-05-27 16:59:37 -0700151 self.local_afe = None
152 else:
153 dargs = {}
154 dargs['server'] = local_server or AFELockManager.LOCAL_SERVER
155 # Make sure local server is pingable.
Luis Lozanof2a3ef42015-12-15 13:49:30 -0800156 error_msg = ('Local autotest server machine %s not responding to ping.' %
157 dargs['server'])
cmticee5bc63b2015-05-27 16:59:37 -0700158 self.CheckMachine(dargs['server'], error_msg)
Caroline Tice6b161382016-09-15 15:03:46 -0700159 self.local_afe = frontend_wrappers.RetryingAFE(
160 timeout_min=30, delay_sec=10, debug=False, **dargs)
cmticee5bc63b2015-05-27 16:59:37 -0700161 if not self.machines:
162 self.machines = self.toolchain_lab_machines + self.GetAllNonlabMachines()
Caroline Tice6b161382016-09-15 15:03:46 -0700163 self.force = force_option
164
Zhizhou Yangcdd9e342019-09-19 20:56:32 -0700165 self.local_machines = []
166 self.skylab_machines = []
167
Caroline Tice6b161382016-09-15 15:03:46 -0700168 def AllLabMachines(self):
169 """Check to see if all machines being used are HW Lab machines."""
170 all_lab = True
171 for m in self.machines:
172 if m not in self.toolchain_lab_machines:
173 all_lab = False
174 break
175 return all_lab
cmticee5bc63b2015-05-27 16:59:37 -0700176
177 def CheckMachine(self, machine, error_msg):
178 """Verifies that machine is responding to ping.
179
180 Args:
181 machine: String containing the name or ip address of machine to check.
182 error_msg: Message to print if ping fails.
183
184 Raises:
185 MachineNotPingable: If machine is not responding to 'ping'
186 """
187 if not machines.MachineIsPingable(machine, logging_level='none'):
Caroline Ticea4486452015-12-08 13:43:23 -0800188 cros_machine = machine + '.cros'
189 if not machines.MachineIsPingable(cros_machine, logging_level='none'):
190 raise MachineNotPingable(error_msg)
cmticee5bc63b2015-05-27 16:59:37 -0700191
192 def MachineIsKnown(self, machine):
193 """Checks to see if either AFE server knows the given machine.
194
195 Args:
196 machine: String containing name or ip address of machine to check.
197
198 Returns:
199 Boolean indicating if the machine is in the list of known machines for
200 either AFE server.
201 """
202 if machine in self.toolchain_lab_machines:
203 return True
204 elif self.local_afe and machine in self.GetAllNonlabMachines():
205 return True
206
207 return False
208
209 def GetAllToolchainLabMachines(self):
210 """Gets a list of all the toolchain machines in the ChromeOS HW lab.
211
212 Returns:
213 A list of names of the toolchain machines in the ChromeOS HW lab.
214 """
Luis Lozanof2a3ef42015-12-15 13:49:30 -0800215 machines_file = os.path.join(
216 os.path.dirname(__file__), 'crosperf', 'default_remotes')
cmticee5bc63b2015-05-27 16:59:37 -0700217 machine_list = []
218 with open(machines_file, 'r') as input_file:
219 lines = input_file.readlines()
220 for line in lines:
Caroline Ticea4486452015-12-08 13:43:23 -0800221 _, remotes = line.split(':')
cmticee5bc63b2015-05-27 16:59:37 -0700222 remotes = remotes.strip()
223 for r in remotes.split():
224 machine_list.append(r.strip())
225 return machine_list
226
227 def GetAllNonlabMachines(self):
228 """Gets a list of all known machines on the local AFE server.
229
230 Returns:
231 A list of the names of the machines on the local AFE server.
232 """
233 non_lab_machines = []
234 if self.local_afe:
235 non_lab_machines = self.local_afe.get_hostnames()
236 return non_lab_machines
237
238 def PrintStatusHeader(self, is_lab_machine):
239 """Prints the status header lines for machines.
240
Caroline Ticea4486452015-12-08 13:43:23 -0800241 Args:
242 is_lab_machine: Boolean indicating whether to print HW Lab header or
243 local machine header (different spacing).
cmticee5bc63b2015-05-27 16:59:37 -0700244 """
245 if is_lab_machine:
Caroline Ticea4486452015-12-08 13:43:23 -0800246 print('\nMachine (Board)\t\t\t\t\tStatus')
247 print('---------------\t\t\t\t\t------\n')
cmticee5bc63b2015-05-27 16:59:37 -0700248 else:
Caroline Ticea4486452015-12-08 13:43:23 -0800249 print('\nMachine (Board)\t\tStatus')
250 print('---------------\t\t------\n')
cmticee5bc63b2015-05-27 16:59:37 -0700251
Zhizhou Yangcdd9e342019-09-19 20:56:32 -0700252 def AddMachineToLocal(self, machine):
253 """Adds a machine to local machine list.
254
255 Args:
256 machine: The machine to be added.
257 """
258 if machine not in self.local_machines:
259 self.local_machines.append(machine)
260
261 def AddMachineToSkylab(self, machine):
262 """Adds a machine to skylab machine list.
263
264 Args:
265 machine: The machine to be added.
266 """
267 if machine not in self.skylab_machines:
268 self.skylab_machines.append(machine)
269
cmticee5bc63b2015-05-27 16:59:37 -0700270 def RemoveLocalMachine(self, m):
271 """Removes a machine from the local AFE server.
272
273 Args:
274 m: The machine to remove.
275
276 Raises:
277 MissingHostInfo: Can't find machine to be removed.
278 """
279 if self.local_afe:
280 host_info = self.local_afe.get_hosts(hostname=m)
281 if host_info:
282 host_info = host_info[0]
283 host_info.delete()
284 else:
285 raise MissingHostInfo('Cannot find/delete machine %s.' % m)
286
287 def AddLocalMachine(self, m):
288 """Adds a machine to the local AFE server.
289
290 Args:
291 m: The machine to be added.
292 """
293 if self.local_afe:
294 error_msg = 'Machine %s is not responding to ping.' % m
295 self.CheckMachine(m, error_msg)
Caroline Ticea4486452015-12-08 13:43:23 -0800296 self.local_afe.create_host(m)
cmticee5bc63b2015-05-27 16:59:37 -0700297
298 def AddMachinesToLocalServer(self):
299 """Adds one or more machines to the local AFE server.
300
301 Verify that the requested machines are legal to add to the local server,
302 i.e. that they are not ChromeOS HW lab machines, and they are not already
303 on the local server. Call AddLocalMachine for each valid machine.
304
305 Raises:
306 DuplicateAdd: Attempt to add a machine that is already on the server.
307 UpdateNonLocalMachine: Attempt to add a ChromeOS HW lab machine.
308 UpdateServerError: Something went wrong while attempting to add a
309 machine.
310 """
311 for m in self.machines:
Caroline Tice3f432712015-12-07 14:51:53 -0800312 for cros_name in [m, m + '.cros']:
313 if cros_name in self.toolchain_lab_machines:
Caroline Ticef6ef4392017-04-06 17:16:05 -0700314 raise UpdateNonLocalMachine(
315 'Machine %s is already in the ChromeOS HW'
316 'Lab. Cannot add it to local server.' % cros_name)
cmticee5bc63b2015-05-27 16:59:37 -0700317 host_info = self.local_afe.get_hosts(hostname=m)
318 if host_info:
319 raise DuplicateAdd('Machine %s is already on the local server.' % m)
320 try:
321 self.AddLocalMachine(m)
322 self.logger.LogOutput('Successfully added %s to local server.' % m)
323 except Exception as e:
324 traceback.print_exc()
Luis Lozanof2a3ef42015-12-15 13:49:30 -0800325 raise UpdateServerError(
326 'Error occurred while attempting to add %s. %s' % (m, str(e)))
cmticee5bc63b2015-05-27 16:59:37 -0700327
328 def RemoveMachinesFromLocalServer(self):
329 """Removes one or more machines from the local AFE server.
330
331 Verify that the requested machines are legal to remove from the local
332 server, i.e. that they are not ChromeOS HW lab machines. Call
333 RemoveLocalMachine for each valid machine.
334
335 Raises:
336 UpdateServerError: Something went wrong while attempting to remove a
337 machine.
338 """
339 for m in self.machines:
Caroline Tice3f432712015-12-07 14:51:53 -0800340 for cros_name in [m, m + '.cros']:
341 if cros_name in self.toolchain_lab_machines:
Luis Lozanof2a3ef42015-12-15 13:49:30 -0800342 raise UpdateNonLocalMachine(
343 'Machine %s is in the ChromeOS HW Lab. '
344 'This script cannot remove lab machines.' % cros_name)
cmticee5bc63b2015-05-27 16:59:37 -0700345 try:
346 self.RemoveLocalMachine(m)
347 self.logger.LogOutput('Successfully removed %s from local server.' % m)
348 except Exception as e:
349 traceback.print_exc()
350 raise UpdateServerError('Error occurred while attempting to remove %s '
351 '(%s).' % (m, str(e)))
352
353 def ListMachineStates(self, machine_states):
354 """Gets and prints the current status for a list of machines.
355
356 Prints out the current status for all of the machines in the current
357 AFELockManager's list of machines (set when the object is initialized).
358
359 Args:
360 machine_states: A dictionary of the current state of every machine in
361 the current AFELockManager's list of machines. Normally obtained by
362 calling AFELockManager::GetMachineStates.
363 """
364 local_machines = []
365 printed_hdr = False
366 for m in machine_states:
367 cros_name = m + '.cros'
368 if (m in self.toolchain_lab_machines or
369 cros_name in self.toolchain_lab_machines):
Caroline Tice3f432712015-12-07 14:51:53 -0800370 name = m if m in self.toolchain_lab_machines else cros_name
cmticee5bc63b2015-05-27 16:59:37 -0700371 if not printed_hdr:
372 self.PrintStatusHeader(True)
373 printed_hdr = True
374 state = machine_states[m]
375 if state['locked']:
Caroline Ticea4486452015-12-08 13:43:23 -0800376 print('%s (%s)\tlocked by %s since %s' %
377 (name, state['board'], state['locked_by'], state['lock_time']))
cmticee5bc63b2015-05-27 16:59:37 -0700378 else:
Caroline Ticea4486452015-12-08 13:43:23 -0800379 print('%s (%s)\tunlocked' % (name, state['board']))
cmticee5bc63b2015-05-27 16:59:37 -0700380 else:
381 local_machines.append(m)
382
383 if local_machines:
384 self.PrintStatusHeader(False)
385 for m in local_machines:
386 state = machine_states[m]
387 if state['locked']:
Caroline Ticea4486452015-12-08 13:43:23 -0800388 print('%s (%s)\tlocked by %s since %s' %
389 (m, state['board'], state['locked_by'], state['lock_time']))
cmticee5bc63b2015-05-27 16:59:37 -0700390 else:
Caroline Ticea4486452015-12-08 13:43:23 -0800391 print('%s (%s)\tunlocked' % (m, state['board']))
cmticee5bc63b2015-05-27 16:59:37 -0700392
cmticee5bc63b2015-05-27 16:59:37 -0700393 def UpdateLockInAFE(self, should_lock_machine, machine):
394 """Calls an AFE server to lock/unlock a machine.
395
396 Args:
397 should_lock_machine: Boolean indicating whether to lock the machine (True)
398 or unlock the machine (False).
399 machine: The machine to update.
400
Zhizhou Yangcdd9e342019-09-19 20:56:32 -0700401 Returns:
402 True if requested action succeeded, else False.
cmticee5bc63b2015-05-27 16:59:37 -0700403 """
cmticee5bc63b2015-05-27 16:59:37 -0700404 kwargs = {'locked': should_lock_machine}
Zhizhou Yangcdd9e342019-09-19 20:56:32 -0700405 if should_lock_machine:
406 kwargs['lock_reason'] = 'toolchain user request (%s)' % self.user
cmticee5bc63b2015-05-27 16:59:37 -0700407
Caroline Tice3f432712015-12-07 14:51:53 -0800408 cros_name = machine + '.cros'
409 if cros_name in self.toolchain_lab_machines:
Caroline Ticea4486452015-12-08 13:43:23 -0800410 machine = cros_name
cmticee5bc63b2015-05-27 16:59:37 -0700411 if machine in self.toolchain_lab_machines:
412 m = machine.split('.')[0]
cmticee5bc63b2015-05-27 16:59:37 -0700413 afe_server = self.afe
414 else:
415 m = machine
416 afe_server = self.local_afe
417
418 try:
Caroline Ticef6ef4392017-04-06 17:16:05 -0700419 afe_server.run(
420 'modify_hosts',
421 host_filter_data={'hostname__in': [m]},
422 update_data=kwargs)
Zhizhou Yangcdd9e342019-09-19 20:56:32 -0700423 except Exception:
424 return False
425 return True
426
427 def UpdateLockInSkylab(self, should_lock_machine, machine):
428 """Ask skylab to lease/release a machine.
429
430 Args:
431 should_lock_machine: Boolean indicating whether to lock the machine (True)
432 or unlock the machine (False).
433 machine: The machine to update.
434
435 Returns:
436 True if requested action succeeded, else False.
437 """
438 try:
439 if should_lock_machine:
440 ret = self.LeaseSkylabMachine(machine)
441 else:
442 ret = self.ReleaseSkylabMachine(machine)
443 except Exception:
444 return False
445 return ret
446
447 def UpdateFileLock(self, should_lock_machine, machine):
448 """Use file lock for local machines,
449
450 Args:
451 should_lock_machine: Boolean indicating whether to lock the machine (True)
452 or unlock the machine (False).
453 machine: The machine to update.
454
455 Returns:
456 True if requested action succeeded, else False.
457 """
458 try:
459 if should_lock_machine:
460 ret = file_lock_machine.Machine(machine, self.locks_dir).Lock(
461 True, sys.argv[0])
462 else:
463 ret = file_lock_machine.Machine(machine, self.locks_dir).Unlock(True)
464 except Exception:
465 return False
466 return ret
cmticee5bc63b2015-05-27 16:59:37 -0700467
468 def UpdateMachines(self, lock_machines):
469 """Sets the locked state of the machines to the requested value.
470
471 The machines updated are the ones in self.machines (specified when the
472 class object was intialized).
473
474 Args:
Caroline Ticea4486452015-12-08 13:43:23 -0800475 lock_machines: Boolean indicating whether to lock the machines (True) or
cmticee5bc63b2015-05-27 16:59:37 -0700476 unlock the machines (False).
cmticef3eb8032015-07-27 13:55:52 -0700477
478 Returns:
479 A list of the machines whose state was successfully updated.
cmticee5bc63b2015-05-27 16:59:37 -0700480 """
cmticef3eb8032015-07-27 13:55:52 -0700481 updated_machines = []
Zhizhou Yangcdd9e342019-09-19 20:56:32 -0700482 action = 'Locking' if lock_machines else 'Unlocking'
cmticee5bc63b2015-05-27 16:59:37 -0700483 for m in self.machines:
Zhizhou Yangcdd9e342019-09-19 20:56:32 -0700484 # TODO(zhizhouy): Handling exceptions with more details when locking
485 # doesn't succeed.
486 machine_type = 'afe'
487 if m in self.skylab_machines:
488 ret = self.UpdateLockInSkylab(lock_machines, m)
489 machine_type = 'skylab'
490 elif m in self.local_machines:
491 ret = self.UpdateFileLock(lock_machines, m)
492 machine_type = 'local'
cmticee5bc63b2015-05-27 16:59:37 -0700493 else:
Zhizhou Yangcdd9e342019-09-19 20:56:32 -0700494 ret = self.UpdateLockInAFE(lock_machines, m)
cmticef3eb8032015-07-27 13:55:52 -0700495
Zhizhou Yangcdd9e342019-09-19 20:56:32 -0700496 if ret:
497 self.logger.LogOutput(
498 '%s %s machine succeeded: %s.' % (action, machine_type, m))
499 updated_machines.append(m)
500 else:
501 self.logger.LogOutput(
502 '%s %s machine failed: %s.' % (action, machine_type, m))
503
504 self.machines = updated_machines
cmticef3eb8032015-07-27 13:55:52 -0700505 return updated_machines
506
507 def _InternalRemoveMachine(self, machine):
508 """Remove machine from internal list of machines.
509
510 Args:
511 machine: Name of machine to be removed from internal list.
512 """
513 # Check to see if machine is lab machine and if so, make sure it has
514 # ".cros" on the end.
515 cros_machine = machine
516 if machine.find('rack') > 0 and machine.find('row') > 0:
517 if machine.find('.cros') == -1:
518 cros_machine = cros_machine + '.cros'
519
Caroline Ticef6ef4392017-04-06 17:16:05 -0700520 self.machines = [
521 m for m in self.machines if m != cros_machine and m != machine
522 ]
cmticee5bc63b2015-05-27 16:59:37 -0700523
524 def CheckMachineLocks(self, machine_states, cmd):
525 """Check that every machine in requested list is in the proper state.
526
527 If the cmd is 'unlock' verify that every machine is locked by requestor.
528 If the cmd is 'lock' verify that every machine is currently unlocked.
529
530 Args:
531 machine_states: A dictionary of the current state of every machine in
532 the current AFELockManager's list of machines. Normally obtained by
533 calling AFELockManager::GetMachineStates.
Caroline Ticea4486452015-12-08 13:43:23 -0800534 cmd: The user-requested action for the machines: 'lock' or 'unlock'.
cmticee5bc63b2015-05-27 16:59:37 -0700535
536 Raises:
cmticee5bc63b2015-05-27 16:59:37 -0700537 DontOwnLock: The lock on a requested machine is owned by someone else.
538 """
539 for k, state in machine_states.iteritems():
540 if cmd == 'unlock':
541 if not state['locked']:
cmticef3eb8032015-07-27 13:55:52 -0700542 self.logger.LogWarning('Attempt to unlock already unlocked machine '
543 '(%s).' % k)
544 self._InternalRemoveMachine(k)
cmticee5bc63b2015-05-27 16:59:37 -0700545
Zhizhou Yangcdd9e342019-09-19 20:56:32 -0700546 if state['locked'] and 'locked_by' in state and \
547 state['locked_by'] != self.user:
cmticee5bc63b2015-05-27 16:59:37 -0700548 raise DontOwnLock('Attempt to unlock machine (%s) locked by someone '
549 'else (%s).' % (k, state['locked_by']))
550 elif cmd == 'lock':
551 if state['locked']:
Caroline Ticef6ef4392017-04-06 17:16:05 -0700552 self.logger.LogWarning(
553 'Attempt to lock already locked machine (%s)' % k)
cmticef3eb8032015-07-27 13:55:52 -0700554 self._InternalRemoveMachine(k)
cmticee5bc63b2015-05-27 16:59:37 -0700555
556 def HasAFEServer(self, local):
557 """Verifies that the AFELockManager has appropriate AFE server.
558
559 Args:
560 local: Boolean indicating whether we are checking for the local server
561 (True) or for the global server (False).
562
563 Returns:
564 A boolean indicating if the AFELockManager has the requested AFE server.
565 """
566 if local:
567 return self.local_afe is not None
568 else:
569 return self.afe is not None
570
571 def GetMachineStates(self, cmd=''):
572 """Gets the current state of all the requested machines.
573
574 Gets the current state of all the requested machines, both from the HW lab
575 sever and from the local server. Stores the data in a dictionary keyed
576 by machine name.
577
578 Args:
579 cmd: The command for which we are getting the machine states. This is
580 important because if one of the requested machines is missing we raise
581 an exception, unless the requested command is 'add'.
582
583 Returns:
584 A dictionary of machine states for all the machines in the AFELockManager
585 object.
586
587 Raises:
588 NoAFEServer: Cannot find the HW Lab or local AFE server.
589 AFEAccessError: An error occurred when querying the server about a
590 machine.
591 """
592 if not self.HasAFEServer(False):
593 raise NoAFEServer('Error: Cannot connect to main AFE server.')
594
595 if self.local and not self.HasAFEServer(True):
596 raise NoAFEServer('Error: Cannot connect to local AFE server.')
597
Caroline Ticea4486452015-12-08 13:43:23 -0800598 machine_list = {}
cmticee5bc63b2015-05-27 16:59:37 -0700599 for m in self.machines:
Zhizhou Yangcdd9e342019-09-19 20:56:32 -0700600 # For local or skylab machines, we simply set {'locked': status} for them
601 # TODO(zhizhouy): This is a quick fix since skylab cannot return host info
602 # as afe does. We need to get more info such as locked_by when skylab
603 # supports that.
604 if m in self.local_machines or m in self.skylab_machines:
605 machine_list[m] = {'locked': 0 if cmd == 'lock' else 1}
606 continue
607
cmticee5bc63b2015-05-27 16:59:37 -0700608 host_info = None
Caroline Tice3f432712015-12-07 14:51:53 -0800609 cros_name = m + '.cros'
610 if (m in self.toolchain_lab_machines or
611 cros_name in self.toolchain_lab_machines):
cmticee5bc63b2015-05-27 16:59:37 -0700612 mod_host = m.split('.')[0]
613 host_info = self.afe.get_hosts(hostname=mod_host)
614 if not host_info:
615 raise AFEAccessError('Unable to get information about %s from main'
616 ' autotest server.' % m)
617 else:
618 host_info = self.local_afe.get_hosts(hostname=m)
619 if not host_info and cmd != 'add':
620 raise AFEAccessError('Unable to get information about %s from '
621 'local autotest server.' % m)
622 if host_info:
623 host_info = host_info[0]
624 name = host_info.hostname
625 values = {}
626 values['board'] = host_info.platform if host_info.platform else '??'
627 values['locked'] = host_info.locked
628 if host_info.locked:
Caroline Ticea4486452015-12-08 13:43:23 -0800629 values['locked_by'] = host_info.locked_by
630 values['lock_time'] = host_info.lock_time
cmticee5bc63b2015-05-27 16:59:37 -0700631 else:
Caroline Ticea4486452015-12-08 13:43:23 -0800632 values['locked_by'] = ''
633 values['lock_time'] = ''
634 machine_list[name] = values
cmticee5bc63b2015-05-27 16:59:37 -0700635 else:
Caroline Ticea4486452015-12-08 13:43:23 -0800636 machine_list[m] = {}
637 return machine_list
cmticee5bc63b2015-05-27 16:59:37 -0700638
Zhizhou Yangcdd9e342019-09-19 20:56:32 -0700639 def CheckMachineInSkylab(self, machine):
640 """Run command to check if machine is in Skylab or not.
641
642 Returns:
643 True if machine in skylab, else False
644 """
645 credential = ''
646 if os.path.exists(self.SKYLAB_CREDENTIAL):
647 credential = '--auth-service-account-json %s' % self.SKYLAB_CREDENTIAL
648 swarming = os.path.join(self.chromeos_root, self.SWARMING)
649 cmd = (('%s query --swarming https://chromeos-swarming.appspot.com ' \
650 "%s 'bots/list?is_dead=FALSE&dimensions=dut_name:%s'") % \
651 (swarming,
652 credential,
653 machine.rstrip('.cros')))
654 ret_tup = self.ce.RunCommandWOutput(cmd)
655 # The command will return a json output as stdout. If machine not in skylab
656 # stdout will look like this:
657 # {
658 # "death_timeout": "600",
659 # "now": "TIMESTAMP"
660 # }
661 # Otherwise there will be a tuple starting with 'items', we simply detect
662 # this keyword for result.
663 if 'items' not in ret_tup[1]:
664 return False
665 else:
666 return True
667
668 def LeaseSkylabMachine(self, machine):
669 """Run command to lease dut from skylab.
670
671 Returns:
672 True if succeeded, False if failed.
673 """
674 credential = ''
675 if os.path.exists(self.SKYLAB_CREDENTIAL):
676 credential = '-service-account-json %s' % self.SKYLAB_CREDENTIAL
677 cmd = (('%s lease-dut -minutes %s %s %s') % \
678 (self.SKYLAB_PATH,
679 self.LEASE_MINS,
680 credential,
681 machine.rstrip('.cros')))
682 # Wait 120 seconds for server to start the lease task, if not started,
683 # we will treat it as unavailable.
684 check_interval_time = 120
685 retval = self.ce.RunCommand(cmd, command_timeout=check_interval_time)
686 return retval == self.SUCCESS
687
688 def ReleaseSkylabMachine(self, machine):
689 """Run command to release dut from skylab.
690
691 Returns:
692 True if succeeded, False if failed.
693 """
694 credential = ''
695 if os.path.exists(self.SKYLAB_CREDENTIAL):
696 credential = '-service-account-json %s' % self.SKYLAB_CREDENTIAL
697 cmd = (('%s release-dut %s %s') % \
698 (self.SKYLAB_PATH,
699 credential,
700 machine.rstrip('.cros')))
701 retval = self.ce.RunCommand(cmd)
702 return retval == self.SUCCESS
703
cmticee5bc63b2015-05-27 16:59:37 -0700704
705def Main(argv):
Caroline Ticea4486452015-12-08 13:43:23 -0800706 """Parse the options, initialize lock manager and dispatch proper method.
cmticee5bc63b2015-05-27 16:59:37 -0700707
Caroline Ticea4486452015-12-08 13:43:23 -0800708 Args:
709 argv: The options with which this script was invoked.
cmticee5bc63b2015-05-27 16:59:37 -0700710
Caroline Ticea4486452015-12-08 13:43:23 -0800711 Returns:
712 0 unless an exception is raised.
713 """
714 parser = argparse.ArgumentParser()
cmticee5bc63b2015-05-27 16:59:37 -0700715
Caroline Tice6b161382016-09-15 15:03:46 -0700716 parser.add_argument(
717 '--list',
718 dest='cmd',
719 action='store_const',
720 const='status',
721 help='List current status of all known machines.')
722 parser.add_argument(
723 '--lock',
724 dest='cmd',
725 action='store_const',
726 const='lock',
727 help='Lock given machine(s).')
728 parser.add_argument(
729 '--unlock',
730 dest='cmd',
731 action='store_const',
732 const='unlock',
733 help='Unlock given machine(s).')
734 parser.add_argument(
735 '--status',
736 dest='cmd',
737 action='store_const',
738 const='status',
739 help='List current status of given machine(s).')
740 parser.add_argument(
741 '--add_machine',
742 dest='cmd',
743 action='store_const',
744 const='add',
745 help='Add machine to local machine server.')
746 parser.add_argument(
747 '--remove_machine',
748 dest='cmd',
749 action='store_const',
750 const='remove',
751 help='Remove machine from the local machine server.')
752 parser.add_argument(
753 '--nolocal',
754 dest='local',
755 action='store_false',
756 default=True,
757 help='Do not try to use local machine server.')
758 parser.add_argument(
759 '--remote', dest='remote', help='machines on which to operate')
760 parser.add_argument(
761 '--chromeos_root',
762 dest='chromeos_root',
763 required=True,
764 help='ChromeOS root to use for autotest scripts.')
765 parser.add_argument(
766 '--local_server',
767 dest='local_server',
768 default=None,
769 help='Alternate local autotest server to use.')
770 parser.add_argument(
771 '--force',
772 dest='force',
773 action='store_true',
774 default=False,
775 help='Force lock/unlock of machines, even if not'
776 ' current lock owner.')
cmticee5bc63b2015-05-27 16:59:37 -0700777
Caroline Ticea4486452015-12-08 13:43:23 -0800778 options = parser.parse_args(argv)
cmticee5bc63b2015-05-27 16:59:37 -0700779
Caroline Ticea4486452015-12-08 13:43:23 -0800780 if not options.remote and options.cmd != 'status':
781 parser.error('No machines specified for operation.')
cmticee5bc63b2015-05-27 16:59:37 -0700782
Caroline Ticea4486452015-12-08 13:43:23 -0800783 if not os.path.isdir(options.chromeos_root):
784 parser.error('Cannot find chromeos_root: %s.' % options.chromeos_root)
cmticee5bc63b2015-05-27 16:59:37 -0700785
Caroline Ticea4486452015-12-08 13:43:23 -0800786 if not options.cmd:
787 parser.error('No operation selected (--list, --status, --lock, --unlock,'
788 ' --add_machine, --remove_machine).')
cmticee5bc63b2015-05-27 16:59:37 -0700789
Caroline Ticea4486452015-12-08 13:43:23 -0800790 machine_list = []
791 if options.remote:
792 machine_list = options.remote.split()
cmticee5bc63b2015-05-27 16:59:37 -0700793
Caroline Ticea4486452015-12-08 13:43:23 -0800794 lock_manager = AFELockManager(machine_list, options.force,
795 options.chromeos_root, options.local_server,
796 options.local)
cmticee5bc63b2015-05-27 16:59:37 -0700797
Caroline Ticea4486452015-12-08 13:43:23 -0800798 machine_states = lock_manager.GetMachineStates(cmd=options.cmd)
799 cmd = options.cmd
cmticee5bc63b2015-05-27 16:59:37 -0700800
Caroline Ticea4486452015-12-08 13:43:23 -0800801 if cmd == 'status':
802 lock_manager.ListMachineStates(machine_states)
cmticee5bc63b2015-05-27 16:59:37 -0700803
Caroline Ticea4486452015-12-08 13:43:23 -0800804 elif cmd == 'lock':
805 if not lock_manager.force:
806 lock_manager.CheckMachineLocks(machine_states, cmd)
807 lock_manager.UpdateMachines(True)
cmticee5bc63b2015-05-27 16:59:37 -0700808
Caroline Ticea4486452015-12-08 13:43:23 -0800809 elif cmd == 'unlock':
810 if not lock_manager.force:
811 lock_manager.CheckMachineLocks(machine_states, cmd)
812 lock_manager.UpdateMachines(False)
cmticee5bc63b2015-05-27 16:59:37 -0700813
Caroline Ticea4486452015-12-08 13:43:23 -0800814 elif cmd == 'add':
815 lock_manager.AddMachinesToLocalServer()
cmticee5bc63b2015-05-27 16:59:37 -0700816
Caroline Ticea4486452015-12-08 13:43:23 -0800817 elif cmd == 'remove':
818 lock_manager.RemoveMachinesFromLocalServer()
cmticee5bc63b2015-05-27 16:59:37 -0700819
Caroline Ticea4486452015-12-08 13:43:23 -0800820 return 0
cmticee5bc63b2015-05-27 16:59:37 -0700821
822
823if __name__ == '__main__':
Caroline Ticea4486452015-12-08 13:43:23 -0800824 sys.exit(Main(sys.argv[1:]))