blob: 0aef2536e766129696da3f076673d910d35347fb [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
54 INSTANCE_NAME_FMT = "{build_target}-{build_id}-{uuid}"
55 IMAGE_NAME_FMT = "image-{build_target}-{build_id}-{uuid}"
56 DATA_DISK_NAME_FMT = "{instance}-data"
57 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
60
61 def __init__(self, acloud_config, oauth2_credentials):
62 """Initialize.
63
64 Args:
65 acloud_config: An AcloudConfig object.
66 oauth2_credentials: An oauth2client.OAuth2Credentials instance.
67 """
68 super(AndroidComputeClient, self).__init__(acloud_config,
69 oauth2_credentials)
70 self._zone = acloud_config.zone
71 self._machine_type = acloud_config.machine_type
72 self._min_machine_size = acloud_config.min_machine_size
73 self._network = acloud_config.network
74 self._orientation = acloud_config.orientation
75 self._resolution = acloud_config.resolution
76 self._metadata = acloud_config.metadata_variable.copy()
77
78 def _CheckMachineSize(self):
79 """Check machine size.
80
81 Check if the desired machine type |self._machine_type| meets
82 the requirement of minimum machine size specified as
83 |self._min_machine_size|.
84
85 Raises:
86 errors.DriverError: if check fails.
87 """
88 if self.CompareMachineSize(self._machine_type, self._min_machine_size,
89 self._zone) < 0:
90 raise errors.DriverError(
91 "%s does not meet the minimum required machine size %s" %
92 (self._machine_type, self._min_machine_size))
93
94 @classmethod
95 def GenerateImageName(cls, build_target=None, build_id=None):
96 """Generate an image name given build_target, build_id.
97
98 Args:
99 build_target: Target name, e.g. "gce_x86-userdebug"
100 build_id: Build id, a string, e.g. "2263051", "P2804227"
101
102 Returns:
103 A string, representing image name.
104 """
105 if not build_target and not build_id:
106 return "image-" + uuid.uuid4().hex
107 name = cls.IMAGE_NAME_FMT.format(build_target=build_target,
108 build_id=build_id,
109 uuid=uuid.uuid4().hex[:8])
110 return name.replace("_", "-").lower()
111
112 @classmethod
113 def GetDataDiskName(cls, instance):
114 """Get data disk name for an instance.
115
116 Args:
117 instance: An instance_name.
118
119 Returns:
120 The corresponding data disk name.
121 """
122 return cls.DATA_DISK_NAME_FMT.format(instance=instance)
123
124 @classmethod
125 def GenerateInstanceName(cls, build_target=None, build_id=None):
126 """Generate an instance name given build_target, build_id.
127
128 Target is not used as instance name has a length limit.
129
130 Args:
131 build_target: Target name, e.g. "gce_x86-userdebug"
132 build_id: Build id, a string, e.g. "2263051", "P2804227"
133
134 Returns:
135 A string, representing instance name.
136 """
137 if not build_target and not build_id:
138 return "instance-" + uuid.uuid4().hex
139 return cls.INSTANCE_NAME_FMT.format(
140 build_target=build_target,
141 build_id=build_id,
142 uuid=uuid.uuid4().hex[:8]).replace("_", "-").lower()
143
144 def CreateDisk(self, disk_name, source_image, size_gb):
145 """Create a gce disk.
146
147 Args:
148 disk_name: A string.
149 source_image: A string, name to the image name.
150 size_gb: Integer, size in gigabytes.
151 """
152 if self.CheckDiskExists(disk_name, self._zone):
153 raise errors.DriverError(
154 "Failed to create disk %s, already exists." % disk_name)
155 if source_image and not self.CheckImageExists(source_image):
156 raise errors.DriverError(
157 "Failed to create disk %s, source image %s does not exist." %
158 (disk_name, source_image))
159 super(AndroidComputeClient, self).CreateDisk(disk_name,
160 source_image=source_image,
161 size_gb=size_gb,
162 zone=self._zone)
163
164 def CreateImage(self, image_name, source_uri):
165 """Create a gce image.
166
167 Args:
168 image_name: String, name of the image.
169 source_uri: A full Google Storage URL to the disk image.
170 e.g. "https://storage.googleapis.com/my-bucket/
171 avd-system-2243663.tar.gz"
172 """
173 if not self.CheckImageExists(image_name):
174 super(AndroidComputeClient, self).CreateImage(image_name,
175 source_uri)
176
177 def _GetExtraDiskArgs(self, extra_disk_name):
178 """Get extra disk arg for given disk.
179
180 Args:
181 extra_disk_name: Name of the disk.
182
183 Returns:
184 A dictionary of disk args.
185 """
186 return [{
187 "type": "PERSISTENT",
188 "mode": "READ_WRITE",
189 "source": "projects/%s/zones/%s/disks/%s" % (
190 self._project, self._zone, extra_disk_name),
191 "autoDelete": True,
192 "boot": False,
193 "interface": "SCSI",
194 "deviceName": extra_disk_name,
195 }]
196
197 def CreateInstance(self, instance, image_name, extra_disk_name=None):
198 """Create a gce instance given an gce image.
199
200 Args:
201 instance: instance name.
202 image_name: A string, the name of the GCE image.
203 """
204 self._CheckMachineSize()
205 disk_args = self._GetDiskArgs(instance, image_name)
206 if extra_disk_name:
207 disk_args.extend(self._GetExtraDiskArgs(extra_disk_name))
208 metadata = self._metadata.copy()
209 metadata["cfg_sta_display_resolution"] = self._resolution
210 metadata["t_force_orientation"] = self._orientation
211 super(AndroidComputeClient, self).CreateInstance(
212 instance, image_name, self._machine_type, metadata, self._network,
213 self._zone, disk_args)
214
215 def CheckBoot(self, instance):
216 """Check once to see if boot completes.
217
218 Args:
219 instance: string, instance name.
220
221 Returns:
222 True if the BOOT_COMPLETED_MSG appears in serial port output.
223 otherwise False.
224 """
225 try:
226 return self.BOOT_COMPLETED_MSG in self.GetSerialPortOutput(
227 instance=instance, port=1)
228 except errors.HttpError as e:
229 if e.code == 400:
230 logging.debug("CheckBoot: Instance is not ready yet %s",
231 str(e))
232 return False
233 raise
234
235 def WaitForBoot(self, instance):
236 """Wait for boot to completes or hit timeout.
237
238 Args:
239 instance: string, instance name.
240 """
241 logger.info("Waiting for instance to boot up: %s", instance)
242 timeout_exception = errors.DeviceBootTimeoutError(
243 "Device %s did not finish on boot within timeout (%s secs)" %
244 (instance, self.BOOT_TIMEOUT_SECS)),
245 utils.PollAndWait(func=self.CheckBoot,
246 expected_return=True,
247 timeout_exception=timeout_exception,
248 timeout_secs=self.BOOT_TIMEOUT_SECS,
249 sleep_interval_secs=self.BOOT_CHECK_INTERVAL_SECS,
250 instance=instance)
251 logger.info("Instance boot completed: %s", instance)
252
253 def GetInstanceIP(self, instance):
254 """Get Instance IP given instance name.
255
256 Args:
257 instance: String, representing instance name.
258
259 Returns:
260 string, IP of the instance.
261 """
262 return super(AndroidComputeClient, self).GetInstanceIP(instance,
263 self._zone)
264
265 def GetSerialPortOutput(self, instance, port=1):
266 """Get serial port output.
267
268 Args:
269 instance: string, instance name.
270 port: int, which COM port to read from, 1-4, default to 1.
271
272 Returns:
273 String, contents of the output.
274
275 Raises:
276 errors.DriverError: For malformed response.
277 """
278 return super(AndroidComputeClient, self).GetSerialPortOutput(
279 instance, self._zone, port)
280
281 def GetInstanceNamesByIPs(self, ips):
282 """Get Instance names by IPs.
283
284 This function will go through all instances, which
285 could be slow if there are too many instances. However, currently
286 GCE doesn't support search for instance by IP.
287
288 Args:
289 ips: A set of IPs.
290
291 Returns:
292 A dictionary where key is ip and value is instance name or None
293 if instance is not found for the given IP.
294 """
295 return super(AndroidComputeClient, self).GetInstanceNamesByIPs(
296 ips, self._zone)