Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion django-cloudlaunch/baselaunch/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@ class OSCredsInline(admin.StackedInline):
formset = forms.DefaultRequiredInlineFormSet
extra = 1

class GCECredsInline(admin.StackedInline):
model = models.GCECredentials
form = forms.GCECredentialsForm
formset = forms.DefaultRequiredInlineFormSet
extra = 1

class AzureCredsInline(admin.StackedInline):
model = models.AzureCredentials
Expand All @@ -68,7 +73,7 @@ class AzureCredsInline(admin.StackedInline):


class UserProfileAdmin(admin.ModelAdmin):
inlines = [AWSCredsInline, OSCredsInline, AzureCredsInline]
inlines = [AWSCredsInline, OSCredsInline, AzureCredsInline, GCECredsInline]


class AppDeploymentsAdmin(admin.ModelAdmin):
Expand Down Expand Up @@ -118,6 +123,7 @@ class PublicServicesAdmin(admin.ModelAdmin):
admin.site.register(models.S3, S3Admin)
admin.site.register(models.Azure, CloudAdmin)
admin.site.register(models.OpenStack, CloudAdmin)
admin.site.register(models.GCE, CloudAdmin)
admin.site.register(models.UserProfile, UserProfileAdmin)
admin.site.register(models.Usage, UsageAdmin)

Expand Down
11 changes: 0 additions & 11 deletions django-cloudlaunch/baselaunch/backend_plugins/cloudman_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,17 +101,6 @@ def process_app_config(name, cloud_version_config, credentials, app_config):
ec2_creds = provider.security.get_or_create_ec2_credentials()
user_data['access_key'] = ec2_creds.access
user_data['secret_key'] = ec2_creds.secret

if hasattr(cloud, 'azure'):
user_data['cloud_type'] = 'azure'
user_data['region_name'] = cloud.azure.region_name
user_data['resource_group'] = cloud.azure.resource_group
user_data['storage_account'] = cloud.azure.storage_account
user_data['vm_default_user_name'] = cloud.azure.vm_default_user_name
user_data['subscription_id'] = credentials.get('azure_subscription_id')
user_data['client_id'] = credentials.get('azure_client_id')
user_data['secret'] = credentials.get('azure_secret')
user_data['tenant'] = credentials.get('azure_tenant')
else:
raise ValidationError({ "error":
"This version of CloudMan supports only EC2-compatible clouds."})
Expand Down
8 changes: 8 additions & 0 deletions django-cloudlaunch/baselaunch/domain_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,5 +55,13 @@ def get_cloud_provider(cloud, cred_dict):
config.update(cred_dict)
return CloudProviderFactory().create_provider(ProviderList.AZURE,
config)
elif isinstance(cloud, models.GCE):
config = {
'gce_service_creds_dict': cred_dict,
'gce_default_zone': cloud.zone_name,
'gce_region_name': cloud.region_name
}
return CloudProviderFactory().create_provider(ProviderList.GCE,
config)
else:
raise Exception("Unrecognised cloud provider: %s" % cloud)
10 changes: 10 additions & 0 deletions django-cloudlaunch/baselaunch/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,16 @@ class Meta:
model = models.OpenStackCredentials
fields = '__all__'

class GCECredentialsForm(ModelForm):

def __init__(self, *args, **kwargs):
super(GCECredentialsForm, self).__init__(*args, **kwargs)
# restrict choices to GCE clouds only
self.fields['cloud'].queryset = models.GCE.objects.all()

class Meta:
model = models.GCECredentials
fields = '__all__'

class AzureCredentialsForm(ModelForm):

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9 on 2017-06-21 14:35
from __future__ import unicode_literals

from django.db import migrations, models
import django.db.models.deletion
import fernet_fields.fields


class Migration(migrations.Migration):

dependencies = [
('baselaunch', '0018_auto_20170613_1553'),
]

