blob: 21aa137c6e344377aca6040eb1f6969b85e46242 [file] [log] [blame]
Keun Soo Yimb293fdb2016-09-21 16:03:44 -07001#!/usr/bin/env python
2#
3# Copyright 2016 - The Android Open Source Project
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070016"""A client that manages Android compute engine instances.
17
18** AndroidComputeClient **
19
20AndroidComputeClient derives from ComputeClient. It manges a google
21compute engine project that is setup for running Android instances.
22It knows how to create android GCE images and instances.
23
24** Class hierarchy **
25
26 base_cloud_client.BaseCloudApiClient
27 ^
28 |
29 gcompute_client.ComputeClient
30 ^
31 |
32 gcompute_client.AndroidComputeClient
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070033"""
34
Fang Dengfed6a6f2017-03-01 18:27:28 -080035import getpass
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070036import logging
37import os
38import uuid
39
40from acloud.internal.lib import gcompute_client
41from acloud.internal.lib import utils
42from acloud.public import errors
43
44logger = logging.getLogger(__name__)
45
46
47class AndroidComputeClient(gcompute_client.ComputeClient):
48 """Client that manages Anadroid Virtual Device."""
Fang Deng93ef3592017-02-08 14:02:47 -080049 INSTANCE_NAME_FMT = "ins-{uuid}-{build_id}-{build_target}"
50 IMAGE_NAME_FMT = "img-{uuid}-{build_id}-{build_target}"
51 DATA_DISK_NAME_FMT = "data-{instance}"
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070052 BOOT_COMPLETED_MSG = "VIRTUAL_DEVICE_BOOT_COMPLETED"
Kevin Chengb5963882018-05-09 00:06:27 -070053 BOOT_STARTED_MSG = "VIRTUAL_DEVICE_BOOT_STARTED"
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070054 BOOT_TIMEOUT_SECS = 5 * 60 # 5 mins, usually it should take ~2 mins
55 BOOT_CHECK_INTERVAL_SECS = 10
Kevin Chengb5963882018-05-09 00:06:27 -070056
57 OPERATION_TIMEOUT_SECS = 20 * 60 # Override parent value, 20 mins
58
Fang Deng93ef3592017-02-08 14:02:47 -080059 NAME_LENGTH_LIMIT = 63
60 # If the generated name ends with '-', replace it with REPLACER.
61 REPLACER = "e"
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070062
63 def __init__(self, acloud_config, oauth2_credentials):
64 """Initialize.
65
66 Args:
67 acloud_config: An AcloudConfig object.
68 oauth2_credentials: An oauth2client.OAuth2Credentials instance.
69 """
70 super(AndroidComputeClient, self).__init__(acloud_config,
71 oauth2_credentials)
72 self._zone = acloud_config.zone
73 self._machine_type = acloud_config.machine_type
74 self._min_machine_size = acloud_config.min_machine_size
75 self._network = acloud_config.network
76 self._orientation = acloud_config.orientation
77 self._resolution = acloud_config.resolution
78 self._metadata = acloud_config.metadata_variable.copy()
Fang Dengfed6a6f2017-03-01 18:27:28 -080079 self._ssh_public_key_path = acloud_config.ssh_public_key_path
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070080
Fang Deng93ef3592017-02-08 14:02:47 -080081 @classmethod
82 def _FormalizeName(cls, name):
83 """Formalize the name to comply with RFC1035.
84
85 The name must be 1-63 characters long and match the regular expression
86 [a-z]([-a-z0-9]*[a-z0-9])? which means the first character must be a
87 lowercase letter, and all following characters must be a dash,
88 lowercase letter, or digit, except the last character, which cannot be
89 a dash.
90
91 Args:
92 name: A string.
93
94 Returns:
95 name: A string that complies with RFC1035.
96 """
97 name = name.replace("_", "-").lower()
98 name = name[:cls.NAME_LENGTH_LIMIT]
99 if name[-1] == "-":
herbertxue308f7662018-05-18 03:25:58 +0000100 name = name[:-1] + cls.REPLACER
Fang Deng93ef3592017-02-08 14:02:47 -0800101 return name
102
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700103 def _CheckMachineSize(self):
104 """Check machine size.
105
106 Check if the desired machine type |self._machine_type| meets
107 the requirement of minimum machine size specified as
108 |self._min_machine_size|.
109
110 Raises:
111 errors.DriverError: if check fails.
112 """
113 if self.CompareMachineSize(self._machine_type, self._min_machine_size,
114 self._zone) < 0:
115 raise errors.DriverError(
116 "%s does not meet the minimum required machine size %s" %
117 (self._machine_type, self._min_machine_size))
118
119 @classmethod
120 def GenerateImageName(cls, build_target=None, build_id=None):
121 """Generate an image name given build_target, build_id.
122
123 Args:
Kevin Chengb5963882018-05-09 00:06:27 -0700124 build_target: Target name, e.g. "aosp_cf_x86_phone-userdebug"
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700125 build_id: Build id, a string, e.g. "2263051", "P2804227"
126
127 Returns:
128 A string, representing image name.
129 """
130 if not build_target and not build_id:
131 return "image-" + uuid.uuid4().hex
herbertxue308f7662018-05-18 03:25:58 +0000132 name = cls.IMAGE_NAME_FMT.format(
133 build_target=build_target,
134 build_id=build_id,
135 uuid=uuid.uuid4().hex[:8])
Fang Deng93ef3592017-02-08 14:02:47 -0800136 return cls._FormalizeName(name)
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700137
138 @classmethod
139 def GetDataDiskName(cls, instance):
140 """Get data disk name for an instance.
141
142 Args:
143 instance: An instance_name.
144
145 Returns:
146 The corresponding data disk name.
147 """
Fang Deng93ef3592017-02-08 14:02:47 -0800148 name = cls.DATA_DISK_NAME_FMT.format(instance=instance)
149 return cls._FormalizeName(name)
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700150
151 @classmethod
152 def GenerateInstanceName(cls, build_target=None, build_id=None):
153 """Generate an instance name given build_target, build_id.
154
155 Target is not used as instance name has a length limit.
156
157 Args:
Kevin Chengb5963882018-05-09 00:06:27 -0700158 build_target: Target name, e.g. "aosp_cf_x86_phone-userdebug"
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700159 build_id: Build id, a string, e.g. "2263051", "P2804227"
160
161 Returns:
162 A string, representing instance name.
163 """
164 if not build_target and not build_id:
165 return "instance-" + uuid.uuid4().hex
Fang Deng93ef3592017-02-08 14:02:47 -0800166 name = cls.INSTANCE_NAME_FMT.format(
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700167 build_target=build_target,
168 build_id=build_id,
Kevin Chengb5963882018-05-09 00:06:27 -0700169 uuid=uuid.uuid4().hex[:8]).replace("_", "-")
Fang Deng93ef3592017-02-08 14:02:47 -0800170 return cls._FormalizeName(name)
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700171
herbertxue308f7662018-05-18 03:25:58 +0000172 def CreateDisk(self,
173 disk_name,
174 source_image,
175 size_gb,
176 zone=None,
177 source_project=None,
178 disk_type=gcompute_client.PersistentDiskType.STANDARD):
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700179 """Create a gce disk.
180
181 Args:
herbertxue308f7662018-05-18 03:25:58 +0000182 disk_name: String, name of disk.
183 source_image: String, name to the image name.
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700184 size_gb: Integer, size in gigabytes.
herbertxue308f7662018-05-18 03:25:58 +0000185 zone: String, name of the zone, e.g. us-central1-b.
186 source_project: String, required if the image is located in a different
187 project.
188 disk_type: String, a value from PersistentDiskType, STANDARD
189 for regular hard disk or SSD for solid state disk.
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700190 """
191 if self.CheckDiskExists(disk_name, self._zone):
192 raise errors.DriverError(
193 "Failed to create disk %s, already exists." % disk_name)
194 if source_image and not self.CheckImageExists(source_image):
195 raise errors.DriverError(
196 "Failed to create disk %s, source image %s does not exist." %
197 (disk_name, source_image))
herbertxue308f7662018-05-18 03:25:58 +0000198 super(AndroidComputeClient, self).CreateDisk(
199 disk_name,
200 source_image=source_image,
201 size_gb=size_gb,
202 zone=zone or self._zone)
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700203
Fang Dengfed6a6f2017-03-01 18:27:28 -0800204 @staticmethod
205 def _LoadSshPublicKey(ssh_public_key_path):
206 """Load the content of ssh public key from a file.
207
208 Args:
209 ssh_public_key_path: String, path to the public key file.
210 E.g. ~/.ssh/acloud_rsa.pub
211 Returns:
212 String, content of the file.
213
214 Raises:
215 errors.DriverError if the public key file does not exist
216 or the content is not valid.
217 """
218 key_path = os.path.expanduser(ssh_public_key_path)
219 if not os.path.exists(key_path):
220 raise errors.DriverError(
221 "SSH public key file %s does not exist." % key_path)
222
223 with open(key_path) as f:
224 rsa = f.read()
225 rsa = rsa.strip() if rsa else rsa
226 utils.VerifyRsaPubKey(rsa)
227 return rsa
228
herbertxue308f7662018-05-18 03:25:58 +0000229 # pylint: disable=too-many-locals
230 def CreateInstance(self,
231 instance,
232 image_name,
233 machine_type=None,
234 metadata=None,
235 network=None,
236 zone=None,
237 disk_args=None,
238 image_project=None,
239 gpu=None,
240 extra_disk_name=None):
241 """Create a gce instance with a gce image.
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700242 Args:
herbertxue308f7662018-05-18 03:25:58 +0000243 instance: String, instance name.
244 image_name: String, source image used to create this disk.
245 machine_type: String, representing machine_type,
246 e.g. "n1-standard-1"
247 metadata: Dict, maps a metadata name to its value.
248 network: String, representing network name, e.g. "default"
249 zone: String, representing zone name, e.g. "us-central1-f"
250 disk_args: A list of extra disk args (strings), see _GetDiskArgs
251 for example, if None, will create a disk using the given
252 image.
253 image_project: String, name of the project where the image
254 belongs. Assume the default project if None.
255 gpu: String, type of gpu to attach. e.g. "nvidia-tesla-k80", if
256 None no gpus will be attached. For more details see:
257 https://cloud.google.com/compute/docs/gpus/add-gpus
258 extra_disk_name: String,the name of the extra disk to attach.
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700259 """
260 self._CheckMachineSize()
261 disk_args = self._GetDiskArgs(instance, image_name)
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700262 metadata = self._metadata.copy()
263 metadata["cfg_sta_display_resolution"] = self._resolution
264 metadata["t_force_orientation"] = self._orientation
Fang Dengfed6a6f2017-03-01 18:27:28 -0800265
266 # Add per-instance ssh key
267 if self._ssh_public_key_path:
268 rsa = self._LoadSshPublicKey(self._ssh_public_key_path)
herbertxue308f7662018-05-18 03:25:58 +0000269 logger.info(
270 "ssh_public_key_path is specified in config: %s, "
271 "will add the key to the instance.", self._ssh_public_key_path)
Fang Dengfed6a6f2017-03-01 18:27:28 -0800272 metadata["sshKeys"] = "%s:%s" % (getpass.getuser(), rsa)
273 else:
herbertxue308f7662018-05-18 03:25:58 +0000274 logger.warning("ssh_public_key_path is not specified in config, "
275 "only project-wide key will be effective.")
Fang Dengfed6a6f2017-03-01 18:27:28 -0800276
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700277 super(AndroidComputeClient, self).CreateInstance(
278 instance, image_name, self._machine_type, metadata, self._network,
herbertxue308f7662018-05-18 03:25:58 +0000279 self._zone, disk_args, image_project, gpu, extra_disk_name)
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700280
Kevin Chengb5963882018-05-09 00:06:27 -0700281 def CheckBootFailure(self, serial_out, instance):
282 """Determine if serial output has indicated any boot failure.
283
284 Subclass has to define this function to detect failures
285 in the boot process
286
287 Args:
288 serial_out: string
289 instance: string, instance name.
290
291 Raises:
292 Raises errors.DeviceBootError exception if a failure is detected.
293 """
294 pass
295
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700296 def CheckBoot(self, instance):
297 """Check once to see if boot completes.
298
299 Args:
300 instance: string, instance name.
301
302 Returns:
Kevin Chengb5963882018-05-09 00:06:27 -0700303 True if the BOOT_COMPLETED_MSG or BOOT_STARTED_MSG appears in serial
304 port output, otherwise False.
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700305 """
306 try:
Kevin Chengb5963882018-05-09 00:06:27 -0700307 serial_out = self.GetSerialPortOutput(instance=instance, port=1)
308 self.CheckBootFailure(serial_out, instance)
309 return ((self.BOOT_COMPLETED_MSG in serial_out)
310 or (self.BOOT_STARTED_MSG in serial_out))
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700311 except errors.HttpError as e:
312 if e.code == 400:
herbertxue308f7662018-05-18 03:25:58 +0000313 logger.debug("CheckBoot: Instance is not ready yet %s", str(e))
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700314 return False
315 raise
316
317 def WaitForBoot(self, instance):
318 """Wait for boot to completes or hit timeout.
319
320 Args:
321 instance: string, instance name.
322 """
323 logger.info("Waiting for instance to boot up: %s", instance)
324 timeout_exception = errors.DeviceBootTimeoutError(
325 "Device %s did not finish on boot within timeout (%s secs)" %
326 (instance, self.BOOT_TIMEOUT_SECS)),
herbertxue308f7662018-05-18 03:25:58 +0000327 utils.PollAndWait(
328 func=self.CheckBoot,
329 expected_return=True,
330 timeout_exception=timeout_exception,
331 timeout_secs=self.BOOT_TIMEOUT_SECS,
332 sleep_interval_secs=self.BOOT_CHECK_INTERVAL_SECS,
333 instance=instance)
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700334 logger.info("Instance boot completed: %s", instance)
335
herbertxue308f7662018-05-18 03:25:58 +0000336 def GetInstanceIP(self, instance, zone=None):
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700337 """Get Instance IP given instance name.
338
339 Args:
340 instance: String, representing instance name.
herbertxue308f7662018-05-18 03:25:58 +0000341 zone: String, representing zone name, e.g. "us-central1-f"
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700342
343 Returns:
344 string, IP of the instance.
345 """
herbertxue308f7662018-05-18 03:25:58 +0000346 return super(AndroidComputeClient, self).GetInstanceIP(
347 instance, zone or self._zone)
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700348
herbertxue308f7662018-05-18 03:25:58 +0000349 def GetSerialPortOutput(self, instance, zone=None, port=1):
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700350 """Get serial port output.
351
352 Args:
353 instance: string, instance name.
herbertxue308f7662018-05-18 03:25:58 +0000354 zone: String, representing zone name, e.g. "us-central1-f"
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700355 port: int, which COM port to read from, 1-4, default to 1.
356
357 Returns:
358 String, contents of the output.
359
360 Raises:
361 errors.DriverError: For malformed response.
362 """
363 return super(AndroidComputeClient, self).GetSerialPortOutput(
herbertxue308f7662018-05-18 03:25:58 +0000364 instance, zone or self._zone, port)
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700365
herbertxue308f7662018-05-18 03:25:58 +0000366 def GetInstanceNamesByIPs(self, ips, zone=None):
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700367 """Get Instance names by IPs.
368
369 This function will go through all instances, which
370 could be slow if there are too many instances. However, currently
371 GCE doesn't support search for instance by IP.
372
373 Args:
374 ips: A set of IPs.
herbertxue308f7662018-05-18 03:25:58 +0000375 zone: String, representing zone name, e.g. "us-central1-f"
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700376
377 Returns:
378 A dictionary where key is ip and value is instance name or None
379 if instance is not found for the given IP.
380 """
381 return super(AndroidComputeClient, self).GetInstanceNamesByIPs(
herbertxue308f7662018-05-18 03:25:58 +0000382 ips, zone or self._zone)