merged master
This commit is contained in:
		
				commit
				
					
						65618bb2aa
					
				
			
		
					 15 changed files with 834 additions and 286 deletions
				
			
		| 
						 | 
					@ -8,7 +8,7 @@ msgid ""
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
"Project-Id-Version: PACKAGE VERSION\n"
 | 
					"Project-Id-Version: PACKAGE VERSION\n"
 | 
				
			||||||
"Report-Msgid-Bugs-To: \n"
 | 
					"Report-Msgid-Bugs-To: \n"
 | 
				
			||||||
"POT-Creation-Date: 2017-09-17 01:31+0530\n"
 | 
					"POT-Creation-Date: 2017-09-16 14:09+0000\n"
 | 
				
			||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 | 
					"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 | 
				
			||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 | 
					"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 | 
				
			||||||
"Language-Team: LANGUAGE <LL@li.org>\n"
 | 
					"Language-Team: LANGUAGE <LL@li.org>\n"
 | 
				
			||||||
| 
						 | 
					@ -18,6 +18,10 @@ msgstr ""
 | 
				
			||||||
"Content-Transfer-Encoding: 8bit\n"
 | 
					"Content-Transfer-Encoding: 8bit\n"
 | 
				
			||||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
 | 
					"Plural-Forms: nplurals=2; plural=(n != 1);\n"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#, python-format
 | 
				
			||||||
 | 
					msgid "Your New VM %(vm_name)s at Data Center Light"
 | 
				
			||||||
 | 
					msgstr "Deine neue VM %(vm_name)s bei Data Center Light"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
msgid "Enter name"
 | 
					msgid "Enter name"
 | 
				
			||||||
msgstr "Name"
 | 
					msgstr "Name"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -184,7 +188,7 @@ msgid "All Rights Reserved"
 | 
				
			||||||
msgstr "Alle Rechte vorbehalten"
 | 
					msgstr "Alle Rechte vorbehalten"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
msgid "Toggle navigation"
 | 
					msgid "Toggle navigation"
 | 
				
			||||||
msgstr "umschalten"
 | 
					msgstr "Umschalten"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
msgid "Why Data Center Light?"
 | 
					msgid "Why Data Center Light?"
 | 
				
			||||||
msgstr "Warum Data Center Light?"
 | 
					msgstr "Warum Data Center Light?"
 | 
				
			||||||
| 
						 | 
					@ -193,7 +197,7 @@ msgid "Login"
 | 
				
			||||||
msgstr "Anmelden"
 | 
					msgstr "Anmelden"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
msgid "Dashboard"
 | 
					msgid "Dashboard"
 | 
				
			||||||
msgstr "Dashboard"
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
msgid "Finally, an affordable VM hosting in Switzerland!"
 | 
					msgid "Finally, an affordable VM hosting in Switzerland!"
 | 
				
			||||||
msgstr "Endlich: bezahlbares VM Hosting in der Schweiz"
 | 
					msgstr "Endlich: bezahlbares VM Hosting in der Schweiz"
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,15 +1,22 @@
 | 
				
			||||||
from dynamicweb.celery import app
 | 
					from datetime import datetime
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from celery.exceptions import MaxRetriesExceededError
 | 
				
			||||||
from celery.utils.log import get_task_logger
 | 
					from celery.utils.log import get_task_logger
 | 
				
			||||||
 | 
					from celery import current_task
 | 
				
			||||||
from django.conf import settings
 | 
					from django.conf import settings
 | 
				
			||||||
 | 
					from django.core.mail import EmailMessage
 | 
				
			||||||
 | 
					from django.utils import translation
 | 
				
			||||||
 | 
					from django.utils.translation import ugettext_lazy as _
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from dynamicweb.celery import app
 | 
				
			||||||
 | 
					from hosting.models import HostingOrder, HostingBill
 | 
				
			||||||
 | 
					from membership.models import StripeCustomer, CustomUser
 | 
				
			||||||
from opennebula_api.models import OpenNebulaManager
 | 
					from opennebula_api.models import OpenNebulaManager
 | 
				
			||||||
from opennebula_api.serializers import VirtualMachineSerializer
 | 
					from opennebula_api.serializers import VirtualMachineSerializer
 | 
				
			||||||
from hosting.models import HostingOrder, HostingBill
 | 
					from utils.hosting_utils import get_all_public_keys
 | 
				
			||||||
from utils.forms import UserBillingAddressForm
 | 
					from utils.forms import UserBillingAddressForm
 | 
				
			||||||
from datetime import datetime
 | 
					from utils.mailer import BaseEmail
 | 
				
			||||||
from membership.models import StripeCustomer
 | 
					 | 
				
			||||||
from django.core.mail import EmailMessage
 | 
					 | 
				
			||||||
from utils.models import BillingAddress
 | 
					from utils.models import BillingAddress
 | 
				
			||||||
