blob: 19007aa64bca28ccb8b6e2ccdd455a363025bdaf [file] [log] [blame]
xixuanba232a32016-08-25 17:01:59 -07001# Copyright (c) 2016 The Chromium OS Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5"""
6This module includes all moblab-related RPCs. These RPCs can only be run
7on moblab.
8"""
9
10# The boto module is only available/used in Moblab for validation of cloud
11# storage access. The module is not available in the test lab environment,
12# and the import error is handled.
13try:
14 import boto
15except ImportError:
16 boto = None
17
18import ConfigParser
Keith Haddow8ef84172016-10-28 15:38:16 -070019import common
xixuanba232a32016-08-25 17:01:59 -070020import logging
21import os
Keith Haddow63cc4472016-10-06 16:21:34 -070022import re
xixuanba232a32016-08-25 17:01:59 -070023import shutil
24import socket
Keith Haddow8ef84172016-10-28 15:38:16 -070025import StringIO
Keith Haddow63cc4472016-10-06 16:21:34 -070026import subprocess
xixuanba232a32016-08-25 17:01:59 -070027
28from autotest_lib.client.common_lib import error
Keith Haddow8ef84172016-10-28 15:38:16 -070029from autotest_lib.client.common_lib import global_config, utils
Michael Tang6a34caf2016-10-21 18:27:03 -070030from autotest_lib.frontend.afe import models
xixuanba232a32016-08-25 17:01:59 -070031from autotest_lib.frontend.afe import rpc_utils
Keith Haddow8ef84172016-10-28 15:38:16 -070032from autotest_lib.server import frontend
xixuanba232a32016-08-25 17:01:59 -070033from autotest_lib.server.hosts import moblab_host
34
Keith Haddow63cc4472016-10-06 16:21:34 -070035
xixuanba232a32016-08-25 17:01:59 -070036_CONFIG = global_config.global_config
37MOBLAB_BOTO_LOCATION = '/home/moblab/.boto'
38
39# Google Cloud Storage bucket url regex pattern. The pattern is used to extract
40# the bucket name from the bucket URL. For example, "gs://image_bucket/google"
41# should result in a bucket name "image_bucket".
42GOOGLE_STORAGE_BUCKET_URL_PATTERN = re.compile(
43 r'gs://(?P<bucket>[a-zA-Z][a-zA-Z0-9-_]*)/?.*')
44
45# Contants used in Json RPC field names.
46_IMAGE_STORAGE_SERVER = 'image_storage_server'
47_GS_ACCESS_KEY_ID = 'gs_access_key_id'
48_GS_SECRETE_ACCESS_KEY = 'gs_secret_access_key'
49_RESULT_STORAGE_SERVER = 'results_storage_server'
50_USE_EXISTING_BOTO_FILE = 'use_existing_boto_file'
51
Keith Haddow63cc4472016-10-06 16:21:34 -070052# Location where dhcp leases are stored.
53_DHCPD_LEASES = '/var/lib/dhcp/dhcpd.leases'
54
55# File where information about the current device is stored.
56_ETC_LSB_RELEASE = '/etc/lsb-release'
xixuanba232a32016-08-25 17:01:59 -070057
58@rpc_utils.moblab_only
59def get_config_values():
60 """Returns all config values parsed from global and shadow configs.
61
62 Config values are grouped by sections, and each section is composed of
63 a list of name value pairs.
64 """
65 sections =_CONFIG.get_sections()
66 config_values = {}
67 for section in sections:
68 config_values[section] = _CONFIG.config.items(section)
69 return rpc_utils.prepare_for_serialization(config_values)
70
71
72def _write_config_file(config_file, config_values, overwrite=False):
73 """Writes out a configuration file.
74
75 @param config_file: The name of the configuration file.
76 @param config_values: The ConfigParser object.
77 @param ovewrite: Flag on if overwriting is allowed.
78 """
79 if not config_file:
80 raise error.RPCException('Empty config file name.')
81 if not overwrite and os.path.exists(config_file):
82 raise error.RPCException('Config file already exists.')
83
84 if config_values:
85 with open(config_file, 'w') as config_file:
86 config_values.write(config_file)
87
88
89def _read_original_config():
90 """Reads the orginal configuratino without shadow.
91
92 @return: A configuration object, see global_config_class.
93 """
94 original_config = global_config.global_config_class()
95 original_config.set_config_files(shadow_file='')
96 return original_config
97
98
99def _read_raw_config(config_file):
100 """Reads the raw configuration from a configuration file.
101
102 @param: config_file: The path of the configuration file.
103
104 @return: A ConfigParser object.
105 """
106 shadow_config = ConfigParser.RawConfigParser()
107 shadow_config.read(config_file)
108 return shadow_config
109
110
111def _get_shadow_config_from_partial_update(config_values):
112 """Finds out the new shadow configuration based on a partial update.
113
114 Since the input is only a partial config, we should not lose the config
115 data inside the existing shadow config file. We also need to distinguish
116 if the input config info overrides with a new value or reverts back to
117 an original value.
118
119 @param config_values: See get_moblab_settings().
120
121 @return: The new shadow configuration as ConfigParser object.
122 """
123 original_config = _read_original_config()
124 existing_shadow = _read_raw_config(_CONFIG.shadow_file)
125 for section, config_value_list in config_values.iteritems():
126 for key, value in config_value_list:
127 if original_config.get_config_value(section, key,
128 default='',
129 allow_blank=True) != value:
130 if not existing_shadow.has_section(section):
131 existing_shadow.add_section(section)
132 existing_shadow.set(section, key, value)
133 elif existing_shadow.has_option(section, key):
134 existing_shadow.remove_option(section, key)
135 return existing_shadow
136
137
138def _update_partial_config(config_values):
139 """Updates the shadow configuration file with a partial config udpate.
140
141 @param config_values: See get_moblab_settings().
142 """
143 existing_config = _get_shadow_config_from_partial_update(config_values)
144 _write_config_file(_CONFIG.shadow_file, existing_config, True)
145
146
147@rpc_utils.moblab_only
148def update_config_handler(config_values):
149 """Update config values and override shadow config.
150
151 @param config_values: See get_moblab_settings().
152 """
153 original_config = _read_original_config()
154 new_shadow = ConfigParser.RawConfigParser()
155 for section, config_value_list in config_values.iteritems():
156 for key, value in config_value_list:
157 if original_config.get_config_value(section, key,
158 default='',
159 allow_blank=True) != value:
160 if not new_shadow.has_section(section):
161 new_shadow.add_section(section)
162 new_shadow.set(section, key, value)
163
164 if not _CONFIG.shadow_file or not os.path.exists(_CONFIG.shadow_file):
165 raise error.RPCException('Shadow config file does not exist.')
166 _write_config_file(_CONFIG.shadow_file, new_shadow, True)
167
168 # TODO (sbasi) crbug.com/403916 - Remove the reboot command and
169 # instead restart the services that rely on the config values.
170 os.system('sudo reboot')
171
172
173@rpc_utils.moblab_only
174def reset_config_settings():
175 """Reset moblab shadow config."""
176 with open(_CONFIG.shadow_file, 'w') as config_file:
177 pass
178 os.system('sudo reboot')
179
180
181@rpc_utils.moblab_only
182def reboot_moblab():
183 """Simply reboot the device."""
184 os.system('sudo reboot')
185
186
187@rpc_utils.moblab_only
188def set_boto_key(boto_key):
189 """Update the boto_key file.
190
191 @param boto_key: File name of boto_key uploaded through handle_file_upload.
192 """
193 if not os.path.exists(boto_key):
194 raise error.RPCException('Boto key: %s does not exist!' % boto_key)
195 shutil.copyfile(boto_key, moblab_host.MOBLAB_BOTO_LOCATION)
196
197
198@rpc_utils.moblab_only
Michael Tang6a34caf2016-10-21 18:27:03 -0700199def set_service_account_credential(service_account_filename):
200 """Update the service account credential file.
201
202 @param service_account_filename: Name of uploaded file through
203 handle_file_upload.
204 """
205 if not os.path.exists(service_account_filename):
206 raise error.RPCException(
207 'Service account file: %s does not exist!' %
208 service_account_filename)
209 shutil.copyfile(
210 service_account_filename,
211 moblab_host.MOBLAB_SERVICE_ACCOUNT_LOCATION)
212
213
214@rpc_utils.moblab_only
xixuanba232a32016-08-25 17:01:59 -0700215def set_launch_control_key(launch_control_key):
216 """Update the launch_control_key file.
217
218 @param launch_control_key: File name of launch_control_key uploaded through
219 handle_file_upload.
220 """
221 if not os.path.exists(launch_control_key):
222 raise error.RPCException('Launch Control key: %s does not exist!' %
223 launch_control_key)
224 shutil.copyfile(launch_control_key,
225 moblab_host.MOBLAB_LAUNCH_CONTROL_KEY_LOCATION)
226 # Restart the devserver service.
227 os.system('sudo restart moblab-devserver-init')
228
229
230###########Moblab Config Wizard RPCs #######################
231def _get_public_ip_address(socket_handle):
232 """Gets the public IP address.
233
234 Connects to Google DNS server using a socket and gets the preferred IP
235 address from the connection.
236
237 @param: socket_handle: a unix socket.
238
239 @return: public ip address as string.
240 """
241 try:
242 socket_handle.settimeout(1)
243 socket_handle.connect(('8.8.8.8', 53))
244 socket_name = socket_handle.getsockname()
245 if socket_name is not None:
246 logging.info('Got socket name from UDP socket.')
247 return socket_name[0]
248 logging.warn('Created UDP socket but with no socket_name.')
249 except socket.error:
250 logging.warn('Could not get socket name from UDP socket.')
251 return None
252
253
254def _get_network_info():
255 """Gets the network information.
256
257 TCP socket is used to test the connectivity. If there is no connectivity, try to
258 get the public IP with UDP socket.
259
260 @return: a tuple as (public_ip_address, connected_to_internet).
261 """
262 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
263 ip = _get_public_ip_address(s)
264 if ip is not None:
265 logging.info('Established TCP connection with well known server.')
266 return (ip, True)
267 s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
268 return (_get_public_ip_address(s), False)
269
270
271@rpc_utils.moblab_only
272def get_network_info():
273 """Returns the server ip addresses, and if the server connectivity.
274
275 The server ip addresses as an array of strings, and the connectivity as a
276 flag.
277 """
278 network_info = {}
279 info = _get_network_info()
280 if info[0] is not None:
281 network_info['server_ips'] = [info[0]]
282 network_info['is_connected'] = info[1]
283
284 return rpc_utils.prepare_for_serialization(network_info)
285
286
287# Gets the boto configuration.
288def _get_boto_config():
289 """Reads the boto configuration from the boto file.
290
291 @return: Boto configuration as ConfigParser object.
292 """
293 boto_config = ConfigParser.ConfigParser()
294 boto_config.read(MOBLAB_BOTO_LOCATION)
295 return boto_config
296
297
298@rpc_utils.moblab_only
299def get_cloud_storage_info():
300 """RPC handler to get the cloud storage access information.
301 """
302 cloud_storage_info = {}
303 value =_CONFIG.get_config_value('CROS', _IMAGE_STORAGE_SERVER)
304 if value is not None:
305 cloud_storage_info[_IMAGE_STORAGE_SERVER] = value
306 value = _CONFIG.get_config_value('CROS', _RESULT_STORAGE_SERVER,
307 default=None)
308 if value is not None:
309 cloud_storage_info[_RESULT_STORAGE_SERVER] = value
310
311 boto_config = _get_boto_config()
312 sections = boto_config.sections()
313
314 if sections:
315 cloud_storage_info[_USE_EXISTING_BOTO_FILE] = True
316 else:
317 cloud_storage_info[_USE_EXISTING_BOTO_FILE] = False
318 if 'Credentials' in sections:
319 options = boto_config.options('Credentials')
320 if _GS_ACCESS_KEY_ID in options:
321 value = boto_config.get('Credentials', _GS_ACCESS_KEY_ID)
322 cloud_storage_info[_GS_ACCESS_KEY_ID] = value
323 if _GS_SECRETE_ACCESS_KEY in options:
324 value = boto_config.get('Credentials', _GS_SECRETE_ACCESS_KEY)
325 cloud_storage_info[_GS_SECRETE_ACCESS_KEY] = value
326
327 return rpc_utils.prepare_for_serialization(cloud_storage_info)
328
329
330def _get_bucket_name_from_url(bucket_url):
331 """Gets the bucket name from a bucket url.
332
333 @param: bucket_url: the bucket url string.
334 """
335 if bucket_url:
336 match = GOOGLE_STORAGE_BUCKET_URL_PATTERN.match(bucket_url)
337 if match:
338 return match.group('bucket')
339 return None
340
341
342def _is_valid_boto_key(key_id, key_secret):
343 """Checks if the boto key is valid.
344
345 @param: key_id: The boto key id string.
346 @param: key_secret: The boto key string.
347
348 @return: A tuple as (valid_boolean, details_string).
349 """
350 if not key_id or not key_secret:
351 return (False, "Empty key id or secret.")
352 conn = boto.connect_gs(key_id, key_secret)
353 try:
354 buckets = conn.get_all_buckets()
355 return (True, None)
356 except boto.exception.GSResponseError:
357 details = "The boto access key is not valid"
358 return (False, details)
359 finally:
360 conn.close()
361
362
363def _is_valid_bucket(key_id, key_secret, bucket_name):
364 """Checks if a bucket is valid and accessible.
365
366 @param: key_id: The boto key id string.
367 @param: key_secret: The boto key string.
368 @param: bucket name string.
369
370 @return: A tuple as (valid_boolean, details_string).
371 """
372 if not key_id or not key_secret or not bucket_name:
373 return (False, "Server error: invalid argument")
374 conn = boto.connect_gs(key_id, key_secret)
375 bucket = conn.lookup(bucket_name)
376 conn.close()
377 if bucket:
378 return (True, None)
379 return (False, "Bucket %s does not exist." % bucket_name)
380
381
382def _is_valid_bucket_url(key_id, key_secret, bucket_url):
383 """Validates the bucket url is accessible.
384
385 @param: key_id: The boto key id string.
386 @param: key_secret: The boto key string.
387 @param: bucket url string.
388
389 @return: A tuple as (valid_boolean, details_string).
390 """
391 bucket_name = _get_bucket_name_from_url(bucket_url)
392 if bucket_name:
393 return _is_valid_bucket(key_id, key_secret, bucket_name)
394 return (False, "Bucket url %s is not valid" % bucket_url)
395
396
397def _validate_cloud_storage_info(cloud_storage_info):
398 """Checks if the cloud storage information is valid.
399
400 @param: cloud_storage_info: The JSON RPC object for cloud storage info.
401
402 @return: A tuple as (valid_boolean, details_string).
403 """
404 valid = True
405 details = None
406 if not cloud_storage_info[_USE_EXISTING_BOTO_FILE]:
407 key_id = cloud_storage_info[_GS_ACCESS_KEY_ID]
408 key_secret = cloud_storage_info[_GS_SECRETE_ACCESS_KEY]
409 valid, details = _is_valid_boto_key(key_id, key_secret)
410
411 if valid:
412 valid, details = _is_valid_bucket_url(
413 key_id, key_secret, cloud_storage_info[_IMAGE_STORAGE_SERVER])
414
415 # allows result bucket to be empty.
416 if valid and cloud_storage_info[_RESULT_STORAGE_SERVER]:
417 valid, details = _is_valid_bucket_url(
418 key_id, key_secret, cloud_storage_info[_RESULT_STORAGE_SERVER])
419 return (valid, details)
420
421
422def _create_operation_status_response(is_ok, details):
423 """Helper method to create a operation status reponse.
424
425 @param: is_ok: Boolean for if the operation is ok.
426 @param: details: A detailed string.
427
428 @return: A serialized JSON RPC object.
429 """
430 status_response = {'status_ok': is_ok}
431 if details:
432 status_response['status_details'] = details
433 return rpc_utils.prepare_for_serialization(status_response)
434
435
436@rpc_utils.moblab_only
437def validate_cloud_storage_info(cloud_storage_info):
438 """RPC handler to check if the cloud storage info is valid.
439
440 @param cloud_storage_info: The JSON RPC object for cloud storage info.
441 """
442 valid, details = _validate_cloud_storage_info(cloud_storage_info)
443 return _create_operation_status_response(valid, details)
444
445
446@rpc_utils.moblab_only
447def submit_wizard_config_info(cloud_storage_info):
448 """RPC handler to submit the cloud storage info.
449
450 @param cloud_storage_info: The JSON RPC object for cloud storage info.
451 """
452 valid, details = _validate_cloud_storage_info(cloud_storage_info)
453 if not valid:
454 return _create_operation_status_response(valid, details)
455 config_update = {}
456 config_update['CROS'] = [
457 (_IMAGE_STORAGE_SERVER, cloud_storage_info[_IMAGE_STORAGE_SERVER]),
458 (_RESULT_STORAGE_SERVER, cloud_storage_info[_RESULT_STORAGE_SERVER])
459 ]
460 _update_partial_config(config_update)
461
462 if not cloud_storage_info[_USE_EXISTING_BOTO_FILE]:
463 boto_config = ConfigParser.RawConfigParser()
464 boto_config.add_section('Credentials')
465 boto_config.set('Credentials', _GS_ACCESS_KEY_ID,
466 cloud_storage_info[_GS_ACCESS_KEY_ID])
467 boto_config.set('Credentials', _GS_SECRETE_ACCESS_KEY,
468 cloud_storage_info[_GS_SECRETE_ACCESS_KEY])
469 _write_config_file(MOBLAB_BOTO_LOCATION, boto_config, True)
470
471 _CONFIG.parse_config_file()
Keith Haddow8345c082016-09-30 11:18:59 -0700472 services = ['moblab-devserver-init', 'moblab-apache-init',
473 'moblab-devserver-cleanup-init', ' moblab-gsoffloader_s-init',
474 'moblab-base-container-init', 'moblab-scheduler-init', 'moblab-gsoffloader-init']
475 cmd = ';/sbin/restart '.join(services)
476 os.system(cmd)
xixuanba232a32016-08-25 17:01:59 -0700477
478 return _create_operation_status_response(True, None)
479
Keith Haddowf1278672016-09-29 12:13:39 -0700480
481@rpc_utils.moblab_only
482def get_version_info():
483 """ RPC handler to get informaiton about the version of the moblab.
Keith Haddow63cc4472016-10-06 16:21:34 -0700484
Keith Haddowf1278672016-09-29 12:13:39 -0700485 @return: A serialized JSON RPC object.
486 """
Keith Haddow63cc4472016-10-06 16:21:34 -0700487 lines = open(_ETC_LSB_RELEASE).readlines()
488 version_response = {
489 x.split('=')[0]: x.split('=')[1] for x in lines if '=' in x}
Keith Haddowf1278672016-09-29 12:13:39 -0700490 return rpc_utils.prepare_for_serialization(version_response)
491
Keith Haddow63cc4472016-10-06 16:21:34 -0700492
493@rpc_utils.moblab_only
494def get_connected_dut_info():
495 """ RPC handler to get informaiton about the DUTs connected to the moblab.
496
497 @return: A serialized JSON RPC object.
498 """
499 # Make a list of the connected DUT's
500 leases = _get_dhcp_dut_leases()
501
502 # Get a list of the AFE configured DUT's
503 hosts = list(rpc_utils.get_host_query((), False, False, True, {}))
504 models.Host.objects.populate_relationships(hosts, models.Label,
505 'label_list')
506 configured_duts = {}
507 for host in hosts:
508 labels = [label.name for label in host.label_list]
509 labels.sort()
510 configured_duts[host.hostname] = ', '.join(labels)
511
512 return rpc_utils.prepare_for_serialization(
513 {'configured_duts': configured_duts,
514 'connected_duts': leases})
515
516
517def _get_dhcp_dut_leases():
518 """ Extract information about connected duts from the dhcp server.
519
520 @return: A dict of ipaddress to mac address for each device connected.
521 """
522 lease_info = open(_DHCPD_LEASES).read()
523
524 leases = {}
525 for lease in lease_info.split('lease'):
526 if lease.find('binding state active;') != -1:
527 ipaddress = lease.split('\n')[0].strip(' {')
528 last_octet = int(ipaddress.split('.')[-1].strip())
529 if last_octet > 150:
530 continue
531 mac_address_search = re.search('hardware ethernet (.*);', lease)
532 if mac_address_search:
533 leases[ipaddress] = mac_address_search.group(1)
534 return leases
535
536
537@rpc_utils.moblab_only
538def add_moblab_dut(ipaddress):
539 """ RPC handler to add a connected DUT to autotest.
540
Michael Tang6a34caf2016-10-21 18:27:03 -0700541 @param ipaddress: IP address of the DUT.
542
Keith Haddow63cc4472016-10-06 16:21:34 -0700543 @return: A string giving information about the status.
544 """
545 cmd = '/usr/local/autotest/cli/atest host create %s &' % ipaddress
546 subprocess.call(cmd, shell=True)
547 return (True, 'DUT %s added to Autotest' % ipaddress)
548
549
550@rpc_utils.moblab_only
551def remove_moblab_dut(ipaddress):
552 """ RPC handler to remove DUT entry from autotest.
553
Michael Tang6a34caf2016-10-21 18:27:03 -0700554 @param ipaddress: IP address of the DUT.
555
Keith Haddow63cc4472016-10-06 16:21:34 -0700556 @return: True if the command succeeds without an exception
557 """
558 models.Host.smart_get(ipaddress).delete()
559 return (True, 'DUT %s deleted from Autotest' % ipaddress)
560
561
562@rpc_utils.moblab_only
563def add_moblab_label(ipaddress, label_name):
564 """ RPC handler to add a label in autotest to a DUT entry.
565
Michael Tang6a34caf2016-10-21 18:27:03 -0700566 @param ipaddress: IP address of the DUT.
567 @param label_name: The label name.
568
Keith Haddow63cc4472016-10-06 16:21:34 -0700569 @return: A string giving information about the status.
570 """
571 # Try to create the label in case it does not already exist.
572 label = None
573 try:
574 label = models.Label.add_object(name=label_name)
575 except:
576 label = models.Label.smart_get(label_name)
577 host_obj = models.Host.smart_get(ipaddress)
578 if label:
579 label.host_set.add(host_obj)
580 return (True, 'Added label %s to DUT %s' % (label_name, ipaddress))
581 return (False, 'Failed to add label %s to DUT %s' % (label_name, ipaddress))
582
583
584@rpc_utils.moblab_only
585def remove_moblab_label(ipaddress, label_name):
586 """ RPC handler to remove a label in autotest from a DUT entry.
587
Michael Tang6a34caf2016-10-21 18:27:03 -0700588 @param ipaddress: IP address of the DUT.
589 @param label_name: The label name.
590
Keith Haddow63cc4472016-10-06 16:21:34 -0700591 @return: A string giving information about the status.
592 """
593 host_obj = models.Host.smart_get(ipaddress)
594 models.Label.smart_get(label_name).host_set.remove(host_obj)
595 return (True, 'Removed label %s from DUT %s' % (label_name, ipaddress))
596
Keith Haddow8ef84172016-10-28 15:38:16 -0700597
598@rpc_utils.moblab_only
599def get_connected_boards():
600 """ RPC handler to get a list of the boards connected to the moblab.
601
602 @return: A de-duped list of board types attached to the moblab.
603 """
604 hosts = list(rpc_utils.get_host_query((), False, False, True, {}))
605 if not hosts:
606 return []
607 models.Host.objects.populate_relationships(hosts, models.Label,
608 'label_list')
609 boards = set()
610 for host in hosts:
611 for label in host.label_list:
612 if 'board:' in label.name:
613 boards.add(label.name.replace('board:', ''))
614 break
615 boards = list(boards)
616 boards.sort()
617 return boards
618
619
620@rpc_utils.moblab_only
621def get_builds_for_board(board_name):
622 """ RPC handler to find the most recent builds for a board.
623
624 @param board_name: The name of a connected board.
625
626 @return: A list no longer than 20 items with the most recent builds.
627 """
628 output = StringIO.StringIO()
629 gs_image_location =_CONFIG.get_config_value('CROS', _IMAGE_STORAGE_SERVER)
630 utils.run('gsutil', args=('ls', gs_image_location + board_name + '-release'), stdout_tee=output)
631 lines = output.getvalue().split('\n')
632 output.close()
633 builds = [line.replace(gs_image_location,'').strip('/ ') for line in lines if line != '']
634 builds.sort()
635 builds.reverse()
636 return builds[:20]
637
638
639@rpc_utils.moblab_only
640def run_suite(board, build, suite, pool=None):
641 """ RPC handler to run a test suite.
642
643 @param board: a board name connected to the moblab.
644 @param build: a build name of a build in the GCS.
645 @param suite: the name of a suite to run
646 @param pool: Optional pool name to run the suite in.
647
648 @return: None
649 """
650 builds = {'cros-version': build}
651 afe = frontend.AFE(user='moblab')
652 afe.run('create_suite_job', board=board, builds=builds, name=suite,
653 pool=pool, run_prod_code=False, test_source_build=build,
654 wait_for_results=False)