diff --git a/django-cloudlaunch/baselaunch/admin.py b/django-cloudlaunch/baselaunch/admin.py index e1eae5c7..543d8ade 100644 --- a/django-cloudlaunch/baselaunch/admin.py +++ b/django-cloudlaunch/baselaunch/admin.py @@ -60,8 +60,15 @@ class OSCredsInline(admin.StackedInline): extra = 1 +class AzureCredsInline(admin.StackedInline): + model = models.AzureCredentials + form = forms.AzureCredentialsForm + formset = forms.DefaultRequiredInlineFormSet + extra = 1 + + class UserProfileAdmin(admin.ModelAdmin): - inlines = [AWSCredsInline, OSCredsInline] + inlines = [AWSCredsInline, OSCredsInline, AzureCredsInline] class AppDeploymentsAdmin(admin.ModelAdmin): @@ -109,6 +116,7 @@ class PublicServicesAdmin(admin.ModelAdmin): admin.site.register(models.AWS, CloudAdmin) admin.site.register(models.EC2, EC2Admin) admin.site.register(models.S3, S3Admin) +admin.site.register(models.Azure, CloudAdmin) admin.site.register(models.OpenStack, CloudAdmin) admin.site.register(models.UserProfile, UserProfileAdmin) admin.site.register(models.Usage, UsageAdmin) diff --git a/django-cloudlaunch/baselaunch/backend_plugins/cloudman_app.py b/django-cloudlaunch/baselaunch/backend_plugins/cloudman_app.py index 48181f66..e5837334 100644 --- a/django-cloudlaunch/baselaunch/backend_plugins/cloudman_app.py +++ b/django-cloudlaunch/baselaunch/backend_plugins/cloudman_app.py @@ -101,6 +101,17 @@ 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."}) diff --git a/django-cloudlaunch/baselaunch/domain_model.py b/django-cloudlaunch/baselaunch/domain_model.py index 877ccfbf..c1587ca8 100644 --- a/django-cloudlaunch/baselaunch/domain_model.py +++ b/django-cloudlaunch/baselaunch/domain_model.py @@ -45,5 +45,15 @@ def get_cloud_provider(cloud, cred_dict): config.update(cred_dict) return CloudProviderFactory().create_provider(ProviderList.AWS, config) + elif isinstance(cloud, models.Azure): + config = { + 'azure_region_name': cloud.region_name, + 'azure_resource_group': cloud.resource_group, + 'azure_storage_account':cloud.storage_account, + 'azure_vm_default_user_name': cloud.vm_default_user_name + } + config.update(cred_dict) + return CloudProviderFactory().create_provider(ProviderList.AZURE, + config) else: raise Exception("Unrecognised cloud provider: %s" % cloud) diff --git a/django-cloudlaunch/baselaunch/forms.py b/django-cloudlaunch/baselaunch/forms.py index 35f01467..1dcee225 100644 --- a/django-cloudlaunch/baselaunch/forms.py +++ b/django-cloudlaunch/baselaunch/forms.py @@ -37,6 +37,22 @@ class Meta: fields = '__all__' +class AzureCredentialsForm(ModelForm): + + def __init__(self, *args, **kwargs): + super(AzureCredentialsForm, self).__init__(*args, **kwargs) + # restrict choices to Azure clouds only + self.fields['cloud'].queryset = models.Azure \ + .objects.all() + + secret = forms.CharField(widget=PasswordInput(render_value=True), + required=False) + + class Meta: + model = models.AzureCredentials + fields = '__all__' + + class DefaultRequiredInlineFormSet(BaseInlineFormSet): def clean(self): diff --git a/django-cloudlaunch/baselaunch/migrations/0019_azure_credentials.py b/django-cloudlaunch/baselaunch/migrations/0019_azure_credentials.py new file mode 100644 index 00000000..9c553d84 --- /dev/null +++ b/django-cloudlaunch/baselaunch/migrations/0019_azure_credentials.py @@ -0,0 +1,53 @@ + +# -*- coding: utf-8 -*- +# Generated by Django 1.9 on 2017-01-20 18:16 +from __future__ import unicode_literals + +import django +import fernet_fields + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('baselaunch', '0018_auto_20170613_1553'), + ] + + operations = [ + migrations.CreateModel( + name='AZURE', + 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)), + ('resource_group', models.CharField(max_length=100)), + ('storage_account', models.CharField(max_length=100)), + ('vm_default_user_name', models.CharField(max_length=100)), + ], + options={ + 'verbose_name': 'Azure', + 'verbose_name_plural': 'Azure', + }, + bases=('baselaunch.cloud',), + ), + migrations.CreateModel( + name='AzureCredentials', + 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')), + ('subscription_id', models.CharField(max_length=50)), + ('client_id', models.CharField(blank=True, max_length=50, null=True)), + ('secret', fernet_fields.fields.EncryptedCharField(blank=True, max_length=50, null=True)), + ('tenant', models.CharField(blank=True, max_length=50, null=True)) + ], + options={ + 'verbose_name': 'Azure Credentials', + 'verbose_name_plural': 'Azure Credentials', + }, + bases=('baselaunch.credentials',), + ) + ] \ No newline at end of file diff --git a/django-cloudlaunch/baselaunch/migrations/0020_azure_model_change.py b/django-cloudlaunch/baselaunch/migrations/0020_azure_model_change.py new file mode 100644 index 00000000..83dd505a --- /dev/null +++ b/django-cloudlaunch/baselaunch/migrations/0020_azure_model_change.py @@ -0,0 +1,37 @@ + +# -*- coding: utf-8 -*- +# Generated by Django 1.9 on 2017-01-20 18:16 +from __future__ import unicode_literals + +import django +import fernet_fields + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('baselaunch', '0019_azure_credentials'), + ] + + operations = [ + migrations.DeleteModel('AZURE'), + migrations.CreateModel( + name='Azure', + 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)), + ('resource_group', models.CharField(max_length=100)), + ('storage_account', models.CharField(max_length=100)), + ('vm_default_user_name', models.CharField(max_length=100)), + ], + options={ + 'verbose_name': 'Azure', + 'verbose_name_plural': 'Azure', + }, + bases=('baselaunch.cloud',), + ) + ] \ No newline at end of file diff --git a/django-cloudlaunch/baselaunch/models.py b/django-cloudlaunch/baselaunch/models.py index c475673b..c63bceb0 100644 --- a/django-cloudlaunch/baselaunch/models.py +++ b/django-cloudlaunch/baselaunch/models.py @@ -102,6 +102,18 @@ class Meta: verbose_name_plural = "OpenStack" +class Azure(Cloud): + resource_group = models.CharField(max_length=100, blank=True, null=False) + region_name = models.CharField(max_length=100, blank=True, null=False) + storage_account = models.CharField(max_length=100, blank=True, null=False) + vm_default_user_name = models.CharField(max_length=100, blank=True, null=False) + + + class Meta: + verbose_name = "Azure" + verbose_name_plural = "Azure" + + class Image(DateNameAwareModel): """ A base Image model used by a virtual appliance. @@ -302,6 +314,26 @@ def as_dict(self): return d +class AzureCredentials(Credentials): + subscription_id = models.CharField(max_length=50, blank=False, null=False) + client_id = models.CharField(max_length=50, blank=False, null=False) + secret = EncryptedCharField(max_length=50, blank=False, null=False) + tenant = models.CharField(max_length=50, blank=True, null=True) + + class Meta: + verbose_name = "Azure Credentials" + verbose_name_plural = "Azure Credentials" + + def as_dict(self): + d = { + 'azure_subscription_id': self.subscription_id, + 'azure_client_id': self.client_id, + 'azure_secret': self.secret, + 'azure_tenant': self.tenant + } + return d + + class UserProfile(models.Model): # Link UserProfile to a User model instance user = models.OneToOneField(User) diff --git a/django-cloudlaunch/baselaunch/serializers.py b/django-cloudlaunch/baselaunch/serializers.py index 0b5a24ee..4dc330de 100644 --- a/django-cloudlaunch/baselaunch/serializers.py +++ b/django-cloudlaunch/baselaunch/serializers.py @@ -509,6 +509,8 @@ def get_region_name(self, obj): return obj.aws.compute.ec2_region_name elif hasattr(obj, 'openstack'): return obj.openstack.region_name + elif hasattr(obj, 'azure'): + return obj.azure.region_name else: return "Cloud provider not recognized" @@ -517,6 +519,8 @@ def get_cloud_type(self, obj): return 'aws'; elif hasattr(obj, 'openstack'): return 'openstack'; + elif hasattr(obj, 'azure'): + return 'azure'; else: return 'unknown'; @@ -545,6 +549,14 @@ def get_extra_data(self, obj): 'region_name': os.region_name, 'identity_api_version': os.identity_api_version } + elif hasattr(obj, 'azure'): + azure = obj.azure + return { + 'region_name': azure.region_name, + 'resource_group': azure.resource_group, + 'storage_account': azure.storage_account, + 'vm_default_user_name': azure.vm_default_user_name + } else: return {} @@ -740,6 +752,8 @@ class CredentialsSerializer(serializers.Serializer): aws = CustomHyperlinkedIdentityField(view_name='awscredentials-list') openstack = CustomHyperlinkedIdentityField( view_name='openstackcredentials-list') + azure = CustomHyperlinkedIdentityField( + view_name='azurecredentials-list') class AWSCredsSerializer(serializers.HyperlinkedModelSerializer): @@ -772,9 +786,25 @@ class Meta: exclude = ('password', 'user_profile') +class AzureCredsSerializer(serializers.HyperlinkedModelSerializer): + id = serializers.IntegerField(read_only=True) + secret = serializers.CharField( + style={'input_type': 'password'}, + write_only=True, + required=False + ) + cloud_id = serializers.CharField(write_only=True) + cloud = CloudSerializer(read_only=True) + + class Meta: + model = models.AzureCredentials + exclude = ('secret', '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) result = serializers.CharField(read_only=True) details = serializers.CharField(read_only=True) @@ -793,6 +823,7 @@ class UserSerializer(UserDetailsSerializer): lookup_field=None) aws_creds = serializers.SerializerMethodField() openstack_creds = serializers.SerializerMethodField() + azure_creds = serializers.SerializerMethodField() def get_aws_creds(self, obj): """ @@ -818,9 +849,21 @@ def get_openstack_creds(self, obj): except models.UserProfile.DoesNotExist: return "" + def get_azure_creds(self, obj): + """ + Include a URL for listing this bucket's contents + """ + try: + creds = obj.userprofile.credentials.filter( + azurecredentials__isnull=False).select_subclasses() + return AzureCredsSerializer(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', 'credentials') + ('aws_creds', 'openstack_creds', 'azure_creds', 'credentials') ### Public Services Serializers ### diff --git a/django-cloudlaunch/baselaunch/urls.py b/django-cloudlaunch/baselaunch/urls.py index 8f681278..c2a2cfb6 100644 --- a/django-cloudlaunch/baselaunch/urls.py +++ b/django-cloudlaunch/baselaunch/urls.py @@ -114,6 +114,8 @@ profile_router.register(r'credentials/aws', views.AWSCredentialsViewSet) profile_router.register(r'credentials/openstack', views.OpenstackCredentialsViewSet) +profile_router.register(r'credentials/azure', + views.AzureCredentialsViewSet) infrastructure_regex_pattern = r'api/v1/infrastructure/' auth_regex_pattern = r'api/v1/auth/' diff --git a/django-cloudlaunch/baselaunch/view_helpers.py b/django-cloudlaunch/baselaunch/view_helpers.py index 632bfb61..28453181 100644 --- a/django-cloudlaunch/baselaunch/view_helpers.py +++ b/django-cloudlaunch/baselaunch/view_helpers.py @@ -72,6 +72,20 @@ def get_credentials_from_request(cloud, request): } else: return {} + elif isinstance(cloud, models.Azure): + azure_subscription_id = request.META.get('HTTP_CL_AZURE_SUBSCRIPTION_ID') + azure_client_id = request.META.get('HTTP_CL_AZURE_CLIENT_ID') + azure_secret = request.META.get('HTTP_CL_AZURE_SECRET') + azure_tenant = request.META.get('HTTP_CL_AZURE_TENANT') + + if azure_subscription_id and azure_client_id and azure_secret and azure_tenant: + return {'azure_subscription_id': azure_subscription_id, + 'azure_client_id': azure_client_id, + 'azure_secret': azure_secret, + 'azure_tenant': azure_tenant + } + else: + return {} else: raise Exception("Unrecognised cloud provider: %s" % cloud) diff --git a/django-cloudlaunch/baselaunch/views.py b/django-cloudlaunch/baselaunch/views.py index 130edfc1..c8e69f03 100644 --- a/django-cloudlaunch/baselaunch/views.py +++ b/django-cloudlaunch/baselaunch/views.py @@ -494,6 +494,21 @@ 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. + """ + queryset = models.AzureCredentials.objects.all() + serializer_class = serializers.AzureCredsSerializer + #permission_classes = [permissions.DjangoModelPermissions] + + def get_queryset(self): + user = self.request.user + if hasattr(user, 'userprofile'): + return user.userprofile.credentials.filter( + azurecredentials__isnull=False).select_subclasses() + return models.AzureCredentials.objects.none() + class DeploymentViewSet(viewsets.ModelViewSet): """ diff --git a/django-cloudlaunch/cloudlaunch/settings.py b/django-cloudlaunch/cloudlaunch/settings.py index caeecdf0..5c06dcc9 100644 --- a/django-cloudlaunch/cloudlaunch/settings.py +++ b/django-cloudlaunch/cloudlaunch/settings.py @@ -52,6 +52,15 @@ 'cl-os-identity-api-version', 'cl-aws-access-key', 'cl-aws-secret-key', + 'cl-azure-region-name', + 'cl-azure-resource_group', + 'cl-azure-subscription-id' + 'cl-azure-client-id', + 'cl-azure-secret', + 'cl-azure-tenant', + 'cl-storage-account', + 'cl-azure-vm-default-user-name' + ) # End: django-cors-headers settings