Commit 113c216a by Bach Dániel

Merge branch 'issue-298' into 'master'

Issue 298

closes #298
parents 34bf3f22 0a778b39
...@@ -273,3 +273,4 @@ def register_operation(op_cls, op_id=None, target_cls=None): ...@@ -273,3 +273,4 @@ def register_operation(op_cls, op_id=None, target_cls=None):
setattr(target_cls, operation_registry_name, dict()) setattr(target_cls, operation_registry_name, dict())
getattr(target_cls, operation_registry_name)[op_id] = op_cls getattr(target_cls, operation_registry_name)[op_id] = op_cls
return op_cls
...@@ -19,8 +19,7 @@ from __future__ import absolute_import ...@@ -19,8 +19,7 @@ from __future__ import absolute_import
from django.contrib.auth.models import Group, User from django.contrib.auth.models import Group, User
from django_tables2 import Table, A from django_tables2 import Table, A
from django_tables2.columns import (TemplateColumn, Column, BooleanColumn, from django_tables2.columns import TemplateColumn, Column, LinkColumn
LinkColumn)
from vm.models import Node, InstanceTemplate, Lease from vm.models import Node, InstanceTemplate, Lease
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
...@@ -40,8 +39,10 @@ class NodeListTable(Table): ...@@ -40,8 +39,10 @@ class NodeListTable(Table):
attrs={'th': {'class': 'node-list-table-thin'}}, attrs={'th': {'class': 'node-list-table-thin'}},
) )
enabled = BooleanColumn( get_status_display = Column(
verbose_name=_("Status"),
attrs={'th': {'class': 'node-list-table-thin'}}, attrs={'th': {'class': 'node-list-table-thin'}},
order_by=("enabled", "schedule_enabled"),
) )
name = TemplateColumn( name = TemplateColumn(
...@@ -66,20 +67,12 @@ class NodeListTable(Table): ...@@ -66,20 +67,12 @@ class NodeListTable(Table):
orderable=False, orderable=False,
) )
actions = TemplateColumn(
verbose_name=_("Actions"),
attrs={'th': {'class': 'node-list-table-thin'}},
template_code=('{% include "dashboard/node-list/column-'
'actions.html" with btn_size="btn-xs" %}'),
orderable=False,
)
class Meta: class Meta:
model = Node model = Node
attrs = {'class': ('table table-bordered table-striped table-hover ' attrs = {'class': ('table table-bordered table-striped table-hover '
'node-list-table')} 'node-list-table')}
fields = ('pk', 'name', 'host', 'enabled', 'priority', 'overcommit', fields = ('pk', 'name', 'host', 'get_status_display', 'priority',
'number_of_VMs', ) 'overcommit', 'number_of_VMs', )
class GroupListTable(Table): class GroupListTable(Table):
......
...@@ -18,6 +18,8 @@ Choose a compute node to migrate {{obj}} to. ...@@ -18,6 +18,8 @@ Choose a compute node to migrate {{obj}} to.
<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}}">
<strong>{{ n }}</strong> <strong>{{ n }}</strong>
<div class="label label-primary"><i class="fa {{n.get_status_icon}}"></i>
{{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 selected == n.pk %}<div class="label label-success">{% trans "recommended" %}</div>{% endif %}
</label> </label>
......
{% load i18n %}
<div class="modal fade" id="confirmation-modal" tabindex="-1" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-body">
{% if text %}
{{ text }}
{% else %}
{%blocktrans with object=object%}
Are you sure you want to flush <strong>{{ object }}</strong>?
{%endblocktrans%}
{% endif %}
<div class="pull-right">
<form action="{% url "dashboard.views.flush-node" pk=node.pk %}?next={{next}}" method="POST">
{% csrf_token %}
<button type="button" class="btn btn-default" data-dismiss="modal">{% trans "Cancel" %}</button>
<input type="hidden" name="flush" value=""/>
<button class="btn btn-warning">{% trans "Yes" %}</button>
</form>
</div>
<div class="clearfix"></div>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div>
...@@ -6,13 +6,12 @@ ...@@ -6,13 +6,12 @@
{% block content %} {% block content %}
<div class="body-content"> <div class="body-content">
<div class="page-header"> <div class="page-header">
<div class="pull-right" id="ops">
{% include "dashboard/vm-detail/_operations.html" %}
</div>
<div class="pull-right" style="padding-top: 15px;"> <div class="pull-right" style="padding-top: 15px;">
<a title="{% trans "Rename" %}" href="#" class="btn btn-default btn-xs node-details-rename-button"><i class="fa fa-pencil"></i></a> <a title="{% trans "Rename" %}" href="#" class="btn btn-default btn-xs node-details-rename-button"><i class="fa fa-pencil"></i></a>
<a title="{% trans "Flush" %}" data-node-pk="{{ node.pk }}" class="btn btn-default btn-xs real-link node-flush" href="{% url "dashboard.views.flush-node" pk=node.pk %}"><i class="fa fa-cloud-upload"></i></a>
<a title="{% trans "Enable" %}" style="display:{% if node.enabled %}none{% else %}inline-block{% endif %}" data-node-pk="{{ node.pk }}" class="btn btn-default btn-xs real-link node-enable" href="{% url "dashboard.views.status-node" pk=node.pk %}?next={{ request.path }}"><i class="fa fa-check"></i></a>
<a title="{% trans "Disable" %}" style="display:{% if not node.enabled %}none{% else %}inline-block{% endif %}" data-node-pk="{{ node.pk }}" class="btn btn-default btn-xs real-link node-enable" href="{% url "dashboard.views.status-node" pk=node.pk %}?next={{ request.path }}"><i class="fa fa-ban"></i></a>
<a title="{% trans "Delete" %}" data-node-pk="{{ node.pk }}" class="btn btn-default btn-xs real-link node-delete" href="{% url "dashboard.views.delete-node" pk=node.pk %}"><i class="fa fa-trash-o"></i></a> <a title="{% trans "Delete" %}" data-node-pk="{{ node.pk }}" class="btn btn-default btn-xs real-link node-delete" href="{% url "dashboard.views.delete-node" pk=node.pk %}"><i class="fa fa-trash-o"></i></a>
<a title="{% trans "Help" %}" href="#" class="btn btn-default btn-xs node-details-help-button"><i class="fa fa-question"></i></a>
</div> </div>
<h1> <h1>
<div id="node-details-rename"> <div id="node-details-rename">
...@@ -26,42 +25,34 @@ ...@@ -26,42 +25,34 @@
{{ node.name }} {{ node.name }}
</div> </div>
</h1> </h1>
<div class="node-details-help js-hidden">
<ul style="list-style: none;">
<li>
<strong>{% trans "Rename" %}:</strong>
{% trans "Change the name of the node." %}
</li>
<li>
<strong>{% trans "Flush" %}:</strong>
{% trans "Disable node and move all instances to other one." %}
</li>
<li>
<strong>{% trans "Enable" %}:</strong>
{% trans "Enables node." %}
</li>
<li>
<strong>{% trans "Disable" %}:</strong>
{% trans "Disables node." %}
</li>
<li>
<strong>{% trans "Delete" %}:</strong>
{% trans "Remove node and it's host." %}
</li>
</ul>
</div>
</div> </div>
<div class="row"> <div class="row">
<div class="col-md-2" id="node-info-pane"> <div class="col-md-2" id="node-info-pane">
<div id="node-info-data" class="big"> <div id="node-info-data" class="big">
<span id="node-details-state" class="label <span id="node-details-state" class="label
{% if node.state == 'ONLINE' %}label-success {% if node.state == 'ACTIVE' %}label-success
{% elif node.state == 'MISSING' %}label-danger {% elif node.state == 'PASSIVE' %}label-warning
{% elif node.state == 'DISABLED' %}label-warning {% else %}label-danger{% endif %}">
{% elif node.state == 'OFFLINE' %}label-warning{% endif %}">
<i class="fa {{ node.get_status_icon }}"></i> {{ node.get_status_display|upper }} <i class="fa {{ node.get_status_icon }}"></i> {{ node.get_status_display|upper }}
</span> </span>
</div> </div>
<div>
{% if node.enabled %}
<span class="label label-success">{% trans "Enabled" %}</span>
{% if node.schedule_enabled %}
<span class="label label-success">{% trans "Schedule enabled" %}</span>
{% else %}
<span class="label label-warning">{% trans "Schedule disabled" %}</span>
{% endif %}
{% else %}
<span class="label label-warning">{% trans "Disabled" %}</span>
{% endif %}
{% if node.online %}
<span class="label label-success">{% trans "Online" %}</span>
{% else %}
<span class="label label-warning">{% trans "Offline" %}</span>
{% endif %}
</div>
</div> </div>
<div class="col-md-10" id="node-detail-pane"> <div class="col-md-10" id="node-detail-pane">
<div class="panel panel-default" id="node-detail-panel"> <div class="panel panel-default" id="node-detail-panel">
......
{% load i18n %}
<div class="btn-group">
<button type="button" class="btn {{ btn_size }} btn-warning nojs-dropdown-toogle dropdown-toggle" data-toggle="dropdown">Action
<i class="fa fa-caret-down"></i>
</button>
<ul class="dropdown-menu nojs-dropdown-toogle" role="menu">
<li>
<a href="#" class="node-details-rename-button">
<i class="fa fa-pencil"></i> {% trans "Rename" %}
</a>
</li>
<li>
<a data-node-pk="{{ record.pk }}" class="real-link node-flush" href="{% url "dashboard.views.flush-node" pk=record.pk %}">
<i class="fa fa-cloud-upload"></i> {% trans "Flush" %}
</a>
</li>
<li>
<a style={% if record.enabled %}"display:none"{% else %}"display:block"{% endif %} data-node-pk="{{ record.pk }}" class="real-link node-enable" href="{% url "dashboard.views.status-node" pk=record.pk %}?next={{ request.path }}">
<i class="fa fa-check"></i> {% trans "Enable" %}
</a>
</li>
<li>
<a style={% if record.enabled %}"display:block"{% else %}"display:none"{% endif %} data-node-pk="{{ record.pk }}" class="real-link node-enable" href="{% url "dashboard.views.status-node" pk=record.pk %}?next={{ request.path }}">
<i class="fa fa-times"></i> {% trans "Disable" %}
</a>
</li>
<li>
<a data-node-pk="{{ record.pk }}" class="real-link node-delete" href="{% url "dashboard.views.delete-node" pk=record.pk %}?next={{ request.path }}">
<i class="fa fa-trash-o"></i> {% trans "Delete" %}
</a>
</li>
</ul>
</div>
...@@ -25,7 +25,7 @@ from .views import ( ...@@ -25,7 +25,7 @@ from .views import (
GroupDetailView, GroupList, IndexView, GroupDetailView, GroupList, IndexView,
InstanceActivityDetail, LeaseCreate, LeaseDelete, LeaseDetail, InstanceActivityDetail, LeaseCreate, LeaseDelete, LeaseDetail,
MyPreferencesView, NodeAddTraitView, NodeCreate, NodeDelete, MyPreferencesView, NodeAddTraitView, NodeCreate, NodeDelete,
NodeDetailView, NodeFlushView, NodeList, NodeStatus, NodeDetailView, NodeList, NodeStatus,
NotificationView, PortDelete, TemplateAclUpdateView, TemplateCreate, NotificationView, PortDelete, TemplateAclUpdateView, TemplateCreate,
TemplateDelete, TemplateDetail, TemplateList, TransferOwnershipConfirmView, TemplateDelete, TemplateDetail, TemplateList, TransferOwnershipConfirmView,
TransferOwnershipView, vm_activity, VmCreate, VmDetailView, TransferOwnershipView, vm_activity, VmCreate, VmDetailView,
...@@ -48,6 +48,8 @@ from .views import ( ...@@ -48,6 +48,8 @@ from .views import (
ClientCheck, TokenLogin, ClientCheck, TokenLogin,
VmGraphView, NodeGraphView, NodeListGraphView, VmGraphView, NodeGraphView, NodeListGraphView,
) )
from .views.vm import vm_ops, vm_mass_ops
from .views.node import node_ops
autocomplete_light.autodiscover() autocomplete_light.autodiscover()
...@@ -75,8 +77,6 @@ urlpatterns = patterns( ...@@ -75,8 +77,6 @@ urlpatterns = patterns(
name="dashboard.views.template-list"), name="dashboard.views.template-list"),
url(r"^template/delete/(?P<pk>\d+)/$", TemplateDelete.as_view(), url(r"^template/delete/(?P<pk>\d+)/$", TemplateDelete.as_view(),
name="dashboard.views.template-delete"), name="dashboard.views.template-delete"),
url(r'^vm/', include('dashboard.vm.urls')),
url(r'^vm/(?P<pk>\d+)/remove_port/(?P<rule>\d+)/$', PortDelete.as_view(), url(r'^vm/(?P<pk>\d+)/remove_port/(?P<rule>\d+)/$', PortDelete.as_view(),
name='dashboard.views.remove-port'), name='dashboard.views.remove-port'),
url(r'^vm/(?P<pk>\d+)/$', VmDetailView.as_view(), url(r'^vm/(?P<pk>\d+)/$', VmDetailView.as_view(),
...@@ -111,8 +111,6 @@ urlpatterns = patterns( ...@@ -111,8 +111,6 @@ urlpatterns = patterns(
name="dashboard.views.delete-node"), name="dashboard.views.delete-node"),
url(r'^node/status/(?P<pk>\d+)/$', NodeStatus.as_view(), url(r'^node/status/(?P<pk>\d+)/$', NodeStatus.as_view(),
name="dashboard.views.status-node"), name="dashboard.views.status-node"),
url(r'^node/flush/(?P<pk>\d+)/$', NodeFlushView.as_view(),
name="dashboard.views.flush-node"),
url(r'^node/create/$', NodeCreate.as_view(), url(r'^node/create/$', NodeCreate.as_view(),
name='dashboard.views.node-create'), name='dashboard.views.node-create'),
...@@ -215,3 +213,21 @@ urlpatterns = patterns( ...@@ -215,3 +213,21 @@ urlpatterns = patterns(
url(r'^token-login/(?P<token>.*)/$', TokenLogin.as_view(), url(r'^token-login/(?P<token>.*)/$', TokenLogin.as_view(),
name="dashboard.views.token-login"), name="dashboard.views.token-login"),
) )
urlpatterns += patterns(
'',
*(url(r'^vm/(?P<pk>\d+)/op/%s/$' % op, v.as_view(), name=v.get_urlname())
for op, v in vm_ops.iteritems())
)
urlpatterns += patterns(
'',
*(url(r'^vm/mass_op/%s/$' % op, v.as_view(), name=v.get_urlname())
for op, v in vm_mass_ops.iteritems())
)
urlpatterns += patterns(
'',
*(url(r'^node/(?P<pk>\d+)/op/%s/$' % op, v.as_view(), name=v.get_urlname())
for op, v in node_ops.iteritems())
)
...@@ -17,6 +17,7 @@ ...@@ -17,6 +17,7 @@
from __future__ import unicode_literals, absolute_import from __future__ import unicode_literals, absolute_import
import json import json
from collections import OrderedDict
from django.conf import settings from django.conf import settings
from django.contrib import messages from django.contrib import messages
...@@ -37,7 +38,39 @@ from vm.models import Node, NodeActivity, Trait ...@@ -37,7 +38,39 @@ from vm.models import Node, NodeActivity, Trait
from ..forms import TraitForm, HostForm, NodeForm from ..forms import TraitForm, HostForm, NodeForm
from ..tables import NodeListTable from ..tables import NodeListTable
from .util import GraphMixin from .util import AjaxOperationMixin, OperationView, GraphMixin
def get_operations(instance, user):
ops = []
for k, v in node_ops.iteritems():
try:
op = v.get_op_by_object(instance)
op.check_auth(user)
op.check_precond()
except Exception:
ops.append(v.bind_to_object(instance, disabled=True))
else:
ops.append(v.bind_to_object(instance))
return ops
class NodeOperationView(AjaxOperationMixin, OperationView):
model = Node
context_object_name = 'node' # much simpler to mock object
node_ops = OrderedDict([
('activate', NodeOperationView.factory(
op='activate', icon='play-circle', effect='success')),
('passivate', NodeOperationView.factory(
op='passivate', icon='play-circle-o', effect='info')),
('disable', NodeOperationView.factory(
op='disable', icon='times-circle-o', effect='danger')),
('flush', NodeOperationView.factory(
op='flush', icon='paint-brush', effect='danger')),
])
class NodeDetailView(LoginRequiredMixin, SuperuserRequiredMixin, class NodeDetailView(LoginRequiredMixin, SuperuserRequiredMixin,
...@@ -54,6 +87,8 @@ class NodeDetailView(LoginRequiredMixin, SuperuserRequiredMixin, ...@@ -54,6 +87,8 @@ class NodeDetailView(LoginRequiredMixin, SuperuserRequiredMixin,
na = NodeActivity.objects.filter( na = NodeActivity.objects.filter(
node=self.object, parent=None node=self.object, parent=None
).order_by('-started').select_related() ).order_by('-started').select_related()
context['ops'] = get_operations(self.object, self.request.user)
context['op'] = {i.op: i for i in context['ops']}
context['activities'] = na context['activities'] = na
context['trait_form'] = form context['trait_form'] = form
context['graphite_enabled'] = ( context['graphite_enabled'] = (
...@@ -316,39 +351,3 @@ class NodeStatus(LoginRequiredMixin, SuperuserRequiredMixin, DetailView): ...@@ -316,39 +351,3 @@ class NodeStatus(LoginRequiredMixin, SuperuserRequiredMixin, DetailView):
else: else:
messages.success(request, success_message) messages.success(request, success_message)
return redirect(self.get_success_url()) return redirect(self.get_success_url())
class NodeFlushView(LoginRequiredMixin, SuperuserRequiredMixin, DetailView):
template_name = "dashboard/confirm/node-flush.html"
model = Node
def get_template_names(self):
if self.request.is_ajax():
return ['dashboard/confirm/ajax-node-flush.html']
else:
return ['dashboard/confirm/node-flush.html']
def get_success_url(self):
next = self.request.GET.get('next')
if next:
return next
else:
return reverse_lazy("dashboard.views.node-detail",
kwargs={'pk': self.object.pk})
def get_context_data(self, **kwargs):
context = super(NodeFlushView, self).get_context_data(**kwargs)
return context
def post(self, request, *args, **kwargs):
if request.POST.get('flush') is not None:
return self.__flush(request)
return redirect(reverse_lazy("dashboard.views.node-detail",
kwargs={'pk': self.get_object().pk}))
def __flush(self, request):
self.object = self.get_object()
self.object.flush.async(user=request.user)
success_message = _("Node successfully flushed.")
messages.success(request, success_message)
return redirect(self.get_success_url())
...@@ -395,7 +395,7 @@ class VmMigrateView(VmOperationView): ...@@ -395,7 +395,7 @@ class VmMigrateView(VmOperationView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
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.state == "ONLINE"] if n.online]
return ctx return ctx
def post(self, request, extra=None, *args, **kwargs): def post(self, request, extra=None, *args, **kwargs):
......
# 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 django.conf.urls import patterns, url
from ..views import vm_ops, vm_mass_ops
urlpatterns = patterns(
'',
*(url(r'^(?P<pk>\d+)/op/%s/$' % op, v.as_view(), name=v.get_urlname())
for op, v in vm_ops.iteritems())
)
urlpatterns += patterns(
'',
*(url(r'^mass_op/%s/$' % op, v.as_view(), name=v.get_urlname())
for op, v in vm_mass_ops.iteritems())
)
This source diff could not be displayed because it is too large. You can view the blob instead.
...@@ -6,7 +6,7 @@ msgid "" ...@@ -6,7 +6,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: \n" "Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2014-09-16 12:15+0200\n" "POT-Creation-Date: 2014-09-24 12:19+0200\n"
"PO-Revision-Date: 2014-09-03 12:51+0200\n" "PO-Revision-Date: 2014-09-03 12:51+0200\n"
"Last-Translator: Mate Ory <ory.mate@ik.bme.hu>\n" "Last-Translator: Mate Ory <ory.mate@ik.bme.hu>\n"
"Language-Team: Hungarian <cloud@ik.bme.hu>\n" "Language-Team: Hungarian <cloud@ik.bme.hu>\n"
...@@ -38,9 +38,9 @@ msgstr "" ...@@ -38,9 +38,9 @@ msgstr ""
msgid "Select an option to proceed!" msgid "Select an option to proceed!"
msgstr "Válasszon a folytatáshoz." msgstr "Válasszon a folytatáshoz."
#: dashboard/static/dashboard/dashboard.js:258 #: dashboard/static/dashboard/dashboard.js:259
#: dashboard/static/dashboard/dashboard.js:306 #: dashboard/static/dashboard/dashboard.js:307
#: dashboard/static/dashboard/dashboard.js:316 #: dashboard/static/dashboard/dashboard.js:317
#: static_collected/all.047675ebf594.js:3633 #: static_collected/all.047675ebf594.js:3633
#: static_collected/all.047675ebf594.js:3681 #: static_collected/all.047675ebf594.js:3681
#: static_collected/all.047675ebf594.js:3691 #: static_collected/all.047675ebf594.js:3691
......
...@@ -55,7 +55,7 @@ def select_node(instance, nodes): ...@@ -55,7 +55,7 @@ def select_node(instance, nodes):
''' '''
# check required traits # check required traits
nodes = [n for n in nodes nodes = [n for n in nodes
if n.enabled and n.online if n.schedule_enabled and n.online
and has_traits(instance.req_traits.all(), n)] and has_traits(instance.req_traits.all(), n)]
if not nodes: if not nodes:
logger.warning('select_node: no usable node for %s', unicode(instance)) logger.warning('select_node: no usable node for %s', unicode(instance))
......
# -*- coding: utf-8 -*-
from south.utils import datetime_utils as datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Adding field 'Node.schedule_enabled'
db.add_column(u'vm_node', 'schedule_enabled',
self.gf('django.db.models.fields.BooleanField')(default=False),
keep_default=False)
def backwards(self, orm):
# Deleting field 'Node.schedule_enabled'
db.delete_column(u'vm_node', 'schedule_enabled')
models = {
u'auth.group': {
'Meta': {'object_name': 'Group'},
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
},
u'auth.permission': {
'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
u'auth.user': {
'Meta': {'object_name': 'User'},
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Group']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Permission']"}),
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
},
u'contenttypes.contenttype': {
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
},
u'firewall.domain': {
'Meta': {'object_name': 'Domain'},
'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'modified_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '40'}),
'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}),
'ttl': ('django.db.models.fields.IntegerField', [], {'default': '600'})
},
u'firewall.group': {
'Meta': {'object_name': 'Group'},
'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'modified_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '20'}),
'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']", 'null': 'True', 'blank': 'True'})
},
u'firewall.host': {
'Meta': {'ordering': "('normalized_hostname', 'vlan')", 'unique_together': "(('hostname', 'vlan'),)", 'object_name': 'Host'},
'comment': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'external_ipv4': ('firewall.fields.IPAddressField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': u"orm['firewall.Group']", 'null': 'True', 'blank': 'True'}),
'hostname': ('django.db.models.fields.CharField', [], {'max_length': '40'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'ipv4': ('firewall.fields.IPAddressField', [], {'unique': 'True', 'max_length': '100'}),
'ipv6': ('firewall.fields.IPAddressField', [], {'max_length': '100', 'unique': 'True', 'null': 'True', 'blank': 'True'}),
'location': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'mac': ('firewall.fields.MACAddressField', [], {'unique': 'True', 'max_length': '17'}),
'modified_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
'normalized_hostname': ('common.models.HumanSortField', [], {'default': "''", 'maximum_number_length': '4', 'max_length': '80', 'monitor': "'hostname'", 'blank': 'True'}),
'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}),
'reverse': ('django.db.models.fields.CharField', [], {'max_length': '40', 'null': 'True', 'blank': 'True'}),
'shared_ip': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'vlan': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['firewall.Vlan']"})
},
u'firewall.vlan': {
'Meta': {'object_name': 'Vlan'},
'comment': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'dhcp_pool': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'domain': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['firewall.Domain']"}),
'host_ipv6_prefixlen': ('django.db.models.fields.IntegerField', [], {'default': '112'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'ipv6_template': ('django.db.models.fields.TextField', [], {'default': "'2001:738:2001:4031:%(b)d:%(c)d:%(d)d:0'"}),
'managed': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'modified_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '20'}),
'network4': ('firewall.fields.IPNetworkField', [], {'max_length': '100'}),
'network6': ('firewall.fields.IPNetworkField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}),
'network_type': ('django.db.models.fields.CharField', [], {'default': "'portforward'", 'max_length': '20'}),
'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']", 'null': 'True', 'blank': 'True'}),
'reverse_domain': ('django.db.models.fields.TextField', [], {'default': "'%(d)d.%(c)d.%(b)d.%(a)d.in-addr.arpa'"}),
'snat_ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39', 'null': 'True', 'blank': 'True'}),
'snat_to': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': u"orm['firewall.Vlan']", 'null': 'True', 'blank': 'True'}),
'vid': ('django.db.models.fields.IntegerField', [], {'unique': 'True'})
},
u'storage.datastore': {
'Meta': {'ordering': "[u'name']", 'object_name': 'DataStore'},
'hostname': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '40'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '100'}),
'path': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '200'})
},
u'storage.disk': {
'Meta': {'ordering': "[u'name']", 'object_name': 'Disk'},
'base': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "u'derivatives'", 'null': 'True', 'to': u"orm['storage.Disk']"}),
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
'datastore': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['storage.DataStore']"}),
'destroyed': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}),
'dev_num': ('django.db.models.fields.CharField', [], {'default': "u'a'", 'max_length': '1'}),
'filename': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '256'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_ready': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
'size': ('sizefield.models.FileSizeField', [], {'default': 'None', 'null': 'True'}),
'type': ('django.db.models.fields.CharField', [], {'max_length': '10'})
},
u'vm.instance': {
'Meta': {'ordering': "(u'pk',)", 'object_name': 'Instance'},
'access_method': ('django.db.models.fields.CharField', [], {'max_length': '10'}),
'arch': ('django.db.models.fields.CharField', [], {'max_length': '10'}),
'boot_menu': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'destroyed_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
'disks': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "u'instance_set'", 'symmetrical': 'False', 'to': u"orm['storage.Disk']"}),
'has_agent': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_base': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'lease': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['vm.Lease']"}),
'max_ram_size': ('django.db.models.fields.IntegerField', [], {}),
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
'node': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "u'instance_set'", 'null': 'True', 'to': u"orm['vm.Node']"}),
'num_cores': ('django.db.models.fields.IntegerField', [], {}),
'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}),
'priority': ('django.db.models.fields.IntegerField', [], {}),
'pw': ('django.db.models.fields.CharField', [], {'max_length': '20'}),
'ram_size': ('django.db.models.fields.IntegerField', [], {}),
'raw_data': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'req_traits': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['vm.Trait']", 'symmetrical': 'False', 'blank': 'True'}),
'status': ('model_utils.fields.StatusField', [], {'default': "u'NOSTATE'", 'max_length': '100', u'no_check_for_status': 'True'}),
'status_changed': ('model_utils.fields.MonitorField', [], {'default': 'datetime.datetime.now', u'monitor': "u'status'"}),
'system': ('django.db.models.fields.TextField', [], {}),
'template': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "u'instance_set'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['vm.InstanceTemplate']"}),
'time_of_delete': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}),
'time_of_suspend': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}),
'vnc_port': ('django.db.models.fields.IntegerField', [], {'default': 'None', 'unique': 'True', 'null': 'True', 'blank': 'True'})
},
u'vm.instanceactivity': {
'Meta': {'ordering': "[u'-finished', u'-started', u'instance', u'-id']", 'object_name': 'InstanceActivity'},
'activity_code': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
'finished': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'instance': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "u'activity_log'", 'to': u"orm['vm.Instance']"}),
'interruptible': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': u"orm['vm.InstanceActivity']"}),
'readable_name_data': ('jsonfield.fields.JSONField', [], {'null': 'True', 'blank': 'True'}),
'result_data': ('jsonfield.fields.JSONField', [], {'null': 'True', 'blank': 'True'}),
'resultant_state': ('django.db.models.fields.CharField', [], {'max_length': '20', 'null': 'True', 'blank': 'True'}),
'started': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
'succeeded': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}),
'task_uuid': ('django.db.models.fields.CharField', [], {'max_length': '50', 'unique': 'True', 'null': 'True', 'blank': 'True'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']", 'null': 'True', 'blank': 'True'})
},
u'vm.instancetemplate': {
'Meta': {'ordering': "(u'name',)", 'object_name': 'InstanceTemplate'},
'access_method': ('django.db.models.fields.CharField', [], {'max_length': '10'}),
'arch': ('django.db.models.fields.CharField', [], {'max_length': '10'}),
'boot_menu': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'disks': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "u'template_set'", 'symmetrical': 'False', 'to': u"orm['storage.Disk']"}),
'has_agent': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'lease': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['vm.Lease']"}),
'max_ram_size': ('django.db.models.fields.IntegerField', [], {}),
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'num_cores': ('django.db.models.fields.IntegerField', [], {}),
'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}),
'parent': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['vm.InstanceTemplate']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
'priority': ('django.db.models.fields.IntegerField', [], {}),
'ram_size': ('django.db.models.fields.IntegerField', [], {}),
'raw_data': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'req_traits': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['vm.Trait']", 'symmetrical': 'False', 'blank': 'True'}),
'system': ('django.db.models.fields.TextField', [], {})
},
u'vm.interface': {
'Meta': {'ordering': "(u'-vlan__managed',)", 'object_name': 'Interface'},
'host': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['firewall.Host']", 'null': 'True', 'blank': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'instance': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "u'interface_set'", 'to': u"orm['vm.Instance']"}),
'vlan': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "u'vm_interface'", 'to': u"orm['firewall.Vlan']"})
},
u'vm.interfacetemplate': {
'Meta': {'object_name': 'InterfaceTemplate'},
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'managed': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'template': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "u'interface_set'", 'to': u"orm['vm.InstanceTemplate']"}),
'vlan': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['firewall.Vlan']"})
},
u'vm.lease': {
'Meta': {'ordering': "[u'name']", 'object_name': 'Lease'},
'delete_interval_seconds': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '100'}),
'suspend_interval_seconds': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'})
},
u'vm.namedbaseresourceconfig': {
'Meta': {'object_name': 'NamedBaseResourceConfig'},
'arch': ('django.db.models.fields.CharField', [], {'max_length': '10'}),
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'max_ram_size': ('django.db.models.fields.IntegerField', [], {}),
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '50'}),
'num_cores': ('django.db.models.fields.IntegerField', [], {}),
'priority': ('django.db.models.fields.IntegerField', [], {}),
'ram_size': ('django.db.models.fields.IntegerField', [], {})
},
u'vm.node': {
'Meta': {'ordering': "(u'-enabled', u'normalized_name')", 'object_name': 'Node'},
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'host': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['firewall.Host']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '50'}),
'normalized_name': ('common.models.HumanSortField', [], {'default': "''", 'maximum_number_length': '4', 'max_length': '100', 'monitor': "u'name'", 'blank': 'True'}),
'overcommit': ('django.db.models.fields.FloatField', [], {'default': '1.0'}),
'priority': ('django.db.models.fields.IntegerField', [], {}),
'schedule_enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'traits': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['vm.Trait']", 'symmetrical': 'False', 'blank': 'True'})
},
u'vm.nodeactivity': {
'Meta': {'object_name': 'NodeActivity'},
'activity_code': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
'finished': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
'node': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "u'activity_log'", 'to': u"orm['vm.Node']"}),
'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': u"orm['vm.NodeActivity']"}),
'readable_name_data': ('jsonfield.fields.JSONField', [], {'null': 'True', 'blank': 'True'}),
'result_data': ('jsonfield.fields.JSONField', [], {'null': 'True', 'blank': 'True'}),
'started': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
'succeeded': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}),
'task_uuid': ('django.db.models.fields.CharField', [], {'max_length': '50', 'unique': 'True', 'null': 'True', 'blank': 'True'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']", 'null': 'True', 'blank': 'True'})
},
u'vm.trait': {
'Meta': {'object_name': 'Trait'},
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
}
}
complete_apps = ['vm']
\ No newline at end of file
...@@ -27,7 +27,7 @@ from django.db.models import ( ...@@ -27,7 +27,7 @@ from django.db.models import (
FloatField, permalink, Sum FloatField, permalink, Sum
) )
from django.utils import timezone from django.utils import timezone
from django.utils.translation import ugettext_lazy as _, ugettext_noop from django.utils.translation import ugettext_lazy as _
from celery.exceptions import TimeoutError from celery.exceptions import TimeoutError
from model_utils.models import TimeStampedModel from model_utils.models import TimeStampedModel
...@@ -37,7 +37,7 @@ from common.models import method_cache, WorkerNotFound, HumanSortField ...@@ -37,7 +37,7 @@ from common.models import method_cache, WorkerNotFound, HumanSortField
from common.operations import OperatedMixin from common.operations import OperatedMixin
from firewall.models import Host from firewall.models import Host
from ..tasks import vm_tasks from ..tasks import vm_tasks
from .activity import node_activity, NodeActivity from .activity import NodeActivity
from .common import Trait from .common import Trait
...@@ -72,6 +72,11 @@ class Node(OperatedMixin, TimeStampedModel): ...@@ -72,6 +72,11 @@ class Node(OperatedMixin, TimeStampedModel):
enabled = BooleanField(verbose_name=_('enabled'), default=False, enabled = BooleanField(verbose_name=_('enabled'), default=False,
help_text=_('Indicates whether the node can ' help_text=_('Indicates whether the node can '
'be used for hosting.')) 'be used for hosting.'))
schedule_enabled = BooleanField(verbose_name=_('schedule enabled'),
default=False, help_text=_(
'Indicates whether a vm can be '
'automatically scheduled to this '
'node.'))
traits = ManyToManyField(Trait, blank=True, traits = ManyToManyField(Trait, blank=True,
help_text=_("Declared traits."), help_text=_("Declared traits."),
verbose_name=_('traits')) verbose_name=_('traits'))
...@@ -130,46 +135,30 @@ class Node(OperatedMixin, TimeStampedModel): ...@@ -130,46 +135,30 @@ class Node(OperatedMixin, TimeStampedModel):
warn('Use Node.info["core_num"]', DeprecationWarning) warn('Use Node.info["core_num"]', DeprecationWarning)
return self.info['core_num'] return self.info['core_num']
STATES = {False: {False: ('OFFLINE', _('offline')), STATES = {None: ({True: ('MISSING', _('missing')),
True: ('DISABLED', _('disabled'))}, False: ('OFFLINE', _('offline'))}),
True: {False: ('MISSING', _('missing')), False: {False: ('DISABLED', _('disabled'))},
True: ('ONLINE', _('online'))}} True: {False: ('PASSIVE', _('passive')),
True: ('ACTIVE', _('active'))}}
def get_state(self): def _get_state(self):
"""The state combined of online and enabled attributes. """The state tuple based on online and enabled attributes.
""" """
return self.STATES[self.enabled][self.online][0] if self.online:
return self.STATES[self.enabled][self.schedule_enabled]
state = property(get_state) else:
return self.STATES[None][self.enabled]
def get_status_display(self): def get_status_display(self):
return self.STATES[self.enabled][self.online][1] return self._get_state()[1]
def disable(self, user=None, base_activity=None): def get_state(self):
''' Disable the node.''' return self._get_state()[0]
if self.enabled:
if base_activity: state = property(get_state)
act_ctx = base_activity.sub_activity(
'disable', readable_name=ugettext_noop("disable node"))
else:
act_ctx = node_activity(
'disable', node=self, user=user,
readable_name=ugettext_noop("disable node"))
with act_ctx:
self.enabled = False
self.save()
def enable(self, user=None, base_activity=None): def enable(self, user=None, base_activity=None):
''' Enable the node. ''' raise NotImplementedError("Use activate or passivate instead.")
if self.enabled is not True:
if base_activity:
act_ctx = base_activity.sub_activity('enable')
else:
act_ctx = node_activity('enable', node=self, user=user)
with act_ctx:
self.enabled = True
self.save()
self.get_info(invalidate_cache=True)
@property @property
@node_available @node_available
...@@ -314,10 +303,11 @@ class Node(OperatedMixin, TimeStampedModel): ...@@ -314,10 +303,11 @@ class Node(OperatedMixin, TimeStampedModel):
def get_status_icon(self): def get_status_icon(self):
return { return {
'OFFLINE': 'fa-minus-circle', 'DISABLED': 'fa-times-circle-o',
'DISABLED': 'fa-moon-o', 'OFFLINE': 'fa-times-circle',
'MISSING': 'fa-warning', 'MISSING': 'fa-warning',
'ONLINE': 'fa-play-circle'}.get(self.get_state(), 'PASSIVE': 'fa-play-circle-o',
'ACTIVE': 'fa-play-circle'}.get(self.get_state(),
'fa-question-circle') 'fa-question-circle')
def get_status_label(self): def get_status_label(self):
...@@ -385,6 +375,11 @@ class Node(OperatedMixin, TimeStampedModel): ...@@ -385,6 +375,11 @@ class Node(OperatedMixin, TimeStampedModel):
def get_absolute_url(self): def get_absolute_url(self):
return ('dashboard.views.node-detail', None, {'pk': self.id}) return ('dashboard.views.node-detail', None, {'pk': self.id})
def save(self, *args, **kwargs):
if not self.enabled:
self.schedule_enabled = False
super(Node, self).save(*args, **kwargs)
@property @property
def metric_prefix(self): def metric_prefix(self):
return 'circle.%s' % self.host.hostname return 'circle.%s' % self.host.hostname
...@@ -116,6 +116,7 @@ class InstanceOperation(Operation): ...@@ -116,6 +116,7 @@ class InstanceOperation(Operation):
return False return False
@register_operation
class AddInterfaceOperation(InstanceOperation): class AddInterfaceOperation(InstanceOperation):
activity_code_suffix = 'add_interface' activity_code_suffix = 'add_interface'
id = 'add_interface' id = 'add_interface'
...@@ -161,9 +162,7 @@ class AddInterfaceOperation(InstanceOperation): ...@@ -161,9 +162,7 @@ class AddInterfaceOperation(InstanceOperation):
vlan=kwargs['vlan']) vlan=kwargs['vlan'])
register_operation(AddInterfaceOperation) @register_operation
class CreateDiskOperation(InstanceOperation): class CreateDiskOperation(InstanceOperation):
activity_code_suffix = 'create_disk' activity_code_suffix = 'create_disk'
...@@ -205,9 +204,7 @@ class CreateDiskOperation(InstanceOperation): ...@@ -205,9 +204,7 @@ class CreateDiskOperation(InstanceOperation):
size=filesizeformat(kwargs['size']), name=kwargs['name']) size=filesizeformat(kwargs['size']), name=kwargs['name'])
register_operation(CreateDiskOperation) @register_operation
class DownloadDiskOperation(InstanceOperation): class DownloadDiskOperation(InstanceOperation):
activity_code_suffix = 'download_disk' activity_code_suffix = 'download_disk'
id = 'download_disk' id = 'download_disk'
...@@ -245,9 +242,8 @@ class DownloadDiskOperation(InstanceOperation): ...@@ -245,9 +242,8 @@ class DownloadDiskOperation(InstanceOperation):
): ):
self.instance.attach_disk(disk) self.instance.attach_disk(disk)
register_operation(DownloadDiskOperation)
@register_operation
class DeployOperation(InstanceOperation): class DeployOperation(InstanceOperation):
activity_code_suffix = 'deploy' activity_code_suffix = 'deploy'
id = 'deploy' id = 'deploy'
...@@ -315,9 +311,7 @@ class DeployOperation(InstanceOperation): ...@@ -315,9 +311,7 @@ class DeployOperation(InstanceOperation):
"wait operating system loading"), interruptible=True) "wait operating system loading"), interruptible=True)
register_operation(DeployOperation) @register_operation
class DestroyOperation(InstanceOperation): class DestroyOperation(InstanceOperation):
activity_code_suffix = 'destroy' activity_code_suffix = 'destroy'
id = 'destroy' id = 'destroy'
...@@ -363,9 +357,7 @@ class DestroyOperation(InstanceOperation): ...@@ -363,9 +357,7 @@ class DestroyOperation(InstanceOperation):
self.instance.save() self.instance.save()
register_operation(DestroyOperation) @register_operation
class MigrateOperation(InstanceOperation): class MigrateOperation(InstanceOperation):
activity_code_suffix = 'migrate' activity_code_suffix = 'migrate'
id = 'migrate' id = 'migrate'
...@@ -417,9 +409,7 @@ class MigrateOperation(InstanceOperation): ...@@ -417,9 +409,7 @@ class MigrateOperation(InstanceOperation):
self.instance.deploy_net() self.instance.deploy_net()
register_operation(MigrateOperation) @register_operation
class RebootOperation(InstanceOperation): class RebootOperation(InstanceOperation):
activity_code_suffix = 'reboot' activity_code_suffix = 'reboot'
id = 'reboot' id = 'reboot'
...@@ -436,9 +426,7 @@ class RebootOperation(InstanceOperation): ...@@ -436,9 +426,7 @@ class RebootOperation(InstanceOperation):
"wait operating system loading"), interruptible=True) "wait operating system loading"), interruptible=True)
register_operation(RebootOperation) @register_operation
class RemoveInterfaceOperation(InstanceOperation): class RemoveInterfaceOperation(InstanceOperation):
activity_code_suffix = 'remove_interface' activity_code_suffix = 'remove_interface'
id = 'remove_interface' id = 'remove_interface'
...@@ -466,9 +454,7 @@ class RemoveInterfaceOperation(InstanceOperation): ...@@ -466,9 +454,7 @@ class RemoveInterfaceOperation(InstanceOperation):
vlan=kwargs['interface'].vlan) vlan=kwargs['interface'].vlan)
register_operation(RemoveInterfaceOperation) @register_operation
class RemoveDiskOperation(InstanceOperation): class RemoveDiskOperation(InstanceOperation):
activity_code_suffix = 'remove_disk' activity_code_suffix = 'remove_disk'
id = 'remove_disk' id = 'remove_disk'
...@@ -495,9 +481,8 @@ class RemoveDiskOperation(InstanceOperation): ...@@ -495,9 +481,8 @@ class RemoveDiskOperation(InstanceOperation):
return create_readable(ugettext_noop('remove disk %(name)s'), return create_readable(ugettext_noop('remove disk %(name)s'),
name=kwargs["disk"].name) name=kwargs["disk"].name)
register_operation(RemoveDiskOperation)
@register_operation
class ResetOperation(InstanceOperation): class ResetOperation(InstanceOperation):
activity_code_suffix = 'reset' activity_code_suffix = 'reset'
id = 'reset' id = 'reset'
...@@ -512,9 +497,8 @@ class ResetOperation(InstanceOperation): ...@@ -512,9 +497,8 @@ class ResetOperation(InstanceOperation):
activity.sub_activity('os_boot', readable_name=ugettext_noop( activity.sub_activity('os_boot', readable_name=ugettext_noop(
"wait operating system loading"), interruptible=True) "wait operating system loading"), interruptible=True)
register_operation(ResetOperation)
@register_operation
class SaveAsTemplateOperation(InstanceOperation): class SaveAsTemplateOperation(InstanceOperation):
activity_code_suffix = 'save_as_template' activity_code_suffix = 'save_as_template'
id = 'save_as_template' id = 'save_as_template'
...@@ -610,9 +594,7 @@ class SaveAsTemplateOperation(InstanceOperation): ...@@ -610,9 +594,7 @@ class SaveAsTemplateOperation(InstanceOperation):
return tmpl return tmpl
register_operation(SaveAsTemplateOperation) @register_operation
class ShutdownOperation(InstanceOperation): class ShutdownOperation(InstanceOperation):
activity_code_suffix = 'shutdown' activity_code_suffix = 'shutdown'
id = 'shutdown' id = 'shutdown'
...@@ -642,9 +624,7 @@ class ShutdownOperation(InstanceOperation): ...@@ -642,9 +624,7 @@ class ShutdownOperation(InstanceOperation):
super(ShutdownOperation, self).on_abort(activity, error) super(ShutdownOperation, self).on_abort(activity, error)
register_operation(ShutdownOperation) @register_operation
class ShutOffOperation(InstanceOperation): class ShutOffOperation(InstanceOperation):
activity_code_suffix = 'shut_off' activity_code_suffix = 'shut_off'
id = 'shut_off' id = 'shut_off'
...@@ -672,9 +652,7 @@ class ShutOffOperation(InstanceOperation): ...@@ -672,9 +652,7 @@ class ShutOffOperation(InstanceOperation):
self.instance.yield_node() self.instance.yield_node()
register_operation(ShutOffOperation) @register_operation
class SleepOperation(InstanceOperation): class SleepOperation(InstanceOperation):
activity_code_suffix = 'sleep' activity_code_suffix = 'sleep'
id = 'sleep' id = 'sleep'
...@@ -718,9 +696,7 @@ class SleepOperation(InstanceOperation): ...@@ -718,9 +696,7 @@ class SleepOperation(InstanceOperation):
# VNC port needs to be kept # VNC port needs to be kept
register_operation(SleepOperation) @register_operation
class WakeUpOperation(InstanceOperation): class WakeUpOperation(InstanceOperation):
activity_code_suffix = 'wake_up' activity_code_suffix = 'wake_up'
id = 'wake_up' id = 'wake_up'
...@@ -764,9 +740,7 @@ class WakeUpOperation(InstanceOperation): ...@@ -764,9 +740,7 @@ class WakeUpOperation(InstanceOperation):
pass pass
register_operation(WakeUpOperation) @register_operation
class RenewOperation(InstanceOperation): class RenewOperation(InstanceOperation):
activity_code_suffix = 'renew' activity_code_suffix = 'renew'
id = 'renew' id = 'renew'
...@@ -801,9 +775,7 @@ class RenewOperation(InstanceOperation): ...@@ -801,9 +775,7 @@ class RenewOperation(InstanceOperation):
suspend=suspend, delete=delete) suspend=suspend, delete=delete)
register_operation(RenewOperation) @register_operation
class ChangeStateOperation(InstanceOperation): class ChangeStateOperation(InstanceOperation):
activity_code_suffix = 'emergency_change_state' activity_code_suffix = 'emergency_change_state'
id = 'emergency_change_state' id = 'emergency_change_state'
...@@ -828,17 +800,23 @@ class ChangeStateOperation(InstanceOperation): ...@@ -828,17 +800,23 @@ class ChangeStateOperation(InstanceOperation):
logger.error('Forced finishing activity %s', i) logger.error('Forced finishing activity %s', i)
register_operation(ChangeStateOperation)
class NodeOperation(Operation): class NodeOperation(Operation):
async_operation = abortable_async_node_operation async_operation = abortable_async_node_operation
host_cls = Node host_cls = Node
online_required = True
superuser_required = True
def __init__(self, node): def __init__(self, node):
super(NodeOperation, self).__init__(subject=node) super(NodeOperation, self).__init__(subject=node)
self.node = node self.node = node
def check_precond(self):
super(NodeOperation, self).check_precond()
if self.online_required and not self.node.online:
raise humanize_exception(ugettext_noop(
"You cannot call this operation on an offline node."),
Exception())
def create_activity(self, parent, user, kwargs): def create_activity(self, parent, user, kwargs):
name = self.get_activity_name(kwargs) name = self.get_activity_name(kwargs)
if parent: if parent:
...@@ -859,24 +837,19 @@ class NodeOperation(Operation): ...@@ -859,24 +837,19 @@ class NodeOperation(Operation):
readable_name=name) readable_name=name)
@register_operation
class FlushOperation(NodeOperation): class FlushOperation(NodeOperation):
activity_code_suffix = 'flush' activity_code_suffix = 'flush'
id = 'flush' id = 'flush'
name = _("flush") name = _("flush")
description = _("Disable node and move all instances to other ones.") description = _("Passivate node and move all instances to other ones.")
required_perms = () required_perms = ()
superuser_required = True
async_queue = "localhost.man.slow" async_queue = "localhost.man.slow"
def on_abort(self, activity, error):
from manager.scheduler import TraitsUnsatisfiableException
if isinstance(error, TraitsUnsatisfiableException):
if self.node_enabled:
self.node.enable(activity.user, activity)
def _operation(self, activity, user): def _operation(self, activity, user):
self.node_enabled = self.node.enabled if self.node.schedule_enabled:
self.node.disable(user, activity) PassivateOperation(self.node).call(parent_activity=activity,
user=user)
for i in self.node.instance_set.all(): for i in self.node.instance_set.all():
name = create_readable(ugettext_noop( name = create_readable(ugettext_noop(
"migrate %(instance)s (%(pk)s)"), instance=i.name, pk=i.pk) "migrate %(instance)s (%(pk)s)"), instance=i.name, pk=i.pk)
...@@ -885,9 +858,75 @@ class FlushOperation(NodeOperation): ...@@ -885,9 +858,75 @@ class FlushOperation(NodeOperation):
i.migrate(user=user) i.migrate(user=user)
register_operation(FlushOperation) @register_operation
class ActivateOperation(NodeOperation):
activity_code_suffix = 'activate'
id = 'activate'
name = _("activate")
description = _("Make node active, i.e. scheduler is allowed to deploy "
"virtual machines to it.")
required_perms = ()
def check_precond(self):
super(ActivateOperation, self).check_precond()
if self.node.enabled and self.node.schedule_enabled:
raise humanize_exception(ugettext_noop(
"You cannot activate an active node."), Exception())
def _operation(self):
self.node.enabled = True
self.node.schedule_enabled = True
self.node.save()
@register_operation
class PassivateOperation(NodeOperation):
activity_code_suffix = 'passivate'
id = 'passivate'
name = _("passivate")
description = _("Make node passive, i.e. scheduler is denied to deploy "
"virtual machines to it, but remaining instances and "
"the ones manually migrated will continue running.")
required_perms = ()
def check_precond(self):
if self.node.enabled and not self.node.schedule_enabled:
raise humanize_exception(ugettext_noop(
"You cannot passivate a passive node."), Exception())
super(PassivateOperation, self).check_precond()
def _operation(self):
self.node.enabled = True
self.node.schedule_enabled = False
self.node.save()
@register_operation
class DisableOperation(NodeOperation):
activity_code_suffix = 'disable'
id = 'disable'
name = _("disable")
description = _("Disable node.")
required_perms = ()
online_required = False
def check_precond(self):
if not self.node.enabled:
raise humanize_exception(ugettext_noop(
"You cannot disable a disabled node."), Exception())
if self.node.instance_set.exists():
raise humanize_exception(ugettext_noop(
"You cannot disable a node which is hosting instances."),
Exception())
super(DisableOperation, self).check_precond()
def _operation(self):
self.node.enabled = False
self.node.schedule_enabled = False
self.node.save()
@register_operation
class ScreenshotOperation(InstanceOperation): class ScreenshotOperation(InstanceOperation):
activity_code_suffix = 'screenshot' activity_code_suffix = 'screenshot'
id = 'screenshot' id = 'screenshot'
...@@ -903,9 +942,7 @@ class ScreenshotOperation(InstanceOperation): ...@@ -903,9 +942,7 @@ class ScreenshotOperation(InstanceOperation):
return self.instance.get_screenshot(timeout=20) return self.instance.get_screenshot(timeout=20)
register_operation(ScreenshotOperation) @register_operation
class RecoverOperation(InstanceOperation): class RecoverOperation(InstanceOperation):
activity_code_suffix = 'recover' activity_code_suffix = 'recover'
id = 'recover' id = 'recover'
...@@ -933,9 +970,7 @@ class RecoverOperation(InstanceOperation): ...@@ -933,9 +970,7 @@ class RecoverOperation(InstanceOperation):
self.instance.save() self.instance.save()
register_operation(RecoverOperation) @register_operation
class ResourcesOperation(InstanceOperation): class ResourcesOperation(InstanceOperation):
activity_code_suffix = 'Resources change' activity_code_suffix = 'Resources change'
id = 'resources_change' id = 'resources_change'
...@@ -963,9 +998,6 @@ class ResourcesOperation(InstanceOperation): ...@@ -963,9 +998,6 @@ class ResourcesOperation(InstanceOperation):
) )
register_operation(ResourcesOperation)
class EnsureAgentMixin(object): class EnsureAgentMixin(object):
accept_states = ('RUNNING', ) accept_states = ('RUNNING', )
...@@ -985,6 +1017,7 @@ class EnsureAgentMixin(object): ...@@ -985,6 +1017,7 @@ class EnsureAgentMixin(object):
raise self.instance.NoAgentError(self.instance) raise self.instance.NoAgentError(self.instance)
@register_operation
class PasswordResetOperation(EnsureAgentMixin, InstanceOperation): class PasswordResetOperation(EnsureAgentMixin, InstanceOperation):
activity_code_suffix = 'password_reset' activity_code_suffix = 'password_reset'
id = 'password_reset' id = 'password_reset'
...@@ -1005,9 +1038,7 @@ class PasswordResetOperation(EnsureAgentMixin, InstanceOperation): ...@@ -1005,9 +1038,7 @@ class PasswordResetOperation(EnsureAgentMixin, InstanceOperation):
self.instance.save() self.instance.save()
register_operation(PasswordResetOperation) @register_operation
class MountStoreOperation(EnsureAgentMixin, InstanceOperation): class MountStoreOperation(EnsureAgentMixin, InstanceOperation):
activity_code_suffix = 'mount_store' activity_code_suffix = 'mount_store'
id = 'mount_store' id = 'mount_store'
...@@ -1034,6 +1065,3 @@ class MountStoreOperation(EnsureAgentMixin, InstanceOperation): ...@@ -1034,6 +1065,3 @@ class MountStoreOperation(EnsureAgentMixin, InstanceOperation):
password = user.profile.smb_password password = user.profile.smb_password
agent_tasks.mount_store.apply_async( agent_tasks.mount_store.apply_async(
queue=queue, args=(inst.vm_name, host, username, password)) queue=queue, args=(inst.vm_name, host, username, password))
register_operation(MountStoreOperation)
...@@ -208,8 +208,10 @@ class NodeTestCase(TestCase): ...@@ -208,8 +208,10 @@ class NodeTestCase(TestCase):
node = Mock(spec=Node) node = Mock(spec=Node)
node.online = True node.online = True
node.enabled = True node.enabled = True
node.schedule_enabled = True
node.STATES = Node.STATES node.STATES = Node.STATES
self.assertEqual(Node.get_state(node), "ONLINE") node._get_state = lambda: Node._get_state(node)
self.assertEqual(Node.get_state(node), "ACTIVE")
assert isinstance(Node.get_status_display(node), _("x").__class__) assert isinstance(Node.get_status_display(node), _("x").__class__)
...@@ -351,51 +353,30 @@ class InstanceActivityTestCase(TestCase): ...@@ -351,51 +353,30 @@ class InstanceActivityTestCase(TestCase):
self.assertTrue(InstanceActivity.is_abortable_for(iaobj, su)) self.assertTrue(InstanceActivity.is_abortable_for(iaobj, su))
def test_disable_enabled(self): def test_disable_enabled(self):
node = MagicMock(spec=Node, enabled=True) node = MagicMock(spec=Node, enabled=True, online=True)
with patch('vm.models.node.node_activity') as nac: node.instance_set.exists.return_value = False
na = MagicMock() Node._ops['disable'](node).check_precond()
nac.return_value = na
na.__enter__.return_value = MagicMock()
Node.disable(node)
self.assertFalse(node.enabled)
node.save.assert_called_once()
na.assert_called()
def test_disable_disabled(self): def test_disable_disabled(self):
node = MagicMock(spec=Node, enabled=False) node = MagicMock(spec=Node, enabled=False)
with patch('vm.models.node.node_activity') as nac: with self.assertRaises(Exception):
na = MagicMock() Node._ops['disable'](node).check_precond()
na.__enter__.side_effect = AssertionError
nac.return_value = na
Node.disable(node)
self.assertFalse(node.enabled)
def test_disable_enabled_sub(self):
node = MagicMock(spec=Node, enabled=True)
act = MagicMock()
subact = MagicMock()
act.sub_activity.return_value = subact
Node.disable(node, base_activity=act)
self.assertFalse(node.enabled)
subact.__enter__.assert_called()
def test_flush(self): def test_flush(self):
insts = [MagicMock(spec=Instance, migrate=MagicMock()), insts = [MagicMock(spec=Instance, migrate=MagicMock()),
MagicMock(spec=Instance, migrate=MagicMock())] MagicMock(spec=Instance, migrate=MagicMock())]
insts[0].name = insts[1].name = "x" insts[0].name = insts[1].name = "x"
node = MagicMock(spec=Node, enabled=True) node = MagicMock(spec=Node, enabled=True, schedule_enabled=True)
node.instance_set.all.return_value = insts node.instance_set.all.return_value = insts
user = MagicMock(spec=User) user = MagicMock(spec=User)
user.is_superuser = MagicMock(return_value=True) user.is_superuser = MagicMock(return_value=True)
flush_op = FlushOperation(node) with patch.object(FlushOperation, 'create_activity') as create_act, \
patch.object(
with patch.object(FlushOperation, 'create_activity') as create_act: Node._ops['passivate'], 'create_activity') as create_act2:
act = create_act.return_value = MagicMock() FlushOperation(node)(user=user)
node.schedule_enabled = True
flush_op(user=user)
create_act.assert_called() create_act.assert_called()
node.disable.assert_called_with(user, act) create_act2.assert_called()
for i in insts: for i in insts:
i.migrate.assert_called() i.migrate.assert_called()
user.is_superuser.assert_called() user.is_superuser.assert_called()
...@@ -404,17 +385,13 @@ class InstanceActivityTestCase(TestCase): ...@@ -404,17 +385,13 @@ class InstanceActivityTestCase(TestCase):
insts = [MagicMock(spec=Instance, migrate=MagicMock()), insts = [MagicMock(spec=Instance, migrate=MagicMock()),
MagicMock(spec=Instance, migrate=MagicMock())] MagicMock(spec=Instance, migrate=MagicMock())]
insts[0].name = insts[1].name = "x" insts[0].name = insts[1].name = "x"
node = MagicMock(spec=Node, enabled=False) node = MagicMock(spec=Node, enabled=False, schedule_enabled=False)
node.instance_set.all.return_value = insts node.instance_set.all.return_value = insts
flush_op = FlushOperation(node) flush_op = FlushOperation(node)
with patch.object(FlushOperation, 'create_activity') as create_act: with patch.object(FlushOperation, 'create_activity') as create_act:
act = create_act.return_value = MagicMock() create_act.return_value = MagicMock()
flush_op(system=True) flush_op(system=True)
create_act.assert_called() create_act.assert_called()
node.disable.assert_called_with(None, act)
# ^ should be called, but real method no-ops if disabled
for i in insts: for i in insts:
i.migrate.assert_called() i.migrate.assert_called()
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