Commit e11fffa5 by Bach Dániel

Merge remote-tracking branch 'origin/master' into feature-vm-tasks

Conflicts:
	circle/vm/models/instance.py
	circle/vm/tests/test_models.py
parents 19cdda94 9f91f3a7
...@@ -752,6 +752,7 @@ class VmStateChangeForm(forms.Form): ...@@ -752,6 +752,7 @@ class VmStateChangeForm(forms.Form):
"but don't interrupt any tasks.")) "but don't interrupt any tasks."))
new_state = forms.ChoiceField(Instance.STATUS, label=_( new_state = forms.ChoiceField(Instance.STATUS, label=_(
"New status")) "New status"))
reset_node = forms.BooleanField(required=False, label=_("Reset node"))
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
show_interrupt = kwargs.pop('show_interrupt') show_interrupt = kwargs.pop('show_interrupt')
...@@ -769,6 +770,17 @@ class VmStateChangeForm(forms.Form): ...@@ -769,6 +770,17 @@ class VmStateChangeForm(forms.Form):
return helper return helper
class RedeployForm(forms.Form):
with_emergency_change_state = forms.BooleanField(
required=False, initial=True, label=_("use emergency state change"))
@property
def helper(self):
helper = FormHelper(self)
helper.form_tag = False
return helper
class VmCreateDiskForm(forms.Form): class VmCreateDiskForm(forms.Form):
name = forms.CharField(max_length=100, label=_("Name")) name = forms.CharField(max_length=100, label=_("Name"))
size = forms.CharField( size = forms.CharField(
......
...@@ -974,6 +974,10 @@ textarea[name="new_members"] { ...@@ -974,6 +974,10 @@ textarea[name="new_members"] {
color: orange; color: orange;
} }
#vm-info-pane {
margin-bottom: 20px;
}
.node-list-table tbody>tr>td, .node-list-table thead>tr>th { .node-list-table tbody>tr>td, .node-list-table thead>tr>th {
vertical-align: middle; vertical-align: middle;
} }
...@@ -996,10 +1000,15 @@ textarea[name="new_members"] { ...@@ -996,10 +1000,15 @@ textarea[name="new_members"] {
max-width: 100%; max-width: 100%;
} }
#vm-list-table tbody td:nth-child(3) { #vm-list-table td.state,
#vm-list-table td.memory {
white-space: nowrap; white-space: nowrap;
} }
#vm-list-table td { #vm-list-table td {
vertical-align: middle; vertical-align: middle;
} }
.disk-resize-btn {
margin-right: 5px;
}
...@@ -618,7 +618,7 @@ function addModalConfirmation(func, data) { ...@@ -618,7 +618,7 @@ function addModalConfirmation(func, data) {
} }
function clientInstalledAction(location) { function clientInstalledAction(location) {
setCookie('downloaded_client', true, 365 * 24 * 60 * 60, "/"); setCookie('downloaded_client', true, 365 * 24 * 60 * 60 * 1000, "/");
window.location.href = location; window.location.href = location;
$('#confirmation-modal').modal("hide"); $('#confirmation-modal').modal("hide");
} }
......
...@@ -18,7 +18,8 @@ ...@@ -18,7 +18,8 @@
</a> </a>
{% if op.resize_disk %} {% if op.resize_disk %}
<span class="operation-wrapper"> <span class="operation-wrapper">
<a href="{{ op.resize_disk.get_url }}?disk={{d.pk}}" class="btn btn-xs btn-warning pull-right operation"> <a href="{{ op.resize_disk.get_url }}?disk={{d.pk}}"
class="btn btn-xs btn-warning pull-right operation disk-resize-btn">
<i class="fa fa-arrows-alt"></i> {% trans "Resize" %} <i class="fa fa-arrows-alt"></i> {% trans "Resize" %}
</a> </a>
</span> </span>
......
...@@ -13,7 +13,7 @@ Choose a compute node to migrate {{obj}} to. ...@@ -13,7 +13,7 @@ Choose a compute node to migrate {{obj}} to.
{% block formfields %} {% block formfields %}
<ul id="vm-migrate-node-list" class="list-unstyled"> <ul id="vm-migrate-node-list" class="list-unstyled">
{% with current=object.node.pk selected=object.select_node.pk %} {% with current=object.node.pk %}
{% for n in nodes %} {% for n in nodes %}
<li class="panel panel-default"><div class="panel-body"> <li class="panel panel-default"><div class="panel-body">
<label for="migrate-to-{{n.pk}}"> <label for="migrate-to-{{n.pk}}">
...@@ -21,11 +21,11 @@ Choose a compute node to migrate {{obj}} to. ...@@ -21,11 +21,11 @@ Choose a compute node to migrate {{obj}} to.
<div class="label label-primary"><i class="fa {{n.get_status_icon}}"></i> <div class="label label-primary"><i class="fa {{n.get_status_icon}}"></i>
{{n.get_status_display}}</div> {{n.get_status_display}}</div>
{% if current == n.pk %}<div class="label label-info">{% trans "current" %}</div>{% endif %} {% if current == n.pk %}<div class="label label-info">{% trans "current" %}</div>{% endif %}
{% if selected == n.pk %}<div class="label label-success">{% trans "recommended" %}</div>{% endif %} {% if recommended == n.pk %}<div class="label label-success">{% trans "recommended" %}</div>{% endif %}
</label> </label>
<input id="migrate-to-{{n.pk}}" type="radio" name="node" value="{{ n.pk }}" style="float: right;" <input id="migrate-to-{{n.pk}}" type="radio" name="node" value="{{ n.pk }}" style="float: right;"
{% if current == n.pk %}disabled="disabled"{% endif %} {% if current == n.pk %}disabled="disabled"{% endif %}
{% if selected == n.pk %}checked="checked"{% endif %} /> {% if recommended == n.pk %}checked="checked"{% endif %} />
<span class="vm-migrate-node-property">{% trans "CPU load" %}: {{ n.cpu_usage }}</span> <span class="vm-migrate-node-property">{% trans "CPU load" %}: {{ n.cpu_usage }}</span>
<span class="vm-migrate-node-property">{% trans "RAM usage" %}: {{ n.byte_ram_usage|filesize }}/{{ n.ram_size|filesize }}</span> <span class="vm-migrate-node-property">{% trans "RAM usage" %}: {{ n.byte_ram_usage|filesize }}/{{ n.ram_size|filesize }}</span>
<div style="clear: both;"></div> <div style="clear: both;"></div>
......
...@@ -47,7 +47,10 @@ ...@@ -47,7 +47,10 @@
<div class="input-group vm-details-home-name"> <div class="input-group vm-details-home-name">
<input id="vm-details-rename-name" class="form-control input-sm" name="new_name" type="text" value="{{ instance.name }}"/> <input id="vm-details-rename-name" class="form-control input-sm" name="new_name" type="text" value="{{ instance.name }}"/>
<span class="input-group-btn"> <span class="input-group-btn">
<button type="submit" class="btn btn-sm vm-details-rename-submit">{% trans "Rename" %}</button> <button type="submit" class="btn btn-sm vm-details-rename-submit
{% if not is_operator %}disabled{% endif %}">
{% trans "Rename" %}
</button>
</span> </span>
</div> </div>
</form> </form>
......
...@@ -11,7 +11,8 @@ ...@@ -11,7 +11,8 @@
<span class="input-group-addon">/</span> <span class="input-group-addon">/</span>
<select class="form-control" name="proto" style="width: 70px;"><option>tcp</option><option>udp</option></select> <select class="form-control" name="proto" style="width: 70px;"><option>tcp</option><option>udp</option></select>
<div class="input-group-btn"> <div class="input-group-btn">
<button type="submit" class="btn btn-success btn-sm">{% trans "Add" %}</button> <button type="submit" class="btn btn-success btn-sm
{% if not is_operator %}disabled{% endif %}">{% trans "Add" %}</button>
</div> </div>
</div> </div>
</form> </form>
......
...@@ -6,7 +6,9 @@ ...@@ -6,7 +6,9 @@
<dd><i class="fa fa-{{ os_type_icon }}"></i> {{ instance.system }}</dd> <dd><i class="fa fa-{{ os_type_icon }}"></i> {{ instance.system }}</dd>
<dt style="margin-top: 5px;"> <dt style="margin-top: 5px;">
{% trans "Name" %}: {% trans "Name" %}:
<a href="#" class="vm-details-home-edit-name-click"><i class="fa fa-pencil"></i></a> {% if is_operator %}
<a href="#" class="vm-details-home-edit-name-click"><i class="fa fa-pencil"></i></a>
{% endif %}
</dt> </dt>
<dd> <dd>
<div class="vm-details-home-edit-name-click"> <div class="vm-details-home-edit-name-click">
...@@ -18,8 +20,9 @@ ...@@ -18,8 +20,9 @@
<div class="input-group"> <div class="input-group">
<input type="text" name="new_name" value="{{ instance.name }}" class="form-control input-sm"/> <input type="text" name="new_name" value="{{ instance.name }}" class="form-control input-sm"/>
<span class="input-group-btn"> <span class="input-group-btn">
<button type="submit" class="btn btn-success btn-sm vm-details-rename-submit"> <button type="submit" class="btn btn-success btn-sm vm-details-rename-submit
<i class="fa fa-pencil"></i> {% trans "Rename" %} {% if not is_operator %}disabled{% endif %}" title="{% trans "Rename" %}">
<i class="fa fa-pencil"></i>
</button> </button>
</span> </span>
</div> </div>
...@@ -28,7 +31,9 @@ ...@@ -28,7 +31,9 @@
</dd> </dd>
<dt style="margin-top: 5px;"> <dt style="margin-top: 5px;">
{% trans "Description" %}: {% trans "Description" %}:
<a href="#" class="vm-details-home-edit-description-click"><i class="fa fa-pencil"></i></a> {% if is_operator %}
<a href="#" class="vm-details-home-edit-description-click"><i class="fa fa-pencil"></i></a>
{% endif %}
</dt> </dt>
<dd> <dd>
{% csrf_token %} {% csrf_token %}
...@@ -38,7 +43,8 @@ ...@@ -38,7 +43,8 @@
<div id="vm-details-home-description" class="js-hidden"> <div id="vm-details-home-description" class="js-hidden">
<form method="POST"> <form method="POST">
<textarea name="new_description" class="form-control">{{ instance.description }}</textarea> <textarea name="new_description" class="form-control">{{ instance.description }}</textarea>
<button type="submit" class="btn btn-xs btn-success vm-details-description-submit"> <button type="submit" class="btn btn-xs btn-success vm-details-description-submit
{% if not is_operator %}disabled{% endif %}">
<i class="fa fa-pencil"></i> {% trans "Update" %} <i class="fa fa-pencil"></i> {% trans "Update" %}
</button> </button>
</form> </form>
...@@ -58,9 +64,17 @@ ...@@ -58,9 +64,17 @@
</h4> </h4>
<dl> <dl>
<dt>{% trans "Suspended at:" %}</dt> <dt>{% trans "Suspended at:" %}</dt>
<dd><i class="fa fa-moon-o"></i> {{ instance.time_of_suspend|timeuntil }}</dd> <dd>
<span title="{{ instance.time_of_suspend }}">
<i class="fa fa-moon-o"></i> {{ instance.time_of_suspend|timeuntil }}
</span>
</dd>
<dt>{% trans "Destroyed at:" %}</dt> <dt>{% trans "Destroyed at:" %}</dt>
<dd><i class="fa fa-times"></i> {{ instance.time_of_delete|timeuntil }}</dd> <dd>
<span title="{{ instance.time_of_delete }}">
<i class="fa fa-times"></i> {{ instance.time_of_delete|timeuntil }}
</span>
</dd>
</dl> </dl>
<div style="font-weight: bold;">{% trans "Tags" %}</div> <div style="font-weight: bold;">{% trans "Tags" %}</div>
...@@ -70,11 +84,13 @@ ...@@ -70,11 +84,13 @@
{% for t in instance.tags.all %} {% for t in instance.tags.all %}
<div class="label label-primary label-tag" style="display: inline-block"> <div class="label label-primary label-tag" style="display: inline-block">
{{ t }} {{ t }}
<a href="#" class="vm-details-remove-tag"><i class="fa fa-times"></i></a> {% if is_operator %}
<a href="#" class="vm-details-remove-tag"><i class="fa fa-times"></i></a>
{% endif %}
</div> </div>
{% endfor %} {% endfor %}
{% else %} {% else %}
<small>{% trans "No tag added!" %}</small> <small>{% trans "No tag added." %}</small>
{% endif %} {% endif %}
</div> </div>
<form action="" method="POST"> <form action="" method="POST">
...@@ -85,11 +101,26 @@ ...@@ -85,11 +101,26 @@
<i class="fa fa-question"></i> <i class="fa fa-question"></i>
</div>--> </div>-->
<div class="input-group-btn"> <div class="input-group-btn">
<input type="submit" class="btn btn-default btn-sm input-tags" value="{% trans "Add tag" %}"/> <input type="submit" class="btn btn-default btn-sm input-tags
{% if not is_operator %}disabled{% endif %}" value="{% trans "Add tag" %}"/>
</div> </div>
</div> </div>
</form> </form>
</div><!-- id:vm-details-tags --> </div><!-- id:vm-details-tags -->
{% if request.user.is_superuser %}
<dl>
<dt>{% trans "Node" %}:</dt>
<dd>
{% if instance.node %}
<a href="{{ instance.node.get_absolute_url }}">
{{ instance.node.name }}
</a>
{% else %}
-
{% endif %}
</dd>
{% endif %}
</dl>
<dl> <dl>
<dt>{% trans "Template" %}:</dt> <dt>{% trans "Template" %}:</dt>
<dd> <dd>
......
...@@ -21,11 +21,13 @@ ...@@ -21,11 +21,13 @@
<a href="{{ i.host.get_absolute_url }}" <a href="{{ i.host.get_absolute_url }}"
class="btn btn-default btn-xs">{% trans "edit" %}</a> class="btn btn-default btn-xs">{% trans "edit" %}</a>
{% endif %} {% endif %}
<a href="{% url "dashboard.views.interface-delete" pk=i.pk %}?next={{ request.path }}" {% if is_owner %}
class="btn btn-danger btn-xs interface-remove" <a href="{% url "dashboard.views.interface-delete" pk=i.pk %}?next={{ request.path }}"
data-interface-pk="{{ i.pk }}"> class="btn btn-danger btn-xs interface-remove"
{% trans "remove" %} data-interface-pk="{{ i.pk }}">
</a> {% trans "remove" %}
</a>
{% endif %}
</h3> </h3>
{% if i.host %} {% if i.host %}
<div class="row"> <div class="row">
......
...@@ -72,6 +72,10 @@ ...@@ -72,6 +72,10 @@
{% trans "Lease" as t %} {% trans "Lease" as t %}
{% include "dashboard/vm-list/header-link.html" with name=t sort="lease" %} {% include "dashboard/vm-list/header-link.html" with name=t sort="lease" %}
</th> </th>
<th data-sort="string" class="orderable sortable">
{% trans "Memory" as t %}
{% include "dashboard/vm-list/header-link.html" with name=t sort="ram_size" %}
</th>
{% if user.is_superuser %} {% if user.is_superuser %}
<th data-sort="string" class="orderable sortable"> <th data-sort="string" class="orderable sortable">
{% trans "IP address" as t %} {% trans "IP address" as t %}
...@@ -86,7 +90,9 @@ ...@@ -86,7 +90,9 @@
{% for i in object_list %} {% for i in object_list %}
<tr class="{% cycle 'odd' 'even' %}" data-vm-pk="{{ i.pk }}"> <tr class="{% cycle 'odd' 'even' %}" data-vm-pk="{{ i.pk }}">
<td class="pk"><div id="vm-{{i.pk}}">{{i.pk}}</div> </td> <td class="pk"><div id="vm-{{i.pk}}">{{i.pk}}</div> </td>
<td class="name"><a class="real-link" href="{% url "dashboard.views.detail" i.pk %}">{{ i.name }}</a> </td> <td class="name"><a class="real-link" href="{% url "dashboard.views.detail" i.pk %}">
{{ i.name }}</a>
</td>
<td class="state"> <td class="state">
<i class="fa fa-fw <i class="fa fa-fw
{% if show_acts_in_progress and i.is_in_status_change %} {% if show_acts_in_progress and i.is_in_status_change %}
...@@ -104,7 +110,12 @@ ...@@ -104,7 +110,12 @@
{# include "dashboard/_display-name.html" with user=i.owner show_org=True #} {# include "dashboard/_display-name.html" with user=i.owner show_org=True #}
</td> </td>
<td class="lease "data-sort-value="{{ i.lease.name }}"> <td class="lease "data-sort-value="{{ i.lease.name }}">
{{ i.lease.name }} <span title="{{ i.time_of_suspend|timeuntil }} | {{ i.time_of_delete|timeuntil }}">
{{ i.lease.name }}
</span>
</td>
<td class="memory "data-sort-value="{{ i.ram_size }}">
{{ i.ram_size }} MiB
</td> </td>
{% if user.is_superuser %} {% if user.is_superuser %}
<td class="ip_addr "data-sort-value="{{ i.ipv4 }}"> <td class="ip_addr "data-sort-value="{{ i.ipv4 }}">
......
...@@ -1210,7 +1210,7 @@ class GroupDetailTest(LoginMixin, TestCase): ...@@ -1210,7 +1210,7 @@ class GroupDetailTest(LoginMixin, TestCase):
gp = self.g1.profile gp = self.g1.profile
acl_users = len(gp.get_users_with_level()) acl_users = len(gp.get_users_with_level())
response = c.post('/dashboard/group/' + response = c.post('/dashboard/group/' +
str(self.g1.pk) + '/acl/', str(gp.pk) + '/acl/',
{'name': 'user3', 'level': 'owner'}) {'name': 'user3', 'level': 'owner'})
self.assertEqual(acl_users, len(gp.get_users_with_level())) self.assertEqual(acl_users, len(gp.get_users_with_level()))
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
...@@ -1221,7 +1221,7 @@ class GroupDetailTest(LoginMixin, TestCase): ...@@ -1221,7 +1221,7 @@ class GroupDetailTest(LoginMixin, TestCase):
gp = self.g1.profile gp = self.g1.profile
acl_users = len(gp.get_users_with_level()) acl_users = len(gp.get_users_with_level())
response = c.post('/dashboard/group/' + response = c.post('/dashboard/group/' +
str(self.g1.pk) + '/acl/', str(gp.pk) + '/acl/',
{'name': 'user3', 'level': 'owner'}) {'name': 'user3', 'level': 'owner'})
self.assertEqual(acl_users, len(gp.get_users_with_level())) self.assertEqual(acl_users, len(gp.get_users_with_level()))
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
...@@ -1232,7 +1232,7 @@ class GroupDetailTest(LoginMixin, TestCase): ...@@ -1232,7 +1232,7 @@ class GroupDetailTest(LoginMixin, TestCase):
self.login(c, 'superuser') self.login(c, 'superuser')
acl_users = len(gp.get_users_with_level()) acl_users = len(gp.get_users_with_level())
response = c.post('/dashboard/group/' + response = c.post('/dashboard/group/' +
str(self.g1.pk) + '/acl/', str(gp.pk) + '/acl/',
{'name': 'user3', 'level': 'owner'}) {'name': 'user3', 'level': 'owner'})
self.assertEqual(acl_users + 1, len(gp.get_users_with_level())) self.assertEqual(acl_users + 1, len(gp.get_users_with_level()))
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
...@@ -1243,7 +1243,7 @@ class GroupDetailTest(LoginMixin, TestCase): ...@@ -1243,7 +1243,7 @@ class GroupDetailTest(LoginMixin, TestCase):
self.login(c, 'user0') self.login(c, 'user0')
acl_users = len(gp.get_users_with_level()) acl_users = len(gp.get_users_with_level())
response = c.post('/dashboard/group/' + response = c.post('/dashboard/group/' +
str(self.g1.pk) + '/acl/', str(gp.pk) + '/acl/',
{'name': 'user3', 'level': 'owner'}) {'name': 'user3', 'level': 'owner'})
self.assertEqual(acl_users + 1, len(gp.get_users_with_level())) self.assertEqual(acl_users + 1, len(gp.get_users_with_level()))
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
...@@ -1253,7 +1253,7 @@ class GroupDetailTest(LoginMixin, TestCase): ...@@ -1253,7 +1253,7 @@ class GroupDetailTest(LoginMixin, TestCase):
gp = self.g1.profile gp = self.g1.profile
acl_groups = len(gp.get_groups_with_level()) acl_groups = len(gp.get_groups_with_level())
response = c.post('/dashboard/group/' + response = c.post('/dashboard/group/' +
str(self.g1.pk) + '/acl/', str(gp.pk) + '/acl/',
{'name': 'group2', 'level': 'owner'}) {'name': 'group2', 'level': 'owner'})
self.assertEqual(acl_groups, len(gp.get_groups_with_level())) self.assertEqual(acl_groups, len(gp.get_groups_with_level()))
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
...@@ -1264,7 +1264,7 @@ class GroupDetailTest(LoginMixin, TestCase): ...@@ -1264,7 +1264,7 @@ class GroupDetailTest(LoginMixin, TestCase):
self.login(c, 'user3') self.login(c, 'user3')
acl_groups = len(gp.get_groups_with_level()) acl_groups = len(gp.get_groups_with_level())
response = c.post('/dashboard/group/' + response = c.post('/dashboard/group/' +
str(self.g1.pk) + '/acl/', str(gp.pk) + '/acl/',
{'name': 'group2', 'level': 'owner'}) {'name': 'group2', 'level': 'owner'})
self.assertEqual(acl_groups, len(gp.get_groups_with_level())) self.assertEqual(acl_groups, len(gp.get_groups_with_level()))
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
...@@ -1275,7 +1275,7 @@ class GroupDetailTest(LoginMixin, TestCase): ...@@ -1275,7 +1275,7 @@ class GroupDetailTest(LoginMixin, TestCase):
self.login(c, 'superuser') self.login(c, 'superuser')
acl_groups = len(gp.get_groups_with_level()) acl_groups = len(gp.get_groups_with_level())
response = c.post('/dashboard/group/' + response = c.post('/dashboard/group/' +
str(self.g1.pk) + '/acl/', str(gp.pk) + '/acl/',
{'name': 'group2', 'level': 'owner'}) {'name': 'group2', 'level': 'owner'})
self.assertEqual(acl_groups + 1, len(gp.get_groups_with_level())) self.assertEqual(acl_groups + 1, len(gp.get_groups_with_level()))
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
...@@ -1286,7 +1286,7 @@ class GroupDetailTest(LoginMixin, TestCase): ...@@ -1286,7 +1286,7 @@ class GroupDetailTest(LoginMixin, TestCase):
self.login(c, 'user0') self.login(c, 'user0')
acl_groups = len(gp.get_groups_with_level()) acl_groups = len(gp.get_groups_with_level())
response = c.post('/dashboard/group/' + response = c.post('/dashboard/group/' +
str(self.g1.pk) + '/acl/', str(gp.pk) + '/acl/',
{'name': 'group2', 'level': 'owner'}) {'name': 'group2', 'level': 'owner'})
self.assertEqual(acl_groups + 1, len(gp.get_groups_with_level())) self.assertEqual(acl_groups + 1, len(gp.get_groups_with_level()))
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
......
...@@ -180,10 +180,7 @@ class GroupPermissionsView(SuperuserRequiredMixin, UpdateView): ...@@ -180,10 +180,7 @@ class GroupPermissionsView(SuperuserRequiredMixin, UpdateView):
class GroupAclUpdateView(AclUpdateView): class GroupAclUpdateView(AclUpdateView):
model = Group model = GroupProfile
def get_object(self):
return super(GroupAclUpdateView, self).get_object().profile
class GroupList(LoginRequiredMixin, SingleTableView): class GroupList(LoginRequiredMixin, SingleTableView):
......
...@@ -68,6 +68,8 @@ node_ops = OrderedDict([ ...@@ -68,6 +68,8 @@ node_ops = OrderedDict([
op='passivate', icon='play-circle-o', effect='info')), op='passivate', icon='play-circle-o', effect='info')),
('disable', NodeOperationView.factory( ('disable', NodeOperationView.factory(
op='disable', icon='times-circle-o', effect='danger')), op='disable', icon='times-circle-o', effect='danger')),
('reset', NodeOperationView.factory(
op='reset', icon='stethoscope', effect='danger')),
('flush', NodeOperationView.factory( ('flush', NodeOperationView.factory(
op='flush', icon='paint-brush', effect='danger')), op='flush', icon='paint-brush', effect='danger')),
]) ])
......
...@@ -285,6 +285,25 @@ class ProfileView(LoginRequiredMixin, DetailView): ...@@ -285,6 +285,25 @@ class ProfileView(LoginRequiredMixin, DetailView):
slug_field = "username" slug_field = "username"
slug_url_kwarg = "username" slug_url_kwarg = "username"
def get(self, *args, **kwargs):
user = self.request.user
target = self.get_object()
# get the list of groups where the user is operator
user_g_w_op = GroupProfile.get_objects_with_level("operator", user)
# get the list of groups the "target" (the profile) is member of
target_groups = GroupProfile.objects.filter(
group__in=target.groups.all())
intersection = set(user_g_w_op).intersection(target_groups)
# if the intersection of the 2 lists is empty the logged in user
# has no permission to check the target's profile
# (except if the user want to see his own profile)
if len(intersection) < 1 and target != user:
raise PermissionDenied
return super(ProfileView, self).get(*args, **kwargs)
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(ProfileView, self).get_context_data(**kwargs) context = super(ProfileView, self).get_context_data(**kwargs)
user = self.get_object() user = self.get_object()
......
...@@ -45,6 +45,7 @@ from common.models import ( ...@@ -45,6 +45,7 @@ from common.models import (
create_readable, HumanReadableException, fetch_human_exception, create_readable, HumanReadableException, fetch_human_exception,
) )
from firewall.models import Vlan, Host, Rule from firewall.models import Vlan, Host, Rule
from manager.scheduler import SchedulerError
from storage.models import Disk from storage.models import Disk
from vm.models import ( from vm.models import (
Instance, instance_activity, InstanceActivity, Node, Lease, Instance, instance_activity, InstanceActivity, Node, Lease,
...@@ -58,7 +59,7 @@ from ..forms import ( ...@@ -58,7 +59,7 @@ from ..forms import (
AclUserOrGroupAddForm, VmResourcesForm, TraitsForm, RawDataForm, AclUserOrGroupAddForm, VmResourcesForm, TraitsForm, RawDataForm,
VmAddInterfaceForm, VmCreateDiskForm, VmDownloadDiskForm, VmSaveForm, VmAddInterfaceForm, VmCreateDiskForm, VmDownloadDiskForm, VmSaveForm,
VmRenewForm, VmStateChangeForm, VmListSearchForm, VmCustomizeForm, VmRenewForm, VmStateChangeForm, VmListSearchForm, VmCustomizeForm,
TransferOwnershipForm, VmDiskResizeForm, TransferOwnershipForm, VmDiskResizeForm, RedeployForm,
) )
from ..models import Favourite, Profile from ..models import Favourite, Profile
...@@ -97,6 +98,8 @@ class VmDetailView(GraphMixin, CheckedDetailView): ...@@ -97,6 +98,8 @@ class VmDetailView(GraphMixin, CheckedDetailView):
context = super(VmDetailView, self).get_context_data(**kwargs) context = super(VmDetailView, self).get_context_data(**kwargs)
instance = context['instance'] instance = context['instance']
user = self.request.user user = self.request.user
is_operator = instance.has_level(user, "operator")
is_owner = instance.has_level(user, "owner")
ops = get_operations(instance, user) ops = get_operations(instance, user)
context.update({ context.update({
'graphite_enabled': settings.GRAPHITE_URL is not None, 'graphite_enabled': settings.GRAPHITE_URL is not None,
...@@ -152,9 +155,11 @@ class VmDetailView(GraphMixin, CheckedDetailView): ...@@ -152,9 +155,11 @@ class VmDetailView(GraphMixin, CheckedDetailView):
context['client_download'] = self.request.COOKIES.get( context['client_download'] = self.request.COOKIES.get(
'downloaded_client') 'downloaded_client')
# can link template # can link template
context['can_link_template'] = ( context['can_link_template'] = instance.template and is_operator
instance.template and instance.template.has_level(user, "operator")
) # is operator/owner
context['is_operator'] = is_operator
context['is_owner'] = is_owner
return context return context
...@@ -174,7 +179,7 @@ class VmDetailView(GraphMixin, CheckedDetailView): ...@@ -174,7 +179,7 @@ class VmDetailView(GraphMixin, CheckedDetailView):
def __set_name(self, request): def __set_name(self, request):
self.object = self.get_object() self.object = self.get_object()
if not self.object.has_level(request.user, 'owner'): if not self.object.has_level(request.user, "operator"):
raise PermissionDenied() raise PermissionDenied()
new_name = request.POST.get("new_name") new_name = request.POST.get("new_name")
Instance.objects.filter(pk=self.object.pk).update( Instance.objects.filter(pk=self.object.pk).update(
...@@ -197,7 +202,7 @@ class VmDetailView(GraphMixin, CheckedDetailView): ...@@ -197,7 +202,7 @@ class VmDetailView(GraphMixin, CheckedDetailView):
def __set_description(self, request): def __set_description(self, request):
self.object = self.get_object() self.object = self.get_object()
if not self.object.has_level(request.user, 'owner'): if not self.object.has_level(request.user, "operator"):
raise PermissionDenied() raise PermissionDenied()
new_description = request.POST.get("new_description") new_description = request.POST.get("new_description")
...@@ -221,7 +226,7 @@ class VmDetailView(GraphMixin, CheckedDetailView): ...@@ -221,7 +226,7 @@ class VmDetailView(GraphMixin, CheckedDetailView):
def __add_tag(self, request): def __add_tag(self, request):
new_tag = request.POST.get('new_tag') new_tag = request.POST.get('new_tag')
self.object = self.get_object() self.object = self.get_object()
if not self.object.has_level(request.user, 'owner'): if not self.object.has_level(request.user, "operator"):
raise PermissionDenied() raise PermissionDenied()
if len(new_tag) < 1: if len(new_tag) < 1:
...@@ -243,7 +248,7 @@ class VmDetailView(GraphMixin, CheckedDetailView): ...@@ -243,7 +248,7 @@ class VmDetailView(GraphMixin, CheckedDetailView):
try: try:
to_remove = request.POST.get('to_remove') to_remove = request.POST.get('to_remove')
self.object = self.get_object() self.object = self.get_object()
if not self.object.has_level(request.user, 'owner'): if not self.object.has_level(request.user, "operator"):
raise PermissionDenied() raise PermissionDenied()
self.object.tags.remove(to_remove) self.object.tags.remove(to_remove)
...@@ -262,8 +267,8 @@ class VmDetailView(GraphMixin, CheckedDetailView): ...@@ -262,8 +267,8 @@ class VmDetailView(GraphMixin, CheckedDetailView):
def __add_port(self, request): def __add_port(self, request):
object = self.get_object() object = self.get_object()
if (not object.has_level(request.user, 'owner') or if not (object.has_level(request.user, "operator") and
not request.user.has_perm('vm.config_ports')): request.user.has_perm('vm.config_ports')):
raise PermissionDenied() raise PermissionDenied()
port = request.POST.get("port") port = request.POST.get("port")
...@@ -420,6 +425,15 @@ class VmMigrateView(VmOperationView): ...@@ -420,6 +425,15 @@ class VmMigrateView(VmOperationView):
ctx = super(VmMigrateView, self).get_context_data(**kwargs) ctx = super(VmMigrateView, self).get_context_data(**kwargs)
ctx['nodes'] = [n for n in Node.objects.filter(enabled=True) ctx['nodes'] = [n for n in Node.objects.filter(enabled=True)
if n.online] if n.online]
inst = self.get_object()
ctx["recommended"] = None
try:
if isinstance(inst, Instance):
ctx["recommended"] = inst.select_node().pk
except SchedulerError:
logger.exception("scheduler error:")
return ctx return ctx
def post(self, request, extra=None, *args, **kwargs): def post(self, request, extra=None, *args, **kwargs):
...@@ -599,6 +613,15 @@ class VmStateChangeView(FormOperationMixin, VmOperationView): ...@@ -599,6 +613,15 @@ class VmStateChangeView(FormOperationMixin, VmOperationView):
return val return val
class RedeployView(FormOperationMixin, VmOperationView):
op = 'redeploy'
icon = 'stethoscope'
effect = 'danger'
show_in_toolbar = True
form_class = RedeployForm
wait_for_result = 0.5
vm_ops = OrderedDict([ vm_ops = OrderedDict([
('deploy', VmOperationView.factory( ('deploy', VmOperationView.factory(
op='deploy', icon='play', effect='success')), op='deploy', icon='play', effect='success')),
...@@ -620,6 +643,7 @@ vm_ops = OrderedDict([ ...@@ -620,6 +643,7 @@ vm_ops = OrderedDict([
('recover', VmOperationView.factory( ('recover', VmOperationView.factory(
op='recover', icon='medkit', effect='warning')), op='recover', icon='medkit', effect='warning')),
('nostate', VmStateChangeView), ('nostate', VmStateChangeView),
('redeploy', RedeployView),
('destroy', VmOperationView.factory( ('destroy', VmOperationView.factory(
extra_bases=[TokenOperationView], extra_bases=[TokenOperationView],
op='destroy', icon='times', effect='danger')), op='destroy', icon='times', effect='danger')),
......
...@@ -657,7 +657,7 @@ class VlanDetail(LoginRequiredMixin, SuperuserRequiredMixin, ...@@ -657,7 +657,7 @@ class VlanDetail(LoginRequiredMixin, SuperuserRequiredMixin,
context = super(VlanDetail, self).get_context_data(**kwargs) context = super(VlanDetail, self).get_context_data(**kwargs)
q = Host.objects.filter(interface__in=Interface.objects.filter( q = Host.objects.filter(interface__in=Interface.objects.filter(
vlan=self.object, instance__destroyed_at=None vlan=self.object
)) ))
context['host_list'] = SmallHostTable(q) context['host_list'] = SmallHostTable(q)
......
...@@ -266,6 +266,7 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin, ...@@ -266,6 +266,7 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
('change_resources', _('Can change resources of a running VM.')), ('change_resources', _('Can change resources of a running VM.')),
('set_resources', _('Can change resources of a new VM.')), ('set_resources', _('Can change resources of a new VM.')),
('create_vm', _('Can create a new VM.')), ('create_vm', _('Can create a new VM.')),
('redeploy', _('Can redeploy a VM.')),
('config_ports', _('Can configure port forwards.')), ('config_ports', _('Can configure port forwards.')),
('recover', _('Can recover a destroyed VM.')), ('recover', _('Can recover a destroyed VM.')),
('emergency_change_state', _('Can change VM state to NOSTATE.')), ('emergency_change_state', _('Can change VM state to NOSTATE.')),
...@@ -763,6 +764,7 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin, ...@@ -763,6 +764,7 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
if self.node is None: if self.node is None:
self.node = self.select_node() self.node = self.select_node()
self.save() self.save()
return self.node
def yield_node(self): def yield_node(self):
if self.node is not None: if self.node is not None:
......
...@@ -114,8 +114,8 @@ class Node(OperatedMixin, TimeStampedModel): ...@@ -114,8 +114,8 @@ class Node(OperatedMixin, TimeStampedModel):
def get_info(self): def get_info(self):
return self.remote_query(vm_tasks.get_info, return self.remote_query(vm_tasks.get_info,
priority='fast', priority='fast',
default={'core_num': '', default={'core_num': 0,
'ram_size': '0', 'ram_size': 0,
'architecture': ''}) 'architecture': ''})
info = property(get_info) info = property(get_info)
......
...@@ -330,7 +330,7 @@ class DeployOperation(InstanceOperation): ...@@ -330,7 +330,7 @@ class DeployOperation(InstanceOperation):
def _operation(self, activity, timeout=15): def _operation(self, activity, timeout=15):
# Allocate VNC port and host node # Allocate VNC port and host node
self.instance.allocate_vnc_port() self.instance.allocate_vnc_port()
self.instance.allocate_node() self.instance.allocate_node(activity)
# Deploy virtual images # Deploy virtual images
self.instance._deploy_disks(parent_activity=activity) self.instance._deploy_disks(parent_activity=activity)
...@@ -487,12 +487,7 @@ class MigrateOperation(RemoteInstanceOperation): ...@@ -487,12 +487,7 @@ class MigrateOperation(RemoteInstanceOperation):
def _operation(self, activity, to_node=None): def _operation(self, activity, to_node=None):
if not to_node: if not to_node:
with activity.sub_activity('scheduling', to_node = self.instance.reallocate_node(activity)
readable_name=ugettext_noop(
"schedule")) as sa:
to_node = self.instance.select_node()
sa.result = to_node
try: try:
with activity.sub_activity( with activity.sub_activity(
'migrate_vm', readable_name=create_readable( 'migrate_vm', readable_name=create_readable(
...@@ -512,6 +507,7 @@ class MigrateOperation(RemoteInstanceOperation): ...@@ -512,6 +507,7 @@ class MigrateOperation(RemoteInstanceOperation):
# Refresh node information # Refresh node information
self.instance.node = to_node self.instance.node = to_node
self.instance.save() self.instance.save()
# Estabilish network connection (vmdriver) # Estabilish network connection (vmdriver)
with activity.sub_activity( with activity.sub_activity(
'deploying_net', readable_name=ugettext_noop( 'deploying_net', readable_name=ugettext_noop(
...@@ -824,7 +820,7 @@ class WakeUpOperation(InstanceOperation): ...@@ -824,7 +820,7 @@ class WakeUpOperation(InstanceOperation):
def _operation(self, activity): def _operation(self, activity):
# Schedule vm # Schedule vm
self.instance.allocate_vnc_port() self.instance.allocate_vnc_port()
self.instance.allocate_node() self.instance.allocate_node(activity)
# Resume vm # Resume vm
self.instance._wake_up_vm(parent_activity=activity) self.instance._wake_up_vm(parent_activity=activity)
...@@ -901,7 +897,8 @@ class ChangeStateOperation(InstanceOperation): ...@@ -901,7 +897,8 @@ class ChangeStateOperation(InstanceOperation):
required_perms = ('vm.emergency_change_state', ) required_perms = ('vm.emergency_change_state', )
concurrency_check = False concurrency_check = False
def _operation(self, user, activity, new_state="NOSTATE", interrupt=False): def _operation(self, user, activity, new_state="NOSTATE", interrupt=False,
reset_node=False):
activity.resultant_state = new_state activity.resultant_state = new_state
if interrupt: if interrupt:
msg_txt = ugettext_noop("Activity is forcibly interrupted.") msg_txt = ugettext_noop("Activity is forcibly interrupted.")
...@@ -911,6 +908,37 @@ class ChangeStateOperation(InstanceOperation): ...@@ -911,6 +908,37 @@ class ChangeStateOperation(InstanceOperation):
i.finish(False, result=message) i.finish(False, result=message)
logger.error('Forced finishing activity %s', i) logger.error('Forced finishing activity %s', i)
if reset_node:
self.instance.node = None
self.instance.save()
@register_operation
class RedeployOperation(InstanceOperation):
activity_code_suffix = 'redeploy'
id = 'redeploy'
name = _("redeploy")
description = _("Change the virtual machine state to NOSTATE "
"and redeploy the VM. This operation allows starting "
"machines formerly running on a failed node.")
acl_level = "owner"
required_perms = ('vm.redeploy', )
concurrency_check = False
def _operation(self, user, activity, with_emergency_change_state=True):
if with_emergency_change_state:
ChangeStateOperation(self.instance).call(
parent_activity=activity, user=user,
new_state='NOSTATE', interrupt=False, reset_node=True)
else:
ShutOffOperation(self.instance).call(
parent_activity=activity, user=user)
self.instance._update_status()
DeployOperation(self.instance).call(
parent_activity=activity, user=user)
class NodeOperation(Operation): class NodeOperation(Operation):
async_operation = abortable_async_node_operation async_operation = abortable_async_node_operation
...@@ -950,6 +978,35 @@ class NodeOperation(Operation): ...@@ -950,6 +978,35 @@ class NodeOperation(Operation):
@register_operation @register_operation
class ResetNodeOperation(NodeOperation):
activity_code_suffix = 'reset'
id = 'reset'
name = _("reset")
description = _("Disable missing node and redeploy all instances "
"on other ones.")
required_perms = ()
online_required = False
async_queue = "localhost.man.slow"
def check_precond(self):
super(ResetNodeOperation, self).check_precond()
if not self.node.enabled or self.node.online:
raise humanize_exception(ugettext_noop(
"You cannot reset a disabled or online node."), Exception())
def _operation(self, activity, user):
if self.node.enabled:
DisableOperation(self.node).call(parent_activity=activity,
user=user)
for i in self.node.instance_set.all():
name = create_readable(ugettext_noop(
"migrate %(instance)s (%(pk)s)"), instance=i.name, pk=i.pk)
with activity.sub_activity('migrate_instance_%d' % i.pk,
readable_name=name):
i.redeploy(user=user)
@register_operation
class FlushOperation(NodeOperation): class FlushOperation(NodeOperation):
id = 'flush' id = 'flush'
name = _("flush") name = _("flush")
......
...@@ -114,8 +114,7 @@ class InstanceTestCase(TestCase): ...@@ -114,8 +114,7 @@ class InstanceTestCase(TestCase):
migrate_op(system=True) migrate_op(system=True)
migr.apply_async.assert_called() migr.apply_async.assert_called()
self.assertIn(call.sub_activity( inst.allocate_node.assert_called()
u'scheduling', readable_name=u'schedule'), act.mock_calls)
inst.select_node.assert_called() inst.select_node.assert_called()
def test_migrate_wo_scheduling(self): def test_migrate_wo_scheduling(self):
...@@ -133,7 +132,7 @@ class InstanceTestCase(TestCase): ...@@ -133,7 +132,7 @@ class InstanceTestCase(TestCase):
migrate_op(to_node=inst.node, system=True) migrate_op(to_node=inst.node, system=True)
migr.apply_async.assert_called() migr.apply_async.assert_called()
self.assertNotIn(call.sub_activity(u'scheduling'), act.mock_calls) inst.allocate_node.assert_called()
def test_migrate_with_error(self): def test_migrate_with_error(self):
inst = Mock(destroyed_at=None, spec=Instance) inst = Mock(destroyed_at=None, spec=Instance)
......
...@@ -16,9 +16,10 @@ ...@@ -16,9 +16,10 @@
# with CIRCLE. If not, see <http://www.gnu.org/licenses/>. # with CIRCLE. If not, see <http://www.gnu.org/licenses/>.
from django.test import TestCase from django.test import TestCase
from mock import MagicMock
from common.operations import operation_registry_name as op_reg_name from common.operations import operation_registry_name as op_reg_name
from vm.models import Instance, Node from vm.models import Instance, InstanceActivity, Node
from vm.operations import ( from vm.operations import (
DeployOperation, DestroyOperation, FlushOperation, MigrateOperation, DeployOperation, DestroyOperation, FlushOperation, MigrateOperation,
RebootOperation, ResetOperation, SaveAsTemplateOperation, RebootOperation, ResetOperation, SaveAsTemplateOperation,
...@@ -45,6 +46,22 @@ class MigrateOperationTestCase(TestCase): ...@@ -45,6 +46,22 @@ class MigrateOperationTestCase(TestCase):
def test_operation_registered(self): def test_operation_registered(self):
assert MigrateOperation.id in getattr(Instance, op_reg_name) assert MigrateOperation.id in getattr(Instance, op_reg_name)
def test_operation_wo_to_node_param(self):
class MigrateException(Exception):
pass
inst = MagicMock(spec=Instance)
act = MagicMock(spec=InstanceActivity)
inst.migrate_vm = MagicMock(side_effect=MigrateException())
inst.select_node = MagicMock(return_value='test')
inst.reallocate_node = (
lambda act: Instance.reallocate_node(inst, act))
self.assertRaises(
MigrateException, MigrateOperation(inst)._operation,
act, to_node=None)
assert inst.select_node.called
inst.migrate_vm.assert_called_once_with(to_node='test', timeout=120)
class RebootOperationTestCase(TestCase): class RebootOperationTestCase(TestCase):
def test_operation_registered(self): def test_operation_registered(self):
......
...@@ -9,7 +9,7 @@ django-braces==1.4.0 ...@@ -9,7 +9,7 @@ django-braces==1.4.0
django-celery==3.1.10 django-celery==3.1.10
django-crispy-forms==1.4.0 django-crispy-forms==1.4.0
django-model-utils==2.0.3 django-model-utils==2.0.3
django-sizefield==0.5 django-sizefield==0.6
django-sshkey==2.2.0 django-sshkey==2.2.0
django-statici18n==1.1 django-statici18n==1.1
django-tables2==0.15.0 django-tables2==0.15.0
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment