Commit c150935a by Czémán Arnold

Merge branch 'master' into issue_477

requirements: upgrade ldap packages
settings: remove NISGroupType

Conflicts:
	circle/dashboard/models.py
	requirements/base.txt
parents 1746ed9a aa92a44d
""" """
Creates Levels for all installed apps that have levels. Creates Levels for all installed apps that have levels.
""" """
from django.db.models import get_models, signals from django.db.models import signals
from django.apps import apps
from django.db import DEFAULT_DB_ALIAS from django.db import DEFAULT_DB_ALIAS
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from ..models import Level, AclBase from ..models import Level, AclBase
def create_levels(app, created_models, verbosity, db=DEFAULT_DB_ALIAS, def create_levels(app_config, verbosity=False, using=DEFAULT_DB_ALIAS,
**kwargs): **kwargs):
"""Create and set the weights of the configured Levels. """Create and set the weights of the configured Levels.
Based on django.contrib.auth.management.__init__.create_permissions""" Based on django.contrib.auth.management.__init__.create_permissions"""
# if not router.allow_migrate(db, auth_app.Permission): # if not router.allow_migrate(using, auth_app.Permission):
# return # return
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
app_models = [k for k in get_models(app) if AclBase in k.__bases__] app_models = [k for k in apps.get_models(app_config)
if AclBase in k.__bases__]
print "Creating levels for models: %s." % ", ".join( print "Creating levels for models: %s." % ", ".join(
[m.__name__ for m in app_models]) [m.__name__ for m in app_models])
...@@ -31,7 +33,7 @@ def create_levels(app, created_models, verbosity, db=DEFAULT_DB_ALIAS, ...@@ -31,7 +33,7 @@ def create_levels(app, created_models, verbosity, db=DEFAULT_DB_ALIAS,
for klass in app_models: for klass in app_models:
# Force looking up the content types in the current database # Force looking up the content types in the current database
# before creating foreign keys to them. # before creating foreign keys to them.
ctype1 = ContentType.objects.db_manager(db).get_for_model(klass) ctype1 = ContentType.objects.db_manager(using).get_for_model(klass)
ctypes.add(ctype1) ctypes.add(ctype1)
weight = 0 weight = 0
try: try:
...@@ -46,7 +48,7 @@ def create_levels(app, created_models, verbosity, db=DEFAULT_DB_ALIAS, ...@@ -46,7 +48,7 @@ def create_levels(app, created_models, verbosity, db=DEFAULT_DB_ALIAS,
# Find all the Levels that have a content_type for a model we're # Find all the Levels that have a content_type for a model we're
# looking for. We don't need to check for codenames since we already have # looking for. We don't need to check for codenames since we already have
# a list of the ones we're going to create. # a list of the ones we're going to create.
all_levels = set(Level.objects.using(db).filter( all_levels = set(Level.objects.using(using).filter(
content_type__in=ctypes, content_type__in=ctypes,
).values_list( ).values_list(
"content_type", "codename" "content_type", "codename"
...@@ -57,7 +59,7 @@ def create_levels(app, created_models, verbosity, db=DEFAULT_DB_ALIAS, ...@@ -57,7 +59,7 @@ def create_levels(app, created_models, verbosity, db=DEFAULT_DB_ALIAS,
for ctype, (codename, name) in searched_levels for ctype, (codename, name) in searched_levels
if (ctype.pk, codename) not in all_levels if (ctype.pk, codename) not in all_levels
] ]
Level.objects.using(db).bulk_create(levels) Level.objects.using(using).bulk_create(levels)
if verbosity >= 2: if verbosity >= 2:
print("Adding levels [%s]." % ", ".join(unicode(l) for l in levels)) print("Adding levels [%s]." % ", ".join(unicode(l) for l in levels))
print("Searched: [%s]." % ", ".join( print("Searched: [%s]." % ", ".join(
...@@ -70,5 +72,5 @@ def create_levels(app, created_models, verbosity, db=DEFAULT_DB_ALIAS, ...@@ -70,5 +72,5 @@ def create_levels(app, created_models, verbosity, db=DEFAULT_DB_ALIAS,
content_type=ctype).update(weight=weight) content_type=ctype).update(weight=weight)
signals.post_syncdb.connect( signals.post_migrate.connect(
create_levels, dispatch_uid="circle.acl.management.create_levels") create_levels, dispatch_uid="circle.acl.management.create_levels")
...@@ -18,7 +18,7 @@ ...@@ -18,7 +18,7 @@
import logging import logging
from django.contrib.auth.models import User, Group from django.contrib.auth.models import User, Group
from django.contrib.contenttypes.generic import ( from django.contrib.contenttypes.fields import (
GenericForeignKey, GenericRelation GenericForeignKey, GenericRelation
) )
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
......
...@@ -21,6 +21,7 @@ ...@@ -21,6 +21,7 @@
"intro.js": "0.9.0", "intro.js": "0.9.0",
"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"
} }
} }
...@@ -12,9 +12,10 @@ def update_permissions_after_migration(sender, **kwargs): ...@@ -12,9 +12,10 @@ def update_permissions_after_migration(sender, **kwargs):
""" """
from django.conf import settings from django.conf import settings
from django.db.models import get_models from django.apps import apps
from django.contrib.auth.management import create_permissions from django.contrib.auth.management import create_permissions
create_permissions(sender, get_models(), 2 if settings.DEBUG else 0) create_permissions(sender, apps.get_models(), 2 if settings.DEBUG else 0)
post_migrate.connect(update_permissions_after_migration) post_migrate.connect(update_permissions_after_migration)
...@@ -166,95 +166,95 @@ if exists(p): ...@@ -166,95 +166,95 @@ if exists(p):
STATICFILES_DIRS.append(p) STATICFILES_DIRS.append(p)
STATICFILES_STORAGE = 'pipeline.storage.PipelineCachedStorage' STATICFILES_STORAGE = 'pipeline.storage.PipelineCachedStorage'
PIPELINE_COMPILERS = (
'pipeline.compilers.less.LessCompiler', PIPELINE = {
) 'COMPILERS' : ('pipeline.compilers.less.LessCompiler',),
PIPELINE_CSS_COMPRESSOR = 'pipeline.compressors.yuglify.YuglifyCompressor' 'LESS_ARGUMENTS': u'--include-path={}'.format(':'.join(STATICFILES_DIRS)),
# PIPELINE_JS_COMPRESSOR = 'pipeline.compressors.slimit.SlimItCompressor' 'CSS_COMPRESSOR': 'pipeline.compressors.yuglify.YuglifyCompressor',
PIPELINE_JS_COMPRESSOR = None 'JS_COMPRESSOR': None,
PIPELINE_DISABLE_WRAPPER = True 'DISABLE_WRAPPER': True,
PIPELINE_LESS_ARGUMENTS = u'--include-path={}'.format(':'.join(STATICFILES_DIRS)) 'STYLESHEETS': {
PIPELINE_CSS = { "all": {"source_filenames": (
"all": {"source_filenames": ( "compile_bootstrap.less",
"compile_bootstrap.less", "bootstrap/dist/css/bootstrap-theme.css",
"bootstrap/dist/css/bootstrap-theme.css", "fontawesome/css/font-awesome.css",
"fontawesome/css/font-awesome.css", "jquery-simple-slider/css/simple-slider.css",
"jquery-simple-slider/css/simple-slider.css", "intro.js/introjs.css",
"intro.js/introjs.css", "template.less",
"template.less", "dashboard/dashboard.less",
"dashboard/dashboard.less", "network/network.less",
"network/network.less", "autocomplete_light/vendor/select2/dist/css/select2.css",
"autocomplete_light/style.css", "autocomplete_light/select2.css",
), ),
"output_filename": "all.css", "output_filename": "all.css",
} }
}
PIPELINE_JS = {
"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",
"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",
"autocomplete_light/autocomplete.js",
"autocomplete_light/widget.js",
"autocomplete_light/addanother.js",
"autocomplete_light/text_widget.js",
"autocomplete_light/remote.js",
),
"output_filename": "all.js",
},
"vm-detail": {"source_filenames": (
"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": ( 'JAVASCRIPT': {
"chart.js/dist/Chart.min.js", "all": {"source_filenames": (
"dashboard/datastore-details.js" # "jquery/dist/jquery.js", # included separately
), "bootbox/bootbox.js",
"output_filename": "datastore.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 ########## SECRET CONFIGURATION
# See: https://docs.djangoproject.com/en/dev/ref/settings/#secret-key # See: https://docs.djangoproject.com/en/dev/ref/settings/#secret-key
# Note: This key should only be used for development and testing. # Note: This key should only be used for development and testing.
...@@ -278,26 +278,31 @@ FIXTURE_DIRS = ( ...@@ -278,26 +278,31 @@ FIXTURE_DIRS = (
########## TEMPLATE CONFIGURATION ########## TEMPLATE CONFIGURATION
# See: https://docs.djangoproject.com/en/dev/ref/settings/#template-context-processors
TEMPLATE_CONTEXT_PROCESSORS = (
'django.contrib.auth.context_processors.auth',
'django.core.context_processors.debug',
'django.core.context_processors.i18n',
'django.core.context_processors.media',
'django.core.context_processors.static',
'django.core.context_processors.tz',
'django.contrib.messages.context_processors.messages',
'django.core.context_processors.request',
'dashboard.context_processors.notifications',
'dashboard.context_processors.extract_settings',
'dashboard.context_processors.broadcast_messages',
)
# See: https://docs.djangoproject.com/en/dev/ref/settings/#template-dirs # See: https://docs.djangoproject.com/en/dev/ref/settings/#TEMPLATES
TEMPLATE_DIRS = ( TEMPLATES = [{
normpath(join(SITE_ROOT, '../../site-circle/templates')), 'BACKEND': 'django.template.backends.django.DjangoTemplates',
normpath(join(SITE_ROOT, 'templates')), 'DIRS' : (
) normpath(join(SITE_ROOT, '../../site-circle/templates')),
normpath(join(SITE_ROOT, 'templates')),
),
'APP_DIRS': True,
'OPTIONS': {
'context_processors': (
'django.contrib.auth.context_processors.auth',
'django.template.context_processors.debug',
'django.template.context_processors.i18n',
'django.template.context_processors.media',
'django.template.context_processors.static',
'django.template.context_processors.tz',
'django.contrib.messages.context_processors.messages',
'django.template.context_processors.request',
'dashboard.context_processors.notifications',
'dashboard.context_processors.extract_settings',
'dashboard.context_processors.broadcast_messages',
),
},
}]
########## END TEMPLATE CONFIGURATION ########## END TEMPLATE CONFIGURATION
...@@ -335,6 +340,10 @@ DJANGO_APPS = ( ...@@ -335,6 +340,10 @@ DJANGO_APPS = (
# Useful template tags: # Useful template tags:
# 'django.contrib.humanize', # 'django.contrib.humanize',
# Django autocomplete light
# it needs registering before django admin
'dal',
'dal_select2',
# Admin panel and documentation: # Admin panel and documentation:
'django.contrib.admin', 'django.contrib.admin',
# 'django.contrib.admindocs', # 'django.contrib.admindocs',
...@@ -347,7 +356,6 @@ THIRD_PARTY_APPS = ( ...@@ -347,7 +356,6 @@ THIRD_PARTY_APPS = (
'taggit', 'taggit',
'statici18n', 'statici18n',
'django_sshkey', 'django_sshkey',
'autocomplete_light',
'pipeline', 'pipeline',
) )
...@@ -502,6 +510,10 @@ if get_env_variable('DJANGO_SAML', 'FALSE') == 'TRUE': ...@@ -502,6 +510,10 @@ if get_env_variable('DJANGO_SAML', 'FALSE') == 'TRUE':
'metadata': {'local': [remote_metadata], }, 'metadata': {'local': [remote_metadata], },
'key_file': join(SITE_ROOT, 'samlcert.key'), # private part 'key_file': join(SITE_ROOT, 'samlcert.key'), # private part
'cert_file': join(SITE_ROOT, 'samlcert.pem'), # public part 'cert_file': join(SITE_ROOT, 'samlcert.pem'), # public part
'encryption_keypairs': [{
'key_file': join(SITE_ROOT, 'samlcert.key'), # private part
'cert_file': join(SITE_ROOT, 'samlcert.pem'), # public part
}]
} }
try: try:
SAML_CONFIG += loads(get_env_variable('DJANGO_SAML_SETTINGS')) SAML_CONFIG += loads(get_env_variable('DJANGO_SAML_SETTINGS'))
...@@ -591,7 +603,7 @@ TWO_FACTOR_ISSUER = get_env_variable("TWO_FACTOR_ISSUER", "CIRCLE") ...@@ -591,7 +603,7 @@ TWO_FACTOR_ISSUER = get_env_variable("TWO_FACTOR_ISSUER", "CIRCLE")
if get_env_variable('LDAP_AUTH', 'FALSE') == 'TRUE': if get_env_variable('LDAP_AUTH', 'FALSE') == 'TRUE':
import ldap import ldap
from django_auth_ldap.config import ( from django_auth_ldap.config import (
LDAPSearch, PosixGroupType, NISGroupType, MemberDNGroupType, LDAPSearch, PosixGroupType, MemberDNGroupType,
GroupOfNamesType, GroupOfUniqueNamesType, ActiveDirectoryGroupType, GroupOfNamesType, GroupOfUniqueNamesType, ActiveDirectoryGroupType,
OrganizationalRoleGroupType, OrganizationalRoleGroupType,
) )
...@@ -607,7 +619,6 @@ if get_env_variable('LDAP_AUTH', 'FALSE') == 'TRUE': ...@@ -607,7 +619,6 @@ if get_env_variable('LDAP_AUTH', 'FALSE') == 'TRUE':
LDAP_GROUP_MAP = { LDAP_GROUP_MAP = {
"POSIX": PosixGroupType(), "POSIX": PosixGroupType(),
"NIS": NISGroupType(),
"MEMBER_DN": MemberDNGroupType(LDAP_GROUP_MEMBER_ATTRIBUTE), "MEMBER_DN": MemberDNGroupType(LDAP_GROUP_MEMBER_ATTRIBUTE),
"GROUP_OF_NAMES": GroupOfNamesType(), "GROUP_OF_NAMES": GroupOfNamesType(),
"GROUP_OF_UNIQUE_NAMES": GroupOfUniqueNamesType(), "GROUP_OF_UNIQUE_NAMES": GroupOfUniqueNamesType(),
......
...@@ -27,7 +27,7 @@ from base import * # noqa ...@@ -27,7 +27,7 @@ from base import * # noqa
DEBUG = True DEBUG = True
# See: https://docs.djangoproject.com/en/dev/ref/settings/#template-debug # See: https://docs.djangoproject.com/en/dev/ref/settings/#template-debug
TEMPLATE_DEBUG = DEBUG TEMPLATES[0]['OPTIONS']['debug'] = DEBUG
########## END DEBUG CONFIGURATION ########## END DEBUG CONFIGURATION
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTOCOL', 'https') SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTOCOL', 'https')
...@@ -110,8 +110,10 @@ if DEBUG: ...@@ -110,8 +110,10 @@ if DEBUG:
from django.dispatch import Signal from django.dispatch import Signal
Signal.send_robust = Signal.send Signal.send_robust = Signal.send
PIPELINE_COMPILERS = ( PIPELINE["COMPILERS"] = (
'dashboard.compilers.DummyLessCompiler', 'dashboard.compilers.DummyLessCompiler',
) )
ADMIN_ENABLED = True ADMIN_ENABLED = True
ALLOWED_HOSTS = ['*']
...@@ -14,9 +14,10 @@ ...@@ -14,9 +14,10 @@
# #
# 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/>.
import os import os
from .base import * # noqa from .base import * # flake8:noqa
# fix https://github.com/django-nose/django-nose/issues/197 # fix https://github.com/django-nose/django-nose/issues/197
......
...@@ -38,7 +38,11 @@ INSTALLED_APPS += ( ...@@ -38,7 +38,11 @@ INSTALLED_APPS += (
'django_nose', 'django_nose',
) )
TEST_RUNNER = 'django_nose.NoseTestSuiteRunner' TEST_RUNNER = 'django_nose.NoseTestSuiteRunner'
NOSE_ARGS = ['--with-doctest', '--exclude-dir=dashboard/tests/selenium'] NOSE_ARGS = [
'--with-doctest',
'--exclude-dir=dashboard/tests/selenium',
'--exclude=circle'
]
PASSWORD_HASHERS = ['django.contrib.auth.hashers.MD5PasswordHasher'] PASSWORD_HASHERS = ['django.contrib.auth.hashers.MD5PasswordHasher']
CACHES = { CACHES = {
...@@ -59,7 +63,7 @@ for i in LOCAL_APPS: ...@@ -59,7 +63,7 @@ for i in LOCAL_APPS:
# don't print SQL queries # don't print SQL queries
LOGGING['handlers']['null'] = {'level': "DEBUG", LOGGING['handlers']['null'] = {'level': "DEBUG",
'class': "django.utils.log.NullHandler"} 'class': "logging.NullHandler"}
LOGGING['loggers']['django.db.backends'] = { LOGGING['loggers']['django.db.backends'] = {
'handlers': ['null'], 'handlers': ['null'],
'propagate': False, 'propagate': False,
......
...@@ -15,13 +15,16 @@ ...@@ -15,13 +15,16 @@
# 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/>.
from django.conf.urls import patterns, include, url from django.conf.urls import include, url
from django.views.generic import TemplateView from django.views.generic import TemplateView
from django.conf import settings from django.conf import settings
from django.contrib import admin from django.contrib import admin
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.shortcuts import redirect from django.shortcuts import redirect
from django.contrib.auth.views import (
password_reset_confirm, password_reset
)
from circle.settings.base import get_env_variable from circle.settings.base import get_env_variable
...@@ -33,9 +36,7 @@ from firewall.views import add_blacklist_item ...@@ -33,9 +36,7 @@ from firewall.views import add_blacklist_item
admin.autodiscover() admin.autodiscover()
urlpatterns = patterns( 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),
...@@ -45,12 +46,11 @@ urlpatterns = patterns( ...@@ -45,12 +46,11 @@ urlpatterns = patterns(
# django/contrib/auth/urls.py (care when new version) # django/contrib/auth/urls.py (care when new version)
url((r'^accounts/reset/(?P<uidb64>[0-9A-Za-z_\-]+)/' url((r'^accounts/reset/(?P<uidb64>[0-9A-Za-z_\-]+)/'
r'(?P<token>[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$'), r'(?P<token>[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$'),
'django.contrib.auth.views.password_reset_confirm', password_reset_confirm,
{'set_password_form': CircleSetPasswordForm}, {'set_password_form': CircleSetPasswordForm},
name='accounts.password_reset_confirm' name='accounts.password_reset_confirm'
), ),
url(r'^accounts/password/reset/$', ("django.contrib.auth.views." url(r'^accounts/password/reset/$', password_reset,
"password_reset"),
{'password_reset_form': CirclePasswordResetForm}, {'password_reset_form': CirclePasswordResetForm},
name="accounts.password-reset", name="accounts.password-reset",
), ),
...@@ -73,27 +73,24 @@ urlpatterns = patterns( ...@@ -73,27 +73,24 @@ urlpatterns = patterns(
name="info.support"), name="info.support"),
url(r'^info/resize-how-to/$', ResizeHelpView.as_view(), url(r'^info/resize-how-to/$', ResizeHelpView.as_view(),
name="info.resize"), name="info.resize"),
) ]
if 'rosetta' in settings.INSTALLED_APPS: if 'rosetta' in settings.INSTALLED_APPS:
urlpatterns += patterns( urlpatterns += [
'',
url(r'^rosetta/', include('rosetta.urls')), url(r'^rosetta/', include('rosetta.urls')),
) ]
if settings.ADMIN_ENABLED: if settings.ADMIN_ENABLED:
urlpatterns += patterns( urlpatterns += [
'',
url(r'^admin/', include(admin.site.urls)), url(r'^admin/', include(admin.site.urls)),
) ]
if get_env_variable('DJANGO_SAML', 'FALSE') == 'TRUE': if get_env_variable('DJANGO_SAML', 'FALSE') == 'TRUE':
urlpatterns += patterns( urlpatterns += [
'', url(r'^saml2/', include('djangosaml2.urls')),
(r'^saml2/', include('djangosaml2.urls')), ]
)
handler500 = 'common.views.handler500' handler500 = 'common.views.handler500'
handler403 = 'common.views.handler403' handler403 = 'common.views.handler403'
...@@ -45,7 +45,8 @@ def handler500(request): ...@@ -45,7 +45,8 @@ def handler500(request):
logger.exception("unhandled exception") logger.exception("unhandled exception")
ctx = get_context(request, exception) ctx = get_context(request, exception)
try: try:
resp = render_to_response("500.html", ctx, RequestContext(request)) resp = render_to_response("500.html", ctx,
RequestContext(request).flatten())
except: except:
resp = render_to_response("500.html", ctx) resp = render_to_response("500.html", ctx)
resp.status_code = 500 resp.status_code = 500
......
...@@ -31,7 +31,7 @@ from django.contrib.auth.models import User, Group ...@@ -31,7 +31,7 @@ from django.contrib.auth.models import User, Group
from django.core.validators import URLValidator from django.core.validators import URLValidator
from django.core.exceptions import PermissionDenied, ValidationError from django.core.exceptions import PermissionDenied, ValidationError
import autocomplete_light from dal import autocomplete
from crispy_forms.helper import FormHelper from crispy_forms.helper import FormHelper
from crispy_forms.layout import ( from crispy_forms.layout import (
Layout, Div, BaseInput, Field, HTML, Submit, TEMPLATE_PACK, Fieldset Layout, Div, BaseInput, Field, HTML, Submit, TEMPLATE_PACK, Fieldset
...@@ -43,7 +43,6 @@ from crispy_forms.bootstrap import FormActions ...@@ -43,7 +43,6 @@ from crispy_forms.bootstrap import FormActions
from django import forms from django import forms
from django.contrib.auth.forms import UserCreationForm as OrgUserCreationForm from django.contrib.auth.forms import UserCreationForm as OrgUserCreationForm
from django.forms.widgets import TextInput, HiddenInput from django.forms.widgets import TextInput, HiddenInput
from django.template import Context
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.utils.html import escape, format_html from django.utils.html import escape, format_html
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
...@@ -67,6 +66,7 @@ from .validators import domain_validator ...@@ -67,6 +66,7 @@ from .validators import domain_validator
from dashboard.models import ConnectCommand, create_profile from dashboard.models import ConnectCommand, create_profile
LANGUAGES_WITH_CODE = ((l[0], string_concat(l[1], " (", l[0], ")")) LANGUAGES_WITH_CODE = ((l[0], string_concat(l[1], " (", l[0], ")"))
for l in LANGUAGES) for l in LANGUAGES)
...@@ -1180,8 +1180,7 @@ class AnyTag(Div): ...@@ -1180,8 +1180,7 @@ class AnyTag(Div):
fields += render_field(field, form, form_style, context, fields += render_field(field, form, form_style, context,
template_pack=template_pack) template_pack=template_pack)
return render_to_string(self.template, Context({'tag': self, return render_to_string(self.template, {'tag': self, 'fields': fields})
'fields': fields}))
class WorkingBaseInput(BaseInput): class WorkingBaseInput(BaseInput):
...@@ -1334,27 +1333,31 @@ class UserEditForm(forms.ModelForm): ...@@ -1334,27 +1333,31 @@ class UserEditForm(forms.ModelForm):
class AclUserOrGroupAddForm(forms.Form): class AclUserOrGroupAddForm(forms.Form):
name = forms.CharField(widget=autocomplete_light.TextWidget( name = forms.CharField(
'AclUserGroupAutocomplete', widget=autocomplete.ListSelect2(
attrs={'class': 'form-control', url='autocomplete.acl.user-group',
'placeholder': _("Name of group or user")})) attrs={'class': 'form-control',
'data-html': 'true',
'data-placeholder': _("Name of group or user")}))
class TransferOwnershipForm(forms.Form): class TransferOwnershipForm(forms.Form):
name = forms.CharField( name = forms.CharField(
widget=autocomplete_light.TextWidget( widget=autocomplete.ListSelect2(
'AclUserAutocomplete', url='autocomplete.acl.user',
attrs={'class': 'form-control', attrs={'class': 'form-control',
'placeholder': _("Name of user")}), 'data-html': 'true',
'data-placeholder': _("Name of user")}),
label=_("E-mail address or identifier of user")) label=_("E-mail address or identifier of user"))
class AddGroupMemberForm(forms.Form): class AddGroupMemberForm(forms.Form):
new_member = forms.CharField( new_member = forms.CharField(
widget=autocomplete_light.TextWidget( widget=autocomplete.ListSelect2(
'AclUserAutocomplete', url='autocomplete.acl.user',
attrs={'class': 'form-control', attrs={'class': 'form-control',
'placeholder': _("Name of user")}), 'data-html': 'true',
'data-placeholder': _("Name of user")}),
label=_("E-mail address or identifier of user")) label=_("E-mail address or identifier of user"))
...@@ -1538,6 +1541,10 @@ class VmResourcesForm(forms.ModelForm): ...@@ -1538,6 +1541,10 @@ class VmResourcesForm(forms.ModelForm):
fields = ('num_cores', 'priority', 'ram_size', ) fields = ('num_cores', 'priority', 'ram_size', )
class VmRenameForm(forms.Form):
new_name = forms.CharField()
vm_search_choices = ( vm_search_choices = (
("owned", _("owned")), ("owned", _("owned")),
("shared", _("shared")), ("shared", _("shared")),
...@@ -1547,6 +1554,8 @@ vm_search_choices = ( ...@@ -1547,6 +1554,8 @@ vm_search_choices = (
class VmListSearchForm(forms.Form): class VmListSearchForm(forms.Form):
use_required_attribute = False
s = forms.CharField(widget=forms.TextInput(attrs={ s = forms.CharField(widget=forms.TextInput(attrs={
'class': "form-control input-tags", 'class': "form-control input-tags",
'placeholder': _("Search...") 'placeholder': _("Search...")
...@@ -1571,6 +1580,8 @@ class VmListSearchForm(forms.Form): ...@@ -1571,6 +1580,8 @@ class VmListSearchForm(forms.Form):
class TemplateListSearchForm(forms.Form): class TemplateListSearchForm(forms.Form):
use_required_attribute = False
s = forms.CharField(widget=forms.TextInput(attrs={ s = forms.CharField(widget=forms.TextInput(attrs={
'class': "form-control input-tags", 'class': "form-control input-tags",
'placeholder': _("Search...") 'placeholder': _("Search...")
...@@ -1590,6 +1601,8 @@ class TemplateListSearchForm(forms.Form): ...@@ -1590,6 +1601,8 @@ class TemplateListSearchForm(forms.Form):
class UserListSearchForm(forms.Form): class UserListSearchForm(forms.Form):
use_required_attribute = False
s = forms.CharField(widget=forms.TextInput(attrs={ s = forms.CharField(widget=forms.TextInput(attrs={
'class': "form-control input-tags", 'class': "form-control input-tags",
'placeholder': _("Search...") 'placeholder': _("Search...")
......
...@@ -18,33 +18,40 @@ ...@@ -18,33 +18,40 @@
from __future__ import unicode_literals, absolute_import from __future__ import unicode_literals, absolute_import
import logging import logging
from optparse import make_option
from django.contrib.auth.models import User from django.contrib.auth.models import User, Group, Permission
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from django.db.models import Q
from firewall.models import Vlan, VlanGroup, Domain, Firewall, Rule from firewall.models import Vlan, VlanGroup, Domain, Firewall, Rule, Host
from firewall.fields import mac_custom
from storage.models import DataStore from storage.models import DataStore
from vm.models import Lease from vm.models import Lease, Node
from dashboard.models import GroupProfile, Profile
from netaddr import IPAddress, EUI
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class Command(BaseCommand): class Command(BaseCommand):
option_list = BaseCommand.option_list + ( def add_arguments(self, parser):
make_option('--force', action="store_true"), parser.add_argument('--force', action="store_true")
make_option('--external-net'), parser.add_argument('--external-net')
make_option('--management-net'), parser.add_argument('--management-net')
make_option('--vm-net'), parser.add_argument('--vm-net')
make_option('--external-if'), parser.add_argument('--external-if')
make_option('--management-if'), parser.add_argument('--management-if')
make_option('--vm-if'), parser.add_argument('--vm-if')
make_option('--datastore-queue'), parser.add_argument('--datastore-queue')
make_option('--firewall-queue'), parser.add_argument('--firewall-queue')
make_option('--admin-user'), parser.add_argument('--admin-user')
make_option('--admin-pass'), parser.add_argument('--admin-pass')
) parser.add_argument('--node-hostname')
parser.add_argument('--node-mac')
parser.add_argument('--node-ip')
parser.add_argument('--node-name')
parser.add_argument('--kvm-present', action="store_true")
def create(self, model, field, **kwargs): def create(self, model, field, **kwargs):
value = kwargs[field] value = kwargs[field]
...@@ -57,21 +64,24 @@ class Command(BaseCommand): ...@@ -57,21 +64,24 @@ class Command(BaseCommand):
else: else:
return qs[0] return qs[0]
# http://docs.saltstack.com/en/latest/ref/states/all/salt.states.cmd.html # http://docs.saltstack.com/en/latest/ref/states/all/salt.states.cmd.html
def print_state(self): def print_state(self):
print "\nchanged=%s" % ("yes" if self.changed else "no") self.stdout.write("\nchanged=%s" % ("yes" if self.changed else "no"))
def handle(self, *args, **options): def handle(self, *args, **options):
self.changed = False self.changed = False
# from pdb import set_trace; set_trace()
if (DataStore.objects.exists() and Vlan.objects.exists() and if (DataStore.objects.exists() and Vlan.objects.exists() and
not options['force']): not options['force']):
return self.print_state() self.print_state()
return
admin = self.create(User, 'username', username=options['admin_user'], admin = self.create(User, 'username', username=options['admin_user'],
is_superuser=True, is_staff=True) is_superuser=True, is_staff=True)
admin.set_password(options['admin_pass']) admin.set_password(options['admin_pass'])
admin.save() admin.save()
self.create(Profile, 'user', user=admin)
self.create(DataStore, 'path', path='/datastore', name='default', self.create(DataStore, 'path', path='/datastore', name='default',
hostname=options['datastore_queue']) hostname=options['datastore_queue'])
...@@ -152,5 +162,112 @@ class Command(BaseCommand): ...@@ -152,5 +162,112 @@ class Command(BaseCommand):
self.create(Rule, 'description', description='allow man->net', self.create(Rule, 'description', description='allow man->net',
direction='out', action='accept', direction='out', action='accept',
foreign_network=vg_net, vlan=man) foreign_network=vg_net, vlan=man)
node_ip = IPAddress(options['node_ip'])
return self.print_state() node_mac = EUI(options['node_mac'], dialect=mac_custom)
node_host = Host.objects.filter(ipv4=node_ip).first()
if node_host is None:
node_host = self.create(Host, 'mac', mac=node_mac,
hostname=options['node_hostname'],
ipv4=node_ip, vlan=man, owner=admin)
else:
Host.objects.filter(pk=node_host.pk).update(
mac=node_mac, hostname=options['node_hostname'],
ipv4=node_ip, vlan=man, owner=admin)
node_host.refresh_from_db()
self.create(Node, 'name', name=options['node_name'], host=node_host,
priority=1, enabled=True, schedule_enabled=True)
# creating groups
susers = self.create(Group, 'name', name='Superusers')
pusers = self.create(Group, 'name', name='Powerusers')
users = self.create(Group, 'name', name='Users')
# creating group profiles
self.create(GroupProfile, 'group', group=susers)
self.create(GroupProfile, 'group', group=pusers)
self.create(GroupProfile, 'group', group=users)
# specifying group permissions
user_permissions = [
'create_vm',
'config_ports',
]
puser_permissions = [
'use_autocomplete',
'config_ports',
'create_vm',
'create_empty_disk',
'download_disk',
'resize_disk',
'access_console',
'change_resources',
'set_resources',
'change_template_resources',
'create_template',
]
suser_permissions = [
'add_group',
'use_autocomplete',
'create_empty_disk',
'download_disk',
'access_console',
'change_resources',
'config_ports',
'create_vm',
'recover',
'set_resources',
'change_template_resources',
'create_base_template',
'create_template'
]
# set group permissions
susers.permissions.set(self._get_permissions(suser_permissions))
pusers.permissions.set(self._get_permissions(puser_permissions))
users.permissions.set(self._get_permissions(user_permissions))
# creating users and their profiles
useruser = self.create(User, 'username', username='user',
is_superuser=False, is_staff=False)
useruser.set_password("user")
useruser.save()
self.create(Profile, 'user', user=useruser)
poweruser = self.create(User, 'username', username="poweruser",
is_superuser=False, is_staff=False)
poweruser.set_password("poweruser")
poweruser.save()
self.create(Profile, 'user', user=poweruser)
superuser = self.create(User, 'username', username="superuser",
is_superuser=False, is_staff=False)
superuser.set_password("superuser")
superuser.save()
self.create(Profile, 'user', user=superuser)
# adding users o groups
users.user_set.add(useruser)
pusers.user_set.add(poweruser)
susers.user_set.add(superuser)
# add groups to vm vlan
vm.set_level(users, 'user')
vm.set_level(pusers, 'user')
vm.set_level(susers, 'user')
# notify admin if there is no harware virtualization
if not options['kvm_present']:
admin.profile.notify("hardware virtualization",
"No hardware virtualization detected, "
"your hardware does not support it or "
"not enabled in BIOS.")
self.print_state()
def _get_permissions(self, code_names):
query = Q()
for cn in code_names:
query |= Q(codename=cn)
return Permission.objects.filter(query)
# -*- coding: utf-8 -*-
# Generated by Django 1.11.3 on 2017-07-07 19:09
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dashboard', '0005_profile_two_factor_secret'),
]
operations = [
migrations.AlterField(
model_name='connectcommand',
name='name',
field=models.CharField(help_text='Name of your custom command.', max_length=128, verbose_name='name'),
),
]
# -*- coding: utf-8 -*-
# Generated by Django 1.11.6 on 2018-07-10 23:41
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('dashboard', '0006_auto_20170707_1909'),
('dashboard', '0006_auto_20170308_1421'),
]
operations = [
]
...@@ -151,7 +151,7 @@ class ConnectCommand(Model): ...@@ -151,7 +151,7 @@ class ConnectCommand(Model):
access_method = CharField(max_length=10, choices=ACCESS_METHODS, access_method = CharField(max_length=10, choices=ACCESS_METHODS,
verbose_name=_('access method'), verbose_name=_('access method'),
help_text=_('Type of the remote access method.')) help_text=_('Type of the remote access method.'))
name = CharField(max_length="128", verbose_name=_('name'), blank=False, name = CharField(max_length=128, verbose_name=_('name'), blank=False,
help_text=_("Name of your custom command.")) help_text=_("Name of your custom command."))
template = CharField(blank=True, null=True, max_length=256, template = CharField(blank=True, null=True, max_length=256,
verbose_name=_('command template'), verbose_name=_('command template'),
...@@ -212,15 +212,16 @@ class Profile(Model): ...@@ -212,15 +212,16 @@ class Profile(Model):
commands = self.user.command_set.filter( commands = self.user.command_set.filter(
access_method=instance.access_method) access_method=instance.access_method)
if commands.count() < 1: if commands.count() < 1:
return [single_command] return [{'id': 0, 'cmd': single_command}]
else: else:
return [ return [{
command.template % { 'id': command.id,
'cmd': command.template % {
'port': instance.get_connect_port(use_ipv6=use_ipv6), 'port': instance.get_connect_port(use_ipv6=use_ipv6),
'host': instance.get_connect_host(use_ipv6=use_ipv6), 'host': instance.get_connect_host(use_ipv6=use_ipv6),
'password': instance.pw, 'password': instance.pw,
'username': 'cloud', 'username': 'cloud',
} for command in commands] }} for command in commands]
else: else:
return [] return []
...@@ -349,14 +350,12 @@ if hasattr(settings, 'SAML_ORG_ID_ATTRIBUTE'): ...@@ -349,14 +350,12 @@ if hasattr(settings, 'SAML_ORG_ID_ATTRIBUTE'):
def saml_save_org_id(sender, **kwargs): def saml_save_org_id(sender, **kwargs):
logger.debug("saml_save_org_id called by %s", sender.username) logger.debug("saml_save_org_id called by %s", sender.username)
attributes = kwargs.pop('attributes') attributes = kwargs.pop('attributes')
atr = settings.SAML_ORG_ID_ATTRIBUTE
try: try:
value = attributes[atr][0].upper() value = attributes[settings.SAML_ORG_ID_ATTRIBUTE][0].upper()
except Exception as e: except Exception as e:
value = None value = None
logger.info("saml_save_org_id couldn't find attribute. %s", logger.info("saml_save_org_id couldn't find attribute. %s",
unicode(e)) unicode(e))
if sender.pk is None: if sender.pk is None:
sender.save() sender.save()
logger.debug("saml_save_org_id saved user %s", unicode(sender)) logger.debug("saml_save_org_id saved user %s", unicode(sender))
......
...@@ -508,13 +508,6 @@ $.ajaxSetup({ ...@@ -508,13 +508,6 @@ $.ajaxSetup({
} }
}); });
/* for autocomplete */
$(function() {
yourlabs.TextWidget.prototype.getValue = function(choice) {
return choice.children().html();
};
});
var tagsToReplace = { var tagsToReplace = {
'&': '&amp;', '&': '&amp;',
'<': '&lt;', '<': '&lt;',
......
...@@ -1031,7 +1031,7 @@ textarea[name="new_members"] { ...@@ -1031,7 +1031,7 @@ textarea[name="new_members"] {
font-weight: bold; font-weight: bold;
} }
.hilight .autocomplete-hl { .select2-results__option--highlighted .autocomplete-hl {
color: orange; color: orange;
} }
......
...@@ -70,12 +70,12 @@ $(function() { ...@@ -70,12 +70,12 @@ $(function() {
}); });
/* for js fallback */ /* for js fallback */
$("#vm-details-pw-show").parent("div").children("input").prop("type", "password"); $(".vm-details-show-password").parent("div").children("input").prop("type", "password");
/* show password */ /* show password */
$("#vm-details-pw-show").click(function() { $(".vm-details-show-password").click(function() {
var input = $(this).parent("div").children("input"); var input = $(this).parent("div").children("input");
var eye = $(this).children("#vm-details-pw-eye"); var eye = $(this).children(".vm-details-password-eye");
var span = $(this); var span = $(this);
span.tooltip("destroy"); span.tooltip("destroy");
...@@ -111,9 +111,10 @@ $(function() { ...@@ -111,9 +111,10 @@ $(function() {
/* rename ajax */ /* rename ajax */
$('.vm-details-rename-submit').click(function() { $('.vm-details-rename-submit').click(function() {
var name = $(this).parent("span").prev("input").val(); var name = $(this).parent("span").prev("input").val();
var url = $("#vm-details-rename-form").attr("action");
$.ajax({ $.ajax({
method: 'POST', method: 'POST',
url: location.href, url: url,
data: {'new_name': name}, data: {'new_name': name},
headers: {"X-CSRFToken": getCookie('csrftoken')}, headers: {"X-CSRFToken": getCookie('csrftoken')},
success: function(data, textStatus, xhr) { success: function(data, textStatus, xhr) {
...@@ -252,4 +253,12 @@ $(function() { ...@@ -252,4 +253,12 @@ $(function() {
return e.preventDefault(); return e.preventDefault();
}); });
// Clipboard for connection strings
if(Clipboard.isSupported()) {
new Clipboard(".vm-details-connection-string-copy", {
text: function(trigger) {
return $($(trigger).data("clipboard-target")).val();
}
});
}
}); });
...@@ -24,7 +24,7 @@ ...@@ -24,7 +24,7 @@
<td> <td>
<select class="form-control" name="perm-u-{{i.user.id}}"{% if i.level not in acl.allowed_levels %} disabled{% endif %}> <select class="form-control" name="perm-u-{{i.user.id}}"{% if i.level not in acl.allowed_levels %} disabled{% endif %}>
{% for id, name in acl.levels %} {% for id, name in acl.levels %}
<option{%if id = i.level%} selected="selected"{%endif%} <option{%if id == i.level%} selected="selected"{%endif%}
{% if id not in acl.allowed_levels %} disabled{% endif %} {% if id not in acl.allowed_levels %} disabled{% endif %}
value="{{id}}">{{name}}</option> value="{{id}}">{{name}}</option>
{% endfor %} {% endfor %}
...@@ -46,7 +46,7 @@ ...@@ -46,7 +46,7 @@
<td> <td>
<select class="form-control" name="perm-g-{{i.group.id}}{% if i.level not in acl.allowed_levels %} disabled{% endif %}"> <select class="form-control" name="perm-g-{{i.group.id}}{% if i.level not in acl.allowed_levels %} disabled{% endif %}">
{% for id, name in acl.levels %} {% for id, name in acl.levels %}
<option{%if id = i.level%} selected="selected"{%endif%} <option{%if id == i.level%} selected="selected"{%endif%}
{% if id not in acl.allowed_levels %} disabled{% endif %} {% if id not in acl.allowed_levels %} disabled{% endif %}
value="{{id}}">{{name}}</option> value="{{id}}">{{name}}</option>
{% endfor %} {% endfor %}
......
...@@ -137,8 +137,10 @@ ...@@ -137,8 +137,10 @@
{% if user.is_superuser %} {% if user.is_superuser %}
<hr /> <hr />
<script type="text/javascript" src="/static/admin/js/jquery.min.js"></script> <script type="text/javascript" src="/static/admin/js/vendor/jquery/jquery.min.js"></script>
<script type="text/javascript" src="/static/admin/js/jquery.init.js"></script> <script type="text/javascript" src="/static/admin/js/jquery.init.js"></script>
<script type="text/javascript" src="/static/autocomplete_light/jquery.init.js"></script>
<script type="text/javascript" src="/static/autocomplete_light/vendor/select2/dist/js/select2.js"></script>
{{ group_perm_form.media }} {{ group_perm_form.media }}
<h3>{% trans "Group permissions" %}</h3> <h3>{% trans "Group permissions" %}</h3>
......
...@@ -52,7 +52,7 @@ ...@@ -52,7 +52,7 @@
<td> <td>
<select class="form-control" name="perm-u-{{i.user.id}}"> <select class="form-control" name="perm-u-{{i.user.id}}">
{% for id, name in acl.levels %} {% for id, name in acl.levels %}
<option{%if id = i.level%} selected="selected"{%endif%} value="{{id}}">{{name}}</option> <option{%if id == i.level%} selected="selected"{%endif%} value="{{id}}">{{name}}</option>
{% endfor %} {% endfor %}
</select> </select>
</td> </td>
...@@ -72,7 +72,7 @@ ...@@ -72,7 +72,7 @@
<td> <td>
<select class="form-control" name="perm-g-{{i.group.id}}"> <select class="form-control" name="perm-g-{{i.group.id}}">
{% for id, name in acl.levels %} {% for id, name in acl.levels %}
<option{%if id = i.level%} selected="selected"{%endif%} value="{{id}}">{{name}}</option> <option{%if id == i.level%} selected="selected"{%endif%} value="{{id}}">{{name}}</option>
{% endfor %} {% endfor %}
</select> </select>
</td> </td>
......
...@@ -55,7 +55,7 @@ ...@@ -55,7 +55,7 @@
</div> </div>
<h1> <h1>
<div id="vm-details-rename" class="vm-details-home-rename-form-div"> <div id="vm-details-rename" class="vm-details-home-rename-form-div">
<form action="" method="POST" id="vm-details-rename-form"> <form action="{{ op.rename.get_url }}" method="POST" id="vm-details-rename-form">
{% csrf_token %} {% csrf_token %}
<div class="input-group vm-details-home-name"> <div class="input-group vm-details-home-name">
<input id="vm-details-rename-name" class="form-control input-sm" name="new_name" type="text" value="{{ instance.name }}"/> <input id="vm-details-rename-name" class="form-control input-sm" name="new_name" type="text" value="{{ instance.name }}"/>
...@@ -133,9 +133,9 @@ ...@@ -133,9 +133,9 @@
<div class="input-group"> <div class="input-group">
<input type="text" id="vm-details-pw-input" class="form-control input-sm input-tags" <input type="text" id="vm-details-pw-input" class="form-control input-sm input-tags"
value="{{ instance.pw }}" spellcheck="false" autocomplete="new-password"/> value="{{ instance.pw }}" spellcheck="false" autocomplete="new-password"/>
<span class="input-group-addon input-tags" id="vm-details-pw-show" <span class="input-group-addon btn btn-default input-tags vm-details-show-password"
title="{% trans "Show password" %}" data-container="body"> title="{% trans "Show password" %}" data-container="body">
<i class="fa fa-eye" id="vm-details-pw-eye"></i> <i class="fa fa-eye vm-details-password-eye"></i>
</span> </span>
</div> </div>
</dd> </dd>
...@@ -158,22 +158,24 @@ ...@@ -158,22 +158,24 @@
<div class="input-group dashboard-vm-details-connect-command"> <div class="input-group dashboard-vm-details-connect-command">
<span class="input-group-addon input-tags">{% trans "Command" %}</span> <span class="input-group-addon input-tags">{% trans "Command" %}</span>
<input type="text" spellcheck="false" <input type="text" spellcheck="false"
value="{{ c }}" value="{{ c.cmd }}"
id="vm-details-connection-string" class="form-control input-tags" /> id="vm-details-connection-string-{{ c.id }}" class="form-control input-tags" />
<span class="input-group-addon input-tags vm-details-connection-string-copy" <span class="input-group-addon btn btn-default input-tags vm-details-show-password"
title="{% trans "Select all" %}" data-container="body"> title="{% trans "Show password" %}" data-container="body">
<i class="fa fa-eye vm-details-password-eye"></i>
</span>
<span class="input-group-addon input-tags btn btn-default vm-details-connection-string-copy"
title="{% trans "Copy to clipboard" %}"
data-container="body"
data-clipboard-target="#vm-details-connection-string-{{ c.id }}">
<i class="fa fa-copy"></i> <i class="fa fa-copy"></i>
</span> </span>
</div> </div>
{% empty %} {% empty %}
<div class="input-group dashboard-vm-details-connect-command"> <div class="input-group dashboard-vm-details-connect-command">
<span class="input-group-addon input-tags">{% trans "Command" %}</span> <span class="input-group-addon input-tags">{% trans "Command" %}</span>
<input type="text" spellcheck="false" value="{% trans "Connection is not possible." %}" <input type="text" spellcheck="false" value="{% trans "Connection is not possible." %}"
id="vm-details-connection-string" class="form-control input-tags" /> id="vm-details-connection-string" class="form-control input-tags" />
<span class="input-group-addon input-tags" id="vm-details-connection-string-copy">
<i class="fa fa-copy" title="{% trans "Select all" %}"></i>
</span>
</div> </div>
{% endfor %} {% endfor %}
{% if instance.get_connect_uri %} {% if instance.get_connect_uri %}
......
...@@ -16,7 +16,7 @@ ...@@ -16,7 +16,7 @@
<small class="vm-details-home-edit-name">{{ instance.name }}</small> <small class="vm-details-home-edit-name">{{ instance.name }}</small>
</div> </div>
<div class="js-hidden vm-details-home-rename-form-div" id="vm-details-home-rename"> <div class="js-hidden vm-details-home-rename-form-div" id="vm-details-home-rename">
<form method="POST"> <form action="{{ op.rename.get_url }}" method="POST">
{% csrf_token %} {% csrf_token %}
<div class="input-group"> <div class="input-group">
<input type="text" name="new_name" value="{{ instance.name }}" class="form-control input-sm"/> <input type="text" name="new_name" value="{{ instance.name }}" class="form-control input-sm"/>
......
...@@ -4,9 +4,9 @@ ...@@ -4,9 +4,9 @@
{% if table.page %} {% if table.page %}
<div class="table-container"> <div class="table-container">
{% endif %} {% endif %}
{% endspaceless %}
{% block table %} {% block table %}
<table{% if table.attrs %} {{ table.attrs.as_html }}{% endif %}> <table{% if table.attrs %} {{ table.attrs.as_html }}{% endif %}>
{% nospaceless %}
{% block table.thead %} {% block table.thead %}
<thead> <thead>
<tr> <tr>
...@@ -42,10 +42,9 @@ ...@@ -42,10 +42,9 @@
{% block table.tfoot %} {% block table.tfoot %}
<tfoot></tfoot> <tfoot></tfoot>
{% endblock table.tfoot %} {% endblock table.tfoot %}
{% endnospaceless %}
</table> </table>
{% endblock table %} {% endblock table %}
{% spaceless %}
{% if table.page %} {% if table.page %}
</div> </div>
{% endif %} {% endif %}
......
...@@ -21,7 +21,7 @@ import warnings ...@@ -21,7 +21,7 @@ import warnings
from factory import Factory, Sequence from factory import Factory, Sequence
from mock import patch, MagicMock from mock import patch, MagicMock
from django.contrib.auth.models import User from django.contrib.auth.models import User, AnonymousUser
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.core.signing import TimestampSigner, JSONSerializer, b64_encode from django.core.signing import TimestampSigner, JSONSerializer, b64_encode
from django.http import HttpRequest, Http404, QueryDict from django.http import HttpRequest, Http404, QueryDict
...@@ -629,13 +629,13 @@ def FakeRequestFactory(user=None, **kwargs): ...@@ -629,13 +629,13 @@ def FakeRequestFactory(user=None, **kwargs):
''' '''
if user is None: if user is None:
user = UserFactory()
auth = kwargs.pop('authenticated', True) auth = kwargs.pop('authenticated', True)
user.is_authenticated = lambda: auth user = UserFactory() if auth else AnonymousUser()
user.is_superuser = kwargs.pop('superuser', False) user.is_superuser = kwargs.pop('superuser', False)
if kwargs.pop('has_perms_mock', False): if kwargs.pop('has_perms_mock', False):
user.has_perms = MagicMock(return_value=True) user.has_perms = MagicMock(return_value=True)
user.save() if auth:
user.save()
request = HttpRequest() request = HttpRequest()
request.user = user request.user = user
......
...@@ -29,22 +29,22 @@ class TemplateSyntaxTestCase(unittest.TestCase): ...@@ -29,22 +29,22 @@ class TemplateSyntaxTestCase(unittest.TestCase):
def test_templates(self): def test_templates(self):
"""Test all templates for syntax errors.""" """Test all templates for syntax errors."""
for loader in Engine.get_default().template_loaders: for loader in Engine.get_default().template_loaders:
print loader print(loader)
self._test_dir(loader.get_template_sources('')) self._test_dir(loader.get_template_sources(''))
def _test_dir(self, dir, path="/"): def _test_dir(self, dir, path="/"):
for i in dir: for i in dir:
i = join(path, i) i = join(path, str(i))
if isfile(i): if isfile(i):
self._test_template(join(path, i)) self._test_template(join(path, i))
elif isdir(i): elif isdir(i):
print "%s:" % i print("%s:" % i)
self._test_dir(listdir(i), i) self._test_dir(listdir(i), i)
def _test_template(self, path): def _test_template(self, path):
print path print(path)
try: try:
Template(open(path).read()).render(Context({})) Template(open(path).read()).render(Context({}))
except (NoReverseMatch, VariableDoesNotExist, KeyError, AttributeError, except (NoReverseMatch, VariableDoesNotExist, KeyError, AttributeError,
ValueError, ) as e: ValueError, ) as e:
print e print(e)
...@@ -30,7 +30,7 @@ from dashboard.views import VmAddInterfaceView ...@@ -30,7 +30,7 @@ from dashboard.views import VmAddInterfaceView
from vm.models import Instance, InstanceTemplate, Lease, Node, Trait from vm.models import Instance, InstanceTemplate, Lease, Node, Trait
from vm.operations import (WakeUpOperation, AddInterfaceOperation, from vm.operations import (WakeUpOperation, AddInterfaceOperation,
AddPortOperation, RemoveInterfaceOperation, AddPortOperation, RemoveInterfaceOperation,
DeployOperation) DeployOperation, RenameOperation)
from ..models import Profile from ..models import Profile
from firewall.models import Vlan, Host, VlanGroup from firewall.models import Vlan, Host, VlanGroup
from mock import Mock, patch from mock import Mock, patch
...@@ -437,31 +437,43 @@ class VmDetailTest(LoginMixin, MockCeleryMixin, TestCase): ...@@ -437,31 +437,43 @@ class VmDetailTest(LoginMixin, MockCeleryMixin, TestCase):
def test_unpermitted_set_name(self): def test_unpermitted_set_name(self):
c = Client() c = Client()
self.login(c, "user2") self.login(c, "user2")
inst = Instance.objects.get(pk=1) with patch.object(RenameOperation, 'async') as mock_method:
inst.set_level(self.u2, 'user') inst = Instance.objects.get(pk=1)
old_name = inst.name mock_method.side_effect = inst.rename
response = c.post("/dashboard/vm/1/", {'new_name': 'test1235'}) inst.set_level(self.u2, 'user')
self.assertEqual(response.status_code, 403) old_name = inst.name
self.assertEqual(Instance.objects.get(pk=1).name, old_name) response = c.post("/dashboard/vm/1/op/rename/",
{'new_name': 'test1235'})
self.assertEqual(response.status_code, 403)
assert not mock_method.called
self.assertEqual(Instance.objects.get(pk=1).name, old_name)
def test_permitted_set_name(self): def test_permitted_set_name(self):
c = Client() c = Client()
self.login(c, "user2") self.login(c, "user2")
inst = Instance.objects.get(pk=1) with patch.object(RenameOperation, 'async') as mock_method:
inst.set_level(self.u2, 'owner') inst = Instance.objects.get(pk=1)
response = c.post("/dashboard/vm/1/", {'new_name': 'test1234'}) mock_method.side_effect = inst.rename
self.assertEqual(response.status_code, 302) inst.set_level(self.u2, 'owner')
self.assertEqual(Instance.objects.get(pk=1).name, 'test1234') response = c.post("/dashboard/vm/1/op/rename/",
{'new_name': 'test1234'})
self.assertEqual(response.status_code, 302)
assert mock_method.called
self.assertEqual(Instance.objects.get(pk=1).name, 'test1234')
def test_permitted_set_name_w_ajax(self): def test_permitted_set_name_w_ajax(self):
c = Client() c = Client()
self.login(c, "user2") self.login(c, "user2")
inst = Instance.objects.get(pk=1) inst = Instance.objects.get(pk=1)
inst.set_level(self.u2, 'owner') with patch.object(RenameOperation, 'async') as mock_method:
response = c.post("/dashboard/vm/1/", {'new_name': 'test123'}, inst.set_level(self.u2, 'owner')
HTTP_X_REQUESTED_WITH='XMLHttpRequest') mock_method.side_effect = inst.rename
self.assertEqual(response.status_code, 200) response = c.post("/dashboard/vm/1/op/rename/",
self.assertEqual(Instance.objects.get(pk=1).name, 'test123') {'new_name': 'test123'},
HTTP_X_REQUESTED_WITH='XMLHttpRequest')
self.assertEqual(response.status_code, 200)
assert mock_method.called
self.assertEqual(Instance.objects.get(pk=1).name, 'test123')
def test_permitted_wake_up_wrong_state(self): def test_permitted_wake_up_wrong_state(self):
c = Client() c = Client()
...@@ -1890,9 +1902,9 @@ class TwoFactorTest(LoginMixin, TestCase): ...@@ -1890,9 +1902,9 @@ class TwoFactorTest(LoginMixin, TestCase):
response = c.get("/two-factor-login/", follow=True) response = c.get("/two-factor-login/", follow=True)
self.assertItemsEqual( self.assertItemsEqual(
response.redirect_chain, response.redirect_chain,
[('http://testserver/', 302), [('/', 302),
('http://testserver/dashboard/', 302), ('/dashboard/', 302),
('http://testserver/accounts/login/?next=/dashboard/', 302)] ('/accounts/login/?next=/dashboard/', 302)]
) )
def test_straight_to_2fa_as_user(self): def test_straight_to_2fa_as_user(self):
...@@ -1901,6 +1913,6 @@ class TwoFactorTest(LoginMixin, TestCase): ...@@ -1901,6 +1913,6 @@ class TwoFactorTest(LoginMixin, TestCase):
response = c.get("/two-factor-login/", follow=True) response = c.get("/two-factor-login/", follow=True)
self.assertItemsEqual( self.assertItemsEqual(
response.redirect_chain, response.redirect_chain,
[('http://testserver/', 302), [('/', 302),
('http://testserver/dashboard/', 302)] ('/dashboard/', 302)]
) )
...@@ -16,9 +16,8 @@ ...@@ -16,9 +16,8 @@
# with CIRCLE. If not, see <http://www.gnu.org/licenses/>. # with CIRCLE. If not, see <http://www.gnu.org/licenses/>.
from __future__ import absolute_import from __future__ import absolute_import
from django.conf.urls import patterns, url, include from django.conf.urls import url
import autocomplete_light
from vm.models import Instance from vm.models import Instance
from .views import ( from .views import (
AclUpdateView, FavouriteView, GroupAclUpdateView, GroupDelete, AclUpdateView, FavouriteView, GroupAclUpdateView, GroupDelete,
...@@ -56,14 +55,13 @@ from .views import ( ...@@ -56,14 +55,13 @@ from .views import (
StorageDetail, DiskDetail, StorageDetail, DiskDetail,
MessageList, MessageDetail, MessageCreate, MessageDelete, MessageList, MessageDetail, MessageCreate, MessageDelete,
EnableTwoFactorView, DisableTwoFactorView, EnableTwoFactorView, DisableTwoFactorView,
AclUserGroupAutocomplete, AclUserAutocomplete,
) )
from .views.vm import vm_ops, vm_mass_ops from .views.vm import vm_ops, vm_mass_ops
from .views.node import node_ops from .views.node import node_ops
autocomplete_light.autodiscover()
urlpatterns = patterns( urlpatterns = [
'',
url(r'^$', IndexView.as_view(), name="dashboard.index"), url(r'^$', IndexView.as_view(), name="dashboard.index"),
url(r"^profile/list/$", UserList.as_view(), url(r"^profile/list/$", UserList.as_view(),
name="dashboard.views.user-list"), name="dashboard.views.user-list"),
...@@ -217,8 +215,6 @@ urlpatterns = patterns( ...@@ -217,8 +215,6 @@ urlpatterns = patterns(
ConnectCommandCreate.as_view(), ConnectCommandCreate.as_view(),
name="dashboard.views.connect-command-create"), name="dashboard.views.connect-command-create"),
url(r'^autocomplete/', include('autocomplete_light.urls')),
url(r"^store/list/$", StoreList.as_view(), url(r"^store/list/$", StoreList.as_view(),
name="dashboard.views.store-list"), name="dashboard.views.store-list"),
url(r"^store/download/$", store_download, url(r"^store/download/$", store_download,
...@@ -253,22 +249,26 @@ urlpatterns = patterns( ...@@ -253,22 +249,26 @@ urlpatterns = patterns(
name="dashboard.views.message-create"), name="dashboard.views.message-create"),
url(r'^message/delete/(?P<pk>\d+)/$', MessageDelete.as_view(), url(r'^message/delete/(?P<pk>\d+)/$', MessageDelete.as_view(),
name="dashboard.views.message-delete"), name="dashboard.views.message-delete"),
)
urlpatterns += patterns( url(r'^autocomplete/acl/user-group/$',
'', AclUserGroupAutocomplete.as_view(),
*(url(r'^vm/(?P<pk>\d+)/op/%s/$' % op, v.as_view(), name=v.get_urlname()) name='autocomplete.acl.user-group'),
for op, v in vm_ops.iteritems()) url(r'^autocomplete/acl/user/$',
) AclUserAutocomplete.as_view(),
name='autocomplete.acl.user'),
]
urlpatterns += patterns( urlpatterns += [
'', url(r'^vm/(?P<pk>\d+)/op/%s/$' % op, v.as_view(), name=v.get_urlname())
*(url(r'^vm/mass_op/%s/$' % op, v.as_view(), name=v.get_urlname()) for op, v in vm_ops.iteritems()
for op, v in vm_mass_ops.iteritems()) ]
)
urlpatterns += patterns( urlpatterns += [
'', url(r'^vm/mass_op/%s/$' % op, v.as_view(), name=v.get_urlname())
*(url(r'^node/(?P<pk>\d+)/op/%s/$' % op, v.as_view(), name=v.get_urlname()) for op, v in vm_mass_ops.iteritems()
for op, v in node_ops.iteritems()) ]
)
urlpatterns += [
url(r'^node/(?P<pk>\d+)/op/%s/$' % op, v.as_view(), name=v.get_urlname())
for op, v in node_ops.iteritems()
]
...@@ -15,3 +15,4 @@ from graph import * ...@@ -15,3 +15,4 @@ from graph import *
from storage import * from storage import *
from request import * from request import *
from message import * from message import *
from autocomplete import *
...@@ -15,13 +15,16 @@ ...@@ -15,13 +15,16 @@
# 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/>.
import autocomplete_light import json
from dal import autocomplete
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.utils.html import escape from django.utils.html import escape
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.db.models import Q
from django.http import HttpResponse
from .views import AclUpdateView from ..views import AclUpdateView
from .models import Profile from ..models import Profile
def highlight(field, q, none_wo_match=True): def highlight(field, q, none_wo_match=True):
...@@ -48,13 +51,21 @@ def highlight(field, q, none_wo_match=True): ...@@ -48,13 +51,21 @@ def highlight(field, q, none_wo_match=True):
return escape(field) return escape(field)
class AclUserGroupAutocomplete(autocomplete_light.AutocompleteGenericBase): class AclUserAutocomplete(autocomplete.Select2ListView):
search_fields = ( search_fields = ('first_name', 'last_name', 'username',
('first_name', 'last_name', 'username', 'email', 'profile__org_id'), 'email', 'profile__org_id')
('name', 'groupprofile__org_id'),
) def filter(self, qs, search_fields):
choice_html_format = (u'<span data-value="%s"><span style="display:none"' if self.q:
u'>%s</span>%s</span>') condition = Q()
for field in search_fields:
condition |= Q(**{field + '__icontains': unicode(self.q)})
return list(qs.filter(condition))
return []
def get_list(self):
users = AclUpdateView.get_allowed_users(self.request.user)
return self.filter(users, self.search_fields)
def choice_displayed_text(self, choice): def choice_displayed_text(self, choice):
q = unicode(self.request.GET.get('q', '')) q = unicode(self.request.GET.get('q', ''))
...@@ -71,35 +82,17 @@ class AclUserGroupAutocomplete(autocomplete_light.AutocompleteGenericBase): ...@@ -71,35 +82,17 @@ class AclUserGroupAutocomplete(autocomplete_light.AutocompleteGenericBase):
else: else:
return _('%s (group)') % name return _('%s (group)') % name
def choice_html(self, choice): def get(self, *args, **kwargs):
return self.choice_html_format % ( return HttpResponse(json.dumps({
self.choice_value(choice), self.choice_label(choice), 'results': [dict(id=unicode(r), text=self.choice_displayed_text(r))
self.choice_displayed_text(choice)) for r in self.get_list()]
}), content_type="application/json")
def choices_for_request(self):
user = self.request.user
self.choices = (AclUpdateView.get_allowed_users(user),
AclUpdateView.get_allowed_groups(user))
return super(AclUserGroupAutocomplete, self).choices_for_request()
def autocomplete_html(self):
html = []
for choice in self.choices_for_request():
html.append(self.choice_html(choice))
if not html:
html = self.empty_html_format % _('no matches found').capitalize()
return self.autocomplete_html_format % ''.join(html)
class AclUserAutocomplete(AclUserGroupAutocomplete):
def choices_for_request(self):
user = self.request.user
self.choices = (AclUpdateView.get_allowed_users(user), )
return super(AclUserGroupAutocomplete, self).choices_for_request()
class AclUserGroupAutocomplete(AclUserAutocomplete):
group_search_fields = ('name', 'groupprofile__org_id')
autocomplete_light.register(AclUserGroupAutocomplete) def get_list(self):
autocomplete_light.register(AclUserAutocomplete) groups = AclUpdateView.get_allowed_groups(self.request.user)
groups = self.filter(groups, self.group_search_fields)
return super(AclUserGroupAutocomplete, self).get_list() + groups
...@@ -171,6 +171,7 @@ class TemplateVms(object): ...@@ -171,6 +171,7 @@ class TemplateVms(object):
def get_minmax(self): def get_minmax(self):
return (0, None) return (0, None)
register_graph(TemplateVms, 'instances', TemplateGraphView) register_graph(TemplateVms, 'instances', TemplateGraphView)
...@@ -197,6 +198,7 @@ class Ram(object): ...@@ -197,6 +198,7 @@ class Ram(object):
def get_minmax(self): def get_minmax(self):
return (0, 105) return (0, 105)
register_graph(Ram, 'memory', VmGraphView) register_graph(Ram, 'memory', VmGraphView)
register_graph(Ram, 'memory', NodeGraphView) register_graph(Ram, 'memory', NodeGraphView)
...@@ -212,6 +214,7 @@ class Cpu(object): ...@@ -212,6 +214,7 @@ class Cpu(object):
else: else:
return (0, self.obj.num_cores * 100 + 5) return (0, self.obj.num_cores * 100 + 5)
register_graph(Cpu, 'cpu', VmGraphView) register_graph(Cpu, 'cpu', VmGraphView)
register_graph(Cpu, 'cpu', NodeGraphView) register_graph(Cpu, 'cpu', NodeGraphView)
...@@ -236,6 +239,7 @@ class VmNetwork(object): ...@@ -236,6 +239,7 @@ class VmNetwork(object):
params)) params))
return 'group(%s)' % ','.join(metrics) if metrics else None return 'group(%s)' % ','.join(metrics) if metrics else None
register_graph(VmNetwork, 'network', VmGraphView) register_graph(VmNetwork, 'network', VmGraphView)
...@@ -251,6 +255,7 @@ class NodeNetwork(object): ...@@ -251,6 +255,7 @@ class NodeNetwork(object):
'10), ".*\.bytes_(sent|recv)-([a-zA-Z0-9]+).*", "\\2 \\1")' % ( '10), ".*\.bytes_(sent|recv)-([a-zA-Z0-9]+).*", "\\2 \\1")' % (
self.obj.metric_prefix)) self.obj.metric_prefix))
register_graph(NodeNetwork, 'network', NodeGraphView) register_graph(NodeNetwork, 'network', NodeGraphView)
...@@ -262,6 +267,7 @@ class NodeVms(object): ...@@ -262,6 +267,7 @@ class NodeVms(object):
def get_minmax(self): def get_minmax(self):
return (0, None) return (0, None)
register_graph(NodeVms, 'vm', NodeGraphView) register_graph(NodeVms, 'vm', NodeGraphView)
...@@ -282,6 +288,7 @@ class NodeAllocated(object): ...@@ -282,6 +288,7 @@ class NodeAllocated(object):
def get_minmax(self): def get_minmax(self):
return (0, None) return (0, None)
register_graph(NodeAllocated, 'alloc', NodeGraphView) register_graph(NodeAllocated, 'alloc', NodeGraphView)
...@@ -302,6 +309,7 @@ class NodeListAllocated(object): ...@@ -302,6 +309,7 @@ class NodeListAllocated(object):
def get_minmax(self): def get_minmax(self):
return (0, None) return (0, None)
register_graph(NodeListAllocated, 'alloc', NodeListGraphView) register_graph(NodeListAllocated, 'alloc', NodeListGraphView)
...@@ -315,4 +323,5 @@ class NodeListVms(object): ...@@ -315,4 +323,5 @@ class NodeListVms(object):
def get_minmax(self): def get_minmax(self):
return (0, None) return (0, None)
register_graph(NodeListVms, 'vm', NodeListGraphView) register_graph(NodeListVms, 'vm', NodeListGraphView)
...@@ -18,7 +18,7 @@ from __future__ import unicode_literals, absolute_import ...@@ -18,7 +18,7 @@ from __future__ import unicode_literals, absolute_import
import logging import logging
from django.core.cache import get_cache from django.core.cache import cache
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import Group, User from django.contrib.auth.models import Group, User
...@@ -103,7 +103,6 @@ class IndexView(LoginRequiredMixin, TemplateView): ...@@ -103,7 +103,6 @@ class IndexView(LoginRequiredMixin, TemplateView):
# toplist # toplist
if settings.STORE_URL: if settings.STORE_URL:
cache_key = "files-%d" % self.request.user.pk cache_key = "files-%d" % self.request.user.pk
cache = get_cache("default")
files = cache.get(cache_key) files = cache.get(cache_key)
if not files: if not files:
try: try:
......
...@@ -27,7 +27,6 @@ from django.db.models import Count ...@@ -27,7 +27,6 @@ from django.db.models import Count
from django.forms.models import inlineformset_factory from django.forms.models import inlineformset_factory
from django.http import HttpResponse from django.http import HttpResponse
from django.shortcuts import redirect from django.shortcuts import redirect
from django.template import RequestContext
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.views.generic import DetailView, TemplateView, View from django.views.generic import DetailView, TemplateView, View
...@@ -330,8 +329,12 @@ class NodeActivityView(LoginRequiredMixin, SuperuserRequiredMixin, View): ...@@ -330,8 +329,12 @@ class NodeActivityView(LoginRequiredMixin, SuperuserRequiredMixin, View):
response = { response = {
'activities': render_to_string( 'activities': render_to_string(
"dashboard/node-detail/_activity-timeline.html", "dashboard/node-detail/_activity-timeline.html",
RequestContext(request, {'activities': activities, {
'show_show_all': show_show_all})) 'activities': activities,
'show_show_all': show_show_all
},
request
)
} }
return HttpResponse( return HttpResponse(
......
...@@ -24,12 +24,11 @@ from django.conf import settings ...@@ -24,12 +24,11 @@ from django.conf import settings
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.template.defaultfilters import urlencode from django.template.defaultfilters import urlencode
from django.core.cache import get_cache from django.core.cache import cache
from django.core.exceptions import SuspiciousOperation from django.core.exceptions import SuspiciousOperation
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.http import HttpResponse from django.http import HttpResponse
from django.shortcuts import redirect, render_to_response, render from django.shortcuts import redirect, render_to_response, render
from django.template import RequestContext
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.views.decorators.http import require_GET, require_POST from django.views.decorators.http import require_GET, require_POST
from django.views.generic import TemplateView from django.views.generic import TemplateView
...@@ -65,7 +64,7 @@ class StoreList(LoginRequiredMixin, TemplateView): ...@@ -65,7 +64,7 @@ class StoreList(LoginRequiredMixin, TemplateView):
context = self.get_context_data(**kwargs) context = self.get_context_data(**kwargs)
return render_to_response( return render_to_response(
"dashboard/store/_list-box.html", "dashboard/store/_list-box.html",
RequestContext(self.request, context), context, self.request
) )
else: else:
return super(StoreList, self).get(*args, **kwargs) return super(StoreList, self).get(*args, **kwargs)
...@@ -193,7 +192,6 @@ def store_new_directory(request): ...@@ -193,7 +192,6 @@ def store_new_directory(request):
@login_required @login_required
def store_refresh_toplist(request): def store_refresh_toplist(request):
cache_key = "files-%d" % request.user.pk cache_key = "files-%d" % request.user.pk
cache = get_cache("default")
try: try:
store = Store(request.user) store = Store(request.user)
toplist = store.toplist() toplist = store.toplist()
......
...@@ -231,7 +231,7 @@ class MyPreferencesView(UpdateView): ...@@ -231,7 +231,7 @@ class MyPreferencesView(UpdateView):
def get(self, request, form=None, *args, **kwargs): def get(self, request, form=None, *args, **kwargs):
# if this is not here, it won't work # if this is not here, it won't work
self.object = self.get_object() self.object = self.get_object()
context = self.get_context_data(*args, **kwargs) context = self.get_context_data(form=form, *args, **kwargs)
if form is not None: if form is not None:
# a little cheating, users can't post invalid # a little cheating, users can't post invalid
# language selection forms (without modifying the HTML) # language selection forms (without modifying the HTML)
......
...@@ -32,7 +32,6 @@ from django.http import ( ...@@ -32,7 +32,6 @@ from django.http import (
HttpResponse, Http404, HttpResponseRedirect, JsonResponse HttpResponse, Http404, HttpResponseRedirect, JsonResponse
) )
from django.shortcuts import redirect, get_object_or_404 from django.shortcuts import redirect, get_object_or_404
from django.template import RequestContext
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.utils.translation import ( from django.utils.translation import (
ugettext as _, ugettext_noop, ungettext_lazy, ugettext as _, ugettext_noop, ungettext_lazy,
...@@ -68,6 +67,7 @@ from ..forms import ( ...@@ -68,6 +67,7 @@ from ..forms import (
VmMigrateForm, VmDeployForm, VmMigrateForm, VmDeployForm,
VmPortRemoveForm, VmPortAddForm, VmPortRemoveForm, VmPortAddForm,
VmRemoveInterfaceForm, VmRemoveInterfaceForm,
VmRenameForm,
) )
from request.models import TemplateAccessType, LeaseType from request.models import TemplateAccessType, LeaseType
from request.forms import LeaseRequestForm, TemplateRequestForm from request.forms import LeaseRequestForm, TemplateRequestForm
...@@ -199,7 +199,6 @@ class VmDetailView(GraphMixin, CheckedDetailView): ...@@ -199,7 +199,6 @@ class VmDetailView(GraphMixin, CheckedDetailView):
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
options = { options = {
'new_name': self.__set_name,
'new_description': self.__set_description, 'new_description': self.__set_description,
'new_tag': self.__add_tag, 'new_tag': self.__add_tag,
'to_remove': self.__remove_tag, 'to_remove': self.__remove_tag,
...@@ -210,29 +209,6 @@ class VmDetailView(GraphMixin, CheckedDetailView): ...@@ -210,29 +209,6 @@ class VmDetailView(GraphMixin, CheckedDetailView):
return v(request) return v(request)
raise Http404() raise Http404()
def __set_name(self, request):
self.object = self.get_object()
if not self.object.has_level(request.user, "operator"):
raise PermissionDenied()
new_name = request.POST.get("new_name")
Instance.objects.filter(pk=self.object.pk).update(
**{'name': new_name})
success_message = _("VM successfully renamed.")
if request.is_ajax():
response = {
'message': success_message,
'new_name': new_name,
'vm_pk': self.object.pk
}
return HttpResponse(
json.dumps(response),
content_type="application/json"
)
else:
messages.success(request, success_message)
return redirect(self.object.get_absolute_url())
def __set_description(self, request): def __set_description(self, request):
self.object = self.get_object() self.object = self.get_object()
if not self.object.has_level(request.user, "operator"): if not self.object.has_level(request.user, "operator"):
...@@ -743,6 +719,31 @@ class VmDeployView(FormOperationMixin, VmOperationView): ...@@ -743,6 +719,31 @@ class VmDeployView(FormOperationMixin, VmOperationView):
return kwargs return kwargs
class VmRenameView(FormOperationMixin, VmOperationView):
op = 'rename'
icon = 'pencil'
effect = 'success'
show_in_toolbar = False
form_class = VmRenameForm
def post(self, request, extra=None, *args, **kwargs):
if extra is None:
extra = {}
form = self.form_class(self.request.POST, **self.get_form_kwargs())
if form.is_valid():
extra.update(form.cleaned_data)
resp = super(FormOperationMixin, self).post(
request, extra, *args, **kwargs)
success_message = _('VM successfully renamed.')
if request.is_ajax():
return JsonResponse({'new_name': extra['new_name']})
else:
messages.success(request, success_message)
return resp
else:
return self.get(request)
vm_ops = OrderedDict([ vm_ops = OrderedDict([
('deploy', VmDeployView), ('deploy', VmDeployView),
('wake_up', VmOperationView.factory( ('wake_up', VmOperationView.factory(
...@@ -792,6 +793,7 @@ vm_ops = OrderedDict([ ...@@ -792,6 +793,7 @@ vm_ops = OrderedDict([
op='install_keys', icon='key', effect='info', op='install_keys', icon='key', effect='info',
show_in_toolbar=False, show_in_toolbar=False,
)), )),
('rename', VmRenameView),
]) ])
...@@ -1286,15 +1288,15 @@ def vm_activity(request, pk): ...@@ -1286,15 +1288,15 @@ def vm_activity(request, pk):
response['activities'] = render_to_string( response['activities'] = render_to_string(
"dashboard/vm-detail/_activity-timeline.html", "dashboard/vm-detail/_activity-timeline.html",
RequestContext(request, context), context, request
) )
response['ops'] = render_to_string( response['ops'] = render_to_string(
"dashboard/vm-detail/_operations.html", "dashboard/vm-detail/_operations.html",
RequestContext(request, context), context, request
) )
response['disk_ops'] = render_to_string( response['disk_ops'] = render_to_string(
"dashboard/vm-detail/_disk-operations.html", "dashboard/vm-detail/_disk-operations.html",
RequestContext(request, context), context, request
) )
return HttpResponse( return HttpResponse(
......
...@@ -143,6 +143,7 @@ class SwitchPortAdmin(admin.ModelAdmin): ...@@ -143,6 +143,7 @@ class SwitchPortAdmin(admin.ModelAdmin):
class EthernetDeviceAdmin(admin.ModelAdmin): class EthernetDeviceAdmin(admin.ModelAdmin):
list_display = ('name', ) list_display = ('name', )
admin.site.register(Host, HostAdmin) admin.site.register(Host, HostAdmin)
admin.site.register(Vlan, VlanAdmin) admin.site.register(Vlan, VlanAdmin)
admin.site.register(Rule, RuleAdmin) admin.site.register(Rule, RuleAdmin)
......
...@@ -52,12 +52,19 @@ class MACAddressFormField(forms.Field): ...@@ -52,12 +52,19 @@ class MACAddressFormField(forms.Field):
class MACAddressField(models.Field): class MACAddressField(models.Field):
description = _('MAC Address object') description = _('MAC Address object')
__metaclass__ = models.SubfieldBase
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
kwargs['max_length'] = 17 kwargs['max_length'] = 17
super(MACAddressField, self).__init__(*args, **kwargs) super(MACAddressField, self).__init__(*args, **kwargs)
def deconstruct(self):
name, path, args, kwargs = super(MACAddressField, self).deconstruct()
del kwargs['max_length']
return name, path, args, kwargs
def from_db_value(self, value, expression, connection, context):
return self.to_python(value)
def to_python(self, value): def to_python(self, value):
if not value: if not value:
return None return None
...@@ -105,16 +112,25 @@ class IPAddressFormField(forms.Field): ...@@ -105,16 +112,25 @@ class IPAddressFormField(forms.Field):
class IPAddressField(models.Field): class IPAddressField(models.Field):
description = _('IP Network object') description = _('IP Network object')
__metaclass__ = models.SubfieldBase
def __init__(self, version=4, serialize=True, *args, **kwargs): def __init__(self, version=4, serialize=True, *args, **kwargs):
kwargs['max_length'] = 100 kwargs['max_length'] = 100
self.version = version self.version = version
super(IPAddressField, self).__init__(*args, **kwargs) super(IPAddressField, self).__init__(*args, **kwargs)
def deconstruct(self):
name, path, args, kwargs = super(IPAddressField, self).deconstruct()
del kwargs['max_length']
if self.version != 4:
kwargs['version'] = self.version
return name, path, args, kwargs
def get_internal_type(self): def get_internal_type(self):
return "CharField" return "CharField"
def from_db_value(self, value, expression, connection, context):
return self.to_python(value)
def to_python(self, value): def to_python(self, value):
if not value: if not value:
return None return None
...@@ -163,13 +179,22 @@ class IPNetworkFormField(forms.Field): ...@@ -163,13 +179,22 @@ class IPNetworkFormField(forms.Field):
class IPNetworkField(models.Field): class IPNetworkField(models.Field):
description = _('IP Network object') description = _('IP Network object')
__metaclass__ = models.SubfieldBase
def __init__(self, version=4, serialize=True, *args, **kwargs): def __init__(self, version=4, serialize=True, *args, **kwargs):
kwargs['max_length'] = 100 kwargs['max_length'] = 100
self.version = version self.version = version
super(IPNetworkField, self).__init__(*args, **kwargs) super(IPNetworkField, self).__init__(*args, **kwargs)
def deconstruct(self):
name, path, args, kwargs = super(IPNetworkField, self).deconstruct()
del kwargs['max_length']
if self.version != 4:
kwargs['version'] = self.version
return name, path, args, kwargs
def from_db_value(self, value, expression, connection, context):
return self.to_python(value)
def to_python(self, value): def to_python(self, value):
if not value: if not value:
return None return None
......
...@@ -25,7 +25,7 @@ from .models import (Host, Rule, Vlan, Domain, Record, BlacklistItem, ...@@ -25,7 +25,7 @@ from .models import (Host, Rule, Vlan, Domain, Record, BlacklistItem,
SwitchPort) SwitchPort)
from .iptables import IptRule, IptChain from .iptables import IptRule, IptChain
import django.conf import django.conf
from django.template import loader, Context from django.template import loader
from django.utils import timezone from django.utils import timezone
...@@ -152,9 +152,9 @@ class BuildFirewall: ...@@ -152,9 +152,9 @@ class BuildFirewall:
template = loader.get_template('firewall/iptables.conf') template = loader.get_template('firewall/iptables.conf')
context['proto'] = 'ipv4' context['proto'] = 'ipv4'
ipv4 = unicode(template.render(Context(context))) ipv4 = unicode(template.render(context))
context['proto'] = 'ipv6' context['proto'] = 'ipv6'
ipv6 = unicode(template.render(Context(context))) ipv6 = unicode(template.render(context))
return (ipv4, ipv6) return (ipv4, ipv6)
......
# -*- coding: utf-8 -*-
# Generated by Django 1.11.3 on 2017-07-07 19:09
from __future__ import unicode_literals
from django.db import migrations
import firewall.fields
class Migration(migrations.Migration):
dependencies = [
('firewall', '0005_auto_20150520_2250'),
]
operations = [
migrations.AlterField(
model_name='host',
name='ipv6',
field=firewall.fields.IPAddressField(blank=True, help_text='The global IPv6 address of the host, for example 2001:db:88:200::10.', null=True, unique=True, verbose_name='IPv6 address', version=6),
),
migrations.AlterField(
model_name='vlan',
name='network6',
field=firewall.fields.IPNetworkField(blank=True, help_text='The IPv6 address and the prefix length of the gateway.', null=True, verbose_name='IPv6 address/prefix', version=6),
),
]
...@@ -80,7 +80,7 @@ class GetNewAddressTestCase(MockCeleryMixin, TestCase): ...@@ -80,7 +80,7 @@ class GetNewAddressTestCase(MockCeleryMixin, TestCase):
self.vlan = Vlan(vid=1, name='test', network4='10.0.0.1/29', self.vlan = Vlan(vid=1, name='test', network4='10.0.0.1/29',
network6='2001:738:2001:4031::/80', domain=d, network6='2001:738:2001:4031::/80', domain=d,
owner=self.u1) owner=self.u1)
self.vlan.clean() self.vlan.full_clean()
self.vlan.save() self.vlan.save()
self.vlan.host_set.all().delete() self.vlan.host_set.all().delete()
for i in range(3, 6): for i in range(3, 6):
......
...@@ -15,7 +15,7 @@ ...@@ -15,7 +15,7 @@
# 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/>.
from django.conf.urls import patterns, url from django.conf.urls import url
from .views import ( from .views import (
IndexView, IndexView,
HostList, HostDetail, HostCreate, HostDelete, HostList, HostDetail, HostCreate, HostDelete,
...@@ -33,8 +33,7 @@ from .views import ( ...@@ -33,8 +33,7 @@ from .views import (
VlanAclUpdateView VlanAclUpdateView
) )
urlpatterns = patterns( urlpatterns = [
'',
url('^$', IndexView.as_view(), name='network.index'), url('^$', IndexView.as_view(), name='network.index'),
# blacklist # blacklist
url('^blacklist/$', BlacklistList.as_view(), url('^blacklist/$', BlacklistList.as_view(),
...@@ -135,4 +134,4 @@ urlpatterns = patterns( ...@@ -135,4 +134,4 @@ urlpatterns = patterns(
remove_switch_port_device, name='network.remove_switch_port_device'), remove_switch_port_device, name='network.remove_switch_port_device'),
url('^switchports/(?P<pk>\d+)/add/$', add_switch_port_device, url('^switchports/(?P<pk>\d+)/add/$', add_switch_port_device,
name='network.add_switch_port_device'), name='network.add_switch_port_device'),
) ]
...@@ -19,7 +19,6 @@ from django.forms import ( ...@@ -19,7 +19,6 @@ from django.forms import (
Textarea, ValidationError Textarea, ValidationError
) )
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.template import RequestContext
from django.template.loader import render_to_string from django.template.loader import render_to_string
from sizefield.widgets import FileSizeWidget from sizefield.widgets import FileSizeWidget
...@@ -68,8 +67,7 @@ class InitialFromFileMixin(object): ...@@ -68,8 +67,7 @@ class InitialFromFileMixin(object):
super(InitialFromFileMixin, self).__init__(*args, **kwargs) super(InitialFromFileMixin, self).__init__(*args, **kwargs)
self.initial['message'] = render_to_string( self.initial['message'] = render_to_string(
self.initial_template, self.initial_template, {}, request
RequestContext(request, {}),
) )
def clean_message(self): def clean_message(self):
......
...@@ -16,7 +16,7 @@ ...@@ -16,7 +16,7 @@
# with CIRCLE. If not, see <http://www.gnu.org/licenses/>. # with CIRCLE. If not, see <http://www.gnu.org/licenses/>.
from __future__ import absolute_import from __future__ import absolute_import
from django.conf.urls import patterns, url from django.conf.urls import url
from .views import ( from .views import (
RequestList, RequestDetail, RequestTypeList, RequestList, RequestDetail, RequestTypeList,
...@@ -26,8 +26,7 @@ from .views import ( ...@@ -26,8 +26,7 @@ from .views import (
LeaseTypeDelete, TemplateAccessTypeDelete, ResizeRequestView, LeaseTypeDelete, TemplateAccessTypeDelete, ResizeRequestView,
) )
urlpatterns = patterns( urlpatterns = [
'',
url(r'^list/$', RequestList.as_view(), url(r'^list/$', RequestList.as_view(),
name="request.views.request-list"), name="request.views.request-list"),
url(r'^(?P<pk>\d+)/$', RequestDetail.as_view(), url(r'^(?P<pk>\d+)/$', RequestDetail.as_view(),
...@@ -62,4 +61,4 @@ urlpatterns = patterns( ...@@ -62,4 +61,4 @@ urlpatterns = patterns(
name="request.views.request-resource"), name="request.views.request-resource"),
url(r'resize/(?P<vm_pk>\d+)/(?P<disk_pk>\d+)/$', url(r'resize/(?P<vm_pk>\d+)/(?P<disk_pk>\d+)/$',
ResizeRequestView.as_view(), name="request.views.request-resize"), ResizeRequestView.as_view(), name="request.views.request-resize"),
) ]
# This import is responsible for running the operations' registration code. # noqa
from . import operations # noqa
...@@ -1403,6 +1403,27 @@ class ResourcesOperation(InstanceOperation): ...@@ -1403,6 +1403,27 @@ class ResourcesOperation(InstanceOperation):
@register_operation @register_operation
class RenameOperation(InstanceOperation):
id = "rename"
name = _("rename")
description = _("Change the name of virtual machine.")
acl_level = "operator"
required_perms = ()
def _operation(self, user, activity, new_name):
old_name = self.instance.name
self.instance.name = new_name
self.instance.full_clean()
self.instance.save()
return create_readable(ugettext_noop(
"Changed name from '%(old_name)s' to '%(new_name)s'."),
old_name=old_name, new_name=new_name
)
@register_operation
class PasswordResetOperation(RemoteAgentOperation): class PasswordResetOperation(RemoteAgentOperation):
id = 'password_reset' id = 'password_reset'
name = _("password reset") name = _("password reset")
......
...@@ -177,9 +177,9 @@ class InterfaceTestCase(MockCeleryMixin, TestCase): ...@@ -177,9 +177,9 @@ class InterfaceTestCase(MockCeleryMixin, TestCase):
i = Instance(id=10, owner=owner, access_method='rdp') i = Instance(id=10, owner=owner, access_method='rdp')
d = Domain(owner=owner) d = Domain(owner=owner)
d.save() d.save()
v = Vlan(vid=55, network4='127.0.0.1/8', v = Vlan(name='vlan', vid=55, network4='127.0.0.1/8',
network6='2001::1/32', domain=d) network6='2001::1/32', domain=d)
v.clean() v.full_clean()
v.save() v.save()
Interface.create(i, v, managed=True, owner=owner) Interface.create(i, v, managed=True, owner=owner)
......
cryptography==2.0
amqp==1.4.7 amqp==1.4.7
anyjson==0.3.3 anyjson==0.3.3
arrow==0.7.0 arrow==0.7.0
billiard==3.3.0.20 billiard==3.3.0.20
bpython==0.14.1 bpython==0.14.1
celery==3.1.18 celery==3.1.18
Django==1.8.15 Django==1.11.6
django-appconf==1.0.1 django-appconf==1.0.2
django-autocomplete-light==2.1.1 django-autocomplete-light==3.2.9
django-braces==1.8.0 django-braces==1.11.0
django-crispy-forms==1.6.0 django-crispy-forms==1.6.1
django-model-utils==2.2 django-model-utils==3.0.0
djangosaml2==0.13.0 django-pipeline==1.6.13
django-sizefield==0.7 django-sizefield==0.9.1
django-statici18n==1.4.0
django-tables2==1.10.0
django-taggit==0.22.1
djangosaml2==0.16.10
git+https://git.ik.bme.hu/circle/django-sshkey.git git+https://git.ik.bme.hu/circle/django-sshkey.git
django-statici18n==1.1.3
django-tables2==0.16.0
django-taggit==0.14.0
docutils==0.12 docutils==0.12
Jinja2==2.7.3 Jinja2==2.7.3
jsonfield==1.0.3 jsonfield==1.0.3
...@@ -39,8 +41,10 @@ six==1.9.0 ...@@ -39,8 +41,10 @@ six==1.9.0
slimit==0.8.1 slimit==0.8.1
sqlparse==0.1.15 sqlparse==0.1.15
pika==0.9.14 pika==0.9.14
django-pipeline==1.4.7
Fabric==1.10.1 Fabric==1.10.1
lxml==3.4.4 lxml==3.4.4
django-auth-ldap==1.2.8 django-auth-ldap==1.7.0
python-ldap==2.4.30 python-ldap==3.1.0
python-memcached==1.58
enum34==1.1.6
ipaddress==1.0.18
# Local development dependencies go here # Local development dependencies go here
-r base.txt -r base.txt
coverage==3.7.1 coverage==3.7.1
django-debug-toolbar==1.3.0 django-debug-toolbar==1.8
django-rosetta==0.7.6 django-rosetta==0.7.13
Sphinx==1.3.1 Sphinx==1.3.1
...@@ -3,9 +3,9 @@ ...@@ -3,9 +3,9 @@
coverage==3.7.1 coverage==3.7.1
factory-boy==2.4.1 factory-boy==2.4.1
mock==1.0.1 mock==1.0.1
django-nose==1.4 django-nose==1.4.4
nose==1.3.6 nose==1.3.7
nose-exclude==0.2.0 nose-exclude==0.5.0
selenium==2.45.0 selenium==2.45.0
selenose==1.3 #selenose==1.3
-e git+https://github.com/kmmbvnr/django-jenkins.git@019774dc2f668bc66b66f90f97eb8e14ae9566a4#egg=django_jenkins-dev -e git+https://github.com/kmmbvnr/django-jenkins.git@019774dc2f668bc66b66f90f97eb8e14ae9566a4#egg=django_jenkins-dev
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