blob: a3167fd2ca6ccb0794fdd818480462edb9c2f83e [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
40import logging
41import os
42import uuid
43
44from acloud.internal.lib import gcompute_client
45from acloud.internal.lib import utils
46from acloud.public import errors
47
48logger = logging.getLogger(__name__)
49
50
51class AndroidComputeClient(gcompute_client.ComputeClient):
52 """Client that manages Anadroid Virtual Device."""
53
Fang Deng93ef3592017-02-08 14:02:47 -080054 INSTANCE_NAME_FMT = "ins-{uuid}-{build_id}-{build_target}"
55 IMAGE_NAME_FMT = "img-{uuid}-{build_id}-{build_target}"
56 DATA_DISK_NAME_FMT = "data-{instance}"
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070057 BOOT_COMPLETED_MSG = "VIRTUAL_DEVICE_BOOT_COMPLETED"
58 BOOT_TIMEOUT_SECS = 5 * 60 # 5 mins, usually it should take ~2 mins
59 BOOT_CHECK_INTERVAL_SECS = 10
Fang Deng93ef3592017-02-08 14:02:47 -080060 NAME_LENGTH_LIMIT = 63
61 # If the generated name ends with '-', replace it with REPLACER.
62 REPLACER = "e"
Keun Soo Yimb293fdb2016-09-21 16:03:44 -070063
64 def __init__(self, acloud_config, oauth2_credentials):
65 """Initialize.
66
67 Args:
68 acloud_config: An AcloudConfig object.
69 oauth2_credentials: An oauth2client.OAuth2Credentials instance.
70 """
71 super(AndroidComputeClient, self).__init__(acloud_config,
72 oauth2_credentials)
73 self._zone = acloud_config.zone
74 self._machine_type = acloud_config.machine_type
75 self._min_machine_size = acloud_config.min_machine_size
76 self._network = acloud_config.network
77 self._orientation = acloud_config.orientation
78 self._resolution = acloud_config.resolution
79 self._metadata = acloud_config.metadata_variable.copy()
80
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] == "-":
100 name = name[:-1] + cls.REPLACER
101 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:
124 build_target: Target name, e.g. "gce_x86-userdebug"
125 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
132 name = cls.IMAGE_NAME_FMT.format(build_target=build_target,
133 build_id=build_id,
134 uuid=uuid.uuid4().hex[:8])
Fang Deng93ef3592017-02-08 14:02:47 -0800135 return cls._FormalizeName(name)
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700136
137 @classmethod
138 def GetDataDiskName(cls, instance):
139 """Get data disk name for an instance.
140
141 Args:
142 instance: An instance_name.
143
144 Returns:
145 The corresponding data disk name.
146 """
Fang Deng93ef3592017-02-08 14:02:47 -0800147 name = cls.DATA_DISK_NAME_FMT.format(instance=instance)
148 return cls._FormalizeName(name)
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700149
150 @classmethod
151 def GenerateInstanceName(cls, build_target=None, build_id=None):
152 """Generate an instance name given build_target, build_id.
153
154 Target is not used as instance name has a length limit.
155
156 Args:
157 build_target: Target name, e.g. "gce_x86-userdebug"
158 build_id: Build id, a string, e.g. "2263051", "P2804227"
159
160 Returns:
161 A string, representing instance name.
162 """
163 if not build_target and not build_id:
164 return "instance-" + uuid.uuid4().hex
Fang Deng93ef3592017-02-08 14:02:47 -0800165 name = cls.INSTANCE_NAME_FMT.format(
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700166 build_target=build_target,
167 build_id=build_id,
168 uuid=uuid.uuid4().hex[:8]).replace("_", "-").lower()
Fang Deng93ef3592017-02-08 14:02:47 -0800169 return cls._FormalizeName(name)
Keun Soo Yimb293fdb2016-09-21 16:03:44 -0700170
171 def CreateDisk(self, disk_name, source_image, size_gb):
172 """Create a gce disk.
173
174 Args:
175 disk_name: A string.
176 source_image: A string, name to the image name.
177 size_gb: Integer, size in gigabytes.
178 """
179 if self.CheckDiskExists(disk_name, self._zone):
180 raise errors.DriverError(
181 "Failed to create disk %s, already exists." % disk_name)
182 if source_image and not self.CheckImageExists(source_image):
183 raise errors.DriverError(
184 "Failed to create disk %s, source image %s does not exist." %
185 (disk_name, source_image))
186 super(AndroidComputeClient, self).CreateDisk(disk_name,
187 source_image=source_image,
188 size_gb=size_gb,
189 zone=self._zone)
190
191 def CreateImage(self, image_name, source_uri):
192 """Create a gce image.
193
194 Args:
195 image_name: String, name of the image.
196 source_uri: A full Google Storage URL to the disk image.
197 e.g. "https://storage.googleapis.com/my-bucket/
198 avd-system-2243663.tar.gz"
199 """
200 if not self.CheckImageExists(image_name):
201 super(AndroidComputeClient, self).CreateImage(image_name,
202 source_uri)
203
204 def _GetExtraDiskArgs(self, extra_disk_name):
205 """Get extra disk arg for given disk.
206
207 Args:
208 extra_disk_name: Name of the disk.
209
210 Returns:
211 A dictionary of disk args.
212 """
213 return [{
214 "type": "PERSISTENT",
215 "mode": "READ_WRITE",
216 "source": "projects/%s/zones/%s/disks/%s" % (
217 self._project, self._zone, extra_disk_name),
218 "autoDelete": True,
219 "boot": False,
220 "interface": "SCSI",
221 "deviceName": extra_disk_name,
222 }]
223
224 def CreateInstance(self, instance, image_name, extra_disk_name=None):
225 """Create a gce instance given an gce image.
226
227 Args:
228 instance: instance name.
229 image_name: A string, the name of the GCE image.
230 """
231 self._CheckMachineSize()
232 disk_args = self._GetDiskArgs(instance, image_name)
233 if extra_disk_name:
234 disk_args.extend(self._GetExtraDiskArgs(extra_disk_name))
235 metadata = self._metadata.copy()
236 metadata["cfg_sta_display_resolution"] = self._resolution
237 metadata["t_force_orientation"] = self._orientation
238 super(AndroidComputeClient, self).CreateInstance(
239 instance, image_name, self._machine_type, metadata, self._network,
240 self._zone, disk_args)
241
242 def CheckBoot(self, instance):
243 """Check once to see if boot completes.
244
245 Args:
246 instance: string, instance name.
247
248 Returns:
249 True if the BOOT_COMPLETED_MSG appears in serial port output.
250 otherwise False.
251 """
252 try:
253 return self.BOOT_COMPLETED_MSG in self.GetSerialPortOutput(
254 instance=instance, port=1)
255 except errors.HttpError as e:
256 if e.code == 400:
257 logging.debug("CheckBoot: Instance is not ready yet %s",
258 str(e))
259 return False
260 raise
261
262 def WaitForBoot(self, instance):
263 """Wait for boot to completes or hit timeout.
264
265 Args:
266 instance: string, instance name.
267 """
268 logger.info("Waiting for instance to boot up: %s", instance)
269 timeout_exception = errors.DeviceBootTimeoutError(
270 "Device %s did not finish on boot within timeout (%s secs)" %
271 (instance, self.BOOT_TIMEOUT_SECS)),
272 utils.PollAndWait(func=self.CheckBoot,
273 expected_return=True,
274 timeout_exception=timeout_exception,
275 timeout_secs=self.BOOT_TIMEOUT_SECS,
276 sleep_interval_secs=self.BOOT_CHECK_INTERVAL_SECS,
277 instance=instance)
278 logger.info("Instance boot completed: %s", instance)
279
280 def GetInstanceIP(self, instance):
281 """Get Instance IP given instance name.
282
283 Args:
284 instance: String, representing instance name.
285
286 Returns:
287 string, IP of the instance.
288 """
289 return super(AndroidComputeClient, self).GetInstanceIP(instance,
290 self._zone)
291
292 def GetSerialPortOutput(self, instance, port=1):
293 """Get serial port output.
294
295 Args:
296 instance: string, instance name.
297 port: int, which COM port to read from, 1-4, default to 1.
298
299 Returns:
300 String, contents of the output.
301
302 Raises:
303 errors.DriverError: For malformed response.
304 """
305 return super(AndroidComputeClient, self).GetSerialPortOutput(
306 instance, self._zone, port)
307
308 def GetInstanceNamesByIPs(self, ips):
309 """Get Instance names by IPs.
310
311 This function will go through all instances, which
312 could be slow if there are too many instances. However, currently
313 GCE doesn't support search for instance by IP.
314
315 Args:
316 ips: A set of IPs.
317
318 Returns:
319 A dictionary where key is ip and value is instance name or None
320 if instance is not found for the given IP.
321 """
322 return super(AndroidComputeClient, self).GetInstanceNamesByIPs(
323 ips, self._zone)