operations = [
migrations.CreateModel(
name='GCE',
fields=[
('cloud_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='baselaunch.Cloud')),
('region_name', models.CharField(max_length=100)),
('zone_name', models.CharField(max_length=100)),
],
options={
'verbose_name': 'GCE',
'verbose_name_plural': 'GCE',
},
bases=('baselaunch.cloud',),
),
migrations.CreateModel(
name='GCECredentials',
fields=[
('credentials_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='baselaunch.Credentials')),
('credentials', fernet_fields.fields.EncryptedTextField()),
],
options={
'verbose_name': 'GCE Credentials',
'verbose_name_plural': 'GCE Credentials',
},
bases=('baselaunch.credentials',),
),
]
16 changes: 16 additions & 0 deletions django-cloudlaunch/baselaunch/migrations/0021_merge.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9 on 2017-07-27 00:54
from __future__ import unicode_literals

from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
('baselaunch', '0020_azure_model_change'),
('baselaunch', '0019_gce_gcecredentials'),
]

operations = [
]
26 changes: 26 additions & 0 deletions django-cloudlaunch/baselaunch/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from django.db import models
from django.template.defaultfilters import slugify
from fernet_fields import EncryptedCharField
from fernet_fields import EncryptedTextField
from model_utils.managers import InheritanceManager
from smart_selects.db_fields import ChainedForeignKey

Expand Down Expand Up @@ -101,6 +102,13 @@ class Meta:
verbose_name = "OpenStack"
verbose_name_plural = "OpenStack"

class GCE(Cloud):
region_name = models.CharField(max_length=100, blank=False, null=False)
zone_name = models.CharField(max_length=100, blank=False, null=False)

class Meta:
verbose_name = "GCE"
verbose_name_plural = "GCE"

class Azure(Cloud):
resource_group = models.CharField(max_length=100, blank=True, null=False)
Expand Down Expand Up @@ -313,6 +321,24 @@ def as_dict(self):
d['os_user_domain_name'] = self.user_domain_name
return d

class GCECredentials(Credentials):
credentials = EncryptedTextField(blank=False, null=False)

def save(self, *args, **kwargs):
if self.credentials:
try:
json.loads(self.credentials)
except Exception as e:
raise Exception("Invalid JSON syntax. GCE Credentials must be in JSON format. Cause: {0}".format(e))

super(GCECredentials, self).save(*args, **kwargs)

class Meta:
verbose_name = "GCE Credentials"
verbose_name_plural = "GCE Credentials"

def as_dict(self):
return json.loads(self.credentials)

class AzureCredentials(Credentials):
subscription_id = models.CharField(max_length=50, blank=False, null=False)
Expand Down
42 changes: 41 additions & 1 deletion django-cloudlaunch/baselaunch/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -511,6 +511,8 @@ def get_region_name(self, obj):
return obj.openstack.region_name
elif hasattr(obj, 'azure'):
return obj.azure.region_name
elif hasattr(obj, 'gce'):
return obj.gce.region_name
else:
return "Cloud provider not recognized"

Expand All @@ -521,6 +523,8 @@ def get_cloud_type(self, obj):
return 'openstack';
elif hasattr(obj, 'azure'):
return 'azure';
elif hasattr(obj, 'gce'):
return 'gce';
else:
return 'unknown';

Expand Down Expand Up @@ -557,6 +561,12 @@ def get_extra_data(self, obj):
'storage_account': azure.storage_account,
'vm_default_user_name': azure.vm_default_user_name
}
elif hasattr(obj, 'gce'):
gce = obj.gce
return {
'region_name': gce.region_name,
'zone_name': gce.zone_name
}
else:
return {}

Expand Down Expand Up @@ -754,6 +764,8 @@ class CredentialsSerializer(serializers.Serializer):
view_name='openstackcredentials-list')
azure = CustomHyperlinkedIdentityField(
view_name='azurecredentials-list')
gce = CustomHyperlinkedIdentityField(
view_name='gcecredentials-list')


class AWSCredsSerializer(serializers.HyperlinkedModelSerializer):
Expand Down Expand Up @@ -801,10 +813,25 @@ class Meta:
exclude = ('secret', 'user_profile')


class GCECredsSerializer(serializers.HyperlinkedModelSerializer):
id = serializers.IntegerField(read_only=True)
credentials = serializers.CharField(
write_only=True,
style={'base_template': 'textarea.html', 'rows': 20},
)
cloud_id = serializers.CharField(write_only=True)
cloud = CloudSerializer(read_only=True)

class Meta:
model = models.GCECredentials
exclude = ('user_profile',)


class CloudConnectionAuthSerializer(serializers.Serializer):
aws_creds = AWSCredsSerializer(write_only=True, required=False)
openstack_creds = OpenstackCredsSerializer(write_only=True, required=False)
azure_creds = AzureCredsSerializer(write_only=True, required=False)
gce_creds = GCECredsSerializer(write_only=True, required=False)
result = serializers.CharField(read_only=True)
details = serializers.CharField(read_only=True)

Expand All @@ -824,6 +851,7 @@ class UserSerializer(UserDetailsSerializer):
aws_creds = serializers.SerializerMethodField()
openstack_creds = serializers.SerializerMethodField()
azure_creds = serializers.SerializerMethodField()
gce_creds = serializers.SerializerMethodField()

