blob: f6a29a3bdac9c4172cbd4c6cd3eca3058de3bd98 [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
34
35TODO(fdeng):
36 Merge caci/framework/gce_manager.py
37 with this module, update callers of gce_manager.py to use this module.
38"""
39
Fang Dengfed6a6f2017-03-01 18:27:28 -080040import getpass
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070041import logging
42import os
43import uuid
44
45from acloud.internal.lib import gcompute_client
46from acloud.internal.lib import utils
47from acloud.public import errors
48
49logger = logging.getLogger(__name__)
50
51
52class AndroidComputeClient(gcompute_client.ComputeClient):
53 """Client that manages Anadroid Virtual Device."""
54
Fang Deng93ef3592017-02-08 14:02:47 -080055 INSTANCE_NAME_FMT = "ins-{uuid}-{build_id}-{build_target}"
56 IMAGE_NAME_FMT = "img-{uuid}-{build_id}-{build_target}"
57 DATA_DISK_NAME_FMT = "data-{instance}"
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070058 BOOT_COMPLETED_MSG = "VIRTUAL_DEVICE_BOOT_COMPLETED"
59 BOOT_TIMEOUT_SECS = 5 * 60 # 5 mins, usually it should take ~2 mins
60 BOOT_CHECK_INTERVAL_SECS = 10
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:
126 build_target: Target name, e.g. "gce_x86-userdebug"
127 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:
159 build_target: Target name, e.g. "gce_x86-userdebug"
160 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,
170 uuid=uuid.uuid4().hex[:8]).replace("_", "-").lower()
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:
255 instance: instance name.
256 image_name: A string, the name of the GCE image.
257 """
258 self._CheckMachineSize()
259 disk_args = self._GetDiskArgs(instance, image_name)
260 if extra_disk_name:
261 disk_args.extend(self._GetExtraDiskArgs(extra_disk_name))
262 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)
269 logger.info("ssh_public_key_path is specified in config: %s, "
270 "will add the key to the instance.",
271 self._ssh_public_key_path)
272 metadata["sshKeys"] = "%s:%s" % (getpass.getuser(), rsa)
273 else:
274 logger.warning(
275 "ssh_public_key_path is not specified in config, "
276 "only project-wide key will be effective.")
277
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700278 super(AndroidComputeClient, self).CreateInstance(
279 instance, image_name, self._machine_type, metadata, self._network,
280 self._zone, disk_args)
281
282 def CheckBoot(self, instance):
283 """Check once to see if boot completes.
284
285 Args:
286 instance: string, instance name.
287
288 Returns:
289 True if the BOOT_COMPLETED_MSG appears in serial port output.
290 otherwise False.
291 """
292 try:
293 return self.BOOT_COMPLETED_MSG in self.GetSerialPortOutput(
294 instance=instance, port=1)
295 except errors.HttpError as e:
296 if e.code == 400:
Fang Dengfed6a6f2017-03-01 18:27:28 -0800297 logger.debug("CheckBoot: Instance is not ready yet %s",
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700298 str(e))
299 return False
300 raise
301
302 def WaitForBoot(self, instance):
303 """Wait for boot to completes or hit timeout.
304
305 Args:
306 instance: string, instance name.
307 """
308 logger.info("Waiting for instance to boot up: %s", instance)
309 timeout_exception = errors.DeviceBootTimeoutError(
310 "Device %s did not finish on boot within timeout (%s secs)" %
311 (instance, self.BOOT_TIMEOUT_SECS)),
312 utils.PollAndWait(func=self.CheckBoot,
313 expected_return=True,
314 timeout_exception=timeout_exception,
315 timeout_secs=self.BOOT_TIMEOUT_SECS,
316 sleep_interval_secs=self.BOOT_CHECK_INTERVAL_SECS,
317 instance=instance)
318 logger.info("Instance boot completed: %s", instance)
319
320 def GetInstanceIP(self, instance):
321 """Get Instance IP given instance name.
322
323 Args:
324 instance: String, representing instance name.
325
326 Returns:
327 string, IP of the instance.
328 """
329 return super(AndroidComputeClient, self).GetInstanceIP(instance,
330 self._zone)
331
332 def GetSerialPortOutput(self, instance, port=1):
333 """Get serial port output.
334
335 Args:
336 instance: string, instance name.
337 port: int, which COM port to read from, 1-4, default to 1.
338
339 Returns:
340 String, contents of the output.
341
342 Raises:
343 errors.DriverError: For malformed response.
344 """
345 return super(AndroidComputeClient, self).GetSerialPortOutput(
346 instance, self._zone, port)
347
348 def GetInstanceNamesByIPs(self, ips):
349 """Get Instance names by IPs.
350
351 This function will go through all instances, which
352 could be slow if there are too many instances. However, currently
353 GCE doesn't support search for instance by IP.
354
355 Args:
356 ips: A set of IPs.
357
358 Returns:
359 A dictionary where key is ip and value is instance name or None
360 if instance is not found for the given IP.
361 """
362 return super(AndroidComputeClient, self).GetInstanceNamesByIPs(
363 ips, self._zone)