blob: 24dfb3f800fda816329f472ed296e5293a541f75 [file] [log] [blame]
Keun Soo Yim8cdfb522018-01-04 15:52:16 -08001#
2# Copyright (C) 2017 The Android Open Source Project
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15#
16
Keun Soo Yim8cdfb522018-01-04 15:52:16 -080017import cmd
Hyunwoo Kocdda4e82018-01-25 19:44:25 +090018import ctypes
Jongmok Hong71258da2018-01-05 19:17:58 +090019import datetime
Keun Soo Yim8cdfb522018-01-04 15:52:16 -080020import imp # Python v2 compatibility
Hsin-Yi Chen9e27bfc2017-12-28 17:16:19 +080021import importlib
Keun Soo Yim8cdfb522018-01-04 15:52:16 -080022import logging
Keun Soo Yim4fa65242018-02-10 17:38:13 -080023import multiprocessing
Keun Soo Yim8cdfb522018-01-04 15:52:16 -080024import os
Hyunwoo Kocdda4e82018-01-25 19:44:25 +090025import queue
Hsin-Yi Chen30d418f2018-01-29 15:36:39 +080026import re
Keun Soo Yim8cdfb522018-01-04 15:52:16 -080027import shutil
28import socket
Hyunwoo Kocdda4e82018-01-25 19:44:25 +090029import stat
Keun Soo Yim8cdfb522018-01-04 15:52:16 -080030import subprocess
31import sys
32import threading
Keun Soo Yim7d95b502018-01-21 21:53:54 -080033import tempfile
Keun Soo Yim8cdfb522018-01-04 15:52:16 -080034import time
Keun Soo Yim7d95b502018-01-21 21:53:54 -080035import zipfile
Keun Soo Yim8cdfb522018-01-04 15:52:16 -080036
37import httplib2
Keun Soo Yim594a3322018-01-21 21:16:17 -080038from googleapiclient import errors
Keun Soo Yim8cdfb522018-01-04 15:52:16 -080039import urlparse
40
Keun Soo Yimed07bdf2018-01-08 14:34:14 -080041from google.protobuf import text_format
42
43from vti.test_serving.proto import TestLabConfigMessage_pb2 as LabCfgMsg
44from vti.test_serving.proto import TestScheduleConfigMessage_pb2 as SchedCfgMsg
45
Yuexi Ma1dac95d2018-01-11 18:39:50 -080046from host_controller.console_argument_parser import ConsoleArgumentError
47from host_controller.console_argument_parser import ConsoleArgumentParser
Yuexi Maa01c0c72018-01-11 19:28:04 -080048from host_controller.command_processor import command_info
Hsin-Yi Chen02592912018-01-17 19:44:17 +080049from host_controller.command_processor import command_test
Keun Soo Yimd115f922018-01-05 09:41:27 -080050from host_controller.tfc import request
51from host_controller.build import build_flasher
52from host_controller.build import build_provider
53from host_controller.build import build_provider_ab
54from host_controller.build import build_provider_gcs
55from host_controller.build import build_provider_local_fs
56from host_controller.tradefed import remote_operation
Hyunwoo Ko432ccc62018-01-10 17:51:38 +090057from host_controller.utils.gsi import img_utils
Jongmok Hong71258da2018-01-05 19:17:58 +090058from vts.utils.python.common import cmd_utils
Keun Soo Yim8cdfb522018-01-04 15:52:16 -080059
60# The default Partner Android Build (PAB) public account.
61# To obtain access permission, please reach out to Android partner engineering
62# department of Google LLC.
63_DEFAULT_ACCOUNT_ID = '543365459'
64
65# The default value for "flash --current".
66_DEFAULT_FLASH_IMAGES = [
67 build_provider.FULL_ZIPFILE,
Keun Soo Yim7d95b502018-01-21 21:53:54 -080068 "bootloader.img",
Keun Soo Yim8cdfb522018-01-04 15:52:16 -080069 "boot.img",
70 "cache.img",
Keun Soo Yim7d95b502018-01-21 21:53:54 -080071 "radio.img",
Keun Soo Yim8cdfb522018-01-04 15:52:16 -080072 "system.img",
73 "userdata.img",
74 "vbmeta.img",
75 "vendor.img",
76]
77
78# The environment variable for default serial numbers.
79_ANDROID_SERIAL = "ANDROID_SERIAL"
80
Keun Soo Yimcb513252018-01-08 14:17:14 -080081DEVICE_STATUS_DICT = {
82 "unknown": 0,
83 "fastboot": 1,
84 "online": 2,
85 "ready": 3,
86 "use": 4,
87 "error": 5}
88
Hyunwoo Ko432ccc62018-01-10 17:51:38 +090089_SPL_DEFAULT_DAY = 5
90
Hyunwoo Kocdda4e82018-01-25 19:44:25 +090091# Maximum number of leased jobs per host.
92_MAX_LEASED_JOBS = 14
93
Keun Soo Yim8cdfb522018-01-04 15:52:16 -080094
Yuexi Maa01c0c72018-01-11 19:28:04 -080095COMMAND_PROCESSORS = [
96 command_info.CommandInfo,
Hsin-Yi Chen02592912018-01-17 19:44:17 +080097 command_test.CommandTest,
Yuexi Maa01c0c72018-01-11 19:28:04 -080098]
99
100
Hyunwoo Kodfd17d92018-02-01 14:58:56 +0900101def ExecJobOnProcessPoolWrapper(arg, **kwargs):
102 """Passes argument to Console.ExecjobOnProcessPool.
103
104 need to invoke a global function from a process pool
105 since methods are not picklable.
106
107 Args:
108 arg: tuple, consists of Console class instance from the main process
109 and a dict containing the leased job information.
110 kwargs: dict, currently not in use.
111 """
112 return Console.ExecJobOnProcessPool(*arg, **kwargs)
113
114
Keun Soo Yim8cdfb522018-01-04 15:52:16 -0800115class Console(cmd.Cmd):
116 """The console for host controllers.
117
118 Attributes:
Yuexi Maa01c0c72018-01-11 19:28:04 -0800119 command_processors: dict of string:BaseCommandProcessor,
120 map between command string and command processors.
Keun Soo Yim8cdfb522018-01-04 15:52:16 -0800121 device_image_info: dict containing info about device image files.
122 prompt: The prompt string at the beginning of each command line.
Hsin-Yi Chen30d418f2018-01-29 15:36:39 +0800123 test_result: dict containing info about the last test result.
Keun Soo Yim8cdfb522018-01-04 15:52:16 -0800124 test_suite_info: dict containing info about test suite package files.
125 tools_info: dict containing info about custom tool files.
Keun Soo Yim0e3b0082018-01-08 15:10:02 -0800126 build_thread: dict containing threading.Thread instances(s) that
127 update build info regularly.
Keun Soo Yimed07bdf2018-01-08 14:34:14 -0800128 scheduler_thread: dict containing threading.Thread instances(s) that
129 update configs regularly.
130 update_thread: threading.Thread that updates device state regularly.
Hyunwoo Kocdda4e82018-01-25 19:44:25 +0900131 leased_job_process: Process. Fetches leased jobs from leased_job_queue
132 and maps them to process pool.
Keun Soo Yim4fa65242018-02-10 17:38:13 -0800133 leased_job_running: multiprocessing.Value, keeps running leased_job_process
Hyunwoo Kodfd17d92018-02-01 14:58:56 +0900134 if equal to 1.
Keun Soo Yim4fa65242018-02-10 17:38:13 -0800135 leased_job_queue: multiprocessing.Queue. Jobs leased will be pushed form
Hyunwoo Kocdda4e82018-01-25 19:44:25 +0900136 the main process, popped and executed on process pool.
Keun Soo Yim4faa3602018-01-11 15:00:08 -0800137 _build_provider_pab: The BuildProviderPAB used to download artifacts.
Keun Soo Yim164798b2018-02-10 17:40:53 -0800138 _vti_address: string, VTI service URI.
Keun Soo Yima1703082018-01-05 16:59:43 -0800139 _vti_client: VtiEndpoewrClient, used to upload data to a test
140 scheduling infrastructure.
Keun Soo Yim8cdfb522018-01-04 15:52:16 -0800141 _tfc_client: The TfcClient that the host controllers connect to.
142 _hosts: A list of HostController objects.
143 _in_file: The input file object.
144 _out_file: The output file object.
145 _serials: A list of string where each string is a device serial.
Keun Soo Yimed07bdf2018-01-08 14:34:14 -0800146 _config_parser: The parser for config command.
Keun Soo Yim8cdfb522018-01-04 15:52:16 -0800147 _copy_parser: The parser for copy command.
148 _device_parser: The parser for device command.
149 _fetch_parser: The parser for fetch command.
150 _flash_parser: The parser for flash command.
Jongmok Hong71258da2018-01-05 19:17:58 +0900151 _gsispl_parser: The parser for gsispl command.
Keun Soo Yim8cdfb522018-01-04 15:52:16 -0800152 _lease_parser: The parser for lease command.
153 _list_parser: The parser for list command.
154 _request_parser: The parser for request command.
Jongmok Hong35a4ee42018-01-09 15:22:18 +0900155 _upload_parser: The parser for upload command.
Keun Soo Yim8cdfb522018-01-04 15:52:16 -0800156 """
157
158 def __init__(self,
Keun Soo Yima1703082018-01-05 16:59:43 -0800159 vti_endpoint_client,
Keun Soo Yim8cdfb522018-01-04 15:52:16 -0800160 tfc,
161 pab,
162 host_controllers,
Keun Soo Yim164798b2018-02-10 17:40:53 -0800163 vti_address=None,
Keun Soo Yim8cdfb522018-01-04 15:52:16 -0800164 in_file=sys.stdin,
165 out_file=sys.stdout):
166 """Initializes the attributes and the parsers."""
167 # cmd.Cmd is old-style class.
168 cmd.Cmd.__init__(self, stdin=in_file, stdout=out_file)
169 self._build_provider = {}
170 self._build_provider["pab"] = pab
171 self._build_provider[
172 "local_fs"] = build_provider_local_fs.BuildProviderLocalFS()
173 self._build_provider["gcs"] = build_provider_gcs.BuildProviderGCS()
174 self._build_provider["ab"] = build_provider_ab.BuildProviderAB()
Keun Soo Yima1703082018-01-05 16:59:43 -0800175 self._vti_endpoint_client = vti_endpoint_client
Keun Soo Yim164798b2018-02-10 17:40:53 -0800176 self._vti_address = vti_address
Keun Soo Yim8cdfb522018-01-04 15:52:16 -0800177 self._tfc_client = tfc
178 self._hosts = host_controllers
179 self._in_file = in_file
180 self._out_file = out_file
181 self.prompt = "> "
Hsin-Yi Chenc57a6cf2018-01-18 12:57:06 +0800182 self.command_processors = {}
Keun Soo Yim8cdfb522018-01-04 15:52:16 -0800183 self.device_image_info = {}
Hsin-Yi Chen30d418f2018-01-29 15:36:39 +0800184 self.test_result = {}
Keun Soo Yim8cdfb522018-01-04 15:52:16 -0800185 self.test_suite_info = {}
186 self.tools_info = {}
Keun Soo Yim0e3b0082018-01-08 15:10:02 -0800187 self.build_thread = {}
Keun Soo Yimed07bdf2018-01-08 14:34:14 -0800188 self.schedule_thread = {}
Keun Soo Yim8cdfb522018-01-04 15:52:16 -0800189 self.update_thread = None
Hyunwoo Kocdda4e82018-01-25 19:44:25 +0900190 self.leased_job_process = None
Hyunwoo Kodfd17d92018-02-01 14:58:56 +0900191 self.leased_job_running = None
192 self.leased_job_queue = None
Keun Soo Yim8cdfb522018-01-04 15:52:16 -0800193 self.fetch_info = {}
Keun Soo Yimc00832e2018-02-10 17:59:46 -0800194 self.test_results = {}
Keun Soo Yim8cdfb522018-01-04 15:52:16 -0800195
Hsin-Yi Chen18ee53c2018-01-22 17:47:07 +0800196 if _ANDROID_SERIAL in os.environ:
197 self._serials = [os.environ[_ANDROID_SERIAL]]
198 else:
199 self._serials = []
200
Yuexi Ma52e76842018-01-11 10:36:55 -0800201 self.InitCommandModuleParsers()
Hsin-Yi Chenc57a6cf2018-01-18 12:57:06 +0800202 self.SetUpCommandProcessors()
Hyunwoo Kodfd17d92018-02-01 14:58:56 +0900203 self.StartLeasedJobExecutionProcess()
Yuexi Ma52e76842018-01-11 10:36:55 -0800204
205 def InitCommandModuleParsers(self):
206 """Init all console command modules"""
207 for name in dir(self):
208 if name.startswith('_Init') and name.endswith('Parser'):
209 attr_func = getattr(self, name)
210 if hasattr(attr_func, '__call__'):
211 attr_func()
Keun Soo Yim8cdfb522018-01-04 15:52:16 -0800212
Hsin-Yi Chenc57a6cf2018-01-18 12:57:06 +0800213 def SetUpCommandProcessors(self):
214 """Sets up all command processors."""
215 for command_processor in COMMAND_PROCESSORS:
216 cp = command_processor()
217 cp._SetUp(self)
218 do_text = "do_%s" % cp.command
219 help_text = "help_%s" % cp.command
220 setattr(self, do_text, cp._Run)
221 setattr(self, help_text, cp._Help)
222 self.command_processors[cp.command] = cp
223
224 def TearDown(self):
225 """Removes all command processors."""
226 for command_processor in self.command_processors.itervalues():
227 command_processor._TearDown()
228 self.command_processors.clear()
229
Hsin-Yi Chen30d418f2018-01-29 15:36:39 +0800230 def FormatString(self, format_string):
231 """Replaces variables with the values in the console's dictionaries.
232
233 Args:
234 format_string: The string containing variables enclosed in {}.
235
236 Returns:
237 The formatted string.
238
239 Raises:
240 KeyError if a variable is not found in the dictionaries or the
241 value is empty.
242 """
243 def ReplaceVariable(match):
244 name = match.group(1)
245 if name in ("build_id", "branch", "target"):
246 value = self.fetch_info[name]
247 elif name in ("result_zip", "suite_plan"):
248 value = self.test_result[name]
Hyunwoo Ko3556b192018-02-08 19:29:12 +0900249 elif name in ("timestamp"):
250 value = datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
Hsin-Yi Chen30d418f2018-01-29 15:36:39 +0800251 else:
252 value = None
253
254 if not value:
255 raise KeyError(name)
256
257 return value
258
259 return re.sub("{([^}]+)}", ReplaceVariable, format_string)
260
Keun Soo Yim8cdfb522018-01-04 15:52:16 -0800261 def _InitRequestParser(self):
262 """Initializes the parser for request command."""
263 self._request_parser = ConsoleArgumentParser(
264 "request", "Send TFC a request to execute a command.")
265 self._request_parser.add_argument(
266 "--cluster",
267 required=True,
268 help="The cluster to which the request is submitted.")
269 self._request_parser.add_argument(
270 "--run-target",
271 required=True,
272 help="The target device to run the command.")
273 self._request_parser.add_argument(
274 "--user",
275 required=True,
276 help="The name of the user submitting the request.")
277 self._request_parser.add_argument(
278 "command",
279 metavar="COMMAND",
280 nargs="+",
281 help='The command to be executed. If the command contains '
282 'arguments starting with "-", place the command after '
283 '"--" at end of line.')
284
285 def ProcessScript(self, script_file_path):
286 """Processes a .py script file.
287
288 A script file implements a function which emits a list of console
289 commands to execute. That function emits an empty list or None if
290 no more command needs to be processed.
291
292 Args:
293 script_file_path: string, the path of a script file (.py file).
294
295 Returns:
296 True if successful; False otherwise
297 """
298 if not script_file_path.endswith(".py"):
299 print("Script file is not .py file: %s" % script_file_path)
300 return False
301
302 script_module = imp.load_source('script_module', script_file_path)
303
Keun Soo Yim8cdfb522018-01-04 15:52:16 -0800304 commands = script_module.EmitConsoleCommands()
305 if commands:
306 for command in commands:
307 self.onecmd(command)
308 return True
309
Keun Soo Yima8ce7de2018-01-08 15:18:03 -0800310 def ProcessConfigurableScript(self, script_file_path, **kwargs):
311 """Processes a .py script file.
312
313 A script file implements a function which emits a list of console
314 commands to execute. That function emits an empty list or None if
315 no more command needs to be processed.
316
317 Args:
318 script_file_path: string, the path of a script file (.py file).
319 kwargs: extra args for the interface function defined in
320 the script file.
321
322 Returns:
323 True if successful; False otherwise
324 """
Keun Soo Yim4faa3602018-01-11 15:00:08 -0800325 if script_file_path and "." not in script_file_path:
326 script_file_path += ".py"
327
Keun Soo Yima8ce7de2018-01-08 15:18:03 -0800328 if not script_file_path.endswith(".py"):
329 print("Script file is not .py file: %s" % script_file_path)
330 return False
331
332 script_module = imp.load_source('script_module', script_file_path)
333
Hyunwoo Kod0f8b912018-02-07 17:36:00 +0900334 commands = script_module.EmitConsoleCommands(**kwargs)
Keun Soo Yima8ce7de2018-01-08 15:18:03 -0800335 if commands:
336 for command in commands:
337 self.onecmd(command)
Hyunwoo Kod6e7d682018-02-05 17:01:48 +0900338 else:
339 return False
Keun Soo Yima8ce7de2018-01-08 15:18:03 -0800340 return True
341
Keun Soo Yim8cdfb522018-01-04 15:52:16 -0800342 def do_request(self, line):
343 """Sends TFC a request to execute a command."""
344 args = self._request_parser.ParseLine(line)
345 req = request.Request(
346 cluster=args.cluster,
347 command_line=" ".join(args.command),
348 run_target=args.run_target,
349 user=args.user)
350 self._tfc_client.NewRequest(req)
351
352 def help_request(self):
353 """Prints help message for request command."""
354 self._request_parser.print_help(self._out_file)
355
356 def _InitListParser(self):
357 """Initializes the parser for list command."""
358 self._list_parser = ConsoleArgumentParser(
359 "list", "Show information about the hosts.")
360 self._list_parser.add_argument(
361 "--host", type=int, help="The index of the host.")
362 self._list_parser.add_argument(
363 "type",
364 choices=("hosts", "devices"),
365 help="The type of the shown objects.")
366
367 def _Print(self, string):
368 """Prints a string and a new line character.
369
370 Args:
371 string: The string to be printed.
372 """
373 self._out_file.write(string + "\n")
374
375 def do_list(self, line):
376 """Shows information about the hosts."""
377 args = self._list_parser.ParseLine(line)
378 if args.host is None:
379 hosts = enumerate(self._hosts)
380 else:
381 hosts = [(args.host, self._hosts[args.host])]
382 if args.type == "hosts":
383 self._PrintHosts(self._hosts)
384 elif args.type == "devices":
385 for ind, host in hosts:
386 devices = host.ListDevices()
387 self._Print("[%3d] %s" % (ind, host.hostname))
388 self._PrintDevices(devices)
389
390 def help_list(self):
391 """Prints help message for list command."""
392 self._list_parser.print_help(self._out_file)
393
394 def _PrintHosts(self, hosts):
395 """Shows a list of host controllers.
396
397 Args:
398 hosts: A list of HostController objects.
399 """
400 self._Print("index name")
401 for ind, host in enumerate(hosts):
402 self._Print("[%3d] %s" % (ind, host.hostname))
403
404 def _PrintDevices(self, devices):
405 """Shows a list of devices.
406
407 Args:
408 devices: A list of DeviceInfo objects.
409 """
410 attr_names = ("device_serial", "state", "run_target", "build_id",
411 "sdk_version", "stub")
412 self._PrintObjects(devices, attr_names)
413
414 def _PrintObjects(self, objects, attr_names):
415 """Shows objects as a table.
416
417 Args:
418 object: The objects to be shown, one object in a row.
419 attr_names: The attributes to be shown, one attribute in a column.
420 """
421 width = [len(name) for name in attr_names]
422 rows = [attr_names]
423 for dev_info in objects:
424 attrs = [
425 _ToPrintString(getattr(dev_info, name, ""))
426 for name in attr_names
427 ]
428 rows.append(attrs)
429 for index, attr in enumerate(attrs):
430 width[index] = max(width[index], len(attr))
431
432 for row in rows:
433 self._Print(" ".join(
434 attr.ljust(width[index]) for index, attr in enumerate(row)))
435
436 def _InitLeaseParser(self):
437 """Initializes the parser for lease command."""
438 self._lease_parser = ConsoleArgumentParser(
439 "lease", "Make a host lease command tasks from TFC.")
440 self._lease_parser.add_argument(
441 "--host", type=int, help="The index of the host.")
442
443 def do_lease(self, line):
444 """Makes a host lease command tasks from TFC."""
445 args = self._lease_parser.ParseLine(line)
446 if args.host is None:
447 if len(self._hosts) > 1:
448 raise ConsoleArgumentError("More than one hosts.")
449 args.host = 0
450 tasks = self._hosts[args.host].LeaseCommandTasks()
451 self._PrintTasks(tasks)
452
453 def help_lease(self):
454 """Prints help message for lease command."""
455 self._lease_parser.print_help(self._out_file)
456
457 def _InitFetchParser(self):
458 """Initializes the parser for fetch command."""
459 self._fetch_parser = ConsoleArgumentParser("fetch",
460 "Fetch a build artifact.")
461 self._fetch_parser.add_argument(
462 '--type',
463 default='pab',
464 choices=('local_fs', 'gcs', 'pab', 'ab'),
465 help='Build provider type')
466 self._fetch_parser.add_argument(
467 '--method',
468 default='GET',
469 choices=('GET', 'POST'),
470 help='Method for fetching')
471 self._fetch_parser.add_argument(
472 "--path", # required for local_fs
473 help="The path of a local directory which keeps the artifacts.")
474 self._fetch_parser.add_argument(
475 "--branch", # required for pab
476 help="Branch to grab the artifact from.")
477 self._fetch_parser.add_argument(
478 "--target", # required for pab
479 help="Target product to grab the artifact from.")
480 # TODO(lejonathan): find a way to not specify this?
481 self._fetch_parser.add_argument(
482 "--account_id",
483 default=_DEFAULT_ACCOUNT_ID,
484 help="Partner Android Build account_id to use.")
485 self._fetch_parser.add_argument(
486 '--build_id',
487 default='latest',
488 help='Build ID to use default latest.')
489 self._fetch_parser.add_argument(
490 "--artifact_name", # required for pab
491 help=
492 "Name of the artifact to be fetched. {id} replaced with build id.")
493 self._fetch_parser.add_argument(
Keun Soo Yim8a68d5b2018-01-21 21:36:41 -0800494 "--userinfo-file",
Keun Soo Yim8cdfb522018-01-04 15:52:16 -0800495 help=
496 "Location of file containing email and password, if using POST.")
Keun Soo Yim54f71d12018-01-21 21:40:11 -0800497 self._fetch_parser.add_argument(
498 "--noauth_local_webserver",
499 default=False,
500 type=bool,
501 help="True to not use a local webserver for authentication.")
Keun Soo Yim8cdfb522018-01-04 15:52:16 -0800502
503 def do_fetch(self, line):
504 """Makes the host download a build artifact from PAB."""
505 args = self._fetch_parser.ParseLine(line)
Hsin-Yi Chen2f3cb9a2018-01-02 16:05:18 +0800506
507 if args.type not in self._build_provider:
508 print("ERROR: uninitialized fetch type %s" % args.type)
509 return
510
511 provider = self._build_provider[args.type]
Keun Soo Yim8cdfb522018-01-04 15:52:16 -0800512 if args.type == "pab":
513 # do we want this somewhere else? No harm in doing multiple times
Keun Soo Yim54f71d12018-01-21 21:40:11 -0800514 provider.Authenticate(args.userinfo_file,
515 args.noauth_local_webserver)
Keun Soo Yim12872ff2018-01-09 15:05:42 -0800516 (device_images, test_suites,
Hsin-Yi Chen2f3cb9a2018-01-02 16:05:18 +0800517 fetch_environment, _) = provider.GetArtifact(
518 account_id=args.account_id,
519 branch=args.branch,
520 target=args.target,
521 artifact_name=args.artifact_name,
522 build_id=args.build_id,
523 method=args.method)
Keun Soo Yim8cdfb522018-01-04 15:52:16 -0800524 self.fetch_info["build_id"] = fetch_environment["build_id"]
525 elif args.type == "local_fs":
Hsin-Yi Chen2f3cb9a2018-01-02 16:05:18 +0800526 device_images, test_suites = provider.Fetch(args.path)
Keun Soo Yim8cdfb522018-01-04 15:52:16 -0800527 self.fetch_info["build_id"] = None
528 elif args.type == "gcs":
Hsin-Yi Chen2f3cb9a2018-01-02 16:05:18 +0800529 device_images, test_suites, tools = provider.Fetch(args.path)
Keun Soo Yim8cdfb522018-01-04 15:52:16 -0800530 self.fetch_info["build_id"] = None
531 elif args.type == "ab":
Hsin-Yi Chen2f3cb9a2018-01-02 16:05:18 +0800532 device_images, test_suites, fetch_environment = provider.Fetch(
533 branch=args.branch,
534 target=args.target,
535 artifact_name=args.artifact_name,
536 build_id=args.build_id)
Keun Soo Yim8cdfb522018-01-04 15:52:16 -0800537 self.fetch_info["build_id"] = fetch_environment["build_id"]
538 else:
539 print("ERROR: unknown fetch type %s" % args.type)
Jongmok Hongdaffd2b2018-01-08 14:05:11 +0900540 return
Keun Soo Yim8cdfb522018-01-04 15:52:16 -0800541
542 self.fetch_info["branch"] = args.branch
543 self.fetch_info["target"] = args.target
544
545 self.device_image_info.update(device_images)
546 self.test_suite_info.update(test_suites)
Hsin-Yi Chen2f3cb9a2018-01-02 16:05:18 +0800547 self.tools_info.update(provider.GetAdditionalFile())
Keun Soo Yim8cdfb522018-01-04 15:52:16 -0800548
549 if self.device_image_info:
550 logging.info("device images:\n%s", "\n".join(
551 image + ": " + path
552 for image, path in self.device_image_info.iteritems()))
553 if self.test_suite_info:
554 logging.info("test suites:\n%s", "\n".join(
555 suite + ": " + path
556 for suite, path in self.test_suite_info.iteritems()))
Hsin-Yi Chen2f3cb9a2018-01-02 16:05:18 +0800557 if self.tools_info:
558 logging.info("additional files:\n%s", "\n".join(
559 rel_path + ": " + full_path
560 for rel_path, full_path in self.tools_info.iteritems()))
Keun Soo Yim8cdfb522018-01-04 15:52:16 -0800561
562 def help_fetch(self):
563 """Prints help message for fetch command."""
564 self._fetch_parser.print_help(self._out_file)
565
566 def DownloadTestResources(self, request_id):
567 """Download all of the test resources for a TFC request id.
568
569 Args:
570 request_id: int, TFC request id
571 """
572 resources = self._tfc_client.TestResourceList(request_id)
573 for resource in resources:
574 self.DownloadTestResource(resource['url'])
575
576 def DownloadTestResource(self, url):
577 """Download a test resource with build provider, given a url.
578
579 Args:
580 url: a resource locator (not necessarily HTTP[s])
581 with the scheme specifying the build provider.
582 """
583 parsed = urlparse.urlparse(url)
584 path = (parsed.netloc + parsed.path).split('/')
Keun Soo Yim8cdfb522018-01-04 15:52:16 -0800585 if parsed.scheme == "pab":
586 if len(path) != 5:
587 print("Invalid pab resource locator: %s" % url)
588 return
589 account_id, branch, target, build_id, artifact_name = path
590 cmd = ("fetch"
591 " --type=pab"
592 " --account_id=%s"
593 " --branch=%s"
594 " --target=%s"
595 " --build_id=%s"
596 " --artifact_name=%s") % (account_id, branch, target,
597 build_id, artifact_name)
598 self.onecmd(cmd)
Keun Soo Yim8cdfb522018-01-04 15:52:16 -0800599 elif parsed.scheme == "ab":
600 if len(path) != 4:
601 print("Invalid ab resource locator: %s" % url)
602 return
603 branch, target, build_id, artifact_name = path
604 cmd = ("fetch"
605 "--type=ab"
606 " --branch=%s"
607 " --target=%s"
608 " --build_id=%s"
609 " --artifact_name=%s") % (branch, target, build_id,
610 artifact_name)
611 self.onecmd(cmd)
612 elif parsed.scheme == gcs:
613 cmd = "fetch --type=gcs --path=%s" % url
614 self.onecmd(cmd)
615 else:
616 print "Invalid URL: %s" % url
617
618 def _InitFlashParser(self):
619 """Initializes the parser for flash command."""
620 self._flash_parser = ConsoleArgumentParser("flash",
621 "Flash images to a device.")
622 self._flash_parser.add_argument(
Keun Soo Yim55c6d052018-01-27 16:33:07 -0800623 "--image",
624 help=("The file name of an image to flash."
625 " Used to flash a single image."))
626 self._flash_parser.add_argument(
Keun Soo Yim8cdfb522018-01-04 15:52:16 -0800627 "--current",
628 metavar="PARTITION_IMAGE",
629 nargs="*",
630 type=lambda x: x.split("="),
631 help="The partitions and images to be flashed. The format is "
632 "<partition>=<image>. If PARTITION_IMAGE list is empty, "
633 "currently fetched " + ", ".join(_DEFAULT_FLASH_IMAGES) +
634 " will be flashed.")
635 self._flash_parser.add_argument(
636 "--serial", default="", help="Serial number for device.")
637 self._flash_parser.add_argument(
638 "--build_dir",
639 help="Directory containing build images to be flashed.")
640 self._flash_parser.add_argument(
641 "--gsi", help="Path to generic system image")
642 self._flash_parser.add_argument(
643 "--vbmeta", help="Path to vbmeta image")
644 self._flash_parser.add_argument(
645 "--flasher_type",
646 default="fastboot",
Hsin-Yi Chen9e27bfc2017-12-28 17:16:19 +0800647 help="Flasher type. Valid arguments are \"fastboot\", \"custom\", "
648 "and full module name followed by class name. The class must "
649 "inherit build_flasher.BuildFlasher, and implement "
650 "__init__(serial, flasher_path) and "
651 "Flash(device_images, additional_files, *flasher_args).")
Keun Soo Yim8cdfb522018-01-04 15:52:16 -0800652 self._flash_parser.add_argument(
Keun Soo Yimfeb66eb2018-01-08 15:15:09 -0800653 "--flasher_path",
654 default=None,
655 help="Path to a flasher binary")
Keun Soo Yim8cdfb522018-01-04 15:52:16 -0800656 self._flash_parser.add_argument(
Hsin-Yi Chen9e27bfc2017-12-28 17:16:19 +0800657 "flasher_args",
658 metavar="ARGUMENTS",
659 nargs="*",
660 help="The arguments passed to the flasher binary. If any argument "
661 "starts with \"-\", place all of them after \"--\" at end of "
662 "line.")
663 self._flash_parser.add_argument(
Keun Soo Yim8cdfb522018-01-04 15:52:16 -0800664 "--reboot_mode",
665 default="bootloader",
666 choices=("bootloader", "download"),
667 help="Reboot device to bootloader/download mode")
668 self._flash_parser.add_argument(
Keun Soo Yim8cdfb522018-01-04 15:52:16 -0800669 "--repackage",
670 default="tar.md5",
671 choices=("tar.md5"),
672 help="Repackage artifacts into given format before flashing.")
Keun Soo Yim55c6d052018-01-27 16:33:07 -0800673 self._flash_parser.add_argument(
674 "--wait-for-boot",
675 default="true",
676 help="false to not wait for devie booting.")
677 self._flash_parser.add_argument(
678 "--reboot",
679 default="false",
680 help="true to reboot the device(s).")
Keun Soo Yim8cdfb522018-01-04 15:52:16 -0800681
682 def do_flash(self, line):
683 """Flash GSI or build images to a device connected with ADB."""
684 args = self._flash_parser.ParseLine(line)
685
Hsin-Yi Chen9e27bfc2017-12-28 17:16:19 +0800686 # path
687 if (self.tools_info is not None and
688 args.flasher_path in self.tools_info):
Keun Soo Yim8cdfb522018-01-04 15:52:16 -0800689 flasher_path = self.tools_info[args.flasher_path]
Hsin-Yi Chen9e27bfc2017-12-28 17:16:19 +0800690 elif args.flasher_path:
691 flasher_path = args.flasher_path
Keun Soo Yim8cdfb522018-01-04 15:52:16 -0800692 else:
Keun Soo Yimfeb66eb2018-01-08 15:15:09 -0800693 flasher_path = ""
Hyunwoo Kod6e7d682018-02-05 17:01:48 +0900694 if os.path.exists(flasher_path):
695 flasher_mode = os.stat(flasher_path).st_mode
696 os.chmod(flasher_path,
697 flasher_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
Keun Soo Yim8cdfb522018-01-04 15:52:16 -0800698
Hsin-Yi Chen9e27bfc2017-12-28 17:16:19 +0800699 # serial numbers
Keun Soo Yim8cdfb522018-01-04 15:52:16 -0800700 if args.serial:
Hsin-Yi Chen9e27bfc2017-12-28 17:16:19 +0800701 flasher_serials = [args.serial]
Keun Soo Yim8cdfb522018-01-04 15:52:16 -0800702 elif self._serials:
Hsin-Yi Chen9e27bfc2017-12-28 17:16:19 +0800703 flasher_serials = self._serials
Keun Soo Yim8cdfb522018-01-04 15:52:16 -0800704 else:
Hsin-Yi Chen9e27bfc2017-12-28 17:16:19 +0800705 flasher_serials = [""]
Keun Soo Yim8cdfb522018-01-04 15:52:16 -0800706
Hsin-Yi Chen9e27bfc2017-12-28 17:16:19 +0800707 # images
Keun Soo Yim55c6d052018-01-27 16:33:07 -0800708 if args.image:
709 partition_image = {}
710 partition_image[args.image] = self.device_image_info[args.image]
Keun Soo Yim8cdfb522018-01-04 15:52:16 -0800711 else:
Keun Soo Yim55c6d052018-01-27 16:33:07 -0800712 if args.current:
713 partition_image = dict((partition, self.device_image_info[image])
714 for partition, image in args.current)
715 else:
716 partition_image = dict((image.rsplit(".img", 1)[0],
717 self.device_image_info[image])
718 for image in _DEFAULT_FLASH_IMAGES
719 if image in self.device_image_info)
Keun Soo Yim8cdfb522018-01-04 15:52:16 -0800720
Hsin-Yi Chen9e27bfc2017-12-28 17:16:19 +0800721 # type
722 if args.flasher_type in ("fastboot", "custom"):
723 flasher_class = build_flasher.BuildFlasher
724 else:
725 class_path = args.flasher_type.rsplit(".", 1)
726 flasher_module = importlib.import_module(class_path[0])
727 flasher_class = getattr(flasher_module, class_path[1])
728 if not issubclass(flasher_class, build_flasher.BuildFlasher):
729 raise TypeError("%s is not a subclass of BuildFlasher." %
730 class_path[1])
731
732 flashers = [flasher_class(s, flasher_path) for s in flasher_serials]
733
734 # Can be parallelized as long as that's proven reliable.
735 for flasher in flashers:
736 if args.flasher_type == "fastboot":
Keun Soo Yim55c6d052018-01-27 16:33:07 -0800737 if args.image is not None:
738 flasher.FlashImage(
739 partition_image,
740 True if args.reboot == "true" else False)
741 elif args.current is not None:
Keun Soo Yim8cdfb522018-01-04 15:52:16 -0800742 flasher.Flash(partition_image)
743 else:
Hsin-Yi Chen9e27bfc2017-12-28 17:16:19 +0800744 if args.gsi is None and args.build_dir is None:
Keun Soo Yim8cdfb522018-01-04 15:52:16 -0800745 self._flash_parser.error(
Hsin-Yi Chen9e27bfc2017-12-28 17:16:19 +0800746 "Nothing requested: "
747 "specify --gsi or --build_dir")
748 if args.build_dir is not None:
749 flasher.Flashall(args.build_dir)
750 if args.gsi is not None:
751 flasher.FlashGSI(args.gsi, args.vbmeta)
752 elif args.flasher_type == "custom":
753 if flasher_path is not None:
754 if args.repackage is not None:
755 flasher.RepackageArtifacts(
756 self.device_image_info, args.repackage)
757 flasher.FlashUsingCustomBinary(
758 self.device_image_info, args.reboot_mode,
759 args.flasher_args, 300)
760 else:
761 self._flash_parser.error(
762 "Please specify the path to custom flash tool.")
763 else:
764 flasher.Flash(
765 partition_image, self.tools_info, *args.flasher_args)
Keun Soo Yim8cdfb522018-01-04 15:52:16 -0800766
Keun Soo Yim55c6d052018-01-27 16:33:07 -0800767 if args.wait_for_boot == "true":
768 for flasher in flashers:
769 flasher.WaitForDevice()
Keun Soo Yim8cdfb522018-01-04 15:52:16 -0800770
771 def help_flash(self):
772 """Prints help message for flash command."""
773 self._flash_parser.print_help(self._out_file)
774
Keun Soo Yim0e3b0082018-01-08 15:10:02 -0800775 def _InitBuildParser(self):
776 """Initializes the parser for build command."""
777 self._build_parser = ConsoleArgumentParser(
778 "build", "Specifies branches and targets to monitor.")
779 self._build_parser.add_argument(
780 "--update",
781 choices=("single", "start", "stop", "list"),
782 default="start",
783 help="Update build info")
784 self._build_parser.add_argument(
785 "--id",
786 default=None,
787 help="session ID only required for 'stop' update command")
788 self._build_parser.add_argument(
789 "--interval",
790 type=int,
791 default=30,
792 help="Interval (seconds) to repeat build update.")
793 self._build_parser.add_argument(
794 "--artifact-type",
795 choices=("device", "gsi", "test"),
796 default="device",
797 help="The type of an artifact to update")
798 self._build_parser.add_argument(
799 "--branch",
800 required=True,
801 help="Branch to grab the artifact from.")
802 self._build_parser.add_argument(
803 "--target",
804 required=True,
805 help="a comma-separate list of build target product(s).")
806 self._build_parser.add_argument(
807 "--account_id",
808 default=_DEFAULT_ACCOUNT_ID,
809 help="Partner Android Build account_id to use.")
Keun Soo Yimb6912672018-01-21 21:44:02 -0800810 self._build_parser.add_argument(
Keun Soo Yima981ec02018-01-21 21:48:58 -0800811 "--method",
812 default="GET",
813 choices=("GET", "POST"),
814 help="Method for getting build information")
815 self._build_parser.add_argument(
816 "--userinfo-file",
817 help=
818 "Location of file containing email and password, if using POST.")
819 self._build_parser.add_argument(
Keun Soo Yimb6912672018-01-21 21:44:02 -0800820 "--noauth_local_webserver",
821 default=False,
822 type=bool,
823 help="True to not use a local webserver for authentication.")
Keun Soo Yim0e3b0082018-01-08 15:10:02 -0800824
Keun Soo Yima981ec02018-01-21 21:48:58 -0800825 def UpdateBuild(self, account_id, branch, targets, artifact_type, method,
826 userinfo_file, noauth_local_webserver):
Keun Soo Yim0e3b0082018-01-08 15:10:02 -0800827 """Updates the build state.
828
829 Args:
830 account_id: string, Partner Android Build account_id to use.
831 branch: string, branch to grab the artifact from.
832 targets: string, a comma-separate list of build target product(s).
Keun Soo Yim54f71d12018-01-21 21:40:11 -0800833 artifact_type: string, artifact type (`device`, 'gsi' or `test').
Keun Soo Yima981ec02018-01-21 21:48:58 -0800834 method: string, method for getting build information.
835 userinfo_file: string, the path of a file containing email and
836 password (if method == POST).
Keun Soo Yim54f71d12018-01-21 21:40:11 -0800837 noauth_local_webserver: boolean, True to not use a local websever.
Keun Soo Yim0e3b0082018-01-08 15:10:02 -0800838 """
839 builds = []
840
Keun Soo Yim54f71d12018-01-21 21:40:11 -0800841 self._build_provider["pab"].Authenticate(
Keun Soo Yima981ec02018-01-21 21:48:58 -0800842 userinfo_file=userinfo_file,
Keun Soo Yim54f71d12018-01-21 21:40:11 -0800843 noauth_local_webserver=noauth_local_webserver)
Keun Soo Yim0e3b0082018-01-08 15:10:02 -0800844 for target in targets.split(","):
845 listed_builds = self._build_provider[
846 "pab"].GetBuildList(
847 account_id=account_id,
848 branch=branch,
849 target=target,
850 page_token="",
851 max_results=100,
Keun Soo Yima981ec02018-01-21 21:48:58 -0800852 method=method)
Keun Soo Yim0e3b0082018-01-08 15:10:02 -0800853
854 for listed_build in listed_builds:
Keun Soo Yima981ec02018-01-21 21:48:58 -0800855 if method == "GET":
856 if "successful" in listed_build:
857 if listed_build["successful"]:
858 build = {}
859 build["manifest_branch"] = branch
860 build["build_id"] = listed_build["build_id"]
861 if "-" in target:
862 build["build_target"], build["build_type"] = target.split("-")
863 else:
864 build["build_target"] = target
865 build["build_type"] = ""
866 build["artifact_type"] = artifact_type
867 build["artifacts"] = []
868 builds.append(build)
869 else:
870 print("Error: listed_build %s" % listed_build)
871 else: # POST
Keun Soo Yim0e3b0082018-01-08 15:10:02 -0800872 build = {}
873 build["manifest_branch"] = branch
Keun Soo Yima981ec02018-01-21 21:48:58 -0800874 build["build_id"] = listed_build[u"1"]
Keun Soo Yim0e3b0082018-01-08 15:10:02 -0800875 if "-" in target:
Keun Soo Yima981ec02018-01-21 21:48:58 -0800876 (build["build_target"],
877 build["build_type"]) = target.split("-")
Keun Soo Yim0e3b0082018-01-08 15:10:02 -0800878 else:
879 build["build_target"] = target
880 build["build_type"] = ""
881 build["artifact_type"] = artifact_type
882 build["artifacts"] = []
883 builds.append(build)
884 self._vti_endpoint_client.UploadBuildInfo(builds)
885
Keun Soo Yima981ec02018-01-21 21:48:58 -0800886 def UpdateBuildLoop(self, account_id, branch, target, artifact_type, method,
887 userinfo_file, noauth_local_webserver, update_interval):
Keun Soo Yim0e3b0082018-01-08 15:10:02 -0800888 """Regularly updates the build information.
889
890 Args:
891 account_id: string, Partner Android Build account_id to use.
892 branch: string, branch to grab the artifact from.
893 targets: string, a comma-separate list of build target product(s).
894 artifact_type: string, artifcat type (`device`, 'gsi' or `test).
Keun Soo Yima981ec02018-01-21 21:48:58 -0800895 method: string, method for getting build information.
896 userinfo_file: string, the path of a file containing email and
897 password (if method == POST).
Keun Soo Yim54f71d12018-01-21 21:40:11 -0800898 noauth_local_webserver: boolean, True to not use a local websever.
Keun Soo Yim0e3b0082018-01-08 15:10:02 -0800899 update_interval: int, number of seconds before repeating
900 """
901 thread = threading.currentThread()
902 while getattr(thread, 'keep_running', True):
903 try:
Keun Soo Yim54f71d12018-01-21 21:40:11 -0800904 self.UpdateBuild(account_id, branch, target,
Keun Soo Yima981ec02018-01-21 21:48:58 -0800905 artifact_type, method, userinfo_file,
906 noauth_local_webserver)
Keun Soo Yim0e3b0082018-01-08 15:10:02 -0800907 except (socket.error, remote_operation.RemoteOperationException,
908 httplib2.HttpLib2Error, errors.HttpError) as e:
909 logging.exception(e)
910 time.sleep(update_interval)
911
912 def do_build(self, line):
913 """Updates build info."""
914 args = self._build_parser.ParseLine(line)
915 if args.update == "single":
Jongmok Hong95bc86c2018-01-10 18:36:43 +0900916 self.UpdateBuild(
Keun Soo Yim0e3b0082018-01-08 15:10:02 -0800917 args.account_id,
918 args.branch,
919 args.target,
Keun Soo Yim54f71d12018-01-21 21:40:11 -0800920 args.artifact_type,
Keun Soo Yima981ec02018-01-21 21:48:58 -0800921 args.method,
922 args.userinfo_file,
Keun Soo Yim54f71d12018-01-21 21:40:11 -0800923 args.noauth_local_webserver)
Keun Soo Yim0e3b0082018-01-08 15:10:02 -0800924 elif args.update == "list":
925 print("Running build update sessions:")
926 for id in self.build_thread:
927 print(" ID %d", id)
928 elif args.update == "start":
929 if args.interval <= 0:
930 raise ConsoleArgumentError(
931 "update interval must be positive")
932 # do not allow user to create new
933 # thread if one is currently running
934 if args.id is None:
935 if not self.build_thread:
936 args.id = 1
937 else:
938 args.id = max(self.build_thread) + 1
939 else:
940 args.id = int(args.id)
941 if args.id in self.build_thread and not hasattr(
942 self.build_thread[args.id], 'keep_running'):
943 print(
944 'build update (session ID: %s) already running. '
945 'run build --update stop first.' % args.id
946 )
947 return
948 self.build_thread[args.id] = threading.Thread(
949 target=self.UpdateBuildLoop,
950 args=(
951 args.account_id,
952 args.branch,
953 args.target,
954 args.artifact_type,
Keun Soo Yima981ec02018-01-21 21:48:58 -0800955 args.method,
956 args.userinfo_file,
Keun Soo Yim54f71d12018-01-21 21:40:11 -0800957 args.noauth_local_webserver,
Keun Soo Yim0e3b0082018-01-08 15:10:02 -0800958 args.interval, ))
959 self.build_thread[args.id].daemon = True
960 self.build_thread[args.id].start()
961 elif args.update == "stop":
962 if args.id is None:
963 print("--id must be set for stop")
964 else:
965 self.build_thread[int(args.id)].keep_running = False
966
967 def help_build(self):
968 """Prints help message for build command."""
969 self._build_parser.print_help(self._out_file)
970 print("Sample: build --target=aosp_sailfish-userdebug "
971 "--branch=<branch name> --artifact-type=device")
972
Keun Soo Yimed07bdf2018-01-08 14:34:14 -0800973 def _InitConfigParser(self):
974 """Initializes the parser for config command."""
975 self._config_parser = ConsoleArgumentParser(
976 "config", "Specifies a global config type to monitor.")
977 self._config_parser.add_argument(
978 "--update",
979 choices=("single", "start", "stop", "list"),
980 default="start",
981 help="Update build info")
982 self._config_parser.add_argument(
983 "--id",
984 default=None,
985 help="session ID only required for 'stop' update command")
986 self._config_parser.add_argument(
987 "--interval",
988 type=int,
989 default=60,
990 help="Interval (seconds) to repeat build update.")
991 self._config_parser.add_argument(
992 "--config-type",
993 choices=("prod", "test"),
994 default="prod",
995 help="Whether it's for prod")
996 self._config_parser.add_argument(
997 "--branch",
998 required=True,
999 help="Branch to grab the artifact from.")
1000 self._config_parser.add_argument(
1001 "--target",
1002 required=True,
1003 help="a comma-separate list of build target product(s).")
1004 self._config_parser.add_argument(
1005 "--account_id",
1006 default=_DEFAULT_ACCOUNT_ID,
1007 help="Partner Android Build account_id to use.")
1008 self._config_parser.add_argument(
1009 '--method',
1010 default='GET',
1011 choices=('GET', 'POST'),
1012 help='Method for fetching')
1013
1014 def UpdateConfig(self, account_id, branch, targets, config_type, method):
1015 """Updates the global configuration data.
1016
1017 Args:
1018 account_id: string, Partner Android Build account_id to use.
1019 branch: string, branch to grab the artifact from.
1020 targets: string, a comma-separate list of build target product(s).
1021 config_type: string, config type (`prod` or `test').
1022 method: string, HTTP method for fetching.
1023 """
1024
1025 self._build_provider["pab"].Authenticate()
1026 for target in targets.split(","):
1027 listed_builds = self._build_provider[
1028 "pab"].GetBuildList(
1029 account_id=account_id,
1030 branch=branch,
1031 target=target,
1032 page_token="",
1033 max_results=1,
1034 method="GET")
1035
1036 if listed_builds and len(listed_builds) > 0:
1037 listed_build = listed_builds[0]
1038 if listed_build["successful"]:
1039 device_images, test_suites, artifacts, configs = self._build_provider[
1040 "pab"].GetArtifact(
1041 account_id=account_id,
1042 branch=branch,
1043 target=target,
1044 artifact_name=("vti-global-config-%s.zip" % config_type),
1045 build_id=listed_build["build_id"],
1046 method=method)
1047 base_path = os.path.dirname(configs[config_type])
1048 schedules_pbs = []
1049 lab_pbs = []
1050 for root, dirs, files in os.walk(base_path):
1051 for config_file in files:
1052 full_path = os.path.join(root, config_file)
Jongmok Hong938ac2b2018-01-17 19:42:04 +09001053 if config_file.endswith(".schedule_config"):
Keun Soo Yimed07bdf2018-01-08 14:34:14 -08001054 with open(full_path, "r") as fd:
1055 context = fd.read()
1056 sched_cfg_msg = SchedCfgMsg.ScheduleConfigMessage()
1057 text_format.Merge(context, sched_cfg_msg)
1058 schedules_pbs.append(sched_cfg_msg)
1059 print sched_cfg_msg.manifest_branch
1060 elif config_file.endswith(".lab_config"):
1061 with open(full_path, "r") as fd:
1062 context = fd.read()
1063 lab_cfg_msg = LabCfgMsg.LabConfigMessage()
1064 text_format.Merge(context, lab_cfg_msg)
1065 lab_pbs.append(lab_cfg_msg)
1066 self._vti_endpoint_client.UploadScheduleInfo(schedules_pbs)
1067 self._vti_endpoint_client.UploadLabInfo(lab_pbs)
1068
1069 def UpdateConfigLoop(self, account_id, branch, target, config_type, method, update_interval):
1070 """Regularly updates the global configuration.
1071
1072 Args:
1073 account_id: string, Partner Android Build account_id to use.
1074 branch: string, branch to grab the artifact from.
1075 targets: string, a comma-separate list of build target product(s).
1076 config_type: string, config type (`prod` or `test').
1077 method: string, HTTP method for fetching.
1078 update_interval: int, number of seconds before repeating
1079 """
1080 thread = threading.currentThread()
1081 while getattr(thread, 'keep_running', True):
1082 try:
1083 self.UpdateConfig(account_id, branch, target, config_type, method)
1084 except (socket.error, remote_operation.RemoteOperationException,
1085 httplib2.HttpLib2Error, errors.HttpError) as e:
1086 logging.exception(e)
1087 time.sleep(update_interval)
1088
1089 def do_config(self, line):
1090 """Updates global config."""
1091 args = self._config_parser.ParseLine(line)
1092 if args.update == "single":
1093 self.UpdateConfig(
1094 args.account_id,
1095 args.branch,
1096 args.target,
1097 args.config_type,
1098 args.method)
1099 elif args.update == "list":
1100 print("Running config update sessions:")
1101 for id in self.schedule_thread:
1102 print(" ID %d", id)
1103 elif args.update == "start":
1104 if args.interval <= 0:
1105 raise ConsoleArgumentError(
1106 "update interval must be positive")
1107 # do not allow user to create new
1108 # thread if one is currently running
1109 if args.id is None:
1110 if not self.schedule_thread:
1111 args.id = 1
1112 else:
1113 args.id = max(self.schedule_thread) + 1
1114 else:
1115 args.id = int(args.id)
1116 if args.id in self.schedule_thread and not hasattr(
1117 self.schedule_thread[args.id], 'keep_running'):
1118 print(
1119 'config update already running. '
1120 'run config --update=stop --id=%s first.' % args.id
1121 )
1122 return
1123 self.schedule_thread[args.id] = threading.Thread(
1124 target=self.UpdateConfigLoop,
1125 args=(
1126 args.account_id,
1127 args.branch,
1128 args.target,
1129 args.config_type,
1130 args.method,
1131 args.interval, ))
1132 self.schedule_thread[args.id].daemon = True
1133 self.schedule_thread[args.id].start()
1134 elif args.update == "stop":
1135 if args.id is None:
1136 print("--id must be set for stop")
1137 else:
1138 self.schedule_thread[int(args.id)].keep_running = False
1139
1140 def help_config(self):
1141 """Prints help message for config command."""
1142 self._config_parser.print_help(self._out_file)
1143 print("Sample: schedule --target=aosp_sailfish-userdebug "
1144 "--branch=git_oc-release")
1145
Keun Soo Yim8cdfb522018-01-04 15:52:16 -08001146 def _InitCopyParser(self):
1147 """Initializes the parser for copy command."""
1148 self._copy_parser = ConsoleArgumentParser("copy", "Copy a file.")
1149
1150 def do_copy(self, line):
1151 """Copy a file from source to destination path."""
1152 src, dst = line.split()
1153 if dst == "{vts_tf_home}":
1154 dst = os.path.dirname(self.test_suite_info["vts"])
1155 elif "{" in dst:
1156 print("unknown dst %s" % dst)
1157 return
1158 shutil.copy(src, dst)
1159
1160 def help_copy(self):
1161 """Prints help message for copy command."""
1162 self._copy_parser.print_help(self._out_file)
1163
1164 def _InitDeviceParser(self):
1165 """Initializes the parser for device command."""
1166 self._device_parser = ConsoleArgumentParser(
1167 "device", "Selects device(s) under test.")
1168 self._device_parser.add_argument(
1169 "--set_serial",
1170 default="",
1171 help="Serial number for device. Can be a comma-separated list.")
1172 self._device_parser.add_argument(
1173 "--update",
1174 choices=("single", "start", "stop"),
Keun Soo Yimcb513252018-01-08 14:17:14 -08001175 default="start",
1176 help="Update device info on cloud scheduler")
Keun Soo Yim8cdfb522018-01-04 15:52:16 -08001177 self._device_parser.add_argument(
1178 "--interval",
1179 type=int,
1180 default=30,
1181 help="Interval (seconds) to repeat device update.")
1182 self._device_parser.add_argument(
1183 "--host", type=int, help="The index of the host.")
Keun Soo Yimcb513252018-01-08 14:17:14 -08001184 self._device_parser.add_argument(
1185 "--server_type",
1186 choices=("vti", "tfc"),
1187 default="vti",
1188 help="The type of a cloud-based test scheduler server.")
Keun Soo Yim4b3fc312018-01-09 15:37:52 -08001189 self._device_parser.add_argument(
1190 "--lease",
1191 default=False,
1192 type=bool,
1193 help="Whether to lease jobs and execute them.")
Hsin-Yi Chen18ee53c2018-01-22 17:47:07 +08001194
1195 def SetSerials(self, serials):
1196 """Sets the default serial numbers for flashing and testing.
1197
1198 Args:
1199 serials: A list of strings, the serial numbers.
1200 """
1201 self._serials = serials
Keun Soo Yim8cdfb522018-01-04 15:52:16 -08001202
Hsin-Yi Chen02592912018-01-17 19:44:17 +08001203 def GetSerials(self):
1204 """Returns the serial numbers saved in the console.
1205
1206 Returns:
1207 A list of strings, the serial numbers.
1208 """
1209 return self._serials
1210
Hyunwoo Kodfd17d92018-02-01 14:58:56 +09001211 def StartLeasedJobExecutionProcess(self, map_interval=1):
Hyunwoo Kocdda4e82018-01-25 19:44:25 +09001212 """Executes process that runs the leased job.
1213
1214 Args:
1215 map_interval: int, time between mapping the leased jobs to process
1216 pool in seconds. Default value is 30
1217 """
Keun Soo Yim4fa65242018-02-10 17:38:13 -08001218 leased_job_queue = multiprocessing.Queue()
1219 leased_job_running = multiprocessing.Value("b", 0)
1220 process = multiprocessing.Process(
Hyunwoo Kocdda4e82018-01-25 19:44:25 +09001221 target=self.ExecLeasedJobs,
Hyunwoo Kodfd17d92018-02-01 14:58:56 +09001222 args=(leased_job_queue,
1223 leased_job_running,
Hyunwoo Kocdda4e82018-01-25 19:44:25 +09001224 map_interval,)
1225 )
Hyunwoo Kodfd17d92018-02-01 14:58:56 +09001226 process.start()
1227
1228 self.leased_job_process = process
1229 self.leased_job_queue = leased_job_queue
1230 self.leased_job_running = leased_job_running
1231
1232 def StopLeasedJobExecutionProcess(self):
1233 """Terminates the process that runs the leased job."""
1234 self.leased_job_running = 0
1235 self.leased_job_process.terminate()
Hyunwoo Kocdda4e82018-01-25 19:44:25 +09001236
1237 def ExecJobOnProcessPool(self, job):
1238 """Executes a job from job queue.
1239
1240 Args:
1241 job: dict, contains the information on the leased job passed to this
1242 process.
1243 """
1244 print("Executing a leased job on process {}".format(os.getpid()))
1245 filepath, kwargs = self._vti_endpoint_client.ExecuteJob(job)
1246 if filepath:
1247 ret = self.ProcessConfigurableScript(
1248 os.path.join(os.getcwd(), "host_controller", "campaigns",
1249 filepath),
1250 **kwargs)
1251 if ret:
Hyunwoo Koa75a3332018-02-07 18:08:21 +09001252 job_status = "complete"
Hyunwoo Kocdda4e82018-01-25 19:44:25 +09001253 else:
Hyunwoo Koa75a3332018-02-07 18:08:21 +09001254 job_status = "infra_error"
Hyunwoo Kocdda4e82018-01-25 19:44:25 +09001255
1256 self._vti_endpoint_client.StopHeartbeat(job_status)
1257 print("Job execution complete. "
1258 "Setting job status to {}".format(job_status))
1259
1260 def ExecLeasedJobs(self, job_queue, process_running, map_interval):
1261 """Pops jobs from job_queue and execute them on process pool.
1262
1263 Args:
Keun Soo Yim4fa65242018-02-10 17:38:13 -08001264 job_queue: multiprocesing.Queue, a queue from which to get the
Hyunwoo Kocdda4e82018-01-25 19:44:25 +09001265 leased jobs. Shared with the main process.
Hyunwoo Kodfd17d92018-02-01 14:58:56 +09001266 process_running: byte, continue to run the while loop if equal to 1.
Hyunwoo Kocdda4e82018-01-25 19:44:25 +09001267 Shared with the main process.
1268 map_interval: int, time between mapping the leased jobs to process
1269 pool in seconds.
1270 """
Keun Soo Yim4fa65242018-02-10 17:38:13 -08001271 leased_job_exec_pool = multiprocessing.Pool(processes=_MAX_LEASED_JOBS)
Hyunwoo Kodfd17d92018-02-01 14:58:56 +09001272
Hyunwoo Kocdda4e82018-01-25 19:44:25 +09001273 while True:
1274 if process_running:
Hyunwoo Kodfd17d92018-02-01 14:58:56 +09001275 arg_list = []
Hyunwoo Kocdda4e82018-01-25 19:44:25 +09001276 while not job_queue.empty():
Hyunwoo Kodfd17d92018-02-01 14:58:56 +09001277 leased_job = job_queue.get()
1278 pair = (self, leased_job)
1279 arg_list.append(pair)
Hyunwoo Kocdda4e82018-01-25 19:44:25 +09001280
Hyunwoo Kodfd17d92018-02-01 14:58:56 +09001281 if len(arg_list) > 0:
1282 leased_job_exec_pool.map_async(ExecJobOnProcessPoolWrapper,
1283 arg_list)
Hyunwoo Kocdda4e82018-01-25 19:44:25 +09001284 time.sleep(map_interval)
1285
Keun Soo Yim4b3fc312018-01-09 15:37:52 -08001286 def UpdateDevice(self, server_type, host, lease):
Keun Soo Yim8cdfb522018-01-04 15:52:16 -08001287 """Updates the device state of all devices on a given host.
1288
1289 Args:
Keun Soo Yimcb513252018-01-08 14:17:14 -08001290 server_type: string, the type of a test secheduling server.
Keun Soo Yim8cdfb522018-01-04 15:52:16 -08001291 host: HostController object
Keun Soo Yim4b3fc312018-01-09 15:37:52 -08001292 lease: boolean, True to lease and execute jobs.
Keun Soo Yim8cdfb522018-01-04 15:52:16 -08001293 """
Keun Soo Yimcb513252018-01-08 14:17:14 -08001294 if server_type == "vti":
1295 devices = []
Keun Soo Yim8cdfb522018-01-04 15:52:16 -08001296
Keun Soo Yimcb513252018-01-08 14:17:14 -08001297 stdout, stderr, returncode = cmd_utils.ExecuteOneShellCommand(
1298 "adb devices")
1299
1300 lines = stdout.split("\n")[1:]
1301 for line in lines:
1302 if len(line.strip()):
1303 device = {}
1304 device["serial"] = line.split()[0]
1305 stdout, _, retcode = cmd_utils.ExecuteOneShellCommand(
1306 "adb -s %s shell getprop ro.product.board" % device["serial"])
1307 if retcode == 0:
1308 device["product"] = stdout.strip()
1309 else:
1310 device["product"] = "error"
1311 device["status"] = DEVICE_STATUS_DICT["online"]
1312 devices.append(device)
1313
1314 stdout, stderr, returncode = cmd_utils.ExecuteOneShellCommand(
1315 "fastboot devices")
1316 lines = stdout.split("\n")
1317 for line in lines:
1318 if len(line.strip()):
1319 device = {}
1320 device["serial"] = line.split()[0]
Hyunwoo Koee6ec272018-01-18 16:04:03 +09001321 _, stderr, retcode = cmd_utils.ExecuteOneShellCommand(
1322 "fastboot -s %s getvar product" % device["serial"])
1323 if retcode == 0:
1324 res = stderr.splitlines()[0].rstrip()
Hyunwoo Kod0f8b912018-02-07 17:36:00 +09001325 print(res)
1326 if ":" in res:
1327 device["product"] = res.split(":")[1].strip()
1328 else:
1329 device["product"] = "error"
Hyunwoo Koee6ec272018-01-18 16:04:03 +09001330 else:
1331 device["product"] = "error"
Keun Soo Yimcb513252018-01-08 14:17:14 -08001332 device["status"] = DEVICE_STATUS_DICT["fastboot"]
1333 devices.append(device)
1334
1335 self._vti_endpoint_client.UploadDeviceInfo(
1336 host.hostname, devices)
Keun Soo Yim4b3fc312018-01-09 15:37:52 -08001337
1338 if lease:
1339 filepath, kwargs = self._vti_endpoint_client.LeaseJob(
Hyunwoo Kocdda4e82018-01-25 19:44:25 +09001340 socket.gethostname(), False)
Hyunwoo Kod0f8b912018-02-07 17:36:00 +09001341
Hyunwoo Kocdda4e82018-01-25 19:44:25 +09001342 if filepath is not None:
1343 self.leased_job_queue.put(kwargs)
1344
Keun Soo Yimcb513252018-01-08 14:17:14 -08001345 elif server_type == "tfc":
1346 devices = host.ListDevices()
1347 for device in devices:
1348 device.Extend(['sim_state', 'sim_operator', 'mac_address'])
1349 snapshots = self._tfc_client.CreateDeviceSnapshot(
1350 host._cluster_ids[0], host.hostname, devices)
1351 self._tfc_client.SubmitHostEvents([snapshots])
1352 else:
1353 print "Error: unknown server_type %s for UpdateDevice" % server_type
1354
Keun Soo Yim4b3fc312018-01-09 15:37:52 -08001355 def UpdateDeviceRepeat(self, server_type, host, lease, update_interval):
Keun Soo Yim8cdfb522018-01-04 15:52:16 -08001356 """Regularly updates the device state of devices on a given host.
1357
1358 Args:
Keun Soo Yimcb513252018-01-08 14:17:14 -08001359 server_type: string, the type of a test secheduling server.
Keun Soo Yim8cdfb522018-01-04 15:52:16 -08001360 host: HostController object
Keun Soo Yim4b3fc312018-01-09 15:37:52 -08001361 lease: boolean, True to lease and execute jobs.
Keun Soo Yimcb513252018-01-08 14:17:14 -08001362 update_interval: int, number of seconds before repeating
Keun Soo Yim8cdfb522018-01-04 15:52:16 -08001363 """
1364 thread = threading.currentThread()
1365 while getattr(thread, 'keep_running', True):
1366 try:
Keun Soo Yim4b3fc312018-01-09 15:37:52 -08001367 self.UpdateDevice(server_type, host, lease)
Keun Soo Yim8cdfb522018-01-04 15:52:16 -08001368 except (socket.error, remote_operation.RemoteOperationException,
1369 httplib2.HttpLib2Error, errors.HttpError) as e:
1370 logging.exception(e)
1371 time.sleep(update_interval)
1372
1373 def do_device(self, line):
1374 """Sets device info such as serial number."""
1375 args = self._device_parser.ParseLine(line)
1376 if args.set_serial:
Hsin-Yi Chen18ee53c2018-01-22 17:47:07 +08001377 self.SetSerials(args.set_serial.split(","))
Keun Soo Yim8cdfb522018-01-04 15:52:16 -08001378 print("serials: %s" % self._serials)
1379 if args.update:
1380 if args.host is None:
1381 if len(self._hosts) > 1:
1382 raise ConsoleArgumentError("More than one host.")
1383 args.host = 0
1384 host = self._hosts[args.host]
1385 if args.update == "single":
Keun Soo Yim4b3fc312018-01-09 15:37:52 -08001386 self.UpdateDevice(args.server_type, host, args.lease)
Keun Soo Yim8cdfb522018-01-04 15:52:16 -08001387 elif args.update == "start":
1388 if args.interval <= 0:
1389 raise ConsoleArgumentError(
1390 "update interval must be positive")
1391 # do not allow user to create new
1392 # thread if one is currently running
1393 if self.update_thread is not None and not hasattr(
1394 self.update_thread, 'keep_running'):
1395 print('device update already running. '
1396 'run device --update stop first.')
1397 return
1398 self.update_thread = threading.Thread(
1399 target=self.UpdateDeviceRepeat,
1400 args=(
Keun Soo Yimcb513252018-01-08 14:17:14 -08001401 args.server_type,
Keun Soo Yim8cdfb522018-01-04 15:52:16 -08001402 host,
Keun Soo Yim4b3fc312018-01-09 15:37:52 -08001403 args.lease,
Keun Soo Yim8cdfb522018-01-04 15:52:16 -08001404 args.interval,
1405 ))
1406 self.update_thread.daemon = True
1407 self.update_thread.start()
Hyunwoo Kocdda4e82018-01-25 19:44:25 +09001408 if args.lease:
Hyunwoo Kodfd17d92018-02-01 14:58:56 +09001409 self.leased_job_running = 1
Keun Soo Yim8cdfb522018-01-04 15:52:16 -08001410 elif args.update == "stop":
1411 self.update_thread.keep_running = False
Hyunwoo Kocdda4e82018-01-25 19:44:25 +09001412 if self.leased_job_running:
Hyunwoo Kodfd17d92018-02-01 14:58:56 +09001413 self.leased_job_running = 0
Keun Soo Yim8cdfb522018-01-04 15:52:16 -08001414
1415 def help_device(self):
1416 """Prints help message for device command."""
1417 self._device_parser.print_help(self._out_file)
1418
Jongmok Hong71258da2018-01-05 19:17:58 +09001419 def _InitGsiSplParser(self):
1420 """Initializes the parser for device command."""
1421 self._gsisplParser = ConsoleArgumentParser(
1422 "gsispl", "Changes security patch level on a selected GSI file.")
1423 self._gsisplParser.add_argument(
1424 "--gsi",
1425 help="Path to GSI image to change security patch level. "
1426 "If path is not given, the most recently fetched system.img "
1427 "kept in device_image_info dictionary is used and then "
1428 "device_image_info will be updated with the new GSI file.")
1429 self._gsisplParser.add_argument(
Hyunwoo Ko432ccc62018-01-10 17:51:38 +09001430 "--version", help="New version ID. It should be YYYY-mm-dd format")
1431 self._gsisplParser.add_argument(
1432 "--version_from_path",
1433 help="Path to vendor provided image file to retrieve SPL version. "
1434 "If just a file name is given, the most recently fetched .img "
1435 "file will be used.")
Jongmok Hong71258da2018-01-05 19:17:58 +09001436
1437 def do_gsispl(self, line):
1438 """Changes security patch level on a selected GSI file."""
1439 args = self._gsisplParser.ParseLine(line)
1440 if args.gsi:
1441 if os.path.isfile(args.gsi):
1442 gsi_path = args.gsi
1443 else:
1444 print "Cannot find system image in given path"
Jongmok Hongdaffd2b2018-01-08 14:05:11 +09001445 return
Jongmok Hong71258da2018-01-05 19:17:58 +09001446 elif "system.img" in self.device_image_info:
1447 gsi_path = self.device_image_info["system.img"]
1448 else:
1449 print "Cannot find system image."
Jongmok Hongdaffd2b2018-01-08 14:05:11 +09001450 return
Jongmok Hong71258da2018-01-05 19:17:58 +09001451
1452 if args.version:
1453 try:
1454 version_date = datetime.datetime.strptime(
1455 args.version, "%Y-%m-%d")
1456 version = "{:04d}-{:02d}-{:02d}".format(
1457 version_date.year, version_date.month, version_date.day)
1458 except ValueError as e:
1459 print "version ID should be YYYY-mm-dd format."
Jongmok Hongdaffd2b2018-01-08 14:05:11 +09001460 return
Hyunwoo Ko432ccc62018-01-10 17:51:38 +09001461 elif args.version_from_path:
1462 if os.path.isabs(args.version_from_path) and os.path.exists(
1463 args.version_from_path):
1464 img_path = args.version_from_path
1465 elif args.version_from_path in self.device_image_info:
1466 img_path = self.device_image_info[args.version_from_path]
Keun Soo Yim7d95b502018-01-21 21:53:54 -08001467 elif (args.version_from_path == "boot.img" and
1468 "full-zipfile" in self.device_image_info):
1469 tempdir_base = os.path.join(os.getcwd(), "tmp")
1470 if not os.path.exists(tempdir_base):
1471 os.mkdir(tempdir_base)
1472 dest_path = tempfile.mkdtemp(dir=tempdir_base)
1473
1474 with zipfile.ZipFile(self.device_image_info["full-zipfile"], 'r') as zip_ref:
1475 zip_ref.extractall(dest_path)
1476 img_path = os.path.join(dest_path, "boot.img")
Hyunwoo Ko432ccc62018-01-10 17:51:38 +09001477 else:
1478 print("Cannot find %s file." % args.version_from_path)
1479 return
1480
1481 version_dict = img_utils.GetSPLVersionFromBootImg(img_path)
1482 if "year" in version_dict and "month" in version_dict:
1483 version = "{:04d}-{:02d}-{:02d}".format(
1484 version_dict["year"], version_dict["month"],
1485 _SPL_DEFAULT_DAY)
1486 else:
1487 print("Failed to fetch SPL version from %s file." % img_path)
1488 return
Jongmok Hong71258da2018-01-05 19:17:58 +09001489 else:
Hyunwoo Ko432ccc62018-01-10 17:51:38 +09001490 print("version ID or path of .img file must be given.")
Jongmok Hongdaffd2b2018-01-08 14:05:11 +09001491 return
Jongmok Hong71258da2018-01-05 19:17:58 +09001492
Jongmok Hong32930e82018-01-10 18:31:16 +09001493 output_path = os.path.join(
1494 os.path.dirname(os.path.abspath(gsi_path)),
Jongmok Hong71258da2018-01-05 19:17:58 +09001495 "system-{}.img".format(version))
Jongmok Honge22f32f2018-01-25 14:21:58 +09001496 stdout, _, err_code = cmd_utils.ExecuteOneShellCommand(
Jongmok Hong71258da2018-01-05 19:17:58 +09001497 "{} {} {} {}".format(
Jongmok Hong32930e82018-01-10 18:31:16 +09001498 os.path.join(os.getcwd(), "host_controller", "gsi",
1499 "change_security_patch_ver.sh"), gsi_path,
Jongmok Hong71258da2018-01-05 19:17:58 +09001500 output_path, version))
1501 if err_code is 0:
1502 if not args.gsi:
Jongmok Hong32930e82018-01-10 18:31:16 +09001503 print("system.img path is updated to : {}".format(output_path))
Jongmok Hong71258da2018-01-05 19:17:58 +09001504 self.device_image_info["system.img"] = output_path
1505 else:
Jongmok Honge22f32f2018-01-25 14:21:58 +09001506 print "gsispl error: {}".format(stdout)
Jongmok Hongdaffd2b2018-01-08 14:05:11 +09001507 return
Jongmok Hong71258da2018-01-05 19:17:58 +09001508
1509 def help_gsispl(self):
1510 """Prints help message for gsispl command."""
1511 self._gsisplParser.print_help(self._out_file)
1512
Keun Soo Yim8cdfb522018-01-04 15:52:16 -08001513 def _PrintTasks(self, tasks):
1514 """Shows a list of command tasks.
1515
1516 Args:
1517 devices: A list of DeviceInfo objects.
1518 """
1519 attr_names = ("request_id", "command_id", "task_id", "device_serials",
1520 "command_line")
1521 self._PrintObjects(tasks, attr_names)
1522
1523 def do_exit(self, line):
1524 """Terminates the console.
1525
1526 Returns:
1527 True, which stops the cmdloop.
1528 """
Hyunwoo Kodfd17d92018-02-01 14:58:56 +09001529 self.StopLeasedJobExecutionProcess()
Keun Soo Yim8cdfb522018-01-04 15:52:16 -08001530 return True
1531
1532 def help_exit(self):
1533 """Prints help message for exit command."""
1534 self._Print("Terminate the console.")
1535
Jongmok Hong35a4ee42018-01-09 15:22:18 +09001536 def _InitUploadParser(self):
1537 """Initializes the parser for upload command."""
Hsin-Yi Chen30d418f2018-01-29 15:36:39 +08001538 self._upload_parser = ConsoleArgumentParser(
1539 "upload",
1540 "Upload <src> file to <dest> Google Cloud Storage. "
1541 "In <src> and <dest>, variables enclosed in {} are replaced "
1542 "with the values stored in the console.")
Hsin-Yi Chen02592912018-01-17 19:44:17 +08001543 self._upload_parser.add_argument(
Jongmok Hong35a4ee42018-01-09 15:22:18 +09001544 "--src",
1545 required=True,
Jongmok Hong32930e82018-01-10 18:31:16 +09001546 default="latest-system.img",
Jongmok Hong35a4ee42018-01-09 15:22:18 +09001547 help="Path to a source file to upload. Only single file can be "
1548 "uploaded per once. Use 'latest- prefix to upload the latest "
1549 "fetch images. e.g. --src=latest-system.img If argument "
1550 "value is not given, the recently fetched system.img will be "
1551 "uploaded.")
1552 self._upload_parser.add_argument(
1553 "--dest",
1554 required=True,
Hsin-Yi Chen30d418f2018-01-29 15:36:39 +08001555 help="Google Cloud Storage URL to which the file is uploaded.")
Jongmok Hong35a4ee42018-01-09 15:22:18 +09001556
1557 def do_upload(self, line):
1558 """Upload args.src file to args.dest Google Cloud Storage."""
1559 args = self._upload_parser.ParseLine(line)
1560
1561 gsutil_path = build_provider_gcs.BuildProviderGCS.GetGsutilPath()
1562 if not gsutil_path:
1563 print("Please check gsutil is installed and on your PATH")
1564 return
1565
1566 if args.src.startswith("latest-"):
1567 src_name = args.src[7:]
1568 if src_name in self.device_image_info:
1569 src_path = self.device_image_info[src_name]
1570 else:
1571 print("Unable to find {} in device_image_info".format(
1572 src_name))
1573 return
Jongmok Hong35a4ee42018-01-09 15:22:18 +09001574 else:
Hsin-Yi Chen30d418f2018-01-29 15:36:39 +08001575 try:
1576 src_path = self.FormatString(args.src)
1577 except KeyError as e:
1578 print("Unknown or uninitialized variable in src: %s" % e)
1579 return
1580
1581 if not os.path.isfile(src_path):
1582 print("Cannot find a file: {}".format(src_path))
Jongmok Hong35a4ee42018-01-09 15:22:18 +09001583 return
1584
Hsin-Yi Chen30d418f2018-01-29 15:36:39 +08001585 try:
1586 dest_path = self.FormatString(args.dest)
1587 except KeyError as e:
1588 print("Unknown or uninitialized variable in dest: %s" % e)
1589 return
1590
1591 if not dest_path.startswith("gs://"):
1592 print("{} is not correct GCS url.".format(dest_path))
Jongmok Hong35a4ee42018-01-09 15:22:18 +09001593 return
1594 """ TODO(jongmok) : Before upload, login status, authorization,
1595 and dest check are required. """
Hsin-Yi Chen30d418f2018-01-29 15:36:39 +08001596 copy_command = "{} cp {} {}".format(gsutil_path, src_path, dest_path)
Jongmok Hong35a4ee42018-01-09 15:22:18 +09001597 _, stderr, err_code = cmd_utils.ExecuteOneShellCommand(
1598 copy_command)
1599
1600 if err_code:
1601 print stderr
1602
1603 def help_upload(self):
1604 """Prints help message for upload command."""
1605 self._upload_parser.print_help(self._out_file)
1606
Keun Soo Yim8cdfb522018-01-04 15:52:16 -08001607 # @Override
Keun Soo Yim4d812fc2018-01-21 21:51:46 -08001608 def onecmd(self, line, depth=1):
Keun Soo Yim4c2181a2018-01-05 16:22:47 -08001609 """Executes command(s) and prints any exception.
1610
Keun Soo Yim4d812fc2018-01-21 21:51:46 -08001611 Parallel execution only for 2nd-level list element.
1612
Keun Soo Yim4c2181a2018-01-05 16:22:47 -08001613 Args:
1614 line: a list of string or string which keeps the command to run.
1615 """
1616 if type(line) == list:
Keun Soo Yim4d812fc2018-01-21 21:51:46 -08001617 if depth == 1: # 1 to use multi-threading
1618 jobs = []
1619 for sub_command in line:
Keun Soo Yim4fa65242018-02-10 17:38:13 -08001620 p = multiprocessing.Process(
Keun Soo Yim4d812fc2018-01-21 21:51:46 -08001621 target=self.onecmd, args=(sub_command, depth + 1,))
1622 jobs.append(p)
1623 p.start()
1624 for job in jobs:
1625 job.join()
1626 return
1627 else:
1628 for sub_command in line:
1629 self.onecmd(sub_command, depth + 1)
Keun Soo Yim4c2181a2018-01-05 16:22:47 -08001630
Keun Soo Yim8cdfb522018-01-04 15:52:16 -08001631 if line:
1632 print("Command: %s" % line)
1633 try:
1634 return cmd.Cmd.onecmd(self, line)
1635 except Exception as e:
1636 self._Print("%s: %s" % (type(e).__name__, e))
1637 return None
1638
1639 # @Override
1640 def emptyline(self):
1641 """Ignores empty lines."""
1642 pass
1643
1644 # @Override
1645 def default(self, line):
1646 """Handles unrecognized commands.
1647
1648 Returns:
1649 True if receives EOF; otherwise delegates to default handler.
1650 """
1651 if line == "EOF":
1652 return self.do_exit(line)
1653 return cmd.Cmd.default(self, line)
1654
1655
1656def _ToPrintString(obj):
1657 """Converts an object to printable string on console.
1658
1659 Args:
1660 obj: The object to be printed.
1661 """
1662 if isinstance(obj, (list, tuple, set)):
1663 return ",".join(str(x) for x in obj)
1664 return str(obj)