1# Copyright 2018 - The Android Open Source Project
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#     http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14r"""List entry point.
15
16List will handle all the logic related to list a local/remote instance
17of an Android Virtual Device.
18"""
19
20from __future__ import print_function
21import getpass
22import logging
23import os
24
25from acloud import errors
26from acloud.internal import constants
27from acloud.internal.lib import auth
28from acloud.internal.lib import gcompute_client
29from acloud.internal.lib import utils
30from acloud.list import instance
31from acloud.public import config
32
33
34logger = logging.getLogger(__name__)
35
36_COMMAND_PS_LAUNCH_CVD = ["ps", "-wweo", "lstart,cmd"]
37
38
39def _ProcessInstances(instance_list):
40    """Get more details of remote instances.
41
42    Args:
43        instance_list: List of dicts which contain info about the remote instances,
44                       they're the response from the GCP GCE api.
45
46    Returns:
47        instance_detail_list: List of instance.Instance() with detail info.
48    """
49    return [instance.RemoteInstance(gce_instance) for gce_instance in instance_list]
50
51
52def PrintInstancesDetails(instance_list, verbose=False):
53    """Display instances information.
54
55    Example of non-verbose case:
56    [1]device serial: 127.0.0.1:55685 (ins-1ff036dc-5128057-cf-x86-phone-userdebug)
57    [2]device serial: 127.0.0.1:60979 (ins-80952669-5128057-cf-x86-phone-userdebug)
58    [3]device serial: 127.0.0.1:6520 (local-instance)
59
60    Example of verbose case:
61    [1] name: ins-244710f0-5091715-aosp-cf-x86-phone-userdebug
62        IP: None
63        create time: 2018-10-25T06:32:08.182-07:00
64        status: TERMINATED
65        avd type: cuttlefish
66        display: 1080x1920 (240)
67
68    [2] name: ins-82979192-5091715-aosp-cf-x86-phone-userdebug
69        IP: 35.232.77.15
70        adb serial: 127.0.0.1:33537
71        create time: 2018-10-25T06:34:22.716-07:00
72        status: RUNNING
73        avd type: cuttlefish
74        display: 1080x1920 (240)
75
76    Args:
77        verbose: Boolean, True to print all details and only full name if False.
78        instance_list: List of instances.
79    """
80    if not instance_list:
81        print("No remote or local instances found")
82
83    for num, instance_info in enumerate(instance_list, 1):
84        idx_str = "[%d]" % num
85        utils.PrintColorString(idx_str, end="")
86        if verbose:
87            print(instance_info.Summary())
88            # add space between instances in verbose mode.
89            print("")
90        else:
91            print(instance_info)
92
93
94def GetRemoteInstances(cfg):
95    """Look for remote instances.
96
97    We're going to query the GCP project for all instances that created by user.
98
99    Args:
100        cfg: AcloudConfig object.
101
102    Returns:
103        instance_list: List of remote instances.
104    """
105    credentials = auth.CreateCredentials(cfg)
106    compute_client = gcompute_client.ComputeClient(cfg, credentials)
107    filter_item = "labels.%s=%s" % (constants.LABEL_CREATE_BY, getpass.getuser())
108    all_instances = compute_client.ListInstances(instance_filter=filter_item)
109
110    logger.debug("Instance list from: (filter: %s\n%s):",
111                 filter_item, all_instances)
112
113    return _ProcessInstances(all_instances)
114
115
116def _GetLocalCuttlefishInstances():
117    """Look for local cuttelfish instances.
118
119    Gather local instances information from cuttlefish runtime config.
120
121    Returns:
122        instance_list: List of local instances.
123    """
124    local_instance_list = []
125    for cf_runtime_config_path in instance.GetAllLocalInstanceConfigs():
126        ins = instance.LocalInstance(cf_runtime_config_path)
127        if ins.CvdStatus():
128            local_instance_list.append(ins)
129        else:
130            logger.info("cvd runtime config found but instance is not active:%s"
131                        , cf_runtime_config_path)
132    return local_instance_list
133
134
135def GetActiveCVD(local_instance_id):
136    """Check if the local AVD with specific instance id is running
137
138    Args:
139        local_instance_id: Integer of instance id.
140
141    Return:
142        LocalInstance object.
143    """
144    cfg_path = instance.GetLocalInstanceConfig(local_instance_id)
145    if cfg_path:
146        ins = instance.LocalInstance(cfg_path)
147        if ins.CvdStatus():
148            return ins
149    cfg_path = instance.GetDefaultCuttlefishConfig()
150    if local_instance_id == 1 and os.path.isfile(cfg_path):
151        ins = instance.LocalInstance(cfg_path)
152        if ins.CvdStatus():
153            return ins
154    return None
155
156
157def GetLocalInstances():
158    """Look for local cuttleifsh and goldfish instances.
159
160    Returns:
161        List of local instances.
162    """
163    # Running instances on local is not supported on all OS.
164    if not utils.IsSupportedPlatform():
165        return []
166
167    return (_GetLocalCuttlefishInstances() +
168            instance.LocalGoldfishInstance.GetExistingInstances())
169
170
171def GetInstances(cfg):
172    """Look for remote/local instances.
173
174    Args:
175        cfg: AcloudConfig object.
176
177    Returns:
178        instance_list: List of instances.
179    """
180    return GetRemoteInstances(cfg) + GetLocalInstances()
181
182
183def ChooseInstancesFromList(instances):
184    """Let user choose instances from a list.
185
186    Args:
187        instances: List of Instance objects.
188
189    Returns:
190         List of Instance objects.
191    """
192    if len(instances) > 1:
193        print("Multiple instances detected, choose any one to proceed:")
194        return utils.GetAnswerFromList(instances, enable_choose_all=True)
195    return instances
196
197
198def ChooseInstances(cfg, select_all_instances=False):
199    """Get instances.
200
201    Retrieve all remote/local instances and if there is more than 1 instance
202    found, ask user which instance they'd like.
203
204    Args:
205        cfg: AcloudConfig object.
206        select_all_instances: True if select all instances by default and no
207                              need to ask user to choose.
208
209    Returns:
210        List of Instance() object.
211    """
212    instances = GetInstances(cfg)
213    if not select_all_instances:
214        return ChooseInstancesFromList(instances)
215    return instances
216
217
218def ChooseOneRemoteInstance(cfg):
219    """Get one remote cuttlefish instance.
220
221    Retrieve all remote cuttlefish instances and if there is more than 1 instance
222    found, ask user which instance they'd like.
223
224    Args:
225        cfg: AcloudConfig object.
226
227    Raises:
228        errors.NoInstancesFound: No cuttlefish remote instance found.
229
230    Returns:
231        list.Instance() object.
232    """
233    instances_list = GetCFRemoteInstances(cfg)
234    if not instances_list:
235        raise errors.NoInstancesFound(
236            "Can't find any cuttlefish remote instances, please try "
237            "'$acloud create' to create instances")
238    if len(instances_list) > 1:
239        print("Multiple instances detected, choose any one to proceed:")
240        instances = utils.GetAnswerFromList(instances_list,
241                                            enable_choose_all=False)
242        return instances[0]
243
244    return instances_list[0]
245
246
247def FilterInstancesByNames(instances, names):
248    """Find instances by names.
249
250    Args:
251        instances: Collection of Instance objects.
252        names: Collection of strings, the names of the instances to search for.
253
254    Returns:
255        List of Instance objects.
256
257    Raises:
258        errors.NoInstancesFound if any instance is not found.
259    """
260    instance_map = {inst.name: inst for inst in instances}
261    found_instances = []
262    missing_instance_names = []
263    for name in names:
264        if name in instance_map:
265            found_instances.append(instance_map[name])
266        else:
267            missing_instance_names.append(name)
268
269    if missing_instance_names:
270        raise errors.NoInstancesFound("Did not find the following instances: %s" %
271                                      " ".join(missing_instance_names))
272    return found_instances
273
274
275def GetInstancesFromInstanceNames(cfg, instance_names):
276    """Get instances from instance names.
277
278    Turn a list of instance names into a list of Instance().
279
280    Args:
281        cfg: AcloudConfig object.
282        instance_names: list of instance name.
283
284    Returns:
285        List of Instance() objects.
286
287    Raises:
288        errors.NoInstancesFound: No instances found.
289    """
290    return FilterInstancesByNames(GetInstances(cfg), instance_names)
291
292
293def FilterInstancesByAdbPort(instances, adb_port):
294    """Find an instance by adb port.
295
296    Args:
297        instances: Collection of Instance objects.
298        adb_port: int, adb port of the instance to search for.
299
300    Returns:
301        List of Instance() objects.
302
303    Raises:
304        errors.NoInstancesFound: No instances found.
305    """
306    all_instance_info = []
307    for instance_object in instances:
308        if instance_object.adb_port == adb_port:
309            return [instance_object]
310        all_instance_info.append(instance_object.fullname)
311
312    # Show devices information to user when user provides wrong adb port.
313    if all_instance_info:
314        hint_message = ("No instance with adb port %d, available instances:\n%s"
315                        % (adb_port, "\n".join(all_instance_info)))
316    else:
317        hint_message = "No instances to delete."
318    raise errors.NoInstancesFound(hint_message)
319
320
321def GetCFRemoteInstances(cfg):
322    """Look for cuttlefish remote instances.
323
324    Args:
325        cfg: AcloudConfig object.
326
327    Returns:
328        instance_list: List of instance names.
329    """
330    instances = GetRemoteInstances(cfg)
331    return [ins for ins in instances if ins.avd_type == constants.TYPE_CF]
332
333
334def Run(args):
335    """Run list.
336
337    Args:
338        args: Namespace object from argparse.parse_args.
339    """
340    instances = GetLocalInstances()
341    cfg = config.GetAcloudConfig(args)
342    if not args.local_only and cfg.SupportRemoteInstance():
343        instances.extend(GetRemoteInstances(cfg))
344
345    PrintInstancesDetails(instances, args.verbose)
346