def get_aws_creds(self, obj):
"""
Expand Down Expand Up @@ -861,9 +889,21 @@ def get_azure_creds(self, obj):
except models.UserProfile.DoesNotExist:
return ""

def get_gce_creds(self, obj):
"""
Include a URL for listing this bucket's contents
"""
try:
creds = obj.userprofile.credentials.filter(
gcecredentials__isnull=False).select_subclasses()
return GCECredsSerializer(instance=creds, many=True,
context=self.context).data
except models.UserProfile.DoesNotExist:
return ""

class Meta(UserDetailsSerializer.Meta):
fields = UserDetailsSerializer.Meta.fields + \
('aws_creds', 'openstack_creds', 'azure_creds', 'credentials')
('aws_creds', 'openstack_creds', 'azure_creds', 'gce_creds', 'credentials')


### Public Services Serializers ###
Expand Down
2 changes: 2 additions & 0 deletions django-cloudlaunch/baselaunch/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,8 @@
views.OpenstackCredentialsViewSet)
profile_router.register(r'credentials/azure',
views.AzureCredentialsViewSet)
profile_router.register(r'credentials/gce',
views.GCECredentialsViewSet)

infrastructure_regex_pattern = r'api/v1/infrastructure/'
auth_regex_pattern = r'api/v1/auth/'
Expand Down
8 changes: 8 additions & 0 deletions django-cloudlaunch/baselaunch/view_helpers.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from baselaunch import domain_model
from baselaunch import models

import json

def get_cloud_provider(view, cloud_id = None):
"""
Expand Down Expand Up @@ -86,6 +87,13 @@ def get_credentials_from_request(cloud, request):
}
else:
return {}
elif isinstance(cloud, models.GCE):
gce_credentials_json = request.META.get('HTTP_CL_GCE_CREDENTIALS_JSON')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@machristie So I was looking at this a bit. Since an HTTP header can't have any carriage returns or line feeds, I was wondering whether this might pose a problem. However, it looks like JSON data in a header is ok, provided that the json encoder makes sure there are no CR/LFs, which is probably doable. I guess we can test this once the corresponding CloudLaunch UI changes are made.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah good catch, I wasn't thinking about CR/LFs. I think you're right and this will work as long as the JSON doesn't have CR/LFs.

The other approach would be to split out the fields into separate headers. I considered doing this because I read somewhere that web servers tend to support headers with values only up to 8KB. But the credentials file I have is only 2,330 bytes, so it should fit comfortably.

Eventually I decided to just treat the credentials as a kind of opaque blob for the sake of simplicity. But I'd be curious to know if you have an opinion on whether I should have separate headers for each field or not.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it makes sense to leave it as a single header, since this would correspond directly to the gce_service_creds_dict on the cloudbridge side. On the UI side, we could have an upload field so that the user can upload his/her credentials file, which would be subsequently sent across through this header if it's being used as temporary creds, or saved in the "credentials" database field, if it's going to be persistent.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess we'll just leave this like it is until further testing from the UI.


if gce_credentials_json:
return json.loads(gce_credentials_json)
else:
return {}
else:
raise Exception("Unrecognised cloud provider: %s" % cloud)

Expand Down
17 changes: 17 additions & 0 deletions django-cloudlaunch/baselaunch/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -494,6 +494,7 @@ def get_queryset(self):
openstackcredentials__isnull=False).select_subclasses()
return models.OpenStackCredentials.objects.none()


class AzureCredentialsViewSet(CredentialsViewSet):
"""
API endpoint that allows Azure credentials to be viewed or edited.
Expand All @@ -510,6 +511,22 @@ def get_queryset(self):
return models.AzureCredentials.objects.none()


class GCECredentialsViewSet(CredentialsViewSet):
"""
API endpoint that allows GCE credentials to be viewed or edited.
"""
queryset = models.GCECredentials.objects.all()
serializer_class = serializers.GCECredsSerializer
#permission_classes = [permissions.DjangoModelPermissions]

def get_queryset(self):
user = self.request.user
if hasattr(user, 'userprofile'):
return user.userprofile.credentials.filter(
gcecredentials__isnull=False).select_subclasses()
return models.GCECredentials.objects.none()


class DeploymentViewSet(viewsets.ModelViewSet):
"""
List compute related urls.
Expand Down
4 changes: 2 additions & 2 deletions django-cloudlaunch/cloudlaunch/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,8 @@
'cl-azure-secret',
'cl-azure-tenant',
'cl-storage-account',
'cl-azure-vm-default-user-name'

'cl-azure-vm-default-user-name',
'cl-gce-credentials-json',
)
# End: django-cors-headers settings

Expand Down