from celery.exceptions import MaxRetriesExceededError
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
logger = get_task_logger(__name__)
 | 
					logger = get_task_logger(__name__)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -45,25 +52,36 @@ def create_vm_task(self, vm_template_id, user, specs, template,
 | 
				
			||||||
                   stripe_customer_id, billing_address_data,
 | 
					                   stripe_customer_id, billing_address_data,
 | 
				
			||||||
                   billing_address_id,
 | 
					                   billing_address_id,
 | 
				
			||||||
                   charge, cc_details):
 | 
					                   charge, cc_details):
 | 
				
			||||||
 | 
					    logger.debug("Running create_vm_task on {}".format(current_task.request.hostname))
 | 
				
			||||||
    vm_id = None
 | 
					    vm_id = None
 | 
				
			||||||
    try:
 | 
					    try:
 | 
				
			||||||
        final_price = specs.get('price')
 | 
					        final_price = specs.get('price')
 | 
				
			||||||
        billing_address = BillingAddress.objects.filter(
 | 
					        billing_address = BillingAddress.objects.filter(
 | 
				
			||||||
            id=billing_address_id).first()
 | 
					            id=billing_address_id).first()
 | 
				
			||||||
        customer = StripeCustomer.objects.filter(id=stripe_customer_id).first()
 | 
					        customer = StripeCustomer.objects.filter(id=stripe_customer_id).first()
 | 
				
			||||||
        # Create OpenNebulaManager
 | 
					 | 
				
			||||||
        manager = OpenNebulaManager(email=settings.OPENNEBULA_USERNAME,
 | 
					 | 
				
			||||||
                                    password=settings.OPENNEBULA_PASSWORD)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Create a vm using oneadmin, also specify the name
 | 
					        if 'pass' in user:
 | 
				
			||||||
 | 
					            on_user = user.get('email')
 | 
				
			||||||
 | 
					            on_pass = user.get('pass')
 | 
				
			||||||
 | 
					            logger.debug("Using user {user} to create VM".format(user=on_user))
 | 
				
			||||||
 | 
					            vm_name = None
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            on_user = settings.OPENNEBULA_USERNAME
 | 
				
			||||||
 | 
					            on_pass = settings.OPENNEBULA_PASSWORD
 | 
				
			||||||
 | 
					            logger.debug("Using OpenNebula admin user to create VM")
 | 
				
			||||||
 | 
					            vm_name = "{email}-{template_name}-{date}".format(
 | 
				
			||||||
 | 
					                email=user.get('email'),
 | 
				
			||||||
 | 
					                template_name=template.get('name'),
 | 
				
			||||||
 | 
					                date=int(datetime.now().strftime("%s")))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Create OpenNebulaManager
 | 
				
			||||||
 | 
					        manager = OpenNebulaManager(email=on_user, password=on_pass)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        vm_id = manager.create_vm(
 | 
					        vm_id = manager.create_vm(
 | 
				
			||||||
            template_id=vm_template_id,
 | 
					            template_id=vm_template_id,
 | 
				
			||||||
            specs=specs,
 | 
					            specs=specs,
 | 
				
			||||||
            ssh_key=settings.ONEADMIN_USER_SSH_PUBLIC_KEY,
 | 
					            ssh_key=settings.ONEADMIN_USER_SSH_PUBLIC_KEY,
 | 
				
			||||||
            vm_name="{email}-{template_name}-{date}".format(
 | 
					            vm_name=vm_name
 | 
				
			||||||
                email=user.get('email'),
 | 
					 | 
				
			||||||
                template_name=template.get('name'),
 | 
					 | 
				
			||||||
                date=int(datetime.now().strftime("%s")))
 | 
					 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if vm_id is None:
 | 
					        if vm_id is None:
 | 
				
			||||||
| 
						 | 
					@ -122,6 +140,54 @@ def create_vm_task(self, vm_template_id, user, specs, template,
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        email = EmailMessage(**email_data)
 | 
					        email = EmailMessage(**email_data)
 | 
				
			||||||
        email.send()
 | 
					        email.send()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if 'pass' in user:
 | 
				
			||||||
 | 
					            lang = 'en-us' 
 | 
				
			||||||
 | 
					            if user.get('language') is not None:
 | 
				
			||||||
 | 
					                logger.debug("Language is set to {}".format(user.get('language')))
 | 
				
			||||||
 | 
					                lang = user.get('language')
 | 
				
			||||||
 | 
					            translation.activate(lang)
 | 
				
			||||||
 | 
					            # Send notification to the user as soon as VM has been booked
 | 
				
			||||||
 | 
					            context = {
 | 
				
			||||||
 | 
					                'vm': vm,
 | 
				
			||||||
 | 
					                'order': order,
 | 
				
			||||||
 | 
					                'base_url': "{0}://{1}".format(user.get('request_scheme'),
 | 
				
			||||||
 | 
					                                               user.get('request_host')),
 | 
				
			||||||
 | 
					                'page_header': _(
 | 
				
			||||||
 | 
					                    'Your New VM %(vm_name)s at Data Center Light') % {
 | 
				
			||||||
 | 
					                                   'vm_name': vm.get('name')}
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            email_data = {
 | 
				
			||||||
 | 
					                'subject': context.get('page_header'),
 | 
				
			||||||
 | 
					                'to': user.get('email'),
 | 
				
			||||||
 | 
					                'context': context,
 | 
				
			||||||
 | 
					                'template_name': 'new_booked_vm',
 | 
				
			||||||
 | 
					                'template_path': 'hosting/emails/',
 | 
				
			||||||
 | 
					                'from_address': settings.DCL_SUPPORT_FROM_ADDRESS,
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            email = BaseEmail(**email_data)
 | 
				
			||||||
 | 
					            email.send()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            # try to see if we have the IP and that if the ssh keys can
 | 
				
			||||||
 | 
					            # be configured
 | 
				
			||||||
 | 
					            new_host = manager.get_primary_ipv4(vm_id)
 | 
				
			||||||
 | 
					            logger.debug("New VM ID is {vm_id}".format(vm_id=vm_id))
 | 
				
			||||||
 | 
					            if new_host is not None:
 | 
				
			||||||
 | 
					                custom_user = CustomUser.objects.get(email=user.get('email'))
 | 
				
			||||||
 | 
					                if custom_user is not None:
 | 
				
			||||||
 | 
					                    public_keys = get_all_public_keys(custom_user)
 | 
				
			||||||
 | 
					                    keys = [{'value': key, 'state': True} for key in
 | 
				
			||||||
 | 
					                            public_keys]
 | 
				
			||||||
 | 
					                    if len(keys) > 0:
 | 
				
			||||||
 | 
					                        logger.debug(
 | 
				
			||||||
 | 
					                            "Calling configure on {host} for {num_keys} keys".format(
 | 
				
			||||||
 | 
					                                host=new_host, num_keys=len(keys)))
 | 
				
			||||||
 | 
					                        # Let's delay the task by 75 seconds to be sure
 | 
				
			||||||
 | 
					                        # that we run the cdist configure after the host
 | 
				
			||||||
 | 
					                        # is up
 | 
				
			||||||
 | 
					                        manager.manage_public_key(keys,
 | 
				
			||||||
 | 
					                                                  hosts=[new_host],
 | 
				
			||||||
 | 
					                                                  countdown=75)
 | 
				
			||||||
    except Exception as e:
 | 
					    except Exception as e:
 | 
				
			||||||
        logger.error(str(e))
 | 
					        logger.error(str(e))
 | 
				
			||||||
        try:
 | 
					        try:
 | 
				
			||||||
| 
						 | 
					@ -134,8 +200,8 @@ def create_vm_task(self, vm_template_id, user, specs, template,
 | 
				
			||||||
            email_data = {
 | 
					            email_data = {
 | 
				
			||||||
                'subject': '{} CELERY TASK ERROR: {}'.format(settings.DCL_TEXT,
 | 
					                'subject': '{} CELERY TASK ERROR: {}'.format(settings.DCL_TEXT,
 | 
				
			||||||
                                                             msg_text),
 | 
					                                                             msg_text),
 | 
				
			||||||
                'from_email': settings.DCL_SUPPORT_FROM_ADDRESS,
 | 
					                'from_email': current_task.request.hostname,
 | 
				
			||||||
                'to': ['info@ungleich.ch'],
 | 
					                'to': settings.DCL_ERROR_EMAILS_TO_LIST,
 | 
				
			||||||
                'body': ',\n'.join(str(i) for i in self.request.args)
 | 
					                'body': ',\n'.join(str(i) for i in self.request.args)
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            email = EmailMessage(**email_data)
 | 
					            email = EmailMessage(**email_data)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -115,8 +115,8 @@ class CeleryTaskTestCase(TestCase):
 | 
				
			||||||
                'response_object').stripe_plan_id}])
 | 
					                'response_object').stripe_plan_id}])
 | 
				
			||||||
        stripe_subscription_obj = subscription_result.get('response_object')
 | 
					        stripe_subscription_obj = subscription_result.get('response_object')
 | 
				
			||||||
        # Check if the subscription was approved and is active
 | 
					        # Check if the subscription was approved and is active
 | 
				
			||||||
        if stripe_subscription_obj is None or \
 | 
					        if stripe_subscription_obj is None \
 | 
				
			||||||
                        stripe_subscription_obj.status != 'active':
 | 
					                or stripe_subscription_obj.status != 'active':
 | 
				
			||||||
            msg = subscription_result.get('error')
 | 
					            msg = subscription_result.get('error')
 | 
				
			||||||
            raise Exception("Creating subscription failed: {}".format(msg))
 | 
					            raise Exception("Creating subscription failed: {}".format(msg))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -173,7 +173,8 @@ TEMPLATES = [
 | 
				
			||||||
                 os.path.join(PROJECT_DIR, 'nosystemd/templates/'),
 | 
					                 os.path.join(PROJECT_DIR, 'nosystemd/templates/'),
 | 
				
			||||||
                 os.path.join(PROJECT_DIR,
 | 
					                 os.path.join(PROJECT_DIR,
 | 
				
			||||||
                              'ungleich/templates/djangocms_blog/'),
 | 
					                              'ungleich/templates/djangocms_blog/'),
 | 
				
			||||||
                 os.path.join(PROJECT_DIR, 'ungleich/templates/cms/ungleichch'),
 | 
					                 os.path.join(PROJECT_DIR,
 | 
				
			||||||
 | 
					                              'ungleich/templates/cms/ungleichch'),
 | 
				
			||||||
                 os.path.join(PROJECT_DIR, 'ungleich/templates/ungleich'),
 | 
					                 os.path.join(PROJECT_DIR, 'ungleich/templates/ungleich'),
 | 
				
			||||||
                 os.path.join(PROJECT_DIR,
 | 
					                 os.path.join(PROJECT_DIR,
 | 
				
			||||||
                              'ungleich_page/templates/ungleich_page'),
 | 
					                              'ungleich_page/templates/ungleich_page'),
 | 
				
			||||||
| 
						 | 
					@ -559,9 +560,21 @@ CELERY_RESULT_BACKEND = env('CELERY_RESULT_BACKEND')
 | 
				
			||||||
CELERY_ACCEPT_CONTENT = ['application/json']
 | 
					CELERY_ACCEPT_CONTENT = ['application/json']
 | 
				
			||||||
CELERY_TASK_SERIALIZER = 'json'
 | 
					CELERY_TASK_SERIALIZER = 'json'
 | 
				
			||||||
CELERY_RESULT_SERIALIZER = 'json'
 | 
					CELERY_RESULT_SERIALIZER = 'json'
 | 
				
			||||||
CELERY_TIMEZONE = 'Europe/Zurich'
 | 
					# CELERY_TIMEZONE = 'Europe/Zurich'
 | 
				
			||||||
CELERY_MAX_RETRIES = int_env('CELERY_MAX_RETRIES', 5)
 | 
					CELERY_MAX_RETRIES = int_env('CELERY_MAX_RETRIES', 5)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					DCL_ERROR_EMAILS_TO = env('DCL_ERROR_EMAILS_TO')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					DCL_ERROR_EMAILS_TO_LIST = []
 | 
				
			||||||
 | 
					if DCL_ERROR_EMAILS_TO is not None:
 | 
				
			||||||
 | 
					    DCL_ERROR_EMAILS_TO_LIST = [x.strip() for x in
 | 
				
			||||||
 | 
					                                DCL_ERROR_EMAILS_TO.split(
 | 
				
			||||||
 | 
					                                            ',')] \
 | 
				
			||||||
 | 
					        if "," in DCL_ERROR_EMAILS_TO else [DCL_ERROR_EMAILS_TO.strip()]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					if 'info@ungleich.ch' not in DCL_ERROR_EMAILS_TO_LIST:
 | 
				
			||||||
 | 
					    DCL_ERROR_EMAILS_TO_LIST.append('info@ungleich.ch')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
ENABLE_DEBUG_LOGGING = bool_env('ENABLE_DEBUG_LOGGING')
 | 
					ENABLE_DEBUG_LOGGING = bool_env('ENABLE_DEBUG_LOGGING')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
if ENABLE_DEBUG_LOGGING:
 | 
					if ENABLE_DEBUG_LOGGING:
 | 
				
			||||||
| 
						 | 
					@ -585,6 +598,9 @@ if ENABLE_DEBUG_LOGGING:
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					TEST_MANAGE_SSH_KEY_PUBKEY = env('TEST_MANAGE_SSH_KEY_PUBKEY')
 | 
				
			||||||
 | 
					TEST_MANAGE_SSH_KEY_HOST = env('TEST_MANAGE_SSH_KEY_HOST')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
DEBUG = bool_env('DEBUG')
 | 
					DEBUG = bool_env('DEBUG')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
if DEBUG:
 | 
					if DEBUG:
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,16 +1,22 @@
 | 
				
			||||||
import datetime
 | 
					import datetime
 | 
				
			||||||
 | 
					import logging
 | 
				
			||||||
 | 
					import subprocess
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import tempfile
 | 
				
			||||||
from django import forms
 | 
					from django import forms
 | 
				
			||||||
from membership.models import CustomUser
 | 
					 | 
				
			||||||
from django.contrib.auth import authenticate
 | 
					from django.contrib.auth import authenticate
 | 
				
			||||||
 | 
					 | 
				
			||||||
from django.utils.translation import ugettext_lazy as _
 | 
					from django.utils.translation import ugettext_lazy as _
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from membership.models import CustomUser
 | 
				
			||||||
 | 
					from utils.hosting_utils import get_all_public_keys
 | 
				
			||||||
from .models import UserHostingKey
 | 
					from .models import UserHostingKey
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					logger = logging.getLogger(__name__)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def generate_ssh_key_name():
 | 
					def generate_ssh_key_name():
 | 
				
			||||||
    return 'dcl-generated-key-' + datetime.datetime.now().strftime('%m%d%y%H%M')
 | 
					    return 'dcl-generated-key-' + datetime.datetime.now().strftime(
 | 
				
			||||||
 | 
					        '%m%d%y%H%M')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class HostingUserLoginForm(forms.Form):
 | 
					class HostingUserLoginForm(forms.Form):
 | 
				
			||||||
| 
						 | 
					@ -38,9 +44,7 @@ class HostingUserLoginForm(forms.Form):
 | 
				
			||||||
            CustomUser.objects.get(email=email)
 | 
					            CustomUser.objects.get(email=email)
 | 
				
			||||||
            return email
 | 
					            return email
 | 
				
			||||||
        except CustomUser.DoesNotExist:
 | 
					        except CustomUser.DoesNotExist:
 | 
				
			||||||
            raise forms.ValidationError("User does not exist")
 | 
					            raise forms.ValidationError(_("User does not exist"))
 | 
				
			||||||
        else:
 | 
					 | 
				
			||||||
            return email
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class HostingUserSignupForm(forms.ModelForm):
 | 
					class HostingUserSignupForm(forms.ModelForm):
 | 
				
			||||||
| 
						 | 
					@ -51,7 +55,8 @@ class HostingUserSignupForm(forms.ModelForm):
 | 
				
			||||||
        model = CustomUser
 | 
					        model = CustomUser
 | 
				
			||||||
        fields = ['name', 'email', 'password']
 | 
					        fields = ['name', 'email', 'password']
 | 
				
			||||||
        widgets = {
 | 
					        widgets = {
 | 
				
			||||||
            'name': forms.TextInput(attrs={'placeholder': 'Enter your name or company name'}),
 | 
					            'name': forms.TextInput(
 | 
				
			||||||
 | 
					                attrs={'placeholder': 'Enter your name or company name'}),
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def clean_confirm_password(self):
 | 
					    def clean_confirm_password(self):
 | 
				
			||||||
| 
						 | 
					@ -65,19 +70,55 @@ class HostingUserSignupForm(forms.ModelForm):
 | 
				
			||||||
class UserHostingKeyForm(forms.ModelForm):
 | 
					class UserHostingKeyForm(forms.ModelForm):
 | 
				
			||||||
    private_key = forms.CharField(widget=forms.HiddenInput(), required=False)
 | 
					    private_key = forms.CharField(widget=forms.HiddenInput(), required=False)
 | 
				
			||||||
    public_key = forms.CharField(widget=forms.Textarea(
 | 
					    public_key = forms.CharField(widget=forms.Textarea(
 | 
				
			||||||
        attrs={'class': 'form_public_key', 'placeholder': _('Paste here your public key')}),
 | 
					        attrs={'class': 'form_public_key',
 | 
				
			||||||
 | 
					               'placeholder': _('Paste here your public key')}),
 | 
				
			||||||
        required=False,
 | 
					        required=False,
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    user = forms.models.ModelChoiceField(queryset=CustomUser.objects.all(),
 | 
					    user = forms.models.ModelChoiceField(queryset=CustomUser.objects.all(),
 | 
				
			||||||
                                         required=False, widget=forms.HiddenInput())
 | 
					                                         required=False,
 | 
				
			||||||
 | 
					                                         widget=forms.HiddenInput())
 | 
				
			||||||
    name = forms.CharField(required=False, widget=forms.TextInput(
 | 
					    name = forms.CharField(required=False, widget=forms.TextInput(
 | 
				
			||||||
        attrs={'class': 'form_key_name', 'placeholder': _('Give a name to your key')}))
 | 
					        attrs={'class': 'form_key_name',
 | 
				
			||||||
 | 
					               'placeholder': _('Give a name to your key')}))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __init__(self, *args, **kwargs):
 | 
					    def __init__(self, *args, **kwargs):
 | 
				
			||||||
        self.request = kwargs.pop("request")
 | 
					        self.request = kwargs.pop("request")
 | 
				
			||||||
        super(UserHostingKeyForm, self).__init__(*args, **kwargs)
 | 
					        super(UserHostingKeyForm, self).__init__(*args, **kwargs)
 | 
				
			||||||
        self.fields['name'].label = _('Key name')
 | 
					        self.fields['name'].label = _('Key name')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def clean_public_key(self):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Validates a public ssh key using `ssh-keygen -lf key.pub`
 | 
				
			||||||
 | 
					        Also checks if a given key already exists in the database and
 | 
				
			||||||
 | 
					        alerts the user of it.
 | 
				
			||||||
 | 
					        :return:
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        if 'generate' in self.request.POST:
 | 
				
			||||||
 | 
					            return self.data.get('public_key')
 | 
				
			||||||
 | 
					        KEY_ERROR_MESSAGE = _("Please input a proper SSH key")
 | 
				
			||||||
 | 
					        openssh_pubkey_str = self.data.get('public_key').strip()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if openssh_pubkey_str in get_all_public_keys(self.request.user):
 | 
				
			||||||
 | 
					            key_name = UserHostingKey.objects.filter(
 | 
				
			||||||
 | 
					                user_id=self.request.user.id,
 | 
				
			||||||
 | 
					                public_key=openssh_pubkey_str).first().name
 | 
				
			||||||
 | 
					            KEY_EXISTS_MESSAGE = _(
 | 
				
			||||||
 | 
					                "This key exists already with the name \"%(name)s\"") % {
 | 
				
			||||||
 | 
					                                     'name': key_name}
 | 
				
			||||||
 | 
					            raise forms.ValidationError(KEY_EXISTS_MESSAGE)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        with tempfile.NamedTemporaryFile(delete=True) as tmp_public_key_file:
 | 
				
			||||||
 | 
					            tmp_public_key_file.write(openssh_pubkey_str.encode('utf-8'))
 | 
				
			||||||
 | 
					            tmp_public_key_file.flush()
 | 
				
			||||||
 | 
					            try:
 | 
				
			||||||
 | 
					                subprocess.check_output(
 | 
				
			||||||
 | 
					                    ['ssh-keygen', '-lf', tmp_public_key_file.name])
 | 
				
			||||||
 | 
					            except subprocess.CalledProcessError as cpe:
 | 
				
			||||||
 | 
					                logger.debug(
 | 
				
			||||||
 | 
					                    "Not a correct ssh format {error}".format(error=str(cpe)))
 | 
				
			||||||
 | 
					                raise forms.ValidationError(KEY_ERROR_MESSAGE)
 | 
				
			||||||
 | 
					        return openssh_pubkey_str
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def clean_name(self):
 | 
					    def clean_name(self):
 | 
				
			||||||
        return self.data.get('name')
 | 
					        return self.data.get('name')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -24,6 +24,9 @@ msgstr "Dein Benutzername und/oder Dein Passwort ist falsch."
 | 
				
			||||||
msgid "Your account is not activated yet."
 | 
					msgid "Your account is not activated yet."
 | 
				
			||||||
msgstr "Dein Account wurde noch nicht aktiviert."
 | 
					msgstr "Dein Account wurde noch nicht aktiviert."
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					msgid "User does not exist"
 | 
				
			||||||
 | 
					msgstr "Der Benutzer existiert nicht"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
msgid "Paste here your public key"
 | 
					msgid "Paste here your public key"
 | 
				
			||||||
msgstr "Füge deinen Public Key ein"
 | 
					msgstr "Füge deinen Public Key ein"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -33,6 +36,13 @@ msgstr "Gebe deinem SSH-Key einen Name"
 | 
				
			||||||
msgid "Key name"
 | 
					msgid "Key name"
 | 
				
			||||||
msgstr "Key-Name"
 | 
					msgstr "Key-Name"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					msgid "Please input a proper SSH key"
 | 
				
			||||||
 | 
					msgstr "Bitte verwende einen gültigen SSH-Key"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#, python-format
 | 
				
			||||||
 | 
					msgid "This key exists already with the name \"%(name)s\""
 | 
				
			||||||
 | 
					msgstr "Der SSH-Key mit dem Name \"%(name)s\" existiert bereits"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
msgid "All Rights Reserved"
 | 
					msgid "All Rights Reserved"
 | 
				
			||||||
msgstr "Alle Rechte vorbehalten"
 | 
					msgstr "Alle Rechte vorbehalten"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,7 +1,37 @@
 | 
				
			||||||
 | 
					$(document).ready(function () {
 | 
				
			||||||
 | 
					    var create_vm_form = $('#virtual_machine_create_form');
 | 
				
			||||||
 | 
					    create_vm_form.submit(function () {
 | 
				
			||||||
 | 
					        $('#btn-create-vm').prop('disabled', true);
 | 
				
			||||||
 | 
					        $.ajax({
 | 
				
			||||||
 | 
					            url: create_vm_form.attr('action'),
 | 
				
			||||||
 | 
					            type: 'POST',
 | 
				
			||||||
 | 
					            data: create_vm_form.serialize(),
 | 
				
			||||||
 | 
					            success: function (data) {
 | 
				
			||||||
 | 
					                if (data.status === true) {
 | 
				
			||||||
 | 
					                    fa_icon = $('.modal-icon > .fa');
 | 
				
			||||||
 | 
					                    fa_icon.attr('class', 'fa fa-check');
 | 
				
			||||||
 | 
					                    $('.modal-header > .close').attr('class', 'close');
 | 
				
			||||||
 | 
					                    $('#createvm-modal-title').text(data.msg_title);
 | 
				
			||||||
 | 
					                    $('#createvm-modal-body').text(data.msg_body);
 | 
				
			||||||
 | 
					                    $('#createvm-modal').on('hidden.bs.modal', function () {
 | 
				
			||||||
 | 
					                        window.location = data.redirect;
 | 
				
			||||||
 | 
					                    })
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            error: function (xmlhttprequest, textstatus, message) {
 | 
				
			||||||
 | 
					                    fa_icon = $('.modal-icon > .fa');
 | 
				
			||||||
 | 
					                    fa_icon.attr('class', 'fa fa-times');
 | 
				
			||||||
 | 
					                    $('.modal-header > .close').attr('class', 'close');
 | 
				
			||||||
 | 
					                    if (typeof(create_vm_error_message) !== 'undefined') {
 | 
				
			||||||
 | 
					                        $('#createvm-modal-title').text(create_vm_error_message);
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    $('#btn-create-vm').prop('disabled', false);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        return false;
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
$( document ).ready(function() {
 | 
					    $('#confirm-cancel').on('click', '.btn-ok', function (e) {
 | 
				
			||||||
 | 
					 | 
				
			||||||
	$('#confirm-cancel').on('click', '.btn-ok', function(e) {
 | 
					 | 
				
			||||||
        $('#virtual_machine_cancel_form').trigger('submit');
 | 
					        $('#virtual_machine_cancel_form').trigger('submit');
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,8 @@
 | 
				
			||||||
{% extends "hosting/base_short.html" %}
 | 
					{% extends "hosting/base_short.html" %}
 | 
				
			||||||
{% load staticfiles bootstrap3 %}
 | 
					{% load staticfiles bootstrap3 %}
 | 
				
			||||||
{% load i18n %}
 | 
					{% load i18n %}
 | 
				
			||||||
 | 
					{% load custom_tags %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{% block content %}
 | 
					{% block content %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<div class="order-detail-container">
 | 
					<div class="order-detail-container">
 | 
				
			||||||
| 
						 | 
					@ -20,30 +22,58 @@
 | 
				
			||||||
    <div class="row">
 | 
					    <div class="row">
 | 
				
			||||||
        <div class="col-xs-12 col-md-8 col-md-offset-2">
 | 
					        <div class="col-xs-12 col-md-8 col-md-offset-2">
 | 
				
			||||||
            <div class="invoice-title">
 | 
					            <div class="invoice-title">
 | 
				
			||||||
    			<h2>{{page_header_text}}</h2><h3 class="pull-right">{% trans "Order #"%} {{order.id}}</h3>
 | 
					                <h2>{{page_header_text}}</h2>
 | 
				
			||||||
 | 
					                <h3 class="pull-right">
 | 
				
			||||||
 | 
					                    {% if order %}
 | 
				
			||||||
 | 
					                    {% trans "Order #"%} {{order.id}}
 | 
				
			||||||
 | 
					                    {% endif %}
 | 
				
			||||||
 | 
					                </h3>
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
            <hr>
 | 
					            <hr>
 | 
				
			||||||
            <div class="row">
 | 
					            <div class="row">
 | 
				
			||||||
                <div class="col-xs-12 col-md-6 pull-right order-confirm-date">
 | 
					                <div class="col-xs-12 col-md-6 pull-right order-confirm-date">
 | 
				
			||||||
                    <address>
 | 
					                    <address>
 | 
				
			||||||
                        <strong>{% trans "Date"%}:</strong><br>
 | 
					                        <strong>{% trans "Date"%}:</strong><br>
 | 
				
			||||||
                        <span id="order-created_at">{{order.created_at|date:'Y-m-d H:i'}}</span><br><br>
 | 
					                        <span id="order-created_at">
 | 
				
			||||||
 | 
					                                {% if order %}
 | 
				
			||||||
 | 
					                                    {{order.created_at|date:'Y-m-d H:i'}}
 | 
				
			||||||
 | 
					                                {% else %}
 | 
				
			||||||
 | 
					                                    {% now "Y-m-d H:i" %}
 | 
				
			||||||
 | 
					                                {% endif %}
 | 
				
			||||||
 | 
					                            </span><br><br>
 | 
				
			||||||
 | 
					                        {% if order %}
 | 
				
			||||||
                        <strong>{% trans "Status:"%}</strong><br>
 | 
					                        <strong>{% trans "Status:"%}</strong><br>
 | 
				
			||||||
                        {% if order.status == 'Approved' %}
 | 
					                        {% if order.status == 'Approved' %}
 | 
				
			||||||
                            <strong class="text-success">{% trans "Approved" %}</strong>
 | 
					                        <strong class="text-success">
 | 
				
			||||||
 | 
					                            {% trans "Approved" %}
 | 
				
			||||||
 | 
					                        </strong>
 | 
				
			||||||
                        {% else %}
 | 
					                        {% else %}
 | 
				
			||||||
                            <strong class="text-danger">{% trans "Declined" %}</strong>
 | 
					                        <strong class="text-danger">
 | 
				
			||||||
 | 
					                            {% trans "Declined" %}
 | 
				
			||||||
 | 
					                        </strong>
 | 
				
			||||||
                        {% endif %}
 | 
					                        {% endif %}
 | 
				
			||||||
                        <br><br>
 | 
					                        <br><br>
 | 
				
			||||||
 | 
					                        {% endif %}
 | 
				
			||||||
                    </address>
 | 
					                    </address>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
                <div class="col-xs-12 col-md-6">
 | 
					                <div class="col-xs-12 col-md-6">
 | 
				
			||||||
                    <address>
 | 
					                    <address>
 | 
				
			||||||
                        <h3><b>{% trans "Billed To:"%}</b></h3>
 | 
					                        <h3><b>{% trans "Billed To:"%}</b></h3>
 | 
				
			||||||
 | 
					                        {% if order %}
 | 
				
			||||||
                        {{user.name}}<br>
 | 
					                        {{user.name}}<br>
 | 
				
			||||||
                        {{order.billing_address.street_address}},{{order.billing_address.postal_code}}<br>
 | 
					                        {{order.billing_address.street_address}},{{order.billing_address.postal_code}}<br>
 | 
				
			||||||
                        {{order.billing_address.city}}, {{order.billing_address.country}}.
 | 
					                        {{order.billing_address.city}},
 | 
				
			||||||
 | 
					                        {{order.billing_address.country}}.
 | 
				
			||||||
 | 
					                        {% else %}
 | 
				
			||||||
 | 
					                        {% with request.session.billing_address_data as billing_address %}
 | 
				
			||||||
 | 
					                        {{billing_address|get_value_from_dict:'cardholder_name'}}<br>
 | 
				
			||||||
 | 
					                        {{billing_address|get_value_from_dict:'street_address'}},
 | 
				
			||||||
 | 
					                        {{billing_address|get_value_from_dict:'postal_code'}}<br>
 | 
				
			||||||
 | 
					                        {{billing_address|get_value_from_dict:'city'}},
 | 
				
			||||||
 | 
					                        {{billing_address|get_value_from_dict:'country'}}.
 | 
				
			||||||
 | 
					                        {% endwith %}
 | 
				
			||||||
 | 
					                        {% endif %}
 | 
				
			||||||
                    </address>
 | 
					                    </address>
 | 
				
			||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -52,8 +82,15 @@
 | 
				
			||||||
                <div class="col-xs-6">
 | 
					                <div class="col-xs-6">
 | 
				
			||||||
                    <address>
 | 
					                    <address>
 | 
				
			||||||
                        <strong>{% trans "Payment Method:"%}</strong><br>
 | 
					                        <strong>{% trans "Payment Method:"%}</strong><br>
 | 
				
			||||||
    					{{order.cc_brand}} {% trans "ending in" %} **** {{order.last4}}<br>
 | 
					                        {% if order %}
 | 
				
			||||||
 | 
					                        {{order.cc_brand}} {% trans "ending in" %} ****
 | 
				
			||||||
 | 
					                        {{order.last4}}<br>
 | 
				
			||||||
                        {{user.email}}
 | 
					                        {{user.email}}
 | 
				
			||||||
 | 
					                        {% else %}
 | 
				
			||||||
 | 
					                        {{cc_brand}} {% trans "ending in" %} ****
 | 
				
			||||||
 | 
					                        {{cc_last4}}<br>
 | 
				
			||||||
 | 
					                        {{request.session.user.email}}
 | 
				
			||||||
 | 
					                        {% endif %}
 | 
				
			||||||
                    </address>
 | 
					                    </address>
 | 
				
			||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
| 
						 | 
					@ -65,31 +102,111 @@
 | 
				
			||||||
            <h3><b>{% trans "Order summary"%}</b></h3>
 | 
					            <h3><b>{% trans "Order summary"%}</b></h3>
 | 
				
			||||||
            <hr>
 | 
					            <hr>
 | 
				
			||||||
            <div class="content">
 | 
					            <div class="content">
 | 
				
			||||||
                <p><b>{% trans "Cores"%}</b> <span class="pull-right">{{vm.cores}}</span></p>
 | 
					                {% if request.session.specs %}
 | 
				
			||||||
 | 
					                {% with request.session.specs as vm %}
 | 
				
			||||||
 | 
					                <p><b>{% trans "Cores"%}</b>
 | 
				
			||||||
 | 
					                    <span class="pull-right">{{vm.cpu}}</span>
 | 
				
			||||||
 | 
					                </p>
 | 
				
			||||||
                <hr>
 | 
					                <hr>
 | 
				
			||||||
                <p><b>{% trans "Memory"%}</b> <span class="pull-right">{{vm.memory}} GB</span></p>
 | 
					                <p><b>{% trans "Memory"%}</b>
 | 
				
			||||||
 | 
					                    <span class="pull-right">{{vm.memory}} GB</span>
 | 
				
			||||||
 | 
					                </p>
 | 
				
			||||||
                <hr>
 | 
					                <hr>
 | 
				
			||||||
                <p><b>{% trans "Disk space"%}</b> <span class="pull-right">{{vm.disk_size}} GB</span></p>
 | 
					                <p><b>{% trans "Disk space"%}</b>
 | 
				
			||||||
 | 
					                    <span class="pull-right">{{vm.disk_size}} GB</span>
 | 
				
			||||||
 | 
					                </p>
 | 
				
			||||||
                <hr>
 | 
					                <hr>
 | 
				
			||||||
                                <h4>{% trans "Total"%}<p class="pull-right"><b>{{vm.price}} CHF</b></p></h4>
 | 
					                <p><b>{% trans "Configuration"%}</b>
 | 
				
			||||||
 | 
					                    <span class="pull-right">{{request.session.template.name}}</span>
 | 
				
			||||||
 | 
					                </p>
 | 
				
			||||||
 | 
					                <hr>
 | 
				
			||||||
 | 
					                <h4>{% trans "Total"%}
 | 
				
			||||||
 | 
					                    <p class="pull-right">
 | 
				
			||||||
 | 
					                        <b>{{vm.price}} CHF</b>
 | 
				
			||||||
 | 
					                        <span class="dcl-price-month"> /{% trans "Month" %}
 | 
				
			||||||
 | 
					                                </span>
 | 
				
			||||||
 | 
					                    </p>
 | 
				
			||||||
 | 
					                </h4>
 | 
				
			||||||
 | 
					                {% endwith %}
 | 
				
			||||||
 | 
					                {% else %}
 | 
				
			||||||
 | 
					                <p><b>{% trans "Cores"%}</b>
 | 
				
			||||||
 | 
					                    <span class="pull-right">{{vm.cores}}</span>
 | 
				
			||||||
 | 
					                </p>
 | 
				
			||||||
 | 
					                <hr>
 | 
				
			||||||
 | 
					                <p><b>{% trans "Memory"%}</b>
 | 
				
			||||||
 | 
					                    <span class="pull-right">{{vm.memory}} GB</span>
 | 
				
			||||||
 | 
					                </p>
 | 
				
			||||||
 | 
					                <hr>
 | 
				
			||||||
 | 
					                <p><b>{% trans "Disk space"%}</b>
 | 
				
			||||||
 | 
					                    <span class="pull-right">{{vm.disk_size}} GB</span>
 | 
				
			||||||
 | 
					                </p>
 | 
				
			||||||
 | 
					                <hr>
 | 
				
			||||||
 | 
					                <h4>{% trans "Total"%}<p class="pull-right"><b>{{vm.price}}
 | 
				
			||||||
 | 
					                    CHF</b><span
 | 
				
			||||||
 | 
					                        class="dcl-price-month"> /{% trans "Month" %}</span>
 | 
				
			||||||
 | 
					                </p></h4>
 | 
				
			||||||
 | 
					                {% endif %}
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
            <br/>
 | 
					            <br/>
 | 
				
			||||||
            {% url 'hosting:payment' as payment_url %}
 | 
					            {% if not order %}
 | 
				
			||||||
            {% if payment_url in request.META.HTTP_REFERER  %}
 | 
					            <form method="post" id="virtual_machine_create_form">
 | 
				
			||||||
            <div class=" content pull-right">
 | 
					                {% csrf_token %}
 | 
				
			||||||
                <a href="{% url 'hosting:virtual_machines'%}" ><button class="btn btn-info">{% trans "Finish Configuration"%}</button></a>
 | 
					                <div class="row">
 | 
				
			||||||
 | 
					                    <div class="col-sm-8">
 | 
				
			||||||
 | 
					                        <p class="dcl-place-order-text">{% blocktrans with vm_price=request.session.specs.price %}By clicking "Place order" this plan will charge your credit card account with the fee of {{ vm_price }}CHF/month{% endblocktrans %}.</p>
 | 
				
			||||||
                    </div>
 | 
					                    </div>
 | 
				
			||||||
 | 
					                    <div class="col-sm-4 content">
 | 
				
			||||||
 | 
					                        <button class="btn btn-info pull-right"
 | 
				
			||||||
 | 
					                                id="btn-create-vm"
 | 
				
			||||||
 | 
					                                data-href="{% url 'hosting:order-confirmation' %}"
 | 
				
			||||||
 | 
					                                data-toggle="modal"
 | 
				
			||||||
 | 
					                                data-target="#createvm-modal">
 | 
				
			||||||
 | 
					                            {% trans "Place order"%}
 | 
				
			||||||
 | 
					                        </button>
 | 
				
			||||||
 | 
					                    </div>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					            </form>
 | 
				
			||||||
            {% endif %}
 | 
					            {% endif %}
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
    {% endif %}
 | 
					    {% endif %}
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
 | 
					<!-- Create VM Modal -->
 | 
				
			||||||
 | 
					<div class="modal fade" id="createvm-modal" tabindex="-1" role="dialog"
 | 
				
			||||||
 | 
					     aria-hidden="true" data-backdrop="static" data-keyboard="false">
 | 
				
			||||||
 | 
					    <div class="modal-dialog">
 | 
				
			||||||
 | 
					        <div class="modal-content">
 | 
				
			||||||
 | 
					            <div class="modal-header">
 | 
				
			||||||
 | 
					                <button type="button" class="close hidden" data-dismiss="modal"
 | 
				
			||||||
 | 
					                        aria-label="create-vm-close">
 | 
				
			||||||
 | 
					                    <span aria-hidden="true">×</span>
 | 
				
			||||||
 | 
					                </button>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            <div class="modal-body">
 | 
				
			||||||
 | 
					                <div class="modal-icon">
 | 
				
			||||||
 | 
					                    <i class="fa fa-cog fa-spin fa-3x fa-fw"></i>
 | 
				
			||||||
 | 
					                    <span class="sr-only">{% trans "Processing..." %}</span>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					                <h4 class="modal-title" id="createvm-modal-title">
 | 
				
			||||||
 | 
					                </h4>
 | 
				
			||||||
 | 
					                <div class="modal-text" id="createvm-modal-body">
 | 
				
			||||||
 | 
					                    {% trans "Hold tight, we are processing your request" %}
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					                <div class="modal-footer">
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					<!-- / Create VM Modal -->
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script type="text/javascript">
 | 
					<script type="text/javascript">
 | 
				
			||||||
 | 
					    {% trans "Some problem encountered. Please try again later." as err_msg %}
 | 
				
			||||||
 | 
					    var create_vm_error_message = '{{err_msg|safe}}.';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    window.onload = function () {
 | 
					    window.onload = function () {
 | 
				
			||||||
            var locale_date = moment.utc(document.getElementById("order-created_at").textContent,'YYYY-MM-DD HH:mm').toDate();
 | 
					        var locale_date = moment.utc(document.getElementById("order-created_at").textContent, 'YYYY-MM-DD HH:mm').toDate();
 | 
				
			||||||
        locale_date = moment(locale_date).format("YYYY-MM-DD h:mm:ss a");
 | 
					        locale_date = moment(locale_date).format("YYYY-MM-DD h:mm:ss a");
 | 
				
			||||||
        document.getElementById('order-created_at').innerHTML = locale_date;
 | 
					        document.getElementById('order-created_at').innerHTML = locale_date;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -11,7 +11,6 @@ from .views import (
 | 
				
			||||||
    SSHKeyChoiceView, DashboardView, SettingsView)
 | 
					    SSHKeyChoiceView, DashboardView, SettingsView)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
urlpatterns = [
 | 
					urlpatterns = [
 | 
				
			||||||
    url(r'index/?$', IndexView.as_view(), name='index'),
 | 
					    url(r'index/?$', IndexView.as_view(), name='index'),
 | 
				
			||||||
    url(r'django/?$', DjangoHostingView.as_view(), name='djangohosting'),
 | 
					    url(r'django/?$', DjangoHostingView.as_view(), name='djangohosting'),
 | 
				
			||||||
| 
						 | 
					@ -22,9 +21,13 @@ urlpatterns = [
 | 
				
			||||||
    url(r'payment/?$', PaymentVMView.as_view(), name='payment'),
 | 
					    url(r'payment/?$', PaymentVMView.as_view(), name='payment'),
 | 
				
			||||||
    url(r'settings/?$', SettingsView.as_view(), name='settings'),
 | 
					    url(r'settings/?$', SettingsView.as_view(), name='settings'),
 | 
				
			||||||
    url(r'orders/?$', OrdersHostingListView.as_view(), name='orders'),
 | 
					    url(r'orders/?$', OrdersHostingListView.as_view(), name='orders'),
 | 
				
			||||||
    url(r'orders/(?P<pk>\d+)/?$', OrdersHostingDetailView.as_view(), name='orders'),
 | 
					    url(r'order-confirmation/?$', OrdersHostingDetailView.as_view(),
 | 
				
			||||||
 | 
					        name='order-confirmation'),
 | 
				
			||||||
 | 
					    url(r'orders/(?P<pk>\d+)/?$', OrdersHostingDetailView.as_view(),
 | 
				
			||||||
 | 
					        name='orders'),
 | 
				
			||||||
    url(r'bills/?$', HostingBillListView.as_view(), name='bills'),
 | 
					    url(r'bills/?$', HostingBillListView.as_view(), name='bills'),
 | 
				
			||||||
    url(r'bills/(?P<pk>\d+)/?$', HostingBillDetailView.as_view(), name='bills'),
 | 
					    url(r'bills/(?P<pk>\d+)/?$', HostingBillDetailView.as_view(),
 | 
				
			||||||
 | 
					        name='bills'),
 | 
				
			||||||
    url(r'cancel_order/(?P<pk>\d+)/?$',
 | 
					    url(r'cancel_order/(?P<pk>\d+)/?$',
 | 
				
			||||||
        OrdersHostingDeleteView.as_view(), name='delete_order'),
 | 
					        OrdersHostingDeleteView.as_view(), name='delete_order'),
 | 
				
			||||||
    url(r'create_virtual_machine/?$', CreateVirtualMachinesView.as_view(),
 | 
					    url(r'create_virtual_machine/?$', CreateVirtualMachinesView.as_view(),
 | 
				
			||||||
| 
						 | 
					@ -41,13 +44,16 @@ urlpatterns = [
 | 
				
			||||||
        name='delete_ssh_key'),
 | 
					        name='delete_ssh_key'),
 | 
				
			||||||
    url(r'create_ssh_key/?$', SSHKeyCreateView.as_view(),
 | 
					    url(r'create_ssh_key/?$', SSHKeyCreateView.as_view(),
 | 
				
			||||||
        name='create_ssh_key'),
 | 
					        name='create_ssh_key'),
 | 
				
			||||||
    url(r'^notifications/$', NotificationsView.as_view(), name='notifications'),
 | 
					    url(r'^notifications/$', NotificationsView.as_view(),
 | 
				
			||||||
 | 
					        name='notifications'),
 | 
				
			||||||
    url(r'^notifications/(?P<pk>\d+)/?$', MarkAsReadNotificationView.as_view(),
 | 
					    url(r'^notifications/(?P<pk>\d+)/?$', MarkAsReadNotificationView.as_view(),
 | 
				
			||||||
        name='read_notification'),
 | 
					        name='read_notification'),
 | 
				
			||||||
    url(r'login/?$', LoginView.as_view(), name='login'),
 | 
					    url(r'login/?$', LoginView.as_view(), name='login'),
 | 
				
			||||||
    url(r'signup/?$', SignupView.as_view(), name='signup'),
 | 
					    url(r'signup/?$', SignupView.as_view(), name='signup'),
 | 
				
			||||||
    url(r'signup-validate/?$', SignupValidateView.as_view(), name='signup-validate'),
 | 
					    url(r'signup-validate/?$', SignupValidateView.as_view(),
 | 
				
			||||||
    url(r'reset-password/?$', PasswordResetView.as_view(), name='reset_password'),
 | 
					        name='signup-validate'),
 | 
				
			||||||
 | 
					    url(r'reset-password/?$', PasswordResetView.as_view(),
 | 
				
			||||||
 | 
					        name='reset_password'),
 | 
				
			||||||
    url(r'reset-password-confirm/(?P<uidb64>[0-9A-Za-z]+)-(?P<token>.+)/$',
 | 
					    url(r'reset-password-confirm/(?P<uidb64>[0-9A-Za-z]+)-(?P<token>.+)/$',
 | 
				
			||||||
        PasswordResetConfirmView.as_view(), name='reset_password_confirm'),
 | 
					        PasswordResetConfirmView.as_view(), name='reset_password_confirm'),
 | 
				
			||||||
    url(r'^logout/?$', auth_views.logout,
 | 
					    url(r'^logout/?$', auth_views.logout,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										295
									
								
								hosting/views.py
									
										
									
									
									
								
							
							
						
						
									
										295
									
								
								hosting/views.py
									
										
									
									
									
								
							| 
						 | 
					@ -1,3 +1,5 @@
 | 
				
			||||||
 | 
					import json
 | 
				
			||||||
 | 
					import logging
 | 
				
			||||||
import uuid
 | 
					import uuid
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from django import forms
 | 
					from django import forms
 | 
				
			||||||
| 
						 | 
					@ -9,21 +11,22 @@ from django.core.exceptions import ValidationError
 | 
				
			||||||
from django.core.files.base import ContentFile
 | 
					from django.core.files.base import ContentFile
 | 
				
			||||||
from django.core.urlresolvers import reverse_lazy, reverse
 | 
					from django.core.urlresolvers import reverse_lazy, reverse
 | 
				
			||||||
from django.http import Http404
 | 
					from django.http import Http404
 | 
				
			||||||
from django.http import HttpResponseRedirect
 | 
					from django.http import HttpResponseRedirect, HttpResponse
 | 
				
			||||||
from django.shortcuts import redirect
 | 
					from django.shortcuts import redirect
 | 
				
			||||||
from django.shortcuts import render
 | 
					from django.shortcuts import render
 | 
				
			||||||
from django.utils.http import urlsafe_base64_decode
 | 
					from django.utils.http import urlsafe_base64_decode
 | 
				
			||||||
from django.utils.safestring import mark_safe
 | 
					from django.utils.safestring import mark_safe
 | 
				
			||||||
from django.utils.translation import ugettext_lazy as _
 | 
					from django.utils.translation import get_language, ugettext_lazy as _
 | 
				
			||||||
from django.views.generic import View, CreateView, FormView, ListView, \
 | 
					from django.views.generic import View, CreateView, FormView, ListView, \
 | 
				
			||||||
    DetailView, \
 | 
					    DetailView, \
 | 
				
			||||||
    DeleteView, TemplateView, UpdateView
 | 
					    DeleteView, TemplateView, UpdateView
 | 
				
			||||||
from guardian.mixins import PermissionRequiredMixin
 | 
					from guardian.mixins import PermissionRequiredMixin
 | 
				
			||||||
from oca.pool import WrongNameError, WrongIdError
 | 
					from oca.pool import WrongIdError
 | 
				
			||||||
from stored_messages.api import mark_read
 | 
					from stored_messages.api import mark_read
 | 
				
			||||||
from stored_messages.models import Message
 | 
					from stored_messages.models import Message
 | 
				
			||||||
from stored_messages.settings import stored_messages_settings
 | 
					from stored_messages.settings import stored_messages_settings
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from datacenterlight.tasks import create_vm_task
 | 
				
			||||||
from membership.models import CustomUser, StripeCustomer
 | 
					from membership.models import CustomUser, StripeCustomer
 | 
				
			||||||
from opennebula_api.models import OpenNebulaManager
 | 
					from opennebula_api.models import OpenNebulaManager
 | 
				
			||||||
from opennebula_api.serializers import VirtualMachineSerializer, \
 | 
					from opennebula_api.serializers import VirtualMachineSerializer, \
 | 
				
			||||||
| 
						 | 
					@ -41,8 +44,11 @@ from .models import HostingOrder, HostingBill, HostingPlan, UserHostingKey
 | 
				
			||||||
from datacenterlight.models import VMTemplate
 | 
					from datacenterlight.models import VMTemplate
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
CONNECTION_ERROR = "Your VMs cannot be displayed at the moment due to a backend \
 | 
					logger = logging.getLogger(__name__)
 | 
				
			||||||
                    connection error. please try again in a few minutes."
 | 
					
 | 
				
			||||||
 | 
					CONNECTION_ERROR = "Your VMs cannot be displayed at the moment due to a \
 | 
				
			||||||
 | 
					                    backend connection error. please try again in a few \
 | 
				
			||||||
 | 
					                    minutes."
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class DashboardView(View):
 | 
					class DashboardView(View):
 | 
				
			||||||
| 
						 | 
					@ -373,17 +379,14 @@ class SSHKeyDeleteView(LoginRequiredMixin, DeleteView):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def delete(self, request, *args, **kwargs):
 | 
					    def delete(self, request, *args, **kwargs):
 | 
				
			||||||
        owner = self.request.user
 | 
					        owner = self.request.user
 | 
				
			||||||
        manager = OpenNebulaManager()
 | 
					        manager = OpenNebulaManager(
 | 
				
			||||||
 | 
					            email=owner.email,
 | 
				
			||||||
 | 
					            password=owner.password
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
        pk = self.kwargs.get('pk')
 | 
					        pk = self.kwargs.get('pk')
 | 
				
			||||||
        # Get user ssh key
 | 
					        # Get user ssh key
 | 
				
			||||||
        public_key = UserHostingKey.objects.get(pk=pk).public_key
 | 
					        public_key = UserHostingKey.objects.get(pk=pk).public_key
 | 
				
			||||||
        # Add ssh key to user
 | 
					        manager.manage_public_key([{'value': public_key, 'state': False}])
 | 
				
			||||||
        try:
 | 
					 | 
				
			||||||
            manager.remove_public_key(user=owner, public_key=public_key)
 | 
					 | 
				
			||||||
        except ConnectionError:
 | 
					 | 
				
			||||||
            pass
 | 
					 | 
				
			||||||
        except WrongNameError:
 | 
					 | 
				
			||||||
            pass
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return super(SSHKeyDeleteView, self).delete(request, *args, **kwargs)
 | 
					        return super(SSHKeyDeleteView, self).delete(request, *args, **kwargs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -424,6 +427,13 @@ class SSHKeyChoiceView(LoginRequiredMixin, View):
 | 
				
			||||||
            user=request.user, public_key=public_key, name=name)
 | 
					            user=request.user, public_key=public_key, name=name)
 | 
				
			||||||
        filename = name + '_' + str(uuid.uuid4())[:8] + '_private.pem'
 | 
					        filename = name + '_' + str(uuid.uuid4())[:8] + '_private.pem'
 | 
				
			||||||
        ssh_key.private_key.save(filename, content)
 | 
					        ssh_key.private_key.save(filename, content)
 | 
				
			||||||
 | 
					        owner = self.request.user
 | 
				
			||||||
 | 
					        manager = OpenNebulaManager(
 | 
				
			||||||
 | 
					            email=owner.email,
 | 
				
			||||||
 | 
					            password=owner.password
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        public_key_str = public_key.decode()
 | 
				
			||||||
 | 
					        manager.manage_public_key([{'value': public_key_str, 'state': True}])
 | 
				
			||||||
        return redirect(reverse_lazy('hosting:ssh_keys'), foo='bar')
 | 
					        return redirect(reverse_lazy('hosting:ssh_keys'), foo='bar')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -468,23 +478,17 @@ class SSHKeyCreateView(LoginRequiredMixin, FormView):
 | 
				
			||||||
            })
 | 
					            })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        owner = self.request.user
 | 
					        owner = self.request.user
 | 
				
			||||||
        manager = OpenNebulaManager()
 | 
					        manager = OpenNebulaManager(
 | 
				
			||||||
 | 
					            email=owner.email,
 | 
				
			||||||
        # Get user ssh key
 | 
					            password=owner.password
 | 
				
			||||||
        public_key = str(form.cleaned_data.get('public_key', ''))
 | 
					        )
 | 
				
			||||||
        # Add ssh key to user
 | 
					        public_key = form.cleaned_data['public_key']
 | 
				
			||||||
        try:
 | 
					        if type(public_key) is bytes:
 | 
				
			||||||
            manager.add_public_key(
 | 
					            public_key = public_key.decode()
 | 
				
			||||||
                user=owner, public_key=public_key, merge=True)
 | 
					        manager.manage_public_key([{'value': public_key, 'state': True}])
 | 
				
			||||||
        except ConnectionError:
 | 
					 | 
				
			||||||
            pass
 | 
					 | 
				
			||||||
        except WrongNameError:
 | 
					 | 
				
			||||||
            pass
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return HttpResponseRedirect(self.success_url)
 | 
					        return HttpResponseRedirect(self.success_url)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def post(self, request, *args, **kwargs):
 | 
					    def post(self, request, *args, **kwargs):
 | 
				
			||||||
        print(self.request.POST.dict())
 | 
					 | 
				
			||||||
        form = self.get_form()
 | 
					        form = self.get_form()
 | 
				
			||||||
        required = 'add_ssh' in self.request.POST
 | 
					        required = 'add_ssh' in self.request.POST
 | 
				
			||||||
        form.fields['name'].required = required
 | 
					        form.fields['name'].required = required
 | 
				
			||||||
| 
						 | 
					@ -607,23 +611,10 @@ class PaymentVMView(LoginRequiredMixin, FormView):
 | 
				
			||||||
    def post(self, request, *args, **kwargs):
 | 
					    def post(self, request, *args, **kwargs):
 | 
				
			||||||
        form = self.get_form()
 | 
					        form = self.get_form()
 | 
				
			||||||
        if form.is_valid():
 | 
					        if form.is_valid():
 | 
				
			||||||
 | 
					 | 
				
			||||||
            # Get billing address data
 | 
					            # Get billing address data
 | 
				
			||||||
            billing_address_data = form.cleaned_data
 | 
					            billing_address_data = form.cleaned_data
 | 
				
			||||||
 | 
					 | 
				
			||||||
            context = self.get_context_data()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            template = request.session.get('template')
 | 
					 | 
				
			||||||
            specs = request.session.get('specs')
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            vm_template_id = template.get('id', 1)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            final_price = specs.get('price')
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            token = form.cleaned_data.get('token')
 | 
					            token = form.cleaned_data.get('token')
 | 
				
			||||||
 | 
					 | 
				
			||||||
            owner = self.request.user
 | 
					            owner = self.request.user
 | 
				
			||||||
 | 
					 | 
				
			||||||
            # Get or create stripe customer
 | 
					            # Get or create stripe customer
 | 
				
			||||||
            customer = StripeCustomer.get_or_create(email=owner.email,
 | 
					            customer = StripeCustomer.get_or_create(email=owner.email,
 | 
				
			||||||
                                                    token=token)
 | 
					                                                    token=token)
 | 
				
			||||||
| 
						 | 
					@ -637,106 +628,18 @@ class PaymentVMView(LoginRequiredMixin, FormView):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            # Create Billing Address
 | 
					            # Create Billing Address
 | 
				
			||||||
            billing_address = form.save()
 | 
					            billing_address = form.save()
 | 
				
			||||||
 | 
					            request.session['billing_address_data'] = billing_address_data
 | 
				
			||||||
            # Make stripe charge to a customer
 | 
					            request.session['billing_address'] = billing_address.id
 | 
				
			||||||
            stripe_utils = StripeUtils()
 | 
					            request.session['token'] = token
 | 
				
			||||||
            charge_response = stripe_utils.make_charge(amount=final_price,
 | 
					            request.session['customer'] = customer.id
 | 
				
			||||||
                                                       customer=customer.stripe_id)
 | 
					            return HttpResponseRedirect("{url}?{query_params}".format(
 | 
				
			||||||
 | 
					                url=reverse('hosting:order-confirmation'),
 | 
				
			||||||
            # Check if the payment was approved
 | 
					 | 
				
			||||||
            if not charge_response.get('response_object'):
 | 
					 | 
				
			||||||
                msg = charge_response.get('error')
 | 
					 | 
				
			||||||
                messages.add_message(
 | 
					 | 
				
			||||||
                    self.request, messages.ERROR, msg,
 | 
					 | 
				
			||||||
                    extra_tags='make_charge_error')
 | 
					 | 
				
			||||||
                return HttpResponseRedirect(
 | 
					 | 
				
			||||||
                    reverse('hosting:payment') + '#payment_error')
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            charge = charge_response.get('response_object')
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            # Create OpenNebulaManager
 | 
					 | 
				
			||||||
            manager = OpenNebulaManager(email=owner.email,
 | 
					 | 
				
			||||||
                                        password=owner.password)
 | 
					 | 
				
			||||||
            # Get user ssh key
 | 
					 | 
				
			||||||
            if not UserHostingKey.objects.filter(
 | 
					 | 
				
			||||||
                    user=self.request.user).exists():
 | 
					 | 
				
			||||||
                context.update({
 | 
					 | 
				
			||||||
                    'sshError': 'error',
 | 
					 | 
				
			||||||
                    'form': form
 | 
					 | 
				
			||||||
                })
 | 
					 | 
				
			||||||
                return render(request, self.template_name, context)
 | 
					 | 
				
			||||||
            # For now just get first one
 | 
					 | 
				
			||||||
            user_key = UserHostingKey.objects.filter(
 | 
					 | 
				
			||||||
                user=self.request.user).first()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            # Create a vm using logged user
 | 
					 | 
				
			||||||
            vm_id = manager.create_vm(
 | 
					 | 
				
			||||||
                template_id=vm_template_id,
 | 
					 | 
				
			||||||
                # XXX: Confi
 | 
					 | 
				
			||||||
                specs=specs,
 | 
					 | 
				
			||||||
                ssh_key=user_key.public_key,
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            # Create a Hosting Order
 | 
					 | 
				
			||||||
            order = HostingOrder.create(
 | 
					 | 
				
			||||||
                price=final_price,
 | 
					 | 
				
			||||||
                vm_id=vm_id,
 | 
					 | 
				
			||||||
                customer=customer,
 | 
					 | 
				
			||||||
                billing_address=billing_address
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            # Create a Hosting Bill
 | 
					 | 
				
			||||||
            HostingBill.create(
 | 
					 | 
				
			||||||
                customer=customer, billing_address=billing_address)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            # Create Billing Address for User if he does not have one
 | 
					 | 
				
			||||||
            if not customer.user.billing_addresses.count():
 | 
					 | 
				
			||||||
                billing_address_data.update({
 | 
					 | 
				
			||||||
                    'user': customer.user.id
 | 
					 | 
				
			||||||
                })
 | 
					 | 
				
			||||||
                billing_address_user_form = UserBillingAddressForm(
 | 
					 | 
				
			||||||
                    billing_address_data)
 | 
					 | 
				
			||||||
                billing_address_user_form.is_valid()
 | 
					 | 
				
			||||||
                billing_address_user_form.save()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            # Associate an order with a stripe payment
 | 
					 | 
				
			||||||
            order.set_stripe_charge(charge)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            # If the Stripe payment was successed, set order status approved
 | 
					 | 
				
			||||||
            order.set_approved()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            vm = VirtualMachineSerializer(manager.get_vm(vm_id)).data
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            # Send notification to the user as soon as VM has been booked
 | 
					 | 
				
			||||||
            context = {
 | 
					 | 
				
			||||||
                'vm': vm,
 | 
					 | 
				
			||||||
                'order': order,
 | 
					 | 
				
			||||||
                'base_url': "{0}://{1}".format(request.scheme,
 | 
					 | 
				
			||||||
                                               request.get_host()),
 | 
					 | 
				
			||||||
                'page_header': _(
 | 
					 | 
				
			||||||
                    'Your New VM %(vm_name)s at Data Center Light') % {
 | 
					 | 
				
			||||||
                    'vm_name': vm.get('name')}
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            email_data = {
 | 
					 | 
				
			||||||
                'subject': context.get('page_header'),
 | 
					 | 
				
			||||||
                'to': request.user.email,
 | 
					 | 
				
			||||||
                'context': context,
 | 
					 | 
				
			||||||
                'template_name': 'new_booked_vm',
 | 
					 | 
				
			||||||
                'template_path': 'hosting/emails/',
 | 
					 | 
				
			||||||
                'from_address': settings.DCL_SUPPORT_FROM_ADDRESS,
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            email = BaseEmail(**email_data)
 | 
					 | 
				
			||||||
            email.send()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            return HttpResponseRedirect(
 | 
					 | 
				
			||||||
                "{url}?{query_params}".format(
 | 
					 | 
				
			||||||
                    url=reverse('hosting:orders', kwargs={'pk': order.id}),
 | 
					 | 
				
			||||||
                query_params='page=payment'))
 | 
					                query_params='page=payment'))
 | 
				
			||||||
        else:
 | 
					        else:
 | 
				
			||||||
            return self.form_invalid(form)
 | 
					            return self.form_invalid(form)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class OrdersHostingDetailView(PermissionRequiredMixin, LoginRequiredMixin,
 | 
					class OrdersHostingDetailView(LoginRequiredMixin,
 | 
				
			||||||
                              DetailView):
 | 
					                              DetailView):
 | 
				
			||||||
    template_name = "hosting/order_detail.html"
 | 
					    template_name = "hosting/order_detail.html"
 | 
				
			||||||
    context_object_name = "order"
 | 
					    context_object_name = "order"
 | 
				
			||||||
| 
						 | 
					@ -744,18 +647,42 @@ class OrdersHostingDetailView(PermissionRequiredMixin, LoginRequiredMixin,
 | 
				
			||||||
    permission_required = ['view_hostingorder']
 | 
					    permission_required = ['view_hostingorder']
 | 
				
			||||||
    model = HostingOrder
 | 
					    model = HostingOrder
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_object(self):
 | 
				
			||||||
 | 
					        return HostingOrder.objects.filter(
 | 
				
			||||||
 | 
					            pk=self.kwargs.get('pk')) if self.kwargs.get('pk') else None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_context_data(self, **kwargs):
 | 
					    def get_context_data(self, **kwargs):
 | 
				
			||||||
        # Get context
 | 
					        # Get context
 | 
				
			||||||
        context = super(DetailView, self).get_context_data(**kwargs)
 | 
					        context = super(DetailView, self).get_context_data(**kwargs)
 | 
				
			||||||
        obj = self.get_object()
 | 
					        obj = self.get_object()
 | 
				
			||||||
        owner = self.request.user
 | 
					        owner = self.request.user
 | 
				
			||||||
        manager = OpenNebulaManager(email=owner.email,
 | 
					        if 'specs' not in self.request.session:
 | 
				
			||||||
                                    password=owner.password)
 | 
					            return HttpResponseRedirect(
 | 
				
			||||||
 | 
					                reverse('hosting:create_virtual_machine'))
 | 
				
			||||||
 | 
					        if 'token' not in self.request.session:
 | 
				
			||||||
 | 
					            return HttpResponseRedirect(reverse('hosting:payment'))
 | 
				
			||||||
 | 
					        stripe_customer_id = self.request.session.get('customer')
 | 
				
			||||||
 | 
					        customer = StripeCustomer.objects.filter(id=stripe_customer_id).first()
 | 
				
			||||||
 | 
					        stripe_utils = StripeUtils()
 | 
				
			||||||
 | 
					        card_details = stripe_utils.get_card_details(customer.stripe_id,
 | 
				
			||||||
 | 
					                                                     self.request.session.get(
 | 
				
			||||||
 | 
					                                                         'token'))
 | 
				
			||||||
 | 
					        if not card_details.get('response_object'):
 | 
				
			||||||
 | 
					            msg = card_details.get('error')
 | 
				
			||||||
 | 
					            messages.add_message(self.request, messages.ERROR, msg,
 | 
				
			||||||
 | 
					                                 extra_tags='failed_payment')
 | 
				
			||||||
 | 
					            return HttpResponseRedirect(
 | 
				
			||||||
 | 
					                reverse('hosting:payment') + '#payment_error')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if self.request.GET.get('page', '') == 'payment':
 | 
					        if self.request.GET.get('page', '') == 'payment':
 | 
				
			||||||
            context['page_header_text'] = _('Confirm Order')
 | 
					            context['page_header_text'] = _('Confirm Order')
 | 
				
			||||||
        else:
 | 
					        else:
 | 
				
			||||||
            context['page_header_text'] = _('Invoice')
 | 
					            context['page_header_text'] = _('Invoice')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if obj is not None:
 | 
				
			||||||
            try:
 | 
					            try:
 | 
				
			||||||
 | 
					                manager = OpenNebulaManager(email=owner.email,
 | 
				
			||||||
 | 
					                                            password=owner.password)
 | 
				
			||||||
                vm = manager.get_vm(obj.vm_id)
 | 
					                vm = manager.get_vm(obj.vm_id)
 | 
				
			||||||
                context['vm'] = VirtualMachineSerializer(vm).data
 | 
					                context['vm'] = VirtualMachineSerializer(vm).data
 | 
				
			||||||
            except WrongIdError:
 | 
					            except WrongIdError:
 | 
				
			||||||
| 
						 | 
					@ -767,11 +694,98 @@ class OrdersHostingDetailView(PermissionRequiredMixin, LoginRequiredMixin,
 | 
				
			||||||
                context['error'] = 'WrongIdError'
 | 
					                context['error'] = 'WrongIdError'
 | 
				
			||||||
            except ConnectionRefusedError:
 | 
					            except ConnectionRefusedError:
 | 
				
			||||||
                messages.error(self.request,
 | 
					                messages.error(self.request,
 | 
				
			||||||
                           _(
 | 
					                               'In order to create a VM, you need to create/upload your SSH KEY first.'
 | 
				
			||||||
                               'In order to create a VM, you need to create/upload your SSH KEY first.')
 | 
					 | 
				
			||||||
                               )
 | 
					                               )
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            context['site_url'] = reverse('hosting:create_virtual_machine')
 | 
				
			||||||
 | 
					            context['cc_last4'] = card_details.get('response_object').get(
 | 
				
			||||||
 | 
					                'last4')
 | 
				
			||||||
 | 
					            context['cc_brand'] = card_details.get('response_object').get(
 | 
				
			||||||
 | 
					                'cc_brand')
 | 
				
			||||||
        return context
 | 
					        return context
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def post(self, request):
 | 
				
			||||||
 | 
					        template = request.session.get('template')
 | 
				
			||||||
 | 
					        specs = request.session.get('specs')
 | 
				
			||||||
 | 
					        stripe_customer_id = request.session.get('customer')
 | 
				
			||||||
 | 
					        customer = StripeCustomer.objects.filter(id=stripe_customer_id).first()
 | 
				
			||||||
 | 
					        billing_address_data = request.session.get('billing_address_data')
 | 
				
			||||||
 | 
					        billing_address_id = request.session.get('billing_address')
 | 
				
			||||||
 | 
					        vm_template_id = template.get('id', 1)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Make stripe charge to a customer
 | 
				
			||||||
 | 
					        stripe_utils = StripeUtils()
 | 
				
			||||||
 | 
					        card_details = stripe_utils.get_card_details(customer.stripe_id,
 | 
				
			||||||
 | 
					                                                     request.session.get(
 | 
				
			||||||
 | 
					                                                         'token'))
 | 
				
			||||||
 | 
					        if not card_details.get('response_object'):
 | 
				
			||||||
 | 
					            msg = card_details.get('error')
 | 
				
			||||||
 | 
					            messages.add_message(self.request, messages.ERROR, msg,
 | 
				
			||||||
 | 
					                                 extra_tags='failed_payment')
 | 
				
			||||||
 | 
					            return HttpResponseRedirect(
 | 
				
			||||||
 | 
					                reverse('datacenterlight:payment') + '#payment_error')
 | 
				
			||||||
 | 
					        card_details_dict = card_details.get('response_object')
 | 
				
			||||||
 | 
					        cpu = specs.get('cpu')
 | 
				
			||||||
 | 
					        memory = specs.get('memory')
 | 
				
			||||||
 | 
					        disk_size = specs.get('disk_size')
 | 
				
			||||||
 | 
					        amount_to_be_charged = (cpu * 5) + (memory * 2) + (disk_size * 0.6)
 | 
				
			||||||
 | 
					        plan_name = "{cpu} Cores, {memory} GB RAM, {disk_size} GB SSD".format(
 | 
				
			||||||
 | 
					            cpu=cpu,
 | 
				
			||||||
 | 
					            memory=memory,
 | 
				
			||||||
 | 
					            disk_size=disk_size)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        stripe_plan_id = StripeUtils.get_stripe_plan_id(cpu=cpu,
 | 
				
			||||||
 | 
					                                                        ram=memory,
 | 
				
			||||||
 | 
					                                                        ssd=disk_size,
 | 
				
			||||||
 | 
					                                                        version=1,
 | 
				
			||||||
 | 
					                                                        app='dcl')
 | 
				
			||||||
 | 
					        stripe_plan = stripe_utils.get_or_create_stripe_plan(
 | 
				
			||||||
 | 
					            amount=amount_to_be_charged,
 | 
				
			||||||
 | 
					            name=plan_name,
 | 
				
			||||||
 | 
					            stripe_plan_id=stripe_plan_id)
 | 
				
			||||||
 | 
					        subscription_result = stripe_utils.subscribe_customer_to_plan(
 | 
				
			||||||
 | 
					            customer.stripe_id,
 | 
				
			||||||
 | 
					            [{"plan": stripe_plan.get(
 | 
				
			||||||
 | 
					                'response_object').stripe_plan_id}])
 | 
				
			||||||
 | 
					        stripe_subscription_obj = subscription_result.get('response_object')
 | 
				
			||||||
 | 
					        # Check if the subscription was approved and is active
 | 
				
			||||||
 | 
					        if stripe_subscription_obj is None or stripe_subscription_obj.status != 'active':
 | 
				
			||||||
 | 
					            msg = subscription_result.get('error')
 | 
				
			||||||
 | 
					            messages.add_message(self.request, messages.ERROR, msg,
 | 
				
			||||||
 | 
					                                 extra_tags='failed_payment')
 | 
				
			||||||
 | 
					            return HttpResponseRedirect(
 | 
				
			||||||
 | 
					                reverse('hosting:payment') + '#payment_error')
 | 
				
			||||||
 | 
					        user = {
 | 
				
			||||||
 | 
					            'name': self.request.user.name,
 | 
				
			||||||
 | 
					            'email': self.request.user.email,
 | 
				
			||||||
 | 
					            'pass': self.request.user.password,
 | 
				
			||||||
 | 
					            'request_scheme': request.scheme,
 | 
				
			||||||
 | 
					            'request_host': request.get_host(),
 | 
				
			||||||
 | 
					            'language': get_language(),
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        create_vm_task.delay(vm_template_id, user, specs, template,
 | 
				
			||||||
 | 
					                             stripe_customer_id, billing_address_data,
 | 
				
			||||||
 | 
					                             billing_address_id,
 | 
				
			||||||
 | 
					                             stripe_subscription_obj, card_details_dict)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        for session_var in ['specs', 'template', 'billing_address',
 | 
				
			||||||
 | 
					                            'billing_address_data',
 | 
				
			||||||
 | 
					                            'token', 'customer']:
 | 
				
			||||||
 | 
					            if session_var in request.session:
 | 
				
			||||||
 | 
					                del request.session[session_var]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        response = {
 | 
				
			||||||
 | 
					            'status': True,
 | 
				
			||||||
 | 
					            'redirect': reverse('hosting:virtual_machines'),
 | 
				
			||||||
 | 
					            'msg_title': str(_('Thank you for the order.')),
 | 
				
			||||||
 | 
					            'msg_body': str(_('Your VM will be up and running in a few moments.'
 | 
				
			||||||
 | 
					                              ' We will send you a confirmation email as soon as'
 | 
				
			||||||
 | 
					                              ' it is ready.'))
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return HttpResponse(json.dumps(response),
 | 
				
			||||||
 | 
					                            content_type="application/json")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class OrdersHostingListView(LoginRequiredMixin, ListView):
 | 
					class OrdersHostingListView(LoginRequiredMixin, ListView):
 | 
				
			||||||
    template_name = "hosting/orders.html"
 | 
					    template_name = "hosting/orders.html"
 | 
				
			||||||
| 
						 | 
					@ -954,7 +968,8 @@ class VirtualMachineView(LoginRequiredMixin, View):
 | 
				
			||||||
                'order': HostingOrder.objects.get(
 | 
					                'order': HostingOrder.objects.get(
 | 
				
			||||||
                    vm_id=serializer.data['vm_id'])
 | 
					                    vm_id=serializer.data['vm_id'])
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        except:
 | 
					        except Exception as ex:
 | 
				
			||||||
 | 
					            logger.debug("Exception generated {}".format(str(ex)))
 | 
				
			||||||
            pass
 | 
					            pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return render(request, self.template_name, context)
 | 
					        return render(request, self.template_name, context)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,13 +1,14 @@
 | 
				
			||||||
import oca
 | 
					 | 
				
			||||||
import socket
 | 
					 | 
				
			||||||
import logging
 | 
					import logging
 | 
				
			||||||
 | 
					import socket
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from oca.pool import WrongNameError, WrongIdError
 | 
					import oca
 | 
				
			||||||
from oca.exceptions import OpenNebulaException
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
from django.conf import settings
 | 
					from django.conf import settings
 | 
				
			||||||
 | 
					from oca.exceptions import OpenNebulaException
 | 
				
			||||||
 | 
					from oca.pool import WrongNameError, WrongIdError
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from hosting.models import HostingOrder
 | 
				
			||||||
from utils.models import CustomUser
 | 
					from utils.models import CustomUser
 | 
				
			||||||
 | 
					from utils.tasks import save_ssh_key, save_ssh_key_error_handler
 | 
				
			||||||
from .exceptions import KeyExistsError, UserExistsError, UserCredentialError
 | 
					from .exceptions import KeyExistsError, UserExistsError, UserCredentialError
 | 
				
			||||||
 | 
					
 | 
				
			||||||
logger = logging.getLogger(__name__)
 | 
					logger = logging.getLogger(__name__)
 | 
				
			||||||
| 
						 | 
					@ -17,7 +18,8 @@ class OpenNebulaManager():
 | 
				
			||||||
    """This class represents an opennebula manager."""
 | 
					    """This class represents an opennebula manager."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __init__(self, email=None, password=None):
 | 
					    def __init__(self, email=None, password=None):
 | 
				
			||||||
 | 
					        self.email = email
 | 
				
			||||||
 | 
					        self.password = password
 | 
				
			||||||
        # Get oneadmin client
 | 
					        # Get oneadmin client
 | 
				
			||||||
        self.oneadmin_client = self._get_opennebula_client(
 | 
					        self.oneadmin_client = self._get_opennebula_client(
 | 
				
			||||||
            settings.OPENNEBULA_USERNAME,
 | 
					            settings.OPENNEBULA_USERNAME,
 | 
				
			||||||
| 
						 | 
					@ -122,14 +124,17 @@ class OpenNebulaManager():
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        except WrongNameError:
 | 
					        except WrongNameError:
 | 
				
			||||||
            user_id = self.oneadmin_client.call(oca.User.METHODS['allocate'],
 | 
					            user_id = self.oneadmin_client.call(oca.User.METHODS['allocate'],
 | 
				
			||||||
                                                user.email, user.password, 'core')
 | 
					                                                user.email, user.password,
 | 
				
			||||||
            logger.debug('Created a user for CustomObject: {user} with user id = {u_id}',
 | 
					                                                'core')
 | 
				
			||||||
 | 
					            logger.debug(
 | 
				
			||||||
 | 
					                'Created a user for CustomObject: {user} with user id = {u_id}',
 | 
				
			||||||
                user=user,
 | 
					                user=user,
 | 
				
			||||||
                u_id=user_id
 | 
					                u_id=user_id
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
            return user_id
 | 
					            return user_id
 | 
				
			||||||
        except ConnectionRefusedError:
 | 
					        except ConnectionRefusedError:
 | 
				
			||||||
            logger.error('Could not connect to host: {host} via protocol {protocol}'.format(
 | 
					            logger.error(
 | 
				
			||||||
 | 
					                'Could not connect to host: {host} via protocol {protocol}'.format(
 | 
				
			||||||
                    host=settings.OPENNEBULA_DOMAIN,
 | 
					                    host=settings.OPENNEBULA_DOMAIN,
 | 
				
			||||||
                    protocol=settings.OPENNEBULA_PROTOCOL)
 | 
					                    protocol=settings.OPENNEBULA_PROTOCOL)
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
| 
						 | 
					@ -141,7 +146,8 @@ class OpenNebulaManager():
 | 
				
			||||||
            opennebula_user = user_pool.get_by_name(email)
 | 
					            opennebula_user = user_pool.get_by_name(email)
 | 
				
			||||||
            return opennebula_user
 | 
					            return opennebula_user
 | 
				
			||||||
        except WrongNameError as wrong_name_err:
 | 
					        except WrongNameError as wrong_name_err:
 | 
				
			||||||
            opennebula_user = self.oneadmin_client.call(oca.User.METHODS['allocate'], email,
 | 
					            opennebula_user = self.oneadmin_client.call(
 | 
				
			||||||
 | 
					                oca.User.METHODS['allocate'], email,
 | 
				
			||||||
                password, 'core')
 | 
					                password, 'core')
 | 
				
			||||||
            logger.debug(
 | 
					            logger.debug(
 | 
				
			||||||
                "User {0} does not exist. Created the user. User id = {1}",
 | 
					                "User {0} does not exist. Created the user. User id = {1}",
 | 
				
			||||||
| 
						 | 
					@ -150,7 +156,8 @@ class OpenNebulaManager():
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
            return opennebula_user
 | 
					            return opennebula_user
 | 
				
			||||||
        except ConnectionRefusedError:
 | 
					        except ConnectionRefusedError:
 | 
				
			||||||
            logger.info('Could not connect to host: {host} via protocol {protocol}'.format(
 | 
					            logger.info(
 | 
				
			||||||
 | 
					                'Could not connect to host: {host} via protocol {protocol}'.format(
 | 
				
			||||||
                    host=settings.OPENNEBULA_DOMAIN,
 | 
					                    host=settings.OPENNEBULA_DOMAIN,
 | 
				
			||||||
                    protocol=settings.OPENNEBULA_PROTOCOL)
 | 
					                    protocol=settings.OPENNEBULA_PROTOCOL)
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
| 
						 | 
					@ -161,7 +168,8 @@ class OpenNebulaManager():
 | 
				
			||||||
            user_pool = oca.UserPool(self.oneadmin_client)
 | 
					            user_pool = oca.UserPool(self.oneadmin_client)
 | 
				
			||||||
            user_pool.info()
 | 
					            user_pool.info()
 | 
				
			||||||
        except ConnectionRefusedError:
 | 
					        except ConnectionRefusedError:
 | 
				
			||||||
            logger.info('Could not connect to host: {host} via protocol {protocol}'.format(
 | 
					            logger.info(
 | 
				
			||||||
 | 
					                'Could not connect to host: {host} via protocol {protocol}'.format(
 | 
				
			||||||
                    host=settings.OPENNEBULA_DOMAIN,
 | 
					                    host=settings.OPENNEBULA_DOMAIN,
 | 
				
			||||||
                    protocol=settings.OPENNEBULA_PROTOCOL)
 | 
					                    protocol=settings.OPENNEBULA_PROTOCOL)
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
| 
						 | 
					@ -183,7 +191,8 @@ class OpenNebulaManager():
 | 
				
			||||||
                raise ConnectionRefusedError
 | 
					                raise ConnectionRefusedError
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        except ConnectionRefusedError:
 | 
					        except ConnectionRefusedError:
 | 
				
			||||||
            logger.info('Could not connect to host: {host} via protocol {protocol}'.format(
 | 
					            logger.info(
 | 
				
			||||||
 | 
					                'Could not connect to host: {host} via protocol {protocol}'.format(
 | 
				
			||||||
                    host=settings.OPENNEBULA_DOMAIN,
 | 
					                    host=settings.OPENNEBULA_DOMAIN,
 | 
				
			||||||
                    protocol=settings.OPENNEBULA_PROTOCOL)
 | 
					                    protocol=settings.OPENNEBULA_PROTOCOL)
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
| 
						 | 
					@ -208,6 +217,33 @@ class OpenNebulaManager():
 | 
				
			||||||
        except:
 | 
					        except:
 | 
				
			||||||
            raise ConnectionRefusedError
 | 
					            raise ConnectionRefusedError
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_primary_ipv4(self, vm_id):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Returns the primary IPv4 of the given vm.
 | 
				
			||||||
 | 
					        To be changed later.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :return: An IP address string, if it exists else returns None
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        all_ipv4s = self.get_vm_ipv4_addresses(vm_id)
 | 
				
			||||||
 | 
					        if len(all_ipv4s) > 0:
 | 
				
			||||||
 | 
					            return all_ipv4s[0]
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            return None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_vm_ipv4_addresses(self, vm_id):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Returns a list of IPv4 addresses of the given vm
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param vm_id: The ID of the vm
 | 
				
			||||||
 | 
					        :return:
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        ipv4s = []
 | 
				
			||||||
 | 
					        vm = self.get_vm(vm_id)
 | 
				
			||||||
 | 
					        for nic in vm.template.nics:
 | 
				
			||||||
 | 
					            if hasattr(nic, 'ip'):
 | 
				
			||||||
 | 
					                ipv4s.append(nic.ip)
 | 
				
			||||||
 | 
					        return ipv4s
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def create_vm(self, template_id, specs, ssh_key=None, vm_name=None):
 | 
					    def create_vm(self, template_id, specs, ssh_key=None, vm_name=None):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        template = self.get_template(template_id)
 | 
					        template = self.get_template(template_id)
 | 
				
			||||||
| 
						 | 
					@ -258,7 +294,8 @@ class OpenNebulaManager():
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        vm_specs += "<CONTEXT>"
 | 
					        vm_specs += "<CONTEXT>"
 | 
				
			||||||
        if ssh_key:
 | 
					        if ssh_key:
 | 
				
			||||||
            vm_specs += "<SSH_PUBLIC_KEY>{ssh}</SSH_PUBLIC_KEY>".format(ssh=ssh_key)
 | 
					            vm_specs += "<SSH_PUBLIC_KEY>{ssh}</SSH_PUBLIC_KEY>".format(
 | 
				
			||||||
 | 
					                ssh=ssh_key)
 | 
				
			||||||
        vm_specs += """<NETWORK>YES</NETWORK>
 | 
					        vm_specs += """<NETWORK>YES</NETWORK>
 | 
				
			||||||
                   </CONTEXT>
 | 
					                   </CONTEXT>
 | 
				
			||||||
                </TEMPLATE>
 | 
					                </TEMPLATE>
 | 
				
			||||||
| 
						 | 
					@ -312,7 +349,9 @@ class OpenNebulaManager():
 | 
				
			||||||
            template_pool.info()
 | 
					            template_pool.info()
 | 
				
			||||||
            return template_pool
 | 
					            return template_pool
 | 
				
			||||||
        except ConnectionRefusedError:
 | 
					        except ConnectionRefusedError:
 | 
				
			||||||
            logger.info('Could not connect to host: {host} via protocol {protocol}'.format(
 | 
					            logger.info(
 | 
				
			||||||
 | 
					                """Could not connect to host: {host} via protocol
 | 
				
			||||||
 | 
					                 {protocol}""".format(
 | 
				
			||||||
                    host=settings.OPENNEBULA_DOMAIN,
 | 
					                    host=settings.OPENNEBULA_DOMAIN,
 | 
				
			||||||
                    protocol=settings.OPENNEBULA_PROTOCOL)
 | 
					                    protocol=settings.OPENNEBULA_PROTOCOL)
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
| 
						 | 
					@ -347,7 +386,8 @@ class OpenNebulaManager():
 | 
				
			||||||
        except:
 | 
					        except:
 | 
				
			||||||
            raise ConnectionRefusedError
 | 
					            raise ConnectionRefusedError
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def create_template(self, name, cores, memory, disk_size, core_price, memory_price,
 | 
					    def create_template(self, name, cores, memory, disk_size, core_price,
 | 
				
			||||||
 | 
					                        memory_price,
 | 
				
			||||||
                        disk_size_price, ssh=''):
 | 
					                        disk_size_price, ssh=''):
 | 
				
			||||||
        """Create and add a new template to opennebula.
 | 
					        """Create and add a new template to opennebula.
 | 
				
			||||||
        :param name:      A string representation describing the template.
 | 
					        :param name:      A string representation describing the template.
 | 
				
			||||||
| 
						 | 
					@ -490,3 +530,57 @@ class OpenNebulaManager():
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        except ConnectionError:
 | 
					        except ConnectionError:
 | 
				
			||||||
            raise
 | 
					            raise
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def manage_public_key(self, keys, hosts=None, countdown=0):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        A function that manages the supplied keys in the
 | 
				
			||||||
 | 
					        authorized_keys file of the given list of hosts. If hosts
 | 
				
			||||||
 | 
					        parameter is not supplied, all hosts of this customer
 | 
				
			||||||
 | 
					        will be configured with the supplied keys
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param keys: A list of ssh keys that are to be added/removed
 | 
				
			||||||
 | 
					                     A key should be a dict of the form
 | 
				
			||||||
 | 
					                     {
 | 
				
			||||||
 | 
					                       'value': 'sha-.....', # public key as string
 | 
				
			||||||
 | 
					                       'state': True         # whether key is to be added or
 | 
				
			||||||
 | 
					                     }                       # removed
 | 
				
			||||||
 | 
					        :param hosts: A list of hosts IP addresses
 | 
				
			||||||
 | 
					        :param countdown: Parameter to be passed to celery apply_async
 | 
				
			||||||
 | 
					               Allows to delay a task by `countdown` number of seconds
 | 
				
			||||||
 | 
					        :return:
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        if hosts is None:
 | 
				
			||||||
 | 
					            hosts = self.get_all_hosts()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if len(hosts) > 0 and len(keys) > 0:
 | 
				
			||||||
 | 
					            save_ssh_key.apply_async((hosts, keys), countdown=countdown,
 | 
				
			||||||
 | 
					                                     link_error=save_ssh_key_error_handler.s())
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            logger.debug(
 | 
				
			||||||
 | 
					                "Keys and/or hosts are empty, so not managing any keys")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_all_hosts(self):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        A utility function to obtain all hosts of this owner
 | 
				
			||||||
 | 
					        :return: A list of hosts IP addresses, empty if none exist
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        owner = CustomUser.objects.filter(
 | 
				
			||||||
 | 
					            email=self.email).first()
 | 
				
			||||||
 | 
					        all_orders = HostingOrder.objects.filter(customer__user=owner)
 | 
				
			||||||
 | 
					        hosts = []
 | 
				
			||||||
 | 
					        if len(all_orders) > 0:
 | 
				
			||||||
 | 
					            logger.debug("The user {} has 1 or more VMs. We need to configure "
 | 
				
			||||||
 | 
					                         "the ssh keys.".format(self.email))
 | 
				
			||||||
 | 
					            for order in all_orders:
 | 
				
			||||||
 | 
					                try:
 | 
				
			||||||
 | 
					                    vm = self.get_vm(order.vm_id)
 | 
				
			||||||
 | 
					                    for nic in vm.template.nics:
 | 
				
			||||||
 | 
					                        if hasattr(nic, 'ip'):
 | 
				
			||||||
 | 
					                            hosts.append(nic.ip)
 | 
				
			||||||
 | 
					                except WrongIdError:
 | 
				
			||||||
 | 
					                    logger.debug(
 | 
				
			||||||
 | 
					                        "VM with ID {} does not exist".format(order.vm_id))
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            logger.debug("The user {} has no VMs. We don't need to configure "
 | 
				
			||||||
 | 
					                         "the ssh keys.".format(self.email))
 | 
				
			||||||
 | 
					        return hosts
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -96,3 +96,5 @@ pyflakes==1.5.0
 | 
				
			||||||
billiard==3.5.0.3
 | 
					billiard==3.5.0.3
 | 
				
			||||||
amqp==2.2.1
 | 
					amqp==2.2.1
 | 
				
			||||||
vine==1.1.4
 | 
					vine==1.1.4
 | 
				
			||||||
 | 
					#git+https://github.com/ungleich/cdist.git#egg=cdist
 | 
				
			||||||
 | 
					file:///home/app/cdist#egg=cdist
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										11
									
								
								utils/hosting_utils.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								utils/hosting_utils.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,11 @@
 | 
				
			||||||
 | 
					from hosting.models import UserHostingKey
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def get_all_public_keys(customer):
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    Returns all the public keys of the user
 | 
				
			||||||
 | 
					    :param customer: The customer whose public keys are needed
 | 
				
			||||||
 | 
					    :return: A list of public keys
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    return UserHostingKey.objects.filter(user_id=customer.id).values_list(
 | 
				
			||||||
 | 
					        "public_key", flat=True)
 | 
				
			||||||
| 
						 | 
					@ -1,8 +1,15 @@
 | 
				
			||||||
 | 
					import tempfile
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import cdist
 | 
				
			||||||
 | 
					from cdist.integration import configure_hosts_simple
 | 
				
			||||||
 | 
					from celery.result import AsyncResult
 | 
				
			||||||
 | 
					from celery import current_task
 | 
				
			||||||
from celery.utils.log import get_task_logger
 | 
					from celery.utils.log import get_task_logger
 | 
				
			||||||
from django.conf import settings
 | 
					from django.conf import settings
 | 
				
			||||||
from dynamicweb.celery import app
 | 
					 | 
				
			||||||
from django.core.mail import EmailMessage
 | 
					from django.core.mail import EmailMessage
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from dynamicweb.celery import app
 | 
				
			||||||
 | 
					
 | 
				
			||||||
logger = get_task_logger(__name__)
 | 
					logger = get_task_logger(__name__)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -18,3 +25,74 @@ def send_plain_email_task(self, email_data):
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    email = EmailMessage(**email_data)
 | 
					    email = EmailMessage(**email_data)
 | 
				
			||||||
    email.send()
 | 
					    email.send()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@app.task(bind=True, max_retries=settings.CELERY_MAX_RETRIES)
 | 
				
			||||||
 | 
					def save_ssh_key(self, hosts, keys):
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    Saves ssh key into the VMs of a user using cdist
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :param hosts: A list of hosts to be configured
 | 
				
			||||||
 | 
					    :param keys: A list of keys to be added. A key should be dict of the
 | 
				
			||||||
 | 
					           form    {
 | 
				
			||||||
 | 
					                       'value': 'sha-.....', # public key as string
 | 
				
			||||||
 | 
					                       'state': True         # whether key is to be added or
 | 
				
			||||||
 | 
					                    }                        # removed
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    logger.debug(
 | 
				
			||||||
 | 
					        "Running save_ssh_key on {}".format(current_task.request.hostname))
 | 
				
			||||||
 | 
					    logger.debug("""Running save_ssh_key task for
 | 
				
			||||||
 | 
					                    Hosts: {hosts_str}
 | 
				
			||||||
 | 
					                    Keys: {keys_str}""".format(hosts_str=", ".join(hosts),
 | 
				
			||||||
 | 
					                                               keys_str=", ".join([
 | 
				
			||||||
 | 
					                                                   "{value}->{state}".format(
 | 
				
			||||||
 | 
					                                                       value=key.get('value'),
 | 
				
			||||||
 | 
					                                                       state=str(
 | 
				
			||||||
 | 
					                                                           key.get('state')))
 | 
				
			||||||
 | 
					                                                   for key in keys]))
 | 
				
			||||||
 | 
					                 )
 | 
				
			||||||
 | 
					    return_value = True
 | 
				
			||||||
 | 
					    with tempfile.NamedTemporaryFile(delete=True) as tmp_manifest:
 | 
				
			||||||
 | 
					        # Generate manifest to be used for configuring the hosts
 | 
				
			||||||
 | 
					        lines_list = [
 | 
				
			||||||
 | 
					            '  --key "{key}" --state {state} \\\n'.format(
 | 
				
			||||||
 | 
					                key=key['value'],
 | 
				
			||||||
 | 
					                state='present' if key['state'] else 'absent'
 | 
				
			||||||
 | 
					            ).encode('utf-8')
 | 
				
			||||||
 | 
					            for key in keys]
 | 
				
			||||||
 | 
					        lines_list.insert(0, b'__ssh_authorized_keys root \\\n')
 | 
				
			||||||
 | 
					        tmp_manifest.writelines(lines_list)
 | 
				
			||||||
 | 
					        tmp_manifest.flush()
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            configure_hosts_simple(hosts,
 | 
				
			||||||
 | 
					                                   tmp_manifest.name,
 | 
				
			||||||
 | 
					                                   verbose=cdist.argparse.VERBOSE_TRACE)
 | 
				
			||||||
 | 
					        except Exception as cdist_exception:
 | 
				
			||||||
 | 
					            logger.error(cdist_exception)
 | 
				
			||||||
 | 
					            return_value = False
 | 
				
			||||||
 | 
					            email_data = {
 | 
				
			||||||
 | 
					                'subject': "celery save_ssh_key error - task id {0}".format(
 | 
				
			||||||
 | 
					                    self.request.id.__str__()),
 | 
				
			||||||
 | 
					                'from_email': current_task.request.hostname,
 | 
				
			||||||
 | 
					                'to': settings.DCL_ERROR_EMAILS_TO_LIST,
 | 
				
			||||||
 | 
					                'body': "Task Id: {0}\nResult: {1}\nTraceback: {2}".format(
 | 
				
			||||||
 | 
					                    self.request.id.__str__(), False, str(cdist_exception)),
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            send_plain_email_task(email_data)
 | 
				
			||||||
 | 
					    return return_value
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@app.task
 | 
				
			||||||
 | 
					def save_ssh_key_error_handler(uuid):
 | 
				
			||||||
 | 
					    result = AsyncResult(uuid)
 | 
				
			||||||
 | 
					    exc = result.get(propagate=False)
 | 
				
			||||||
 | 
					    logger.error('Task {0} raised exception: {1!r}\n{2!r}'.format(
 | 
				
			||||||
 | 
					        uuid, exc, result.traceback))
 | 
				
			||||||
 | 
					    email_data = {
 | 
				
			||||||
 | 
					        'subject': "[celery error] Save SSH key error {0}".format(uuid),
 | 
				
			||||||
 | 
					        'from_email': current_task.request.hostname,
 | 
				
			||||||
 | 
					        'to': settings.DCL_ERROR_EMAILS_TO_LIST,
 | 
				
			||||||
 | 
					        'body': "Task Id: {0}\nResult: {1}\nTraceback: {2}".format(
 | 
				
			||||||
 | 
					            uuid, exc, result.traceback),
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    send_plain_email_task(email_data)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,16 +1,20 @@
 | 
				
			||||||
import uuid
 | 
					import uuid
 | 
				
			||||||
 | 
					from time import sleep
 | 
				
			||||||
from unittest.mock import patch
 | 
					from unittest.mock import patch
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import stripe
 | 
					import stripe
 | 
				
			||||||
 | 
					from celery.result import AsyncResult
 | 
				
			||||||
 | 
					from django.conf import settings
 | 
				
			||||||
from django.http.request import HttpRequest
 | 
					from django.http.request import HttpRequest
 | 
				
			||||||
from django.test import Client
 | 
					from django.test import Client
 | 
				
			||||||
from django.test import TestCase
 | 
					from django.test import TestCase, override_settings
 | 
				
			||||||
 | 
					from unittest import skipIf
 | 
				
			||||||
from model_mommy import mommy
 | 
					from model_mommy import mommy
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from datacenterlight.models import StripePlan
 | 
					from datacenterlight.models import StripePlan
 | 
				
			||||||
from membership.models import StripeCustomer
 | 
					from membership.models import StripeCustomer
 | 
				
			||||||
from utils.stripe_utils import StripeUtils
 | 
					from utils.stripe_utils import StripeUtils
 | 
				
			||||||
from django.conf import settings
 | 
					from .tasks import save_ssh_key
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class BaseTestCase(TestCase):
 | 
					class BaseTestCase(TestCase):
 | 
				
			||||||
| 
						 | 
					@ -235,3 +239,57 @@ class StripePlanTestCase(TestStripeCustomerDescription):
 | 
				
			||||||
                'response_object').stripe_plan_id}])
 | 
					                'response_object').stripe_plan_id}])
 | 
				
			||||||
        self.assertIsNone(result.get('response_object'), None)
 | 
					        self.assertIsNone(result.get('response_object'), None)
 | 
				
			||||||
        self.assertIsNotNone(result.get('error'))
 | 
					        self.assertIsNotNone(result.get('error'))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class SaveSSHKeyTestCase(TestCase):
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    A test case to test the celery save_ssh_key task
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @override_settings(
 | 
				
			||||||
 | 
					        task_eager_propagates=True,
 | 
				
			||||||
 | 
					        task_always_eager=True,
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    def setUp(self):
 | 
				
			||||||
 | 
					        self.public_key = settings.TEST_MANAGE_SSH_KEY_PUBKEY
 | 
				
			||||||
 | 
					        self.hosts = settings.TEST_MANAGE_SSH_KEY_HOST
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @skipIf(settings.TEST_MANAGE_SSH_KEY_PUBKEY is None or
 | 
				
			||||||
 | 
					            settings.TEST_MANAGE_SSH_KEY_PUBKEY == "" or
 | 
				
			||||||
 | 
					            settings.TEST_MANAGE_SSH_KEY_HOST is None or
 | 
				
			||||||
 | 
					            settings.TEST_MANAGE_SSH_KEY_HOST is "",
 | 
				
			||||||
 | 
					            """Skipping test_save_ssh_key_add because either host
 | 
				
			||||||
 | 
					             or public key were not specified or were empty""")
 | 
				
			||||||
 | 
					    def test_save_ssh_key_add(self):
 | 
				
			||||||
 | 
					        async_task = save_ssh_key.delay([self.hosts],
 | 
				
			||||||
 | 
					                                        [{'value': self.public_key,
 | 
				
			||||||
 | 
					                                          'state': True}])
 | 
				
			||||||
 | 
					        save_ssh_key_result = None
 | 
				
			||||||
 | 
					        for i in range(0, 10):
 | 
				
			||||||
 | 
					            sleep(5)
 | 
				
			||||||
 | 
					            res = AsyncResult(async_task.task_id)
 | 
				
			||||||
 | 
					            if type(res.result) is bool:
 | 
				
			||||||
 | 
					                save_ssh_key_result = res.result
 | 
				
			||||||
 | 
					                break
 | 
				
			||||||
 | 
					        self.assertIsNotNone(save_ssh_key, "save_ssh_key_result is None")
 | 
				
			||||||
 | 
					        self.assertTrue(save_ssh_key_result, "save_ssh_key_result is False")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @skipIf(settings.TEST_MANAGE_SSH_KEY_PUBKEY is None or
 | 
				
			||||||
 | 
					            settings.TEST_MANAGE_SSH_KEY_PUBKEY == "" or
 | 
				
			||||||
 | 
					            settings.TEST_MANAGE_SSH_KEY_HOST is None or
 | 
				
			||||||
 | 
					            settings.TEST_MANAGE_SSH_KEY_HOST is "",
 | 
				
			||||||
 | 
					            """Skipping test_save_ssh_key_add because either host
 | 
				
			||||||
 | 
					             or public key were not specified or were empty""")
 | 
				
			||||||
 | 
					    def test_save_ssh_key_remove(self):
 | 
				
			||||||
 | 
					        async_task = save_ssh_key.delay([self.hosts],
 | 
				
			||||||
 | 
					                                        [{'value': self.public_key,
 | 
				
			||||||
 | 
					                                          'state': False}])
 | 
				
			||||||
 | 
					        save_ssh_key_result = None
 | 
				
			||||||
 | 
					        for i in range(0, 10):
 | 
				
			||||||
 | 
					            sleep(5)
 | 
				
			||||||
 | 
					            res = AsyncResult(async_task.task_id)
 | 
				
			||||||
 | 
					            if type(res.result) is bool:
 | 
				
			||||||
 | 
					                save_ssh_key_result = res.result
 | 
				
			||||||
 | 
					                break
 | 
				
			||||||
 | 
					        self.assertIsNotNone(save_ssh_key, "save_ssh_key_result is None")
 | 
				
			||||||
 | 
					        self.assertTrue(save_ssh_key_result, "save_ssh_key_result is False")
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue