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))