Commit 23530b0b by Szabolcs Gelencser

Merge usernetworks

parents 132dd4a5 cc5b0edb
# 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.core.exceptions import PermissionDenied
class CheckedObjectMixin(object):
read_level = 'user'
def get_object(self, **kwargs):
obj = super(CheckedObjectMixin, self).get_object()
if not obj.has_level(self.request.user, self.read_level):
raise PermissionDenied()
return obj
...@@ -22,6 +22,7 @@ ...@@ -22,6 +22,7 @@
"favico.js": "~0.3.5", "favico.js": "~0.3.5",
"datatables": "~1.10.4", "datatables": "~1.10.4",
"chart.js": "2.3.0", "chart.js": "2.3.0",
"clipboard": "~1.6.1" "clipboard": "~1.6.1",
"jsPlumb": "2.5.7"
} }
} }
No preview for this file type
...@@ -548,4 +548,8 @@ POLICY_FILES = { ...@@ -548,4 +548,8 @@ POLICY_FILES = {
POLICY_DIRS = { POLICY_DIRS = {
'compute': ['nova_policy.d'], 'compute': ['nova_policy.d'],
'volume': ['cinder_policy.d'], 'volume': ['cinder_policy.d'],
} }
\ No newline at end of file
DEFAULT_USERNET_VLAN_NAME = (
get_env_variable("DEFAULT_USERNET_VLAN_NAME", "usernet"))
USERNET_MAX = 2 ** 12
...@@ -110,6 +110,7 @@ if DEBUG: ...@@ -110,6 +110,7 @@ if DEBUG:
PIPELINE["COMPILERS"] = ( PIPELINE["COMPILERS"] = (
'dashboard.compilers.DummyLessCompiler', 'dashboard.compilers.DummyLessCompiler',
'pipeline.compilers.es6.ES6Compiler',
) )
ADMIN_ENABLED = True ADMIN_ENABLED = True
......
# Copyright 2017 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/>.
""" Static files and pipeline configuration. """
# flake8: noqa
from os.path import abspath, dirname, join, normpath, isfile, exists
from util import get_env_variable
########## PATH CONFIGURATION
# Absolute filesystem path to the Django project directory:
BASE_DIR = dirname(dirname(abspath(__file__)))
# Absolute filesystem path to the top-level project folder:
SITE_ROOT = dirname(BASE_DIR)
########## MEDIA CONFIGURATION
# See: https://docs.djangoproject.com/en/dev/ref/settings/#media-root
MEDIA_ROOT = normpath(join(SITE_ROOT, 'media'))
# See: https://docs.djangoproject.com/en/dev/ref/settings/#media-url
MEDIA_URL = get_env_variable('DJANGO_MEDIA_URL', default='/media/')
########## END MEDIA CONFIGURATION
########## STATIC FILE CONFIGURATION
# See: https://docs.djangoproject.com/en/dev/ref/settings/#static-root
STATIC_ROOT = normpath(join(SITE_ROOT, 'static_collected'))
# See: https://docs.djangoproject.com/en/dev/ref/settings/#static-url
STATIC_URL = get_env_variable('DJANGO_STATIC_URL', default='/static/')
# See: https://docs.djangoproject.com/en/dev/ref/contrib/staticfiles/#staticfiles-finders
STATICFILES_FINDERS = (
'django.contrib.staticfiles.finders.FileSystemFinder',
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
'pipeline.finders.PipelineFinder',
)
########## END STATIC FILE CONFIGURATION
STATICFILES_DIRS = [normpath(join(SITE_ROOT, 'bower_components'))]
p = normpath(join(SITE_ROOT, '../../site-circle/static'))
if exists(p):
STATICFILES_DIRS.append(p)
STATICFILES_STORAGE = 'pipeline.storage.PipelineStorage'
PIPELINE = {
'COMPILERS' : ('pipeline.compilers.less.LessCompiler',
'pipeline.compilers.es6.ES6Compiler', ),
'LESS_ARGUMENTS': u'--include-path={}'.format(':'.join(STATICFILES_DIRS)),
'CSS_COMPRESSOR': 'pipeline.compressors.yuglify.YuglifyCompressor',
'BABEL_ARGUMENTS': u'--presets env',
'JS_COMPRESSOR': None,
'DISABLE_WRAPPER': True,
'STYLESHEETS': {
"all": {
"source_filenames": (
"compile_bootstrap.less",
"bootstrap/dist/css/bootstrap-theme.css",
"fontawesome/css/font-awesome.css",
"jquery-simple-slider/css/simple-slider.css",
"intro.js/introjs.css",
"template.less",
"dashboard/dashboard.less",
"network/network.less",
"autocomplete_light/vendor/select2/dist/css/select2.css",
"autocomplete_light/select2.css",
),
"output_filename": "all.css",
},
"network-editor": {
"source_filenames": (
"network/editor.less",
),
"output_filename": "network-editor.css",
},
},
'JAVASCRIPT': {
"all": {
"source_filenames": (
# "jquery/dist/jquery.js", # included separately
"bootbox/bootbox.js",
"bootstrap/dist/js/bootstrap.js",
"intro.js/intro.js",
"jquery-knob/dist/jquery.knob.min.js",
"jquery-simple-slider/js/simple-slider.js",
"favico.js/favico.js",
"datatables/media/js/jquery.dataTables.js",
"autocomplete_light/jquery.init.js",
"autocomplete_light/autocomplete.init.js",
"autocomplete_light/vendor/select2/dist/js/select2.js",
"autocomplete_light/select2.js",
"jsPlumb/dist/js/dom.jsPlumb-1.7.5-min.js",
"dashboard/dashboard.js",
"dashboard/activity.js",
"dashboard/group-details.js",
"dashboard/group-list.js",
"dashboard/js/stupidtable.min.js", # no bower file
"dashboard/node-create.js",
"dashboard/node-details.js",
"dashboard/node-list.js",
"dashboard/profile.js",
"dashboard/store.js",
"dashboard/template-list.js",
"dashboard/vm-common.js",
"dashboard/vm-create.js",
"dashboard/vm-list.js",
"dashboard/help.js",
"js/host.js",
"js/network.js",
"js/switch-port.js",
"js/host-list.js",
),
"output_filename": "all.js",
},
"vm-detail": {
"source_filenames": (
"clipboard/dist/clipboard.min.js",
"dashboard/vm-details.js",
"no-vnc/include/util.js",
"no-vnc/include/webutil.js",
"no-vnc/include/base64.js",
"no-vnc/include/websock.js",
"no-vnc/include/des.js",
"no-vnc/include/keysym.js",
"no-vnc/include/keysymdef.js",
"no-vnc/include/keyboard.js",
"no-vnc/include/input.js",
"no-vnc/include/display.js",
"no-vnc/include/jsunzip.js",
"no-vnc/include/rfb.js",
"dashboard/vm-console.js",
"dashboard/vm-tour.js",
),
"output_filename": "vm-detail.js",
},
"datastore": {
"source_filenames": (
"chart.js/dist/Chart.min.js",
"dashboard/datastore-details.js"
),
"output_filename": "datastore.js",
},
"network-editor": {
"source_filenames": (
"jsPlumb/dist/js/jsplumb.min.js",
"network/editor.es6",
),
"output_filename": "network-editor.js",
},
},
}
# Copyright 2017 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 os import environ
from django.core.exceptions import ImproperlyConfigured
# Normally you should not import ANYTHING from Django directly
# into your settings, but ImproperlyConfigured is an exception.
def get_env_variable(var_name, default=None):
""" Get the environment variable or return exception/default """
try:
return environ[var_name]
except KeyError:
if default is None:
error_msg = "Set the %s environment variable" % var_name
raise ImproperlyConfigured(error_msg)
else:
return default
...@@ -30,7 +30,7 @@ utils.patch_middleware_get_user() ...@@ -30,7 +30,7 @@ utils.patch_middleware_get_user()
urlpatterns = [ urlpatterns = [
url(r'^$', lambda x: redirect(reverse("dashboard.index"))), url(r'^$', lambda x: redirect(reverse("dashboard.index"))),
# url(r'^network/', include('network.urls')), url(r'^network/', include('network.urls')),
# url(r'^blacklist-add/', add_blacklist_item), # url(r'^blacklist-add/', add_blacklist_item),
url(r'^dashboard/', include('dashboard.urls')), url(r'^dashboard/', include('dashboard.urls')),
url(r'^request/', include('request.urls')), url(r'^request/', include('request.urls')),
......
...@@ -19,8 +19,11 @@ from sys import exc_info ...@@ -19,8 +19,11 @@ from sys import exc_info
import logging import logging
from django.shortcuts import render_to_response from django.shortcuts import render_to_response, redirect
from django.contrib import messages
from django.template import RequestContext from django.template import RequestContext
from django.http import JsonResponse
from django.utils.translation import ugettext_lazy as _
from .models import HumanReadableException from .models import HumanReadableException
...@@ -59,3 +62,29 @@ def handler403(request): ...@@ -59,3 +62,29 @@ def handler403(request):
resp = render_to_response("403.html", ctx) resp = render_to_response("403.html", ctx)
resp.status_code = 403 resp.status_code = 403
return resp return resp
class CreateLimitedResourceMixin(object):
resource_name = None
model = None
profile_attribute = None
def post(self, *args, **kwargs):
user = self.request.user
try:
limit = getattr(user.profile, self.profile_attribute)
except Exception as e:
logger.debug('No profile or %s: %s', self.profile_attribute, e)
else:
current = self.model.objects.filter(owner=user).count()
logger.debug('%s current use: %d, limit: %d',
self.resource_name, current, limit)
if current > limit:
messages.error(self.request,
_('%s limit (%d) exceeded.')
% (self.resource_name, limit))
if self.request.is_ajax():
return JsonResponse({'redirect': '/'})
else:
return redirect('/')
return super(CreateLimitedResourceMixin, self).post(*args, **kwargs)
...@@ -947,16 +947,27 @@ class VmRemoveInterfaceForm(OperationForm): ...@@ -947,16 +947,27 @@ class VmRemoveInterfaceForm(OperationForm):
class VmAddInterfaceForm(OperationForm): class VmAddInterfaceForm(OperationForm):
network_type = 'vlan'
label = _('Vlan')
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
choices = kwargs.pop('choices') choices = kwargs.pop('choices')
super(VmAddInterfaceForm, self).__init__(*args, **kwargs) super(VmAddInterfaceForm, self).__init__(*args, **kwargs)
field = forms.ModelChoiceField( field = forms.ModelChoiceField(
queryset=choices, required=True, label=_('Vlan')) queryset=choices, required=False,
label=self.label)
if not choices: if not choices:
field.widget.attrs['disabled'] = 'disabled' field.widget.attrs['disabled'] = 'disabled'
field.empty_label = _('No more networks.') field.empty_label = _('No more networks.')
self.fields['vlan'] = field self.fields[self.network_type] = field
class VmAddUserInterfaceForm(VmAddInterfaceForm):
network_type = 'vxlan'
label = _('Vxlan')
class DeployChoiceField(forms.ModelChoiceField): class DeployChoiceField(forms.ModelChoiceField):
...@@ -1298,6 +1309,9 @@ class UserEditForm(forms.ModelForm): ...@@ -1298,6 +1309,9 @@ class UserEditForm(forms.ModelForm):
instance_limit = forms.IntegerField( instance_limit = forms.IntegerField(
label=_('Instance limit'), label=_('Instance limit'),
min_value=0, widget=NumberInput) min_value=0, widget=NumberInput)
network_limit = forms.IntegerField(
label=_('Virtual network limit'),
min_value=0, widget=NumberInput)
two_factor_secret = forms.CharField( two_factor_secret = forms.CharField(
label=_('Two-factor authentication secret'), label=_('Two-factor authentication secret'),
help_text=_("Remove the secret key to disable two-factor " help_text=_("Remove the secret key to disable two-factor "
...@@ -1307,18 +1321,22 @@ class UserEditForm(forms.ModelForm): ...@@ -1307,18 +1321,22 @@ class UserEditForm(forms.ModelForm):
super(UserEditForm, self).__init__(*args, **kwargs) super(UserEditForm, self).__init__(*args, **kwargs)
self.fields["instance_limit"].initial = ( self.fields["instance_limit"].initial = (
self.instance.profile.instance_limit) self.instance.profile.instance_limit)
self.fields["network_limit"].initial = (
self.instance.profile.network_limit)
self.fields["two_factor_secret"].initial = ( self.fields["two_factor_secret"].initial = (
self.instance.profile.two_factor_secret) self.instance.profile.two_factor_secret)
class Meta: class Meta:
model = User model = User
fields = ('email', 'first_name', 'last_name', 'instance_limit', fields = ('email', 'first_name', 'last_name', 'instance_limit',
'is_active', "two_factor_secret", ) 'network_limit', 'is_active', 'two_factor_secret', )
def save(self, commit=True): def save(self, commit=True):
user = super(UserEditForm, self).save() user = super(UserEditForm, self).save()
user.profile.instance_limit = ( user.profile.instance_limit = (
self.cleaned_data['instance_limit'] or None) self.cleaned_data['instance_limit'] or None)
user.profile.network_limit = (
self.cleaned_data['network_limit'] or None)
user.profile.two_factor_secret = ( user.profile.two_factor_secret = (
self.cleaned_data['two_factor_secret'] or None) self.cleaned_data['two_factor_secret'] or None)
user.profile.save() user.profile.save()
......
...@@ -125,6 +125,11 @@ class Command(BaseCommand): ...@@ -125,6 +125,11 @@ class Command(BaseCommand):
vm.snat_to.add(net) vm.snat_to.add(net)
vm.snat_to.add(vm) vm.snat_to.add(vm)
# Add unmanged vlan for user networks
self.create(Vlan, 'vid', name='usernet', vid=5,
network4='0.0.0.0/0', domain=vm_domain,
managed=False)
# default vlan groups # default vlan groups
vg_all = self.create(VlanGroup, 'name', name='all') vg_all = self.create(VlanGroup, 'name', name='all')
vg_all.vlans.add(vm, man, net) vg_all.vlans.add(vm, man, net)
......
# -*- coding: utf-8 -*-
# Generated by Django 1.11.6 on 2017-11-10 00:41
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dashboard', '0006_auto_20170707_1909'),
]
operations = [
migrations.AddField(
model_name='profile',
name='network_limit',
field=models.IntegerField(default=2, verbose_name=b'Virtual network limit'),
),
]
...@@ -178,6 +178,8 @@ class Profile(Model): ...@@ -178,6 +178,8 @@ class Profile(Model):
unique=True, blank=True, null=True, max_length=64, unique=True, blank=True, null=True, max_length=64,
help_text=_('Unique identifier of the person, e.g. a student number.')) help_text=_('Unique identifier of the person, e.g. a student number.'))
instance_limit = IntegerField(default=5) instance_limit = IntegerField(default=5)
network_limit = IntegerField(default=2,
verbose_name="Virtual network limit")
use_gravatar = BooleanField( use_gravatar = BooleanField(
verbose_name=_("Use Gravatar"), default=True, verbose_name=_("Use Gravatar"), default=True,
help_text=_("Whether to use email address as Gravatar profile image")) help_text=_("Whether to use email address as Gravatar profile image"))
......
...@@ -28,7 +28,7 @@ $(function() { ...@@ -28,7 +28,7 @@ $(function() {
}); });
/* operations */ /* operations */
$('#ops, #vm-details-resources-disk, #vm-details-renew-op, #vm-details-pw-reset, #vm-details-add-interface, .operation-wrapper').on('click', '.operation', function(e) { $('#ops, #vm-details-resources-disk, #vm-details-renew-op, #vm-details-pw-reset, #vm-details-add-interface, #vm-details-add-user-interface, .operation-wrapper').on('click', '.operation', function(e) {
var icon = $(this).children("i").addClass('fa-spinner fa-spin'); var icon = $(this).children("i").addClass('fa-spinner fa-spin');
$.ajax({ $.ajax({
......
...@@ -223,6 +223,7 @@ $(function () { ...@@ -223,6 +223,7 @@ $(function () {
register_search($("#dashboard-group-search-form"), $("#dashboard-group-list"), generateGroupHTML); register_search($("#dashboard-group-search-form"), $("#dashboard-group-list"), generateGroupHTML);
register_search($("#dashboard-user-search-form"), $("#dashboard-user-list"), generateUserHTML); register_search($("#dashboard-user-search-form"), $("#dashboard-user-list"), generateUserHTML);
register_search($("#dashboard-template-search-form"), $("#dashboard-template-list"), generateTemplateHTML); register_search($("#dashboard-template-search-form"), $("#dashboard-template-list"), generateTemplateHTML);
register_search($("#dashboard-vxlan-search-form"), $("#dashboard-vxlan-list"), generateVxlanHTML);
/* notification message toggle */ /* notification message toggle */
$(document).on('click', ".notification-message-subject", function() { $(document).on('click', ".notification-message-subject", function() {
...@@ -331,6 +332,18 @@ function generateNodeHTML(data, is_last) { ...@@ -331,6 +332,18 @@ function generateNodeHTML(data, is_last) {
'</a>'; '</a>';
} }
function generateVxlanHTML(data, is_last) {
html = '<a href="' + data.url + '" class="list-group-item real-link' + (is_last ? ' list-group-item-last' : '') + '">' +
'<span class="index-vxlan-list-name">' +
'<i class="fa ' + data.icon + '" title="' + data.name + '"></i> ' + safe_tags_replace(data.name);
if(data.vni !== null && data.vni !== undefined)
html += ' <small class="text-muted"> vni: ' + data.vni + '</small>';
html += '</span>' +
'<div style="clear: both;"></div>' +
'</a>';
return html;
}
/* copare vm-s by fav, pk order */ /* copare vm-s by fav, pk order */
function compareVmByFav(a, b) { function compareVmByFav(a, b) {
if(a.fav && b.fav) { if(a.fav && b.fav) {
......
...@@ -584,7 +584,8 @@ footer a, footer a:hover, footer a:visited { ...@@ -584,7 +584,8 @@ footer a, footer a:hover, footer a:visited {
} }
#dashboard-vm-list, #dashboard-node-list, #dashboard-group-list, #dashboard-vm-list, #dashboard-node-list, #dashboard-group-list,
#dashboard-template-list, #dashboard-files-toplist, #dashboard-user-list { #dashboard-template-list, #dashboard-files-toplist, #dashboard-user-list,
#dashboard-vxlan-list {
min-height: 200px; min-height: 200px;
} }
......
{% load i18n %}
<div class="panel panel-default">
<div class="panel-heading">
<span class="btn btn-default btn-xs infobtn pull-right" data-container="body" title="{% trans "List of virtual networks that are available for you." %}">
<i class="fa fa-info-circle"></i>
</span>
<a href="{% url "network.editor" %}" class="btn btn-default btn-xs pull-right" data-container="body" title="{% trans "Edit network topology." %}">
<i class="fa fa-pencil-square-o"></i> Editor
</a>
<h3 class="no-margin"><i class="fa fa-globe"></i> {% trans "Virtual networks" %}
</h3>
</div>
<div class="list-group" id="vxlan-list-view">
<div id="dashboard-vxlan-list">
{% for vxlan in vxlans %}
<a href="{% url "network.vxlan" vni=vxlan.vni %}" class="list-group-item
{% if forloop.last and vxlan|length < 5 %} list-group-item-last{% endif %}">
<span class="index-vxlan-list-name">
<i class="fa fa-sitemap"></i> {{ vxlan.name }}
{% if user.is_superuser %}
<small class="text-muted"> vni: {{ vxlan.vni }} </small>
{% endif %}
</span>
</a>
{% endfor %}
</div>
<div class="list-group-item list-group-footer">
<div class="row">
<div class="col-xs-5 col-sm-6">
<form action="{% url "network.vxlan-list" %}" method="GET" id="dashboard-vxlan-search-form">
<div class="input-group input-group-sm">
<input name="s" type="text" class="form-control" placeholder="{% trans "Search..." %}" />
<div class="input-group-btn">
<button type="submit" class="btn btn-primary"><i class="fa fa-search"></i></button>
</div>
</div>
</form>
</div>
<div class="col-xs-7 col-sm-6 text-right">
<a href="{% url "network.vxlan-list" %}" class="btn btn-primary btn-xs">
<i class="fa fa-chevron-circle-right"></i> {% trans "show all" %}
</a>
<a href="{% url "network.vxlan-create" %}" class="btn btn-success btn-xs">
<i class="fa fa-plus-circle"></i> {% trans "new" %}
</a>
</div>
</div>
</div>
</div>
</div>
...@@ -48,6 +48,11 @@ ...@@ -48,6 +48,11 @@
{# {% include "dashboard/index-users.html" %}#} {# {% include "dashboard/index-users.html" %}#}
{# </div>#} {# </div>#}
{# {% endif %}#} {# {% endif %}#}
<div class="col-lg-4 col-sm-6">
{% include "dashboard/index-vxlans.html" %}
</div>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}
...@@ -8,6 +8,15 @@ ...@@ -8,6 +8,15 @@
<i class="fa fa-{{op.icon}}"></i> {% trans "add interface" %}</a> <i class="fa fa-{{op.icon}}"></i> {% trans "add interface" %}</a>
{% endif %}{% endwith %} {% endif %}{% endwith %}
</div> </div>
<br />
<br />
<div id="vm-details-add-user-interface">
{% with op=op.add_user_interface %}{% if op %}
<a href="{{op.get_url}}" class="btn btn-{{op.effect}} operation pull-right"
{% if op.disabled %}disabled{% endif %}>
<i class="fa fa-{{op.icon}}"></i> {% trans "add user interface" %}</a>
{% endif %}{% endwith %}
</div>
<h2> <h2>
{% trans "Interfaces" %} {% trans "Interfaces" %}
</h2> </h2>
...@@ -16,12 +25,28 @@ ...@@ -16,12 +25,28 @@
{% for i in instance.interface_set.all %} {% for i in instance.interface_set.all %}
<div> <div>
<h3 class="list-group-item-heading dashboard-vm-details-network-h3"> <h3 class="list-group-item-heading dashboard-vm-details-network-h3">
<i class="fa fa-{% if i.host %}globe{% else %}link{% endif %}"></i> {{ i.vlan.name }} <i class="fa fa-{% if i.host %}globe{% else %}link{% endif %}"></i>
{% if not i.host%}({% trans "unmanaged" %}){% endif %} {% if i.vxlan %}
{{ i.vxlan.name }} (user)
{% else %}
{{ i.vlan.name }}
{% if not i.host%}({% trans "unmanaged" %}){% endif %}
{% endif %}
{% if user.is_superuser and i.host %} {% if user.is_superuser and i.host %}
<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 %}
{% if i.vxlan %}
{% with op=op.remove_user_interface %}{% if op %}
<span class="operation-wrapper">
<a href="{{op.get_url}}?interface={{ i.pk }}"
class="btn btn-{{op.effect}} btn-xs operation interface-remove"
{% if op.disabled %}disabled{% endif %}>{% trans "remove" %}
</a>
</span>
{% endif %}{% endwith %}
{% else %}
{% with op=op.remove_interface %}{% if op %} {% with op=op.remove_interface %}{% if op %}
<span class="operation-wrapper"> <span class="operation-wrapper">
<a href="{{op.get_url}}?interface={{ i.pk }}" <a href="{{op.get_url}}?interface={{ i.pk }}"
...@@ -30,6 +55,7 @@ ...@@ -30,6 +55,7 @@
</a> </a>
</span> </span>
{% endif %}{% endwith %} {% endif %}{% endwith %}
{% endif %}
</h3> </h3>
{% if i.host %} {% if i.host %}
<div class="row"> <div class="row">
......
...@@ -15,6 +15,9 @@ ...@@ -15,6 +15,9 @@
<!--<i class="fa fa-refresh fa-spin fa-2x"></i>--> <!--<i class="fa fa-refresh fa-spin fa-2x"></i>-->
</div> </div>
<a class="pull-right btn btn-success btn-xs vm-create" href="{% url "dashboard.views.vm-create" %}"><i class="fa fa-plus-circle"></i> {% trans "new virtual machine" %}</a> <a class="pull-right btn btn-success btn-xs vm-create" href="{% url "dashboard.views.vm-create" %}"><i class="fa fa-plus-circle"></i> {% trans "new virtual machine" %}</a>
<a href="{% url "network.editor" %}" class="btn btn-primary btn-xs pull-right" data-container="body" title="{% trans "Edit network topology." %}">
<i class="fa fa-pencil-square-o"></i> Edit topology
</a>
<h3 class="no-margin"><i class="fa fa-desktop"></i> {% trans "Virtual machines" %}</h3> <h3 class="no-margin"><i class="fa fa-desktop"></i> {% trans "Virtual machines" %}</h3>
</div> </div>
<div class="panel-body"> <div class="panel-body">
......
...@@ -25,6 +25,8 @@ from django.contrib.auth.models import Group, User ...@@ -25,6 +25,8 @@ from django.contrib.auth.models import Group, User
from django.core.cache import cache from django.core.cache import cache
from django.views.generic import TemplateView from django.views.generic import TemplateView
from vm.models import Instance, Node, InstanceTemplate from vm.models import Instance, Node, InstanceTemplate
from dashboard.views.vm import vm_ops
from network.models import Vxlan
from ..store_api import Store from ..store_api import Store
......
...@@ -20,13 +20,14 @@ from django.core.urlresolvers import reverse_lazy ...@@ -20,13 +20,14 @@ from django.core.urlresolvers import reverse_lazy
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from crispy_forms.helper import FormHelper from crispy_forms.helper import FormHelper
from crispy_forms.layout import Layout, Fieldset, Div, Submit, BaseInput from crispy_forms.layout import Layout, Fieldset, Div, Submit, BaseInput, Field
from crispy_forms.bootstrap import FormActions, FieldWithButtons, StrictButton from crispy_forms.bootstrap import FormActions, FieldWithButtons, StrictButton
from firewall.models import ( from firewall.models import (
Host, Vlan, Domain, Group, Record, BlacklistItem, Rule, VlanGroup, Host, Vlan, Domain, Group, Record, BlacklistItem, Rule, VlanGroup,
SwitchPort, Firewall SwitchPort, Firewall
) )
from network.models import Vxlan
class LinkButton(BaseInput): class LinkButton(BaseInput):
...@@ -348,3 +349,53 @@ class VlanGroupForm(ModelForm): ...@@ -348,3 +349,53 @@ class VlanGroupForm(ModelForm):
class Meta: class Meta:
model = VlanGroup model = VlanGroup
fields = ("name", "vlans", "description", "owner", ) fields = ("name", "vlans", "description", "owner", )
class VxlanSuperUserForm(ModelForm):
helper = FormHelper()
helper.layout = Layout(
Div(
Fieldset(
'',
'name',
'vni',
'vlan',
'description',
'comment',
'owner',
)
),
FormActions(
Submit('submit', _('Save')),
LinkButton('back', _('Back'), reverse_lazy(
'network.vxlan-list'))
)
)
class Meta:
model = Vxlan
fields = ('name', 'vni', 'vlan', 'description', 'comment', 'owner', )
class VxlanForm(ModelForm):
helper = FormHelper()
helper.layout = Layout(
Div(
Fieldset(
'',
'name',
'description',
'comment',
Field('vni', type='hidden'),
)
),
FormActions(
Submit('submit', _('Save')),
LinkButton('back', _('Back'), reverse_lazy(
'network.vxlan-list'))
)
)
class Meta:
model = Vxlan
fields = ('name', 'description', 'comment', 'vni', )
# -*- coding: utf-8 -*-
# Generated by Django 1.11.6 on 2017-10-25 18:56
from __future__ import unicode_literals
from django.conf import settings
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
import firewall.fields
class Migration(migrations.Migration):
initial = True
dependencies = [
('firewall', '0006_auto_20170707_1909'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Vxlan',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('vni', models.IntegerField(help_text='VXLAN Network Identifier.', unique=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(16777215)], verbose_name='VNI')),
('name', models.CharField(help_text='The short name of the virtual network.', max_length=20, unique=True, validators=[firewall.fields.val_alfanum], verbose_name='Name')),
('description', models.TextField(blank=True, help_text='Description of the goals and elements of the virtual network.', verbose_name='description')),
('comment', models.TextField(blank=True, help_text='Notes, comments about the network', verbose_name='comment')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='created at')),
('modified_at', models.DateTimeField(auto_now=True, verbose_name='modified at')),
('owner', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='owner')),
('vlan', models.ForeignKey(help_text='The server vlan.', on_delete=django.db.models.deletion.CASCADE, to='firewall.Vlan', verbose_name='vlan')),
],
options={
'ordering': ('vni',),
'verbose_name': 'vxlan',
'verbose_name_plural': 'vxlans',
},
),
]
# -*- coding: utf-8 -*-
# Generated by Django 1.11.6 on 2017-11-10 00:41
from __future__ import unicode_literals
import django.core.validators
from django.db import migrations, models
import firewall.fields
class Migration(migrations.Migration):
dependencies = [
('network', '0001_initial'),
]
operations = [
migrations.AlterModelOptions(
name='vxlan',
options={'ordering': ('vni',), 'permissions': (('create_vxlan', 'Can create a Vxlan network.'),), 'verbose_name': 'vxlan', 'verbose_name_plural': 'vxlans'},
),
migrations.AlterField(
model_name='vxlan',
name='name',
field=models.CharField(help_text='The short name of the virtual network.', max_length=20, validators=[firewall.fields.val_alfanum], verbose_name='Name'),
),
migrations.AlterField(
model_name='vxlan',
name='vni',
field=models.IntegerField(help_text='VXLAN Network Identifier.', unique=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(4095)], verbose_name='VNI'),
),
]
# -*- coding: utf-8 -*-
# Generated by Django 1.11.6 on 2017-11-30 01:01
from __future__ import unicode_literals
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('contenttypes', '0002_remove_content_type_name'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('network', '0002_auto_20171110_0041'),
]
operations = [
migrations.CreateModel(
name='EditorElement',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('x', models.IntegerField()),
('y', models.IntegerField()),
('free_port_num', models.IntegerField()),
('object_id', models.PositiveIntegerField()),
('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')),
('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
]
...@@ -15,4 +15,103 @@ ...@@ -15,4 +15,103 @@
# You should have received a copy of the GNU General Public License along # You should have received a copy of the GNU General Public License along
# with CIRCLE. If not, see <http://www.gnu.org/licenses/>. # with CIRCLE. If not, see <http://www.gnu.org/licenses/>.
# Create your models here. from django.db import models
from django.core.validators import MinValueValidator, MaxValueValidator
from django.core.urlresolvers import reverse
from django.utils.translation import ugettext_lazy as _
from django.contrib.contenttypes.fields import (
GenericRelation, GenericForeignKey
)
from django.contrib.contenttypes.models import ContentType
from firewall.models import Vlan
from firewall.fields import val_alfanum
from openstack_auth.user import User
class EditorElement(models.Model):
x = models.IntegerField()
y = models.IntegerField()
free_port_num = models.IntegerField()
owner = models.ForeignKey(User)
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
object_id = models.PositiveIntegerField()
content_object = GenericForeignKey()
def as_data(self):
type = 'network' if isinstance(self.content_object, Vxlan) else 'vm'
if type == 'network':
id = "net-%s" % self.content_object.vni
else:
id = "vm-%s" % self.content_object.pk
return {
'name': unicode(self.content_object),
'id': id,
'x': self.x,
'y': self.y,
'free_port_num': self.free_port_num,
'type': type,
'description': self.content_object.description,
}
class Vxlan(models.Model):
"""
A virtual L2 network,
These networks are isolated by the vxlan (virtual extensible lan)
technology, which is commonly used by managed network switches
to partition the network, with a more scalable way opposite vlan
technology. Usually, it used over vlan networks.
Each vxlan network has a unique identifier (VNI), a name, and
a server vlan network.
"""
# NOTE: VXLAN VNI's maximal value is 2^24-1, but MAC address generator
# only supports 2^12-1 maximal value.
vni = models.IntegerField(unique=True,
verbose_name=_('VNI'),
help_text=_('VXLAN Network Identifier.'),
validators=[MinValueValidator(0),
MaxValueValidator(2 ** 12 - 1)])
vlan = models.ForeignKey(Vlan,
verbose_name=_('vlan'),
help_text=_('The server vlan.'))
name = models.CharField(max_length=20,
verbose_name=_('Name'),
help_text=_('The short name of the '
'virtual network.'),
validators=[val_alfanum])
description = models.TextField(blank=True, verbose_name=_('description'),
help_text=_(
'Description of the goals and elements '
'of the virtual network.'))
comment = models.TextField(blank=True,
verbose_name=_('comment'),
help_text=_(
'Notes, comments about the network'))
created_at = models.DateTimeField(auto_now_add=True,
verbose_name=_('created at'))
owner = models.ForeignKey(User, blank=True, null=True,
verbose_name=_('owner'))
modified_at = models.DateTimeField(auto_now=True,
verbose_name=_('modified at'))
editor_elements = GenericRelation(EditorElement)
class Meta:
app_label = 'network'
verbose_name = _("vxlan")
verbose_name_plural = _("vxlans")
ordering = ('vni', )
permissions = (
('create_vxlan', _('Can create a Vxlan network.')),
)
def __unicode__(self):
return self.name
def get_absolute_url(self):
return reverse('network.vxlan', kwargs={'vni': self.vni})
.flex-container {
display: flex;
flex-direction: row;
}
#filterConatiner {
margin-left: 10px;
margin-right: 10px;
}
#dragPanel {
flex: 250px;
width: 250px;
margin: 0px;
overflow: hidden;
height: 700px;
}
#dragContainer{
overflow-y: scroll;
height: 574px;
}
#dropContainer{
position: relative !important;
text-align: justify !important;
flex: auto;
width: 90%;
height: auto;
background: grey;
resize: none;
}
.unused-element {
cursor: pointer;
}
.element {
position: absolute;
left: 10px;
display: inline;
line-height: 75px;
text-align: center;
vertical-align: middle;
color: black;
font-size: 20px;
height: 75px;
padding-left: 10px;
padding-right: 10px;
border-radius: 8px;
background: white;
-webkit-border-radius: 8px;
border-style: solid;
border-width: 1px;
z-index: 40;
cursor: pointer;
}
.container {
margin: 10px;
width: 100%;
padding: 0px;
}
body {
height: 100%;
}
...@@ -24,6 +24,8 @@ from django_tables2.columns import (LinkColumn, TemplateColumn, Column, ...@@ -24,6 +24,8 @@ from django_tables2.columns import (LinkColumn, TemplateColumn, Column,
BooleanColumn) BooleanColumn)
from firewall.models import Host, Vlan, Domain, Group, Record, Rule, SwitchPort from firewall.models import Host, Vlan, Domain, Group, Record, Rule, SwitchPort
from network.models import Vxlan
from vm.models import Interface
class MACColumn(Column): class MACColumn(Column):
...@@ -233,6 +235,16 @@ class VlanGroupTable(Table): ...@@ -233,6 +235,16 @@ class VlanGroupTable(Table):
order_by = 'name' order_by = 'name'
class VxlanTable(Table):
name = LinkColumn('network.vxlan', args=[A('vni')])
class Meta:
model = Vxlan
attrs = {'class': 'table table-striped table-condensed'}
fields = ('vni', 'name', )
order_by = 'vni'
class HostRecordsTable(Table): class HostRecordsTable(Table):
fqdn = LinkColumn( fqdn = LinkColumn(
"network.record", args=[A("pk")], "network.record", args=[A("pk")],
...@@ -282,3 +294,16 @@ class FirewallRuleTable(Table): ...@@ -282,3 +294,16 @@ class FirewallRuleTable(Table):
'action', 'proto', 'actions') 'action', 'proto', 'actions')
order_by = '-pk' order_by = '-pk'
empty_text = _("No related rules found.") empty_text = _("No related rules found.")
class SmallVmTable(Table):
instance = Column(accessor=A('instance.name'), verbose_name='VM')
instance_id = LinkColumn('dashboard.views.detail', args=[A('instance.pk')],
accessor=A('instance.pk'), verbose_name='ID')
class Meta:
model = Interface
attrs = {'class': 'table table-striped'}
fields = ('instance', )
sequence = ('instance_id', 'instance', )
order_by = 'instance.pk'
...@@ -29,7 +29,22 @@ ...@@ -29,7 +29,22 @@
</div> </div>
{% trans "Vlans" %} {% trans "Vlans" %}
<div> <div>
<small>{% trans "Hosts are machines on the network" %}</small> <small>{% trans "802.1Q virtual networks" %}</small>
</div>
</h3>
<h3>
<div class="pull-right">
<a href="{% url "network.vxlan-list" %}" class="btn btn-xs btn-default">
<i class="fa fa-list"></i> {% trans "list" %}
</a>
<a href="{% url "network.vxlan-create" %}" class="btn btn-xs btn-success">
<i class="fa fa-plus-circle"></i> {% trans "new" %}
</a>
</div>
{% trans "Vxlans" %}
<div>
<small>{% trans "VXLAN virtual networks" %}</small>
</div> </div>
</h3> </h3>
...@@ -44,7 +59,7 @@ ...@@ -44,7 +59,7 @@
</div> </div>
{% trans "Domains" %} {% trans "Domains" %}
<div> <div>
<small>{% trans "Hosts are machines on the network" %}</small> <small>{% trans "Domain names" %}</small>
</div> </div>
</h3> </h3>
...@@ -59,7 +74,7 @@ ...@@ -59,7 +74,7 @@
</div> </div>
{% trans "Records" %} {% trans "Records" %}
<div> <div>
<small>{% trans "Hosts are machines on the network" %}</small> <small>{% trans "DNS records" %}</small>
</div> </div>
</h3> </h3>
...@@ -74,7 +89,7 @@ ...@@ -74,7 +89,7 @@
</div> </div>
{% trans "Blacklist items" %} {% trans "Blacklist items" %}
<div> <div>
<small>{% trans "Hosts are machines on the network" %}</small> <small>{% trans "List of denied IPs and exceptions" %}</small>
</div> </div>
</h3> </h3>
...@@ -89,7 +104,7 @@ ...@@ -89,7 +104,7 @@
</div> </div>
{% trans "Rules" %} {% trans "Rules" %}
<div> <div>
<small>{% trans "Hosts are machines on the network" %}</small> <small>{% trans "Firewall rules" %}</small>
</div> </div>
</h3> </h3>
...@@ -104,7 +119,7 @@ ...@@ -104,7 +119,7 @@
</div> </div>
{% trans "Switch ports" %} {% trans "Switch ports" %}
<div> <div>
<small>{% trans "Hosts are machines on the network" %}</small> <small>{% trans "Switch ports" %}</small>
</div> </div>
</h3> </h3>
...@@ -119,7 +134,7 @@ ...@@ -119,7 +134,7 @@
</div> </div>
{% trans "Vlan groups" %} {% trans "Vlan groups" %}
<div> <div>
<small>{% trans "Hosts are machines on the network" %}</small> <small>{% trans "Groups of 802.1Q virtual networks" %}</small>
</div> </div>
</h3> </h3>
...@@ -134,7 +149,7 @@ ...@@ -134,7 +149,7 @@
</div> </div>
{% trans "Host groups" %} {% trans "Host groups" %}
<div> <div>
<small>{% trans "Hosts are machines on the network" %}</small> <small>{% trans "Groups of hosts are machines on the network" %}</small>
</div> </div>
</h3> </h3>
</div> </div>
......
{% extends "dashboard/base.html" %}
{% load staticfiles %}
{% load i18n %}
{% load pipeline %}
{% block title-page %}{% trans 'Network Editor' %}{% endblock %}
{% block extra_css %}
{% stylesheet "network-editor" %}
{% endblock %}
{% block content %}
<div class="flex-container" id="workspace">
<div class="panel panel-default text-center" id="dragPanel">
<div class="panel-heading">
<div class="row">
<div class="col-md-9 text-left">
<h3 class="no-margin"><i class="fa fa-sitemap"></i> {% trans 'Editor' %}</h3>
</div>
<div class="col-md-3 text-left">
<button class="btn btn-success btn-xs" id="saveButton"><i class="fa fa-floppy-o"></i></button>
</div>
</div>
</div>
<div class="panel-heading text-center">
<div id="filterConatiner">
<div class="row">
<input type="text" class="form-control" id="searchField" placeholder="{% trans 'Search' %}"/><br />
</div>
<div class="row">
<div class="col-md-6">
<i id="vm-filter"></i> <i class="fa fa-desktop"></i> vm
</div>
<div class="col-md-6">
<i id="net-filter"></i> <i class="fa fa-sitemap"></i> net
</div>
</div>
</div>
</div>
<div class="panel-body" id="dragContainer">
</div>
</div>
<div class="" id="dropContainer" oncontextmenu="return false;"></div>
</div>
{% endblock %}
{% block extra_js %}
{% javascript "network-editor" %}
{% endblock %}
{% extends "dashboard/base.html" %}
{% load render_table from django_tables2 %}
{% load i18n %}
{% load l10n %}
{% load staticfiles %}
{% load crispy_forms_tags %}
{% block title-page %}{% trans "Create" %} | {% trans "vxlan" %}{% endblock %}
{% block content %}
<div class="page-header">
<h2>{% trans "Create a new vxlan" %}</h2>
</div>
<div class="row">
<div class="col-sm-8">
{% crispy form %}
</div>
<div class="col-sm-4">
</div>
</div>
{% endblock %}
{% extends "dashboard/base.html" %}
{% load render_table from django_tables2 %}
{% load i18n %}
{% load l10n %}
{% load staticfiles %}
{% load crispy_forms_tags %}
{% block title-page %}{{ form.name.value }} | {% trans "vxlan" %}{% endblock %}
{% block content %}
<div class="page-header">
<a href="{% url "network.vxlan-delete" vni=vxlan.vni %}" class="btn btn-danger pull-right"><i class="fa fa-times-circle"></i> {% trans "Delete this vxlan" %}</a>
<h2>{{ form.name.value }} <small>{% trans "details of vxlan" %}</small></h2>
</div>
<div class="row">
<div class="col-sm-6">
{% crispy form %}
</div>
<div class="col-sm-6">
<div class="page-header">
<h3>{% trans "Connected virtual machines" %}</h3>
</div>
{% render_table vm_list %}
<div class="page-header">
<h3>{% trans "Manage access" %}</h3>
</div>
{% include "dashboard/_manage_access.html" with table_id="vxlan-access-table" %}
</div>
</div>
{% endblock %}
{% extends "dashboard/base.html" %}
{% load render_table from django_tables2 %}
{% load i18n %}
{% load l10n %}
{% load staticfiles %}
{% block title-page %}{% trans "Vxlans" %}{% endblock %}
{% block content %}
<div class="page-header">
<a href="{% url "network.vxlan-create" %}" class="btn btn-success pull-right"><i class="fa fa-plus-circle"></i> {% trans "Create a new vxlan" %}</a>
<a href="{% url "network.editor" %}" class="btn btn-primary pull-right" data-container="body" title="{% trans "Edit network topology." %}">
<i class="fa fa-pencil-square-o"></i> Edit topology
</a>
<h1>Vxlans <small>{% trans "list of all vxlans" %}</small></h1>
</div>
<div class="table-responsive">
{% render_table table %}
</div>
{% endblock %}
{% extends "network/base.html" %}
{% load render_table from django_tables2 %}
{% load i18n %}
{% load l10n %}
{% load staticfiles %}
{% load crispy_forms_tags %}
{% block title-page %}{% trans "Create" %} | {% trans "vxlan" %}{% endblock %}
{% block content %}
<div class="page-header">
<h2>{% trans "Create a new vxlan" %}</h2>
</div>
<div class="row">
<div class="col-sm-8">
{% crispy form %}
</div>
<div class="col-sm-4">
</div>
</div>
{% endblock %}
{% extends "network/base.html" %}
{% load render_table from django_tables2 %}
{% load i18n %}
{% load l10n %}
{% load staticfiles %}
{% load crispy_forms_tags %}
{% block title-page %}{{ form.name.value }} | {% trans "vxlan" %}{% endblock %}
{% block content %}
<div class="page-header">
<a href="{% url "network.vxlan-delete" vni=vxlan.vni %}" class="btn btn-danger pull-right"><i class="fa fa-times-circle"></i> {% trans "Delete this vxlan" %}</a>
<h2>{{ form.name.value }} <small>{% trans "details of vxlan" %}</small></h2>
</div>
<div class="row">
<div class="col-sm-6">
{% crispy form %}
</div>
<div class="col-sm-6">
<div class="page-header">
<h3>{% trans "Connected virtual machines" %}</h3>
</div>
{% render_table vm_list %}
<div class="page-header">
<h3>{% trans "Manage access" %}</h3>
</div>
{% include "dashboard/_manage_access.html" with table_id="vxlan-access-table" %}
</div>
</div>
{% endblock %}
{% extends "network/base.html" %}
{% load render_table from django_tables2 %}
{% load i18n %}
{% load l10n %}
{% load staticfiles %}
{% block title-page %}{% trans "Vxlans" %}{% endblock %}
{% block content %}
<div class="page-header">
<a href="{% url "network.vxlan-create" %}" class="btn btn-success pull-right"><i class="fa fa-plus-circle"></i> {% trans "Create a new vxlan" %}</a>
<h1>Vxlans <small>{% trans "list of all vxlans" %}</small></h1>
</div>
<div class="table-responsive">
{% render_table table %}
</div>
{% endblock %}
...@@ -20,6 +20,7 @@ from .views import ( ...@@ -20,6 +20,7 @@ from .views import (
IndexView, IndexView,
HostList, HostDetail, HostCreate, HostDelete, HostList, HostDetail, HostCreate, HostDelete,
VlanList, VlanDetail, VlanDelete, VlanCreate, VlanList, VlanDetail, VlanDelete, VlanCreate,
VxlanList, VxlanDetail, VxlanDelete, VxlanCreate, VxlanAclUpdateView,
DomainList, DomainDetail, DomainDelete, DomainCreate, DomainList, DomainDetail, DomainDelete, DomainCreate,
GroupList, GroupDetail, GroupDelete, GroupCreate, GroupList, GroupDetail, GroupDelete, GroupCreate,
RecordList, RecordDetail, RecordCreate, RecordDelete, RecordList, RecordDetail, RecordCreate, RecordDelete,
...@@ -30,7 +31,7 @@ from .views import ( ...@@ -30,7 +31,7 @@ from .views import (
FirewallList, FirewallDetail, FirewallCreate, FirewallDelete, FirewallList, FirewallDetail, FirewallCreate, FirewallDelete,
remove_host_group, add_host_group, remove_host_group, add_host_group,
remove_switch_port_device, add_switch_port_device, remove_switch_port_device, add_switch_port_device,
VlanAclUpdateView VlanAclUpdateView, NetworkEditorView
) )
urlpatterns = [ urlpatterns = [
...@@ -125,6 +126,19 @@ urlpatterns = [ ...@@ -125,6 +126,19 @@ urlpatterns = [
url('^rules/delete/(?P<pk>\d+)/$', RuleDelete.as_view(), url('^rules/delete/(?P<pk>\d+)/$', RuleDelete.as_view(),
name="network.rule_delete"), name="network.rule_delete"),
# vxlan
url('^vxlans/$', VxlanList.as_view(), name='network.vxlan-list'),
url('^vxlans/create$', VxlanCreate.as_view(), name='network.vxlan-create'),
url('^vxlans/(?P<vni>\d+)/$', VxlanDetail.as_view(), name='network.vxlan'),
url('^vxlans/(?P<pk>\d+)/acl/$', VxlanAclUpdateView.as_view(),
name='network.vxlan-acl'),
url('^vxlans/delete/(?P<vni>\d+)/$', VxlanDelete.as_view(),
name="network.vxlan-delete"),
# editor
url('^editor/$', NetworkEditorView.as_view(),
name="network.editor"),
# non class based views # non class based views
url('^hosts/(?P<pk>\d+)/remove/(?P<group_pk>\d+)/$', remove_host_group, url('^hosts/(?P<pk>\d+)/remove/(?P<group_pk>\d+)/$', remove_host_group,
name='network.remove_host_group'), name='network.remove_host_group'),
......
# -*- coding: utf-8 -*-
# Generated by Django 1.11.6 on 2017-11-05 20:11
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('network', '0001_initial'),
('vm', '0002_interface_model'),
]
operations = [
migrations.AddField(
model_name='interface',
name='vxlan',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='vm_interface', to='network.Vxlan', verbose_name='vxlan'),
),
migrations.AddField(
model_name='interfacetemplate',
name='vxlan',
field=models.ForeignKey(blank=True, help_text='Virtual network the interface belongs to.', null=True, on_delete=django.db.models.deletion.CASCADE, to='network.Vxlan', verbose_name='vxlan'),
),
migrations.AlterField(
model_name='interface',
name='vlan',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='vm_interface', to='firewall.Vlan', verbose_name='vlan'),
),
migrations.AlterField(
model_name='interfacetemplate',
name='vlan',
field=models.ForeignKey(blank=True, help_text='Network the interface belongs to.', null=True, on_delete=django.db.models.deletion.CASCADE, to='firewall.Vlan', verbose_name='vlan'),
),
]
# -*- coding: utf-8 -*-
# Generated by Django 1.11.6 on 2018-02-26 11:59
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('vm', '0003_auto_20171105_2011'),
('vm', '0007_auto_20180226_1239'),
]
operations = [
]
...@@ -34,6 +34,7 @@ from django.db import IntegrityError ...@@ -34,6 +34,7 @@ from django.db import IntegrityError
from django.dispatch import Signal from django.dispatch import Signal
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 _, ugettext_noop
from django.contrib.contenttypes.fields import GenericRelation
from model_utils import Choices from model_utils import Choices
from model_utils.managers import QueryManager from model_utils.managers import QueryManager
...@@ -51,6 +52,8 @@ from .activity import (ActivityInProgressError, InstanceActivity) ...@@ -51,6 +52,8 @@ from .activity import (ActivityInProgressError, InstanceActivity)
from .common import BaseResourceConfigModel, Lease from .common import BaseResourceConfigModel, Lease
from .network import Interface from .network import Interface
from .node import Node, Trait from .node import Node, Trait
from network.models import EditorElement
from openstack_auth.user import User from openstack_auth.user import User
...@@ -247,6 +250,7 @@ class Instance(OperatedMixin, TimeStampedModel): ...@@ -247,6 +250,7 @@ class Instance(OperatedMixin, TimeStampedModel):
destroyed_at = DateTimeField(blank=True, null=True, destroyed_at = DateTimeField(blank=True, null=True,
help_text=_("The virtual machine's time of " help_text=_("The virtual machine's time of "
"destruction.")) "destruction."))
editor_elements = GenericRelation(EditorElement)
objects = Manager() objects = Manager()
active = QueryManager(destroyed_at=None) active = QueryManager(destroyed_at=None)
......
...@@ -25,6 +25,7 @@ from django.utils.translation import ugettext_lazy as _, ugettext_noop ...@@ -25,6 +25,7 @@ from django.utils.translation import ugettext_lazy as _, ugettext_noop
from common.models import create_readable from common.models import create_readable
from firewall.models import Vlan, Host from firewall.models import Vlan, Host
from network.models import Vxlan
from ..tasks import net_tasks from ..tasks import net_tasks
logger = getLogger(__name__) logger = getLogger(__name__)
...@@ -35,9 +36,15 @@ class InterfaceTemplate(Model): ...@@ -35,9 +36,15 @@ class InterfaceTemplate(Model):
"""Network interface template for an instance template. """Network interface template for an instance template.
If the interface is managed, a host will be created for it. If the interface is managed, a host will be created for it.
Use with Vxlan is never managed.
""" """
vlan = ForeignKey(Vlan, verbose_name=_('vlan'), vlan = ForeignKey(Vlan, blank=True, null=True,
verbose_name=_('vlan'),
help_text=_('Network the interface belongs to.')) help_text=_('Network the interface belongs to.'))
vxlan = ForeignKey(Vxlan, blank=True, null=True,
verbose_name=_('vxlan'),
help_text=_('Virtual network the interface '
'belongs to.'))
managed = BooleanField(verbose_name=_('managed'), default=True, managed = BooleanField(verbose_name=_('managed'), default=True,
help_text=_('If a firewall host (i.e. IP address ' help_text=_('If a firewall host (i.e. IP address '
'association) should be generated.')) 'association) should be generated.'))
...@@ -54,7 +61,10 @@ class InterfaceTemplate(Model): ...@@ -54,7 +61,10 @@ class InterfaceTemplate(Model):
verbose_name_plural = _('interface templates') verbose_name_plural = _('interface templates')
def __unicode__(self): def __unicode__(self):
return "%s - %s - %s" % (self.template, self.vlan, self.managed) if self.vlan:
return "%s - %s - %s" % (self.template, self.vlan, self.managed)
else: # vxlan
return "%s - %s - %s" % (self.template, self.vxlan, False)
class Interface(Model): class Interface(Model):
...@@ -64,8 +74,12 @@ class Interface(Model): ...@@ -64,8 +74,12 @@ class Interface(Model):
MODEL_TYPES = (('virtio', 'virtio'), ('ne2k_pci', 'ne2k_pci'), MODEL_TYPES = (('virtio', 'virtio'), ('ne2k_pci', 'ne2k_pci'),
('pcnet', 'pcnet'), ('rtl8139', 'rtl8139'), ('pcnet', 'pcnet'), ('rtl8139', 'rtl8139'),
('e1000', 'e1000')) ('e1000', 'e1000'))
vlan = ForeignKey(Vlan, verbose_name=_('vlan'), vlan = ForeignKey(Vlan, blank=True, null=True,
verbose_name=_('vlan'),
related_name="vm_interface") related_name="vm_interface")
vxlan = ForeignKey(Vxlan, blank=True, null=True,
verbose_name=_('vxlan'),
related_name="vm_interface")
host = ForeignKey(Host, verbose_name=_('host'), blank=True, null=True) host = ForeignKey(Host, verbose_name=_('host'), blank=True, null=True)
instance = ForeignKey('Instance', verbose_name=_('instance'), instance = ForeignKey('Instance', verbose_name=_('instance'),
related_name='interface_set') related_name='interface_set')
...@@ -77,50 +91,66 @@ class Interface(Model): ...@@ -77,50 +91,66 @@ class Interface(Model):
ordering = ("-vlan__managed", ) ordering = ("-vlan__managed", )
def __unicode__(self): def __unicode__(self):
return 'cloud-' + str(self.instance.id) + '-' + str(self.vlan.vid) if self.vxlan is None:
return 'cloud-%s-%s' % (str(self.instance.id),
str(self.vlan.vid))
else: # vxlan
return 'cloudx-%s-%s' % (str(self.instance.id),
str(self.vxlan.vni))
@property @property
def mac(self): def mac(self):
try: try:
return self.host.mac return self.host.mac
except: except:
return Interface.generate_mac(self.instance, self.vlan) return Interface.generate_mac(
self.instance,
self.vlan.vid if self.vxlan is None else self.vxlan.vni,
self.vxlan is not None
)
@classmethod @classmethod
def generate_mac(cls, instance, vlan): def generate_mac(cls, instance, vid, is_vxlan):
"""Generate MAC address for a VM instance on a VLAN. """Generate MAC address for a VM instance on a VLAN.
""" """
# MAC 02:XX:XX:XX:XX:XX # MAC 02:XX:XX:XX:XX:XX
# \________/\__/ # \______/ |\__/
# VM ID VLAN ID # VM ID | V(X)LAN ID
# __________|_____
# / \
# VXLAN: 1, VLAN: 0
class mac_custom(mac_unix): class mac_custom(mac_unix):
word_fmt = '%.2X' word_fmt = '%.2X'
i = instance.id & 0xfffffff i = instance.id & 0xffffff
v = vlan.vid & 0xfff v = vid & 0xfff
m = (0x02 << 40) | (i << 12) | v vx = 1 if is_vxlan else 0
m = (0x02 << 40) | (i << 16) | (vx << 12) | v
return EUI(m, dialect=mac_custom) return EUI(m, dialect=mac_custom)
def get_vmnetwork_desc(self): def get_vmnetwork_desc(self):
return { return {
'name': self.__unicode__(), 'name': self.__unicode__(),
'bridge': 'cloud', 'bridge': ('cloud' if self.vxlan is None
else 'cloudx-%s' % self.vxlan.vni),
'mac': str(self.mac), 'mac': str(self.mac),
'ipv4': str(self.host.ipv4) if self.host is not None else None, 'ipv4': str(self.host.ipv4) if self.host is not None else None,
'ipv6': str(self.host.ipv6) if self.host is not None else None, 'ipv6': str(self.host.ipv6) if self.host is not None else None,
'vlan': self.vlan.vid, 'vlan': self.vlan.vid,
'vxlan': self.vxlan.vni if self.vxlan is not None else None,
'model': self.model, 'model': self.model,
'managed': self.host is not None 'managed': self.host is not None
} }
@classmethod @classmethod
def create(cls, instance, vlan, managed, owner=None, base_activity=None): def create(cls, instance, vlan, managed, vxlan=None,
owner=None, base_activity=None):
"""Create a new interface for a VM instance to the specified VLAN. """Create a new interface for a VM instance to the specified VLAN.
""" """
if managed: if managed and vxlan is None:
host = Host() host = Host()
host.vlan = vlan host.vlan = vlan
# TODO change Host's mac field's type to EUI in firewall # TODO change Host's mac field's type to EUI in firewall
host.mac = str(cls.generate_mac(instance, vlan)) host.mac = str(cls.generate_mac(instance, vlan.vid, False))
host.hostname = instance.vm_name host.hostname = instance.vm_name
# Get addresses from firewall # Get addresses from firewall
if base_activity is None: if base_activity is None:
...@@ -159,7 +189,7 @@ class Interface(Model): ...@@ -159,7 +189,7 @@ class Interface(Model):
else: else:
host = None host = None
iface = cls(vlan=vlan, host=host, instance=instance) iface = cls(vlan=vlan, vxlan=vxlan, host=host, instance=instance)
iface.save() iface.save()
return iface return iface
...@@ -180,7 +210,10 @@ class Interface(Model): ...@@ -180,7 +210,10 @@ class Interface(Model):
def save_as_template(self, instance_template): def save_as_template(self, instance_template):
"""Create a template based on this interface. """Create a template based on this interface.
""" """
i = InterfaceTemplate(vlan=self.vlan, managed=self.host is not None, i = InterfaceTemplate(vlan=self.vlan,
managed=(
self.host is not None or
self.vxlan or not None),
template=instance_template) template=instance_template)
i.save() i.save()
return i return i
...@@ -189,6 +189,8 @@ class AddInterfaceOperation(InstanceOperation): ...@@ -189,6 +189,8 @@ class AddInterfaceOperation(InstanceOperation):
"the VM.") "the VM.")
required_perms = () required_perms = ()
accept_states = ('STOPPED', 'PENDING', 'ACTIVE') accept_states = ('STOPPED', 'PENDING', 'ACTIVE')
accept_states = ('STOPPED', 'PENDING', 'RUNNING')
network_type = None
def rollback(self, net, activity): def rollback(self, net, activity):
with activity.sub_activity( with activity.sub_activity(
......
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