Force user to generate ssh key in order to create a VM #3147. As user I want to terminate a VM using web interface #3148. Change password in opennebula when user change his password on hosting app #3149
This commit is contained in:
		
					parent
					
						
							
								1a6e1a44d8
							
						
					
				
			
			
				commit
				
					
						3873540849
					
				
			
		
					 7 changed files with 214 additions and 43 deletions
				
			
		|  | @ -101,7 +101,9 @@ class UserHostingKeyForm(forms.ModelForm): | |||
|         # print(self.fields) | ||||
| 
 | ||||
|     def clean_name(self): | ||||
|         return ''.join(random.choice(string.ascii_lowercase) for i in range(7)) | ||||
|         return "dcl-priv-key-%s" % ( | ||||
|             ''.join(random.choice(string.ascii_lowercase) for i in range(7)) | ||||
|         ) | ||||
| 
 | ||||
|     def clean_user(self): | ||||
|         return self.request.user | ||||
|  | @ -109,8 +111,6 @@ class UserHostingKeyForm(forms.ModelForm): | |||
|     def clean(self): | ||||
|         cleaned_data = self.cleaned_data | ||||
| 
 | ||||
|         print(cleaned_data) | ||||
| 
 | ||||
|         if not cleaned_data.get('public_key'): | ||||
|             private_key, public_key = UserHostingKey.generate_keys() | ||||
|             cleaned_data.update({ | ||||
|  |  | |||
|  | @ -185,18 +185,31 @@ class VirtualMachinePlan(AssignPermissionsMixin, models.Model): | |||
|         instance.assign_permissions(user) | ||||
|         return instance | ||||
| 
 | ||||
|     def cancel_plan(self): | ||||
|     def cancel_plan(self, vm_id): | ||||
|         self.status = self.CANCELED_STATUS | ||||
|         self.save(update_fields=['status']) | ||||
| 
 | ||||
|     @classmethod | ||||
|     def terminate_opennebula_vm(self, user, vm_id): | ||||
| 
 | ||||
|         opennebula_client = OpenNebulaManager( | ||||
|             user.email, | ||||
|             user.password, | ||||
|         ) | ||||
| 
 | ||||
|         return opennebula_client.terminate_vm(vm_id) | ||||
| 
 | ||||
| 
 | ||||
|     @classmethod | ||||
|     def create_opennebula_vm(self, user, specs): | ||||
|         # import pdb | ||||
|         # pdb.set_trace() | ||||
| 
 | ||||
| 
 | ||||
|         # Init opennebula manager using given user | ||||
|         opennebula_client = OpenNebulaManager( | ||||
|             user.email, | ||||
|             user.password[0:20], | ||||
|             create_user=True | ||||
|             user.password, | ||||
|         ) | ||||
| 
 | ||||
|         # Create a vm in opennebula using given specs | ||||
|  |  | |||
|  | @ -35,7 +35,7 @@ class OpenNebulaManager: | |||
|         '11': 'CLONING_FAILURE', | ||||
|     } | ||||
| 
 | ||||
|     def __init__(self, email=None, password=None, create_user=True): | ||||
|     def __init__(self, email=None, password=None): | ||||
| 
 | ||||
|         # Get oneadmin client | ||||
|         self.oneadmin_client = self._get_opennebula_client( | ||||
|  | @ -43,9 +43,6 @@ class OpenNebulaManager: | |||
|             settings.OPENNEBULA_PASSWORD | ||||
|         ) | ||||
| 
 | ||||
|         if not create_user: | ||||
|             return | ||||
| 
 | ||||
|         # Get or create oppenebula user using given credentials | ||||
|         self.opennebula_user = self._get_or_create_user( | ||||
|             email, | ||||
|  | @ -121,9 +118,17 @@ class OpenNebulaManager: | |||
| 
 | ||||
|         return vm_data | ||||
| 
 | ||||
|     def change_user_password(self, new_password): | ||||
|         self.oneadmin_client.call( | ||||
|             oca.User.METHODS['passwd'], | ||||
|             self.opennebula_user.id, | ||||
|             new_password | ||||
|         ) | ||||
| 
 | ||||
|     def create_vm(self, specs): | ||||
|         vm_id = None | ||||
|         try: | ||||
| 
 | ||||
|             # We do have the vm_template param set. Get and parse it | ||||
|             # and check it to be in the desired range. | ||||
|             # We have 8 possible VM templates for the moment which are 1x, 2x, 4x ... | ||||
|  | @ -136,6 +141,9 @@ class OpenNebulaManager: | |||
|                                         <TYPE>{disk_type}</TYPE> | ||||
|                                         <SIZE>{size}</SIZE> | ||||
|                                       </DISK> | ||||
|                                       <CONTEXT> | ||||
|                                         <SSH_PUBLIC_KEY>{ssh_key}</SSH_PUBLIC_KEY> | ||||
|                                       </CONTEXT> | ||||
|                                     </VM> | ||||
|                                     """ | ||||
|             vm_id = oca.VirtualMachine.allocate( | ||||
|  | @ -145,7 +153,8 @@ class OpenNebulaManager: | |||
|                     vcpu=specs.get('cores'), | ||||
|                     cpu=0.1 * specs.get('cores'), | ||||
|                     disk_type='fs', | ||||
|                     size=10000 * specs.get('disk_size') | ||||
|                     size=10000 * specs.get('disk_size'), | ||||
|                     ssh_key=specs.get('ssh_key') | ||||
|                 ) | ||||
|             ) | ||||
| 
 | ||||
|  | @ -155,10 +164,6 @@ class OpenNebulaManager: | |||
|                 self.opennebula_user.id, | ||||
|                 self.opennebula_user.group_ids[0] | ||||
|             ) | ||||
|             # oca.VirtualMachine.chown( | ||||
|             #     vm_id, | ||||
|                 | ||||
|             # ) | ||||
| 
 | ||||
|         except socket.timeout as socket_err: | ||||
|             logger.error("Socket timeout error: {0}".format(socket_err)) | ||||
|  | @ -171,6 +176,29 @@ class OpenNebulaManager: | |||
| 
 | ||||
|         return vm_id | ||||
| 
 | ||||
|     def terminate_vm(self, vm_id): | ||||
| 
 | ||||
|         TERMINATE_ACTION = 'terminate' | ||||
|         vm_terminated = False | ||||
| 
 | ||||
|         try: | ||||
|             self.oneadmin_client.call( | ||||
|                 oca.VirtualMachine.METHODS['action'], | ||||
|                 TERMINATE_ACTION, | ||||
|                 int(vm_id), | ||||
|             ) | ||||
|             vm_terminated = True | ||||
|         except socket.timeout as socket_err: | ||||
|             logger.error("Socket timeout error: {0}".format(socket_err)) | ||||
|         except OpenNebulaException as opennebula_err: | ||||
|             logger.error("OpenNebulaException error: {0}".format(opennebula_err)) | ||||
|         except OSError as os_err: | ||||
|             logger.error("OSError : {0}".format(os_err)) | ||||
|         except ValueError as value_err: | ||||
|             logger.error("ValueError : {0}".format(value_err)) | ||||
| 
 | ||||
|         return vm_terminated | ||||
| 
 | ||||
|     def get_vm_templates(self): | ||||
|         template_pool = oca.VmTemplatePool(self.oneadmin_client) | ||||
|         template_pool.info() | ||||
|  |  | |||
|  | @ -170,16 +170,28 @@ | |||
| 											{% csrf_token %}  | ||||
| 											</form>	 | ||||
| 												 | ||||
| 												<button type="text" data-href="{% url 'hosting:virtual_machines' virtual_machine.id %}" data-toggle="modal" data-target="#confirm-cancel" class="btn btn-danger">{% trans "Cancel Virtual Machine"%}</button> | ||||
| 												<button type="text" data-href="{% url 'hosting:virtual_machines' virtual_machine.id %}" data-toggle="modal" data-target="#confirm-cancel" class="btn btn-danger">{% trans "Terminate Virtual Machine"%}</button> | ||||
| 																						 | ||||
| 										</div> | ||||
|      | ||||
| 									</div> | ||||
|                                     <div class="col-md-12"> | ||||
|                                         <br/> | ||||
|                                         {% if messages %} | ||||
|                                             <div class="alert alert-warning"> | ||||
|                                                 {% for message in messages %} | ||||
|                                                 <span>{{ message }}</span> | ||||
|                                                 {% endfor %} | ||||
|                                             </div> | ||||
|                                         {% endif %} | ||||
|                                     </div> | ||||
| 
 | ||||
| 									<!-- Cancel Modal --> | ||||
| 									<div class="modal fade" id="confirm-cancel" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true"> | ||||
| 									    <div class="modal-dialog"> | ||||
| 									        <div class="modal-content"> | ||||
| 									            <div class="modal-header"> | ||||
| 									                {% trans "Cancel your Virtual Machine"%} | ||||
| 									                {% trans "Terminate your Virtual Machine"%} | ||||
| 									            </div> | ||||
| 									            <div class="modal-body"> | ||||
| 									                {% trans "Are you sure do you want to cancel your Virtual Machine "%} {{vm.virtual_machine}}  {% trans "plan?"%} | ||||
|  |  | |||
|  | @ -9,11 +9,18 @@ | |||
|                     <form method="POST" action="" > | ||||
|                         {% csrf_token %} | ||||
| 				        <h3><i class="fa fa-key" aria-hidden="true"></i>{% trans "Access Key"%} </h3> | ||||
|                         {% if messages %} | ||||
|                         <div class="alert alert-warning"> | ||||
|                             {% for message in messages %} | ||||
|                             <span>{{ message }}</span> | ||||
|                             {% endfor %} | ||||
|                         </div> | ||||
|                         {% endif %} | ||||
| 				        <hr/>	 | ||||
|                         {% if not user_key %} | ||||
|                             <div class="alert alert-warning"> | ||||
|                             <h3> | ||||
|                                 {% trans "Upload your own key. "%}  | ||||
|                             </div> | ||||
|                             </h3> | ||||
|                             <div class="form-group"> | ||||
|                               <label for="comment">Paste here your public key</label> | ||||
|                               <textarea class="form-control" rows="6" name="public_key"></textarea> | ||||
|  | @ -22,10 +29,10 @@ | |||
|                                 <button class="btn btn-success">{% trans "Upload Key"%} </a> | ||||
|                             </div> | ||||
|                          | ||||
|                             <div class="alert alert-warning"> | ||||
|                             <h3> | ||||
|                                 {% trans "Or generate a new key pair."%}  | ||||
| 
 | ||||
|                             </div> | ||||
|                             </h3> | ||||
|                             <div class="form-group"> | ||||
|                                 <button class="btn btn-success">{% trans "Generate Key Pair"%} </a> | ||||
|                             </div> | ||||
|  | @ -58,7 +65,7 @@ | |||
| 				        {% if private_key %} | ||||
| 				 		<div class="alert alert-warning"> | ||||
| 				 			  | ||||
|   							<strong>{% trans "Warning!"%}</strong>{% trans "You can view your SSH  private key once. Don't lost your key"%}   | ||||
|   							<strong>{% trans "Warning!"%}</strong>{% trans "You can download your SSH  private key once. Don't lost your key"%}   | ||||
| 						</div> | ||||
| 						<div class="form-group"> | ||||
| 						  <textarea class="form-control" rows="6" id="ssh_key" type="hidden" style="display:none">{{private_key}}</textarea> | ||||
|  |  | |||
|  | @ -11,6 +11,16 @@ | |||
|                     <a class="btn btn-success" href="{% url 'hosting:create-virtual-machine' %}" >{% trans "Create VM"%} </a>                     | ||||
|                 </p> | ||||
| 				<br/> | ||||
|                 <div class="col-md-12"> | ||||
|                     <br/> | ||||
|                     {% if messages %} | ||||
|                         <div class="alert alert-warning"> | ||||
|                             {% for message in messages %} | ||||
|                             <span>{{ message }}</span> | ||||
|                             {% endfor %} | ||||
|                         </div> | ||||
|                     {% endif %} | ||||
|                 </div> | ||||
| 				<thead>  | ||||
| 				<tr>  | ||||
| 					<th>{% trans "ID"%}</th> | ||||
|  |  | |||
							
								
								
									
										143
									
								
								hosting/views.py
									
										
									
									
									
								
							
							
						
						
									
										143
									
								
								hosting/views.py
									
										
									
									
									
								
							|  | @ -11,7 +11,8 @@ from django.contrib.auth import authenticate, login | |||
| from django.contrib import messages | ||||
| from django.conf import settings | ||||
| from django.shortcuts import redirect | ||||
| 
 | ||||
| from django.utils.http import urlsafe_base64_decode | ||||
| from django.contrib.auth.tokens import default_token_generator | ||||
| 
 | ||||
| from guardian.mixins import PermissionRequiredMixin | ||||
| from stored_messages.settings import stored_messages_settings | ||||
|  | @ -186,6 +187,43 @@ class PasswordResetConfirmView(PasswordResetConfirmViewMixin): | |||
|     template_name = 'hosting/confirm_reset_password.html' | ||||
|     success_url = reverse_lazy('hosting:login') | ||||
| 
 | ||||
|     def post(self, request, uidb64=None, token=None, *arg, **kwargs): | ||||
|         try: | ||||
|             uid = urlsafe_base64_decode(uidb64) | ||||
|             user = CustomUser.objects.get(pk=uid) | ||||
| 
 | ||||
|             opennebula_client = OpenNebulaManager( | ||||
|                 email=user.email, | ||||
|                 password=user.password, | ||||
|             ) | ||||
| 
 | ||||
|         except (TypeError, ValueError, OverflowError, CustomUser.DoesNotExist): | ||||
|             user = None | ||||
|             opennebula_client = None | ||||
| 
 | ||||
|         form = self.form_class(request.POST) | ||||
| 
 | ||||
|         if user is not None and default_token_generator.check_token(user, token): | ||||
|             if form.is_valid(): | ||||
|                 new_password = form.cleaned_data['new_password2'] | ||||
|                 user.set_password(new_password) | ||||
|                 user.save() | ||||
|                 messages.success(request, 'Password has been reset.') | ||||
| 
 | ||||
|                 # Change opennebula password | ||||
|                 opennebula_client.change_user_password(new_password) | ||||
| 
 | ||||
|                 return self.form_valid(form) | ||||
|             else: | ||||
|                 messages.error(request, 'Password reset has not been successful.') | ||||
|                 form.add_error(None, 'Password reset has not been successful.') | ||||
|                 return self.form_invalid(form) | ||||
| 
 | ||||
|         else: | ||||
|             messages.error(request, 'The reset password link is no longer valid.') | ||||
|             form.add_error(None, 'The reset password link is no longer valid.') | ||||
|             return self.form_invalid(form) | ||||
| 
 | ||||
| 
 | ||||
| class NotificationsView(LoginRequiredMixin, TemplateView): | ||||
|     template_name = 'hosting/notifications.html' | ||||
|  | @ -261,14 +299,6 @@ class GenerateVMSSHKeysView(LoginRequiredMixin, FormView): | |||
|                 'form': UserHostingKeyForm(request=self.request) | ||||
|             }) | ||||
| 
 | ||||
|         del(context['form']) | ||||
|         context.update({ | ||||
|             'form': form | ||||
|         }) | ||||
|         form = UserHostingKeyForm(request=self.request) | ||||
| 
 | ||||
|         print("context", context) | ||||
| 
 | ||||
|         # return HttpResponseRedirect(reverse('hosting:key_pair')) | ||||
|         return render(self.request, self.template_name, context) | ||||
| 
 | ||||
|  | @ -332,6 +362,21 @@ class PaymentVMView(LoginRequiredMixin, FormView): | |||
| 
 | ||||
|         return context | ||||
| 
 | ||||
|     def get(self, request, *args, **kwargs): | ||||
| 
 | ||||
|         try: | ||||
|             UserHostingKey.objects.get( | ||||
|                 user=self.request.user | ||||
|             ) | ||||
|         except UserHostingKey.DoesNotExist: | ||||
|             messages.success( | ||||
|                 request, | ||||
|                 'In order to create a VM, you create/upload your SSH KEY first.' | ||||
|             ) | ||||
|             return HttpResponseRedirect(reverse('hosting:key_pair')) | ||||
| 
 | ||||
|         return self.render_to_response(self.get_context_data()) | ||||
| 
 | ||||
|     def post(self, request, *args, **kwargs): | ||||
|         form = self.get_form() | ||||
| 
 | ||||
|  | @ -415,13 +460,27 @@ class PaymentVMView(LoginRequiredMixin, FormView): | |||
|             # If the Stripe payment was successed, set order status approved | ||||
|             order.set_approved() | ||||
| 
 | ||||
|             # Get user ssh key | ||||
|             try: | ||||
|                 user_key = UserHostingKey.objects.get( | ||||
|                     user=self.request.user | ||||
|                 ) | ||||
| 
 | ||||
|                 # Add ssh_key to specs | ||||
|                 specs.update({ | ||||
|                     'ssh_key': user_key.public_key | ||||
|                 }) | ||||
| 
 | ||||
|             except UserHostingKey.DoesNotExist: | ||||
|                 pass | ||||
| 
 | ||||
|             # Create a vm using logged user | ||||
|             oppennebula_vm_id = VirtualMachinePlan.create_opennebula_vm( | ||||
|             opennebula_vm_id = VirtualMachinePlan.create_opennebula_vm( | ||||
|                 self.request.user, | ||||
|                 specs | ||||
|             ) | ||||
| 
 | ||||
|             plan.oppenebula_id = oppennebula_vm_id | ||||
|             plan.opennebula_id = opennebula_vm_id | ||||
|             plan.save() | ||||
| 
 | ||||
|             # Send notification to ungleich as soon as VM has been booked | ||||
|  | @ -503,6 +562,18 @@ class CreateVirtualMachinesView(LoginRequiredMixin, View): | |||
|     login_url = reverse_lazy('hosting:login') | ||||
| 
 | ||||
|     def get(self, request, *args, **kwargs): | ||||
| 
 | ||||
|         try: | ||||
|             UserHostingKey.objects.get( | ||||
|                 user=self.request.user | ||||
|             ) | ||||
|         except UserHostingKey.DoesNotExist: | ||||
|             messages.success( | ||||
|                 request, | ||||
|                 'In order to create a VM, you need to create/upload your SSH KEY first.' | ||||
|             ) | ||||
|             return HttpResponseRedirect(reverse('hosting:key_pair')) | ||||
| 
 | ||||
|         context = { | ||||
|             'vm_types': VirtualMachineType.get_serialized_vm_types(), | ||||
|             'configuration_options': VirtualMachinePlan.VM_CONFIGURATION | ||||
|  | @ -565,18 +636,26 @@ class VirtualMachineView(PermissionRequiredMixin, LoginRequiredMixin, View): | |||
|     #     except Exception as error: | ||||
|     #         raise Http404() | ||||
| 
 | ||||
|     # def get_success_url(self): | ||||
|     #     vm = self.get_object() | ||||
|     #     final_url = "%s%s" % (reverse('hosting:virtual_machines', kwargs={'pk': vm.id}), | ||||
|     #                           '#status-v') | ||||
|     #     return final_url | ||||
|     def get_object(self): | ||||
|         opennebula_vm_id = self.kwargs.get('pk') | ||||
|         opennebula_vm = None | ||||
|         try: | ||||
|             opennebula_vm = VirtualMachinePlan.objects.get(opennebula_id=opennebula_vm_id) | ||||
|         except Exception as error: | ||||
|             print(error) | ||||
|             raise Http404() | ||||
|         return opennebula_vm | ||||
| 
 | ||||
|     def get_success_url(self): | ||||
|         final_url = reverse('hosting:virtual_machines') | ||||
|         return final_url | ||||
| 
 | ||||
|     def get(self, request, *args, **kwargs): | ||||
|         vm_id = self.kwargs.get('pk') | ||||
|         opennebula_vm_id = self.kwargs.get('pk') | ||||
|         try: | ||||
|             opennebula_vm = VirtualMachinePlan.get_vm( | ||||
|                 self.request.user, | ||||
|                 vm_id | ||||
|                 opennebula_vm_id | ||||
|             ) | ||||
|         except Exception as error: | ||||
|             print(error) | ||||
|  | @ -588,9 +667,24 @@ class VirtualMachineView(PermissionRequiredMixin, LoginRequiredMixin, View): | |||
|         # context = {} | ||||
|         return render(request, self.template_name, context) | ||||
| 
 | ||||
|     def post(self, *args, **kwargs): | ||||
|     def post(self, request, *args, **kwargs): | ||||
|         vm = self.get_object() | ||||
|         vm.cancel_plan() | ||||
| 
 | ||||
|         opennebula_vm_id = self.kwargs.get('pk') | ||||
| 
 | ||||
|         terminated = VirtualMachinePlan.terminate_opennebula_vm( | ||||
|             self.request.user, | ||||
|             opennebula_vm_id | ||||
|         ) | ||||
| 
 | ||||
|         if not terminated: | ||||
|             messages.error( | ||||
|                 request, | ||||
|                 'Error terminating VM %s' % (opennebula_vm_id) | ||||
|             ) | ||||
|             return HttpResponseRedirect(self.get_success_url()) | ||||
| 
 | ||||
|         #vm.cancel_plan() | ||||
| 
 | ||||
|         context = { | ||||
|             'vm': vm, | ||||
|  | @ -606,8 +700,14 @@ class VirtualMachineView(PermissionRequiredMixin, LoginRequiredMixin, View): | |||
|         email = BaseEmail(**email_data) | ||||
|         email.send() | ||||
| 
 | ||||
|         messages.error( | ||||
|             request, | ||||
|             'VM %s terminated successfully' % (opennebula_vm_id) | ||||
|         ) | ||||
| 
 | ||||
|         return HttpResponseRedirect(self.get_success_url()) | ||||
| 
 | ||||
| 
 | ||||
| class HostingBillListView(LoginRequiredMixin, ListView): | ||||
|     template_name = "hosting/bills.html" | ||||
|     login_url = reverse_lazy('hosting:login') | ||||
|  | @ -616,6 +716,7 @@ class HostingBillListView(LoginRequiredMixin, ListView): | |||
|     paginate_by = 10 | ||||
|     ordering = '-id' | ||||
| 
 | ||||
| 
 | ||||
| class HostingBillDetailView(PermissionRequiredMixin, LoginRequiredMixin, DetailView): | ||||
|     template_name = "hosting/bill_detail.html" | ||||
|     login_url = reverse_lazy('hosting:login') | ||||
|  | @ -624,7 +725,7 @@ class HostingBillDetailView(PermissionRequiredMixin, LoginRequiredMixin, DetailV | |||
|     model = HostingBill | ||||
| 
 | ||||
|     def get_object(self, queryset=None): | ||||
|         #Get HostingBill for primary key (Select from customer users) | ||||
|         # Get HostingBill for primary key (Select from customer users) | ||||
|         pk = self.kwargs['pk'] | ||||
|         object = HostingBill.objects.filter(customer__id=pk).first() | ||||
|         if object is None: | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue