diff --git a/awx/main/management/commands/_base_instance.py b/awx/main/management/commands/_base_instance.py new file mode 100644 index 0000000000..8731744e8b --- /dev/null +++ b/awx/main/management/commands/_base_instance.py @@ -0,0 +1,139 @@ +from django.core.management.base import BaseCommand, CommandError +from optparse import make_option +from django.conf import settings + +class OptionEnforceError(Exception): + def __init__(self, value): + self.value = value + def __str__(self): + return repr(self.value) + +class BaseCommandInstance(BaseCommand): + #option_list = BaseCommand.option_list + + def __init__(self): + super(BaseCommandInstance, self).__init__() + self.enforce_roles = False + self.enforce_hostname_set = False + self.enforce_unique_find = False + + self.option_primary = False + self.option_secondary = False + self.option_hostname = None + self.option_uuid = None + + self.UUID = settings.SYSTEM_UUID + self.unique_fields = {} + + @staticmethod + def generate_option_hostname(): + return make_option('--hostname', + dest='hostname', + default='', + help='Find instance by specified hostname.') + + @staticmethod + def generate_option_hostname_set(): + return make_option('--hostname', + dest='hostname', + default='', + help='Hostname to assign to the new instance.') + + @staticmethod + def generate_option_primary(): + return make_option('--primary', + action='store_true', + default=False, + dest='primary', + help='Register instance as primary.') + + @staticmethod + def generate_option_secondary(): + return make_option('--secondary', + action='store_true', + default=False, + dest='secondary', + help='Register instance as secondary.') + + @staticmethod + def generate_option_uuid(): + return make_option('--uuid', + dest='uuid', + default='', + help='Find instance by specified uuid.') + + def include_options_roles(self): + BaseCommand.option_list += ( BaseCommandInstance.generate_option_primary(), BaseCommandInstance.generate_option_secondary(), ) + self.enforce_roles = True + + def include_option_hostname_set(self): + BaseCommand.option_list += ( BaseCommandInstance.generate_option_hostname_set(), ) + self.enforce_hostname_set = True + + def include_option_hostname_uuid_find(self): + BaseCommand.option_list += ( BaseCommandInstance.generate_option_hostname(), BaseCommandInstance.generate_option_uuid(), ) + self.enforce_unique_find = True + + def get_option_hostname(self): + return self.option_hostname + def get_option_uuid(self): + return self.option_uuid + def is_option_primary(self): + return self.option_primary + def is_option_secondary(self): + return self.option_secondary + def get_UUID(self): + return self.UUID + # for the enforce_unique_find policy + def get_unique_fields(self): + return self.unique_fields + + @property + def usage_error(self): + if self.enforce_roles and self.enforce_hostname_set: + return CommandError('--hostname and one of --primary or --secondary is required.') + elif self.enforce_hostname_set: + return CommandError('--hostname is required.') + elif self.enforce_roles: + return CommandError('One of --primary or --secondary is required.') + + def handle(self, *args, **options): + if self.enforce_hostname_set and self.enforce_unique_find: + raise OptionEnforceError('Can not enforce --hostname as a setter and --hostname as a getter') + + if self.enforce_roles: + self.option_primary = options['primary'] + self.option_secondary = options['secondary'] + + if self.is_option_primary() and self.is_option_secondary() or not (self.is_option_primary() or self.is_option_secondary()): + raise self.usage_error + + if self.enforce_hostname_set: + if options['hostname']: + self.option_hostname = options['hostname'] + else: + raise self.usage_error + + if self.enforce_unique_find: + if options['hostname']: + self.unique_fields['hostname'] = self.option_hostname = options['hostname'] + + if options['uuid']: + self.unique_fields['uuid'] = self.option_uuid = options['uuid'] + + if len(self.unique_fields) == 0: + self.unique_fields['uuid'] = self.get_UUID() + + @staticmethod + def __instance_str(instance, fields): + string = '(' + for field in fields: + string += '%s="%s",' % (field, getattr(instance, field)) + if len(fields) > 0: + string = string[:-1] + string += ')' + return string + + @staticmethod + def instance_str(instance): + return BaseCommandInstance.__instance_str(instance, ('uuid', 'hostname', 'role')) \ No newline at end of file diff --git a/awx/main/management/commands/list_instances.py b/awx/main/management/commands/list_instances.py index b225f66c46..c2a52178c0 100644 --- a/awx/main/management/commands/list_instances.py +++ b/awx/main/management/commands/list_instances.py @@ -5,15 +5,18 @@ from optparse import make_option from django.conf import settings from django.core.management.base import BaseCommand, CommandError +from awx.main.management.commands._base_instance import BaseCommandInstance +instance_str = BaseCommandInstance.instance_str from awx.main.models import Instance - -class Command(BaseCommand): +class Command(BaseCommandInstance): """List instances from the Tower database """ def handle(self, **options): + super(Command, self).__init__() + for instance in Instance.objects.all(): print("uuid: %s; hostname: %s; primary: %s; created: %s; modified: %s" % (instance.uuid, instance.hostname, instance.primary, instance.created, instance.modified)) diff --git a/awx/main/management/commands/register_instance.py b/awx/main/management/commands/register_instance.py index a11e2bacc4..179846624e 100644 --- a/awx/main/management/commands/register_instance.py +++ b/awx/main/management/commands/register_instance.py @@ -1,114 +1,61 @@ # Copyright (c) 2014 Ansible, Inc. # All Rights Reserved -from optparse import make_option - from django.conf import settings from django.core.management.base import BaseCommand, CommandError +from awx.main.management.commands._base_instance import BaseCommandInstance +instance_str = BaseCommandInstance.instance_str + from awx.main.models import Instance -class Command(BaseCommand): - """Regsiter this instance with the database for HA tracking. +class Command(BaseCommandInstance): + """Internal tower command. + Regsiter this instance with the database for HA tracking. - This command is idempotent. It will register the machine if it is not - yet present and do nothing if the machine is already registered. + This command is idempotent. - If a second primary machine is registered, the first primary machine will - become a secondary machine, and the newly registered primary machine - will be primary (unless `--timid` is provided, in which case the command - will simply error out). - - This command will also error out under the following circumstances: + This command will error out in the following conditions: * Attempting to register a secondary machine with no primary machines. - * Attempting to register this machine with a different state than its - existing registration plus `--timid`. + * Attempting to register a primary instance when a different primary + instance exists. + * Attempting to re-register an instance with changed values. """ - option_list = BaseCommand.option_list + ( - make_option('--timid', - action='store_true', - dest='timid', - help='Fail if the primary host specified is already registered as ' - 'another instance or there already exists a primary different ' - 'from the one specified.'), - make_option('--hostname', - dest='hostname', - default='', - help='instance to register'), - make_option('--primary', - action='store_true', - dest='primary', - help='register instance as primary'), - make_option('--secondary', - action='store_false', - dest='primary', - help='register instance as secondary'), - ) + def __init__(self): + super(Command, self).__init__() - def handle(self, **options): - uuid = settings.SYSTEM_UUID - timid = options['timid'] + self.include_options_roles() + self.include_option_hostname_set() - # Is there an existing record for this machine? - # If so, retrieve that record and look for issues. + def handle(self, *args, **options): + super(Command, self).handle(*args, **options) + + uuid = self.get_UUID() + + # Is there an existing record for this machine? If so, retrieve that record and look for issues. try: instance = Instance.objects.get(uuid=uuid) - existing = True + if instance.hostname != self.get_option_hostname(): + raise CommandError('Instance already registered with a different hostname %s.' % instance_str(instance)) + print("Instance already registered %s" % instance_str(instance)) except Instance.DoesNotExist: - instance = Instance(uuid=uuid) - existing = False + # Get a status on primary machines (excluding this one, regardless of its status). + other_instances = Instance.objects.exclude(uuid=uuid) + primaries = other_instances.filter(primary=True).count() - # Get a status on primary machines (excluding this one, regardless - # of its status). - other_instances = Instance.objects.exclude(uuid=uuid) - primaries = other_instances.filter(primary=True).count() + # If this instance is being set to primary and a *different* primary machine alreadyexists, error out. + if self.is_option_primary() and primaries: + raise CommandError('Another instance is already registered as primary.') - # Sanity check: If we're supposed to be being timid, then ensure - # that there's no crazy mosh pits happening here. - # - # If the primacy setting doesn't match what is already on - # the instance, error out. - if existing and timid and instance.primary != options['primary']: - raise CommandError('This instance is already registered as a ' - '%s instance.' % instance.role) + # Lastly, if there are no primary machines at all, then don't allow this to be registered as a secondary machine. + if self.is_option_secondary() and not primaries: + raise CommandError('Unable to register a secondary machine until another primary machine has been registered.') - # If this instance is being set to primary and a *different* primary - # machine alreadyexists, error out if we're supposed to be timid. - if timid and options['primary'] and primaries: - raise CommandError('Another instance is already registered as ' - 'primary.') + # Okay, we've checked for appropriate errata; perform the registration. + instance = Instance(uuid=uuid, primary=self.is_option_primary(), hostname=self.get_option_hostname()) + instance.save() - # Lastly, if there are no primary machines at all, then don't allow - # this to be registered as a secondary machine. - if not options['primary'] and not primaries: - raise CommandError('Unable to register a secondary machine until ' - 'another primary machine has been registered.') - - # Sanity check: An IP address is required if this is an initial - # registration. - if not existing and not options['hostname']: - raise CommandError('An explicit hostname is required at initial ' - 'registration.') - - # If this is a primary machine and there is another primary machine, - # it must be de-primary-ified. - if options['primary'] and primaries: - for old_primary in other_instances.filter(primary=True): - old_primary.primary = False - old_primary.save() - - # Okay, we've checked for appropriate errata; perform the registration. - dirty = any([ - instance.primary is not options['primary'], - options['hostname'] and - instance.hostname != options['hostname'], - ]) - instance.primary = options['primary'] - if options['hostname']: - instance.hostname = options['hostname'] - instance.save() - - # Done! - print('Instance %s registered (changed: %r).' % (uuid, dirty)) + # Done! + print('Successfully registered instance %s.' % instance_str(instance)) diff --git a/awx/main/management/commands/remove_instance.py b/awx/main/management/commands/remove_instance.py index 56c817e760..04dc832101 100644 --- a/awx/main/management/commands/remove_instance.py +++ b/awx/main/management/commands/remove_instance.py @@ -5,50 +5,40 @@ from optparse import make_option from django.conf import settings from django.core.management.base import BaseCommand, CommandError +from awx.main.management.commands._base_instance import BaseCommandInstance +instance_str = BaseCommandInstance.instance_str from awx.main.models import Instance -class Command(BaseCommand): - """Remove an existing instance from the HA instance table. +class Command(BaseCommandInstance): + """Internal tower command. + Remove an existing instance from the HA instance table. - This command is idempotent. It will remove the machine if it is - present and do nothing if the machine is absent. + This command is idempotent. - This command will cowardly refuse to remove the primary machine. + This command will error out in the following conditions: + + * Attempting to remove a primary instance. """ - option_list = BaseCommand.option_list + ( - make_option('--hostname', dest='hostname', default=''), - make_option('--uuid', dest='uuid', default=''), - ) + def __init__(self): + super(Command, self).__init__() - def handle(self, **options): - # Remove any empty options from the options dictionary. - fields = {} - for field in ('uuid', 'hostname'): - if options[field]: - fields[field] = options[field] + self.include_option_hostname_uuid_find() - # At least one of hostname or uuid must be set. - if not fields: - raise CommandError('You must provide either --uuid or --hostname.') - - # Is there an existing record for this machine? - # If so, retrieve that record and look for issues. + def handle(self, *args, **options): + # Is there an existing record for this machine? If so, retrieve that record and look for issues. try: # Get the instance. - instance = Instance.objects.get(**fields) + instance = Instance.objects.get(**self.get_unique_fields()) # Sanity check: Do not remove the primary instance. if instance.primary: - raise CommandError('I cowardly refuse to remove the primary ' - 'instance.') + raise CommandError('I cowardly refuse to remove the primary instance %s.' % instance_str(instance)) # Remove the instance. instance.delete() - dirty = True + print('Successfully removed instance %s.' % instance_str(instance)) except Instance.DoesNotExist: - dirty = False + print('No matching instance found to remove.') - # Done! - print('Instance removed (changed: %r).' % dirty) diff --git a/awx/main/management/commands/update_instance.py b/awx/main/management/commands/update_instance.py new file mode 100644 index 0000000000..dd7b114d2f --- /dev/null +++ b/awx/main/management/commands/update_instance.py @@ -0,0 +1,60 @@ +# Copyright (c) 2014 Ansible, Inc. +# All Rights Reserved + +from optparse import make_option + +from django.conf import settings +from django.core.management.base import BaseCommand, CommandError +from django.db import transaction + +from awx.main.management.commands._base_instance import BaseCommandInstance +instance_str = BaseCommandInstance.instance_str + +from awx.main.models import Instance + +class Command(BaseCommandInstance): + """Set an already registered instance to primary or secondary for HA + tracking. + + This command is idempotent. Settings a new primary instance when a + primary instance already exists will result in the existing primary + instance set to secondary and the new primary set to primary. + + This command will error out under the following circumstances: + + * Attempting to update a secondary instance with no primary instances. + * When a matching instance is not found. + """ + def __init__(self): + super(Command, self).__init__() + + self.include_options_roles() + self.include_option_hostname_uuid_find() + + @transaction.atomic + def handle(self, *args, **options): + super(Command, self).handle(*args, **options) + + # Is there an existing record for this machine? If so, retrieve that record and look for issues. + try: + instance = Instance.objects.get(**self.get_unique_fields()) + existing = True + except Instance.DoesNotExist: + raise CommandError('No matching instance found to update.') + + # Get a status on primary machines (excluding this one, regardless of its status). + other_instances = Instance.objects.exclude(**self.get_unique_fields()) + primaries = other_instances.filter(primary=True).count() + + # If this is a primary machine and there is another primary machine, it must be de-primary-ified. + if self.is_option_primary() and primaries: + for old_primary in other_instances.filter(primary=True): + old_primary.primary = False + old_primary.save() + + # Okay, we've checked for appropriate errata; perform the registration. + instance.primary = self.is_option_primary() + instance.save() + + # Done! + print('Successfully updated instance role %s' % instance_str(instance)) \ No newline at end of file