blob: 22e6fef57063fc2535f32ff382c04956335a8632 [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
21import shutil
22import socket
23import re
24
25import common
26
27from autotest_lib.client.common_lib import error
28from autotest_lib.client.common_lib import global_config
29from autotest_lib.frontend.afe import rpc_utils
30from autotest_lib.server.hosts import moblab_host
31
32_CONFIG = global_config.global_config
33MOBLAB_BOTO_LOCATION = '/home/moblab/.boto'
34
35# Google Cloud Storage bucket url regex pattern. The pattern is used to extract
36# the bucket name from the bucket URL. For example, "gs://image_bucket/google"
37# should result in a bucket name "image_bucket".
38GOOGLE_STORAGE_BUCKET_URL_PATTERN = re.compile(
39 r'gs://(?P<bucket>[a-zA-Z][a-zA-Z0-9-_]*)/?.*')
40
41# Contants used in Json RPC field names.
42_IMAGE_STORAGE_SERVER = 'image_storage_server'
43_GS_ACCESS_KEY_ID = 'gs_access_key_id'
44_GS_SECRETE_ACCESS_KEY = 'gs_secret_access_key'
45_RESULT_STORAGE_SERVER = 'results_storage_server'
46_USE_EXISTING_BOTO_FILE = 'use_existing_boto_file'
47
48
49@rpc_utils.moblab_only
50def get_config_values():
51 """Returns all config values parsed from global and shadow configs.
52
53 Config values are grouped by sections, and each section is composed of
54 a list of name value pairs.
55 """
56 sections =_CONFIG.get_sections()
57 config_values = {}
58 for section in sections:
59 config_values[section] = _CONFIG.config.items(section)
60 return rpc_utils.prepare_for_serialization(config_values)
61
62
63def _write_config_file(config_file, config_values, overwrite=False):
64 """Writes out a configuration file.
65
66 @param config_file: The name of the configuration file.
67 @param config_values: The ConfigParser object.
68 @param ovewrite: Flag on if overwriting is allowed.
69 """
70 if not config_file:
71 raise error.RPCException('Empty config file name.')
72 if not overwrite and os.path.exists(config_file):
73 raise error.RPCException('Config file already exists.')
74
75 if config_values:
76 with open(config_file, 'w') as config_file:
77 config_values.write(config_file)
78
79
80def _read_original_config():
81 """Reads the orginal configuratino without shadow.
82
83 @return: A configuration object, see global_config_class.
84 """
85 original_config = global_config.global_config_class()
86 original_config.set_config_files(shadow_file='')
87 return original_config
88
89
90def _read_raw_config(config_file):
91 """Reads the raw configuration from a configuration file.
92
93 @param: config_file: The path of the configuration file.
94
95 @return: A ConfigParser object.
96 """
97 shadow_config = ConfigParser.RawConfigParser()
98 shadow_config.read(config_file)
99 return shadow_config
100
101
102def _get_shadow_config_from_partial_update(config_values):
103 """Finds out the new shadow configuration based on a partial update.
104
105 Since the input is only a partial config, we should not lose the config
106 data inside the existing shadow config file. We also need to distinguish
107 if the input config info overrides with a new value or reverts back to
108 an original value.
109
110 @param config_values: See get_moblab_settings().
111
112 @return: The new shadow configuration as ConfigParser object.
113 """
114 original_config = _read_original_config()
115 existing_shadow = _read_raw_config(_CONFIG.shadow_file)
116 for section, config_value_list in config_values.iteritems():
117 for key, value in config_value_list:
118 if original_config.get_config_value(section, key,
119 default='',
120 allow_blank=True) != value:
121 if not existing_shadow.has_section(section):
122 existing_shadow.add_section(section)
123 existing_shadow.set(section, key, value)
124 elif existing_shadow.has_option(section, key):
125 existing_shadow.remove_option(section, key)
126 return existing_shadow
127
128
129def _update_partial_config(config_values):
130 """Updates the shadow configuration file with a partial config udpate.
131
132 @param config_values: See get_moblab_settings().
133 """
134 existing_config = _get_shadow_config_from_partial_update(config_values)
135 _write_config_file(_CONFIG.shadow_file, existing_config, True)
136
137
138@rpc_utils.moblab_only
139def update_config_handler(config_values):
140 """Update config values and override shadow config.
141
142 @param config_values: See get_moblab_settings().
143 """
144 original_config = _read_original_config()
145 new_shadow = ConfigParser.RawConfigParser()
146 for section, config_value_list in config_values.iteritems():
147 for key, value in config_value_list:
148 if original_config.get_config_value(section, key,
149 default='',
150 allow_blank=True) != value:
151 if not new_shadow.has_section(section):
152 new_shadow.add_section(section)
153 new_shadow.set(section, key, value)
154
155 if not _CONFIG.shadow_file or not os.path.exists(_CONFIG.shadow_file):
156 raise error.RPCException('Shadow config file does not exist.')
157 _write_config_file(_CONFIG.shadow_file, new_shadow, True)
158
159 # TODO (sbasi) crbug.com/403916 - Remove the reboot command and
160 # instead restart the services that rely on the config values.
161 os.system('sudo reboot')
162
163
164@rpc_utils.moblab_only
165def reset_config_settings():
166 """Reset moblab shadow config."""
167 with open(_CONFIG.shadow_file, 'w') as config_file:
168 pass
169 os.system('sudo reboot')
170
171
172@rpc_utils.moblab_only
173def reboot_moblab():
174 """Simply reboot the device."""
175 os.system('sudo reboot')
176
177
178@rpc_utils.moblab_only
179def set_boto_key(boto_key):
180 """Update the boto_key file.
181
182 @param boto_key: File name of boto_key uploaded through handle_file_upload.
183 """
184 if not os.path.exists(boto_key):
185 raise error.RPCException('Boto key: %s does not exist!' % boto_key)
186 shutil.copyfile(boto_key, moblab_host.MOBLAB_BOTO_LOCATION)
187
188
189@rpc_utils.moblab_only
190def set_launch_control_key(launch_control_key):
191 """Update the launch_control_key file.
192
193 @param launch_control_key: File name of launch_control_key uploaded through
194 handle_file_upload.
195 """
196 if not os.path.exists(launch_control_key):
197 raise error.RPCException('Launch Control key: %s does not exist!' %
198 launch_control_key)
199 shutil.copyfile(launch_control_key,
200 moblab_host.MOBLAB_LAUNCH_CONTROL_KEY_LOCATION)
201 # Restart the devserver service.
202 os.system('sudo restart moblab-devserver-init')
203
204
205###########Moblab Config Wizard RPCs #######################
206def _get_public_ip_address(socket_handle):
207 """Gets the public IP address.
208
209 Connects to Google DNS server using a socket and gets the preferred IP
210 address from the connection.
211
212 @param: socket_handle: a unix socket.
213
214 @return: public ip address as string.
215 """
216 try:
217 socket_handle.settimeout(1)
218 socket_handle.connect(('8.8.8.8', 53))
219 socket_name = socket_handle.getsockname()
220 if socket_name is not None:
221 logging.info('Got socket name from UDP socket.')
222 return socket_name[0]
223 logging.warn('Created UDP socket but with no socket_name.')
224 except socket.error:
225 logging.warn('Could not get socket name from UDP socket.')
226 return None
227
228
229def _get_network_info():
230 """Gets the network information.
231
232 TCP socket is used to test the connectivity. If there is no connectivity, try to
233 get the public IP with UDP socket.
234
235 @return: a tuple as (public_ip_address, connected_to_internet).
236 """
237 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
238 ip = _get_public_ip_address(s)
239 if ip is not None:
240 logging.info('Established TCP connection with well known server.')
241 return (ip, True)
242 s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
243 return (_get_public_ip_address(s), False)
244
245
246@rpc_utils.moblab_only
247def get_network_info():
248 """Returns the server ip addresses, and if the server connectivity.
249
250 The server ip addresses as an array of strings, and the connectivity as a
251 flag.
252 """
253 network_info = {}
254 info = _get_network_info()
255 if info[0] is not None:
256 network_info['server_ips'] = [info[0]]
257 network_info['is_connected'] = info[1]
258
259 return rpc_utils.prepare_for_serialization(network_info)
260
261
262# Gets the boto configuration.
263def _get_boto_config():
264 """Reads the boto configuration from the boto file.
265
266 @return: Boto configuration as ConfigParser object.
267 """
268 boto_config = ConfigParser.ConfigParser()
269 boto_config.read(MOBLAB_BOTO_LOCATION)
270 return boto_config
271
272
273@rpc_utils.moblab_only
274def get_cloud_storage_info():
275 """RPC handler to get the cloud storage access information.
276 """
277 cloud_storage_info = {}
278 value =_CONFIG.get_config_value('CROS', _IMAGE_STORAGE_SERVER)
279 if value is not None:
280 cloud_storage_info[_IMAGE_STORAGE_SERVER] = value
281 value = _CONFIG.get_config_value('CROS', _RESULT_STORAGE_SERVER,
282 default=None)
283 if value is not None:
284 cloud_storage_info[_RESULT_STORAGE_SERVER] = value
285
286 boto_config = _get_boto_config()
287 sections = boto_config.sections()
288
289 if sections:
290 cloud_storage_info[_USE_EXISTING_BOTO_FILE] = True
291 else:
292 cloud_storage_info[_USE_EXISTING_BOTO_FILE] = False
293 if 'Credentials' in sections:
294 options = boto_config.options('Credentials')
295 if _GS_ACCESS_KEY_ID in options:
296 value = boto_config.get('Credentials', _GS_ACCESS_KEY_ID)
297 cloud_storage_info[_GS_ACCESS_KEY_ID] = value
298 if _GS_SECRETE_ACCESS_KEY in options:
299 value = boto_config.get('Credentials', _GS_SECRETE_ACCESS_KEY)
300 cloud_storage_info[_GS_SECRETE_ACCESS_KEY] = value
301
302 return rpc_utils.prepare_for_serialization(cloud_storage_info)
303
304
305def _get_bucket_name_from_url(bucket_url):
306 """Gets the bucket name from a bucket url.
307
308 @param: bucket_url: the bucket url string.
309 """
310 if bucket_url:
311 match = GOOGLE_STORAGE_BUCKET_URL_PATTERN.match(bucket_url)
312 if match:
313 return match.group('bucket')
314 return None
315
316
317def _is_valid_boto_key(key_id, key_secret):
318 """Checks if the boto key is valid.
319
320 @param: key_id: The boto key id string.
321 @param: key_secret: The boto key string.
322
323 @return: A tuple as (valid_boolean, details_string).
324 """
325 if not key_id or not key_secret:
326 return (False, "Empty key id or secret.")
327 conn = boto.connect_gs(key_id, key_secret)
328 try:
329 buckets = conn.get_all_buckets()
330 return (True, None)
331 except boto.exception.GSResponseError:
332 details = "The boto access key is not valid"
333 return (False, details)
334 finally:
335 conn.close()
336
337
338def _is_valid_bucket(key_id, key_secret, bucket_name):
339 """Checks if a bucket is valid and accessible.
340
341 @param: key_id: The boto key id string.
342 @param: key_secret: The boto key string.
343 @param: bucket name string.
344
345 @return: A tuple as (valid_boolean, details_string).
346 """
347 if not key_id or not key_secret or not bucket_name:
348 return (False, "Server error: invalid argument")
349 conn = boto.connect_gs(key_id, key_secret)
350 bucket = conn.lookup(bucket_name)
351 conn.close()
352 if bucket:
353 return (True, None)
354 return (False, "Bucket %s does not exist." % bucket_name)
355
356
357def _is_valid_bucket_url(key_id, key_secret, bucket_url):
358 """Validates the bucket url is accessible.
359
360 @param: key_id: The boto key id string.
361 @param: key_secret: The boto key string.
362 @param: bucket url string.
363
364 @return: A tuple as (valid_boolean, details_string).
365 """
366 bucket_name = _get_bucket_name_from_url(bucket_url)
367 if bucket_name:
368 return _is_valid_bucket(key_id, key_secret, bucket_name)
369 return (False, "Bucket url %s is not valid" % bucket_url)
370
371
372def _validate_cloud_storage_info(cloud_storage_info):
373 """Checks if the cloud storage information is valid.
374
375 @param: cloud_storage_info: The JSON RPC object for cloud storage info.
376
377 @return: A tuple as (valid_boolean, details_string).
378 """
379 valid = True
380 details = None
381 if not cloud_storage_info[_USE_EXISTING_BOTO_FILE]:
382 key_id = cloud_storage_info[_GS_ACCESS_KEY_ID]
383 key_secret = cloud_storage_info[_GS_SECRETE_ACCESS_KEY]
384 valid, details = _is_valid_boto_key(key_id, key_secret)
385
386 if valid:
387 valid, details = _is_valid_bucket_url(
388 key_id, key_secret, cloud_storage_info[_IMAGE_STORAGE_SERVER])
389
390 # allows result bucket to be empty.
391 if valid and cloud_storage_info[_RESULT_STORAGE_SERVER]:
392 valid, details = _is_valid_bucket_url(
393 key_id, key_secret, cloud_storage_info[_RESULT_STORAGE_SERVER])
394 return (valid, details)
395
396
397def _create_operation_status_response(is_ok, details):
398 """Helper method to create a operation status reponse.
399
400 @param: is_ok: Boolean for if the operation is ok.
401 @param: details: A detailed string.
402
403 @return: A serialized JSON RPC object.
404 """
405 status_response = {'status_ok': is_ok}
406 if details:
407 status_response['status_details'] = details
408 return rpc_utils.prepare_for_serialization(status_response)
409
410
411@rpc_utils.moblab_only
412def validate_cloud_storage_info(cloud_storage_info):
413 """RPC handler to check if the cloud storage info is valid.
414
415 @param cloud_storage_info: The JSON RPC object for cloud storage info.
416 """
417 valid, details = _validate_cloud_storage_info(cloud_storage_info)
418 return _create_operation_status_response(valid, details)
419
420
421@rpc_utils.moblab_only
422def submit_wizard_config_info(cloud_storage_info):
423 """RPC handler to submit the cloud storage info.
424
425 @param cloud_storage_info: The JSON RPC object for cloud storage info.
426 """
427 valid, details = _validate_cloud_storage_info(cloud_storage_info)
428 if not valid:
429 return _create_operation_status_response(valid, details)
430 config_update = {}
431 config_update['CROS'] = [
432 (_IMAGE_STORAGE_SERVER, cloud_storage_info[_IMAGE_STORAGE_SERVER]),
433 (_RESULT_STORAGE_SERVER, cloud_storage_info[_RESULT_STORAGE_SERVER])
434 ]
435 _update_partial_config(config_update)
436
437 if not cloud_storage_info[_USE_EXISTING_BOTO_FILE]:
438 boto_config = ConfigParser.RawConfigParser()
439 boto_config.add_section('Credentials')
440 boto_config.set('Credentials', _GS_ACCESS_KEY_ID,
441 cloud_storage_info[_GS_ACCESS_KEY_ID])
442 boto_config.set('Credentials', _GS_SECRETE_ACCESS_KEY,
443 cloud_storage_info[_GS_SECRETE_ACCESS_KEY])
444 _write_config_file(MOBLAB_BOTO_LOCATION, boto_config, True)
445
446 _CONFIG.parse_config_file()
Keith Haddow8345c082016-09-30 11:18:59 -0700447 services = ['moblab-devserver-init', 'moblab-apache-init',
448 'moblab-devserver-cleanup-init', ' moblab-gsoffloader_s-init',
449 'moblab-base-container-init', 'moblab-scheduler-init', 'moblab-gsoffloader-init']
450 cmd = ';/sbin/restart '.join(services)
451 os.system(cmd)
xixuanba232a32016-08-25 17:01:59 -0700452
453 return _create_operation_status_response(True, None)
454
Keith Haddowf1278672016-09-29 12:13:39 -0700455
456@rpc_utils.moblab_only
457def get_version_info():
458 """ RPC handler to get informaiton about the version of the moblab.
459 @return: A serialized JSON RPC object.
460 """
461 lines = open('/etc/lsb-release').readlines()
462 lines.remove('')
463 version_response = {x.split('=')[0]: x.split('=')[1] for x in lines}
464 return rpc_utils.prepare_for_serialization(version_response)
465