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"""Delete entry point.
15
16Delete will handle all the logic related to deleting a local/remote instance
17of an Android Virtual Device.
18"""
19
20from __future__ import print_function
21
22import logging
23import re
24import subprocess
25
26from acloud import errors
27from acloud.internal import constants
28from acloud.internal.lib import cvd_utils
29from acloud.internal.lib import emulator_console
30from acloud.internal.lib import goldfish_utils
31from acloud.internal.lib import oxygen_client
32from acloud.internal.lib import ssh
33from acloud.internal.lib import utils
34from acloud.list import list as list_instances
35from acloud.public import config
36from acloud.public import device_driver
37from acloud.public import report
38
39
40logger = logging.getLogger(__name__)
41
42_COMMAND_GET_PROCESS_ID = ["pgrep", "run_cvd"]
43_COMMAND_GET_PROCESS_COMMAND = ["ps", "-o", "command", "-p"]
44_RE_RUN_CVD = re.compile(r"^(?P<run_cvd>.+run_cvd)")
45_LOCAL_INSTANCE_PREFIX = "local-"
46_RE_OXYGEN_RELEASE_ERROR = re.compile(
47    r".*Error received while trying to release device: (?P<error>.*)$", re.DOTALL)
48
49
50def DeleteInstances(cfg, instances_to_delete):
51    """Delete instances according to instances_to_delete.
52
53    Args:
54        cfg: AcloudConfig object.
55        instances_to_delete: List of list.Instance() object.
56
57    Returns:
58        Report object.
59    """
60    delete_report = report.Report(command="delete")
61    remote_instance_list = []
62    for instance in instances_to_delete:
63        if instance.islocal:
64            if instance.avd_type == constants.TYPE_GF:
65                DeleteLocalGoldfishInstance(instance, delete_report)
66            elif instance.avd_type == constants.TYPE_CF:
67                DeleteLocalCuttlefishInstance(instance, delete_report)
68            else:
69                delete_report.AddError("Deleting %s is not supported." %
70                                       instance.avd_type)
71                delete_report.SetStatus(report.Status.FAIL)
72        else:
73            remote_instance_list.append(instance.name)
74        # Delete ssvnc viewer
75        if instance.vnc_port:
76            utils.CleanupSSVncviewer(instance.vnc_port)
77
78    if remote_instance_list:
79        # TODO(119283708): We should move DeleteAndroidVirtualDevices into
80        # delete.py after gce is deprecated.
81        # Stop remote instances.
82        return DeleteRemoteInstances(cfg, remote_instance_list, delete_report)
83
84    return delete_report
85
86
87@utils.TimeExecute(function_description="Deleting remote instances",
88                   result_evaluator=utils.ReportEvaluator,
89                   display_waiting_dots=False)
90def DeleteRemoteInstances(cfg, instances_to_delete, delete_report=None):
91    """Delete remote instances.
92
93    Args:
94        cfg: AcloudConfig object.
95        instances_to_delete: List of instance names(string).
96        delete_report: Report object.
97
98    Returns:
99        Report instance if there are instances to delete, None otherwise.
100
101    Raises:
102        error.ConfigError: when config doesn't support remote instances.
103    """
104    if not cfg.SupportRemoteInstance():
105        raise errors.ConfigError("No gcp project info found in config! "
106                                 "The execution of deleting remote instances "
107                                 "has been aborted.")
108    utils.PrintColorString("")
109    for instance in instances_to_delete:
110        utils.PrintColorString(" - %s" % instance, utils.TextColors.WARNING)
111    utils.PrintColorString("")
112    utils.PrintColorString("status: waiting...", end="")
113
114    # TODO(119283708): We should move DeleteAndroidVirtualDevices into
115    # delete.py after gce is deprecated.
116    # Stop remote instances.
117    delete_report = device_driver.DeleteAndroidVirtualDevices(
118        cfg, instances_to_delete, delete_report)
119
120    return delete_report
121
122
123@utils.TimeExecute(function_description="Deleting local cuttlefish instances",
124                   result_evaluator=utils.ReportEvaluator)
125def DeleteLocalCuttlefishInstance(instance, delete_report):
126    """Delete a local cuttlefish instance.
127
128    Delete local instance and write delete instance
129    information to report.
130
131    Args:
132        instance: instance.LocalInstance object.
133        delete_report: Report object.
134
135    Returns:
136        delete_report.
137    """
138    ins_lock = instance.GetLock()
139    if not ins_lock.Lock():
140        delete_report.AddError("%s is locked by another process." %
141                               instance.name)
142        delete_report.SetStatus(report.Status.FAIL)
143        return delete_report
144
145    try:
146        ins_lock.SetInUse(False)
147        instance.Delete()
148        delete_report.SetStatus(report.Status.SUCCESS)
149        device_driver.AddDeletionResultToReport(
150            delete_report, [instance.name], failed=[],
151            error_msgs=[],
152            resource_name="instance")
153    except subprocess.CalledProcessError as e:
154        delete_report.AddError(str(e))
155        delete_report.SetStatus(report.Status.FAIL)
156    finally:
157        ins_lock.Unlock()
158
159    return delete_report
160
161
162@utils.TimeExecute(function_description="Deleting local goldfish instances",
163                   result_evaluator=utils.ReportEvaluator)
164def DeleteLocalGoldfishInstance(instance, delete_report):
165    """Delete a local goldfish instance.
166
167    Args:
168        instance: LocalGoldfishInstance object.
169        delete_report: Report object.
170
171    Returns:
172        delete_report.
173    """
174    lock = instance.GetLock()
175    if not lock.Lock():
176        delete_report.AddError("%s is locked by another process." %
177                               instance.name)
178        delete_report.SetStatus(report.Status.FAIL)
179        return delete_report
180
181    try:
182        lock.SetInUse(False)
183        if instance.adb.EmuCommand("kill") == 0:
184            delete_report.SetStatus(report.Status.SUCCESS)
185            device_driver.AddDeletionResultToReport(
186                delete_report, [instance.name], failed=[],
187                error_msgs=[],
188                resource_name="instance")
189        else:
190            delete_report.AddError("Cannot kill %s." % instance.device_serial)
191            delete_report.SetStatus(report.Status.FAIL)
192    finally:
193        lock.Unlock()
194
195    return delete_report
196
197
198def ResetLocalInstanceLockByName(name, delete_report):
199    """Set the lock state of a local instance to be not in use.
200
201    Args:
202        name: The instance name.
203        delete_report: Report object.
204    """
205    ins_lock = list_instances.GetLocalInstanceLockByName(name)
206    if not ins_lock:
207        delete_report.AddError("%s is not a valid local instance name." % name)
208        delete_report.SetStatus(report.Status.FAIL)
209        return
210
211    if not ins_lock.Lock():
212        delete_report.AddError("%s is locked by another process." % name)
213        delete_report.SetStatus(report.Status.FAIL)
214        return
215
216    try:
217        ins_lock.SetInUse(False)
218        delete_report.SetStatus(report.Status.SUCCESS)
219        device_driver.AddDeletionResultToReport(
220            delete_report, [name], failed=[], error_msgs=[],
221            resource_name="instance")
222    finally:
223        ins_lock.Unlock()
224
225
226@utils.TimeExecute(function_description=("Deleting remote host goldfish "
227                                         "instance"),
228                   result_evaluator=utils.ReportEvaluator)
229def DeleteHostGoldfishInstance(cfg, name, ssh_user,
230                               ssh_private_key_path, delete_report):
231    """Delete a goldfish instance on a remote host by console command.
232
233    Args:
234        cfg: An AcloudConfig object.
235        name: String, the instance name.
236        remote_host : String, the IP address of the host.
237        ssh_user: String or None, the ssh user for the host.
238        ssh_private_key_path: String or None, the ssh private key for the host.
239        delete_report: A Report object.
240
241    Returns:
242        delete_report.
243    """
244    ip_addr, port = goldfish_utils.ParseRemoteHostConsoleAddress(name)
245    try:
246        with emulator_console.RemoteEmulatorConsole(
247                ip_addr, port,
248                (ssh_user or constants.GCE_USER),
249                (ssh_private_key_path or cfg.ssh_private_key_path),
250                cfg.extra_args_ssh_tunnel) as console:
251            console.Kill()
252        delete_report.SetStatus(report.Status.SUCCESS)
253        device_driver.AddDeletionResultToReport(
254            delete_report, [name], failed=[], error_msgs=[],
255            resource_name="instance")
256    except errors.DeviceConnectionError as e:
257        delete_report.AddError("%s is not deleted: %s" % (name, str(e)))
258        delete_report.SetStatus(report.Status.FAIL)
259    return delete_report
260
261
262@utils.TimeExecute(function_description=("Deleting remote host cuttlefish "
263                                         "instance"),
264                   result_evaluator=utils.ReportEvaluator)
265def CleanUpRemoteHost(cfg, remote_host, host_user, host_ssh_private_key_path,
266                      base_dir, delete_report):
267    """Clean up the remote host.
268
269    Args:
270        cfg: An AcloudConfig instance.
271        remote_host : String, ip address or host name of the remote host.
272        host_user: String of user login into the instance.
273        host_ssh_private_key_path: String of host key for logging in to the
274                                   host.
275        base_dir: String, the base directory on the remote host.
276        delete_report: A Report object.
277
278    Returns:
279        delete_report.
280    """
281    ssh_obj = ssh.Ssh(
282        ip=ssh.IP(ip=remote_host),
283        user=host_user,
284        ssh_private_key_path=(
285            host_ssh_private_key_path or cfg.ssh_private_key_path))
286    try:
287        cvd_utils.CleanUpRemoteCvd(ssh_obj, base_dir, raise_error=True)
288        delete_report.SetStatus(report.Status.SUCCESS)
289        device_driver.AddDeletionResultToReport(
290            delete_report, [remote_host], failed=[],
291            error_msgs=[],
292            resource_name="remote host")
293    except subprocess.CalledProcessError as e:
294        delete_report.AddError(str(e))
295        delete_report.SetStatus(report.Status.FAIL)
296    return delete_report
297
298
299def DeleteInstanceByNames(cfg, instances, host_user,
300                          host_ssh_private_key_path):
301    """Delete instances by the given instance names.
302
303    This method can identify the following types of instance names:
304    local cuttlefish instance: local-instance-<id>
305    local goldfish instance: local-goldfish-instance-<id>
306    remote host cuttlefish instance: host-<ip_addr>-<build_info>
307    remote host goldfish instance: host-goldfish-<ip_addr>-<port>-<build_info>
308    remote instance: ins-<uuid>-<build_info>
309
310    Args:
311        cfg: AcloudConfig object.
312        instances: List of instance name.
313        host_user: String or None, the ssh user for remote hosts.
314        host_ssh_private_key_path: String or None, the ssh private key for
315                                   remote hosts.
316
317    Returns:
318        A Report instance.
319    """
320    delete_report = report.Report(command="delete")
321    local_names = set(name for name in instances if
322                      name.startswith(_LOCAL_INSTANCE_PREFIX))
323    remote_host_cf_names = set(
324        name for name in instances if cvd_utils.ParseRemoteHostAddress(name))
325    remote_host_gf_names = set(
326        name for name in instances if
327        goldfish_utils.ParseRemoteHostConsoleAddress(name))
328    remote_names = list(set(instances) - local_names - remote_host_cf_names -
329                        remote_host_gf_names)
330
331    if local_names:
332        active_instances = list_instances.GetLocalInstancesByNames(local_names)
333        inactive_names = local_names.difference(ins.name for ins in
334                                                active_instances)
335        if active_instances:
336            utils.PrintColorString("Deleting local instances")
337            delete_report = DeleteInstances(cfg, active_instances)
338        if inactive_names:
339            utils.PrintColorString("Unlocking local instances")
340            for name in inactive_names:
341                ResetLocalInstanceLockByName(name, delete_report)
342
343    if remote_host_cf_names:
344        for name in remote_host_cf_names:
345            ip_addr, base_dir = cvd_utils.ParseRemoteHostAddress(name)
346            CleanUpRemoteHost(cfg, ip_addr, host_user,
347                              host_ssh_private_key_path, base_dir,
348                              delete_report)
349
350    if remote_host_gf_names:
351        for name in remote_host_gf_names:
352            DeleteHostGoldfishInstance(
353                cfg, name, host_user, host_ssh_private_key_path, delete_report)
354
355    if remote_names:
356        delete_report = DeleteRemoteInstances(cfg, remote_names, delete_report)
357    return delete_report
358
359
360def _ReleaseOxygenDevice(cfg, instances, ip):
361    """ Release one Oxygen device.
362
363    Args:
364        cfg: AcloudConfig object.
365        instances: List of instance name.
366        ip: String of device ip.
367
368    Returns:
369        A Report instance.
370    """
371    if len(instances) != 1:
372        raise errors.CommandArgError(
373            "The release device function doesn't support multiple instances. "
374            "Please check the specified instance names: %s" % instances)
375    instance_name = instances[0]
376    delete_report = report.Report(command="delete")
377    try:
378        oxygen_client.OxygenClient.ReleaseDevice(instance_name, ip,
379                                                 cfg.oxygen_client)
380        delete_report.SetStatus(report.Status.SUCCESS)
381        device_driver.AddDeletionResultToReport(
382            delete_report, [instance_name], failed=[],
383            error_msgs=[],
384            resource_name="instance")
385    except subprocess.CalledProcessError as e:
386        logger.error("Failed to release device from Oxygen, error: %s",
387            e.output)
388        error = str(e)
389        match = _RE_OXYGEN_RELEASE_ERROR.match(e.output)
390        if match:
391            error = match.group("error").strip()
392        delete_report.AddError(error)
393        delete_report.SetErrorType(constants.ACLOUD_OXYGEN_RELEASE_ERROR)
394        delete_report.SetStatus(report.Status.FAIL)
395    return delete_report
396
397
398def Run(args):
399    """Run delete.
400
401    After delete command executed, tool will return one Report instance.
402    If there is no instance to delete, just reutrn empty Report.
403
404    Args:
405        args: Namespace object from argparse.parse_args.
406
407    Returns:
408        A Report instance.
409    """
410    # Prioritize delete instances by names without query all instance info from
411    # GCP project.
412    cfg = config.GetAcloudConfig(args)
413    if args.oxygen:
414        return _ReleaseOxygenDevice(cfg, args.instance_names, args.ip)
415    if args.instance_names:
416        return DeleteInstanceByNames(cfg,
417                                     args.instance_names,
418                                     args.host_user,
419                                     args.host_ssh_private_key_path)
420    if args.remote_host:
421        delete_report = report.Report(command="delete")
422        CleanUpRemoteHost(cfg, args.remote_host, args.host_user,
423                          args.host_ssh_private_key_path,
424                          cvd_utils.GetRemoteHostBaseDir(1),
425                          delete_report)
426        return delete_report
427
428    instances = list_instances.GetLocalInstances()
429    if not args.local_only and cfg.SupportRemoteInstance():
430        instances.extend(list_instances.GetRemoteInstances(cfg))
431
432    if args.adb_port:
433        instances = list_instances.FilterInstancesByAdbPort(instances,
434                                                            args.adb_port)
435    elif not args.all:
436        # Provide instances list to user and let user choose what to delete if
437        # user didn't specify instances in args.
438        instances = list_instances.ChooseInstancesFromList(instances)
439
440    if not instances:
441        utils.PrintColorString("No instances to delete")
442    return DeleteInstances(cfg, instances)
443