github.com/juju/juju@v0.0.0-20240327075706-a90865de2538/acceptancetests/winazurearm.py (about) 1 #!/usr/bin/python 2 3 from __future__ import print_function 4 5 from argparse import ArgumentParser 6 from datetime import ( 7 datetime, 8 timedelta, 9 ) 10 import fnmatch 11 import logging 12 import os 13 import sys 14 15 import pytz 16 17 __metaclass__ = type 18 19 20 AZURE_SUBSCRIPTION_ID = "AZURE_SUBSCRIPTION_ID" 21 AZURE_CLIENT_ID = "AZURE_CLIENT_ID" 22 AZURE_SECRET = "AZURE_SECRET" 23 AZURE_TENANT = "AZURE_TENANT" 24 25 DEFAULT_RESOURCE_PREFIX = 'default-' 26 JUJU_MACHINE_PREFIX = 'machine-' 27 OLD_MACHINE_AGE = 6 28 29 30 # The azure lib is very chatty even at the info level. This logger 31 # strictly reports the activity of this script. 32 log = logging.getLogger("winazurearm") 33 handler = logging.StreamHandler(sys.stderr) 34 handler.setFormatter(logging.Formatter( 35 fmt='%(asctime)s %(levelname)s %(message)s', 36 datefmt='%Y-%m-%d %H:%M:%S')) 37 log.addHandler(handler) 38 39 40 class ARMClient: 41 """A collection of Azure RM clients.""" 42 43 def __init__(self, subscription_id, client_id, secret, tenant, 44 read_only=False): 45 self.subscription_id = subscription_id 46 self.client_id = client_id 47 self.secret = secret 48 self.tenant = tenant 49 self.read_only = read_only 50 self.credentials = None 51 self.resource = None 52 self.compute = None 53 54 def __eq__(self, other): 55 # Testing is the common case for checking equality. 56 return ( 57 type(other) == type(self) and 58 self.subscription_id == other.subscription_id and 59 self.client_id == other.client_id and 60 self.secret == other.secret and 61 self.tenant == other.tenant and 62 self.read_only == other.read_only) 63 64 def init_services(self): 65 """Delay imports and activation of Azure RM services until needed.""" 66 from azure.common.credentials import ServicePrincipalCredentials 67 from azure.mgmt.resource.resources import ResourceManagementClient 68 from azure.mgmt.compute import ComputeManagementClient 69 self.credentials = ServicePrincipalCredentials( 70 client_id=self.client_id, secret=self.secret, tenant=self.tenant) 71 self.resource = ResourceManagementClient( 72 self.credentials, self.subscription_id) 73 self.compute = ComputeManagementClient( 74 self.credentials, self.subscription_id) 75 76 77 class ResourceGroupDetails: 78 79 def __init__(self, client, group, deployments=None): 80 self.client = client 81 self.is_loaded = False 82 self.group = group 83 self.deployments = deployments 84 85 def __eq__(self, other): 86 # Testing is the common case for checking equality. 87 return ( 88 type(other) == type(self) and 89 self.client == other.client and 90 self.is_loaded is other.is_loaded and 91 self.group is other.group and 92 self.deployments == other.deployments) 93 94 @property 95 def name(self): 96 return self.group.name 97 98 def load_details(self): 99 self.deployments = list( 100 self.client.resource.deployments.list(self.name)) 101 self.is_loaded = True 102 103 def print_out(self, recursive=False): 104 print(self.name) 105 if recursive: 106 for deployment in self.deployments: 107 print(' Deployment {}'.format(deployment.name)) 108 109 def is_old(self, now, old_age): 110 """Return True if the resource group is old. 111 112 :param now: The datetime object that is the basis for old age. 113 :param old_age: The age of the resource group to must be. 114 """ 115 if old_age == 0: 116 # In the case of O hours old, the caller is stating any resource 117 # group that exists is old. 118 return True 119 ago = timedelta(hours=old_age) 120 if not self.deployments: 121 # Juju resource groups have at least one deployment, so we can use 122 # the timestamp of the oldest deployment in the group as the 123 # group's age. If there are no deployments, we don't consider it to 124 # be a valid group. 125 log.debug('{} has no deployments'.format(self.name)) 126 return False 127 creation_time = min([d.properties.timestamp for d in self.deployments]) 128 age = now - creation_time 129 if age > ago: 130 hours_old = (age.total_seconds() // 3600) 131 log.debug('{} is {} hours old:'.format(self.name, hours_old)) 132 log.debug(' {}'.format(creation_time)) 133 return True 134 return False 135 136 def delete(self): 137 """Delete the resource group and all subordinate resources. 138 139 Returns a AzureOperationPoller. 140 """ 141 return self.client.resource.resource_groups.delete(self.name) 142 143 def delete_vm(self, name): 144 """Delete the VirtualMachine. 145 146 Returns a AzureOperationPoller. 147 """ 148 return self.client.compute.virtual_machines.delete(self.name, name) 149 150 151 def list_resources(client, glob='*', recursive=False, print_out=False): 152 """Return a list of ResourceGroupDetails. 153 154 Use print_out=True to print a listing of resources. 155 156 :param client: The ARMClient. 157 :param glob: The glob to find matching resource groups to delete. 158 :param recursive: Get the resources in the resource group? 159 :param print_out: Print the found resources to STDOUT? 160 :return: A list of ResourceGroupDetails 161 """ 162 resource_groups = list(iter_resources(client, glob, recursive)) 163 if print_out: 164 for group in resource_groups: 165 group.print_out(recursive=recursive) 166 return resource_groups 167 168 169 def iter_resources(client, glob='*', recursive=False): 170 """Return an iterator of ResourceGroupDetails. 171 172 :param client: The ARMClient. 173 :param glob: The glob to find matching resource groups to delete. 174 :param recursive: Get the resources in the resource group? 175 :return: An iterator of ResourceGroupDetails 176 """ 177 resource_groups = client.resource.resource_groups.list() 178 for group in resource_groups: 179 if group.name.lower().startswith(DEFAULT_RESOURCE_PREFIX): 180 # This is not a resource group. Use the UI to delete Default 181 # resources. 182 log.debug('Skipping {}'.format(group.name)) 183 continue 184 if not fnmatch.fnmatch(group.name, glob): 185 log.debug('Skipping {}'.format(group.name)) 186 continue 187 rgd = ResourceGroupDetails(client, group) 188 if recursive: 189 print(' - loading {}'.format(group.name)) 190 rgd.load_details() 191 yield rgd 192 193 194 def delete_resources(client, glob='*', old_age=OLD_MACHINE_AGE, now=None): 195 """Delete old resource groups and return the number deleted. 196 197 :param client: The ARMClient. 198 :param glob: The glob to find matching resource groups to delete. 199 :param old_age: The age of the resource group to delete. 200 :param now: The datetime object that is the basis for old age. 201 """ 202 if not now: 203 now = datetime.now(pytz.utc) 204 resources = list_resources(client, glob=glob, recursive=True) 205 pollers = [] 206 deleted_count = 0 207 for rgd in resources: 208 name = rgd.name 209 if not rgd.is_old(now, old_age): 210 continue 211 log.debug('Deleting {}'.format(name)) 212 if not client.read_only: 213 poller = rgd.delete() 214 deleted_count += 1 215 if poller: 216 pollers.append((name, poller)) 217 else: 218 # Deleting a group created using the old API might not return 219 # a poller! Or maybe the resource was deleting already. 220 log.debug( 221 'poller is None for {}.delete(). Already deleted?'.format( 222 name)) 223 for name, poller in pollers: 224 log.debug('Waiting for {} to be deleted'.format(name)) 225 # It is an error to ask for a poller's result() when it is done. 226 # Calling result() makes the poller wait for done, but the result 227 # of a delete operation is None. 228 if not poller.done(): 229 poller.result() 230 return deleted_count 231 232 233 def find_vm_deployment(resource_group, name): 234 """Return a matching DeploymentExtended, or None. 235 236 Juju 2.x shows the machine's name in the resource group as the instance_id. 237 238 :param resource_group: A ResourceGroupDetails. 239 :param name: The name of a VM instance to find. 240 :return: A DeploymentExtended 241 """ 242 if not name.startswith(JUJU_MACHINE_PREFIX): 243 return None 244 for d in resource_group.deployments: 245 if d.name == name: 246 return d 247 return None 248 249 250 def delete_instance(client, name_id, resource_group=None): 251 """Delete a VM instance. 252 253 When resource_group is provided, VM name is used to locate the VM. 254 Otherwise, all resource groups are searched for a matching VM id. 255 256 :param name_id: The name or id of a VM instance. 257 :param resource_group: The optional name of the resource group the 258 VM belongs to. 259 """ 260 if resource_group: 261 glob = resource_group 262 else: 263 glob = '*' 264 resource_groups = iter_resources(client, glob=glob, recursive=True) 265 group_names = [] 266 for resource_group in resource_groups: 267 group_names.append(resource_group.name) 268 deployment = find_vm_deployment(resource_group, name_id) 269 if deployment: 270 log.debug( 271 'Found {} {}'.format(resource_group.name, deployment.name)) 272 if not client.read_only: 273 poller = resource_group.delete_vm(deployment.name) 274 log.debug( 275 'Waiting for {} to be deleted'.format(deployment.name)) 276 if not poller.done(): 277 poller.result() 278 return 279 else: 280 group_names = ', '.join(group_names) 281 raise ValueError( 282 'The vm name {} was not found in {}'.format(name_id, group_names)) 283 284 285 def parse_args(argv): 286 """Return the argument parser for this program.""" 287 parser = ArgumentParser(description='Query and manage azure.') 288 parser.add_argument( 289 '-d', '--dry-run', action='store_true', default=False, 290 help='Do not make changes.') 291 parser.add_argument( 292 '-v', '--verbose', action='store_const', 293 default=logging.INFO, const=logging.DEBUG, 294 help='Verbose test harness output.') 295 parser.add_argument( 296 '--subscription-id', 297 help=("The subscription id to make requests with. " 298 "Environment: $AZURE_SUBSCRIPTION_ID."), 299 default=os.environ.get(AZURE_SUBSCRIPTION_ID)) 300 parser.add_argument( 301 '--client-id', 302 help=("The client id to make requests with. " 303 "Environment: $AZURE_CLIENT_ID."), 304 default=os.environ.get(AZURE_CLIENT_ID)) 305 parser.add_argument( 306 '--secret', 307 help=("The secret to make requests with. " 308 "Environment: $AZURE_SECRET."), 309 default=os.environ.get(AZURE_SECRET)) 310 parser.add_argument( 311 '--tenant', 312 help=("The tenant to make requests with. " 313 "Environment: $AZURE_TENANT."), 314 default=os.environ.get(AZURE_TENANT)) 315 subparsers = parser.add_subparsers(help='sub-command help', dest="command") 316 ls_parser = subparsers.add_parser( 317 'list-resources', help='List resource groups.') 318 ls_parser.add_argument( 319 '-r', '--recursive', default=False, action='store_true', 320 help='Show resources with a resources group.') 321 ls_parser.add_argument( 322 'filter', default='*', nargs='?', 323 help='A glob pattern to match services to.') 324 dr_parser = subparsers.add_parser( 325 'delete-resources', 326 help='delete old resource groups and their vm, networks, etc.') 327 dr_parser.add_argument( 328 '-o', '--old-age', default=OLD_MACHINE_AGE, type=int, 329 help='Set old machine age to n hours.') 330 dr_parser.add_argument( 331 'filter', default='*', nargs='?', 332 help='A glob pattern to select resource groups to delete.') 333 di_parser = subparsers.add_parser('delete-instance', help='Delete a vm.') 334 di_parser.add_argument( 335 'name_id', help='The name or id of an instance (name needs group).') 336 di_parser.add_argument( 337 'resource_group', default=None, nargs='?', 338 help='The resource-group name of the machine name.') 339 args = parser.parse_args(argv[1:]) 340 if not all( 341 [args.subscription_id, args.client_id, args.secret, args.tenant]): 342 log.error("$AZURE_SUBSCRIPTION_ID, $AZURE_CLIENT_ID, $AZURE_SECRET, " 343 "$AZURE_TENANT was not provided.") 344 return args 345 346 347 def main(argv): 348 args = parse_args(argv) 349 log.setLevel(args.verbose) 350 client = ARMClient( 351 args.subscription_id, args.client_id, args.secret, args.tenant, 352 read_only=args.dry_run) 353 client.init_services() 354 try: 355 if args.command == 'list-resources': 356 list_resources( 357 client, glob=args.filter, recursive=args.recursive, 358 print_out=True) 359 elif args.command == 'delete-resources': 360 delete_resources(client, glob=args.filter, old_age=args.old_age) 361 elif args.command == 'delete-instance': 362 delete_instance( 363 client, args.name_id, resource_group=args.resource_group) 364 except Exception as e: 365 print(e) 366 return 1 367 return 0 368 369 370 if __name__ == '__main__': 371 sys.exit(main(sys.argv))