blob: f0ec8e30a1c3e8a4b557bf7897223a67265ecb6e [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
Keith Haddow63cc4472016-10-06 16:21:34 -070029from autotest_lib.frontend.afe import models, model_logic, model_attributes
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
197def set_launch_control_key(launch_control_key):
198 """Update the launch_control_key file.
199
200 @param launch_control_key: File name of launch_control_key uploaded through
201 handle_file_upload.
202 """
203 if not os.path.exists(launch_control_key):
204 raise error.RPCException('Launch Control key: %s does not exist!' %
205 launch_control_key)
206 shutil.copyfile(launch_control_key,
207 moblab_host.MOBLAB_LAUNCH_CONTROL_KEY_LOCATION)
208 # Restart the devserver service.
209 os.system('sudo restart moblab-devserver-init')
210
211
212###########Moblab Config Wizard RPCs #######################
213def _get_public_ip_address(socket_handle):
214 """Gets the public IP address.
215
216 Connects to Google DNS server using a socket and gets the preferred IP
217 address from the connection.
218
219 @param: socket_handle: a unix socket.
220
221 @return: public ip address as string.
222 """
223 try:
224 socket_handle.settimeout(1)
225 socket_handle.connect(('8.8.8.8', 53))
226 socket_name = socket_handle.getsockname()
227 if socket_name is not None:
228 logging.info('Got socket name from UDP socket.')
229 return socket_name[0]
230 logging.warn('Created UDP socket but with no socket_name.')
231 except socket.error:
232 logging.warn('Could not get socket name from UDP socket.')
233 return None
234
235
236def _get_network_info():
237 """Gets the network information.
238
239 TCP socket is used to test the connectivity. If there is no connectivity, try to
240 get the public IP with UDP socket.
241
242 @return: a tuple as (public_ip_address, connected_to_internet).
243 """
244 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
245 ip = _get_public_ip_address(s)
246 if ip is not None:
247 logging.info('Established TCP connection with well known server.')
248 return (ip, True)
249 s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
250 return (_get_public_ip_address(s), False)
251
252
253@rpc_utils.moblab_only
254def get_network_info():
255 """Returns the server ip addresses, and if the server connectivity.
256
257 The server ip addresses as an array of strings, and the connectivity as a
258 flag.
259 """
260 network_info = {}
261 info = _get_network_info()
262 if info[0] is not None:
263 network_info['server_ips'] = [info[0]]
264 network_info['is_connected'] = info[1]
265
266 return rpc_utils.prepare_for_serialization(network_info)
267
268
269# Gets the boto configuration.
270def _get_boto_config():
271 """Reads the boto configuration from the boto file.
272
273 @return: Boto configuration as ConfigParser object.
274 """
275 boto_config = ConfigParser.ConfigParser()
276 boto_config.read(MOBLAB_BOTO_LOCATION)
277 return boto_config
278
279
280@rpc_utils.moblab_only
281def get_cloud_storage_info():
282 """RPC handler to get the cloud storage access information.
283 """
284 cloud_storage_info = {}
285 value =_CONFIG.get_config_value('CROS', _IMAGE_STORAGE_SERVER)
286 if value is not None:
287 cloud_storage_info[_IMAGE_STORAGE_SERVER] = value
288 value = _CONFIG.get_config_value('CROS', _RESULT_STORAGE_SERVER,
289 default=None)
290 if value is not None:
291 cloud_storage_info[_RESULT_STORAGE_SERVER] = value
292
293 boto_config = _get_boto_config()
294 sections = boto_config.sections()
295
296 if sections:
297 cloud_storage_info[_USE_EXISTING_BOTO_FILE] = True
298 else:
299 cloud_storage_info[_USE_EXISTING_BOTO_FILE] = False
300 if 'Credentials' in sections:
301 options = boto_config.options('Credentials')
302 if _GS_ACCESS_KEY_ID in options:
303 value = boto_config.get('Credentials', _GS_ACCESS_KEY_ID)
304 cloud_storage_info[_GS_ACCESS_KEY_ID] = value
305 if _GS_SECRETE_ACCESS_KEY in options:
306 value = boto_config.get('Credentials', _GS_SECRETE_ACCESS_KEY)
307 cloud_storage_info[_GS_SECRETE_ACCESS_KEY] = value
308
309 return rpc_utils.prepare_for_serialization(cloud_storage_info)
310
311
312def _get_bucket_name_from_url(bucket_url):
313 """Gets the bucket name from a bucket url.
314
315 @param: bucket_url: the bucket url string.
316 """
317 if bucket_url:
318 match = GOOGLE_STORAGE_BUCKET_URL_PATTERN.match(bucket_url)
319 if match:
320 return match.group('bucket')
321 return None
322
323
324def _is_valid_boto_key(key_id, key_secret):
325 """Checks if the boto key is valid.
326
327 @param: key_id: The boto key id string.
328 @param: key_secret: The boto key string.
329
330 @return: A tuple as (valid_boolean, details_string).
331 """
332 if not key_id or not key_secret:
333 return (False, "Empty key id or secret.")
334 conn = boto.connect_gs(key_id, key_secret)
335 try:
336 buckets = conn.get_all_buckets()
337 return (True, None)
338 except boto.exception.GSResponseError:
339 details = "The boto access key is not valid"
340 return (False, details)
341 finally:
342 conn.close()
343
344
345def _is_valid_bucket(key_id, key_secret, bucket_name):
346 """Checks if a bucket is valid and accessible.
347
348 @param: key_id: The boto key id string.
349 @param: key_secret: The boto key string.
350 @param: bucket name string.
351
352 @return: A tuple as (valid_boolean, details_string).
353 """
354 if not key_id or not key_secret or not bucket_name:
355 return (False, "Server error: invalid argument")
356 conn = boto.connect_gs(key_id, key_secret)
357 bucket = conn.lookup(bucket_name)
358 conn.close()
359 if bucket:
360 return (True, None)
361 return (False, "Bucket %s does not exist." % bucket_name)
362
363
364def _is_valid_bucket_url(key_id, key_secret, bucket_url):
365 """Validates the bucket url is accessible.
366
367 @param: key_id: The boto key id string.
368 @param: key_secret: The boto key string.
369 @param: bucket url string.
370
371 @return: A tuple as (valid_boolean, details_string).
372 """
373 bucket_name = _get_bucket_name_from_url(bucket_url)
374 if bucket_name:
375 return _is_valid_bucket(key_id, key_secret, bucket_name)
376 return (False, "Bucket url %s is not valid" % bucket_url)
377
378
379def _validate_cloud_storage_info(cloud_storage_info):
380 """Checks if the cloud storage information is valid.
381
382 @param: cloud_storage_info: The JSON RPC object for cloud storage info.
383
384 @return: A tuple as (valid_boolean, details_string).
385 """
386 valid = True
387 details = None
388 if not cloud_storage_info[_USE_EXISTING_BOTO_FILE]:
389 key_id = cloud_storage_info[_GS_ACCESS_KEY_ID]
390 key_secret = cloud_storage_info[_GS_SECRETE_ACCESS_KEY]
391 valid, details = _is_valid_boto_key(key_id, key_secret)
392
393 if valid:
394 valid, details = _is_valid_bucket_url(
395 key_id, key_secret, cloud_storage_info[_IMAGE_STORAGE_SERVER])
396
397 # allows result bucket to be empty.
398 if valid and cloud_storage_info[_RESULT_STORAGE_SERVER]:
399 valid, details = _is_valid_bucket_url(
400 key_id, key_secret, cloud_storage_info[_RESULT_STORAGE_SERVER])
401 return (valid, details)
402
403
404def _create_operation_status_response(is_ok, details):
405 """Helper method to create a operation status reponse.
406
407 @param: is_ok: Boolean for if the operation is ok.
408 @param: details: A detailed string.
409
410 @return: A serialized JSON RPC object.
411 """
412 status_response = {'status_ok': is_ok}
413 if details:
414 status_response['status_details'] = details
415 return rpc_utils.prepare_for_serialization(status_response)
416
417
418@rpc_utils.moblab_only
419def validate_cloud_storage_info(cloud_storage_info):
420 """RPC handler to check if the cloud storage info is valid.
421
422 @param cloud_storage_info: The JSON RPC object for cloud storage info.
423 """
424 valid, details = _validate_cloud_storage_info(cloud_storage_info)
425 return _create_operation_status_response(valid, details)
426
427
428@rpc_utils.moblab_only
429def submit_wizard_config_info(cloud_storage_info):
430 """RPC handler to submit the cloud storage info.
431
432 @param cloud_storage_info: The JSON RPC object for cloud storage info.
433 """
434 valid, details = _validate_cloud_storage_info(cloud_storage_info)
435 if not valid:
436 return _create_operation_status_response(valid, details)
437 config_update = {}
438 config_update['CROS'] = [
439 (_IMAGE_STORAGE_SERVER, cloud_storage_info[_IMAGE_STORAGE_SERVER]),
440 (_RESULT_STORAGE_SERVER, cloud_storage_info[_RESULT_STORAGE_SERVER])
441 ]
442 _update_partial_config(config_update)
443
444 if not cloud_storage_info[_USE_EXISTING_BOTO_FILE]:
445 boto_config = ConfigParser.RawConfigParser()
446 boto_config.add_section('Credentials')
447 boto_config.set('Credentials', _GS_ACCESS_KEY_ID,
448 cloud_storage_info[_GS_ACCESS_KEY_ID])
449 boto_config.set('Credentials', _GS_SECRETE_ACCESS_KEY,
450 cloud_storage_info[_GS_SECRETE_ACCESS_KEY])
451 _write_config_file(MOBLAB_BOTO_LOCATION, boto_config, True)
452
453 _CONFIG.parse_config_file()
Keith Haddow8345c082016-09-30 11:18:59 -0700454 services = ['moblab-devserver-init', 'moblab-apache-init',
455 'moblab-devserver-cleanup-init', ' moblab-gsoffloader_s-init',
456 'moblab-base-container-init', 'moblab-scheduler-init', 'moblab-gsoffloader-init']
457 cmd = ';/sbin/restart '.join(services)
458 os.system(cmd)
xixuanba232a32016-08-25 17:01:59 -0700459
460 return _create_operation_status_response(True, None)
461
Keith Haddowf1278672016-09-29 12:13:39 -0700462
463@rpc_utils.moblab_only
464def get_version_info():
465 """ RPC handler to get informaiton about the version of the moblab.
Keith Haddow63cc4472016-10-06 16:21:34 -0700466
Keith Haddowf1278672016-09-29 12:13:39 -0700467 @return: A serialized JSON RPC object.
468 """
Keith Haddow63cc4472016-10-06 16:21:34 -0700469 lines = open(_ETC_LSB_RELEASE).readlines()
470 version_response = {
471 x.split('=')[0]: x.split('=')[1] for x in lines if '=' in x}
Keith Haddowf1278672016-09-29 12:13:39 -0700472 return rpc_utils.prepare_for_serialization(version_response)
473
Keith Haddow63cc4472016-10-06 16:21:34 -0700474
475@rpc_utils.moblab_only
476def get_connected_dut_info():
477 """ RPC handler to get informaiton about the DUTs connected to the moblab.
478
479 @return: A serialized JSON RPC object.
480 """
481 # Make a list of the connected DUT's
482 leases = _get_dhcp_dut_leases()
483
484 # Get a list of the AFE configured DUT's
485 hosts = list(rpc_utils.get_host_query((), False, False, True, {}))
486 models.Host.objects.populate_relationships(hosts, models.Label,
487 'label_list')
488 configured_duts = {}
489 for host in hosts:
490 labels = [label.name for label in host.label_list]
491 labels.sort()
492 configured_duts[host.hostname] = ', '.join(labels)
493
494 return rpc_utils.prepare_for_serialization(
495 {'configured_duts': configured_duts,
496 'connected_duts': leases})
497
498
499def _get_dhcp_dut_leases():
500 """ Extract information about connected duts from the dhcp server.
501
502 @return: A dict of ipaddress to mac address for each device connected.
503 """
504 lease_info = open(_DHCPD_LEASES).read()
505
506 leases = {}
507 for lease in lease_info.split('lease'):
508 if lease.find('binding state active;') != -1:
509 ipaddress = lease.split('\n')[0].strip(' {')
510 last_octet = int(ipaddress.split('.')[-1].strip())
511 if last_octet > 150:
512 continue
513 mac_address_search = re.search('hardware ethernet (.*);', lease)
514 if mac_address_search:
515 leases[ipaddress] = mac_address_search.group(1)
516 return leases
517
518
519@rpc_utils.moblab_only
520def add_moblab_dut(ipaddress):
521 """ RPC handler to add a connected DUT to autotest.
522
523 @return: A string giving information about the status.
524 """
525 cmd = '/usr/local/autotest/cli/atest host create %s &' % ipaddress
526 subprocess.call(cmd, shell=True)
527 return (True, 'DUT %s added to Autotest' % ipaddress)
528
529
530@rpc_utils.moblab_only
531def remove_moblab_dut(ipaddress):
532 """ RPC handler to remove DUT entry from autotest.
533
534 @return: True if the command succeeds without an exception
535 """
536 models.Host.smart_get(ipaddress).delete()
537 return (True, 'DUT %s deleted from Autotest' % ipaddress)
538
539
540@rpc_utils.moblab_only
541def add_moblab_label(ipaddress, label_name):
542 """ RPC handler to add a label in autotest to a DUT entry.
543
544 @return: A string giving information about the status.
545 """
546 # Try to create the label in case it does not already exist.
547 label = None
548 try:
549 label = models.Label.add_object(name=label_name)
550 except:
551 label = models.Label.smart_get(label_name)
552 host_obj = models.Host.smart_get(ipaddress)
553 if label:
554 label.host_set.add(host_obj)
555 return (True, 'Added label %s to DUT %s' % (label_name, ipaddress))
556 return (False, 'Failed to add label %s to DUT %s' % (label_name, ipaddress))
557
558
559@rpc_utils.moblab_only
560def remove_moblab_label(ipaddress, label_name):
561 """ RPC handler to remove a label in autotest from a DUT entry.
562
563 @return: A string giving information about the status.
564 """
565 host_obj = models.Host.smart_get(ipaddress)
566 models.Label.smart_get(label_name).host_set.remove(host_obj)
567 return (True, 'Removed label %s from DUT %s' % (label_name, ipaddress))
568