443 lines
16 KiB
Python
443 lines
16 KiB
Python
# Copyright 2018 - The Android Open Source Project
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
r"""Delete entry point.
|
|
|
|
Delete will handle all the logic related to deleting a local/remote instance
|
|
of an Android Virtual Device.
|
|
"""
|
|
|
|
from __future__ import print_function
|
|
|
|
import logging
|
|
import re
|
|
import subprocess
|
|
|
|
from acloud import errors
|
|
from acloud.internal import constants
|
|
from acloud.internal.lib import cvd_utils
|
|
from acloud.internal.lib import emulator_console
|
|
from acloud.internal.lib import goldfish_utils
|
|
from acloud.internal.lib import oxygen_client
|
|
from acloud.internal.lib import ssh
|
|
from acloud.internal.lib import utils
|
|
from acloud.list import list as list_instances
|
|
from acloud.public import config
|
|
from acloud.public import device_driver
|
|
from acloud.public import report
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
_COMMAND_GET_PROCESS_ID = ["pgrep", "run_cvd"]
|
|
_COMMAND_GET_PROCESS_COMMAND = ["ps", "-o", "command", "-p"]
|
|
_RE_RUN_CVD = re.compile(r"^(?P<run_cvd>.+run_cvd)")
|
|
_LOCAL_INSTANCE_PREFIX = "local-"
|
|
_RE_OXYGEN_RELEASE_ERROR = re.compile(
|
|
r".*Error received while trying to release device: (?P<error>.*)$", re.DOTALL)
|
|
|
|
|
|
def DeleteInstances(cfg, instances_to_delete):
|
|
"""Delete instances according to instances_to_delete.
|
|
|
|
Args:
|
|
cfg: AcloudConfig object.
|
|
instances_to_delete: List of list.Instance() object.
|
|
|
|
Returns:
|
|
Report object.
|
|
"""
|
|
delete_report = report.Report(command="delete")
|
|
remote_instance_list = []
|
|
for instance in instances_to_delete:
|
|
if instance.islocal:
|
|
if instance.avd_type == constants.TYPE_GF:
|
|
DeleteLocalGoldfishInstance(instance, delete_report)
|
|
elif instance.avd_type == constants.TYPE_CF:
|
|
DeleteLocalCuttlefishInstance(instance, delete_report)
|
|
else:
|
|
delete_report.AddError("Deleting %s is not supported." %
|
|
instance.avd_type)
|
|
delete_report.SetStatus(report.Status.FAIL)
|
|
else:
|
|
remote_instance_list.append(instance.name)
|
|
# Delete ssvnc viewer
|
|
if instance.vnc_port:
|
|
utils.CleanupSSVncviewer(instance.vnc_port)
|
|
|
|
if remote_instance_list:
|
|
# TODO(119283708): We should move DeleteAndroidVirtualDevices into
|
|
# delete.py after gce is deprecated.
|
|
# Stop remote instances.
|
|
return DeleteRemoteInstances(cfg, remote_instance_list, delete_report)
|
|
|
|
return delete_report
|
|
|
|
|
|
@utils.TimeExecute(function_description="Deleting remote instances",
|
|
result_evaluator=utils.ReportEvaluator,
|
|
display_waiting_dots=False)
|
|
def DeleteRemoteInstances(cfg, instances_to_delete, delete_report=None):
|
|
"""Delete remote instances.
|
|
|
|
Args:
|
|
cfg: AcloudConfig object.
|
|
instances_to_delete: List of instance names(string).
|
|
delete_report: Report object.
|
|
|
|
Returns:
|
|
Report instance if there are instances to delete, None otherwise.
|
|
|
|
Raises:
|
|
error.ConfigError: when config doesn't support remote instances.
|
|
"""
|
|
if not cfg.SupportRemoteInstance():
|
|
raise errors.ConfigError("No gcp project info found in config! "
|
|
"The execution of deleting remote instances "
|
|
"has been aborted.")
|
|
utils.PrintColorString("")
|
|
for instance in instances_to_delete:
|
|
utils.PrintColorString(" - %s" % instance, utils.TextColors.WARNING)
|
|
utils.PrintColorString("")
|
|
utils.PrintColorString("status: waiting...", end="")
|
|
|
|
# TODO(119283708): We should move DeleteAndroidVirtualDevices into
|
|
# delete.py after gce is deprecated.
|
|
# Stop remote instances.
|
|
delete_report = device_driver.DeleteAndroidVirtualDevices(
|
|
cfg, instances_to_delete, delete_report)
|
|
|
|
return delete_report
|
|
|
|
|
|
@utils.TimeExecute(function_description="Deleting local cuttlefish instances",
|
|
result_evaluator=utils.ReportEvaluator)
|
|
def DeleteLocalCuttlefishInstance(instance, delete_report):
|
|
"""Delete a local cuttlefish instance.
|
|
|
|
Delete local instance and write delete instance
|
|
information to report.
|
|
|
|
Args:
|
|
instance: instance.LocalInstance object.
|
|
delete_report: Report object.
|
|
|
|
Returns:
|
|
delete_report.
|
|
"""
|
|
ins_lock = instance.GetLock()
|
|
if not ins_lock.Lock():
|
|
delete_report.AddError("%s is locked by another process." %
|
|
instance.name)
|
|
delete_report.SetStatus(report.Status.FAIL)
|
|
return delete_report
|
|
|
|
try:
|
|
ins_lock.SetInUse(False)
|
|
instance.Delete()
|
|
delete_report.SetStatus(report.Status.SUCCESS)
|
|
device_driver.AddDeletionResultToReport(
|
|
delete_report, [instance.name], failed=[],
|
|
error_msgs=[],
|
|
resource_name="instance")
|
|
except subprocess.CalledProcessError as e:
|
|
delete_report.AddError(str(e))
|
|
delete_report.SetStatus(report.Status.FAIL)
|
|
finally:
|
|
ins_lock.Unlock()
|
|
|
|
return delete_report
|
|
|
|
|
|
@utils.TimeExecute(function_description="Deleting local goldfish instances",
|
|
result_evaluator=utils.ReportEvaluator)
|
|
def DeleteLocalGoldfishInstance(instance, delete_report):
|
|
"""Delete a local goldfish instance.
|
|
|
|
Args:
|
|
instance: LocalGoldfishInstance object.
|
|
delete_report: Report object.
|
|
|
|
Returns:
|
|
delete_report.
|
|
"""
|
|
lock = instance.GetLock()
|
|
if not lock.Lock():
|
|
delete_report.AddError("%s is locked by another process." %
|
|
instance.name)
|
|
delete_report.SetStatus(report.Status.FAIL)
|
|
return delete_report
|
|
|
|
try:
|
|
lock.SetInUse(False)
|
|
if instance.adb.EmuCommand("kill") == 0:
|
|
delete_report.SetStatus(report.Status.SUCCESS)
|
|
device_driver.AddDeletionResultToReport(
|
|
delete_report, [instance.name], failed=[],
|
|
error_msgs=[],
|
|
resource_name="instance")
|
|
else:
|
|
delete_report.AddError("Cannot kill %s." % instance.device_serial)
|
|
delete_report.SetStatus(report.Status.FAIL)
|
|
finally:
|
|
lock.Unlock()
|
|
|
|
return delete_report
|
|
|
|
|
|
def ResetLocalInstanceLockByName(name, delete_report):
|
|
"""Set the lock state of a local instance to be not in use.
|
|
|
|
Args:
|
|
name: The instance name.
|
|
delete_report: Report object.
|
|
"""
|
|
ins_lock = list_instances.GetLocalInstanceLockByName(name)
|
|
if not ins_lock:
|
|
delete_report.AddError("%s is not a valid local instance name." % name)
|
|
delete_report.SetStatus(report.Status.FAIL)
|
|
return
|
|
|
|
if not ins_lock.Lock():
|
|
delete_report.AddError("%s is locked by another process." % name)
|
|
delete_report.SetStatus(report.Status.FAIL)
|
|
return
|
|
|
|
try:
|
|
ins_lock.SetInUse(False)
|
|
delete_report.SetStatus(report.Status.SUCCESS)
|
|
device_driver.AddDeletionResultToReport(
|
|
delete_report, [name], failed=[], error_msgs=[],
|
|
resource_name="instance")
|
|
finally:
|
|
ins_lock.Unlock()
|
|
|
|
|
|
@utils.TimeExecute(function_description=("Deleting remote host goldfish "
|
|
"instance"),
|
|
result_evaluator=utils.ReportEvaluator)
|
|
def DeleteHostGoldfishInstance(cfg, name, ssh_user,
|
|
ssh_private_key_path, delete_report):
|
|
"""Delete a goldfish instance on a remote host by console command.
|
|
|
|
Args:
|
|
cfg: An AcloudConfig object.
|
|
name: String, the instance name.
|
|
remote_host : String, the IP address of the host.
|
|
ssh_user: String or None, the ssh user for the host.
|
|
ssh_private_key_path: String or None, the ssh private key for the host.
|
|
delete_report: A Report object.
|
|
|
|
Returns:
|
|
delete_report.
|
|
"""
|
|
ip_addr, port = goldfish_utils.ParseRemoteHostConsoleAddress(name)
|
|
try:
|
|
with emulator_console.RemoteEmulatorConsole(
|
|
ip_addr, port,
|
|
(ssh_user or constants.GCE_USER),
|
|
(ssh_private_key_path or cfg.ssh_private_key_path),
|
|
cfg.extra_args_ssh_tunnel) as console:
|
|
console.Kill()
|
|
delete_report.SetStatus(report.Status.SUCCESS)
|
|
device_driver.AddDeletionResultToReport(
|
|
delete_report, [name], failed=[], error_msgs=[],
|
|
resource_name="instance")
|
|
except errors.DeviceConnectionError as e:
|
|
delete_report.AddError("%s is not deleted: %s" % (name, str(e)))
|
|
delete_report.SetStatus(report.Status.FAIL)
|
|
return delete_report
|
|
|
|
|
|
@utils.TimeExecute(function_description=("Deleting remote host cuttlefish "
|
|
"instance"),
|
|
result_evaluator=utils.ReportEvaluator)
|
|
def CleanUpRemoteHost(cfg, remote_host, host_user, host_ssh_private_key_path,
|
|
base_dir, delete_report):
|
|
"""Clean up the remote host.
|
|
|
|
Args:
|
|
cfg: An AcloudConfig instance.
|
|
remote_host : String, ip address or host name of the remote host.
|
|
host_user: String of user login into the instance.
|
|
host_ssh_private_key_path: String of host key for logging in to the
|
|
host.
|
|
base_dir: String, the base directory on the remote host.
|
|
delete_report: A Report object.
|
|
|
|
Returns:
|
|
delete_report.
|
|
"""
|
|
ssh_obj = ssh.Ssh(
|
|
ip=ssh.IP(ip=remote_host),
|
|
user=host_user,
|
|
ssh_private_key_path=(
|
|
host_ssh_private_key_path or cfg.ssh_private_key_path))
|
|
try:
|
|
cvd_utils.CleanUpRemoteCvd(ssh_obj, base_dir, raise_error=True)
|
|
delete_report.SetStatus(report.Status.SUCCESS)
|
|
device_driver.AddDeletionResultToReport(
|
|
delete_report, [remote_host], failed=[],
|
|
error_msgs=[],
|
|
resource_name="remote host")
|
|
except subprocess.CalledProcessError as e:
|
|
delete_report.AddError(str(e))
|
|
delete_report.SetStatus(report.Status.FAIL)
|
|
return delete_report
|
|
|
|
|
|
def DeleteInstanceByNames(cfg, instances, host_user,
|
|
host_ssh_private_key_path):
|
|
"""Delete instances by the given instance names.
|
|
|
|
This method can identify the following types of instance names:
|
|
local cuttlefish instance: local-instance-<id>
|
|
local goldfish instance: local-goldfish-instance-<id>
|
|
remote host cuttlefish instance: host-<ip_addr>-<build_info>
|
|
remote host goldfish instance: host-goldfish-<ip_addr>-<port>-<build_info>
|
|
remote instance: ins-<uuid>-<build_info>
|
|
|
|
Args:
|
|
cfg: AcloudConfig object.
|
|
instances: List of instance name.
|
|
host_user: String or None, the ssh user for remote hosts.
|
|
host_ssh_private_key_path: String or None, the ssh private key for
|
|
remote hosts.
|
|
|
|
Returns:
|
|
A Report instance.
|
|
"""
|
|
delete_report = report.Report(command="delete")
|
|
local_names = set(name for name in instances if
|
|
name.startswith(_LOCAL_INSTANCE_PREFIX))
|
|
remote_host_cf_names = set(
|
|
name for name in instances if cvd_utils.ParseRemoteHostAddress(name))
|
|
remote_host_gf_names = set(
|
|
name for name in instances if
|
|
goldfish_utils.ParseRemoteHostConsoleAddress(name))
|
|
remote_names = list(set(instances) - local_names - remote_host_cf_names -
|
|
remote_host_gf_names)
|
|
|
|
if local_names:
|
|
active_instances = list_instances.GetLocalInstancesByNames(local_names)
|
|
inactive_names = local_names.difference(ins.name for ins in
|
|
active_instances)
|
|
if active_instances:
|
|
utils.PrintColorString("Deleting local instances")
|
|
delete_report = DeleteInstances(cfg, active_instances)
|
|
if inactive_names:
|
|
utils.PrintColorString("Unlocking local instances")
|
|
for name in inactive_names:
|
|
ResetLocalInstanceLockByName(name, delete_report)
|
|
|
|
if remote_host_cf_names:
|
|
for name in remote_host_cf_names:
|
|
ip_addr, base_dir = cvd_utils.ParseRemoteHostAddress(name)
|
|
CleanUpRemoteHost(cfg, ip_addr, host_user,
|
|
host_ssh_private_key_path, base_dir,
|
|
delete_report)
|
|
|
|
if remote_host_gf_names:
|
|
for name in remote_host_gf_names:
|
|
DeleteHostGoldfishInstance(
|
|
cfg, name, host_user, host_ssh_private_key_path, delete_report)
|
|
|
|
if remote_names:
|
|
delete_report = DeleteRemoteInstances(cfg, remote_names, delete_report)
|
|
return delete_report
|
|
|
|
|
|
def _ReleaseOxygenDevice(cfg, instances, ip):
|
|
""" Release one Oxygen device.
|
|
|
|
Args:
|
|
cfg: AcloudConfig object.
|
|
instances: List of instance name.
|
|
ip: String of device ip.
|
|
|
|
Returns:
|
|
A Report instance.
|
|
"""
|
|
if len(instances) != 1:
|
|
raise errors.CommandArgError(
|
|
"The release device function doesn't support multiple instances. "
|
|
"Please check the specified instance names: %s" % instances)
|
|
instance_name = instances[0]
|
|
delete_report = report.Report(command="delete")
|
|
try:
|
|
oxygen_client.OxygenClient.ReleaseDevice(instance_name, ip,
|
|
cfg.oxygen_client)
|
|
delete_report.SetStatus(report.Status.SUCCESS)
|
|
device_driver.AddDeletionResultToReport(
|
|
delete_report, [instance_name], failed=[],
|
|
error_msgs=[],
|
|
resource_name="instance")
|
|
except subprocess.CalledProcessError as e:
|
|
logger.error("Failed to release device from Oxygen, error: %s",
|
|
e.output)
|
|
error = str(e)
|
|
match = _RE_OXYGEN_RELEASE_ERROR.match(e.output)
|
|
if match:
|
|
error = match.group("error").strip()
|
|
delete_report.AddError(error)
|
|
delete_report.SetErrorType(constants.ACLOUD_OXYGEN_RELEASE_ERROR)
|
|
delete_report.SetStatus(report.Status.FAIL)
|
|
return delete_report
|
|
|
|
|
|
def Run(args):
|
|
"""Run delete.
|
|
|
|
After delete command executed, tool will return one Report instance.
|
|
If there is no instance to delete, just reutrn empty Report.
|
|
|
|
Args:
|
|
args: Namespace object from argparse.parse_args.
|
|
|
|
Returns:
|
|
A Report instance.
|
|
"""
|
|
# Prioritize delete instances by names without query all instance info from
|
|
# GCP project.
|
|
cfg = config.GetAcloudConfig(args)
|
|
if args.oxygen:
|
|
return _ReleaseOxygenDevice(cfg, args.instance_names, args.ip)
|
|
if args.instance_names:
|
|
return DeleteInstanceByNames(cfg,
|
|
args.instance_names,
|
|
args.host_user,
|
|
args.host_ssh_private_key_path)
|
|
if args.remote_host:
|
|
delete_report = report.Report(command="delete")
|
|
CleanUpRemoteHost(cfg, args.remote_host, args.host_user,
|
|
args.host_ssh_private_key_path,
|
|
cvd_utils.GetRemoteHostBaseDir(1),
|
|
delete_report)
|
|
return delete_report
|
|
|
|
instances = list_instances.GetLocalInstances()
|
|
if not args.local_only and cfg.SupportRemoteInstance():
|
|
instances.extend(list_instances.GetRemoteInstances(cfg))
|
|
|
|
if args.adb_port:
|
|
instances = list_instances.FilterInstancesByAdbPort(instances,
|
|
args.adb_port)
|
|
elif not args.all:
|
|
# Provide instances list to user and let user choose what to delete if
|
|
# user didn't specify instances in args.
|
|
instances = list_instances.ChooseInstancesFromList(instances)
|
|
|
|
if not instances:
|
|
utils.PrintColorString("No instances to delete")
|
|
return DeleteInstances(cfg, instances)
|