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