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