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