diff --git a/awx/api/serializers.py b/awx/api/serializers.py index e654247066..45aa860d0b 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -966,6 +966,7 @@ class InventorySourceOptionsSerializer(BaseSerializer): field_opts = metadata.get('source_regions', {}) field_opts['ec2_region_choices'] = self.opts.model.get_ec2_region_choices() field_opts['rax_region_choices'] = self.opts.model.get_rax_region_choices() + field_opts['gce_region_choices'] = self.opts.model.get_gce_region_choices() return metadata def to_native(self, obj): diff --git a/awx/main/fields.py b/awx/main/fields.py index 683d89ee72..9453eaa329 100644 --- a/awx/main/fields.py +++ b/awx/main/fields.py @@ -4,6 +4,8 @@ # Django from django.db import models from django.db.models.fields.related import SingleRelatedObjectDescriptor +from django.utils.translation import ugettext_lazy as _ + # South from south.modelsinspector import add_introspection_rules @@ -38,3 +40,57 @@ class AutoOneToOneField(models.OneToOneField): add_introspection_rules([([AutoOneToOneField], [], {})], [r'^awx\.main\.fields\.AutoOneToOneField']) + + +# Copied, flat out, from Django 1.6. +# Vendored here because Tower is run against Django 1.5. +# +# Original: +# github.com/django/django/blob/master/django/db/models/fields/__init__.py +# +# Django is: +# Copyright (c) Django Software Foundation and individual contributors. +# All rights reserved. +# +# Used under license: +# github.com/django/django/blob/master/LICENSE +class BinaryField(models.Field): + description = _("Raw binary data") + empty_values = [None, b''] + + def __init__(self, *args, **kwargs): + kwargs['editable'] = False + super(BinaryField, self).__init__(*args, **kwargs) + if self.max_length is not None: + self.validators.append(validators.MaxLengthValidator(self.max_length)) + + def get_internal_type(self): + return "BinaryField" + + def get_default(self): + if self.has_default() and not callable(self.default): + return self.default + default = super(BinaryField, self).get_default() + if default == '': + return b'' + return default + + def get_db_prep_value(self, value, connection, prepared=False): + value = super(BinaryField, self).get_db_prep_value(value, connection, prepared) + if value is not None: + return connection.Database.Binary(value) + return value + + def value_to_string(self, obj): + """Binary data is serialized as base64""" + return b64encode(force_bytes(self._get_val_from_obj(obj))).decode('ascii') + + def to_python(self, value): + # If it's a string, it should be base64-encoded data + if isinstance(value, six.text_type): + return six.memoryview(b64decode(force_bytes(value))) + return value + + +add_introspection_rules([([BinaryField], [], {})], + [r'^awx\.main\.fields\.BinaryField']) diff --git a/awx/main/models/credential.py b/awx/main/models/credential.py index 0e8f421d6b..b3d2bbf8a3 100644 --- a/awx/main/models/credential.py +++ b/awx/main/models/credential.py @@ -13,12 +13,27 @@ from django.core.exceptions import ValidationError, NON_FIELD_ERRORS from django.core.urlresolvers import reverse # AWX +from awx.main import storage +from awx.main.fields import BinaryField from awx.main.utils import decrypt_field from awx.main.models.base import * __all__ = ['Credential'] +class PEM(models.Model): + """Model representing a PEM p12 private key created with openssl. + + These are notably used as credentials for authenticating to Google + services, and Tower uses them for Google Compute Engine. + """ + filename = models.CharField(max_length=100, unique=True) + contents = BinaryField() + + class Meta: + app_label = 'main' + + class Credential(PasswordFieldsModel, CommonModelNameNotUnique): ''' A credential contains information about how to talk to a remote resource @@ -36,7 +51,7 @@ class Credential(PasswordFieldsModel, CommonModelNameNotUnique): ] PASSWORD_FIELDS = ('password', 'ssh_key_data', 'ssh_key_unlock', - 'sudo_password', 'vault_password') + 'sudo_password', 'vault_password', 'pem_file') class Meta: app_label = 'main' @@ -122,6 +137,12 @@ class Credential(PasswordFieldsModel, CommonModelNameNotUnique): default='', help_text=_('Vault password (or "ASK" to prompt the user).'), ) + pem_file = models.FileField( + blank=True, + null=True, + upload_to='irrelevant', + storage=storage.DatabaseStorage(PEM), + ) @property def needs_password(self): @@ -304,7 +325,7 @@ class Credential(PasswordFieldsModel, CommonModelNameNotUnique): # If update_fields has been specified, add our field names to it, # if hit hasn't been specified, then we're just doing a normal save. update_fields = kwargs.get('update_fields', []) - cloud = self.kind in ('aws', 'rax') + cloud = self.kind in ('aws', 'rax', 'gce', 'vmware', 'azure') if self.cloud != cloud: self.cloud = cloud if 'cloud' not in update_fields: diff --git a/awx/main/models/inventory.py b/awx/main/models/inventory.py index a80696118a..29bef49bb9 100644 --- a/awx/main/models/inventory.py +++ b/awx/main/models/inventory.py @@ -810,6 +810,17 @@ class InventorySourceOptions(BaseModel): regions.insert(0, ('ALL', 'All')) return regions + @classmethod + def get_gce_region_choices(self): + """Return a complete list of regions in GCE, as a list of + two-tuples. + """ + # It's not possible to get a list of regions from GCE without + # authenticating first. Therefore, use a list from settings. + regions = list(getattr(settings, 'GCE_REGION_CHOICES', [])) + regions.insert(0, ('all', 'All')) + return regions + def clean_credential(self): if not self.source: return None diff --git a/awx/settings/defaults.py b/awx/settings/defaults.py index d5187a3555..c89d53a7de 100644 --- a/awx/settings/defaults.py +++ b/awx/settings/defaults.py @@ -418,7 +418,7 @@ GCE_REGION_CHOICES = [ ('us-central1-b', 'US Central (B)'), ('us-central1-f', 'US Central (F)'), ('europe-west1-a', 'Europe West (A)'), - ('europe-west1-b', 'Europe West (B)') + ('europe-west1-b', 'Europe West (B)'), ('asia-east1-a', 'Asia East (A)'), ('asia-east1-b', 'Asia East (B)'), ] @@ -427,7 +427,7 @@ GCE_REGIONS_BLACKLIST = [] # Inventory variable name/value for determining whether a host is active # in Google Compute Engine. GCE_ENABLED_VAR = 'status' -GCE_ENABLED_VALUE = '' +GCE_ENABLED_VALUE = 'running' # Filter for allowed group and host names when importing inventory from # Google Compute Engine. diff --git a/awx/ui/static/js/forms/Credentials.js b/awx/ui/static/js/forms/Credentials.js index 0936fd781c..f84f63f186 100644 --- a/awx/ui/static/js/forms/Credentials.js +++ b/awx/ui/static/js/forms/Credentials.js @@ -101,6 +101,8 @@ angular.module('CredentialFormDefinition', []) '
Access keys for Amazon Web Services used for inventory management or deployment.
\n' + '
Rackspace
\n' + '
Access information for Rackspace Cloud used for inventory management or deployment.
\n' + + '
Google Compute Engine
\n' + + '
Credentials for Google Compute Engine, used for inventory management or deployment.
\n' + '
VMWare
\n' + '
Access information for VMWare vSphere used for inventory management or deployment.
\n' + '\n'