Commit e43cc43e by Czémán Arnold

Merge branch 'usernet' into occi-fromeo

parents a80bd8e7 cc5b0edb
......@@ -15,4 +15,14 @@
# You should have received a copy of the GNU General Public License along
# with CIRCLE. If not, see <http://www.gnu.org/licenses/>.
# Create your views here.
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 @@
"favico.js": "~0.3.5",
"datatables": "~1.10.4",
"chart.js": "2.3.0",
"clipboard": "~1.6.1"
"clipboard": "~1.6.1",
"jsPlumb": "2.5.7"
}
}
......@@ -18,8 +18,7 @@
"""Common settings and globals."""
# flake8: noqa
from os import environ
from os.path import (abspath, basename, dirname, join, normpath, isfile,
exists, expanduser)
from os.path import abspath, basename, join, normpath, isfile, expanduser
from sys import path
from subprocess import check_output
from uuid import getnode
......@@ -28,33 +27,14 @@ from django.core.exceptions import ImproperlyConfigured
from django.utils.translation import ugettext_lazy as _
from json import loads
# from socket import SOCK_STREAM
from util import get_env_variable
from static_and_pipeline import *
# 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
########## 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)
# Site name:
SITE_NAME = basename(BASE_DIR)
......@@ -136,125 +116,6 @@ USE_TZ = True
########## END GENERAL CONFIGURATION
########## 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.PipelineCachedStorage'
PIPELINE = {
'COMPILERS' : ('pipeline.compilers.less.LessCompiler',),
'LESS_ARGUMENTS': u'--include-path={}'.format(':'.join(STATICFILES_DIRS)),
'CSS_COMPRESSOR': 'pipeline.compressors.yuglify.YuglifyCompressor',
'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",
}
},
'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",
"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",
},
},
}
########## SECRET CONFIGURATION
# See: https://docs.djangoproject.com/en/dev/ref/settings/#secret-key
# Note: This key should only be used for development and testing.
......@@ -588,3 +449,7 @@ REQUEST_HOOK_URL = get_env_variable("REQUEST_HOOK_URL", "")
SSHKEY_EMAIL_ADD_KEY = False
TWO_FACTOR_ISSUER = get_env_variable("TWO_FACTOR_ISSUER", "CIRCLE")
DEFAULT_USERNET_VLAN_NAME = (
get_env_variable("DEFAULT_USERNET_VLAN_NAME", "usernet"))
USERNET_MAX = 2 ** 12
......@@ -112,6 +112,7 @@ if DEBUG:
PIPELINE["COMPILERS"] = (
'dashboard.compilers.DummyLessCompiler',
'pipeline.compilers.es6.ES6Compiler',
)
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
......@@ -19,8 +19,11 @@ from sys import exc_info
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.http import JsonResponse
from django.utils.translation import ugettext_lazy as _
from .models import HumanReadableException
......@@ -59,3 +62,29 @@ def handler403(request):
resp = render_to_response("403.html", ctx)
resp.status_code = 403
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):
class VmAddInterfaceForm(OperationForm):
network_type = 'vlan'
label = _('Vlan')
def __init__(self, *args, **kwargs):
choices = kwargs.pop('choices')
super(VmAddInterfaceForm, self).__init__(*args, **kwargs)
field = forms.ModelChoiceField(
queryset=choices, required=True, label=_('Vlan'))
queryset=choices, required=False,
label=self.label)
if not choices:
field.widget.attrs['disabled'] = 'disabled'
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):
......@@ -1299,6 +1310,9 @@ class UserEditForm(forms.ModelForm):
instance_limit = forms.IntegerField(
label=_('Instance limit'),
min_value=0, widget=NumberInput)
network_limit = forms.IntegerField(
label=_('Virtual network limit'),
min_value=0, widget=NumberInput)
two_factor_secret = forms.CharField(
label=_('Two-factor authentication secret'),
help_text=_("Remove the secret key to disable two-factor "
......@@ -1308,18 +1322,22 @@ class UserEditForm(forms.ModelForm):
super(UserEditForm, self).__init__(*args, **kwargs)
self.fields["instance_limit"].initial = (
self.instance.profile.instance_limit)
self.fields["network_limit"].initial = (
self.instance.profile.network_limit)
self.fields["two_factor_secret"].initial = (
self.instance.profile.two_factor_secret)
class Meta:
model = User
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):
user = super(UserEditForm, self).save()
user.profile.instance_limit = (
self.cleaned_data['instance_limit'] or None)
user.profile.network_limit = (
self.cleaned_data['network_limit'] or None)
user.profile.two_factor_secret = (
self.cleaned_data['two_factor_secret'] or None)
user.profile.save()
......
......@@ -114,6 +114,11 @@ class Command(BaseCommand):
vm.snat_to.add(net)
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
vg_all = self.create(VlanGroup, 'name', name='all')
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):
unique=True, blank=True, null=True, max_length=64,
help_text=_('Unique identifier of the person, e.g. a student number.'))
instance_limit = IntegerField(default=5)
network_limit = IntegerField(default=2,
verbose_name="Virtual network limit")
use_gravatar = BooleanField(
verbose_name=_("Use Gravatar"), default=True,
help_text=_("Whether to use email address as Gravatar profile image"))
......
......@@ -28,7 +28,7 @@ $(function() {
});
/* 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');
$.ajax({
......
......@@ -223,6 +223,7 @@ $(function () {
register_search($("#dashboard-group-search-form"), $("#dashboard-group-list"), generateGroupHTML);
register_search($("#dashboard-user-search-form"), $("#dashboard-user-list"), generateUserHTML);
register_search($("#dashboard-template-search-form"), $("#dashboard-template-list"), generateTemplateHTML);
register_search($("#dashboard-vxlan-search-form"), $("#dashboard-vxlan-list"), generateVxlanHTML);
/* notification message toggle */
$(document).on('click', ".notification-message-subject", function() {
......@@ -331,6 +332,18 @@ function generateNodeHTML(data, is_last) {
'</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 */
function compareVmByFav(a, b) {
if(a.fav && b.fav) {
......
......@@ -584,7 +584,8 @@ footer a, footer a:hover, footer a:visited {
}
#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;
}
......
{% 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>
......@@ -54,6 +54,12 @@
{% include "dashboard/index-users.html" %}
</div>
{% endif %}
{% if perms.network.create_vxlan %}
<div class="col-lg-4 col-sm-6">
{% include "dashboard/index-vxlans.html" %}
</div>
{% endif %}
</div>
</div>
{% endblock %}
......@@ -8,6 +8,15 @@
<i class="fa fa-{{op.icon}}"></i> {% trans "add interface" %}</a>
{% endif %}{% endwith %}
</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>
{% trans "Interfaces" %}
</h2>
......@@ -16,12 +25,28 @@
{% for i in instance.interface_set.all %}
<div>
<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 }}
{% if not i.host%}({% trans "unmanaged" %}){% endif %}
<i class="fa fa-{% if i.host %}globe{% else %}link{% endif %}"></i>
{% 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 %}
<a href="{{ i.host.get_absolute_url }}"
class="btn btn-default btn-xs">{% trans "edit" %}</a>
{% 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 %}
<span class="operation-wrapper">
<a href="{{op.get_url}}?interface={{ i.pk }}"
......@@ -30,6 +55,7 @@
</a>
</span>
{% endif %}{% endwith %}
{% endif %}
</h3>
{% if i.host %}
<div class="row">
......
......@@ -15,6 +15,9 @@
<!--<i class="fa fa-refresh fa-spin fa-2x"></i>-->
</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 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>
</div>
<div class="panel-body">
......
......@@ -29,6 +29,7 @@ from braces.views import LoginRequiredMixin
from dashboard.models import GroupProfile
from vm.models import Instance, Node, InstanceTemplate
from dashboard.views.vm import vm_ops
from network.models import Vxlan
from ..store_api import Store
......@@ -100,6 +101,11 @@ class IndexView(LoginRequiredMixin, TemplateView):
context['templates'] = InstanceTemplate.get_objects_with_level(
'operator', user, disregard_superuser=True).all()[:5]
# vxlan
if user.has_perm('network.create_vxlan'):
context['vxlans'] = Vxlan.get_objects_with_level(
'user', user).all()[:5]
# toplist
if settings.STORE_URL:
cache_key = "files-%d" % self.request.user.pk
......
......@@ -48,6 +48,7 @@ from common.models import (
split_activity_code,
)
from firewall.models import Vlan, Host, Rule
from network.models import Vxlan
from manager.scheduler import SchedulerError
from storage.models import Disk
from vm.models import (
......@@ -67,7 +68,7 @@ from ..forms import (
VmMigrateForm, VmDeployForm,
VmPortRemoveForm, VmPortAddForm,
VmRemoveInterfaceForm,
VmRenameForm,
VmRenameForm, VmAddUserInterfaceForm,
)
from request.models import TemplateAccessType, LeaseType
from request.forms import LeaseRequestForm, TemplateRequestForm
......@@ -349,6 +350,32 @@ class VmRemoveInterfaceView(FormOperationMixin, VmOperationView):
return val
class VmRemoveUserInterfaceView(FormOperationMixin, VmOperationView):
op = 'remove_user_interface'
form_class = VmRemoveInterfaceForm
show_in_toolbar = False
wait_for_result = 0.5
icon = 'times'
effect = "danger"
with_reload = True
def get_form_kwargs(self):
instance = self.get_op().instance
choices = instance.interface_set.all()
interface_pk = self.request.GET.get('interface')
if interface_pk:
try:
default = choices.get(pk=interface_pk)
except (ValueError, Interface.DoesNotExist):
raise Http404()
else:
default = None
val = super(VmRemoveUserInterfaceView, self).get_form_kwargs()
val.update({'choices': choices, 'default': default})
return val
class VmAddInterfaceView(FormOperationMixin, VmOperationView):
op = 'add_interface'
......@@ -357,10 +384,11 @@ class VmAddInterfaceView(FormOperationMixin, VmOperationView):
icon = 'globe'
effect = 'success'
with_reload = True
network_model = Vlan
def get_form_kwargs(self):
inst = self.get_op().instance
choices = Vlan.get_objects_with_level(
choices = self.network_model.get_objects_with_level(
"user", self.request.user).exclude(
vm_interface__instance__in=[inst])
val = super(VmAddInterfaceView, self).get_form_kwargs()
......@@ -368,6 +396,17 @@ class VmAddInterfaceView(FormOperationMixin, VmOperationView):
return val
class VmAddUserInterfaceView(VmAddInterfaceView):
op = 'add_user_interface'
form_class = VmAddUserInterfaceForm
show_in_toolbar = False
icon = 'link'
effect = 'success'
with_reload = True
network_model = Vxlan
class VmDiskModifyView(FormOperationMixin, VmOperationView):
show_in_toolbar = False
with_reload = True
......@@ -778,6 +817,8 @@ vm_ops = OrderedDict([
icon='times', effect="danger")),
('add_interface', VmAddInterfaceView),
('remove_interface', VmRemoveInterfaceView),
('add_user_interface', VmAddUserInterfaceView),
('remove_user_interface', VmRemoveUserInterfaceView),
('remove_port', VmPortRemoveView),
('add_port', VmPortAddView),
('renew', VmRenewView),
......
......@@ -20,13 +20,14 @@ from django.core.urlresolvers import reverse_lazy
from django.utils.translation import ugettext_lazy as _
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 firewall.models import (
Host, Vlan, Domain, Group, Record, BlacklistItem, Rule, VlanGroup,
SwitchPort, Firewall
)
from network.models import Vxlan
class LinkButton(BaseInput):
......@@ -348,3 +349,53 @@ class VlanGroupForm(ModelForm):
class Meta:
model = VlanGroup
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,109 @@
# You should have received a copy of the GNU General Public License along
# with CIRCLE. If not, see <http://www.gnu.org/licenses/>.
# Create your models here.
from django.db import models
from django.contrib.auth.models import User
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 acl.models import AclBase
from firewall.models import Vlan
from firewall.fields import val_alfanum
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(AclBase, 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.
"""
ACL_LEVELS = (
('user', _('user')),
('operator', _('operator')),
('owner', _('owner')),
)
# 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,
BooleanColumn)
from firewall.models import Host, Vlan, Domain, Group, Record, Rule, SwitchPort
from network.models import Vxlan
from vm.models import Interface
class MACColumn(Column):
......@@ -233,6 +235,16 @@ class VlanGroupTable(Table):
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):
fqdn = LinkColumn(
"network.record", args=[A("pk")],
......@@ -282,3 +294,16 @@ class FirewallRuleTable(Table):
'action', 'proto', 'actions')
order_by = '-pk'
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 @@
</div>
{% trans "Vlans" %}
<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>
</h3>
......@@ -44,7 +59,7 @@
</div>
{% trans "Domains" %}
<div>
<small>{% trans "Hosts are machines on the network" %}</small>
<small>{% trans "Domain names" %}</small>
</div>
</h3>
......@@ -59,7 +74,7 @@
</div>
{% trans "Records" %}
<div>
<small>{% trans "Hosts are machines on the network" %}</small>
<small>{% trans "DNS records" %}</small>
</div>
</h3>
......@@ -74,7 +89,7 @@
</div>
{% trans "Blacklist items" %}
<div>
<small>{% trans "Hosts are machines on the network" %}</small>
<small>{% trans "List of denied IPs and exceptions" %}</small>
</div>
</h3>
......@@ -89,7 +104,7 @@
</div>
{% trans "Rules" %}
<div>
<small>{% trans "Hosts are machines on the network" %}</small>
<small>{% trans "Firewall rules" %}</small>
</div>
</h3>
......@@ -104,7 +119,7 @@
</div>
{% trans "Switch ports" %}
<div>
<small>{% trans "Hosts are machines on the network" %}</small>
<small>{% trans "Switch ports" %}</small>
</div>
</h3>
......@@ -119,7 +134,7 @@
</div>
{% trans "Vlan groups" %}
<div>
<small>{% trans "Hosts are machines on the network" %}</small>
<small>{% trans "Groups of 802.1Q virtual networks" %}</small>
</div>
</h3>
......@@ -134,7 +149,7 @@
</div>
{% trans "Host groups" %}
<div>
<small>{% trans "Hosts are machines on the network" %}</small>
<small>{% trans "Groups of hosts are machines on the network" %}</small>
</div>
</h3>
</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 (
IndexView,
HostList, HostDetail, HostCreate, HostDelete,
VlanList, VlanDetail, VlanDelete, VlanCreate,
VxlanList, VxlanDetail, VxlanDelete, VxlanCreate, VxlanAclUpdateView,
DomainList, DomainDetail, DomainDelete, DomainCreate,
GroupList, GroupDetail, GroupDelete, GroupCreate,
RecordList, RecordDetail, RecordCreate, RecordDelete,
......@@ -30,7 +31,7 @@ from .views import (
FirewallList, FirewallDetail, FirewallCreate, FirewallDelete,
remove_host_group, add_host_group,
remove_switch_port_device, add_switch_port_device,
VlanAclUpdateView
VlanAclUpdateView, NetworkEditorView
)
urlpatterns = [
......@@ -125,6 +126,19 @@ urlpatterns = [
url('^rules/delete/(?P<pk>\d+)/$', RuleDelete.as_view(),
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
url('^hosts/(?P<pk>\d+)/remove/(?P<group_pk>\d+)/$', 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'),
),
]
......@@ -34,6 +34,7 @@ from django.db import IntegrityError
from django.dispatch import Signal
from django.utils import timezone
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.managers import QueryManager
......@@ -50,6 +51,8 @@ from .activity import (ActivityInProgressError, InstanceActivity)
from .common import BaseResourceConfigModel, Lease
from .network import Interface
from .node import Node, Trait
from network.models import EditorElement
logger = getLogger(__name__)
pre_state_changed = Signal(providing_args=["new_state"])
......@@ -256,6 +259,7 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
destroyed_at = DateTimeField(blank=True, null=True,
help_text=_("The virtual machine's time of "
"destruction."))
editor_elements = GenericRelation(EditorElement)
objects = Manager()
active = QueryManager(destroyed_at=None)
......
......@@ -25,6 +25,7 @@ from django.utils.translation import ugettext_lazy as _, ugettext_noop
from common.models import create_readable
from firewall.models import Vlan, Host
from network.models import Vxlan
from ..tasks import net_tasks
logger = getLogger(__name__)
......@@ -35,9 +36,15 @@ class InterfaceTemplate(Model):
"""Network interface template for an instance template.
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.'))
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,
help_text=_('If a firewall host (i.e. IP address '
'association) should be generated.'))
......@@ -54,7 +61,10 @@ class InterfaceTemplate(Model):
verbose_name_plural = _('interface templates')
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):
......@@ -64,8 +74,12 @@ class Interface(Model):
MODEL_TYPES = (('virtio', 'virtio'), ('ne2k_pci', 'ne2k_pci'),
('pcnet', 'pcnet'), ('rtl8139', 'rtl8139'),
('e1000', 'e1000'))
vlan = ForeignKey(Vlan, verbose_name=_('vlan'),
vlan = ForeignKey(Vlan, blank=True, null=True,
verbose_name=_('vlan'),
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)
instance = ForeignKey('Instance', verbose_name=_('instance'),
related_name='interface_set')
......@@ -77,50 +91,66 @@ class Interface(Model):
ordering = ("-vlan__managed", )
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
def mac(self):
try:
return self.host.mac
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
def generate_mac(cls, instance, vlan):
def generate_mac(cls, instance, vid, is_vxlan):
"""Generate MAC address for a VM instance on a VLAN.
"""
# 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):
word_fmt = '%.2X'
i = instance.id & 0xfffffff
v = vlan.vid & 0xfff
m = (0x02 << 40) | (i << 12) | v
i = instance.id & 0xffffff
v = vid & 0xfff
vx = 1 if is_vxlan else 0
m = (0x02 << 40) | (i << 16) | (vx << 12) | v
return EUI(m, dialect=mac_custom)
def get_vmnetwork_desc(self):
return {
'name': self.__unicode__(),
'bridge': 'cloud',
'bridge': ('cloud' if self.vxlan is None
else 'cloudx-%s' % self.vxlan.vni),
'mac': str(self.mac),
'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,
'vlan': self.vlan.vid,
'vxlan': self.vxlan.vni if self.vxlan is not None else None,
'model': self.model,
'managed': self.host is not None
}
@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.
"""
if managed:
if managed and vxlan is None:
host = Host()
host.vlan = vlan
# 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
# Get addresses from firewall
if base_activity is None:
......@@ -159,7 +189,7 @@ class Interface(Model):
else:
host = None
iface = cls(vlan=vlan, host=host, instance=instance)
iface = cls(vlan=vlan, vxlan=vxlan, host=host, instance=instance)
iface.save()
return iface
......@@ -180,7 +210,10 @@ class Interface(Model):
def save_as_template(self, instance_template):
"""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)
i.save()
return i
......@@ -202,32 +202,26 @@ class RemoteAgentOperation(EnsureAgentMixin, RemoteInstanceOperation):
concurrency_check = False
@register_operation
class AddInterfaceOperation(InstanceOperation):
id = 'add_interface'
name = _("add interface")
description = _("Add a new network interface for the specified VLAN to "
"the VM.")
class AddInterfaceOperationBase(InstanceOperation):
id = None
name = None
description = None
required_perms = ()
accept_states = ('STOPPED', 'PENDING', 'RUNNING')
network_type = None
def rollback(self, net, activity):
with activity.sub_activity(
'destroying_net',
readable_name=ugettext_noop("destroy network (rollback)")):
readable_name=ugettext_noop('destroy network (rollback)')):
net.destroy()
net.delete()
def _operation(self, activity, user, system, vlan, managed=None):
if not vlan.has_level(user, 'user'):
raise humanize_exception(ugettext_noop(
"User acces to vlan %(vlan)s is required."),
PermissionDenied(), vlan=vlan)
if managed is None:
managed = vlan.managed
net = Interface.create(base_activity=activity, instance=self.instance,
managed=managed, owner=user, vlan=vlan)
def _operation(self, activity, user, system, vlan, vxlan, managed):
net = Interface.create(base_activity=activity,
instance=self.instance,
managed=managed, owner=user,
vlan=vlan, vxlan=vxlan)
if self.instance.is_running:
try:
......@@ -242,8 +236,46 @@ class AddInterfaceOperation(InstanceOperation):
self.instance._restart_networking(parent_activity=activity)
def get_activity_name(self, kwargs):
return create_readable(ugettext_noop("add %(vlan)s interface"),
vlan=kwargs['vlan'])
return create_readable(ugettext_noop('add %(vlan)s interface'),
vlan=kwargs[self.network_type])
@register_operation
class AddInterfaceOperation(AddInterfaceOperationBase):
id = 'add_interface'
name = _('add interface')
description = _('Add a new network interface for the specified VLAN to '
'the VM.')
network_type = 'vlan'
def _operation(self, activity, user, system, vlan, managed=None):
if not vlan.has_level(user, 'user'):
raise humanize_exception(ugettext_noop(
'User acces to vlan %(vlan)s is required.'),
PermissionDenied(), vlan=vlan)
if managed is None:
managed = vlan.managed
super(AddInterfaceOperation, self)._operation(
activity=activity, user=user, system=system,
vlan=vlan, vxlan=None, managed=managed)
@register_operation
class AddUserInterfaceOperation(AddInterfaceOperationBase):
id = 'add_user_interface'
name = _('add user interface')
description = _('Add a new user network interface for the specified to '
'the VM.')
network_type = 'vxlan'
def _operation(self, activity, user, system, vxlan, managed=None):
if not vxlan.has_level(user, 'user'):
raise humanize_exception(ugettext_noop(
'User acces to vxlan %(vxlan)s is required.'),
PermissionDenied(), vxlan=vxlan)
super(AddUserInterfaceOperation, self)._operation(
activity=activity, user=user, system=system,
vlan=vxlan.vlan, vxlan=vxlan, managed=managed)
@register_operation
......@@ -624,6 +656,15 @@ class RemoveInterfaceOperation(InstanceOperation):
@register_operation
class RemoveUserInterfaceOperation(RemoveInterfaceOperation):
id = 'remove_user_interface'
name = _("remove user interface")
description = _("Remove the specified user network interface.")
required_perms = ()
accept_states = ('STOPPED', 'PENDING', 'RUNNING')
@register_operation
class RemovePortOperation(InstanceOperation):
id = 'remove_port'
name = _("close port")
......
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