blob: 3fb4baca05612ce7406aed3a9d0c4675a71cf464 [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.
16
17"""A client that manages Android compute engine instances.
18
19** AndroidComputeClient **
20
21AndroidComputeClient derives from ComputeClient. It manges a google
22compute engine project that is setup for running Android instances.
23It knows how to create android GCE images and instances.
24
25** Class hierarchy **
26
27 base_cloud_client.BaseCloudApiClient
28 ^
29 |
30 gcompute_client.ComputeClient
31 ^
32 |
33 gcompute_client.AndroidComputeClient
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070034"""
35
Fang Dengfed6a6f2017-03-01 18:27:28 -080036import getpass
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070037import logging
38import os
39import uuid
40
41from acloud.internal.lib import gcompute_client
42from acloud.internal.lib import utils
43from acloud.public import errors
44
45logger = logging.getLogger(__name__)
46
47
48class AndroidComputeClient(gcompute_client.ComputeClient):
49 """Client that manages Anadroid Virtual Device."""
50
Fang Deng93ef3592017-02-08 14:02:47 -080051 INSTANCE_NAME_FMT = "ins-{uuid}-{build_id}-{build_target}"
52 IMAGE_NAME_FMT = "img-{uuid}-{build_id}-{build_target}"
53 DATA_DISK_NAME_FMT = "data-{instance}"
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070054 BOOT_COMPLETED_MSG = "VIRTUAL_DEVICE_BOOT_COMPLETED"
Kevin Chengb5963882018-05-09 00:06:27 -070055 BOOT_STARTED_MSG = "VIRTUAL_DEVICE_BOOT_STARTED"
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070056 BOOT_TIMEOUT_SECS = 5 * 60 # 5 mins, usually it should take ~2 mins
57 BOOT_CHECK_INTERVAL_SECS = 10
Kevin Chengb5963882018-05-09 00:06:27 -070058
59 OPERATION_TIMEOUT_SECS = 20 * 60 # Override parent value, 20 mins
60
Fang Deng93ef3592017-02-08 14:02:47 -080061 NAME_LENGTH_LIMIT = 63
62 # If the generated name ends with '-', replace it with REPLACER.
63 REPLACER = "e"
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070064
65 def __init__(self, acloud_config, oauth2_credentials):
66 """Initialize.
67
68 Args:
69 acloud_config: An AcloudConfig object.
70 oauth2_credentials: An oauth2client.OAuth2Credentials instance.
71 """
72 super(AndroidComputeClient, self).__init__(acloud_config,
73 oauth2_credentials)
74 self._zone = acloud_config.zone
75 self._machine_type = acloud_config.machine_type
76 self._min_machine_size = acloud_config.min_machine_size
77 self._network = acloud_config.network
78 self._orientation = acloud_config.orientation
79 self._resolution = acloud_config.resolution
80 self._metadata = acloud_config.metadata_variable.copy()
Fang Dengfed6a6f2017-03-01 18:27:28 -080081 self._ssh_public_key_path = acloud_config.ssh_public_key_path
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070082
Fang Deng93ef3592017-02-08 14:02:47 -080083 @classmethod
84 def _FormalizeName(cls, name):
85 """Formalize the name to comply with RFC1035.
86
87 The name must be 1-63 characters long and match the regular expression
88 [a-z]([-a-z0-9]*[a-z0-9])? which means the first character must be a
89 lowercase letter, and all following characters must be a dash,
90 lowercase letter, or digit, except the last character, which cannot be
91 a dash.
92
93 Args:
94 name: A string.
95
96 Returns:
97 name: A string that complies with RFC1035.
98 """
99 name = name.replace("_", "-").lower()
100 name = name[:cls.NAME_LENGTH_LIMIT]
101 if name[-1] == "-":
102 name = name[:-1] + cls.REPLACER
103 return name
104
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700105 def _CheckMachineSize(self):
106 """Check machine size.
107
108 Check if the desired machine type |self._machine_type| meets
109 the requirement of minimum machine size specified as
110 |self._min_machine_size|.
111
112 Raises:
113 errors.DriverError: if check fails.
114 """
115 if self.CompareMachineSize(self._machine_type, self._min_machine_size,
116 self._zone) < 0:
117 raise errors.DriverError(
118 "%s does not meet the minimum required machine size %s" %
119 (self._machine_type, self._min_machine_size))
120
121 @classmethod
122 def GenerateImageName(cls, build_target=None, build_id=None):
123 """Generate an image name given build_target, build_id.
124
125 Args:
Kevin Chengb5963882018-05-09 00:06:27 -0700126 build_target: Target name, e.g. "aosp_cf_x86_phone-userdebug"
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700127 build_id: Build id, a string, e.g. "2263051", "P2804227"
128
129 Returns:
130 A string, representing image name.
131 """
132 if not build_target and not build_id:
133 return "image-" + uuid.uuid4().hex
134 name = cls.IMAGE_NAME_FMT.format(build_target=build_target,
135 build_id=build_id,
136 uuid=uuid.uuid4().hex[:8])
Fang Deng93ef3592017-02-08 14:02:47 -0800137 return cls._FormalizeName(name)
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700138
139 @classmethod
140 def GetDataDiskName(cls, instance):
141 """Get data disk name for an instance.
142
143 Args:
144 instance: An instance_name.
145
146 Returns:
147 The corresponding data disk name.
148 """
Fang Deng93ef3592017-02-08 14:02:47 -0800149 name = cls.DATA_DISK_NAME_FMT.format(instance=instance)
150 return cls._FormalizeName(name)
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700151
152 @classmethod
153 def GenerateInstanceName(cls, build_target=None, build_id=None):
154 """Generate an instance name given build_target, build_id.
155
156 Target is not used as instance name has a length limit.
157
158 Args:
Kevin Chengb5963882018-05-09 00:06:27 -0700159 build_target: Target name, e.g. "aosp_cf_x86_phone-userdebug"
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700160 build_id: Build id, a string, e.g. "2263051", "P2804227"
161
162 Returns:
163 A string, representing instance name.
164 """
165 if not build_target and not build_id:
166 return "instance-" + uuid.uuid4().hex
Fang Deng93ef3592017-02-08 14:02:47 -0800167 name = cls.INSTANCE_NAME_FMT.format(
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700168 build_target=build_target,
169 build_id=build_id,
Kevin Chengb5963882018-05-09 00:06:27 -0700170 uuid=uuid.uuid4().hex[:8]).replace("_", "-")
Fang Deng93ef3592017-02-08 14:02:47 -0800171 return cls._FormalizeName(name)
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700172
173 def CreateDisk(self, disk_name, source_image, size_gb):
174 """Create a gce disk.
175
176 Args:
177 disk_name: A string.
178 source_image: A string, name to the image name.
179 size_gb: Integer, size in gigabytes.
180 """
181 if self.CheckDiskExists(disk_name, self._zone):
182 raise errors.DriverError(
183 "Failed to create disk %s, already exists." % disk_name)
184 if source_image and not self.CheckImageExists(source_image):
185 raise errors.DriverError(
186 "Failed to create disk %s, source image %s does not exist." %
187 (disk_name, source_image))
188 super(AndroidComputeClient, self).CreateDisk(disk_name,
189 source_image=source_image,
190 size_gb=size_gb,
191 zone=self._zone)
192
193 def CreateImage(self, image_name, source_uri):
194 """Create a gce image.
195
196 Args:
197 image_name: String, name of the image.
198 source_uri: A full Google Storage URL to the disk image.
199 e.g. "https://storage.googleapis.com/my-bucket/
200 avd-system-2243663.tar.gz"
201 """
202 if not self.CheckImageExists(image_name):
203 super(AndroidComputeClient, self).CreateImage(image_name,
204 source_uri)
205
206 def _GetExtraDiskArgs(self, extra_disk_name):
207 """Get extra disk arg for given disk.
208
209 Args:
210 extra_disk_name: Name of the disk.
211
212 Returns:
213 A dictionary of disk args.
214 """
215 return [{
216 "type": "PERSISTENT",
217 "mode": "READ_WRITE",
218 "source": "projects/%s/zones/%s/disks/%s" % (
219 self._project, self._zone, extra_disk_name),
220 "autoDelete": True,
221 "boot": False,
222 "interface": "SCSI",
223 "deviceName": extra_disk_name,
224 }]
225
Fang Dengfed6a6f2017-03-01 18:27:28 -0800226 @staticmethod
227 def _LoadSshPublicKey(ssh_public_key_path):
228 """Load the content of ssh public key from a file.
229
230 Args:
231 ssh_public_key_path: String, path to the public key file.
232 E.g. ~/.ssh/acloud_rsa.pub
233 Returns:
234 String, content of the file.
235
236 Raises:
237 errors.DriverError if the public key file does not exist
238 or the content is not valid.
239 """
240 key_path = os.path.expanduser(ssh_public_key_path)
241 if not os.path.exists(key_path):
242 raise errors.DriverError(
243 "SSH public key file %s does not exist." % key_path)
244
245 with open(key_path) as f:
246 rsa = f.read()
247 rsa = rsa.strip() if rsa else rsa
248 utils.VerifyRsaPubKey(rsa)
249 return rsa
250
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700251 def CreateInstance(self, instance, image_name, extra_disk_name=None):
252 """Create a gce instance given an gce image.
253
254 Args:
Kevin Chengb5963882018-05-09 00:06:27 -0700255 instance: A string, the name of the instance.
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700256 image_name: A string, the name of the GCE image.
Kevin Chengb5963882018-05-09 00:06:27 -0700257 extra_disk_name: A string, the name of the extra disk to attach.
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700258 """
259 self._CheckMachineSize()
260 disk_args = self._GetDiskArgs(instance, image_name)
261 if extra_disk_name:
262 disk_args.extend(self._GetExtraDiskArgs(extra_disk_name))
263 metadata = self._metadata.copy()
264 metadata["cfg_sta_display_resolution"] = self._resolution
265 metadata["t_force_orientation"] = self._orientation
Fang Dengfed6a6f2017-03-01 18:27:28 -0800266
267 # Add per-instance ssh key
268 if self._ssh_public_key_path:
269 rsa = self._LoadSshPublicKey(self._ssh_public_key_path)
270 logger.info("ssh_public_key_path is specified in config: %s, "
271 "will add the key to the instance.",
272 self._ssh_public_key_path)
273 metadata["sshKeys"] = "%s:%s" % (getpass.getuser(), rsa)
274 else:
275 logger.warning(
276 "ssh_public_key_path is not specified in config, "
277 "only project-wide key will be effective.")
278
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700279 super(AndroidComputeClient, self).CreateInstance(
280 instance, image_name, self._machine_type, metadata, self._network,
281 self._zone, disk_args)
282
Kevin Chengb5963882018-05-09 00:06:27 -0700283 def CheckBootFailure(self, serial_out, instance):
284 """Determine if serial output has indicated any boot failure.
285
286 Subclass has to define this function to detect failures
287 in the boot process
288
289 Args:
290 serial_out: string
291 instance: string, instance name.
292
293 Raises:
294 Raises errors.DeviceBootError exception if a failure is detected.
295 """
296 pass
297
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700298 def CheckBoot(self, instance):
299 """Check once to see if boot completes.
300
301 Args:
302 instance: string, instance name.
303
304 Returns:
Kevin Chengb5963882018-05-09 00:06:27 -0700305 True if the BOOT_COMPLETED_MSG or BOOT_STARTED_MSG appears in serial
306 port output, otherwise False.
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700307 """
308 try:
Kevin Chengb5963882018-05-09 00:06:27 -0700309 serial_out = self.GetSerialPortOutput(instance=instance, port=1)
310 self.CheckBootFailure(serial_out, instance)
311 return ((self.BOOT_COMPLETED_MSG in serial_out)
312 or (self.BOOT_STARTED_MSG in serial_out))
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700313 except errors.HttpError as e:
314 if e.code == 400:
Fang Dengfed6a6f2017-03-01 18:27:28 -0800315 logger.debug("CheckBoot: Instance is not ready yet %s",
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700316 str(e))
317 return False
318 raise
319
320 def WaitForBoot(self, instance):
321 """Wait for boot to completes or hit timeout.
322
323 Args:
324 instance: string, instance name.
325 """
326 logger.info("Waiting for instance to boot up: %s", instance)
327 timeout_exception = errors.DeviceBootTimeoutError(
328 "Device %s did not finish on boot within timeout (%s secs)" %
329 (instance, self.BOOT_TIMEOUT_SECS)),
330 utils.PollAndWait(func=self.CheckBoot,
331 expected_return=True,
332 timeout_exception=timeout_exception,
333 timeout_secs=self.BOOT_TIMEOUT_SECS,
334 sleep_interval_secs=self.BOOT_CHECK_INTERVAL_SECS,
335 instance=instance)
336 logger.info("Instance boot completed: %s", instance)
337
338 def GetInstanceIP(self, instance):
339 """Get Instance IP given instance name.
340
341 Args:
342 instance: String, representing instance name.
343
344 Returns:
345 string, IP of the instance.
346 """
347 return super(AndroidComputeClient, self).GetInstanceIP(instance,
348 self._zone)
349
350 def GetSerialPortOutput(self, instance, port=1):
351 """Get serial port output.
352
353 Args:
354 instance: string, instance name.
355 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(
364 instance, self._zone, port)
365
366 def GetInstanceNamesByIPs(self, ips):
367 """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.
375
376 Returns:
377 A dictionary where key is ip and value is instance name or None
378 if instance is not found for the given IP.
379 """
380 return super(AndroidComputeClient, self).GetInstanceNamesByIPs(
381 ips, self._zone)