From 7725a755fbab7ad859dd66f8b7267c774e5cc956 Mon Sep 17 00:00:00 2001 From: Balazs Hinel <gollam6@gmail.com> Date: Sat, 23 Jan 2016 17:02:14 +0100 Subject: [PATCH] Administration of VMware cloud in an educational environment --- circle/circle/settings/base.py | 29 +++++++++++++++++++++++++++++ circle/dashboard/forms.py | 500 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------------------------------------------------------------------------------------- circle/dashboard/static/dashboard/cluster-details.js | 31 +++++++++++++++++++++++++++++++ circle/dashboard/static/dashboard/dashboard.less | 2 +- circle/dashboard/tables.py | 21 ++++++++++++++++++++- circle/dashboard/templates/base.html | 2 +- circle/dashboard/templates/dashboard/cluster-create.html | 12 ++++++++++++ circle/dashboard/templates/dashboard/cluster-detail.html | 74 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ circle/dashboard/templates/dashboard/cluster-edit.html | 14 ++++++++++++++ circle/dashboard/templates/dashboard/create-vmware-vm.html | 12 ++++++++++++ circle/dashboard/templates/dashboard/index-vmware-clusters.html | 39 +++++++++++++++++++++++++++++++++++++++ circle/dashboard/templates/dashboard/index-vmware-instances.html | 26 ++++++++++++++++++++++++++ circle/dashboard/templates/dashboard/index.html | 9 +++++++++ circle/dashboard/templates/dashboard/vmware-vm-instance-detail.html | 56 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ circle/dashboard/templates/dashboard/vmwarevminstance-add.html | 12 ++++++++++++ circle/dashboard/templates/dashboard/vmwarevminstance-list/column-add.html | 5 +++++ circle/dashboard/templates/dashboard/vmwarevminstance-list/column-modify.html | 5 +++++ circle/dashboard/templates/dashboard/vmwarevminstance-list/column-remove.html | 5 +++++ circle/dashboard/urls.py | 24 ++++++++++++++++++++++++ circle/dashboard/views/cluster.py | 228 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ circle/dashboard/views/index.py | 12 +++++++++++- circle/dashboard/views/vmwarevminstance.py | 168 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ circle/vm/admin.py | 4 +++- circle/vm/migrations/0003_cluster_vmwarevminstance.py | 56 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ circle/vm/models/__init__.py | 4 +++- circle/vm/models/cluster.py | 308 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ circle/vm/models/vmwarevminstance.py | 159 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ circle/vm/tasks/local_periodic_tasks.py | 15 ++++++++++++++- 28 files changed, 1722 insertions(+), 110 deletions(-) create mode 100644 circle/dashboard/static/dashboard/cluster-details.js create mode 100644 circle/dashboard/templates/dashboard/cluster-create.html create mode 100644 circle/dashboard/templates/dashboard/cluster-detail.html create mode 100644 circle/dashboard/templates/dashboard/cluster-edit.html create mode 100644 circle/dashboard/templates/dashboard/create-vmware-vm.html create mode 100644 circle/dashboard/templates/dashboard/index-vmware-clusters.html create mode 100644 circle/dashboard/templates/dashboard/index-vmware-instances.html create mode 100644 circle/dashboard/templates/dashboard/vmware-vm-instance-detail.html create mode 100644 circle/dashboard/templates/dashboard/vmwarevminstance-add.html create mode 100644 circle/dashboard/templates/dashboard/vmwarevminstance-list/column-add.html create mode 100644 circle/dashboard/templates/dashboard/vmwarevminstance-list/column-modify.html create mode 100644 circle/dashboard/templates/dashboard/vmwarevminstance-list/column-remove.html create mode 100644 circle/dashboard/views/cluster.py create mode 100644 circle/dashboard/views/vmwarevminstance.py create mode 100644 circle/vm/migrations/0003_cluster_vmwarevminstance.py create mode 100644 circle/vm/models/cluster.py create mode 100644 circle/vm/models/vmwarevminstance.py diff --git a/circle/circle/settings/base.py b/circle/circle/settings/base.py index 4956b2b..84d33b0 100644 --- a/circle/circle/settings/base.py +++ b/circle/circle/settings/base.py @@ -201,6 +201,7 @@ PIPELINE_JS = { "datatables/media/js/jquery.dataTables.js", "dashboard/dashboard.js", "dashboard/activity.js", + "dashboard/cluster-details.js", "dashboard/group-details.js", "dashboard/group-list.js", "dashboard/js/stupidtable.min.js", # no bower file @@ -343,6 +344,7 @@ THIRD_PARTY_APPS = ( 'django_sshkey', 'autocomplete_light', 'pipeline', + 'pyVmomi', ) @@ -440,6 +442,33 @@ CACHES = { } +import ldap +from django_auth_ldap.config import LDAPSearch, GroupOfNamesType +import logging + +logger = logging.getLogger('django_auth_ldap') +logger.addHandler(logging.StreamHandler()) +logger.setLevel(logging.DEBUG) + +# Baseline configuration. +AUTH_LDAP_SERVER_URI = "ldap://sch.bme.hu" + +AUTH_LDAP_BIND_DN = "cn=_vmware_reader,ou=VMware,ou=KSZK,ou=Hosts,dc=sch,dc=bme,dc=hu" +AUTH_LDAP_BIND_PASSWORD = "scheu3iSeez" +AUTH_LDAP_USER_SEARCH = LDAPSearch("ou=Users,ou=SCHAccount,dc=sch,dc=bme,dc=hu", + ldap.SCOPE_SUBTREE, "(uid=%(user)s)") + +AUTH_LDAP_USER_ATTR_MAP = { + "first_name": "givenName", + "last_name": "sn", + "email": "mail" +} + +AUTHENTICATION_BACKENDS = ( + 'django_auth_ldap.backend.LDAPBackend', + 'django.contrib.auth.backends.ModelBackend', +) + if get_env_variable('DJANGO_SAML', 'FALSE') == 'TRUE': try: from shutil import which # python >3.4 diff --git a/circle/dashboard/forms.py b/circle/dashboard/forms.py index 7c3a9c1..4549167 100644 --- a/circle/dashboard/forms.py +++ b/circle/dashboard/forms.py @@ -52,8 +52,8 @@ from django.core.urlresolvers import reverse_lazy from django_sshkey.models import UserKey from firewall.models import Vlan, Host from vm.models import ( - InstanceTemplate, Lease, InterfaceTemplate, Node, Trait, Instance -) + InstanceTemplate, Lease, InterfaceTemplate, Node, Trait, Instance, Cluster, + VMwareVMInstance) from storage.models import DataStore, Disk from django.contrib.admin.widgets import FilteredSelectMultiple from django.contrib.auth.models import Permission @@ -77,7 +77,6 @@ priority_choices = ( class NoFormTagMixin(object): - @property def helper(self): helper = FormHelper(self) @@ -187,7 +186,7 @@ class VmCustomizeForm(forms.Form): self.initial['ram_size'] = self.template.ram_size else: - self.allowed_fields = ("name", "template", "customized", ) + self.allowed_fields = ("name", "template", "customized",) # initial name and template pk self.initial['name'] = self.template.name @@ -212,7 +211,6 @@ class VmCustomizeForm(forms.Form): class GroupCreateForm(NoFormTagMixin, forms.ModelForm): - description = forms.CharField(label=_("Description"), required=False, widget=forms.Textarea(attrs={'rows': 3})) @@ -256,11 +254,10 @@ class GroupCreateForm(NoFormTagMixin, forms.ModelForm): class Meta: model = Group - fields = ('name', ) + fields = ('name',) class GroupProfileUpdateForm(NoFormTagMixin, forms.ModelForm): - def __init__(self, *args, **kwargs): new_groups = kwargs.pop('new_groups', None) superuser = kwargs.pop('superuser', False) @@ -295,7 +292,6 @@ class GroupProfileUpdateForm(NoFormTagMixin, forms.ModelForm): class HostForm(NoFormTagMixin, forms.ModelForm): - def setowner(self, user): self.instance.owner = user @@ -316,48 +312,48 @@ class HostForm(NoFormTagMixin, forms.ModelForm): css_class="row", ), Div( # host data - Div( # hostname - HTML('<label for="node-hostname-box">' - 'Name' - '</label>'), - css_class="col-sm-3", - ), - Div( # hostname - 'hostname', - css_class="col-sm-9", - ), - Div( # mac - HTML('<label for="node-mac-box">' - 'MAC' - '</label>'), - css_class="col-sm-3", - ), - Div( - 'mac', - css_class="col-sm-9", - ), - Div( # ip - HTML('<label for="node-ip-box">' - 'IP' - '</label>'), - css_class="col-sm-3", - ), - Div( - 'ipv4', - css_class="col-sm-9", - ), - Div( # vlan - HTML('<label for="node-vlan-box">' - 'VLAN' - '</label>'), - css_class="col-sm-3", - ), - Div( - 'vlan', - css_class="col-sm-9", - ), - css_class="row", - ), + Div( # hostname + HTML('<label for="node-hostname-box">' + 'Name' + '</label>'), + css_class="col-sm-3", + ), + Div( # hostname + 'hostname', + css_class="col-sm-9", + ), + Div( # mac + HTML('<label for="node-mac-box">' + 'MAC' + '</label>'), + css_class="col-sm-3", + ), + Div( + 'mac', + css_class="col-sm-9", + ), + Div( # ip + HTML('<label for="node-ip-box">' + 'IP' + '</label>'), + css_class="col-sm-3", + ), + Div( + 'ipv4', + css_class="col-sm-9", + ), + Div( # vlan + HTML('<label for="node-vlan-box">' + 'VLAN' + '</label>'), + css_class="col-sm-3", + ), + Div( + 'vlan', + css_class="col-sm-9", + ), + css_class="row", + ), ), ) return helper @@ -368,7 +364,6 @@ class HostForm(NoFormTagMixin, forms.ModelForm): class NodeForm(forms.ModelForm): - def __init__(self, *args, **kwargs): super(NodeForm, self).__init__(*args, **kwargs) self.helper = FormHelper(self) @@ -388,11 +383,11 @@ class NodeForm(forms.ModelForm): ), Div( Div( # nodename - HTML('<label for="node-nodename-box">' - 'Name' - '</label>'), - css_class="col-sm-3", - ), + HTML('<label for="node-nodename-box">' + 'Name' + '</label>'), + css_class="col-sm-3", + ), Div( 'name', css_class="col-sm-9", @@ -401,11 +396,11 @@ class NodeForm(forms.ModelForm): ), Div( Div( # priority - HTML('<label for="node-nodename-box">' - 'Priority' - '</label>'), - css_class="col-sm-3", - ), + HTML('<label for="node-nodename-box">' + 'Priority' + '</label>'), + css_class="col-sm-3", + ), Div( 'priority', css_class="col-sm-9", @@ -414,11 +409,11 @@ class NodeForm(forms.ModelForm): ), Div( Div( # enabled - HTML('<label for="node-nodename-box">' - 'Enabled' - '</label>'), - css_class="col-sm-3", - ), + HTML('<label for="node-nodename-box">' + 'Enabled' + '</label>'), + css_class="col-sm-3", + ), Div( 'enabled', css_class="col-sm-9", @@ -426,22 +421,22 @@ class NodeForm(forms.ModelForm): css_class="row", ), Div( # nested host - HTML("""{% load crispy_forms_tags %} + HTML("""{% load crispy_forms_tags %} {% crispy hostform %} """) - ), + ), Div( Div( AnyTag( # tip: don't try to use Button class - "button", - AnyTag( - "i", - css_class="fa fa-play" - ), - HTML("Start"), - css_id="node-create-submit", - css_class="btn btn-success", - ), + "button", + AnyTag( + "i", + css_class="fa fa-play" + ), + HTML("Start"), + css_id="node-create-submit", + css_class="btn btn-success", + ), css_class="col-sm-12 text-right", ), css_class="row", @@ -457,6 +452,317 @@ class NodeForm(forms.ModelForm): fields = ['name', 'priority', 'enabled', ] +class ClusterCreateForm(forms.ModelForm): + password = forms.CharField(label=_("Password"), + widget=forms.PasswordInput) + + def __init__(self, *args, **kwargs): + super(ClusterCreateForm, self).__init__(*args, **kwargs) + self.helper = FormHelper(self) + self.helper.form_show_labels = False + self.helper.layout = Layout( + Div( + Div( + Div( + Div( + AnyTag( + 'h3', + HTML(_("Cluster")), + ), + css_class="col-sm-3", + ), + css_class="row", + ), + Div( + Div( # cluster name + HTML('<label for="node-nodename-box">' + 'Name' + '</label>'), + css_class="col-sm-3", + ), + Div( + 'name', + css_class="col-sm-9", + ), + css_class="row", + ), + Div( + Div( # cluster address + HTML('<label for="node-nodename-box">' + 'Address' + '</label>'), + css_class="col-sm-3", + ), + Div( + 'address', + css_class="col-sm-9", + ), + css_class="row", + ), + Div( + Div( # username used for the connection + HTML('<label for="node-nodename-box">' + 'Username' + '</label>'), + css_class="col-sm-3", + ), + Div( + 'username', + css_class="col-sm-9", + ), + css_class="row", + ), + Div( + Div( # password used for the connection + HTML('<label for="node-nodename-box">' + 'Password' + '</label>'), + css_class="col-sm-3", + ), + Div( + 'password', + css_class="col-sm-9", + ), + css_class="row", + ), + Div( + Div( + AnyTag( # tip: don't try to use Button class + "button", + AnyTag( + "i", + css_class="fa fa-save" + ), + HTML("Save"), + css_id="node-create-submit", + css_class="btn btn-success", + ), + css_class="col-sm-12 text-right", + ), + css_class="row", + ), + css_class="col-sm-11", + ), + css_class="row", + ), + ) + + def save(self): + new_cluster = super(ClusterCreateForm, self).save() + + return new_cluster + + class Meta: + model = Cluster + fields = ['name', 'address', 'username', 'password', ] + + +class VMwareVMInstanceCreateForm(forms.ModelForm): + + def __init__(self, *args, **kwargs): + super(VMwareVMInstanceCreateForm, self).__init__(*args) + + if "cluster_pk" in kwargs: + self.cluster_pk = kwargs.pop('cluster_pk') + + self.helper = FormHelper(self) + self.helper.form_show_labels = False + self.helper.layout = Layout( + Div( + Div( + Div( + Div( + HTML('<label>' + 'Name' + '</label>'), + css_class="col-sm-3", + ), + Div( + 'name', + css_class="col-sm-9", + ), + css_class="row", + ), + Div( + Div( + HTML('<label>' + '# of CPU cores' + '</label>'), + css_class="col-sm-3", + ), + Div( + 'cpu_cores', + css_class="col-sm-9", + ), + css_class="row", + ), + Div( + Div( + HTML('<label>' + 'Amount of memory' + '</label>'), + css_class="col-sm-3", + ), + Div( + 'memory_size', + css_class="col-sm-9", + ), + css_class="row", + ), + Div( + Div( + HTML('<label>' + 'Time of expiration' + '</label>'), + css_class="col-sm-3", + ), + Div( + 'time_of_expiration', + css_class="col-sm-9", + ), + css_class="row", + ), + Div( + Div( + HTML('<label>' + 'Owner' + '</label>'), + css_class="col-sm-3", + ), + Div( + 'owner', + css_class="col-sm-9", + ), + css_class="row", + ), + Div( + Div( + AnyTag( # tip: don't try to use Button class + "button", + AnyTag( + "i", + css_class="fa fa-save" + ), + HTML("Create"), + css_id="node-create-submit", + css_class="btn btn-success", + ), + css_class="col-sm-12 text-right", + ), + css_class="row", + ), + css_class="col-sm-11", + ), + css_class="row", + ), + ) + + def save(self, **kwargs): + new_vmware_vm = super(VMwareVMInstanceCreateForm, self).save(commit=False) + own_cluster = Cluster.objects.get(pk=self.cluster_pk) + new_vmware_vm.cluster = own_cluster + new_vmware_vm.uuid = 'placeholder' + new_vmware_vm.operating_system = 'placeholder' + + new_vmware_vm.save() + + return new_vmware_vm + + class Meta: + model = VMwareVMInstance + fields = ['name', 'cpu_cores', 'memory_size', 'time_of_expiration', 'owner', ] + + +class VMwareVMInstanceForm(forms.ModelForm): + + time_of_expiration = forms.DateField(widget=forms.TextInput(attrs= + { + 'class': 'datepicker', + })) + + def __init__(self, *args, **kwargs): + + if "uuid" in kwargs: + self.uuid = kwargs.pop('uuid') + + if "cluster_pk" in kwargs: + self.cluster_pk = kwargs.pop('cluster_pk') + + super(VMwareVMInstanceForm, self).__init__(*args, **kwargs) + self.helper = FormHelper(self) + self.helper.form_show_labels = False + self.helper.layout = Layout( + Div( + Div( + Div( + Div( # time of expiration + HTML('<label>' + 'Time of expiration' + '</label>'), + css_class="col-sm-3", + ), + Div( + 'time_of_expiration', + css_class="col-sm-9", + ), + css_class="row", + ), + Div( + Div( # the owner of the vm + HTML('<label>' + 'Owner' + '</label>'), + css_class="col-sm-3", + ), + Div( + 'owner', + css_class="col-sm-9", + ), + css_class="row", + ), + Div( + Div( + AnyTag( # tip: don't try to use Button class + "button", + AnyTag( + "i", + css_class="fa fa-plus" + ), + HTML("Add"), + css_id="node-create-submit", + css_class="btn btn-success", + ), + css_class="col-sm-12 text-right", + ), + css_class="row", + ), + css_class="col-sm-11", + ), + css_class="row", + ), + ) + + def save(self): + new_virtual_machine = super(VMwareVMInstanceForm, self).save(commit=False) + own_cluster = Cluster.objects.get(pk=self.cluster_pk) + + new_virtual_machine.cluster = own_cluster + new_virtual_machine.instanceUUID = self.uuid + + vm_details = own_cluster.get_vm_details_by_uuid(self.uuid) + + new_virtual_machine.name = vm_details["name"] + new_virtual_machine.cpu_cores = vm_details["cpu"] + new_virtual_machine.memory_size = vm_details["memory"] + new_virtual_machine.operating_system = vm_details["os"] + new_virtual_machine.save() + return new_virtual_machine + + class Meta: + model = VMwareVMInstance + fields = ['time_of_expiration', 'owner'] + + class TemplateForm(forms.ModelForm): networks = forms.ModelMultipleChoiceField( queryset=None, required=False, label=_("Networks")) @@ -507,11 +813,11 @@ class TemplateForm(forms.ModelForm): 'name', 'access_method', 'description', 'system', 'tags', 'arch', 'lease', 'has_agent') if (self.user.has_perm('vm.change_template_resources') - or not self.instance.pk): + or not self.instance.pk): self.allowed_fields += tuple(set(self.fields.keys()) - set(['raw_data'])) if self.user.is_superuser: - self.allowed_fields += ('raw_data', ) + self.allowed_fields += ('raw_data',) for name, field in self.fields.items(): if name not in self.allowed_fields: field.widget.attrs['disabled'] = 'disabled' @@ -600,7 +906,7 @@ class TemplateForm(forms.ModelForm): class Meta: model = InstanceTemplate - exclude = ('state', 'disks', ) + exclude = ('state', 'disks',) widgets = { 'system': forms.TextInput, 'max_ram_size': forms.HiddenInput, @@ -609,7 +915,6 @@ class TemplateForm(forms.ModelForm): class LeaseForm(forms.ModelForm): - def __init__(self, *args, **kwargs): super(LeaseForm, self).__init__(*args, **kwargs) self.generate_fields() @@ -743,7 +1048,6 @@ class LeaseForm(forms.ModelForm): class VmRenewForm(OperationForm): - force = forms.BooleanField(required=False, label=_( "Set expiration times even if they are shorter than " "the current value.")) @@ -783,11 +1087,10 @@ class VmMigrateForm(forms.Form): class VmStateChangeForm(OperationForm): - interrupt = forms.BooleanField(required=False, label=_( "Forcibly interrupt all running activities."), - help_text=_("Set all activities to finished state, " - "but don't interrupt any tasks.")) + help_text=_("Set all activities to finished state, " + "but don't interrupt any tasks.")) new_state = forms.ChoiceField(Instance.STATUS, label=_( "New status")) reset_node = forms.BooleanField(required=False, label=_("Reset node")) @@ -856,7 +1159,7 @@ class VmDiskResizeForm(OperationForm): " GB or MB!")) if int(size_in_bytes) < int(disk.size): raise forms.ValidationError(_("Disk size must be greater than the " - "actual size.")) + "actual size.")) return cleaned_data @property @@ -980,7 +1283,6 @@ class DeployChoiceField(forms.ModelChoiceField): class VmDeployForm(OperationForm): - def __init__(self, *args, **kwargs): choices = kwargs.pop('choices', None) instance = kwargs.pop('instance', None) @@ -1125,7 +1427,6 @@ class CirclePasswordResetForm(PasswordResetForm): class CircleSetPasswordForm(SetPasswordForm): - @property def helper(self): helper = FormHelper() @@ -1136,7 +1437,6 @@ class CircleSetPasswordForm(SetPasswordForm): class LinkButton(BaseInput): - """ Used to create a link button descriptor for the {% crispy %} template tag:: @@ -1183,7 +1483,6 @@ class AnyTag(Div): class WorkingBaseInput(BaseInput): - def __init__(self, name, value, input_type="text", **kwargs): self.input_type = input_type self.field_classes = "" # we need this for some reason @@ -1191,7 +1490,6 @@ class WorkingBaseInput(BaseInput): class TraitForm(forms.ModelForm): - def __init__(self, *args, **kwargs): super(TraitForm, self).__init__(*args, **kwargs) self.helper = FormHelper(self) @@ -1223,7 +1521,7 @@ class MyProfileForm(forms.ModelForm): class Meta: fields = ('preferred_language', 'email_notifications', - 'use_gravatar', ) + 'use_gravatar',) model = Profile @property @@ -1238,9 +1536,8 @@ class MyProfileForm(forms.ModelForm): class UnsubscribeForm(forms.ModelForm): - class Meta: - fields = ('email_notifications', ) + fields = ('email_notifications',) model = Profile @property @@ -1251,7 +1548,6 @@ class UnsubscribeForm(forms.ModelForm): class CirclePasswordChangeForm(PasswordChangeForm): - @property def helper(self): helper = FormHelper() @@ -1392,10 +1688,9 @@ class ConnectCommandForm(forms.ModelForm): class TraitsForm(forms.ModelForm): - class Meta: model = Instance - fields = ('req_traits', ) + fields = ('req_traits',) @property def helper(self): @@ -1415,7 +1710,7 @@ class RawDataForm(forms.ModelForm): class Meta: model = Instance - fields = ('raw_data', ) + fields = ('raw_data',) @property def helper(self): @@ -1477,7 +1772,7 @@ class GroupPermissionForm(forms.ModelForm): class Meta: model = Group - fields = ('permissions', ) + fields = ('permissions',) @property def helper(self): @@ -1525,7 +1820,7 @@ class VmResourcesForm(forms.ModelForm): class Meta: model = Instance - fields = ('num_cores', 'priority', 'ram_size', ) + fields = ('num_cores', 'priority', 'ram_size',) vm_search_choices = ( @@ -1586,7 +1881,6 @@ class UserListSearchForm(forms.Form): class DataStoreForm(ModelForm): - @property def helper(self): helper = FormHelper() @@ -1605,7 +1899,7 @@ class DataStoreForm(ModelForm): class Meta: model = DataStore - fields = ("name", "path", "hostname", ) + fields = ("name", "path", "hostname",) class DiskForm(ModelForm): @@ -1623,7 +1917,7 @@ class DiskForm(ModelForm): class Meta: model = Disk fields = ("name", "filename", "datastore", "type", "bus", "size", - "base", "dev_num", "destroyed", "is_ready", ) + "base", "dev_num", "destroyed", "is_ready",) class MessageForm(ModelForm): diff --git a/circle/dashboard/static/dashboard/cluster-details.js b/circle/dashboard/static/dashboard/cluster-details.js new file mode 100644 index 0000000..75f903b --- /dev/null +++ b/circle/dashboard/static/dashboard/cluster-details.js @@ -0,0 +1,31 @@ +$(function() { + /* rename */ + $("#cluster-details-h1-name, .cluster-details-rename-button").click(function() { + $("#cluster-details-h1-name span").hide(); + $("#cluster-details-rename-form").show().css('display', 'inline-block'); + $("#cluster-details-rename-name").select(); + }); + + /* rename ajax */ + $('#cluster-details-rename-submit').click(function() { + if(!$("#cluster-details-rename-name")[0].checkValidity()) { + return true; + } + var name = $('#cluster-details-rename-name').val(); + + $.ajax({ + method: 'POST', + url: location.href, + data: {'new_name': name}, + headers: {"X-CSRFToken": getCookie('csrftoken')}, + success: function(data, textStatus, xhr) { + $("#cluster-details-h1-name span").text(data.new_name).show(); + $('#cluster-details-rename-form').hide(); + }, + error: function(xhr, textStatus, error) { + addMessage("Error during renaming.", "danger"); + } + }); + return false; + }); +}); diff --git a/circle/dashboard/static/dashboard/dashboard.less b/circle/dashboard/static/dashboard/dashboard.less index e239d37..d963152 100644 --- a/circle/dashboard/static/dashboard/dashboard.less +++ b/circle/dashboard/static/dashboard/dashboard.less @@ -211,7 +211,7 @@ html { display: none; } -#group-details-rename-form { +#cluster-details-rename-form, #group-details-rename-form { display: inline-block; } diff --git a/circle/dashboard/tables.py b/circle/dashboard/tables.py index a64f68c..aa85047 100644 --- a/circle/dashboard/tables.py +++ b/circle/dashboard/tables.py @@ -28,7 +28,7 @@ from django_tables2.columns import ( from django_sshkey.models import UserKey from storage.models import Disk -from vm.models import Node, InstanceTemplate, Lease +from vm.models import Node, InstanceTemplate, Lease, Cluster from dashboard.models import ConnectCommand, Message @@ -111,6 +111,25 @@ class NodeListTable(Table): 'minion_online', 'overcommit', 'number_of_VMs', ) +class ClusterListTable(Table): + + pk = Column( + verbose_name="ID", + attrs={'th': {'class': 'cluster-list-table-thin'}}, + ) + + name = TemplateColumn( + template_name="dashboard/cluster-list/column-name.html", + order_by="normalized_name" + ) + + class Meta: + model = Cluster + attrs = {'class': ('table table-bordered table-striped table-hover ' + 'node-list-table')} + fields = ('name', ) + + class GroupListTable(Table): pk = TemplateColumn( template_name='dashboard/group-list/column-id.html', diff --git a/circle/dashboard/templates/base.html b/circle/dashboard/templates/base.html index 206dd15..0a68c50 100644 --- a/circle/dashboard/templates/base.html +++ b/circle/dashboard/templates/base.html @@ -23,6 +23,7 @@ {% block extra_css %}{% endblock %} + <script src="{% static "jquery/dist/jquery.min.js" %}"></script> </head> <body> @@ -87,7 +88,6 @@ <span class="pull-right">{{ COMPANY_NAME }}</span> </footer> - <script src="{% static "jquery/dist/jquery.min.js" %}"></script> <script src="{{ STATIC_URL }}jsi18n/{{ LANGUAGE_CODE }}/djangojs.js"></script> {% javascript 'all' %} diff --git a/circle/dashboard/templates/dashboard/cluster-create.html b/circle/dashboard/templates/dashboard/cluster-create.html new file mode 100644 index 0000000..9c02c02 --- /dev/null +++ b/circle/dashboard/templates/dashboard/cluster-create.html @@ -0,0 +1,12 @@ +{% load crispy_forms_tags %} +{% load i18n %} + +<p class="text-muted"> +{% trans "Adding VMware clusters enable managing virtual machines inside them." %} +</p> + + +<form method="POST" action="{% url "dashboard.views.cluster-create" %}"> +{% csrf_token %} +{% crispy form %} +</form> diff --git a/circle/dashboard/templates/dashboard/cluster-detail.html b/circle/dashboard/templates/dashboard/cluster-detail.html new file mode 100644 index 0000000..6aa129c --- /dev/null +++ b/circle/dashboard/templates/dashboard/cluster-detail.html @@ -0,0 +1,74 @@ +{% extends "dashboard/base.html" %} +{% load staticfiles %} +{% load crispy_forms_tags %} +{% load i18n %} +{% load render_table from django_tables2 %} + +{% block title-page %}{{ cluster.name }} | {% trans "Cluster" %}{% endblock %} + +{% block content %} +<div class="body-content"> + <div class="page-header"> + <div class="pull-right" style="padding-top: 15px;"> + <a data-group-uuid="{{ record.uuid }}" + class="btn btn-success btn-xs real-link vmwarecluster-edit" + href="{% url "dashboard.views.vmwarevminstance-create" cluster=cluster.pk %}?next={{ request.path }}"> + <i class="fa fa-plus"></i> add new vm + </a> + + <a data-group-uuid="{{ record.uuid }}" + class="btn btn-info btn-xs real-link vmwarecluster-edit" + href="{% url "dashboard.views.cluster-edit" pk=cluster.pk %}?next={{ request.path }}"> + <i class="fa fa-pencil"></i> edit + </a> + + <a data-group-uuid="{{ record.uuid }}" + class="btn btn-danger btn-xs real-link vmwarecluster-edit" + href="{% url "dashboard.views.cluster-delete" pk=cluster.pk %}?next={{ request.path }}"> + <i class="fa fa-trash-o"></i> delete + </a> + </div> + <h1> + <form action="" method="POST" id="cluster-details-rename-form" class="js-hidden"> + {% csrf_token %} + <div class="input-group"> + <input id="cluster-details-rename-name" class="form-control" name="new_name" + type="text" value="{{ cluster.name }}" required /> + <span class="input-group-btn"> + <button type="submit" id="cluster-details-rename-submit" class="btn"> + {% trans "Modify" %} + </button> + </span> + </div> + </form> + <div id="cluster-details-h1-name"> + <span class="no-js-hidden">{{ cluster.name }}</span> + </div> + </h1> + </div><!-- .page-header --> + {% if unmanaged_vms_table %} + <h3>Unmanaged virtual machines</h3> + <div class="panel-body"> + <div class="table-responsive"> + {% render_table unmanaged_vms_table %} + </div> + </div> + {% endif %} + {% if managed_vms_table %} + <h3>Managed virtual machines</h3> + <div class="panel-body"> + <div class="table-responsive"> + {% render_table managed_vms_table %} + </div> + </div> + {% endif %} + {% if deleted_vms_table %} + <h3>Deleted virtual machines</h3> + <div class="panel-body"> + <div class="table-responsive"> + {% render_table deleted_vms_table %} + </div> + </div> + {% endif %} +</div> +{% endblock %} diff --git a/circle/dashboard/templates/dashboard/cluster-edit.html b/circle/dashboard/templates/dashboard/cluster-edit.html new file mode 100644 index 0000000..b884fea --- /dev/null +++ b/circle/dashboard/templates/dashboard/cluster-edit.html @@ -0,0 +1,14 @@ +{% extends "dashboard/base.html" %} +{% load crispy_forms_tags %} +{% load i18n %} + +{% block title-page %}{% trans "Edit cluster " %}{{ cluster.name }}{% endblock %} + +{% block content %} + +<form method="POST" action="{% url "dashboard.views.cluster-edit" pk=cluster.pk %}"> +{% csrf_token %} +{% crispy form %} +</form> + +{% endblock %} \ No newline at end of file diff --git a/circle/dashboard/templates/dashboard/create-vmware-vm.html b/circle/dashboard/templates/dashboard/create-vmware-vm.html new file mode 100644 index 0000000..d9c7cef --- /dev/null +++ b/circle/dashboard/templates/dashboard/create-vmware-vm.html @@ -0,0 +1,12 @@ +{% load crispy_forms_tags %} +{% load i18n %} + +<p class="text-muted"> +{% trans "Create a new virtual machine." %} +</p> + + +<form method="POST" action="{% url "dashboard.views.vmwarevminstance-create" cluster=cluster_pk %}"> +{% csrf_token %} +{% crispy form %} +</form> diff --git a/circle/dashboard/templates/dashboard/index-vmware-clusters.html b/circle/dashboard/templates/dashboard/index-vmware-clusters.html new file mode 100644 index 0000000..40ea970 --- /dev/null +++ b/circle/dashboard/templates/dashboard/index-vmware-clusters.html @@ -0,0 +1,39 @@ +{% load i18n %} +<div class="panel panel-default"> + <div class="panel-heading"> + <div class="pull-right toolbar"> + <span class="btn btn-default btn-xs infobtn" data-container="body" title="{% trans "List of VMware clusters, which run the virtual machines." %}"> + <i class="fa fa-info-circle"></i> + </span> + </div> + <h3 class="no-margin"> + <i class="fa fa-cloud"></i> {% trans "VMware clusters" %} + </h3> + </div > + <div class="list-group" id="cluster-list-view"> + <div id="dashboard-cluster-list"> + {% for i in clusters %} + <a href="{{ i.get_absolute_url }}" class="list-group-item real-link + {% if forloop.last and clusters|length < 5 %} list-group-item-last{% endif %}"> + <span class="index-cluster-list-name"> + <i class="fa {{ i.get_status_icon }}" title="{{ i.get_status_display }}"></i> + {{ i.name }} + </span> + <div style="clear: both;"></div> + </a> + {% endfor %} + </div> + + <div class="list-group-item list-group-footer"> + <div class="row"> + <div class="col-xs-12 text-right"> + {% if request.user.is_superuser %} + <a class="btn btn-success btn-xs cluster-create" href="{% url "dashboard.views.cluster-create" %}"> + <i class="fa fa-plus-circle"></i> {% trans "add" %} + </a> + {% endif %} + </div> + </div> + </div> + </div><!-- #cluster-list-view --> +</div> diff --git a/circle/dashboard/templates/dashboard/index-vmware-instances.html b/circle/dashboard/templates/dashboard/index-vmware-instances.html new file mode 100644 index 0000000..e696168 --- /dev/null +++ b/circle/dashboard/templates/dashboard/index-vmware-instances.html @@ -0,0 +1,26 @@ +{% load i18n %} +<div class="panel panel-default"> + <div class="panel-heading"> + <div class="pull-right toolbar"> + <span class="btn btn-default btn-xs infobtn" data-container="body" title="{% trans "List of your assigned VMware clusters." %}"> + <i class="fa fa-info-circle"></i> + </span> + </div> + <h3 class="no-margin"> + <i class="fa fa-cloud"></i> {% trans "VMware instances" %} + </h3> + </div > + <div class="list-group" id="vmware-instance-list-view"> + <div id="dashboard-vmware-instance-list"> + {% for i in vmware_instances %} + <a href="{{ i.get_absolute_url }}" class="list-group-item real-link + {% if forloop.last and clusters|length < 5 %} list-group-item-last{% endif %}"> + <span class="index-vmware-instance-list-name"> + {{ i.name }} + </span> + <div style="clear: both;"></div> + </a> + {% endfor %} + </div> + </div><!-- #vmware-instance-list-view --> +</div> diff --git a/circle/dashboard/templates/dashboard/index.html b/circle/dashboard/templates/dashboard/index.html index 0a1e6b9..e8d9cc3 100644 --- a/circle/dashboard/templates/dashboard/index.html +++ b/circle/dashboard/templates/dashboard/index.html @@ -19,6 +19,9 @@ <div class="col-lg-4 col-sm-6"> {% include "dashboard/index-vm.html" %} </div> + <div class="col-lg-4 col-sm-6"> + {% include "dashboard/index-vmware-instances.html" %} + </div> {% else %} <div class="alert alert-info"> {% trans "You have no permission to start or manage virtual machines." %} @@ -43,6 +46,12 @@ </div> {% endif %} + {% if perms.vm.create_cluster %} + <div class="col-lg-4 col-sm-6"> + {% include "dashboard/index-vmware-clusters.html" %} + </div> + {% endif %} + {% if perms.vm.view_statistics %} <div class="col-lg-4 col-sm-6"> {% include "dashboard/index-nodes.html" %} diff --git a/circle/dashboard/templates/dashboard/vmware-vm-instance-detail.html b/circle/dashboard/templates/dashboard/vmware-vm-instance-detail.html new file mode 100644 index 0000000..dbd0161 --- /dev/null +++ b/circle/dashboard/templates/dashboard/vmware-vm-instance-detail.html @@ -0,0 +1,56 @@ +{% extends "dashboard/base.html" %} +{% load staticfiles %} +{% load i18n %} +{% load pipeline %} + +{% block title-page %}{{ instance.name }} | vm{% endblock %} + +{% block content %} + + +<div class="body-content"> + <div class="page-header"> + <div class="pull-right" id="ops"> + {% include "dashboard/vm-detail/_operations.html" %} + </div> + <h1> + <div id="vm-details-h1-name" class="vm-details-home-edit-name"> + {{ instance.name }} + </div> + </h1> + <div style="clear: both;"></div> + </div> + {% if instance.node and not instance.node.online %} + <div class="alert alert-warning"> + {% if user.is_superuser %} + {% blocktrans with name=instance.node.name %} + The node <strong>{{ name }}</strong> is missing. + {% endblocktrans %} + {% else %} + {% trans "Currently you cannot execute any operations because the virtual machine's node is missing." %} + {% endif %} + </div> + {% endif %} + <div class="row"> + <div class="col-md-4" id="vm-info-pane"> + <div class="big"> + <span id="vm-details-state" class="label label-success"> + <i class="fa {{ status_icon }}"></i> + <span>{{ vm_info.state|upper }}</span> + </span> + </div> + <br /> + <dl class="dl-horizontal vm-details-connection"> + <dt>{% trans "# of CPU cores" %}</dt> + <dd>{{ vm_info.cpu }}</dd> + <dt>{% trans "Memory" %}</dt> + <dd>{{ vm_info.memory }} MB</dd> + + <dt>{% trans "Time of expiration" %}</dt> + <dd>{{ instance.time_of_expiration }}</dd> + </dl> + </div> + </div> +</div> + +{% endblock %} \ No newline at end of file diff --git a/circle/dashboard/templates/dashboard/vmwarevminstance-add.html b/circle/dashboard/templates/dashboard/vmwarevminstance-add.html new file mode 100644 index 0000000..8321855 --- /dev/null +++ b/circle/dashboard/templates/dashboard/vmwarevminstance-add.html @@ -0,0 +1,12 @@ +{% load crispy_forms_tags %} +{% load i18n %} + +<p class="text-muted"> +{% trans "Add a virtual machine to the managed list, choose an owner and expiration date." %} +</p> + + +<form method="POST" action="{% url "dashboard.views.vmwarevminstance-add" uuid=instance_uuid cluster=cluster_pk %}"> +{% csrf_token %} +{% crispy form %} +</form> diff --git a/circle/dashboard/templates/dashboard/vmwarevminstance-list/column-add.html b/circle/dashboard/templates/dashboard/vmwarevminstance-list/column-add.html new file mode 100644 index 0000000..b0c9d04 --- /dev/null +++ b/circle/dashboard/templates/dashboard/vmwarevminstance-list/column-add.html @@ -0,0 +1,5 @@ +<a data-group-uuid="{{ record.uuid }}" + class="btn btn-success btn-xs real-link vmwarevminstance-add" + href="{% url "dashboard.views.vmwarevminstance-add" cluster=record.cluster_pk uuid=record.uuid %}?next={{ request.path }}"> + <i class="fa fa-plus"></i> add +</a> \ No newline at end of file diff --git a/circle/dashboard/templates/dashboard/vmwarevminstance-list/column-modify.html b/circle/dashboard/templates/dashboard/vmwarevminstance-list/column-modify.html new file mode 100644 index 0000000..05dc74e --- /dev/null +++ b/circle/dashboard/templates/dashboard/vmwarevminstance-list/column-modify.html @@ -0,0 +1,5 @@ +<a data-group-uuid="{{ record.uuid }}" + class="btn btn-info btn-xs real-link vmwarevminstance-modify" + href="{% url "dashboard.views.vmwarevminstance-add" cluster=record.cluster_pk uuid=record.uuid %}?next={{ request.path }}"> + <i class="fa fa-pencil"></i> modify +</a> \ No newline at end of file diff --git a/circle/dashboard/templates/dashboard/vmwarevminstance-list/column-remove.html b/circle/dashboard/templates/dashboard/vmwarevminstance-list/column-remove.html new file mode 100644 index 0000000..97907dc --- /dev/null +++ b/circle/dashboard/templates/dashboard/vmwarevminstance-list/column-remove.html @@ -0,0 +1,5 @@ +<a data-group-uuid="{{ record.uuid }}" + class="btn btn-danger btn-xs real-link vmwarevminstance-remove" + href="{% url "dashboard.views.vmwarevminstance-remove" cluster=record.cluster_pk uuid=record.uuid %}?next={{ request.path }}"> + <i class="fa fa-minus"></i> remove +</a> \ No newline at end of file diff --git a/circle/dashboard/urls.py b/circle/dashboard/urls.py index 7df4bb5..a8c6136 100644 --- a/circle/dashboard/urls.py +++ b/circle/dashboard/urls.py @@ -19,6 +19,9 @@ from __future__ import absolute_import from django.conf.urls import patterns, url, include import autocomplete_light +from dashboard.views.cluster import ClusterList, ClusterCreate, ClusterDetailView, ClusterDelete, ClusterEdit +from dashboard.views.vmwarevminstance import VMwareVMInstanceDelete, VMwareVMInstanceDetail, \ + VMwareVMInstanceCreate, VMwareVMInstanceAdd from vm.models import Instance from .views import ( AclUpdateView, FavouriteView, GroupAclUpdateView, GroupDelete, @@ -246,6 +249,27 @@ urlpatterns = patterns( name="dashboard.views.message-create"), url(r'^message/delete/(?P<pk>\d+)/$', MessageDelete.as_view(), name="dashboard.views.message-delete"), + + url(r'^cluster/list/$', ClusterList.as_view(), name='dashboard.views.cluster-list'), + url(r'^cluster/create/$', ClusterCreate.as_view(), + name='dashboard.views.cluster-create'), + url(r'^cluster/(?P<pk>\d+)/$', ClusterDetailView.as_view(), + name='dashboard.views.cluster-detail'), + url(r'^cluster/(?P<pk>\d+)/edit/$', ClusterEdit.as_view(), + name='dashboard.views.cluster-edit'), + url(r'^cluster/delete/(?P<pk>\d+)/$', ClusterDelete.as_view(), + name="dashboard.views.cluster-delete"), + + url(r'^vmwarevminstance/add/cluster/(?P<cluster>\d+)/uuid/(?P<uuid>[A-Za-z0-9\-]+)/$', VMwareVMInstanceAdd.as_view(), + name='dashboard.views.vmwarevminstance-add'), + url(r'^vmware-vm-instance/create/cluster/(?P<cluster>\d+)/$', VMwareVMInstanceCreate.as_view(), + name='dashboard.views.vmwarevminstance-create'), + url(r'^vmware-vm-instance/(?P<pk>\d+)/$', VMwareVMInstanceDetail.as_view(), + name='dashboard.views.vmwarevminstance-detail'), + url(r'^vmware-vm-instance/remove/cluster/(?P<cluster>\d+)/uuid/(?P<uuid>[A-Za-z0-9\-]+)/$', VMwareVMInstanceDelete.as_view(), + name='dashboard.views.vmwarevminstance-remove'), + url(r'^vmware-vm-instance/remove/cluster/(?P<cluster>\d+)/uuid/(?P<uuid>[A-Za-z0-9\-]+)/$', VMwareVMInstanceDelete.as_view(), + name='dashboard.views.vmwarevminstance-remove'), ) urlpatterns += patterns( diff --git a/circle/dashboard/views/cluster.py b/circle/dashboard/views/cluster.py new file mode 100644 index 0000000..6593dc0 --- /dev/null +++ b/circle/dashboard/views/cluster.py @@ -0,0 +1,228 @@ +from __future__ import unicode_literals, absolute_import + +import json + +from django.contrib import messages +from django.core.exceptions import PermissionDenied +from django.core.urlresolvers import reverse_lazy +from django.http import HttpResponse +from django.shortcuts import redirect +from django.core.urlresolvers import reverse +from django.utils.translation import ugettext as _ +from django.views.generic import DetailView, TemplateView, UpdateView +from braces.views import LoginRequiredMixin, SuperuserRequiredMixin +from django_tables2 import SingleTableView + +from vm.models import Node, Trait +from ..forms import TraitForm, ClusterCreateForm +from ..tables import ClusterListTable +from .util import GraphMixin, DeleteViewBase +from vm.models.cluster import Cluster + +import django_tables2 as tables +from django_tables2.columns import TemplateColumn + + +class UnmanagedVmsTable(tables.Table): + name = tables.Column(orderable=False, verbose_name="Name") + state = tables.Column(orderable=False, verbose_name="Current power state") + os = tables.Column(orderable=False, verbose_name='Operating system') + memory = TemplateColumn("{{ value }} MB", orderable=False, verbose_name='Memory') + cpu = tables.Column(orderable=False, verbose_name='CPU cores') + add_btn = TemplateColumn(orderable=False, template_name="dashboard/vmwarevminstance-list/column-add.html", + verbose_name="Actions") + + class Meta: + attrs = {'class': 'table table-bordered table-striped table-hover'} + + +class ManagedVmsTable(tables.Table): + name = tables.Column(orderable=False, verbose_name="Name") + time_of_expiration = tables.Column(orderable=False, verbose_name="Expiration time") + state = tables.Column(orderable=False, verbose_name="Current power state") + os = tables.Column(orderable=False, verbose_name='Operating system') + owner = tables.Column(orderable=False, verbose_name='Owner') + memory = TemplateColumn("{{ value }} MB", orderable=False, verbose_name='Memory') + cpu = tables.Column(orderable=False, verbose_name='# of CPU cores') + add_btn = TemplateColumn(orderable=False, template_name="dashboard/vmwarevminstance-list/column-modify.html", + verbose_name="Actions") + + class Meta: + attrs = {'class': 'table table-bordered table-striped table-hover'} + + +class DeletedVmsTable(tables.Table): + name = tables.Column(orderable=False, verbose_name="Name") + os = tables.Column(orderable=False, verbose_name='Operating system') + memory = TemplateColumn("{{ value }} MB", orderable=False, verbose_name='Memory') + cpu = tables.Column(orderable=False, verbose_name='# of CPU cores') + owner = tables.Column(orderable=False, verbose_name='Owner') + remove_btn = TemplateColumn(orderable=False, template_name="dashboard/vmwarevminstance-list/column-remove.html", + verbose_name="Actions") + + class Meta: + attrs = {'class': 'table table-bordered table-striped table-hover'} + + +class ClusterDetailView(LoginRequiredMixin, DetailView): + template_name = "dashboard/cluster-detail.html" + model = Cluster + + def get(self, *args, **kwargs): + if not self.request.user.has_perm('vm.view_statistics'): + raise PermissionDenied() + return super(ClusterDetailView, self).get(*args, **kwargs) + + def get_context_data(self, **kwargs): + context = super(ClusterDetailView, self).get_context_data(**kwargs) + unmanaged_vms, managed_vms, deleted_vms, error_msg = self.object.get_list_of_vms() + + if error_msg is not None: + messages.error(self.request, error_msg) + else: + unmanaged_vms_table = UnmanagedVmsTable(unmanaged_vms) + managed_vms_table = ManagedVmsTable(managed_vms) + deleted_vms_table = DeletedVmsTable(deleted_vms) + + context.update({ + 'unmanaged_vms_table': unmanaged_vms_table, + 'managed_vms_table': managed_vms_table, + 'deleted_vms_table': deleted_vms_table, + }) + + return context + + def post(self, request, *args, **kwargs): + if not request.user.is_superuser: + raise PermissionDenied() + if request.POST.get('new_name'): + return self.__set_name(request) + if request.POST.get('to_remove'): + return self.__remove_trait(request) + return redirect(reverse_lazy("dashboard.views.cluster-detail", + kwargs={'pk': self.get_object().pk})) + + def __set_name(self, request): + self.object = self.get_object() + new_name = request.POST.get("new_name") + Cluster.objects.filter(pk=self.object.pk).update( + **{'name': new_name}) + + success_message = _("Cluster successfully renamed.") + if request.is_ajax(): + response = { + 'message': success_message, + 'new_name': new_name, + 'node_pk': self.object.pk + } + return HttpResponse( + json.dumps(response), + content_type="application/json" + ) + else: + messages.success(request, success_message) + return redirect(reverse_lazy("dashboard.views.cluster-detail", + kwargs={'pk': self.object.pk})) + + +class ClusterList(LoginRequiredMixin, GraphMixin, SingleTableView): + template_name = "dashboard/cluster-list.html" + table_class = ClusterListTable + table_pagination = False + + def get(self, *args, **kwargs): + if not self.request.user.has_perm('vm.view_statistics'): + raise PermissionDenied() + if self.request.is_ajax(): + nodes = Node.objects.all() + nodes = [{ + 'name': i.name, + 'icon': i.get_status_icon(), + 'url': i.get_absolute_url(), + 'label': i.get_status_label(), + 'status': i.state.lower()} for i in nodes] + + return HttpResponse( + json.dumps(list(nodes)), + content_type="application/json", + ) + else: + return super(ClusterList, self).get(*args, **kwargs) + + def get_queryset(self): + return Cluster.objects.all() + + +class ClusterCreate(LoginRequiredMixin, TemplateView): + + model = Cluster + form_class = ClusterCreateForm + template_name = 'dashboard/cluster-create.html' + + def get_template_names(self): + if self.request.is_ajax(): + return ['dashboard/_modal.html'] + else: + return ['dashboard/nojs-wrapper.html'] + + def get(self, request, form=None, *args, **kwargs): + if not request.user.has_module_perms('auth'): + raise PermissionDenied() + if form is None: + form = self.form_class() + context = self.get_context_data(**kwargs) + context.update({ + 'template': 'dashboard/cluster-create.html', + 'box_title': _('Add a Cluster'), + 'form': form, + 'ajax_title': True, + }) + return self.render_to_response(context) + + def post(self, request, *args, **kwargs): + if not request.user.has_module_perms('auth'): + raise PermissionDenied() + form = self.form_class(request.POST) + if not form.is_valid(): + return self.get(request, form, *args, **kwargs) + form.cleaned_data + savedform = form.save() + messages.success(request, _('Cluster successfully created.')) + if request.is_ajax(): + return HttpResponse(json.dumps({'redirect': + reverse("dashboard.index")}), + content_type="application/json") + else: + return redirect(reverse("dashboard.index")) + + +class ClusterEdit(SuperuserRequiredMixin, UpdateView): + model = Cluster + success_message = _("Cluster successfully updated.") + template_name = 'dashboard/cluster-edit.html' + form_class = ClusterCreateForm + + def check_auth(self): + # SuperuserRequiredMixin + pass + + def get_success_url(self): + return reverse_lazy('dashboard.index') + + def get_context_data(self, **kwargs): + context = super(ClusterEdit, self).get_context_data(**kwargs) + context['cluster'] = self.object + + return context + + +class ClusterDelete(SuperuserRequiredMixin, DeleteViewBase): + model = Cluster + success_message = _("Cluster successfully deleted.") + + def check_auth(self): + # SuperuserRequiredMixin + pass + + def get_success_url(self): + return reverse_lazy('dashboard.index') diff --git a/circle/dashboard/views/index.py b/circle/dashboard/views/index.py index b92fad3..ae55d06 100644 --- a/circle/dashboard/views/index.py +++ b/circle/dashboard/views/index.py @@ -27,7 +27,7 @@ from django.views.generic import TemplateView from braces.views import LoginRequiredMixin from dashboard.models import GroupProfile -from vm.models import Instance, Node, InstanceTemplate +from vm.models import Instance, Node, InstanceTemplate, Cluster, VMwareVMInstance from dashboard.views.vm import vm_ops from ..store_api import Store @@ -78,6 +78,16 @@ class IndexView(LoginRequiredMixin, TemplateView): } }) + # VMWare clusters + context.update({ + 'clusters': Cluster.objects.all() + }) + + # VMWare instances + context.update({ + 'vmware_instances': VMwareVMInstance.objects.filter(owner=user) + }) + # groups if user.has_module_perms('auth'): profiles = GroupProfile.get_objects_with_level('operator', user) diff --git a/circle/dashboard/views/vmwarevminstance.py b/circle/dashboard/views/vmwarevminstance.py new file mode 100644 index 0000000..ce4b85a --- /dev/null +++ b/circle/dashboard/views/vmwarevminstance.py @@ -0,0 +1,168 @@ +from __future__ import unicode_literals, absolute_import + +import json + +from braces.views import LoginRequiredMixin, SuperuserRequiredMixin +from django.contrib import messages +from django.core.exceptions import PermissionDenied +from django.core.urlresolvers import reverse, reverse_lazy +from django.http import HttpResponse +from django.shortcuts import redirect +from django.utils.translation import ugettext as _ +from django.views.generic import TemplateView, DetailView +from dashboard.views import DeleteViewBase +from vm.models import VMwareVMInstance +from vm.models.cluster import Cluster +from ..forms import VMwareVMInstanceForm, VMwareVMInstanceCreateForm + + +class VMwareVMInstanceCreate(LoginRequiredMixin, TemplateView): + + model = VMwareVMInstance + form_class = VMwareVMInstanceCreateForm + template_name = 'dashboard/create-vmware-vm.html' + + def get_template_names(self): + if self.request.is_ajax(): + return ['dashboard/_modal.html'] + else: + return ['dashboard/nojs-wrapper.html'] + + def get(self, request, form=None, *args, **kwargs): + cluster = None + + if 'cluster' in kwargs: + cluster = kwargs.pop("cluster") + + if not request.user.has_module_perms('auth'): + raise PermissionDenied() + if form is None: + form = self.form_class() + context = self.get_context_data(**kwargs) + context.update({ + 'template': 'dashboard/create-vmware-vm.html', + 'box_title': _('Create a VMware virtual machine'), + 'form': form, + 'ajax_title': True, + 'cluster_pk': cluster, + }) + return self.render_to_response(context) + + def post(self, request, *args, **kwargs): + if not request.user.has_module_perms('auth'): + raise PermissionDenied() + + cluster = None + + if 'cluster' in kwargs: + cluster = kwargs.pop("cluster") + + form = self.form_class(request.POST, cluster_pk=int(cluster)) + if not form.is_valid(): + return self.get(request, form, *args, **kwargs) + + savedform = form.save() + messages.success(request, _('Virtual machine successfully created.')) + if request.is_ajax(): + return HttpResponse(json.dumps({'redirect': + reverse("dashboard.index")}), + content_type="application/json") + else: + return redirect(reverse("dashboard.index")) + + +class VMwareVMInstanceAdd(LoginRequiredMixin, TemplateView): + + model = VMwareVMInstance + form_class = VMwareVMInstanceForm + template_name = 'dashboard/vmwarevminstance-add.html' + + def get_template_names(self): + if self.request.is_ajax(): + return ['dashboard/_modal.html'] + else: + return ['dashboard/nojs-wrapper.html'] + + def get(self, request, form=None, *args, **kwargs): + if not request.user.has_module_perms('auth'): + raise PermissionDenied() + + uuid = None + cluster = None + + if 'uuid' in kwargs: + uuid = kwargs.pop("uuid") + + if 'cluster' in kwargs: + cluster = kwargs.pop("cluster") + + cluster_instance = Cluster.objects.get(pk=cluster) + vm_info = cluster_instance.get_vm_details_by_uuid(uuid) + + if form is None: + form = self.form_class(uuid=uuid) + context = self.get_context_data(**kwargs) + context.update({ + 'template': 'dashboard/vmwarevminstance-add.html', + 'box_title': _('Add a virtual machine: '+vm_info["name"]), + 'form': form, + 'ajax_title': True, + 'instance_uuid': uuid, + 'cluster_pk': cluster, + }) + return self.render_to_response(context) + + def post(self, request, *args, **kwargs): + if not request.user.has_module_perms('auth'): + raise PermissionDenied() + + if 'cluster' in kwargs: + cluster = kwargs.pop("cluster") + + if 'uuid' in kwargs: + uuid = kwargs.pop("uuid") + + form = self.form_class(request.POST, cluster_pk=int(cluster), uuid=uuid) + if not form.is_valid(): + return self.get(request, form, *args, **kwargs) + form.cleaned_data + savedform = form.save() + messages.success(request, _('Virtual machine successfully added.')) + if request.is_ajax(): + return HttpResponse(json.dumps({'redirect': + reverse("dashboard.index")}), + content_type="application/json") + else: + return redirect(reverse("dashboard.index")) + + +class VMwareVMInstanceDelete(SuperuserRequiredMixin, DeleteViewBase): + model = VMwareVMInstance + success_message = _("Instance has been successfully deleted.") + + def check_auth(self): + # SuperuserRequiredMixin + pass + + def get_success_url(self): + return reverse_lazy('dashboard.index') + + +class VMwareVMInstanceDetail(LoginRequiredMixin, DetailView): + template_name = "dashboard/vmware-vm-instance-detail.html" + model = VMwareVMInstance + + def get(self, *args, **kwargs): + if not self.request.user.has_perm('vm.view_statistics'): + raise PermissionDenied() + return super(VMwareVMInstanceDetail, self).get(*args, **kwargs) + + def get_context_data(self, **kwargs): + context = super(VMwareVMInstanceDetail, self).get_context_data(**kwargs) + context['instance'] = self.object + vm_info = self.object.get_vm_info() + + context['vm_info'] = vm_info + context['status_icon'] = self.object.get_status_icon(vm_info['state']) + + return context \ No newline at end of file diff --git a/circle/vm/admin.py b/circle/vm/admin.py index 28198c4..6337a03 100644 --- a/circle/vm/admin.py +++ b/circle/vm/admin.py @@ -19,13 +19,15 @@ from django.contrib import admin from .models import (Instance, InstanceActivity, InstanceTemplate, Interface, InterfaceTemplate, Lease, NamedBaseResourceConfig, Node, - NodeActivity, Trait) + NodeActivity, Trait, Cluster, VMwareVMInstance) class InstanceActivityAdmin(admin.ModelAdmin): exclude = ('parent', ) +admin.site.register(Cluster) +admin.site.register(VMwareVMInstance) admin.site.register(Instance) admin.site.register(InstanceActivity, InstanceActivityAdmin) admin.site.register(InstanceTemplate) diff --git a/circle/vm/migrations/0003_cluster_vmwarevminstance.py b/circle/vm/migrations/0003_cluster_vmwarevminstance.py new file mode 100644 index 0000000..a3f45a0 --- /dev/null +++ b/circle/vm/migrations/0003_cluster_vmwarevminstance.py @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations +import model_utils.fields +import common.operations +import django.utils.timezone +from django.conf import settings + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('vm', '0002_interface_model'), + ] + + operations = [ + migrations.CreateModel( + name='Cluster', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, verbose_name='created', editable=False)), + ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, verbose_name='modified', editable=False)), + ('name', models.CharField(help_text='Human readable name of cluster.', unique=True, max_length=50, verbose_name='name')), + ('address', models.CharField(help_text='The address of the vCenter.', max_length=200, verbose_name='address')), + ('username', models.CharField(default='', help_text='The username used for the connection.', max_length=200, verbose_name='username')), + ('password', models.CharField(default='', help_text='The password used for the connection.', max_length=200, verbose_name='password')), + ], + options={ + 'db_table': 'vm_cluster', + }, + bases=(common.operations.OperatedMixin, models.Model), + ), + migrations.CreateModel( + name='VMwareVMInstance', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, verbose_name='created', editable=False)), + ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, verbose_name='modified', editable=False)), + ('name', models.CharField(help_text='The name of the virtual machine.', unique=True, max_length=50, verbose_name='name')), + ('instanceUUID', models.CharField(help_text='A unique identifier of the VM.', unique=True, max_length=200, verbose_name='instanceUUID')), + ('time_of_expiration', models.DateTimeField(default=None, help_text='The time, when the virtual machine will expire.', null=True, verbose_name='time of expiration', blank=True)), + ('operating_system', models.CharField(help_text='The OS of the VM.', unique=True, max_length=200, verbose_name='operating system')), + ('cpu_cores', models.IntegerField(help_text='The number of CPU cores in the VM.')), + ('memory_size', models.IntegerField(help_text='The amount of memory (MB) in the VM.')), + ('cluster', models.ForeignKey(to='vm.Cluster')), + ('owner', models.ForeignKey(to=settings.AUTH_USER_MODEL)), + ], + options={ + 'db_table': 'vm_vmware_vminstance', + 'verbose_name': 'VMware virtual machine instance', + }, + bases=(common.operations.OperatedMixin, models.Model), + ), + ] diff --git a/circle/vm/models/__init__.py b/circle/vm/models/__init__.py index 14722fb..bb8adcf 100644 --- a/circle/vm/models/__init__.py +++ b/circle/vm/models/__init__.py @@ -15,11 +15,13 @@ from .instance import pwgen from .network import InterfaceTemplate from .network import Interface from .node import Node +from .cluster import Cluster +from .vmwarevminstance import VMwareVMInstance __all__ = [ 'InstanceActivity', 'BaseResourceConfigModel', 'NamedBaseResourceConfig', 'VirtualMachineDescModel', 'InstanceTemplate', 'Instance', 'post_state_changed', 'pre_state_changed', 'InterfaceTemplate', 'Interface', 'Trait', 'Node', 'NodeActivity', 'Lease', 'node_activity', - 'pwgen' + 'pwgen', 'Cluster', 'VMwareVMInstance', ] diff --git a/circle/vm/models/cluster.py b/circle/vm/models/cluster.py new file mode 100644 index 0000000..cfb22a9 --- /dev/null +++ b/circle/vm/models/cluster.py @@ -0,0 +1,308 @@ +# Copyright 2014 Budapest University of Technology and Economics (BME IK) +# +# This file is part of CIRCLE Cloud. +# +# CIRCLE is free software: you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) +# any later version. +# +# CIRCLE is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along +# with CIRCLE. If not, see <http://www.gnu.org/licenses/>. + +from __future__ import absolute_import, unicode_literals +from logging import getLogger +from django.db.models import ( + CharField, permalink +) +from django.utils.translation import ugettext_lazy as _ +from model_utils.models import TimeStampedModel +from requests import ConnectionError + +from common.operations import OperatedMixin +from pyVim.connect import SmartConnect, Disconnect +import pyVmomi +from pyVmomi import vim, vmodl + +logger = getLogger(__name__) + + +def collect_properties(service_instance, view_ref, obj_type, path_set=None, + include_mors=False): + """ + Collect properties for managed objects from a view ref + + Check the vSphere API documentation for example on retrieving + object properties: + + - http://goo.gl/erbFDz + + Args: + si (ServiceInstance): ServiceInstance connection + view_ref (pyVmomi.vim.view.*): Starting point of inventory navigation + obj_type (pyVmomi.vim.*): Type of managed object + path_set (list): List of properties to retrieve + include_mors (bool): If True include the managed objects + refs in the result + + Returns: + A list of properties for the managed objects + + """ + collector = service_instance.content.propertyCollector + + # Create object specification to define the starting point of + # inventory navigation + obj_spec = pyVmomi.vmodl.query.PropertyCollector.ObjectSpec() + obj_spec.obj = view_ref + obj_spec.skip = True + + # Create a traversal specification to identify the path for collection + traversal_spec = pyVmomi.vmodl.query.PropertyCollector.TraversalSpec() + traversal_spec.name = 'traverseEntities' + traversal_spec.path = 'view' + traversal_spec.skip = False + traversal_spec.type = view_ref.__class__ + obj_spec.selectSet = [traversal_spec] + + # Identify the properties to the retrieved + property_spec = pyVmomi.vmodl.query.PropertyCollector.PropertySpec() + property_spec.type = obj_type + + if not path_set: + property_spec.all = True + + property_spec.pathSet = path_set + + # Add the object and property specification to the + # property filter specification + filter_spec = pyVmomi.vmodl.query.PropertyCollector.FilterSpec() + filter_spec.objectSet = [obj_spec] + filter_spec.propSet = [property_spec] + + # Retrieve properties + props = collector.RetrieveContents([filter_spec]) + + data = [] + for obj in props: + properties = {} + for prop in obj.propSet: + properties[prop.name] = prop.val + + if include_mors: + properties['obj'] = obj.obj + + data.append(properties) + return data + + +def get_container_view(service_instance, obj_type, container=None): + """ + Get a vSphere Container View reference to all objects of type 'obj_type' + + It is up to the caller to take care of destroying the View when no longer + needed. + + Args: + obj_type (list): A list of managed object types + + Returns: + A container view ref to the discovered managed objects + + """ + if not container: + container = service_instance.content.rootFolder + + view_ref = service_instance.content.viewManager.CreateContainerView( + container=container, + type=obj_type, + recursive=True + ) + return view_ref + + +class Cluster(OperatedMixin, TimeStampedModel): + """A VMware cluster. + """ + name = CharField(max_length=50, unique=True, + verbose_name=_('name'), + help_text=_('Human readable name of cluster.')) + address = CharField(max_length=200, + verbose_name=_('address'), + help_text=_('The address of the vCenter.')) + + username = CharField(max_length=200, + verbose_name=_('username'), + help_text=_('The username used for the connection.'), + default='') + + password = CharField(max_length=200, + verbose_name=_('password'), + help_text=_('The password used for the connection.'), + default='') + + class Meta: + app_label = 'vm' + db_table = 'vm_cluster' + + @permalink + def get_absolute_url(self): + return 'dashboard.views.cluster-detail', None, {'pk': self.id} + + def get_list_of_vms(self): + + # return [ + # { + # 'uuid': "5020222b-4d02-fe42-c630-af8325b384f9", + # 'name': "unmanaged_vm", + # 'cpu': 2, + # 'memory': 2048, + # 'state': "poweredOn", + # 'os': "linux", + # 'cluster_pk': 1, + # }, + # ], [ + # { + # 'uuid': "5020222b-4d02-ce42-c630-af8325b384f9", + # 'name': "managed_vm", + # 'cpu': 2, + # 'memory': 2048, + # 'state': "poweredOn", + # 'os': "linux", + # 'time_of_expiration': "12/31/2015", + # 'cluster_pk': 1, + # }, + # ], [ + # { + # 'uuid': "5020222b-4d02-de42-c630-af8325b384f9", + # 'name': "deleted_vm", + # 'cluster_pk': 1, + # }, + # ], None + + try: + unmanaged_vm_list = [] + managed_vm_list = [] + deleted_vm_list = [] + + si = SmartConnect(host=self.address, + user=self.username, + pwd=self.password, + port=443) + + # the info to acquire from each vm + vm_properties = ["name", "config.instanceUuid", "config.hardware.numCPU", + "config.hardware.memoryMB", "summary.runtime.powerState", + "config.guestFullName"] + + view = get_container_view(si, obj_type=[vim.VirtualMachine]) + vm_data_from_vcenter = collect_properties(si, view_ref=view, + obj_type=vim.VirtualMachine, + path_set=vm_properties, + include_mors=True) + + from vm.models import VMwareVMInstance + + list_of_vcenter_vm_uuids = [] + + for curr_vcenter_vm in vm_data_from_vcenter: + list_of_vcenter_vm_uuids.append(curr_vcenter_vm["config.instanceUuid"]) + + state = { + "poweredOn": "powered on", + "poweredOff": "powered off", + "suspended": "suspended" + }.get(curr_vcenter_vm["summary.runtime.powerState"], "unknown") + + vm_info = { + 'uuid': curr_vcenter_vm["config.instanceUuid"], + 'name': curr_vcenter_vm["name"], + 'cpu': curr_vcenter_vm["config.hardware.numCPU"], + 'memory': curr_vcenter_vm["config.hardware.memoryMB"], + 'state': state, + 'os': curr_vcenter_vm["config.guestFullName"], + 'cluster_pk': self.pk, + } + + if VMwareVMInstance.objects.filter(instanceUUID=curr_vcenter_vm["config.instanceUuid"]).count() == 0: + # vm is not managed + + unmanaged_vm_list.extend([vm_info]) + else: + # this vm is managed + # we may need to update our info in the database + + curr_vm = VMwareVMInstance.objects.get(instanceUUID=curr_vcenter_vm["config.instanceUuid"]) + curr_vm.name = vm_info["name"] + curr_vm.cpu_cores = vm_info["cpu"] + curr_vm.memory = vm_info["memory"] + curr_vm.operating_system = vm_info["os"] + curr_vm.save() + + vm_info["owner"] = curr_vm.owner.username + + managed_vm_list.extend([vm_info]) + + Disconnect(si) + + for curr_managed_vm in VMwareVMInstance.objects.all(): + if curr_managed_vm.instanceUUID not in list_of_vcenter_vm_uuids: + vm_info = { + 'uuid': curr_managed_vm.instanceUUID, + 'name': curr_managed_vm.name, + 'cpu': curr_managed_vm.cpu_cores, + 'memory': curr_managed_vm.memory_size, + 'os': curr_managed_vm.operating_system, + 'cluster_pk': self.pk, + 'owner': curr_managed_vm.owner.username, + } + + deleted_vm_list.extend([vm_info]) + + return unmanaged_vm_list, managed_vm_list, deleted_vm_list, None + except ConnectionError: + return None, None, None, "Connection to the cluster failed. Please check the connection settings." + except vim.fault.InvalidLogin as e: + return None, None, None, e.msg + + def get_vm_details_by_uuid(self, uuid): + try: + si = SmartConnect(host=self.address, + user=self.username, + pwd=self.password, + port=443) + + search_index = si.content.searchIndex + vm = search_index.FindByUuid(None, uuid, True, True) + + state = { + "poweredOn": "powered on", + "poweredOff": "powered off", + "suspended": "suspended" + }.get(vm.summary.runtime.powerState, "unknown") + + vm_info = { + 'uuid': vm.summary.config.instanceUuid, + 'name': vm.summary.config.name, + 'cpu': vm.summary.config.numCpu, + 'memory': vm.summary.config.memorySizeMB, + 'state': state, + 'os': vm.summary.config.guestFullName, + } + + Disconnect(si) + + return vm_info + + except ConnectionError: + return None, "Connection to the cluster failed. Please check the connection settings." + except vim.fault.InvalidLogin as e: + return None, e.msg + + def __unicode__(self): + return self.name diff --git a/circle/vm/models/vmwarevminstance.py b/circle/vm/models/vmwarevminstance.py new file mode 100644 index 0000000..622de41 --- /dev/null +++ b/circle/vm/models/vmwarevminstance.py @@ -0,0 +1,159 @@ +# Copyright 2014 Budapest University of Technology and Economics (BME IK) +# +# This file is part of CIRCLE Cloud. +# +# CIRCLE is free software: you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) +# any later version. +# +# CIRCLE is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along +# with CIRCLE. If not, see <http://www.gnu.org/licenses/>. + +from __future__ import absolute_import, unicode_literals + +from logging import getLogger + +from django.contrib.auth.models import User +from django.db.models import ( + CharField, ForeignKey, permalink, DateTimeField, IntegerField) +from django.utils.translation import ugettext_lazy as _ +from model_utils.models import TimeStampedModel +from pyVim.connect import SmartConnect, Disconnect +from pyVmomi import vim +from requests import ConnectionError + +from common.operations import OperatedMixin +from vm.models import Cluster + +logger = getLogger(__name__) + + +class VMwareVMInstance(OperatedMixin, TimeStampedModel): + """A VMware virtual machine instance. + """ + name = CharField(max_length=50, unique=True, + verbose_name=_('name'), + help_text=_('The name of the virtual machine.')) + instanceUUID = CharField(max_length=200, + verbose_name=_('instanceUUID'), + help_text=_('A unique identifier of the VM.'), + unique=True) + + time_of_expiration = DateTimeField(blank=True, default=None, null=True, + verbose_name=_('time of expiration'), + help_text=_("The time, when the virtual machine" + " will expire.")) + cluster = ForeignKey(Cluster, null=False) + + owner = ForeignKey(User, null=False) + + operating_system = CharField(max_length=200, + verbose_name=_('operating system'), + help_text=_('The OS of the VM.'), + unique=True) + + cpu_cores = IntegerField(help_text=_('The number of CPU cores in the VM.')) + + memory_size = IntegerField(help_text=_('The amount of memory (MB) in the VM.')) + + class Meta: + app_label = 'vm' + db_table = 'vm_vmware_vminstance' + verbose_name = 'VMware virtual machine instance' + + @permalink + def get_absolute_url(self): + return 'dashboard.views.vmwarevminstance-detail', None, {'pk': self.id} + + def __unicode__(self): + return self.name + + def shutdown_vm(self): + try: + si = SmartConnect(host=self.cluster.address, + user=self.cluster.username, + pwd=self.cluster.password, + port=443) + + search_index = si.content.searchIndex + vm = search_index.FindByUuid(None, self.instanceUUID, True, True) + + vm.ShutdownGuest() + Disconnect(si) + + except ConnectionError: + pass + except vim.fault.InvalidLogin as e: + pass + except vim.fault.ToolsUnavailable: + pass + + def start_vm(self): + try: + si = SmartConnect(host=self.cluster.address, + user=self.cluster.username, + pwd=self.cluster.password, + port=443) + + search_index = si.content.searchIndex + vm = search_index.FindByUuid(None, self.instanceUUID, True, True) + + vm.PowerOnVM_Task() + Disconnect(si) + + except ConnectionError: + pass + except vim.fault.InvalidLogin as e: + pass + + def restart_vm(self): + try: + si = SmartConnect(host=self.cluster.address, + user=self.cluster.username, + pwd=self.cluster.password, + port=443) + + search_index = si.content.searchIndex + vm = search_index.FindByUuid(None, self.instanceUUID, True, True) + + vm.RebootGuest() + Disconnect(si) + + except ConnectionError: + pass + except vim.fault.InvalidLogin as e: + pass + + def suspend_vm(self): + try: + si = SmartConnect(host=self.cluster.address, + user=self.cluster.username, + pwd=self.cluster.password, + port=443) + + search_index = si.content.searchIndex + vm = search_index.FindByUuid(None, self.instanceUUID, True, True) + + vm.StandbyGuest() + Disconnect(si) + + except ConnectionError: + pass + except vim.fault.InvalidLogin as e: + pass + + def get_vm_info(self): + return self.cluster.get_vm_details_by_uuid(self.instanceUUID) + + def get_status_icon(self, state): + return { + 'powered on': 'fa-play', + 'powered off': 'fa-stop', + 'suspended': 'fa-pause', + }.get(state, 'fa-question') \ No newline at end of file diff --git a/circle/vm/tasks/local_periodic_tasks.py b/circle/vm/tasks/local_periodic_tasks.py index 884185e..fa7333f 100644 --- a/circle/vm/tasks/local_periodic_tasks.py +++ b/circle/vm/tasks/local_periodic_tasks.py @@ -16,11 +16,13 @@ # with CIRCLE. If not, see <http://www.gnu.org/licenses/>. import logging + +import datetime from django.utils import timezone from django.utils.translation import ugettext_noop from manager.mancelery import celery -from vm.models import Node, Instance +from vm.models import Node, Instance, Cluster logger = logging.getLogger(__name__) @@ -76,3 +78,14 @@ def garbage_collector(timeout=15): i.notify_owners_about_expiration() else: logger.debug("Instance %d didn't expire." % i.pk) + + for c in Cluster.objects.all(): + for i in c.vmwarevminstance_set.all(): + if datetime.now > i.time_of_expiration: + i.suspend_vm() + i.owner.profile.notify( + ugettext_noop('%(instance)s suspended'), + ugettext_noop( + 'Your instance <a href="%(url)s">%(instance)s</a> ' + 'has been suspended due to expiration.'), + instance=i.name, url=i.get_absolute_url()) \ No newline at end of file -- libgit2 0.26.0