From 3bbf8a6e1ec97032d90ee3dd730b0f821abc4d86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20Wargu=C5=82a?= Date: Tue, 7 Oct 2014 09:52:15 +0200 Subject: [PATCH 001/189] Add Regions models --- src/ralph/account/admin.py | 14 +- src/ralph/account/fixtures/initial_data.yaml | 6 + .../migrations/0005_auto__add_region.py | 195 ++++++++++++++++++ src/ralph/account/models.py | 17 +- 4 files changed, 229 insertions(+), 3 deletions(-) create mode 100644 src/ralph/account/fixtures/initial_data.yaml create mode 100644 src/ralph/account/migrations/0005_auto__add_region.py diff --git a/src/ralph/account/admin.py b/src/ralph/account/admin.py index 1253911d78..2dd792d23b 100644 --- a/src/ralph/account/admin.py +++ b/src/ralph/account/admin.py @@ -11,11 +11,14 @@ from django.utils.translation import ugettext_lazy as _ from lck.django.activitylog.admin import IPInline, UserAgentInline -from lck.django.common.admin import ForeignKeyAutocompleteTabularInline +from lck.django.common.admin import ( + ForeignKeyAutocompleteTabularInline, + ModelAdmin, +) from lck.django.profile.admin import ProfileInlineFormSet from tastypie.models import ApiKey -from ralph.account.models import BoundPerm, Profile +from ralph.account.models import BoundPerm, Profile, Region class ProfileInline(admin.StackedInline): @@ -138,3 +141,10 @@ class CustomGroupAdmin(GroupAdmin): admin.site.unregister(Group) admin.site.register(Group, CustomGroupAdmin) + + +class RegionAdmin(ModelAdmin): + list_display = ('name',) + search_fields = ('name',) + +admin.site.register(Region, RegionAdmin) diff --git a/src/ralph/account/fixtures/initial_data.yaml b/src/ralph/account/fixtures/initial_data.yaml new file mode 100644 index 0000000000..c72a0e06d3 --- /dev/null +++ b/src/ralph/account/fixtures/initial_data.yaml @@ -0,0 +1,6 @@ +# Initial regions + +- model: account.Region + pk: 1 + fields: + name: Default region diff --git a/src/ralph/account/migrations/0005_auto__add_region.py b/src/ralph/account/migrations/0005_auto__add_region.py new file mode 100644 index 0000000000..4927e643f2 --- /dev/null +++ b/src/ralph/account/migrations/0005_auto__add_region.py @@ -0,0 +1,195 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding model 'Region' + db.create_table('account_region', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('name', self.gf('django.db.models.fields.CharField')(unique=True, max_length=75, db_index=True)), + )) + db.send_create_signal('account', ['Region']) + + # Adding M2M table for field regions on 'Profile' + db.create_table('account_profile_regions', ( + ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), + ('profile', models.ForeignKey(orm['account.profile'], null=False)), + ('region', models.ForeignKey(orm['account.region'], null=False)) + )) + db.create_unique('account_profile_regions', ['profile_id', 'region_id']) + + + def backwards(self, orm): + # Deleting model 'Region' + db.delete_table('account_region') + + # Removing M2M table for field regions on 'Profile' + db.delete_table('account_profile_regions') + + + models = { + 'account.boundperm': { + 'Meta': {'object_name': 'BoundPerm'}, + 'cache_version': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "u'+'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['account.Profile']", 'blank': 'True', 'null': 'True'}), + 'group': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['auth.Group']", 'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'modified_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "u'+'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['account.Profile']", 'blank': 'True', 'null': 'True'}), + 'perm': (u'dj.choices.fields.ChoiceField', [], {'unique': 'False', 'primary_key': 'False', 'db_column': 'None', 'blank': 'False', 'null': 'False', '_in_south': 'True', 'db_index': 'False'}), + 'profile': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['account.Profile']", 'null': 'True', 'blank': 'True'}), + 'role': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['business.VentureRole']", 'null': 'True', 'blank': 'True'}), + 'venture': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['business.Venture']", 'null': 'True', 'blank': 'True'}) + }, + 'account.profile': { + 'Meta': {'object_name': 'Profile'}, + 'activation_token': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '40', 'blank': 'True'}), + 'birth_date': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}), + 'city': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'company': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}), + 'cost_center': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'blank': 'True'}), + 'country': ('django.db.models.fields.PositiveIntegerField', [], {'default': '153'}), + 'department': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}), + 'employee_id': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}), + 'gender': ('django.db.models.fields.PositiveIntegerField', [], {'default': '2'}), + 'home_page': (u'dj.choices.fields.ChoiceField', [], {'unique': 'False', 'primary_key': 'False', 'db_column': 'None', 'blank': 'False', u'default': '1', 'null': 'False', '_in_south': 'True', 'db_index': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'last_active': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'location': ('django.db.models.fields.CharField', [], {'max_length': '128', 'blank': 'True'}), + 'manager': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'blank': 'True'}), + 'nick': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '30', 'blank': 'True'}), + 'profit_center': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'blank': 'True'}), + 'regions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['account.Region']", 'symmetrical': 'False'}), + 'time_zone': ('django.db.models.fields.FloatField', [], {'default': '1.0'}), + 'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True'}) + }, + 'account.region': { + 'Meta': {'object_name': 'Region'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '75', 'db_index': 'True'}) + }, + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'business.businesssegment': { + 'Meta': {'object_name': 'BusinessSegment'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '75', 'db_index': 'True'}) + }, + 'business.department': { + 'Meta': {'ordering': "(u'name',)", 'object_name': 'Department'}, + 'icon': (u'dj.choices.fields.ChoiceField', [], {'unique': 'False', 'primary_key': 'False', 'db_column': 'None', 'blank': 'True', u'default': 'None', 'null': 'True', '_in_south': 'True', 'db_index': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '75', 'db_index': 'True'}) + }, + 'business.profitcenter': { + 'Meta': {'object_name': 'ProfitCenter'}, + 'description': ('django.db.models.fields.TextField', [], {'default': 'None', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '75', 'db_index': 'True'}) + }, + 'business.venture': { + 'Meta': {'ordering': "(u'parent__symbol', u'symbol')", 'unique_together': "((u'parent', u'symbol'),)", 'object_name': 'Venture'}, + 'business_segment': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['business.BusinessSegment']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}), + 'cache_version': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'data_center': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['discovery.DataCenter']", 'null': 'True', 'blank': 'True'}), + 'department': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['business.Department']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_infrastructure': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'margin_kind': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['discovery.MarginKind']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '75', 'db_index': 'True'}), + 'parent': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "u'child_set'", 'null': 'True', 'blank': 'True', 'to': "orm['business.Venture']"}), + 'path': ('django.db.models.fields.TextField', [], {'default': "u''", 'blank': 'True'}), + 'preboot': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['deployment.Preboot']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}), + 'profit_center': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['business.ProfitCenter']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}), + 'show_in_ralph': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'symbol': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '32', 'blank': 'True'}), + 'verified': ('django.db.models.fields.BooleanField', [], {'default': 'False'}) + }, + 'business.venturerole': { + 'Meta': {'ordering': "(u'parent__name', u'name')", 'unique_together': "((u'name', u'venture'),)", 'object_name': 'VentureRole'}, + 'cache_version': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '75'}), + 'parent': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "u'child_set'", 'null': 'True', 'blank': 'True', 'to': "orm['business.VentureRole']"}), + 'path': ('django.db.models.fields.TextField', [], {'default': "u''", 'blank': 'True'}), + 'preboot': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['deployment.Preboot']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}), + 'venture': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['business.Venture']"}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'deployment.preboot': { + 'Meta': {'ordering': "(u'name',)", 'object_name': 'Preboot'}, + 'cache_version': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'description': ('django.db.models.fields.TextField', [], {'default': "u''", 'blank': 'True'}), + 'files': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['deployment.PrebootFile']", 'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '75', 'db_index': 'True'}) + }, + 'deployment.prebootfile': { + 'Meta': {'object_name': 'PrebootFile'}, + 'description': ('django.db.models.fields.TextField', [], {'default': "u''", 'blank': 'True'}), + 'file': ('django.db.models.fields.files.FileField', [], {'default': 'None', 'max_length': '100', 'null': 'True', 'blank': 'True'}), + 'ftype': (u'dj.choices.fields.ChoiceField', [], {'unique': 'False', 'primary_key': 'False', 'db_column': 'None', 'blank': 'False', u'default': '101', 'null': 'False', '_in_south': 'True', 'db_index': 'False'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '75', 'db_index': 'True'}), + 'raw_config': ('django.db.models.fields.TextField', [], {'blank': 'True'}) + }, + 'discovery.datacenter': { + 'Meta': {'ordering': "(u'name',)", 'object_name': 'DataCenter'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '75', 'db_index': 'True'}) + }, + 'discovery.marginkind': { + 'Meta': {'object_name': 'MarginKind'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'margin': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '75', 'db_index': 'True'}), + 'remarks': ('django.db.models.fields.TextField', [], {'default': "u''", 'blank': 'True'}) + } + } + + complete_apps = ['account'] \ No newline at end of file diff --git a/src/ralph/account/models.py b/src/ralph/account/models.py index d176b6aa67..6e3f4e57e0 100644 --- a/src/ralph/account/models.py +++ b/src/ralph/account/models.py @@ -11,6 +11,7 @@ from django.conf import settings from django.contrib.auth.models import Group +from django.core.exceptions import ValidationError from django.core.handlers.wsgi import WSGIRequest from django.core.urlresolvers import reverse, NoReverseMatch from django.db import models as db @@ -21,7 +22,7 @@ from dj.choices import Choices from dj.choices.fields import ChoiceField from lck.django.activitylog.models import MonitoredActivity -from lck.django.common.models import TimeTrackable, EditorTrackable +from lck.django.common.models import EditorTrackable, Named, TimeTrackable from lck.django.profile.models import ( BasicInfo, ActivationSupport, @@ -82,6 +83,19 @@ class Perm(Choices): has_scrooge_access = _("has_scrooge_access") +class Region(Named): + """Used for distinguishing the origin of the object by region""" + + @classmethod + def check_default_exist(cls): + return cls.objects.filter(default=True).exists() + + def clean(self, *args, **kwargs): + if self.default and self.check_default_exist(): + raise ValidationError(_('default region already exist')) + return super(Region, self).clean(*args, **kwargs) + + class Profile(BasicInfo, ActivationSupport, GravatarSupport, MonitoredActivity): @@ -102,6 +116,7 @@ class Meta: department = db.CharField(max_length=64, blank=True) manager = db.CharField(max_length=1024, blank=True) location = db.CharField(max_length=128, blank=True) + regions = db.ManyToManyField(Region) def __unicode__(self): return self.nick From 32f2fea6188c3b3771058f8e27e2d39b33f2e4d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20Wargu=C5=82a?= Date: Wed, 8 Oct 2014 08:40:24 +0200 Subject: [PATCH 002/189] Fix test migrations --- .../migrations/0005_auto__add_region.py | 21 ++++++++++--------- src/ralph/account/models.py | 3 ++- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/src/ralph/account/migrations/0005_auto__add_region.py b/src/ralph/account/migrations/0005_auto__add_region.py index 4927e643f2..33e379e896 100644 --- a/src/ralph/account/migrations/0005_auto__add_region.py +++ b/src/ralph/account/migrations/0005_auto__add_region.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -import datetime +from south.utils import datetime_utils as datetime from south.db import db from south.v2 import SchemaMigration from django.db import models @@ -15,21 +15,22 @@ def forwards(self, orm): )) db.send_create_signal('account', ['Region']) - # Adding M2M table for field regions on 'Profile' - db.create_table('account_profile_regions', ( + # Adding M2M table for field profile on 'Region' + m2m_table_name = db.shorten_name('account_region_profile') + db.create_table(m2m_table_name, ( ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), - ('profile', models.ForeignKey(orm['account.profile'], null=False)), - ('region', models.ForeignKey(orm['account.region'], null=False)) + ('region', models.ForeignKey(orm['account.region'], null=False)), + ('profile', models.ForeignKey(orm['account.profile'], null=False)) )) - db.create_unique('account_profile_regions', ['profile_id', 'region_id']) + db.create_unique(m2m_table_name, ['region_id', 'profile_id']) def backwards(self, orm): # Deleting model 'Region' db.delete_table('account_region') - # Removing M2M table for field regions on 'Profile' - db.delete_table('account_profile_regions') + # Removing M2M table for field profile on 'Region' + db.delete_table(db.shorten_name('account_region_profile')) models = { @@ -65,14 +66,14 @@ def backwards(self, orm): 'manager': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'blank': 'True'}), 'nick': ('django.db.models.fields.CharField', [], {'default': "u''", 'max_length': '30', 'blank': 'True'}), 'profit_center': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'blank': 'True'}), - 'regions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['account.Region']", 'symmetrical': 'False'}), 'time_zone': ('django.db.models.fields.FloatField', [], {'default': '1.0'}), 'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True'}) }, 'account.region': { 'Meta': {'object_name': 'Region'}, 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '75', 'db_index': 'True'}) + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '75', 'db_index': 'True'}), + 'profile': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['account.Profile']", 'symmetrical': 'False'}) }, 'auth.group': { 'Meta': {'object_name': 'Group'}, diff --git a/src/ralph/account/models.py b/src/ralph/account/models.py index 6e3f4e57e0..448bc873aa 100644 --- a/src/ralph/account/models.py +++ b/src/ralph/account/models.py @@ -86,6 +86,8 @@ class Perm(Choices): class Region(Named): """Used for distinguishing the origin of the object by region""" + profile = db.ManyToManyField('Profile') + @classmethod def check_default_exist(cls): return cls.objects.filter(default=True).exists() @@ -116,7 +118,6 @@ class Meta: department = db.CharField(max_length=64, blank=True) manager = db.CharField(max_length=1024, blank=True) location = db.CharField(max_length=128, blank=True) - regions = db.ManyToManyField(Region) def __unicode__(self): return self.nick From cd279be6f84e4baf53c00e0290a3b6cc07784142 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20Wargu=C5=82a?= Date: Wed, 8 Oct 2014 08:48:42 +0200 Subject: [PATCH 003/189] Fix migration for old South version --- src/ralph/account/migrations/0005_auto__add_region.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/ralph/account/migrations/0005_auto__add_region.py b/src/ralph/account/migrations/0005_auto__add_region.py index 33e379e896..5d23c1711f 100644 --- a/src/ralph/account/migrations/0005_auto__add_region.py +++ b/src/ralph/account/migrations/0005_auto__add_region.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from south.utils import datetime_utils as datetime +import datetime from south.db import db from south.v2 import SchemaMigration from django.db import models @@ -16,13 +16,12 @@ def forwards(self, orm): db.send_create_signal('account', ['Region']) # Adding M2M table for field profile on 'Region' - m2m_table_name = db.shorten_name('account_region_profile') - db.create_table(m2m_table_name, ( + db.create_table('account_region_profile', ( ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), ('region', models.ForeignKey(orm['account.region'], null=False)), ('profile', models.ForeignKey(orm['account.profile'], null=False)) )) - db.create_unique(m2m_table_name, ['region_id', 'profile_id']) + db.create_unique('account_region_profile', ['region_id', 'profile_id']) def backwards(self, orm): @@ -30,7 +29,7 @@ def backwards(self, orm): db.delete_table('account_region') # Removing M2M table for field profile on 'Region' - db.delete_table(db.shorten_name('account_region_profile')) + db.delete_table('account_region_profile') models = { From 5fc10fa5ddf199e6d91f5b6352d1fba5c7c73948 Mon Sep 17 00:00:00 2001 From: Tymoteusz Jankowski Date: Wed, 8 Oct 2014 09:47:23 +0200 Subject: [PATCH 004/189] added RegionFactory & Profile.get_regions method --- src/ralph/account/models.py | 13 ++++++++----- src/ralph/util/tests/utils.py | 10 +++++++++- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/src/ralph/account/models.py b/src/ralph/account/models.py index 6e3f4e57e0..8f52bc9f54 100644 --- a/src/ralph/account/models.py +++ b/src/ralph/account/models.py @@ -90,11 +90,6 @@ class Region(Named): def check_default_exist(cls): return cls.objects.filter(default=True).exists() - def clean(self, *args, **kwargs): - if self.default and self.check_default_exist(): - raise ValidationError(_('default region already exist')) - return super(Region, self).clean(*args, **kwargs) - class Profile(BasicInfo, ActivationSupport, GravatarSupport, MonitoredActivity): @@ -197,6 +192,14 @@ def filter_by_perm(self, query, perm): venture__parent__parent__parent__boundperm__perm=perm.id) ).distinct() + def get_regions(self): + regions = self.regions.all() + if not regions: + default_region = Region.objects.get(pk=1) + self.add(default_region) + self.save() + return regions + class BoundPerm(TimeTrackable, EditorTrackable): profile = db.ForeignKey( diff --git a/src/ralph/util/tests/utils.py b/src/ralph/util/tests/utils.py index 596c3ce74e..7324a1f453 100644 --- a/src/ralph/util/tests/utils.py +++ b/src/ralph/util/tests/utils.py @@ -10,7 +10,10 @@ from factory.django import DjangoModelFactory from django.contrib.auth.models import User -from ralph.account.models import Profile +from ralph.account.models import ( + Profile, + Region, +) from ralph.cmdb import models_ci from ralph.cmdb.tests.utils import CIFactory @@ -78,3 +81,8 @@ class ServiceProfitCenterRelationFactory(DjangoModelFactory): child = factory.SubFactory(ServiceFactory) type = models_ci.CI_RELATION_TYPES.CONTAINS readonly = False + + +class RegionFactory(DjangoModelFactory): + FACTORY_FOR = Region + name = factory.Sequence(lambda n: 'Region #{}'.format(n)) From f41daaa98f080b39ad40435665822aa46fae5de4 Mon Sep 17 00:00:00 2001 From: Tymoteusz Jankowski Date: Wed, 8 Oct 2014 15:43:50 +0200 Subject: [PATCH 005/189] added get_default_region & is_region_granted methods + small fixes --- src/ralph/account/models.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/ralph/account/models.py b/src/ralph/account/models.py index 8f52bc9f54..c63fc8c907 100644 --- a/src/ralph/account/models.py +++ b/src/ralph/account/models.py @@ -11,7 +11,6 @@ from django.conf import settings from django.contrib.auth.models import Group -from django.core.exceptions import ValidationError from django.core.handlers.wsgi import WSGIRequest from django.core.urlresolvers import reverse, NoReverseMatch from django.db import models as db @@ -90,6 +89,13 @@ class Region(Named): def check_default_exist(cls): return cls.objects.filter(default=True).exists() + @classmethod + def get_default_region(cls): + return cls.objects.get(pk=1) + + def __unicode__(self): + return self.name + class Profile(BasicInfo, ActivationSupport, GravatarSupport, MonitoredActivity): @@ -192,12 +198,16 @@ def filter_by_perm(self, query, perm): venture__parent__parent__parent__boundperm__perm=perm.id) ).distinct() + def is_region_granted(self, region): + return self.regions.filter(pk=region.id).exists() + def get_regions(self): regions = self.regions.all() if not regions: default_region = Region.objects.get(pk=1) - self.add(default_region) + self.regions.add(default_region) self.save() + regions = self.regions.all() return regions From fb8c3340ef887603a9e45859e7bd8dd191d91371 Mon Sep 17 00:00:00 2001 From: Tymoteusz Jankowski Date: Thu, 9 Oct 2014 09:54:50 +0200 Subject: [PATCH 006/189] changed region to region_set according to latest migration --- src/ralph/account/models.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ralph/account/models.py b/src/ralph/account/models.py index 1c8402b866..3d1fe7902d 100644 --- a/src/ralph/account/models.py +++ b/src/ralph/account/models.py @@ -203,12 +203,12 @@ def is_region_granted(self, region): return self.regions.filter(pk=region.id).exists() def get_regions(self): - regions = self.regions.all() + regions = self.region_set.all() if not regions: default_region = Region.objects.get(pk=1) - self.regions.add(default_region) + self.region_set.add(default_region) self.save() - regions = self.regions.all() + regions = self.region_set.all() return regions From bc37ff19358f45e35228b771fff8f071fbc20ba4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20Wargu=C5=82a?= Date: Thu, 9 Oct 2014 13:24:09 +0200 Subject: [PATCH 007/189] Add Regionalized middleware --- src/ralph/account/admin.py | 1 + src/ralph/account/models.py | 18 +++++++++--------- src/ralph/settings.py | 1 + 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/ralph/account/admin.py b/src/ralph/account/admin.py index 2dd792d23b..2dac1451d0 100644 --- a/src/ralph/account/admin.py +++ b/src/ralph/account/admin.py @@ -146,5 +146,6 @@ class CustomGroupAdmin(GroupAdmin): class RegionAdmin(ModelAdmin): list_display = ('name',) search_fields = ('name',) + exclude = ['profile'] admin.site.register(Region, RegionAdmin) diff --git a/src/ralph/account/models.py b/src/ralph/account/models.py index 448bc873aa..1f1b40018c 100644 --- a/src/ralph/account/models.py +++ b/src/ralph/account/models.py @@ -88,15 +88,6 @@ class Region(Named): profile = db.ManyToManyField('Profile') - @classmethod - def check_default_exist(cls): - return cls.objects.filter(default=True).exists() - - def clean(self, *args, **kwargs): - if self.default and self.check_default_exist(): - raise ValidationError(_('default region already exist')) - return super(Region, self).clean(*args, **kwargs) - class Profile(BasicInfo, ActivationSupport, GravatarSupport, MonitoredActivity): @@ -122,6 +113,15 @@ class Meta: def __unicode__(self): return self.nick + def get_regions(self): + regions = self.region_set.all() + if not regions: + regions = Region.objects.filter( + name=settings.DEFAULT_REGION_NAME, + ) + return regions + + def has_perm(self, perm, obj=None, role=None): if not self.is_active: return False diff --git a/src/ralph/settings.py b/src/ralph/settings.py index a9337c1560..cd7a14f88f 100644 --- a/src/ralph/settings.py +++ b/src/ralph/settings.py @@ -51,6 +51,7 @@ 'django.contrib.messages.middleware.MessageMiddleware', 'lck.django.activitylog.middleware.ActivityMiddleware', 'lck.django.common.middleware.ForceLanguageCodeMiddleware', + 'ralph_assets.middleware.RequestMiddleware', ) ROOT_URLCONF = 'ralph.urls' TEMPLATE_DIRS = (CURRENT_DIR + "templates",) From c2f90dc869798c2778fecfc498b0f8d81e477a79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20Wargu=C5=82a?= Date: Thu, 9 Oct 2014 15:14:00 +0200 Subject: [PATCH 008/189] Add default region --- src/ralph/settings.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ralph/settings.py b/src/ralph/settings.py index cd7a14f88f..a364aa0258 100644 --- a/src/ralph/settings.py +++ b/src/ralph/settings.py @@ -755,3 +755,4 @@ # url to page where user requests permission to module (eg. assets) # REQUEST_PERM_URL = 'http://tickets.office/request/ralph_module/permission' +DEFAULT_REGION_NAME = 'Default Region' From 26c20e9db9cc5a682b9d64987848ab58707bca82 Mon Sep 17 00:00:00 2001 From: Arkadiusz Adamski Date: Fri, 10 Oct 2014 13:40:25 +0200 Subject: [PATCH 009/189] added regions to admin --- src/ralph/account/admin.py | 60 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/src/ralph/account/admin.py b/src/ralph/account/admin.py index 2dac1451d0..b04f412ffa 100644 --- a/src/ralph/account/admin.py +++ b/src/ralph/account/admin.py @@ -5,7 +5,13 @@ from __future__ import print_function from __future__ import unicode_literals +from django import forms from django.contrib import admin +from django.forms.models import ( + ModelForm, + BaseModelFormSet, + modelformset_factory, +) from django.contrib.auth.admin import UserAdmin, GroupAdmin from django.contrib.auth.models import User, Group from django.utils.translation import ugettext_lazy as _ @@ -70,6 +76,59 @@ class ApiKeyInline(admin.StackedInline): extra = 0 +class RegionInlineFormSet(BaseModelFormSet): + + def __init__(self, *args, **kwargs): + self.instance = kwargs.pop('instance') + super(RegionInlineFormSet, self).__init__(*args, **kwargs) + + def _construct_form(self, *args, **kwargs): + return super(RegionInlineFormSet, self)._construct_form( + user_instance=self.instance, *args, **kwargs + ) + + +class RegionForm(ModelForm): + assigned = forms.BooleanField(label=_('assigned'), required=False) + + def __init__(self, *args, **kwargs): + self.user_instance = kwargs.pop('user_instance', None) + super(RegionForm, self).__init__(*args, **kwargs) + if self.user_instance: + self.fields['assigned'].initial = Region.profile.through.objects.filter( # noqa + region=self.instance, + profile=self.user_instance.profile + ).exists() + + def save(self, *args, **kwargs): + objects = Region.profile.through.objects + options = { + 'profile': self.user_instance.profile, + 'region': self.cleaned_data['id'], + } + if self.cleaned_data['assigned']: + objects.create(**options) + else: + objects.filter(**options).delete() + + +class RegionInline(admin.TabularInline): + formset = RegionInlineFormSet + form = RegionForm + model = Region + readonly_fields = ('name',) + fields = ('name', 'assigned') + + def get_formset(self, *args, **kwargs): + return modelformset_factory( + self.model, + extra=0, + form=self.form, + formset=self.formset, + max_num=1, + ) + + class ProfileAdmin(UserAdmin): def groups_show(self): @@ -83,6 +142,7 @@ def groups_show(self): ApiKeyInline, ProfileIPInline, ProfileUserAgentInline, + RegionInline, ] list_display = ( 'username', From e7662e5fcf62895f1185eee9447f256e1d9672f0 Mon Sep 17 00:00:00 2001 From: Tymoteusz Jankowski Date: Fri, 10 Oct 2014 15:22:27 +0200 Subject: [PATCH 010/189] fixed typo --- src/ralph/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ralph/settings.py b/src/ralph/settings.py index a364aa0258..efb43f7413 100644 --- a/src/ralph/settings.py +++ b/src/ralph/settings.py @@ -755,4 +755,4 @@ # url to page where user requests permission to module (eg. assets) # REQUEST_PERM_URL = 'http://tickets.office/request/ralph_module/permission' -DEFAULT_REGION_NAME = 'Default Region' +DEFAULT_REGION_NAME = 'Default region' From 317778aedad4eb14b564b7d304051ccc5d24fd36 Mon Sep 17 00:00:00 2001 From: Tymoteusz Jankowski Date: Fri, 10 Oct 2014 15:27:20 +0200 Subject: [PATCH 011/189] fixed regionadmin --- src/ralph/account/admin.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ralph/account/admin.py b/src/ralph/account/admin.py index b04f412ffa..ef9ef92863 100644 --- a/src/ralph/account/admin.py +++ b/src/ralph/account/admin.py @@ -123,6 +123,7 @@ def get_formset(self, *args, **kwargs): return modelformset_factory( self.model, extra=0, + exclude=['profile', 'name'], form=self.form, formset=self.formset, max_num=1, @@ -139,10 +140,10 @@ def groups_show(self): inlines = [ ProfileInline, ProfileBoundPermInline, + RegionInline, ApiKeyInline, ProfileIPInline, ProfileUserAgentInline, - RegionInline, ] list_display = ( 'username', From 3ab13b6ee22e56d3e642ca5b57150b260eac768a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20Wargu=C5=82a?= Date: Mon, 13 Oct 2014 08:26:04 +0200 Subject: [PATCH 012/189] Remove redefinition method get_regions() --- src/ralph/account/models.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/ralph/account/models.py b/src/ralph/account/models.py index bc53f59a55..c27bbc92bc 100644 --- a/src/ralph/account/models.py +++ b/src/ralph/account/models.py @@ -127,7 +127,6 @@ def get_regions(self): ) return regions - def has_perm(self, perm, obj=None, role=None): if not self.is_active: return False @@ -207,15 +206,6 @@ def filter_by_perm(self, query, perm): def is_region_granted(self, region): return self.regions.filter(pk=region.id).exists() - def get_regions(self): - regions = self.region_set.all() - if not regions: - default_region = Region.objects.get(pk=1) - self.region_set.add(default_region) - self.save() - regions = self.region_set.all() - return regions - class BoundPerm(TimeTrackable, EditorTrackable): profile = db.ForeignKey( From 5c2187f8c87246038708858b6a592cbb9344ad1d Mon Sep 17 00:00:00 2001 From: Tymoteusz Jankowski Date: Mon, 13 Oct 2014 10:00:24 +0200 Subject: [PATCH 013/189] changes.rst update --- CHANGES.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 5838034ef0..24459dd812 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,6 +1,11 @@ Change Log ---------- +DEV +~~~ + +* Added support for regions + 2.1.0 ~~~~~ From 1644e5e22e5c8e4052d064381456081ac0e41c20 Mon Sep 17 00:00:00 2001 From: Andrzej Jankowski Date: Wed, 15 Oct 2014 08:41:02 +0200 Subject: [PATCH 014/189] device admin form - fix --- src/ralph/discovery/admin.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/ralph/discovery/admin.py b/src/ralph/discovery/admin.py index 2446276b53..3167f656e3 100644 --- a/src/ralph/discovery/admin.py +++ b/src/ralph/discovery/admin.py @@ -372,6 +372,21 @@ def save_formset(self, request, form, formset, change): if formset.model.__name__ == 'RolePropertyValue': for instance in formset.save(commit=False): instance.save(user=request.user) + elif formset.model.__name__ == 'IPAddress': + for instance in formset.save(commit=False): + if not instance.id: + # Sometimes IP address exists and does not have any + # assigned device. In this case we should reuse it, + # otherwise we can get IntegrityError. + try: + ip_id = models.IPAddress.objects.filter( + address=instance.address, + ).values_list('id', flat=True)[0] + except IndexError: + pass + else: + instance.id = ip_id + instance.save() else: formset.save(commit=True) From 7b1fd93d2e94e1ba3063a30fad734e7c7c81a6c9 Mon Sep 17 00:00:00 2001 From: Andrzej Jankowski Date: Wed, 15 Oct 2014 13:26:33 +0200 Subject: [PATCH 015/189] dhcp - additional validation --- src/ralph/dnsedit/admin.py | 19 +++++++++++++++++++ src/ralph/ui/forms/addresses.py | 8 ++++++++ 2 files changed, 27 insertions(+) diff --git a/src/ralph/dnsedit/admin.py b/src/ralph/dnsedit/admin.py index 66947283e3..415846922a 100644 --- a/src/ralph/dnsedit/admin.py +++ b/src/ralph/dnsedit/admin.py @@ -5,6 +5,7 @@ from __future__ import print_function from __future__ import unicode_literals +from django import forms from django.contrib import admin from django.utils.translation import ugettext_lazy as _ from lck.django.common.admin import ModelAdmin @@ -12,6 +13,23 @@ from ralph.dnsedit.models import DHCPEntry, DNSHistory, DHCPServer, DNSServer +class DHCPEntryAdminForm(forms.ModelForm): + + class Meta: + model = DHCPEntry + + def clean_ip(self): + ip = self.cleaned_data['ip'] + queryset = DHCPEntry.objects.filter(ip=ip) + if self.instance: + queryset = queryset.exclude(id=self.instance.id) + if queryset.count() > 0: + raise forms.ValidationError( + _('DHCP entry with this IP address already exists.'), + ) + return ip + + class DHCPEntryAdmin(ModelAdmin): def ip_address(self): @@ -22,6 +40,7 @@ def ip_address(self): list_display = (ip_address, 'mac') search_fields = ('ip', 'mac') save_on_top = True + form = DHCPEntryAdminForm admin.site.register(DHCPEntry, DHCPEntryAdmin) diff --git a/src/ralph/ui/forms/addresses.py b/src/ralph/ui/forms/addresses.py index 147f820ba0..d07cd9b792 100644 --- a/src/ralph/ui/forms/addresses.py +++ b/src/ralph/ui/forms/addresses.py @@ -8,6 +8,7 @@ from bob.forms import AutocompleteWidget from django import forms +from django.utils.translation import ugettext_lazy as _ from lck.django.common.models import MACAddressField from powerdns.models import Domain, Record @@ -228,6 +229,13 @@ def clean_ip(self): ip = unicode(ipaddr.IPAddress(ip)) except ValueError: raise forms.ValidationError("Invalid IP address") + queryset = DHCPEntry.objects.filter(ip=ip) + if self.instance: + queryset = queryset.exclude(id=self.instance.id) + if queryset.count() > 0: + raise forms.ValidationError( + _('DHCP entry with this IP address already exists.'), + ) return ip def clean_mac(self): From fe7cdfdfb904d1b7d46abd8c01ac2fc4407fca9b Mon Sep 17 00:00:00 2001 From: Tymoteusz Jankowski Date: Thu, 16 Oct 2014 09:00:59 +0200 Subject: [PATCH 016/189] added middleware requried by regions feature --- src/ralph/middleware.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 src/ralph/middleware.py diff --git a/src/ralph/middleware.py b/src/ralph/middleware.py new file mode 100644 index 0000000000..583de29c66 --- /dev/null +++ b/src/ralph/middleware.py @@ -0,0 +1,32 @@ +from threading import current_thread + +from django.conf import settings + +from ralph.account.models import Region + + +_requests = {} + + +def get_actual_regions(): + thread_name = current_thread().name + if thread_name not in _requests: + return Region.objects.filter( + name=settings.DEFAULT_REGION_NAME, + ) + return _requests[thread_name]['regions'] + + +class RequestMiddleware(object): + def process_request(self, request): + if hasattr(request, 'user') and not request.user.is_anonymous(): + data = { + 'user_id': request.user.id, + 'regions': request.user.profile.get_regions(), + } + _requests[current_thread().name] = data + + def process_response(self, request, response): + if hasattr(request, 'user') and not request.user.is_anonymous(): + _requests.pop(current_thread().name, None) + return response From 5e109f8b38c497136fcf2079b07f7e71c89ba181 Mon Sep 17 00:00:00 2001 From: Tymoteusz Jankowski Date: Thu, 16 Oct 2014 09:17:29 +0200 Subject: [PATCH 017/189] updated request middleware path in settings --- src/ralph/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ralph/settings.py b/src/ralph/settings.py index efb43f7413..75cf9da5bc 100644 --- a/src/ralph/settings.py +++ b/src/ralph/settings.py @@ -51,7 +51,7 @@ 'django.contrib.messages.middleware.MessageMiddleware', 'lck.django.activitylog.middleware.ActivityMiddleware', 'lck.django.common.middleware.ForceLanguageCodeMiddleware', - 'ralph_assets.middleware.RequestMiddleware', + 'ralph.middleware.RequestMiddleware', ) ROOT_URLCONF = 'ralph.urls' TEMPLATE_DIRS = (CURRENT_DIR + "templates",) From 8dd58319b9b54ebc60eabd1c3920dc7f0b8139e1 Mon Sep 17 00:00:00 2001 From: Tymoteusz Jankowski Date: Thu, 16 Oct 2014 11:17:11 +0200 Subject: [PATCH 018/189] get_or_create in get_default_region --- src/ralph/account/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ralph/account/models.py b/src/ralph/account/models.py index c27bbc92bc..b1fa08ecaa 100644 --- a/src/ralph/account/models.py +++ b/src/ralph/account/models.py @@ -89,7 +89,7 @@ class Region(Named): @classmethod def get_default_region(cls): - return cls.objects.get(name=settings.DEFAULT_REGION_NAME) + return cls.objects.get_or_create(name=settings.DEFAULT_REGION_NAME) def __unicode__(self): return self.name From a303734ff012f9374b0cfcb966b17813b33d4069 Mon Sep 17 00:00:00 2001 From: Tymoteusz Jankowski Date: Thu, 16 Oct 2014 11:48:37 +0200 Subject: [PATCH 019/189] fixed return in get_default_region --- src/ralph/account/models.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ralph/account/models.py b/src/ralph/account/models.py index b1fa08ecaa..be285cb0a5 100644 --- a/src/ralph/account/models.py +++ b/src/ralph/account/models.py @@ -89,7 +89,8 @@ class Region(Named): @classmethod def get_default_region(cls): - return cls.objects.get_or_create(name=settings.DEFAULT_REGION_NAME) + obj, created = cls.objects.get_or_create(name=settings.DEFAULT_REGION_NAME) + return obj def __unicode__(self): return self.name From 68790fc931dca3b8981fae11b55348e509951ca1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20Wargu=C5=82a?= Date: Fri, 17 Oct 2014 13:47:04 +0200 Subject: [PATCH 020/189] Fix build csv report with region handling --- src/ralph/middleware.py | 2 +- src/ralph/settings.py | 2 +- src/ralph/util/reports.py | 3 +++ 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/ralph/middleware.py b/src/ralph/middleware.py index 583de29c66..39dff052e8 100644 --- a/src/ralph/middleware.py +++ b/src/ralph/middleware.py @@ -17,7 +17,7 @@ def get_actual_regions(): return _requests[thread_name]['regions'] -class RequestMiddleware(object): +class RegionMiddleware(object): def process_request(self, request): if hasattr(request, 'user') and not request.user.is_anonymous(): data = { diff --git a/src/ralph/settings.py b/src/ralph/settings.py index 75cf9da5bc..21899c19ee 100644 --- a/src/ralph/settings.py +++ b/src/ralph/settings.py @@ -51,7 +51,7 @@ 'django.contrib.messages.middleware.MessageMiddleware', 'lck.django.activitylog.middleware.ActivityMiddleware', 'lck.django.common.middleware.ForceLanguageCodeMiddleware', - 'ralph.middleware.RequestMiddleware', + 'ralph.middleware.RegionMiddleware', ) ROOT_URLCONF = 'ralph.urls' TEMPLATE_DIRS = (CURRENT_DIR + "templates",) diff --git a/src/ralph/util/reports.py b/src/ralph/util/reports.py index 647228a37f..839e979ab7 100644 --- a/src/ralph/util/reports.py +++ b/src/ralph/util/reports.py @@ -20,6 +20,8 @@ from django.views.generic.base import View from django.http import HttpResponse, HttpResponseBadRequest +from ralph.middleware import RegionMiddleware + class PicklableRequest(object): @@ -60,6 +62,7 @@ def get_result(request): request = copy.deepcopy(request) SessionMiddleware().process_request(request) AuthenticationMiddleware().process_request(request) + RegionMiddleware().process_request(request) view.request = request return view.get_result( request, *url_match.args, **url_match.kwargs From 16257be648b571e4b939c4de07ce36469fccfdc0 Mon Sep 17 00:00:00 2001 From: Mateusz Kurek Date: Fri, 17 Oct 2014 15:04:47 +0200 Subject: [PATCH 021/189] added virtual model name and openstack filter by model --- src/ralph/util/api_scrooge.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/ralph/util/api_scrooge.py b/src/ralph/util/api_scrooge.py index 50aaaaf814..019ca58e8e 100644 --- a/src/ralph/util/api_scrooge.py +++ b/src/ralph/util/api_scrooge.py @@ -20,7 +20,9 @@ def get_virtual_usages(parent_service_uid=None): """Yields dicts reporting the number of virtual cores, memory and disk.""" - devices = Device.objects.filter(model__type=DeviceType.virtual_server) + devices = Device.objects.filter( + model__type=DeviceType.virtual_server + ).select_related('model') if parent_service_uid: devices = devices.filter( parent__service__uid=parent_service_uid, @@ -50,6 +52,8 @@ def get_virtual_usages(parent_service_uid=None): 'virtual_cores': cores or 0, 'virtual_memory': memory or 0, 'virtual_disk': disk or 0, + 'model_id': device.model_id, + 'model_name': device.model.name, } @@ -123,11 +127,14 @@ def get_environments(): } -def get_openstack_tenants(): - for tenant in Device.objects.filter( +def get_openstack_tenants(model_name=None): + tenants = Device.objects.filter( sn__startswith='openstack', model__type=DeviceType.cloud_server - ): + ) + if model_name: + tenants = tenants.filter(model__name=model_name) + for tenant in tenants: yield { 'device_id': tenant.id, 'tenant_id': tenant.sn[len('openstack-'):], From f371313bc960d3d706b9665820f3467a64ad205e Mon Sep 17 00:00:00 2001 From: Tomasz Mieszkowski Date: Fri, 17 Oct 2014 15:55:04 +0200 Subject: [PATCH 022/189] Initial version of a new proxmox plugin + some changes required by it. --- setup.py | 1 + src/ralph/discovery/http.py | 8 +- src/ralph/scan/plugins/proxmox_2_3.py | 217 ++++++++++++++++++++++++++ src/ralph/scan/plugins/ssh_proxmox.py | 4 +- src/ralph/settings.py | 17 ++ 5 files changed, 242 insertions(+), 5 deletions(-) create mode 100644 src/ralph/scan/plugins/proxmox_2_3.py diff --git a/setup.py b/setup.py index a50900489e..94a383533d 100644 --- a/setup.py +++ b/setup.py @@ -75,6 +75,7 @@ 'Pillow==2.4.0', 'pysphere==0.1.8', 'python-keystoneclient>=0.11.0', + 'proxmoxer==0.1.4', ], entry_points={ 'console_scripts': [ diff --git a/src/ralph/discovery/http.py b/src/ralph/discovery/http.py index bff73eb82b..6e94ec84e1 100644 --- a/src/ralph/discovery/http.py +++ b/src/ralph/discovery/http.py @@ -97,8 +97,10 @@ def guess_family(headers, document): if family in ('Apache', 'Unspecified'): if ' diff --git a/src/ralph/ui/static/partials/rack/rack_view.html b/src/ralph/ui/static/partials/rack/rack_view.html index b082009681..5a755c5bdb 100644 --- a/src/ralph/ui/static/partials/rack/rack_view.html +++ b/src/ralph/ui/static/partials/rack/rack_view.html @@ -27,6 +27,16 @@

Info {{ rack.info.name }}

Remarks {{ activeItem ? activeItem.remarks : '-' }} + + Links + + + Show in assets
+ Show in core +
+ - + + Slot {{ activeSlot.slot_no }} @@ -43,6 +53,16 @@

Info {{ rack.info.name }}

SN {{ activeSlot ? activeSlot.sn : '-' }} + + Links + + + Show in assets
+ Show in core +
+ - + + diff --git a/src/ralph/ui/static/ui/css/rack.css b/src/ralph/ui/static/ui/css/rack.css index 1dd54a5df4..5f8bfdbce5 100644 --- a/src/ralph/ui/static/ui/css/rack.css +++ b/src/ralph/ui/static/ui/css/rack.css @@ -520,6 +520,7 @@ position: relative; } .racks .rack .wrapper .devices .device { + cursor: pointer; width: 300px; position: absolute; height: 25px; @@ -796,6 +797,9 @@ -moz-flex-flow: row wrap; flex-flow: row wrap; } +.racks .rack .wrapper .devices .device .children .child { + cursor: pointer; +} .racks .rack .wrapper .devices .device .children [class*="slot-"] { width: 50px; box-sizing: border-box; diff --git a/src/ralph/ui/static/ui/css/rack.less b/src/ralph/ui/static/ui/css/rack.less index db3dd33cbc..e4283f1761 100644 --- a/src/ralph/ui/static/ui/css/rack.less +++ b/src/ralph/ui/static/ui/css/rack.less @@ -199,6 +199,7 @@ float: left; position: relative; .device { + cursor: pointer; width: @devices-width-back; position: absolute; height: @u-height; @@ -307,6 +308,9 @@ -webkit-flex-flow: row wrap; -moz-flex-flow: row wrap; flex-flow: row wrap; + .child { + cursor: pointer; + } [class*="slot-"] { width: @devices-width-front / 8; box-sizing: border-box; diff --git a/src/ralph/ui/static/ui/js/app/rack/controllers.js b/src/ralph/ui/static/ui/js/app/rack/controllers.js index e4a7e2bbb9..6131018e3e 100644 --- a/src/ralph/ui/static/ui/js/app/rack/controllers.js +++ b/src/ralph/ui/static/ui/js/app/rack/controllers.js @@ -19,6 +19,9 @@ angular $scope.rack = RackModel.get({rackId: rackId}); $scope.$on('change_active_item', function (event, item) { $scope.activeItem = item; + if (item.children.indexOf($scope.activeSlot) === -1) { + $scope.activeSlot = null; + } }); $scope.$on('change_active_slot', function (event, slot) { $scope.activeSlot = slot; diff --git a/src/ralph/ui/static/ui/js/app/rack/directives.js b/src/ralph/ui/static/ui/js/app/rack/directives.js index 4afac77cf1..d82ffa6695 100644 --- a/src/ralph/ui/static/ui/js/app/rack/directives.js +++ b/src/ralph/ui/static/ui/js/app/rack/directives.js @@ -32,6 +32,17 @@ angular return { restrict: 'E', templateUrl: '/static/partials/rack/listing.html', + link: function ($scope) { + $scope.u_range = []; + $scope.$on('change_active_item', function(event, item){ + $scope.u_range = []; + if (typeof item !== 'undefined' && item !== null) { + for (var i = item.position; i <= item.position+item.height-1; i++) { + $scope.u_range.push(i); + } + } + }); + } }; }) .directive('pduItem', function () { From 95e2238a8bd2825d534cbc0f6b587b8be9e55ad2 Mon Sep 17 00:00:00 2001 From: Andrzej Jankowski Date: Mon, 26 Jan 2015 11:40:19 +0100 Subject: [PATCH 141/189] New CiscoSSHHandler added to provide better support for Cisco switches. * New connector added - CiscoSSHHandler * Change ssh_cisco_catalyst_plugin to use new connetor * ssh_cisco_catalyst_plugin now does not send `terminal length` command - it's now handled by connector class --- src/ralph/scan/plugins/ssh_cisco_catalyst.py | 58 +----------------- .../tests/plugins/test_ssh_cisco_catalyst.py | 2 +- src/ralph/util/network.py | 60 ++++++++++++++++++- 3 files changed, 61 insertions(+), 59 deletions(-) diff --git a/src/ralph/scan/plugins/ssh_cisco_catalyst.py b/src/ralph/scan/plugins/ssh_cisco_catalyst.py index dcf2b7c41f..357359c5f4 100644 --- a/src/ralph/scan/plugins/ssh_cisco_catalyst.py +++ b/src/ralph/scan/plugins/ssh_cisco_catalyst.py @@ -6,10 +6,7 @@ from __future__ import print_function from __future__ import unicode_literals -import paramiko import re -import socket -import time from django.conf import settings from lck.django.common.models import MACAddressField @@ -17,9 +14,7 @@ from ralph.discovery.cisco import cisco_inventory from ralph.discovery.models import DeviceType, SERIAL_BLACKLIST from ralph.scan.errors import ( - AuthError, ConnectionError, - ConsoleError, NoMatchError, NotConfiguredError, ) @@ -31,60 +26,10 @@ SSH_USER, SSH_PASSWORD = SETTINGS['ssh_user'], SETTINGS['ssh_pass'] -class CiscoSSHClient(paramiko.SSHClient): - - """SSHClient modified for Cisco's broken SSH console.""" - - def __init__(self, *args, **kwargs): - super(CiscoSSHClient, self).__init__(*args, **kwargs) - self.set_log_channel('critical_only') - - def _auth(self, username, password, pkey, key_filenames, - allow_agent, look_for_keys): - self._transport.auth_password(username, password) - self._cisco_chan = self._transport.open_session() - self._cisco_chan.invoke_shell() - self._cisco_chan.sendall('\r\n') - self._cisco_chan.settimeout(15.0) - time.sleep(4) - try: - chunk = self._cisco_chan.recv(1024) - except socket.timeout: - raise AuthError('Authentication failed.') - else: - if not chunk.endswith(('#', '>')): - raise ConsoleError('Expected system prompt, got %r.' % chunk) - - def cisco_command(self, command): - # XXX Work around random characters appearing at the beginning of the - # command. - self._cisco_chan.sendall('\b') - time.sleep(0.125) - self._cisco_chan.sendall(command) - buffer = '' - end = command[-32:] - while not buffer.strip('\b ').endswith(end): - chunk = self._cisco_chan.recv(1024) - buffer += chunk - self._cisco_chan.sendall('\r\n') - buffer = [''] - while True: - chunk = self._cisco_chan.recv(1024) - lines = chunk.split('\n') - buffer[-1] += lines[0] - buffer.extend(lines[1:]) - if '% Invalid input' in buffer: - raise ConsoleError('Invalid input %r.' % buffer) - if buffer[-1].endswith(('#', '>')): - return buffer[1:-1] - - def _connect_ssh(ip, username, password): if not network.check_tcp_port(ip, 22): raise ConnectionError('Port 22 closed.') - return network.connect_ssh( - ip, username, password, client=CiscoSSHClient, - ) + return network.connect_cisco_ssh(ip, username, password) def get_subswitches(switch_version, hostname, ip_address): @@ -165,7 +110,6 @@ def scan_address(ip_address, **kwargs): ssh = _connect_ssh(ip_address, SSH_USER, SSH_PASSWORD) hostname = network.hostname(ip_address) try: - ssh.cisco_command('terminal length 500') mac = '\n'.join(ssh.cisco_command( "show version | include Base ethernet MAC Address", )) diff --git a/src/ralph/scan/tests/plugins/test_ssh_cisco_catalyst.py b/src/ralph/scan/tests/plugins/test_ssh_cisco_catalyst.py index 923e8dc8ad..573d8be2c8 100644 --- a/src/ralph/scan/tests/plugins/test_ssh_cisco_catalyst.py +++ b/src/ralph/scan/tests/plugins/test_ssh_cisco_catalyst.py @@ -221,7 +221,7 @@ def test_scan(self, connect_mock, network_mock): "show version | include Base ethernet MAC Address", ) command_mock.assert_any_call("show inventory") - self.assertEqual(command_mock.call_count, 4) + self.assertEqual(command_mock.call_count, 3) def test_get_subswitches(self): correct_ret = [ diff --git a/src/ralph/util/network.py b/src/ralph/util/network.py index 73f9796118..7debbdf83b 100644 --- a/src/ralph/util/network.py +++ b/src/ralph/util/network.py @@ -9,9 +9,11 @@ from __future__ import print_function from __future__ import unicode_literals +import logging import socket -import sys import StringIO +import sys +import time from dns.exception import DNSException from lck.cache import memoize @@ -118,6 +120,62 @@ def connect_ssh( return ssh +class CiscoSSHHandler(object): + + def __init__(self, ip, username=None, password=None): + self.ip = ip + self.username = username + self.password = password + + # setup logging + self.logger = logging.getLogger(__name__) + self.logger.info("Using username: {}".format(self.username)) + + # setup paramiko + self.ssh = paramiko.SSHClient() + self.ssh.load_system_host_keys() + self.ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + + def connect(self): + self.ssh.connect( + self.ip, username=self.username, password=self.password, + allow_agent=False, + ) + self.channel = self.ssh.invoke_shell() + + # disables output scrolling + self.channel.send("terminal length 0\n") + + time.sleep(1) + self.output = self.channel.recv(9999) + self.channel.send("\n") + + def cisco_command(self, command): + self.logger.info("Running command: {}".format(command)) + self.channel.send(command + "\n") + time.sleep(2) + buff = [''] + while True: + lines = self.channel.recv(9999).split("\n") + buff[-1] += lines[0] + buff.extend(lines[1:]) + if buff[-1].endswith(('#', '>')): + return buff[1:-1] + + def close(self): + self.logger.info("Close connection.") + self.ssh.close() + + +def connect_cisco_ssh(ip, username, password): + ssh = CiscoSSHHandler(ip, username, password) + try: + ssh.connect() + except (paramiko.AuthenticationException, EOFError) as e: + raise AuthError(str(e)) + return ssh + + def validate_ip(address): ip = ipaddr.IPAddress(address) if ip.is_unspecified or ip.is_loopback or ip.is_link_local: From 89e831a3339b29287f4760ebe96ceeda4cbdaeaf Mon Sep 17 00:00:00 2001 From: Arkadiusz Adamski Date: Fri, 30 Jan 2015 14:03:33 +0100 Subject: [PATCH 142/189] added management IP in 'Addresses' tab (device view) --- src/ralph/discovery/admin.py | 19 ++-------- src/ralph/discovery/tests/test_admin.py | 35 +++++++------------ src/ralph/ui/forms/addresses.py | 6 ++++ .../ui/templates/ui/device_addresses.html | 30 +++++++++------- .../ui/templates/ui/ip_with_host_widget.html | 2 +- src/ralph/ui/views/common.py | 19 +++++++++- 6 files changed, 59 insertions(+), 52 deletions(-) diff --git a/src/ralph/discovery/admin.py b/src/ralph/discovery/admin.py index d7702a9ba5..a1fbc4534a 100644 --- a/src/ralph/discovery/admin.py +++ b/src/ralph/discovery/admin.py @@ -12,8 +12,6 @@ from django import forms from django.conf import settings from django.contrib import admin -from django.core.urlresolvers import reverse -from django.utils.safestring import mark_safe from django.utils.translation import ugettext_lazy as _ from lck.django.common.admin import ( ForeignKeyAutocompleteTabularInline, @@ -428,20 +426,9 @@ def clean(self): 'is_management' in self.changed_data ): is_management = cleaned_data.get('is_management', False) - if is_management: - asset = device.get_asset() - if asset: - msg = """You can not assign management IP address to this - device. You should use Assets module - click here.""".format( - reverse( - 'device_edit', kwargs={ - 'mode': 'dc', - 'asset_id': asset.id, - }, - ), - ) - raise forms.ValidationError(mark_safe(msg)) + if is_management and device.management_ip: + msg = 'This device already has management IP.' + self._errors['device'] = self.error_class([msg]) return cleaned_data diff --git a/src/ralph/discovery/tests/test_admin.py b/src/ralph/discovery/tests/test_admin.py index 1140627e01..cab69d42d7 100644 --- a/src/ralph/discovery/tests/test_admin.py +++ b/src/ralph/discovery/tests/test_admin.py @@ -10,7 +10,7 @@ from ralph.discovery.tests.util import DeviceFactory from ralph.ui.tests.global_utils import login_as_su -from ralph.ui.widgets import ReadOnlyWidget, ReadOnlySelectWidget +from ralph.ui.widgets import ReadOnlyWidget from ralph_assets.tests.utils.assets import DCAssetFactory, DeviceInfoFactory @@ -83,39 +83,30 @@ class IPAddressAdminTest(TestCase): def setUp(self): self.client = login_as_su() self.device = DeviceFactory() - self.url = '/admin/discovery/ipaddress/add/' self.url = reverse('admin:discovery_ipaddress_add') - def _make_request(self): + def _make_request(self, ip): return self.client.post( self.url, { '-1-TOTAL_FORMS': 0, '-1-INITIAL_FORMS': 0, '-1-MAX_NUM_FORMS': 0, - 'address': '127.0.0.1', + 'address': ip, 'device': self.device.id, 'is_management': True, + 'dead_ping_count': 0, + 'last_seen_0': '2014-12-24', + 'last_seen_1': '16:36', }, follow=True, ) - def test_device_without_asset_validation(self): - response = self._make_request() - self.assertFalse(response.context_data['adminform'].form.is_valid()) - self.assertEqual( - response.context_data['adminform'].form.non_field_errors(), [], - ) + def test_add_more_than_one_management_ip(self): + self.device.management_ip = '127.0.0.1' + response = self._make_request(ip='192.168.1.1') + form = response.context_data['adminform'].form - def test_device_with_asset_validation(self): - asset = DCAssetFactory( - device_info=DeviceInfoFactory( - ralph_device_id=self.device.id, - ), - ) - asset.model.category.is_blade = False - asset.model.category.save() - response = self._make_request() - self.assertFalse(response.context_data['adminform'].form.is_valid()) - msg = response.context_data['adminform'].form.non_field_errors()[0] - self.assertTrue('You can not assign management IP address' in msg) + self.assertFalse(form.is_valid()) + self.assertTrue('device' in form.errors) + self.assertEqual(self.device.ipaddress_set.all().count(), 1) diff --git a/src/ralph/ui/forms/addresses.py b/src/ralph/ui/forms/addresses.py index 04852eabbb..9b9431a508 100644 --- a/src/ralph/ui/forms/addresses.py +++ b/src/ralph/ui/forms/addresses.py @@ -389,3 +389,9 @@ def __init__(self, *args, **kwargs): def compress(self, value): return value + + +class IPManagementForm(forms.Form): + management_ip = IPWithHostField( + label=_('Management IP'), required=False + ) diff --git a/src/ralph/ui/templates/ui/device_addresses.html b/src/ralph/ui/templates/ui/device_addresses.html index 3ba3095b78..9c64828916 100644 --- a/src/ralph/ui/templates/ui/device_addresses.html +++ b/src/ralph/ui/templates/ui/device_addresses.html @@ -161,18 +161,6 @@

Detected addresses

{% endfor %} - {% if device.find_management %} - - - - - - - -
Server Management Address{{ device.find_management|address_icon }} - {{ device.find_management }}
- {% endif %}
{% if next_hostname %}
Next available hostname in this DC: {{ next_hostname }}
@@ -191,6 +179,24 @@

Detected addresses

{% endif %} + {% if device.find_management %} +

Management address

+
+ {% csrf_token %} + + {{ ip_management_form }} + + {% if canedit %} +
+ {% spaceless %} + + {% endspaceless %} +
+ {% endif %} +
+ {% endif %} + {% if balancers %}

Load balancers

diff --git a/src/ralph/ui/templates/ui/ip_with_host_widget.html b/src/ralph/ui/templates/ui/ip_with_host_widget.html index eb08943f1d..17a7464df7 100644 --- a/src/ralph/ui/templates/ui/ip_with_host_widget.html +++ b/src/ralph/ui/templates/ui/ip_with_host_widget.html @@ -1,5 +1,5 @@ {% load i18n %}
- +
{% trans "hostname" %}{{ widgets.0 }}
{% trans "Hostname" %}{{ widgets.0 }}
{% trans "IP address" %}{{ widgets.1 }}
diff --git a/src/ralph/ui/views/common.py b/src/ralph/ui/views/common.py index c738628f25..a2e67a10e0 100644 --- a/src/ralph/ui/views/common.py +++ b/src/ralph/ui/views/common.py @@ -92,8 +92,9 @@ ) from ralph.ui.forms.addresses import ( DHCPFormSet, - IPAddressFormSet, DNSFormSet, + IPAddressFormSet, + IPManagementForm, ) from ralph.ui.forms.deployment import ( ServerMoveStep1Form, @@ -738,6 +739,7 @@ def __init__(self, *args, **kwargs): self.dns_formset = None self.dhcp_formset = None self.ip_formset = None + self.ip_management_form = None def get_dns(self, limit_types=None): ips = set(ip.address for ip in self.object.ipaddress_set.all()) @@ -959,6 +961,14 @@ def post(self, *args, **kwargs): return HttpResponseRedirect(self.request.path) else: messages.error(self.request, "Errors in the addresses form.") + elif 'management' in self.request.POST: + form = IPManagementForm(self.request.POST) + if form.is_valid(): + self.object.management_ip = form.cleaned_data['management_ip'] + self.object.save() + messages.success( + self.request, "Management IP address updated." + ) return self.get(*args, **kwargs) def get_dhcp(self): @@ -995,6 +1005,12 @@ def get_context_data(self, **kwargs): ).order_by('address'), prefix='ip' ) + if not self.ip_management_form: + self.ip_management_form = IPManagementForm( + initial={ + 'management_ip': self.object.management_ip.as_tuple() + } + ) profile = self.request.user.get_profile() can_edit = profile.has_perm(self.edit_perm, self.object.venture) next_hostname = None @@ -1024,6 +1040,7 @@ def get_context_data(self, **kwargs): 'ipformset': self.ip_formset, 'next_hostname': next_hostname, 'first_free_ip_addresses': first_free_ip_addresses, + 'ip_management_form': self.ip_management_form }) return ret From c4d32ccd512f57af1328ece8ef16a9e53af7d9ce Mon Sep 17 00:00:00 2001 From: Arkadiusz Adamski Date: Fri, 30 Jan 2015 15:23:37 +0100 Subject: [PATCH 143/189] added department to search form For better searching. --- src/ralph/ui/forms/search.py | 4 ++++ src/ralph/ui/views/search.py | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/src/ralph/ui/forms/search.py b/src/ralph/ui/forms/search.py index d8d990f3ef..d2ee99caaf 100644 --- a/src/ralph/ui/forms/search.py +++ b/src/ralph/ui/forms/search.py @@ -93,6 +93,10 @@ class SearchForm(forms.Form): 'class': 'span12', 'title': TooltipContent.empty_field, })) + department = forms.CharField( + required=False, widget=forms.TextInput(attrs={ + 'class': 'span12', + })) position = forms.CharField(required=False, widget=forms.TextInput(attrs={ 'class': 'span12', diff --git a/src/ralph/ui/views/search.py b/src/ralph/ui/views/search.py index 927cc0f0e4..764e61498b 100644 --- a/src/ralph/ui/views/search.py +++ b/src/ralph/ui/views/search.py @@ -439,6 +439,10 @@ def get_queryset(self): 'ralph_device_id', flat=True ) self.query = self.query.exclude(id__in=device_info_ids) + if data['department']: + self.query = self.query.filter( + venture__department__name__icontains=data['department'] + ) profile = self.request.user.get_profile() if not profile.has_perm(Perm.read_dc_structure): self.query = profile.filter_by_perm( From bd33649c6f22f3fe75199c1b49a25a883b176dc5 Mon Sep 17 00:00:00 2001 From: Marcin Kliks Date: Mon, 2 Feb 2015 11:55:38 +0100 Subject: [PATCH 144/189] Update installation.rst --- doc/installation.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/installation.rst b/doc/installation.rst index dfc41da726..b03aad6007 100644 --- a/doc/installation.rst +++ b/doc/installation.rst @@ -2,8 +2,8 @@ Install / Upgrade Ralph ======================= -Prebuilt docker image -===================== +Prebuilt docker image - recommeneded option +=========================================== It is the easiest way to try out Ralph using pre-built docker image with the worker, database, and server all together. Keep in mind, that @@ -82,8 +82,8 @@ properly. In order to enable them in your settings, follow the instructions in the :doc:`change log ` for the version you installed. -Installing Ralph -================= +Installing Ralph - advanced installation +======================================== .. note:: From 73cca2920b49df2c77ae88d2045f0d9f59f1d551 Mon Sep 17 00:00:00 2001 From: Marcin Kliks Date: Mon, 2 Feb 2015 12:42:59 +0100 Subject: [PATCH 145/189] Updated documentation. --- doc/installation.rst | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/doc/installation.rst b/doc/installation.rst index b03aad6007..750ec82587 100644 --- a/doc/installation.rst +++ b/doc/installation.rst @@ -5,10 +5,8 @@ Install / Upgrade Ralph Prebuilt docker image - recommeneded option =========================================== -It is the easiest way to try out Ralph using pre-built docker image -with the worker, database, and server all together. Keep in mind, that -for now we provide only test cutting-edge releases, not the stable -ones. +It is the easiest way to try out Ralph using pre-built docker image with the worker, database, and server all together. +We decided to push new images from time to time when we decide it's stable enough to use. 1. Install docker using instructions at https://docs.docker.com/installation/ 2. Create volume data for mysql data and configuration:: From f7a60e9605703821f96ba2529fa32db8df7f681b Mon Sep 17 00:00:00 2001 From: Arkadiusz Adamski Date: Mon, 2 Feb 2015 15:18:42 +0100 Subject: [PATCH 146/189] fixed 500 error when managment IP is blank --- src/ralph/ui/views/common.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/ralph/ui/views/common.py b/src/ralph/ui/views/common.py index a2e67a10e0..19eaecedc9 100644 --- a/src/ralph/ui/views/common.py +++ b/src/ralph/ui/views/common.py @@ -1006,9 +1006,12 @@ def get_context_data(self, **kwargs): prefix='ip' ) if not self.ip_management_form: + management_ip = self.object.management_ip self.ip_management_form = IPManagementForm( initial={ - 'management_ip': self.object.management_ip.as_tuple() + 'management_ip': ( + management_ip and management_ip.as_tuple() + ) } ) profile = self.request.user.get_profile() From 128423afc3f1c1d752f4b4a067f6182cbba32ec4 Mon Sep 17 00:00:00 2001 From: Arkadiusz Adamski Date: Mon, 2 Feb 2015 17:00:04 +0100 Subject: [PATCH 147/189] removed duplicated UserFactory The CMDB use own UserFactory and UserFactory from /ui/tests/global_utils.py. For this reason each factory have own sequence. --- src/ralph/ui/tests/global_utils.py | 6 +++--- src/ralph/util/tests/utils.py | 9 +-------- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/src/ralph/ui/tests/global_utils.py b/src/ralph/ui/tests/global_utils.py index 1d386bf1f9..773e234458 100644 --- a/src/ralph/ui/tests/global_utils.py +++ b/src/ralph/ui/tests/global_utils.py @@ -46,10 +46,10 @@ class UserTestCase(TestCase): def setUp(self): self.user = UserFactory( - is_staff = self.is_staff, - is_superuser = self.is_superuser, + is_staff=self.is_staff, + is_superuser=self.is_superuser, ) - + self.headers = { 'HTTP_ACCEPT': 'application/json', 'HTTP_AUTHORIZATION': 'ApiKey {}:{}'.format( diff --git a/src/ralph/util/tests/utils.py b/src/ralph/util/tests/utils.py index 7e4d559dc6..e35999f35c 100644 --- a/src/ralph/util/tests/utils.py +++ b/src/ralph/util/tests/utils.py @@ -8,11 +8,11 @@ import factory from factory.django import DjangoModelFactory -from django.contrib.auth.models import User from ralph.account.models import Region from ralph.cmdb import models_ci from ralph.cmdb.tests.utils import CIFactory +from ralph.ui.tests.global_utils import UserFactory class AttributeDict(dict): @@ -44,13 +44,6 @@ def type(self): return models_ci.CIType.objects.get(name='Environment') -class UserFactory(DjangoModelFactory): - FACTORY_FOR = User - username = factory.Sequence(lambda n: 'user_{0}'.format(n)) - first_name = factory.Sequence(lambda n: 'John {0}'.format(n)) - last_name = factory.Sequence(lambda n: 'Snow {0}'.format(n)) - - @factory.sequence def get_profile(n): """Due to strange logic in lck.django we can't use subfactories to create From 24311111bb163bf6e8cc223a8933fd54b1a5e617 Mon Sep 17 00:00:00 2001 From: Andrzej Jankowski Date: Tue, 3 Feb 2015 07:51:50 +0100 Subject: [PATCH 148/189] Fixed ReadOnlyWidget and ReadOnlySelectWidget. Widgets shouldn't paste "None" string as a value when value is None - empty string is much better. Previous behavior produce many validation errors. E.g. IntegerField validation said that it's not a number - that's right because "None" it is not a number :) --- src/ralph/ui/widgets.py | 37 ++++++++++++++++++++++++------------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/src/ralph/ui/widgets.py b/src/ralph/ui/widgets.py index de095940a9..20ba5715da 100644 --- a/src/ralph/ui/widgets.py +++ b/src/ralph/ui/widgets.py @@ -25,15 +25,17 @@ def render(self, name, value, attrs=None, choices=()): display = unicode(labels.get(value, '')) attr_class = self.attrs.get('class', '') return mark_safe( - '
' - '' - '%s' - '
' % ( - escape(attr_class), - escape(name), - escape(value), - escape(display), - ), + """ +
+ + {display} +
+ """.format( + classes=escape(attr_class), + name=escape(name), + value=escape(value) if value is not None else '', + display=escape(display), + ) ) @@ -78,10 +80,19 @@ class ReadOnlyWidget(forms.Widget): def render(self, name, value, attrs=None, choices=()): attr_class = escape(self.attrs.get('class', '')) - return mark_safe(''' - -
%s
''' % ( - escape(name), escape(value), attr_class, escape(value))) + return mark_safe( + """ + +
+ {value} +
+ + """.format( + name=escape(name), + value=escape(value) if value is not None else "", + attributes=attr_class, + ) + ) class ReadOnlyPreWidget(forms.Widget): From 6e93854ad8f6ab3d9dde617c77eb7f05a913639f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Py=C5=BCalski?= Date: Tue, 3 Feb 2015 09:03:50 +0100 Subject: [PATCH 149/189] Corrections for scan confirmation * Added model_type to columns in subdevices * Subdevice creation is done with priority=215 * Handled asset=None situation --- src/ralph/scan/data.py | 3 ++- src/ralph/scan/forms.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/ralph/scan/data.py b/src/ralph/scan/data.py index 51f7abd83f..52afccbeda 100644 --- a/src/ralph/scan/data.py +++ b/src/ralph/scan/data.py @@ -847,7 +847,7 @@ def set_device_data(device, data, save_priority=SAVE_PRIORITY, warnings=[]): if 'asset' in data and 'ralph_assets' in settings.INSTALLED_APPS: from ralph_assets.api_ralph import assign_asset asset = data['asset'] - if not isinstance(asset, Asset): + if asset and not isinstance(asset, Asset): asset = get_asset_by_name(asset) if asset: assign_asset(device.id, asset.id) @@ -919,6 +919,7 @@ def device_from_data( ethernets=ethernets, model_name=model_name, model_type=model_type, + priority=save_priority, ) set_device_data( device, data, save_priority=save_priority, warnings=warnings diff --git a/src/ralph/scan/forms.py b/src/ralph/scan/forms.py index 303dc0fa37..e15603228c 100644 --- a/src/ralph/scan/forms.py +++ b/src/ralph/scan/forms.py @@ -338,7 +338,7 @@ class DiffForm(forms.Form): 'diag_firmware', 'mgmt_firmware', ]), - 'subdevices': CSVInfo(['hostname', 'serial_number', 'id']), + 'subdevices': CSVInfo(['hostname', 'serial_number', 'id', 'model_name']), 'connections': CSVInfo([ 'connection_type', 'connected_device_mac_addresses', From 684bc16be53f33c32428add70a7c6b5750474fb0 Mon Sep 17 00:00:00 2001 From: Arkadiusz Adamski Date: Tue, 3 Feb 2015 09:03:48 +0100 Subject: [PATCH 150/189] management IP form always visible Changes: * removed if statement from template * form has moved to top --- .../ui/templates/ui/device_addresses.html | 34 +++++++++---------- 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/src/ralph/ui/templates/ui/device_addresses.html b/src/ralph/ui/templates/ui/device_addresses.html index 9c64828916..a3894a5526 100644 --- a/src/ralph/ui/templates/ui/device_addresses.html +++ b/src/ralph/ui/templates/ui/device_addresses.html @@ -110,6 +110,22 @@

DHCP Entries

+

Management address

+
+ {% csrf_token %} + + {{ ip_management_form }} + + {% if canedit %} +
+ {% spaceless %} + + {% endspaceless %} +
+ {% endif %} +
+

Detected addresses

{% csrf_token %} @@ -179,24 +195,6 @@

Detected addresses

{% endif %}
- {% if device.find_management %} -

Management address

-
- {% csrf_token %} - - {{ ip_management_form }} - - {% if canedit %} -
- {% spaceless %} - - {% endspaceless %} -
- {% endif %} -
- {% endif %} - {% if balancers %}

Load balancers

From 1848c31dad6fbb2179c79390abc065b3fd81a2ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Py=C5=BCalski?= Date: Wed, 4 Feb 2015 08:37:47 +0100 Subject: [PATCH 151/189] Added two missing features to deploy/clean plugin * The hostname is now copied from deployment * Also copying venture/role from deployment * Factories for venture/role moved to utils --- src/ralph/deployment/plugins/clean.py | 5 ++++- src/ralph/deployment/tests/plugins/test_clean.py | 10 ++++++++-- .../ui/tests/functional/tests_bulkedit_form.py | 13 +------------ src/ralph/util/tests/utils.py | 12 ++++++++++++ 4 files changed, 25 insertions(+), 15 deletions(-) diff --git a/src/ralph/deployment/plugins/clean.py b/src/ralph/deployment/plugins/clean.py index e3efc540b0..c58f8bc9f1 100644 --- a/src/ralph/deployment/plugins/clean.py +++ b/src/ralph/deployment/plugins/clean.py @@ -70,7 +70,6 @@ def do_clean(dev, user): datetime.date.today().strftime('%Y-%m-%d'), ) dev.remarks = remark + dev.remarks - dev.save() @plugin.register(chain='deployment', requires=[], priority=1000) @@ -80,6 +79,10 @@ def clean(deployment_id): if deployment.status != DeploymentStatus.open: return True do_clean(deployment.device, deployment.user) + deployment.device.name = deployment.hostname + deployment.device.venture = deployment.venture + deployment.device.venture_role = deployment.venture_role + deployment.device.save() ip, created = IPAddress.concurrent_get_or_create(address=deployment.ip) ip.device = deployment.device ip.hostname = deployment.hostname diff --git a/src/ralph/deployment/tests/plugins/test_clean.py b/src/ralph/deployment/tests/plugins/test_clean.py index 93579216b1..ffc55f02bc 100644 --- a/src/ralph/deployment/tests/plugins/test_clean.py +++ b/src/ralph/deployment/tests/plugins/test_clean.py @@ -22,16 +22,19 @@ ) from ralph.deployment.models import Deployment from ralph.deployment.plugins.clean import clean +from ralph.util.tests.utils import VentureRoleFactory class CleanPluginTest(TestCase): def setUp(self): + self.venture_role = VentureRoleFactory() device = Device.create( ethernets=[('', 'deadbeefcafe', 0)], model_name='HAL 9000', model_type=DeviceType.unknown, remarks="I'm sorry, Dave.", + name='discovery.two', ) self.deployment = Deployment( hostname='discovery.one', @@ -39,8 +42,8 @@ def setUp(self): mac='deadbeefcafe', device=device, preboot=None, - venture=None, - venture_role=None, + venture=self.venture_role.venture, + venture_role=self.venture_role, ) self.deployment.save() self.ip = IPAddress(address='127.0.0.1', device=device) @@ -66,6 +69,9 @@ def test_clean_plugin(self): ) self.assertEquals(device.ipaddress_set.count(), 1) self.assertEquals(device.ipaddress_set.all()[0].address, '127.0.0.1') + self.assertEquals(device.name, 'discovery.one') + self.assertEquals(device.venture, self.venture_role.venture) + self.assertEquals(device.venture_role, self.venture_role) self.assertFalse(device.diskshare_set.exists()) self.assertFalse(device.disksharemount_set.exists()) self.assertFalse(device.software_set.exists()) diff --git a/src/ralph/ui/tests/functional/tests_bulkedit_form.py b/src/ralph/ui/tests/functional/tests_bulkedit_form.py index 0ab0c2802a..112b98ce52 100644 --- a/src/ralph/ui/tests/functional/tests_bulkedit_form.py +++ b/src/ralph/ui/tests/functional/tests_bulkedit_form.py @@ -9,21 +9,10 @@ from factory import Sequence, SubFactory from factory.django import DjangoModelFactory -from ralph.business.models import Venture, VentureRole from ralph.discovery.models_device import Device from ralph.discovery.models_history import HistoryChange from ralph.ui.tests.global_utils import login_as_su - - -class VentureFactory(DjangoModelFactory): - FACTORY_FOR = Venture - name = Sequence(lambda n: 'Venture #{}'.format(n)) - - -class VentureRoleFactory(DjangoModelFactory): - FACTORY_FOR = VentureRole - name = Sequence(lambda n: 'Venture role #{}'.format(n)) - venture = SubFactory(VentureFactory) +from ralph.util.tests.utils import VentureFactory, VentureRoleFactory class ParentDeviceFactory(DjangoModelFactory): diff --git a/src/ralph/util/tests/utils.py b/src/ralph/util/tests/utils.py index e35999f35c..85d5f759c5 100644 --- a/src/ralph/util/tests/utils.py +++ b/src/ralph/util/tests/utils.py @@ -10,6 +10,7 @@ from factory.django import DjangoModelFactory from ralph.account.models import Region +from ralph.business.models import Venture, VentureRole from ralph.cmdb import models_ci from ralph.cmdb.tests.utils import CIFactory from ralph.ui.tests.global_utils import UserFactory @@ -83,3 +84,14 @@ class ServiceProfitCenterRelationFactory(DjangoModelFactory): class RegionFactory(DjangoModelFactory): FACTORY_FOR = Region name = factory.Sequence(lambda n: 'Region #{}'.format(n)) + + +class VentureFactory(DjangoModelFactory): + FACTORY_FOR = Venture + name = factory.Sequence(lambda n: 'Venture #{}'.format(n)) + + +class VentureRoleFactory(DjangoModelFactory): + FACTORY_FOR = VentureRole + name = factory.Sequence(lambda n: 'Venture role #{}'.format(n)) + venture = factory.SubFactory(VentureFactory) From ccf619ec534e2977682d3c1c1073b15a1e880e87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Py=C5=BCalski?= Date: Wed, 4 Feb 2015 10:59:21 +0100 Subject: [PATCH 152/189] Improved Xen detection The Xen servers > 6.5 don't contain 'xen' string in their snmp_name. We need to detect them using their HTTP response (looking for 'XenServer' string in the returned HTML document). Also - renamed the existing Dell detection test, so new tests can be added. --- src/ralph/discovery/http.py | 2 ++ src/ralph/discovery/tests/test_http.py | 18 +++++++++++++++++- src/ralph/scan/plugins/ssh_xen.py | 3 ++- 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/src/ralph/discovery/http.py b/src/ralph/discovery/http.py index c0a8f95f3e..a3e226ca5e 100644 --- a/src/ralph/discovery/http.py +++ b/src/ralph/discovery/http.py @@ -122,6 +122,8 @@ def guess_family(headers, document): family = 'ESX' elif 'ATEN International Co Ltd.' in document: family = 'Thomas-Krenn' + elif 'XenServer' in document: + family = 'Xen' elif family in ('lighttpd',): if 'Modular Server Control' in document: family = 'Modular' diff --git a/src/ralph/discovery/tests/test_http.py b/src/ralph/discovery/tests/test_http.py index 0b3f3129b8..1acfc96812 100644 --- a/src/ralph/discovery/tests/test_http.py +++ b/src/ralph/discovery/tests/test_http.py @@ -13,7 +13,23 @@ class DiscoveryHttpTest(TestCase): - def test_guess_family(self): + def test_guess_family_dell_when_data_from_dell_server(self): headers = {} family = guess_family(headers, DATA) self.assertEqual(family, 'Dell') + + def test_guess_family_xen_when_data_from_xen(self): + headers = {} + family = guess_family(headers, """ + + XenServer 6.5.0 + + + +

Citrix Systems, Inc. XenServer 6.5.0 +

XenCenter CD image +

XenCenter installer + + """ + ) + self.assertEqual(family, 'Xen') diff --git a/src/ralph/scan/plugins/ssh_xen.py b/src/ralph/scan/plugins/ssh_xen.py index 712b17f880..1f7b274a6d 100644 --- a/src/ralph/scan/plugins/ssh_xen.py +++ b/src/ralph/scan/plugins/ssh_xen.py @@ -253,9 +253,10 @@ def _ssh_xen(ssh, ip_address): def scan_address(ip_address, **kwargs): snmp_name = kwargs.get('snmp_name', '') or '' + http_family = kwargs.get('http_family', '') or '' if 'nx-os' in snmp_name.lower(): raise NoMatchError("Incompatible Nexus found.") - if 'xen' not in snmp_name: + if 'xen' not in snmp_name and 'xen' not in http_family.lower(): raise NoMatchError("XEN not found.") auths = SETTINGS.get('xen_auths') messages = [] From ff7cf19277ce3c64fff35b4657f38036f54245aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Py=C5=BCalski?= Date: Wed, 4 Feb 2015 13:35:46 +0100 Subject: [PATCH 153/189] Moved the settings in settings.py Some settings must be put before pluggable apps initialization. If not, then the plugins that use ralph will crash trying to import things from ralph. --- src/ralph/settings.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/ralph/settings.py b/src/ralph/settings.py index 5e71b258be..2d384bfce1 100644 --- a/src/ralph/settings.py +++ b/src/ralph/settings.py @@ -410,6 +410,14 @@ execfile(cfg_loc) break +# url to page where user requests permission to module (eg. assets) +# REQUEST_PERM_URL = 'http://tickets.office/request/ralph_module/permission' +DEFAULT_REGION_NAME = 'Default region' + +# a list of object's fields (e.g. Asset, Device) for which notification of +# changed value should be send (see ralph.util.models.SyncFieldMixin) +SYNC_FIELD_MIXIN_NOTIFICATIONS_WHITELIST = ['service', 'device_environment'] + import pluggableapp pluggableapp.initialize(locals()) @@ -780,10 +788,3 @@ from django.core.urlresolvers import reverse_lazy LOGIN_REDIRECT_URL = reverse_lazy('find_user_home') -# url to page where user requests permission to module (eg. assets) -# REQUEST_PERM_URL = 'http://tickets.office/request/ralph_module/permission' -DEFAULT_REGION_NAME = 'Default region' - -# a list of object's fields (e.g. Asset, Device) for which notification of -# changed value should be send (see ralph.util.models.SyncFieldMixin) -SYNC_FIELD_MIXIN_NOTIFICATIONS_WHITELIST = ['service', 'device_environment'] From 83a3d8e8d9a4b3d34686d10970fbce14bc64ebfb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Py=C5=BCalski?= Date: Tue, 3 Feb 2015 12:47:15 +0100 Subject: [PATCH 154/189] Detaching logical subdevices not found by scan Previously only physical subdevices not found by scan were detached. Extended this feature on logical children --- src/ralph/scan/data.py | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/src/ralph/scan/data.py b/src/ralph/scan/data.py index 51f7abd83f..3d7c8e6cbc 100644 --- a/src/ralph/scan/data.py +++ b/src/ralph/scan/data.py @@ -58,6 +58,12 @@ SAVE_PRIORITY = 215 +def has_logical_children(device): + """Returns True if this device has logical children or False if it has + physical ones.""" + return device.model and device.model.type in (DeviceType.switch_stack,) + + def get_choice_by_name(choices, name): """ Find choices by name (with spaces or without spaces) or by raw_name. @@ -388,7 +394,7 @@ def get_device_data(device): ).raw if part.model else '', } for part in device.genericcomponent_set.order_by('sn') ] - if device.model and device.model.type in (DeviceType.switch_stack,): + if has_logical_children(device): data['subdevices'] = [ get_device_data(dev) for dev in device.logicalchild_set.order_by('id') @@ -806,10 +812,7 @@ def set_device_data(device, data, save_priority=SAVE_PRIORITY, warnings=[]): save_priority=save_priority, warnings=warnings ) - if ( - device.model and - device.model.type in (DeviceType.switch_stack,) - ): + if has_logical_children(device): subdevice.logical_parent = device if subdevice.parent and subdevice.parent.id == device.id: subdevice.parent = None @@ -817,8 +820,13 @@ def set_device_data(device, data, save_priority=SAVE_PRIORITY, warnings=[]): subdevice.parent = device subdevice.save(priority=save_priority) subdevice_ids.append(subdevice.id) - for subdevice in device.child_set.exclude(id__in=subdevice_ids): - subdevice.parent = None + set_, parent_attr = ( + (device.logicalchild_set, 'logical_parent') + if has_logical_children(device) + else (device.child_set, 'parent') + ) + for subdevice in set_.exclude(id__in=subdevice_ids): + setattr(subdevice, parent_attr, None) subdevice.save(priority=save_priority) if 'connections' in data: parsed_connections = set() @@ -847,7 +855,7 @@ def set_device_data(device, data, save_priority=SAVE_PRIORITY, warnings=[]): if 'asset' in data and 'ralph_assets' in settings.INSTALLED_APPS: from ralph_assets.api_ralph import assign_asset asset = data['asset'] - if not isinstance(asset, Asset): + if asset and not isinstance(asset, Asset): asset = get_asset_by_name(asset) if asset: assign_asset(device.id, asset.id) From 899b8ddff926b3e228c0efc3c8f68157080882e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Py=C5=BCalski?= Date: Fri, 6 Feb 2015 09:15:45 +0100 Subject: [PATCH 155/189] Allowed ralph plugins without menu entry and urls Some apps might not need an entry in menu or their own views (and hence their own urls.py). This fix will cause them not to crash. --- src/ralph/app.py | 7 ++++--- src/ralph/ui/views/common.py | 3 ++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/ralph/app.py b/src/ralph/app.py index 30e767f3eb..bf9793caf3 100644 --- a/src/ralph/app.py +++ b/src/ralph/app.py @@ -18,9 +18,8 @@ def api_resources(self, version): of API.""" return [] - @abc.abstractproperty - def url_prefix(self): - """The first part of paths for this application.""" + url_prefix = None + has_menu = True @abc.abstractproperty def module_name(self): @@ -40,6 +39,8 @@ def home_url(self): def __init__(self, *args, **kwargs): super(RalphModule, self).__init__(*args, **kwargs) + if not self.url_prefix: + return self.register_pattern( '', r'^{}/'.format(self.url_prefix), diff --git a/src/ralph/ui/views/common.py b/src/ralph/ui/views/common.py index 19eaecedc9..c91eb9eba1 100644 --- a/src/ralph/ui/views/common.py +++ b/src/ralph/ui/views/common.py @@ -232,7 +232,8 @@ def dispatch(self, request, *args, **kwargs): for app in pluggableapp.app_dict.values(): if ( not isinstance(app, RalphModule) or - app.module_name in settings.HIDE_MENU + app.module_name in settings.HIDE_MENU or + not app.has_menu ): continue menu_module = importlib.import_module( From b0bb9eb5a3a60535aa3d0127d63c0fd6219ecd5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Py=C5=BCalski?= Date: Mon, 9 Feb 2015 08:10:54 +0100 Subject: [PATCH 156/189] Moved property setting to a separate function We want to set device properties from other places than just save() of the device form. Thus we move the logic to a separate function. Also: corrected errors given by a new version of pep8 --- src/ralph/cmdb/api.py | 2 +- src/ralph/cmdb/importer.py | 6 +++-- src/ralph/cmdb/integration/lib/jira.py | 4 +++- src/ralph/discovery/models_device.py | 24 +++++++++++++++++++ src/ralph/discovery/tests/test_models.py | 30 +++++++++++++++++++++++- src/ralph/ui/views/common.py | 24 +------------------ src/ralph/ui/views/racks.py | 10 +++++--- src/ralph/util/models.py | 2 +- src/ralph/util/tests/utils.py | 15 +++++++++++- 9 files changed, 84 insertions(+), 33 deletions(-) diff --git a/src/ralph/cmdb/api.py b/src/ralph/cmdb/api.py index e643b283a2..8411e8d6fd 100644 --- a/src/ralph/cmdb/api.py +++ b/src/ralph/cmdb/api.py @@ -17,7 +17,6 @@ from ralph.cmdb.monkey import method_check import tastypie from tastypie.resources import Resource -Resource.method_check = method_check from django.conf import settings from django.contrib.auth.models import User @@ -51,6 +50,7 @@ from ralph.cmdb.models_ci import CIOwner, CIOwnershipType from ralph.cmdb.util import breadth_first_search_ci, can_change_ci_state +Resource.method_check = method_check THROTTLE_AT = settings.API_THROTTLING['throttle_at'] TIMEFRAME = settings.API_THROTTLING['timeframe'] diff --git a/src/ralph/cmdb/importer.py b/src/ralph/cmdb/importer.py index 37bd1e403d..fdaf083f93 100644 --- a/src/ralph/cmdb/importer.py +++ b/src/ralph/cmdb/importer.py @@ -42,10 +42,8 @@ from __future__ import unicode_literals import os -os.environ['DJANGO_SETTINGS_MODULE'] = "ralph.settings" import logging -logger = logging.getLogger(__name__) from django.contrib.contenttypes.models import ContentType @@ -57,6 +55,10 @@ from lck.django.common import nested_commit_on_success +os.environ['DJANGO_SETTINGS_MODULE'] = "ralph.settings" +logger = logging.getLogger(__name__) + + def get_layers_for_ci_type(ci_type_id): try: ci_type = cdb.CIType.objects.get(pk=ci_type_id) diff --git a/src/ralph/cmdb/integration/lib/jira.py b/src/ralph/cmdb/integration/lib/jira.py index 474ec37fcd..a91dfed1e0 100644 --- a/src/ralph/cmdb/integration/lib/jira.py +++ b/src/ralph/cmdb/integration/lib/jira.py @@ -6,11 +6,13 @@ from django.utils import simplejson as json import logging -logger = logging.getLogger(__name__) from ralph.cmdb.integration.exceptions import IssueTrackerException +logger = logging.getLogger(__name__) + + class Jira(object): def __init__(self): diff --git a/src/ralph/discovery/models_device.py b/src/ralph/discovery/models_device.py index 73c824ed26..747265e428 100644 --- a/src/ralph/discovery/models_device.py +++ b/src/ralph/discovery/models_device.py @@ -925,6 +925,30 @@ def save(self, sync_fields=True, *args, **kwargs): # the reason (for more, see the source code of SavePrioritized). return super(Device, self).save(*args, **kwargs) + def set_property(self, symbol, value, user): + from ralph.business.models import RoleProperty, RolePropertyValue + try: + p = self.venture_role.roleproperty_set.get(symbol=symbol) + except RoleProperty.DoesNotExist: + p = self.venture.roleproperty_set.get(symbol=symbol) + if value != p.default and not {value, p.default} == {None, ''}: + pv, created = RolePropertyValue.concurrent_get_or_create( + property=p, + device=self, + ) + pv.value = value + pv.save(user=user) + else: + try: + pv = RolePropertyValue.objects.get( + property=p, + device=self, + ) + except RolePropertyValue.DoesNotExist: + pass + else: + pv.delete() + def get_asset(self): asset = None if self.id and 'ralph_assets' in settings.INSTALLED_APPS: diff --git a/src/ralph/discovery/tests/test_models.py b/src/ralph/discovery/tests/test_models.py index 67ab324a46..b89bfc6be2 100644 --- a/src/ralph/discovery/tests/test_models.py +++ b/src/ralph/discovery/tests/test_models.py @@ -19,8 +19,12 @@ from ralph.discovery.tests.util import DeviceFactory, IPAddressFactory from ralph.discovery.models import DeviceType, Device, UptimeSupport from ralph.discovery.models_history import HistoryChange +from ralph.ui.tests.global_utils import UserFactory from ralph.util.models import fields_synced_signal, ChangeTuple -from ralph.util.tests.utils import UserFactory +from ralph.util.tests.utils import ( + UserFactory, RolePropertyTypeFactory, RolePropertyFactory, + VentureRoleFactory, +) from ralph_assets.models import Orientation from ralph_assets.tests.utils.assets import DCAssetFactory, DeviceInfoFactory @@ -184,3 +188,27 @@ def test_orientation_property(self): self.assertEqual(dev_2.orientation, 'middle') with self.assertRaises(AttributeError): dev_2.orientation = Orientation.back.id + + +class DevicePropertiesTest(TestCase): + + def setUp(self): + sample_role = VentureRoleFactory() + RolePropertyFactory( + symbol='my_custom_property_1', + type=RolePropertyTypeFactory(symbol='STRING'), + role=sample_role, + ) + self.sample_device = DeviceFactory( + venture=sample_role.venture, venture_role=sample_role) + self.sample_user = UserFactory() + + def test_successful_save(self): + self.sample_device.set_property( + symbol='my_custom_property_1', value='Test 123', + user=self.sample_user, + ) + self.assertEqual( + self.sample_device.get_property_set(), + {'my_custom_property_1': 'Test 123'} + ) diff --git a/src/ralph/ui/views/common.py b/src/ralph/ui/views/common.py index c91eb9eba1..15997641f5 100644 --- a/src/ralph/ui/views/common.py +++ b/src/ralph/ui/views/common.py @@ -55,8 +55,6 @@ from ralph.scan.models import ScanSummary from ralph.scan.util import get_pending_changes, update_scan_summary from ralph.business.models import ( - RoleProperty, - RolePropertyValue, Venture, VentureRole, ) @@ -659,27 +657,7 @@ def get(self, *args, **kwargs): def save_properties(self, device, properties): for symbol, value in properties.iteritems(): - try: - p = device.venture_role.roleproperty_set.get(symbol=symbol) - except RoleProperty.DoesNotExist: - p = device.venture.roleproperty_set.get(symbol=symbol) - if value != p.default and not {value, p.default} == {None, ''}: - pv, created = RolePropertyValue.concurrent_get_or_create( - property=p, - device=device, - ) - pv.value = value - pv.save(user=self.request.user) - else: - try: - pv = RolePropertyValue.objects.get( - property=p, - device=device, - ) - except RolePropertyValue.DoesNotExist: - pass - else: - pv.delete() + device.set_property(symbol, value, self.request.user) def get_property_form(self, data=None): if not self.object.venture_role: diff --git a/src/ralph/ui/views/racks.py b/src/ralph/ui/views/racks.py index 97041d9c7e..c75af7cbe1 100644 --- a/src/ralph/ui/views/racks.py +++ b/src/ralph/ui/views/racks.py @@ -201,11 +201,15 @@ def sort_tree(self, query, sort): depth, item = top.pop() c = children[item] if sort in ('-position', 'position'): - key = lambda x: (x.get_position() or '').rjust(100) + def key(x): + return (x.get_position() or '').rjust(100) c.sort(key=key, reverse=not sort.startswith('-')) elif sort == '': - key = lambda x: (x.model.type if x.model else None, - (x.get_position() or '').rjust(100)) + def key(x): + return ( + x.model.type if x.model else None, + (x.get_position() or '').rjust(100) + ) c.sort(key=key, reverse=True) top.extend((depth + 1, i) for i in c) item.depth = depth diff --git a/src/ralph/util/models.py b/src/ralph/util/models.py index 66d913daf9..9789127a32 100644 --- a/src/ralph/util/models.py +++ b/src/ralph/util/models.py @@ -18,6 +18,7 @@ from ralph.settings import SYNC_FIELD_MIXIN_NOTIFICATIONS_WHITELIST +from django.contrib.auth.tests import models as auth_test_models ChangeTuple = namedtuple('ChangeTuple', ['field', 'old_value', 'new_value']) @@ -34,7 +35,6 @@ def create_api_key_ignore_dberrors(*args, **kwargs): # workaround for a unit test bug in Django 1.4.x -from django.contrib.auth.tests import models as auth_test_models del auth_test_models.ProfileTestCase.test_site_profile_not_available # signal used by SyncFieldMixin for sending notifications on changed fields diff --git a/src/ralph/util/tests/utils.py b/src/ralph/util/tests/utils.py index 85d5f759c5..9a34260a5a 100644 --- a/src/ralph/util/tests/utils.py +++ b/src/ralph/util/tests/utils.py @@ -10,7 +10,9 @@ from factory.django import DjangoModelFactory from ralph.account.models import Region -from ralph.business.models import Venture, VentureRole +from ralph.business.models import ( + Venture, VentureRole, RolePropertyType, RoleProperty, +) from ralph.cmdb import models_ci from ralph.cmdb.tests.utils import CIFactory from ralph.ui.tests.global_utils import UserFactory @@ -95,3 +97,14 @@ class VentureRoleFactory(DjangoModelFactory): FACTORY_FOR = VentureRole name = factory.Sequence(lambda n: 'Venture role #{}'.format(n)) venture = factory.SubFactory(VentureFactory) + + +class RolePropertyTypeFactory(DjangoModelFactory): + FACTORY_FOR = RolePropertyType + symbol = factory.Sequence(lambda n: 'property_type_{}'.format(n)) + + +class RolePropertyFactory(DjangoModelFactory): + FACTORY_FOR = RoleProperty + symbol = factory.Sequence(lambda n: 'property_{}'.format(n)) + type = factory.SubFactory(RolePropertyTypeFactory) From 819f2e50cf3e138ca1b0b0f7945e7a77fe353f1a Mon Sep 17 00:00:00 2001 From: Tymoteusz Jankowski Date: Tue, 10 Feb 2015 11:41:59 +0100 Subject: [PATCH 157/189] Fixed settings position in settings file. Before, DEFAULT_REGION_NAME (and SYNC_FIELD_MIXIN_NOTIFICATIONS_WHITELIST) was below code loading ~/.ralph/settings which disallows change DEFAULT_REGION_NAME (and SYNC_FIELD_MIXIN_NOTIFICATIONS_WHITELIST) in ~/.ralph/settings. This change fixes that. --- src/ralph/settings.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/ralph/settings.py b/src/ralph/settings.py index 2d384bfce1..1e158af669 100644 --- a/src/ralph/settings.py +++ b/src/ralph/settings.py @@ -402,6 +402,13 @@ "Unsupported settings path mode '%s'" % SETTINGS_PATH_MODE ) +# change regions' default name +DEFAULT_REGION_NAME = 'Default region' + +# a list of object's fields (e.g. Asset, Device) for which notification of +# changed value should be send (see ralph.util.models.SyncFieldMixin) +SYNC_FIELD_MIXIN_NOTIFICATIONS_WHITELIST = ['service', 'device_environment'] + for cfg_loc in [local_settings, '{}/settings'.format(ralph_settings_path), '/etc/ralph/settings']: @@ -410,14 +417,6 @@ execfile(cfg_loc) break -# url to page where user requests permission to module (eg. assets) -# REQUEST_PERM_URL = 'http://tickets.office/request/ralph_module/permission' -DEFAULT_REGION_NAME = 'Default region' - -# a list of object's fields (e.g. Asset, Device) for which notification of -# changed value should be send (see ralph.util.models.SyncFieldMixin) -SYNC_FIELD_MIXIN_NOTIFICATIONS_WHITELIST = ['service', 'device_environment'] - import pluggableapp pluggableapp.initialize(locals()) From b63bfe490145b88d71c7a2d39656f1474810024b Mon Sep 17 00:00:00 2001 From: Andrzej Jankowski Date: Tue, 10 Feb 2015 14:41:10 +0100 Subject: [PATCH 158/189] Changes in deployments plugins order. Added clean plugin as required to each other plugins to make sure that new venture and role will be assigned to deployed server. Removed venture and role assignation form clean plugin - it's now not required. --- src/ralph/deployment/plugins/change_status.py | 2 +- src/ralph/deployment/plugins/clean.py | 2 -- src/ralph/deployment/plugins/dhcp.py | 2 +- src/ralph/deployment/plugins/dns.py | 2 +- src/ralph/deployment/plugins/role.py | 2 +- src/ralph/deployment/tests/plugins/test_clean.py | 6 ------ 6 files changed, 4 insertions(+), 12 deletions(-) diff --git a/src/ralph/deployment/plugins/change_status.py b/src/ralph/deployment/plugins/change_status.py index 33ad1bcfd6..27c2f0b96a 100644 --- a/src/ralph/deployment/plugins/change_status.py +++ b/src/ralph/deployment/plugins/change_status.py @@ -11,7 +11,7 @@ @plugin.register(chain='deployment', - requires=['dns', 'dhcp', 'role'], + requires=['dns', 'dhcp', 'role', 'clean'], priority=0) def change_status(deployment_id): deployment = Deployment.objects.get(id=deployment_id) diff --git a/src/ralph/deployment/plugins/clean.py b/src/ralph/deployment/plugins/clean.py index c58f8bc9f1..c97a65148a 100644 --- a/src/ralph/deployment/plugins/clean.py +++ b/src/ralph/deployment/plugins/clean.py @@ -80,8 +80,6 @@ def clean(deployment_id): return True do_clean(deployment.device, deployment.user) deployment.device.name = deployment.hostname - deployment.device.venture = deployment.venture - deployment.device.venture_role = deployment.venture_role deployment.device.save() ip, created = IPAddress.concurrent_get_or_create(address=deployment.ip) ip.device = deployment.device diff --git a/src/ralph/deployment/plugins/dhcp.py b/src/ralph/deployment/plugins/dhcp.py index f34cd71ab2..e44dcdd6bc 100644 --- a/src/ralph/deployment/plugins/dhcp.py +++ b/src/ralph/deployment/plugins/dhcp.py @@ -12,7 +12,7 @@ from ralph.deployment.models import Deployment -@plugin.register(chain='deployment', requires=['dns'], priority=0) +@plugin.register(chain='deployment', requires=['dns', 'clean'], priority=0) def dhcp(deployment_id): deployment = Deployment.objects.get(id=deployment_id) try: diff --git a/src/ralph/deployment/plugins/dns.py b/src/ralph/deployment/plugins/dns.py index 0a8f1da1e1..7a808f0f3e 100644 --- a/src/ralph/deployment/plugins/dns.py +++ b/src/ralph/deployment/plugins/dns.py @@ -11,7 +11,7 @@ from ralph.deployment.models import Deployment -@plugin.register(chain='deployment', requires=['role'], priority=0) +@plugin.register(chain='deployment', requires=['role', 'clean'], priority=0) def dns(deployment_id): deployment = Deployment.objects.get(id=deployment_id) reset_dns(deployment.hostname, deployment.ip) diff --git a/src/ralph/deployment/plugins/role.py b/src/ralph/deployment/plugins/role.py index 107de8629b..25ec28d8a3 100644 --- a/src/ralph/deployment/plugins/role.py +++ b/src/ralph/deployment/plugins/role.py @@ -10,7 +10,7 @@ from ralph.deployment.models import Deployment -@plugin.register(chain='deployment', requires=[], priority=0) +@plugin.register(chain='deployment', requires=['clean'], priority=0) def role(deployment_id): deployment = Deployment.objects.get(id=deployment_id) deployment.device.venture = deployment.venture diff --git a/src/ralph/deployment/tests/plugins/test_clean.py b/src/ralph/deployment/tests/plugins/test_clean.py index ffc55f02bc..d683b95395 100644 --- a/src/ralph/deployment/tests/plugins/test_clean.py +++ b/src/ralph/deployment/tests/plugins/test_clean.py @@ -22,13 +22,11 @@ ) from ralph.deployment.models import Deployment from ralph.deployment.plugins.clean import clean -from ralph.util.tests.utils import VentureRoleFactory class CleanPluginTest(TestCase): def setUp(self): - self.venture_role = VentureRoleFactory() device = Device.create( ethernets=[('', 'deadbeefcafe', 0)], model_name='HAL 9000', @@ -42,8 +40,6 @@ def setUp(self): mac='deadbeefcafe', device=device, preboot=None, - venture=self.venture_role.venture, - venture_role=self.venture_role, ) self.deployment.save() self.ip = IPAddress(address='127.0.0.1', device=device) @@ -70,8 +66,6 @@ def test_clean_plugin(self): self.assertEquals(device.ipaddress_set.count(), 1) self.assertEquals(device.ipaddress_set.all()[0].address, '127.0.0.1') self.assertEquals(device.name, 'discovery.one') - self.assertEquals(device.venture, self.venture_role.venture) - self.assertEquals(device.venture_role, self.venture_role) self.assertFalse(device.diskshare_set.exists()) self.assertFalse(device.disksharemount_set.exists()) self.assertFalse(device.software_set.exists()) From 25e21b6dbefee550c45beca636546e4dc582e365 Mon Sep 17 00:00:00 2001 From: Slawomir Sawicki Date: Wed, 11 Feb 2015 08:36:51 +0100 Subject: [PATCH 159/189] Added service and environment to mass deploy service and environment are required when using mass deploy form. --- src/ralph/deployment/util.py | 6 +++ src/ralph/ui/forms/deployment.py | 70 ++++++++++++++++++++++++++------ 2 files changed, 63 insertions(+), 13 deletions(-) diff --git a/src/ralph/deployment/util.py b/src/ralph/deployment/util.py index 05b63f6494..1ae543a568 100644 --- a/src/ralph/deployment/util.py +++ b/src/ralph/deployment/util.py @@ -18,11 +18,13 @@ from ralph.deployment.models import Preboot, Deployment, DeploymentStatus from ralph.discovery.models import ( Device, + DeviceEnvironment, DeviceType, Ethernet, EthernetSpeed, IPAddress, Network, + ServiceCatalog, ) from ralph.dnsedit.models import DHCPEntry from ralph.dnsedit.util import clean_dns_entries, reset_dns @@ -278,6 +280,10 @@ def create_deployments(data, user, mass_deployment): venture=item['venture'], venture_role=item['venture_role'], mass_deployment=mass_deployment, + service=ServiceCatalog.objects.get(name=item['service']), + device_environment=DeviceEnvironment.objects.get( + name=item['device_environment'], + ), ) diff --git a/src/ralph/ui/forms/deployment.py b/src/ralph/ui/forms/deployment.py index a95c55aa1e..140293ff9e 100644 --- a/src/ralph/ui/forms/deployment.py +++ b/src/ralph/ui/forms/deployment.py @@ -29,7 +29,14 @@ rack_exists, venture_and_role_exists, ) -from ralph.discovery.models import Device, Network, IPAddress, DeviceType +from ralph.discovery.models import ( + Device, + DeviceEnvironment, + DeviceType, + Network, + IPAddress, + ServiceCatalog, +) from ralph.discovery.models_component import is_mac_valid from ralph.dnsedit.models import DHCPEntry from ralph.dnsedit.util import ( @@ -180,6 +187,30 @@ def _validate_preboot(preboot, row_number): ) +def _validate_service(service_name, row_number): + try: + ServiceCatalog.objects.get(name=service_name) + except ServiceCatalog.DoesNotExist: + raise forms.ValidationError( + "Row %s: " + "Couldn't find service with name %s" % ( + row_number, service_name + ) + ) + + +def _validate_environment(environment_name, row_number): + try: + DeviceEnvironment.objects.get(name=environment_name) + except DeviceEnvironment.DoesNotExist: + raise forms.ValidationError( + "Row %s: " + "Couldn't find environment with name %s" % ( + row_number, environment_name + ) + ) + + def _validate_deploy_children(mac, row_number): mac = MACAddressField.normalize(mac) try: @@ -246,7 +277,7 @@ class PrepareMassDeploymentForm(forms.Form): required=False, help_text=( "Template: mac ; management-ip ; network ; venture-symbol ; " - "role ; preboot{}".format( + "role ; service ; environment ; preboot{}".format( ' ; asset sn or barcode (not required)' if assets_enabled else '' @@ -258,9 +289,10 @@ def clean_csv(self): csv_string = self.cleaned_data['csv'].strip().lower() rows = UnicodeReader(cStringIO.StringIO(csv_string)) parsed_macs = set() + for row_number, cols in enumerate(rows, start=1): _validate_cols_count( - 6, cols, row_number, 1 if self.assets_enabled else 0 + 8, cols, row_number, 1 if self.assets_enabled else 0 ) mac = cols[0].strip() _validate_mac(mac, parsed_macs, row_number) @@ -277,10 +309,14 @@ def clean_csv(self): _validate_venture_and_role( venture_symbol, venture_role, row_number, ) - preboot = cols[5].strip() + service = cols[5].strip() + _validate_service(service, row_number) + environment = cols[6].strip() + _validate_environment(environment, row_number) + preboot = cols[7].strip() _validate_preboot(preboot, row_number) - if self.assets_enabled and len(cols) == 7: - asset_identity = cols[6].strip() + if self.assets_enabled and len(cols) == 9: + asset_identity = cols[8].strip() _validate_asset_identity(asset_identity, row_number, mac) return csv_string @@ -370,7 +406,7 @@ class MassDeploymentForm(forms.Form): widget=forms.widgets.Textarea(attrs={'class': 'span12 csv-input'}), help_text=( "Template: hostname ; ip ; rack-sn ; mac ; management-ip ; " - "network ; venture-symbol ; role ; preboot{}".format( + "network ; venture-symbol ; role ; service ; environment ; preboot{}".format( ' ; asset sn or barcode (not required)' if assets_enabled else '' @@ -386,7 +422,7 @@ def clean_csv(self): parsed_ip_addresses = set() parsed_macs = set() for row_number, cols in enumerate(rows, start=1): - _validate_cols_count(9, cols, row_number, 1) + _validate_cols_count(11, cols, row_number, 1) _validate_cols_not_empty(cols, row_number) mac = cols[3].strip() _validate_mac(mac, parsed_macs, row_number) @@ -444,17 +480,23 @@ def clean_csv(self): ) ) try: - preboot = Preboot.objects.get(name=cols[8].strip()) + preboot = Preboot.objects.get(name=cols[10].strip()) except Preboot.DoesNotExist: raise forms.ValidationError( "Row %s: Couldn't find preboot %s" % ( - row_number, cols[8].strip() + row_number, cols[10].strip() ) ) asset_identity = None - if self.assets_enabled and len(cols) == 10: - asset_identity = cols[9].strip() + if self.assets_enabled and len(cols) == 12: + asset_identity = cols[11].strip() _validate_asset_identity(asset_identity, row_number, mac) + + service = cols[8].strip() + _validate_service(service, row_number) + environment = cols[9].strip() + _validate_environment(environment, row_number) + cleaned_csv.append({ 'hostname': hostname, 'ip': ip, @@ -465,7 +507,9 @@ def clean_csv(self): 'preboot': preboot, 'management_ip': management_ip, 'network': network, - 'asset_identity': asset_identity + 'asset_identity': asset_identity, + 'service': service, + 'device_environment': environment, }) return cleaned_csv From 0c687136f224f2574d1842a162fd0330d81571b8 Mon Sep 17 00:00:00 2001 From: Arkadiusz Adamski Date: Fri, 13 Feb 2015 12:54:29 +0100 Subject: [PATCH 160/189] added default value to connection variable To prevent raise `UnboundLocalError`. --- .../scan/postprocess/lldp_connections.py | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/src/ralph/scan/postprocess/lldp_connections.py b/src/ralph/scan/postprocess/lldp_connections.py index 39f3a2fccb..64a53dc515 100644 --- a/src/ralph/scan/postprocess/lldp_connections.py +++ b/src/ralph/scan/postprocess/lldp_connections.py @@ -42,6 +42,7 @@ def _network_connections_in_results(data): def _create_or_update_connection(device, connection_data): + connection = None if connection_data['connection_type'] != 'network': return try: @@ -62,19 +63,20 @@ def _create_or_update_connection(device, connection_data): Exception message: {}. """.format(sn, mac_addresses, ip_addresses, unicode(e)) logger.exception(msg) - connetion_details = connection_data.get('details', {}) - if connetion_details: - outbound_port = connetion_details.get('outbound_port') - inbound_port = connetion_details.get('inbound_port') - try: - details = NetworkConnection.objects.get(connection=connection) - except NetworkConnection.DoesNotExist: - details = NetworkConnection(connection=connection) - if outbound_port: - details.outbound_port = outbound_port - if inbound_port: - details.inbound_port = inbound_port - details.save() + else: + connection_details = connection_data.get('details', {}) + if connection_details: + outbound_port = connection_details.get('outbound_port') + inbound_port = connection_details.get('inbound_port') + try: + details = NetworkConnection.objects.get(connection=connection) + except NetworkConnection.DoesNotExist: + details = NetworkConnection(connection=connection) + if outbound_port: + details.outbound_port = outbound_port + if inbound_port: + details.inbound_port = inbound_port + details.save() return connection From 15c03387a095fc2cb0123493cf64ef7a628bc8ea Mon Sep 17 00:00:00 2001 From: Arkadiusz Adamski Date: Mon, 16 Feb 2015 09:21:21 +0100 Subject: [PATCH 161/189] fixed W503 In new version of pep8 (1.6.2) raise W503 as error. --- src/ralph/cmdb/forms.py | 4 ++-- src/ralph/cmdb/importer.py | 4 ++-- src/ralph/cmdb/models_signals.py | 4 ++-- src/ralph/cmdb/util.py | 10 +++++----- src/ralph/cmdb/views_changes.py | 3 +-- src/ralph/discovery/management/commands/report.py | 9 +++------ src/ralph/ui/views/search.py | 4 ++-- 7 files changed, 17 insertions(+), 21 deletions(-) diff --git a/src/ralph/cmdb/forms.py b/src/ralph/cmdb/forms.py index 41eb78bc1a..99fe3f5740 100644 --- a/src/ralph/cmdb/forms.py +++ b/src/ralph/cmdb/forms.py @@ -173,8 +173,8 @@ def clean_state(self, *args, **kwargs): state = self.instance.state changed_state = self.cleaned_data.get('state') if ( - self.instance.id - and not can_change_ci_state(self.instance, changed_state) + self.instance.id and + not can_change_ci_state(self.instance, changed_state) ): message = """You can not change state to {}, because this service has linked devices. Click here to diff --git a/src/ralph/cmdb/importer.py b/src/ralph/cmdb/importer.py index fdaf083f93..7e0894e228 100644 --- a/src/ralph/cmdb/importer.py +++ b/src/ralph/cmdb/importer.py @@ -167,8 +167,8 @@ def import_assets_by_contenttype(self, asset_class, _type, layers, logger.info('Importing devices.') asset_content_type = ContentType.objects.get_for_model(asset_class) prefix = cdb.CIContentTypePrefix.objects.filter( - content_type_name=asset_content_type.app_label + '.' - + asset_content_type.model.replace(' ', '') + content_type_name=asset_content_type.app_label + '.' + + asset_content_type.model.replace(' ', '') ) if not prefix: raise TypeError( diff --git a/src/ralph/cmdb/models_signals.py b/src/ralph/cmdb/models_signals.py index dd4257c32e..b9eed185b4 100644 --- a/src/ralph/cmdb/models_signals.py +++ b/src/ralph/cmdb/models_signals.py @@ -184,8 +184,8 @@ def can_register_change(instance): 'Settings not configured for OP tickets registration. Skipping.') return False return ( - instance.registration_type == chdb.CI_CHANGE_REGISTRATION_TYPES.WAITING - and not instance.external_key and instance.time.date() >= OP_START_DATE + instance.registration_type == chdb.CI_CHANGE_REGISTRATION_TYPES.WAITING and # noqa + not instance.external_key and instance.time.date() >= OP_START_DATE ) diff --git a/src/ralph/cmdb/util.py b/src/ralph/cmdb/util.py index b5e481c2b4..2192600221 100644 --- a/src/ralph/cmdb/util.py +++ b/src/ralph/cmdb/util.py @@ -216,11 +216,11 @@ def set_event(current_ci): def can_change_ci_state(ci, changed_state): if ( - changed_state - and ci.type.id == CI_TYPES.SERVICE - and ci.state == CI_STATE_TYPES.ACTIVE - and changed_state in (CI_STATE_TYPES.INACTIVE, CI_STATE_TYPES.WAITING) - and Device.objects.filter(service=ci).count() > 0 + changed_state and + ci.type.id == CI_TYPES.SERVICE and + ci.state == CI_STATE_TYPES.ACTIVE and + changed_state in (CI_STATE_TYPES.INACTIVE, CI_STATE_TYPES.WAITING) and + Device.objects.filter(service=ci).count() > 0 ): return False return True diff --git a/src/ralph/cmdb/views_changes.py b/src/ralph/cmdb/views_changes.py index 91a7299555..ada2d24ad5 100644 --- a/src/ralph/cmdb/views_changes.py +++ b/src/ralph/cmdb/views_changes.py @@ -424,8 +424,7 @@ def get(self, *args, **kwargs): ci.content_object.venture) if venture_id: if (venture) and ( - (venture.id == int(venture_id)) - or + (venture.id == int(venture_id)) or ((venture is None) and (int(venture_id) == -1))): self.data.append(dict( count=count, diff --git a/src/ralph/discovery/management/commands/report.py b/src/ralph/discovery/management/commands/report.py index 88f5f85cd2..7630a6c50a 100644 --- a/src/ralph/discovery/management/commands/report.py +++ b/src/ralph/discovery/management/commands/report.py @@ -27,23 +27,20 @@ def field(field_name): def subfield(field_name, subfield_name): return lambda dev: unicode( (getattr(getattr(dev, field_name), subfield_name) - if getattr(dev, field_name) else None) - or '' + if getattr(dev, field_name) else None) or '' ) def first_subfield(field_name, subfield_name): return lambda dev: unicode( (getattr(getattr(dev, field_name).all()[0], subfield_name) - if getattr(dev, field_name).count() else None) - or '' + if getattr(dev, field_name).count() else None) or '' ) def device_type(dev): return DeviceType.DescFromID( - (dev.model.type if dev.model else None) - or DeviceType.unknown.id + (dev.model.type if dev.model else None) or DeviceType.unknown.id ).title() diff --git a/src/ralph/ui/views/search.py b/src/ralph/ui/views/search.py index 764e61498b..a17fa06bc0 100644 --- a/src/ralph/ui/views/search.py +++ b/src/ralph/ui/views/search.py @@ -430,8 +430,8 @@ def get_queryset(self): if data['with_changes']: changed_devices_ids = self._get_changed_devices_ids() self.query = self.query.filter(id__in=changed_devices_ids) - if (data.get('without_asset', False) - and 'ralph_assets' in settings.INSTALLED_APPS): + if (data.get('without_asset', False) and + 'ralph_assets' in settings.INSTALLED_APPS): from ralph_assets.models_assets import DeviceInfo device_info_ids = DeviceInfo.objects.exclude( ralph_device_id=None From 00c239b7fe477fd7122668cf626f18dc7d6a0cc3 Mon Sep 17 00:00:00 2001 From: Slawomir Sawicki Date: Fri, 13 Feb 2015 15:39:19 +0100 Subject: [PATCH 162/189] Added service and environment to mass deployment - Added service and environment option to mass deployment - Created functional tests --- src/ralph/deployment/tests/test_util.py | 68 +++++++- src/ralph/deployment/tests/utils.py | 29 ++++ src/ralph/discovery/tests/util.py | 8 + src/ralph/ui/forms/deployment.py | 10 +- .../tests/functional/test_mass_deployment.py | 149 ++++++++++++++++++ src/ralph/ui/urls.py | 6 +- 6 files changed, 259 insertions(+), 11 deletions(-) create mode 100644 src/ralph/deployment/tests/utils.py create mode 100644 src/ralph/ui/tests/functional/test_mass_deployment.py diff --git a/src/ralph/deployment/tests/test_util.py b/src/ralph/deployment/tests/test_util.py index fd0d798f10..27350f2bcc 100644 --- a/src/ralph/deployment/tests/test_util.py +++ b/src/ralph/deployment/tests/test_util.py @@ -4,8 +4,17 @@ from __future__ import print_function from __future__ import unicode_literals +from django.contrib.auth.models import User from django.test import TestCase +from ralph_assets.tests.utils.assets import AssetFactory, DeviceInfoFactory +from ralph.cmdb.tests.utils import ( + DeviceEnvironmentFactory, + ServiceCatalogFactory, +) +from ralph.deployment import util +from ralph.deployment.models import Deployment +from ralph.deployment.tests.utils import MassDeploymentFactory, PrebootFactory from ralph.deployment.util import ( _change_device_ip_address, _change_ip_address_dhcp_entry, @@ -16,10 +25,15 @@ ChangeIPAddressError, ) from ralph.discovery.models import IPAddress -from ralph.discovery.tests.util import DeviceFactory, IPAddressFactory +from ralph.discovery.tests.util import ( + DeviceFactory, + IPAddressFactory, + EthernetFactory, +) from ralph.dnsedit.models import DHCPEntry from ralph.dnsedit.tests.util import DHCPEntryFactory - +from ralph.ui.tests.global_utils import UserFactory +from ralph.util.tests.utils import VentureRoleFactory class ChangeIPAddressValidationTest(TestCase): @@ -171,3 +185,53 @@ def test_change_ip_address_dhcp_entry_fail(self): self.sample_ip_3, self.sample_ip_1, ) + + +class CreateDeploymentsTest(TestCase): + + def setUp(self): + venture_role = VentureRoleFactory() + device_environment = DeviceEnvironmentFactory() + service = ServiceCatalogFactory() + self.mass_deployment = MassDeploymentFactory() + self.user = UserFactory() + self.data = { + "mac": "00:00:00:00:00:00", + "ip": "192.168.1.1", + "hostname": "testhost.dc2", + "preboot": PrebootFactory(), + "venture": venture_role.venture, + "venture_role": venture_role, + "service": service.name, + "device_environment": device_environment.name, + "asset_identity": None, + } + + def test_when_everything_works_fine(self): + EthernetFactory(mac="000000000000") + util.create_deployments([self.data], self.user, self.mass_deployment) + + self.assertEqual(Deployment.objects.count(), 1) + + def test_when_device_does_not_exist_and_asset_identity_is_given(self): + device = DeviceFactory() + device_info = DeviceInfoFactory(ralph_device_id=device.id) + asset = AssetFactory( + barcode="testbarcode", + device_info=device_info, + ) + self.data.update({"asset_identity": asset.barcode}) + util.create_deployments([self.data], self.user, self.mass_deployment) + + deployments = Deployment.objects.all() + self.assertEqual(deployments.count(), 1) + self.assertEqual(deployments[0].device, device) + + def test_when_device_does_not_exist_and_there_is_no_asset_identity(self): + device = DeviceFactory() + util._create_device = lambda x: device + util.create_deployments([self.data], self.user, self.mass_deployment) + + deployments = Deployment.objects.all() + self.assertEqual(deployments.count(), 1) + self.assertEqual(deployments[0].device, device) diff --git a/src/ralph/deployment/tests/utils.py b/src/ralph/deployment/tests/utils.py new file mode 100644 index 0000000000..b036cd71a2 --- /dev/null +++ b/src/ralph/deployment/tests/utils.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals + +import factory +from factory.django import DjangoModelFactory + +from ralph.deployment.models import MassDeployment, Preboot, Deployment +from ralph.discovery.tests.util import DeviceFactory + + +class DeploymentFactory(DjangoModelFactory): + FACTORY_FOR = Deployment + + device = factory.SubFactory(DeviceFactory) + mac = "000000000000" + ip = "" + + +class MassDeploymentFactory(DjangoModelFactory): + FACTORY_FOR = MassDeployment + + +class PrebootFactory(DjangoModelFactory): + FACTORY_FOR = Preboot diff --git a/src/ralph/discovery/tests/util.py b/src/ralph/discovery/tests/util.py index b73f174e21..d173103054 100644 --- a/src/ralph/discovery/tests/util.py +++ b/src/ralph/discovery/tests/util.py @@ -14,6 +14,7 @@ from factory import sequence, Sequence, lazy_attribute, Factory, SubFactory from factory.django import DjangoModelFactory +from ralph.discovery.models import Ethernet from ralph.discovery.models_device import ( Database, DatabaseType, @@ -56,6 +57,13 @@ def barcode(self): return str(uuid1()) +class EthernetFactory(DjangoModelFactory): + FACTORY_FOR = Ethernet + + device = SubFactory(DeviceFactory) + mac = "000000000000" + + class DeprecatedDataCenterFactory(DjangoModelFactory): FACTORY_FOR = DeprecatedRalphDC diff --git a/src/ralph/ui/forms/deployment.py b/src/ralph/ui/forms/deployment.py index 140293ff9e..afc51c0ac1 100644 --- a/src/ralph/ui/forms/deployment.py +++ b/src/ralph/ui/forms/deployment.py @@ -188,9 +188,7 @@ def _validate_preboot(preboot, row_number): def _validate_service(service_name, row_number): - try: - ServiceCatalog.objects.get(name=service_name) - except ServiceCatalog.DoesNotExist: + if not ServiceCatalog.objects.filter(name=service_name).exists(): raise forms.ValidationError( "Row %s: " "Couldn't find service with name %s" % ( @@ -200,9 +198,7 @@ def _validate_service(service_name, row_number): def _validate_environment(environment_name, row_number): - try: - DeviceEnvironment.objects.get(name=environment_name) - except DeviceEnvironment.DoesNotExist: + if not DeviceEnvironment.objects.filter(name=environment_name).exists(): raise forms.ValidationError( "Row %s: " "Couldn't find environment with name %s" % ( @@ -468,7 +464,7 @@ def clean_csv(self): _validate_management_ip(management_ip, row_number) try: venture_role = VentureRole.objects.get( - venture__symbol=cols[6].strip().upper(), + venture__symbol=cols[6].strip(), name=cols[7].strip() ) venture = venture_role.venture diff --git a/src/ralph/ui/tests/functional/test_mass_deployment.py b/src/ralph/ui/tests/functional/test_mass_deployment.py new file mode 100644 index 0000000000..64f00dd391 --- /dev/null +++ b/src/ralph/ui/tests/functional/test_mass_deployment.py @@ -0,0 +1,149 @@ +# -*- coding: utf-8 -*- + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals + +from django.core.urlresolvers import reverse +from django.test import TestCase +from lck.django.common.models import MACAddressField + +from ralph.cmdb.tests.utils import ( + DeviceEnvironmentFactory, + ServiceCatalogFactory, +) +from ralph.dnsedit.tests.util import DNSRecordFactory, DNSDomainFactory +from ralph.deployment.tests.utils import PrebootFactory, DeploymentFactory +from ralph.deployment.models import MassDeployment, Deployment +from ralph.discovery.models import DeviceType +from ralph.discovery.tests.util import ( + DeviceFactory, + EthernetFactory, + NetworkFactory, + DeviceFactory, + DeviceModelFactory, + IPAddressFactory, +) +from ralph.ui.tests.global_utils import login_as_su +from ralph.util.tests.utils import VentureRoleFactory + + +class MassDeploymentTest(TestCase): + + def setUp(self): + self.client = login_as_su() + self.device_environment = DeviceEnvironmentFactory(name='testenv') + self.service = ServiceCatalogFactory(name='testservice') + self.preboot = PrebootFactory(name="prebotname") + self.ethernet = EthernetFactory.create( + mac="0025b0000000", + ) + self.network = NetworkFactory.create( + address='10.80.80.0/20', + name='testnetwork', + ) + self.network.save() + self.venture_role = VentureRoleFactory( + name="testventurerole", + venture__symbol='testventure', + venture__name='testventure' + ) + + def _base_check_for_mass_deployment( + self, + response, + csv_field_name, + expected_csv, + ): + self.assertEqual(response.status_code, 302) + self.assertEqual(MassDeployment.objects.count(), 1) + self.assertEqual( + getattr(MassDeployment.objects.all()[0], csv_field_name), + expected_csv, + ) + + def test_prepare_mass_deployment(self): + csv = ( + '{0}; 10.80.80.100; {1}; ' + '{2}; {3}; {4} ; {5} ; {6}'.format( + self.ethernet.mac, + self.network.name, + self.venture_role.venture.symbol, + self.venture_role.name, + self.service.name, + self.device_environment.name, + self.preboot.name, + ) + ) + response = self.client.post(reverse('prepare_mass_deploy'), { + 'csv': csv + }) + + self._base_check_for_mass_deployment(response, 'csv', csv) + + def test_mass_deployment(self): + ip = '10.80.80.101' + self.test_prepare_mass_deployment() + ip_address = IPAddressFactory() + device_model = DeviceModelFactory( + type=DeviceType.rack, + name="testrack", + ) + device = DeviceFactory( + name="testdevice", + model=device_model, + sn="testsn", + ) + device.ipaddress.add(ip_address) + ethernet = EthernetFactory.create( + mac="0022b0000000", + device=device, + ) + self.network.racks.add(device) + mass_deployment = MassDeployment.objects.all()[0] + dns_domain = DNSDomainFactory(name='dc') + dns_record = DNSRecordFactory( + name='d001.dc', + type='A', + content=ip_address.address, + domain=dns_domain, + ) + url = reverse( + 'mass_deploy', + kwargs={"deployment": mass_deployment.id}, + ) + csv = ( + '{0}; {1}; {2}; ' + '{3}; 10.80.80.102; {4}; {5} ; {6} ; {7} ; {8} ; {9}'.format( + dns_record.name, + ip, + device.sn, + ethernet.mac, + self.network.name, + self.venture_role.venture.symbol.upper(), + self.venture_role.name, + self.service.name, + self.device_environment.name, + self.preboot.name, + ) + ) + response = self.client.post(url, { + 'csv': csv, + }) + + self._base_check_for_mass_deployment(response, 'generated_csv', csv) + deployment = Deployment.objects.get(device=device) + self.assertEqual(deployment.venture, self.venture_role.venture) + self.assertEqual(deployment.venture_role, self.venture_role) + self.assertEqual(deployment.service, self.service) + self.assertEqual( + deployment.device_environment, + self.device_environment, + ) + self.assertEqual(deployment.preboot, self.preboot) + self.assertEqual(deployment.ip, ip) + self.assertEqual( + deployment.mac, + MACAddressField.normalize(ethernet.mac), + ) diff --git a/src/ralph/ui/urls.py b/src/ralph/ui/urls.py index 32871b9d7f..0846eda1f5 100644 --- a/src/ralph/ui/urls.py +++ b/src/ralph/ui/urls.py @@ -311,9 +311,11 @@ login_required(NetworksDeviceList.as_view()), {}, 'networks'), url(r'^deployment/mass/start/$', - login_required(PrepareMassDeployment.as_view())), + login_required(PrepareMassDeployment.as_view()), + {}, + 'prepare_mass_deploy'), url(r'^deployment/mass/define/(?P[0-9]+)/$', - login_required(MassDeployment.as_view())), + login_required(MassDeployment.as_view()), {}, 'mass_deploy'), url(r'^scan/list/(?Pnew|existing)/$', login_required( From e66b32127957f7638e192bcb1eeaf47d36d2d1d7 Mon Sep 17 00:00:00 2001 From: Oskar Jagodzinski Date: Wed, 4 Feb 2015 08:25:11 +0100 Subject: [PATCH 163/189] change in Juniper plugin scan plugin now distinguishes between different device models in stack to generate different stack model type, cli scrapping rewrited in re module --- src/ralph/scan/plugins/ssh_juniper.py | 35 ++++++++++++------- .../scan/tests/plugins/test_ssh_juniper.py | 10 +++++- 2 files changed, 31 insertions(+), 14 deletions(-) diff --git a/src/ralph/scan/plugins/ssh_juniper.py b/src/ralph/scan/plugins/ssh_juniper.py index 79b637a2cf..29fa22fea6 100644 --- a/src/ralph/scan/plugins/ssh_juniper.py +++ b/src/ralph/scan/plugins/ssh_juniper.py @@ -16,6 +16,7 @@ from ralph.scan.plugins import get_base_result_template from ralph.util.network import check_tcp_port, connect_ssh +import re SETTINGS = settings.SCAN_PLUGINS.get(__name__, {}) @@ -37,24 +38,24 @@ def _ssh_lines(ssh, command): def _get_hostname(ssh): for line in _ssh_lines(ssh, 'show version'): - if line.lower().startswith('hostname'): - line_chunks = line.split(':') - try: - return line_chunks[1].strip() - except IndexError: - pass # unexpected result... maybe next line will be ok... + compar = re.match('.*hostname: (.*)', line.lower()) + if compar is not None: + return compar.groups()[0] + + +def _get_model(ssh): + for line in _ssh_lines(ssh, 'show version'): + compar = re.match('.*model: (.*)', line.lower()) + if compar is not None: + return compar.groups()[0] def _get_mac_addresses(ssh): mac_addresses = [] for line in _ssh_lines(ssh, 'show chassis mac-addresses'): - line = line.lower() - if line.startswith('public base address'): - mac_addresses.append( - MACAddressField.normalize( - line.replace('public base address', '').strip() - ) - ) + compar = re.match('.*(base|public) address[ ]*(.*)', line.lower()) + if compar is not None: + mac_addresses.append(MACAddressField.normalize(compar.groups()[1])) return mac_addresses @@ -111,6 +112,7 @@ def _ssh_juniper(ssh, ip_address): 'management_ip_addresses': [ip_address], } hostname = _get_hostname(ssh) + model = _get_model(ssh) mac_addresses = _get_mac_addresses(ssh) if hostname: device['hostname'] = hostname @@ -138,7 +140,14 @@ def _ssh_juniper(ssh, ip_address): ) subdevices.append(subdevice) device['subdevices'] = subdevices + device['model_name'] = 'Juniper Virtual Chassis Ethernet Switch' + if model: + if "ex" in model.lower(): + device['model_name'] = 'Juniper EX Virtual Chassis Ethernet Switch' + if "qfx" in model.lower(): + device['model_name'] = 'Juniper QFX Virtual Chassis Ethernet Switch' + device['serial_number'] = chassis_id elif switches: device['model_name'] = switches[0]['model'] diff --git a/src/ralph/scan/tests/plugins/test_ssh_juniper.py b/src/ralph/scan/tests/plugins/test_ssh_juniper.py index 7dd2562a84..d604ff86d9 100644 --- a/src/ralph/scan/tests/plugins/test_ssh_juniper.py +++ b/src/ralph/scan/tests/plugins/test_ssh_juniper.py @@ -117,6 +117,10 @@ def test_ssh_juniper_stacked(self): "show version", JUNIPER_SHOW_VERSION_SAMPLE, ), + ( + "show version", + JUNIPER_SHOW_VERSION_SAMPLE, + ), ( "show chassis mac-addresses", JUNIPER_GET_MAC_ADDRESSES_SAMPLE, @@ -127,7 +131,7 @@ def test_ssh_juniper_stacked(self): { 'hostname': 'rack01-sw1.dc', 'management_ip_addresses': ['10.10.10.10'], - 'model_name': 'Juniper Virtual Chassis Ethernet Switch', + 'model_name': 'Juniper EX Virtual Chassis Ethernet Switch', 'serial_number': 'aaaa.bbbb.cccc', 'subdevices': [ { @@ -159,6 +163,10 @@ def test_ssh_juniper_not_stacked(self): "show version", JUNIPER_SHOW_VERSION_SAMPLE, ), + ( + "show version", + JUNIPER_SHOW_VERSION_SAMPLE, + ), ( "show chassis mac-addresses", JUNIPER_GET_MAC_ADDRESSES_SAMPLE, From 9107ba2f0a02e17a8e6479261d31acb4d5e2353b Mon Sep 17 00:00:00 2001 From: Tymoteusz Jankowski Date: Tue, 17 Feb 2015 10:20:41 +0100 Subject: [PATCH 164/189] Upgraded pytz package. Pytz packaged upgraded to version=2013.6. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index b5aa2b9205..35178c721a 100644 --- a/setup.py +++ b/setup.py @@ -60,7 +60,7 @@ 'pysnmp==4.2.2', 'PyYAML==3.10', 'python-graph-core==1.8.2', - 'pytz==2013b', + 'pytz==2013.6', 'pyzabbix>=0.1', 'ralph_assets==2.4.0', 'requests>=0.14.2', From 864e65025f74555be3d5fdc0b62a7d15e02a9063 Mon Sep 17 00:00:00 2001 From: Andrzej Jankowski Date: Mon, 2 Mar 2015 11:33:23 +0100 Subject: [PATCH 165/189] Rack Visualisation view changes. List of changes: - Service Catalog, Hostname and Management IP address are now display in Info Box on the left - Service Catalog and hostname are now display on rack visualisation (instead of model) - Minor CSS changes. --- .../partials/rack/device_types/asset.html | 2 +- .../ui/static/partials/rack/rack_view.html | 25 +++++++++++++++++++ src/ralph/ui/static/ui/css/rack.css | 8 +++--- src/ralph/ui/static/ui/css/rack.less | 8 +++--- 4 files changed, 34 insertions(+), 9 deletions(-) diff --git a/src/ralph/ui/static/partials/rack/device_types/asset.html b/src/ralph/ui/static/partials/rack/device_types/asset.html index 1c4e00e5f9..c6836465c4 100644 --- a/src/ralph/ui/static/partials/rack/device_types/asset.html +++ b/src/ralph/ui/static/partials/rack/device_types/asset.html @@ -4,7 +4,7 @@ ng-class="{has_children: item.children.length}" >

- {{ item.model }} + {{ item.service }}: {{ item.hostname }}
{{ item.barcode }}
{{ item.sn }}
diff --git a/src/ralph/ui/static/partials/rack/rack_view.html b/src/ralph/ui/static/partials/rack/rack_view.html index 5a755c5bdb..2399971fb8 100644 --- a/src/ralph/ui/static/partials/rack/rack_view.html +++ b/src/ralph/ui/static/partials/rack/rack_view.html @@ -3,6 +3,14 @@

Info {{ rack.info.name }}

{{ rack.info.description }}
+ + + + + + + + @@ -23,6 +31,15 @@

Info {{ rack.info.name }}

+ + + + @@ -41,6 +58,14 @@

Info {{ rack.info.name }}

@@ -85,8 +85,8 @@

Info {{ rack.info.name }}

diff --git a/src/ralph/ui/static/ui/css/data_center.css b/src/ralph/ui/static/ui/css/data_center.css index fdd136ecdf..9aaead208f 100644 --- a/src/ralph/ui/static/ui/css/data_center.css +++ b/src/ralph/ui/static/ui/css/data_center.css @@ -970,3 +970,7 @@ width: 100%; top: 10px; } +.data-center .grid .grid_wrapper .rack:hover, +.data-center .grid .grid_wrapper .rack.active { + border: solid 1px white; +} diff --git a/src/ralph/ui/static/ui/css/data_center.less b/src/ralph/ui/static/ui/css/data_center.less index 8155d3524b..6b42e44477 100644 --- a/src/ralph/ui/static/ui/css/data_center.less +++ b/src/ralph/ui/static/ui/css/data_center.less @@ -195,6 +195,9 @@ top:10px; } } + &:hover, &.active { + .active-border(); + } } } } diff --git a/src/ralph/ui/static/ui/css/rack.css b/src/ralph/ui/static/ui/css/rack.css index 44e34ad016..376eed40ef 100644 --- a/src/ralph/ui/static/ui/css/rack.css +++ b/src/ralph/ui/static/ui/css/rack.css @@ -5,7 +5,7 @@ margin-top: 10px; background: #fff; padding: 10px; - border: 1px solid #ccc; + border: 1px solid #cccccc; position: fixed; width: 250px; margin-right: 10px; @@ -32,12 +32,22 @@ } .rack-info .info td { padding: 5px; - border-bottom: 1px solid #ccc; + border-bottom: 1px solid #cccccc; } .rack-info .info td:first-child { width: 20%; font-weight: bold; } +.rack-info .info tr:nth-last-child(2) > td, +.rack-info .info tr:last-child > td { + border-bottom: none; +} +.slot_info tr:first-child > td { + border-top: 1px solid #cccccc; +} +.slot_info tr:nth-last-child(2) > td { + border-bottom: 1px solid #cccccc; +} .racks .rack .wrapper .devices .device.position-u-1, .racks .rack .wrapper .listing-u .position.position-u-1 { bottom: 0px; @@ -464,9 +474,7 @@ width: 25px; } .racks .rack .wrapper .listing-u .position.active { - border-top-color: #ff9933; - border-bottom-color: #cc6600; - background: #ff8000; + border: 1px solid white; } .racks .rack .wrapper .pdu { position: relative; @@ -502,8 +510,7 @@ margin-bottom: 3px; } .racks .rack .wrapper .pdu pdu-item:hover { - background: #a6a6a6; - border: 1px solid #8c8c8c; + border: 1px solid white; } .racks .rack .wrapper .pdu pdu-item .item { padding: 5px; @@ -529,19 +536,17 @@ background: #93d932; } .racks .rack .wrapper .devices .device:hover { - background: #9edd48; + border: 1px solid white; } .racks .rack .wrapper .devices .device.active { - background: #c0e988; - border-color: #5c8b1a; + border: 1px solid white; } .racks .rack .wrapper .devices .device.accessory { background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAK0lEQVQYV2NkIBIwEqmOAVnhfxyawGrQFaLbANI82BRi8w+GG/GGFNHhCAARkQoLdFrYLgAAAABJRU5ErkJggg==) repeat-x; background-color: #fff; } .racks .rack .wrapper .devices .device.accessory:hover { - background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAMElEQVQYV2NkIBIwEqmOAa6wa6/Tf2yaypz3gdWgKIQJwjSANA82hUR5hlAwER2OAO66KAsAcNbaAAAAAElFTkSuQmCC) repeat-x; - background-color: #fff; + border: 1px solid white; } .racks .rack .wrapper .devices .device.accessory .info { position: relative; @@ -558,9 +563,8 @@ background-color: #fff; border-color: #ccc; } -.racks .rack .wrapper .devices .device.accessory.brush:hover { - background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAKCAYAAABxJ+R3AAAAF0lEQVQIW2NkAIKuvU7/y5z3MTIOBAcA8RQoCys/1KUAAAAASUVORK5CYII=); - background-color: #fff; +.racks .rack .wrapper .devices .device.accessory.brush:over { + border: 1px solid white; } .racks .rack .wrapper .devices .device.accessory.organizer { background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAAECAYAAACp8Z5+AAAAGklEQVQIW2NkYGD4D8SMQAwGcAY2AbBKDBUAVuYCBQPd34sAAAAASUVORK5CYII=); @@ -568,8 +572,7 @@ border-color: #ccc; } .racks .rack .wrapper .devices .device.accessory.organizer:hover { - background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAAECAYAAACp8Z5+AAAAG0lEQVQIW2M88LLjv4N4BSMDFMAZGAIwlRgqAF59CAW955lqAAAAAElFTkSuQmCC); - background-color: #fff; + border: 1px solid white; } .racks .rack .wrapper .devices .device.empty { background: #fff; @@ -589,11 +592,9 @@ .racks .rack .wrapper .devices .device.empty:after { right: 10px; } -.racks .rack .wrapper .devices .device.empty:hover { - background: #eee; -} +.racks .rack .wrapper .devices .device.empty:hover, .racks .rack .wrapper .devices .device.empty.active { - background: #ddd; + border: 1px solid white; } .racks .rack .wrapper .devices .device.height-u-1 { height: 25px; @@ -823,7 +824,7 @@ white-space: nowrap; } .racks .rack .wrapper .devices .device .children [class*="slot-"]:hover { - background: rgba(255, 255, 255, 0.5); + border: 1px solid white; } .racks .rack .wrapper .devices .device .children [class*="slot-"] .slot_no { display: block; @@ -851,3 +852,36 @@ .racks .rack .wrapper .devices .device.rows-2.cols-8.half-slots [class$="B"] { width: 25px; } +@media all and (max-width: 1500px) { + .racks .rack { + width: 350px; + } + .racks .rack.front .wrapper .devices { + width: 300px; + } + .racks .rack.front .wrapper .devices .device { + width: 300px; + } + .racks .rack .wrapper .devices { + width: 200px; + } + .racks .rack .wrapper .devices .device { + width: 200px; + } + .racks .rack .wrapper .devices .device.rows-1.cols-2 [class*="slot-"] { + width: 150px; + } + .racks .rack .wrapper .devices .device.rows-2.cols-4 [class*="slot-"] { + width: 75px; + } + .racks .rack .wrapper .devices .device.rows-2.cols-6 [class*="slot-"] { + width: 50px; + } + .racks .rack .wrapper .devices .device.rows-2.cols-8 [class*="slot-"] { + width: 37.5px; + } + .racks .rack .wrapper .devices .device.rows-2.cols-8.half-slots [class$="A"], + .racks .rack .wrapper .devices .device.rows-2.cols-8.half-slots [class$="B"] { + width: 18.75px; + } +} diff --git a/src/ralph/ui/static/ui/css/rack.less b/src/ralph/ui/static/ui/css/rack.less index 99f98e56da..0f6cc4f3c9 100644 --- a/src/ralph/ui/static/ui/css/rack.less +++ b/src/ralph/ui/static/ui/css/rack.less @@ -30,7 +30,7 @@ margin-top: 10px; background: #fff; padding: 10px; - border: 1px solid #ccc; + border: @border-info; position: fixed; width: @rack-info-width; margin-right: 10px; @@ -58,12 +58,31 @@ } td { padding: 5px; - border-bottom: 1px solid #ccc; + border-bottom: @border-info; } td:first-child { width: 20%; font-weight: bold; } + + tr:nth-last-child(2), tr:last-child { + & > td { + border-bottom: none; + } + } + } +} + +.slot_info { + tr:first-child { + & > td { + border-top: @border-info; + } + } + tr:nth-last-child(2) { + & > td { + border-bottom: @border-info; + } } } @@ -145,10 +164,7 @@ text-align: center; width: @listing-u-width; &.active { - @color: rgb(255, 128, 0); - border-top-color: lighten(@color, 10%); - border-bottom-color: darken(@color, 10%); - background: @color; + .active-border(); } } } @@ -181,9 +197,7 @@ border: 1px solid darken(@background-color, 10%); &:nth-last-of-type(1) {margin-bottom: @margin;} &:hover { - @hover-color: lighten(@background-color, 5%); - background: @hover-color; - border: 1px solid darken(@hover-color, 10%); + .active-border(); } .item { padding: @padding; @@ -207,18 +221,16 @@ border-top: 1px solid lighten(@device-background, 10%); background: @device-background; &:hover { - background: lighten(@device-background, 5%); + .active-border(); } &.active { - background: lighten(@device-background, 20%); - border-color: darken(@device-background, 20%); + .active-border(); } &.accessory { background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAK0lEQVQYV2NkIBIwEqmOAVnhfxyawGrQFaLbANI82BRi8w+GG/GGFNHhCAARkQoLdFrYLgAAAABJRU5ErkJggg==) repeat-x; background-color: #fff; &:hover { - background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAMElEQVQYV2NkIBIwEqmOAa6wa6/Tf2yaypz3gdWgKIQJwjSANA82hUR5hlAwER2OAO66KAsAcNbaAAAAAElFTkSuQmCC) repeat-x; - background-color: #fff; + .active-border(); } .info { position: relative; @@ -234,9 +246,8 @@ background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAKCAYAAABxJ+R3AAAAEklEQVQIW2NkgID/QMzIOBAcAH5DCguOmamFAAAAAElFTkSuQmCC); background-color: #fff; border-color: #ccc; - &:hover { - background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAKCAYAAABxJ+R3AAAAF0lEQVQIW2NkAIKuvU7/y5z3MTIOBAcA8RQoCys/1KUAAAAASUVORK5CYII=); - background-color: #fff; + &:over { + .active-border() } } &.organizer { @@ -244,8 +255,7 @@ background-color: #fff; border-color: #ccc; &:hover { - background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAAECAYAAACp8Z5+AAAAG0lEQVQIW2M88LLjv4N4BSMDFMAZGAIwlRgqAF59CAW955lqAAAAAElFTkSuQmCC); - background-color: #fff; + .active-border() } } } @@ -257,8 +267,9 @@ &:before, &:after {position: absolute; content:":";line-height: 1em;} &:before {left:10px;} &:after {right:10px;} - &:hover {background: #eee;} - &.active {background: #ddd;} + &:hover, &.active { + .active-border() + } } .generate-u(@max-u); &.height-u-0 .sn, &.height-u-1 .sn {display: none;} @@ -332,7 +343,7 @@ text-overflow: ellipsis; white-space: nowrap; &:hover { - background: rgba(255,255,255,.5); + .active-border(); } .slot_no { display: block; @@ -379,3 +390,45 @@ } } } + +@media all and (max-width: 1500px) { + .racks { + .rack { + &.front .wrapper .devices { + width: @devices-width-front-small; + .device {width: @devices-width-front-small;} + } + width: @rack-width-small; + .wrapper { + .devices { + width: @devices-width-back-small; + .device { + width: @devices-width-back-small; + &.rows-1.cols-2 { + @cols: 2; + [class*="slot-"] {width: @devices-width-front-small / @cols;} + } + &.rows-2.cols-4 { + @cols: 4; + [class*="slot-"] {width: @devices-width-front-small / @cols;} + } + &.rows-2.cols-6 { + @cols: 6; + [class*="slot-"] {width: @devices-width-front-small / @cols;} + } + &.rows-2.cols-8 { + @cols: 8; + [class*="slot-"] {width: @devices-width-front-small / @cols;} + &.half-slots { + [class$="A"], + [class$="B"] { + width: @devices-width-front-small / (@cols * 2); + } + } + } + } + } + } + } + } +} diff --git a/src/ralph/ui/static/ui/css/variables.less b/src/ralph/ui/static/ui/css/variables.less index e4b7dae17a..66935e3f56 100644 --- a/src/ralph/ui/static/ui/css/variables.less +++ b/src/ralph/ui/static/ui/css/variables.less @@ -1,10 +1,13 @@ @max-u: 48; @rack-border-width: 4px; @rack-width: 450px; +@rack-width-small: 350px; @pdu-width: 50px; @listing-u-width: 25px; @devices-width-back: (@rack-width - (@pdu-width * 2) - (@listing-u-width * 2)); @devices-width-front: (@rack-width - (@listing-u-width * 2)); +@devices-width-back-small: (@rack-width-small - (@pdu-width * 2) - (@listing-u-width * 2)); +@devices-width-front-small: (@rack-width-small - (@listing-u-width * 2)); @device-background: rgb(147, 217, 50); @u-height: 25px; @info-padding: 3px; @@ -17,3 +20,9 @@ @grid-color: rgba(255, 255, 255, 0.5); @grid-size: 40px; @grid-count: 40; + +.active-border() { + border: 1px solid white; +} + +@border-info: 1px solid #ccc; diff --git a/src/ralph/ui/static/ui/js/app/data_center/controllers.js b/src/ralph/ui/static/ui/js/app/data_center/controllers.js index 7d73af8a60..b7a3a4fd7e 100644 --- a/src/ralph/ui/static/ui/js/app/data_center/controllers.js +++ b/src/ralph/ui/static/ui/js/app/data_center/controllers.js @@ -28,6 +28,10 @@ angular }; $scope.$on('edit_rack', function (event, rack) { + angular.forEach($scope.data_center.rack_set, function(rack) { + rack.active = false; + }); + rack.active = true; $scope.rack = rack; }); }]); diff --git a/src/ralph/ui/static/ui/js/app/data_center/directives.js b/src/ralph/ui/static/ui/js/app/data_center/directives.js index 9db1995a5a..da61ee4e45 100644 --- a/src/ralph/ui/static/ui/js/app/data_center/directives.js +++ b/src/ralph/ui/static/ui/js/app/data_center/directives.js @@ -16,7 +16,7 @@ angular var startX = 0, startY = 0; var offset; var changedPosition = false; - scope.is_move = false; + scope.rack.active = false; element.on('mousedown', function(event) { event.preventDefault(); offset = element.offset(); diff --git a/src/ralph/ui/static/ui/js/app/rack/directives.js b/src/ralph/ui/static/ui/js/app/rack/directives.js index b552ff9dd1..36a4978128 100644 --- a/src/ralph/ui/static/ui/js/app/rack/directives.js +++ b/src/ralph/ui/static/ui/js/app/rack/directives.js @@ -20,6 +20,7 @@ angular templateUrl: '/static/partials/rack/device.html', controller: function($scope) { $scope.setActiveItem = function(item) { + item.active = true; $scope.$emit('change_active_item', item); }; $scope.setActiveSlot = function(slot) { From f2e576b86c7a533eaf02282b85bb69cc8b111d08 Mon Sep 17 00:00:00 2001 From: Marcin Kliks Date: Tue, 17 Mar 2015 15:53:38 +0100 Subject: [PATCH 174/189] Update installation.rst --- doc/installation.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/doc/installation.rst b/doc/installation.rst index 750ec82587..82e38d63c9 100644 --- a/doc/installation.rst +++ b/doc/installation.rst @@ -83,6 +83,11 @@ the :doc:`change log ` for the version you installed. Installing Ralph - advanced installation ======================================== +.. note:: + + Warning: The latest stable version on PyPi is very old (6 months old). Please help us testing new release using Docker Images. + + .. note:: You can install Ralph on a variety of sensible operating systems. This guide From f88c62067a4fcd5bdf988681c5dc3230ef5d385f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Py=C5=BCalski?= Date: Tue, 17 Mar 2015 13:53:52 +0000 Subject: [PATCH 175/189] Added admin link to data center The admin link is needed for the display. I have generalised the method for generating admin links. --- src/ralph/ui/static/partials/data_center/data_center_view.html | 1 + src/ralph/ui/static/partials/data_center/rack.html | 2 +- src/ralph/ui/static/ui/js/app/data_center/controllers.js | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/ralph/ui/static/partials/data_center/data_center_view.html b/src/ralph/ui/static/partials/data_center/data_center_view.html index 2d9e2be4fd..4f0c149a9c 100644 --- a/src/ralph/ui/static/partials/data_center/data_center_view.html +++ b/src/ralph/ui/static/partials/data_center/data_center_view.html @@ -28,6 +28,7 @@

Info {{ racks.name }}

Edit {{ rack.name }}

Please click to edit basic info about rack, click to rotate or just use drag and drop to edit position. + Use Admin to edit DC properties.
diff --git a/src/ralph/ui/static/partials/data_center/rack.html b/src/ralph/ui/static/partials/data_center/rack.html index e666280116..b7dd5e3e4b 100644 --- a/src/ralph/ui/static/partials/data_center/rack.html +++ b/src/ralph/ui/static/partials/data_center/rack.html @@ -1,6 +1,6 @@
-
+
{{ rack.name | clean }}
diff --git a/src/ralph/ui/static/ui/js/app/data_center/controllers.js b/src/ralph/ui/static/ui/js/app/data_center/controllers.js index b7a3a4fd7e..abf54e5c84 100644 --- a/src/ralph/ui/static/ui/js/app/data_center/controllers.js +++ b/src/ralph/ui/static/ui/js/app/data_center/controllers.js @@ -28,7 +28,7 @@ angular }; $scope.$on('edit_rack', function (event, rack) { - angular.forEach($scope.data_center.rack_set, function(rack) { + $scope.data_center.rack_set.forEach(function(rack) { rack.active = false; }); rack.active = true; From 7f9464ef26c6d220a4ee37877249af9d4af252a6 Mon Sep 17 00:00:00 2001 From: Piotr Jarolewski Date: Fri, 20 Mar 2015 14:14:12 +0100 Subject: [PATCH 176/189] Change .gitignore config --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index 36d660bd2d..7bf3b87f7c 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,7 @@ /src/ralph/logs /src/ralph/uploads /src/ralph/urls_local.py +*.egg-info/ +*.egg +*.py[cod] +.coverage From 4c864faa52cd6cd0f34c5fac773acea2495a676b Mon Sep 17 00:00:00 2001 From: Arkadiusz Adamski Date: Tue, 24 Mar 2015 13:53:10 +0100 Subject: [PATCH 177/189] fixed mistake (:hover instead of :over) --- src/ralph/ui/static/ui/css/rack.css | 2 +- src/ralph/ui/static/ui/css/rack.less | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ralph/ui/static/ui/css/rack.css b/src/ralph/ui/static/ui/css/rack.css index 376eed40ef..0e16da5a94 100644 --- a/src/ralph/ui/static/ui/css/rack.css +++ b/src/ralph/ui/static/ui/css/rack.css @@ -563,7 +563,7 @@ background-color: #fff; border-color: #ccc; } -.racks .rack .wrapper .devices .device.accessory.brush:over { +.racks .rack .wrapper .devices .device.accessory.brush:hover { border: 1px solid white; } .racks .rack .wrapper .devices .device.accessory.organizer { diff --git a/src/ralph/ui/static/ui/css/rack.less b/src/ralph/ui/static/ui/css/rack.less index 0f6cc4f3c9..d7695cfc86 100644 --- a/src/ralph/ui/static/ui/css/rack.less +++ b/src/ralph/ui/static/ui/css/rack.less @@ -246,7 +246,7 @@ background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAKCAYAAABxJ+R3AAAAEklEQVQIW2NkgID/QMzIOBAcAH5DCguOmamFAAAAAElFTkSuQmCC); background-color: #fff; border-color: #ccc; - &:over { + &:hover { .active-border() } } @@ -393,7 +393,7 @@ @media all and (max-width: 1500px) { .racks { - .rack { + .rack { &.front .wrapper .devices { width: @devices-width-front-small; .device {width: @devices-width-front-small;} From 96dfd285d5f74f0334f4d2e9e1fe76c12d1046b7 Mon Sep 17 00:00:00 2001 From: Arkadiusz Adamski Date: Wed, 25 Mar 2015 11:08:01 +0100 Subject: [PATCH 178/189] deleted unnecessary settings Each module (assets, scrooge) contains own tests settings. --- src/ralph/settings-test-assets.py | 20 -------------------- src/ralph/settings-test-pricing.py | 18 ------------------ 2 files changed, 38 deletions(-) delete mode 100644 src/ralph/settings-test-assets.py delete mode 100644 src/ralph/settings-test-pricing.py diff --git a/src/ralph/settings-test-assets.py b/src/ralph/settings-test-assets.py deleted file mode 100644 index bb0a166efa..0000000000 --- a/src/ralph/settings-test-assets.py +++ /dev/null @@ -1,20 +0,0 @@ -# -# A testing profile. -# -DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': ':memory:', - 'USER': '', - 'PASSWORD': '', - 'HOST': '', - 'PORT': '', - 'OPTIONS': {}, - } -} - -PLUGGABLE_APPS = ['cmdb', 'assets'] - -SOUTH_TESTS_MIGRATE = False - -ASSETS_AUTO_ASSIGN_HOSTNAME = True diff --git a/src/ralph/settings-test-pricing.py b/src/ralph/settings-test-pricing.py deleted file mode 100644 index 44f27eb172..0000000000 --- a/src/ralph/settings-test-pricing.py +++ /dev/null @@ -1,18 +0,0 @@ -# -# A testing profile. -# -DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': ':memory:', - 'USER': '', - 'PASSWORD': '', - 'HOST': '', - 'PORT': '', - 'OPTIONS': {}, - } -} - -PLUGGABLE_APPS = ['assets', 'scrooge'] - -SOUTH_TESTS_MIGRATE = False From 96762c4678d912f14916b5f48a832c0745ad76ea Mon Sep 17 00:00:00 2001 From: Arkadiusz Adamski Date: Thu, 26 Mar 2015 09:52:23 +0100 Subject: [PATCH 179/189] added new layout in CSS and adpat front to new API Added visualization rear of chassis. --- src/ralph/ui/static/partials/rack/device.html | 1 - .../ui/static/partials/rack/device_item.html | 1 + .../partials/rack/device_types/accessory.html | 7 +- .../partials/rack/device_types/asset.html | 15 ++-- src/ralph/ui/static/partials/rack/rack.html | 2 +- .../ui/static/partials/rack/rack_view.html | 4 +- src/ralph/ui/static/ui/css/data_center.css | 2 +- src/ralph/ui/static/ui/css/rack.css | 89 +++++++++++++++---- src/ralph/ui/static/ui/css/rack.less | 61 ++++++------- .../ui/static/ui/js/app/rack/directives.js | 42 +++++---- 10 files changed, 140 insertions(+), 84 deletions(-) delete mode 100644 src/ralph/ui/static/partials/rack/device.html create mode 100644 src/ralph/ui/static/partials/rack/device_item.html diff --git a/src/ralph/ui/static/partials/rack/device.html b/src/ralph/ui/static/partials/rack/device.html deleted file mode 100644 index 270018773d..0000000000 --- a/src/ralph/ui/static/partials/rack/device.html +++ /dev/null @@ -1 +0,0 @@ -
diff --git a/src/ralph/ui/static/partials/rack/device_item.html b/src/ralph/ui/static/partials/rack/device_item.html new file mode 100644 index 0000000000..7ab7e26532 --- /dev/null +++ b/src/ralph/ui/static/partials/rack/device_item.html @@ -0,0 +1 @@ +
diff --git a/src/ralph/ui/static/partials/rack/device_types/accessory.html b/src/ralph/ui/static/partials/rack/device_types/accessory.html index 78f3392063..6e43ed95fd 100644 --- a/src/ralph/ui/static/partials/rack/device_types/accessory.html +++ b/src/ralph/ui/static/partials/rack/device_types/accessory.html @@ -1,9 +1,10 @@
- {{ item.remarks }} + {{ device.remarks }}
diff --git a/src/ralph/ui/static/partials/rack/device_types/asset.html b/src/ralph/ui/static/partials/rack/device_types/asset.html index 885d810850..f00c22be67 100644 --- a/src/ralph/ui/static/partials/rack/device_types/asset.html +++ b/src/ralph/ui/static/partials/rack/device_types/asset.html @@ -1,17 +1,18 @@
- {{ item.service }}: {{ item.hostname }} -
{{ item.barcode }}
-
{{ item.sn }}
+ {{ device.service }}: {{ device.hostname }} +
{{ device.barcode }}
+
{{ device.sn }}
diff --git a/src/ralph/ui/static/partials/rack/rack.html b/src/ralph/ui/static/partials/rack/rack.html index a9dcef85c5..969088e015 100644 --- a/src/ralph/ui/static/partials/rack/rack.html +++ b/src/ralph/ui/static/partials/rack/rack.html @@ -8,7 +8,7 @@
- +
diff --git a/src/ralph/ui/static/partials/rack/rack_view.html b/src/ralph/ui/static/partials/rack/rack_view.html index 23dbd00fe1..7113ac4272 100644 --- a/src/ralph/ui/static/partials/rack/rack_view.html +++ b/src/ralph/ui/static/partials/rack/rack_view.html @@ -97,7 +97,7 @@

Info {{ rack.info.name }}

Service Catalog{{ activeItem ? activeItem.service : '-' }}
Hostname{{ activeItem ? activeItem.hostname : '-' }}
Model {{ activeItem ? activeItem.model : '-' }} Height {{ activeItem ? activeItem.height : '-' }}
Management + + {{ activeItem.management_ip }} + + - +
Remarks {{ activeItem ? activeItem.remarks : '-' }} Slot {{ activeSlot.slot_no }} + + + + + + + + diff --git a/src/ralph/ui/static/ui/css/rack.css b/src/ralph/ui/static/ui/css/rack.css index 5f8bfdbce5..bfff92d12a 100644 --- a/src/ralph/ui/static/ui/css/rack.css +++ b/src/ralph/ui/static/ui/css/rack.css @@ -546,7 +546,7 @@ .racks .rack .wrapper .devices .device.accessory .info { position: relative; } -.racks .rack .wrapper .devices .device.accessory .info .model { +.racks .rack .wrapper .devices .device.accessory .info .asset-description { top: 0; left: 0; position: absolute; @@ -744,7 +744,7 @@ display: none; } .racks .rack .wrapper .devices .device.position-u-1 .info .barcode, -.racks .rack .wrapper .devices .device.position-u-1 .info .model { +.racks .rack .wrapper .devices .device.position-u-1 .info .asset-description { top: 0; } .racks .rack .wrapper .devices .device .info { @@ -757,7 +757,7 @@ min-height: 100%; } .racks .rack .wrapper .devices .device .info .barcode, -.racks .rack .wrapper .devices .device .info .model, +.racks .rack .wrapper .devices .device .info .asset-description, .racks .rack .wrapper .devices .device .info .sn { position: absolute; } @@ -769,7 +769,7 @@ left: 3px; bottom: 3px; } -.racks .rack .wrapper .devices .device .info .model { +.racks .rack .wrapper .devices .device .info .asset-description { color: #333; font-weight: bold; left: 3px; diff --git a/src/ralph/ui/static/ui/css/rack.less b/src/ralph/ui/static/ui/css/rack.less index e4283f1761..3f3db804b2 100644 --- a/src/ralph/ui/static/ui/css/rack.less +++ b/src/ralph/ui/static/ui/css/rack.less @@ -222,7 +222,7 @@ } .info { position: relative; - .model { + .asset-description { top:0; left:0; position: absolute; @@ -262,7 +262,7 @@ } .generate-u(@max-u); &.height-u-0 .sn, &.height-u-1 .sn {display: none;} - &.position-u-1 .info .barcode, &.position-u-1 .info .model {top:0;} + &.position-u-1 .info .barcode, &.position-u-1 .info .asset-description {top:0;} .info { display: block; color: #333; @@ -271,7 +271,7 @@ font-size: .825em; position: relative; min-height: 100%; - .barcode, .model, .sn {position: absolute;} + .barcode, .asset-description, .sn {position: absolute;} .barcode { right: @info-padding; bottom: @info-padding; @@ -280,7 +280,7 @@ left: @info-padding; bottom: @info-padding; } - .model { + .asset-description { color: #333; font-weight: bold; left: @info-padding; From c21a6e7c00cf57e4b69fd1c7b6cd1912827fd11f Mon Sep 17 00:00:00 2001 From: Marcin Kliks Date: Mon, 2 Mar 2015 12:00:04 +0100 Subject: [PATCH 166/189] Minimal responsive design for Ralph base ui. I provide some minimal support for mobile app to be developed further. --- src/ralph/ui/templates/ui/base.html | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/ralph/ui/templates/ui/base.html b/src/ralph/ui/templates/ui/base.html index 7f5b68207c..ac0ea28502 100644 --- a/src/ralph/ui/templates/ui/base.html +++ b/src/ralph/ui/templates/ui/base.html @@ -4,9 +4,11 @@ + Ralph | {% block title %}{% block titlesection %}{{ active_menu.module.label }}{% block titlesubsection %}{% endblock %} - {{ active_submodule|title }}{% endblock %}{% endblock %} {% block styles %} + @@ -39,7 +41,7 @@ {% include 'ui/menu_extras.html' %}
-
+
{% block header %} {% main_menu_bs3fake main_menu active_module position='fixed' white="true" title='Ralph 2' %} {% endblock %} {{ view }} @@ -51,7 +53,7 @@ {% endif %}
{% block base_sidebar %} -
- + diff --git a/src/ralph/ui/static/ui/css/rack.css b/src/ralph/ui/static/ui/css/rack.css index bfff92d12a..44e34ad016 100644 --- a/src/ralph/ui/static/ui/css/rack.css +++ b/src/ralph/ui/static/ui/css/rack.css @@ -758,7 +758,8 @@ } .racks .rack .wrapper .devices .device .info .barcode, .racks .rack .wrapper .devices .device .info .asset-description, -.racks .rack .wrapper .devices .device .info .sn { +.racks .rack .wrapper .devices .device .info .sn, +.racks .rack .wrapper .devices .device .info .accessory-remarks { position: absolute; } .racks .rack .wrapper .devices .device .info .barcode { @@ -775,6 +776,14 @@ left: 3px; top: 3px; } +.racks .rack .wrapper .devices .device .info .accessory-remarks { + right: 3px; + bottom: 3px; + color: #333; + font-weight: bold; + background-color: #fbf7aa; + padding: 1px 5px; +} .racks .rack .wrapper .devices .device .children { position: absolute; top: 25px; diff --git a/src/ralph/ui/static/ui/css/rack.less b/src/ralph/ui/static/ui/css/rack.less index 3f3db804b2..99f98e56da 100644 --- a/src/ralph/ui/static/ui/css/rack.less +++ b/src/ralph/ui/static/ui/css/rack.less @@ -271,7 +271,7 @@ font-size: .825em; position: relative; min-height: 100%; - .barcode, .asset-description, .sn {position: absolute;} + .barcode, .asset-description, .sn, .accessory-remarks {position: absolute;} .barcode { right: @info-padding; bottom: @info-padding; @@ -286,6 +286,14 @@ left: @info-padding; top:@info-padding; } + .accessory-remarks { + right: @info-padding; + bottom: @info-padding; + color: #333; + font-weight: bold; + background-color: #fbf7aa; + padding: 1px 5px; + } } .children { position: absolute; diff --git a/src/ralph/ui/static/ui/js/app/rack/controllers.js b/src/ralph/ui/static/ui/js/app/rack/controllers.js index 6131018e3e..f4b69a8650 100644 --- a/src/ralph/ui/static/ui/js/app/rack/controllers.js +++ b/src/ralph/ui/static/ui/js/app/rack/controllers.js @@ -19,7 +19,10 @@ angular $scope.rack = RackModel.get({rackId: rackId}); $scope.$on('change_active_item', function (event, item) { $scope.activeItem = item; - if (item.children.indexOf($scope.activeSlot) === -1) { + if ( + typeof(item.children) === 'undefined' || + item.children.indexOf($scope.activeSlot) === -1 + ) { $scope.activeSlot = null; } }); diff --git a/src/ralph/ui/static/ui/js/app/rack/directives.js b/src/ralph/ui/static/ui/js/app/rack/directives.js index d82ffa6695..b552ff9dd1 100644 --- a/src/ralph/ui/static/ui/js/app/rack/directives.js +++ b/src/ralph/ui/static/ui/js/app/rack/directives.js @@ -37,7 +37,11 @@ angular $scope.$on('change_active_item', function(event, item){ $scope.u_range = []; if (typeof item !== 'undefined' && item !== null) { - for (var i = item.position; i <= item.position+item.height-1; i++) { + var itemHeight = 1; // for accessories... + if (typeof(item.height) !== 'undefined') { + itemHeight = item.height; + } + for (var i = item.position; i <= item.position+itemHeight-1; i++) { $scope.u_range.push(i); } } From 4e2f27057346fa331fd43df345c0ca3b0738dd51 Mon Sep 17 00:00:00 2001 From: Slawomir Sawicki Date: Tue, 10 Mar 2015 10:42:28 +0100 Subject: [PATCH 169/189] Fixed 500 status response when bulkedit in core --- src/ralph/ui/views/common.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/ralph/ui/views/common.py b/src/ralph/ui/views/common.py index 15997641f5..a8c25c77cd 100644 --- a/src/ralph/ui/views/common.py +++ b/src/ralph/ui/views/common.py @@ -34,6 +34,7 @@ from bob.data_table import DataTableColumn, DataTableMixin from powerdns.models import Record +from ralph.discovery.models import ServiceCatalog, DeviceEnvironment from ralph.discovery.models_component import Ethernet from ralph.account.models import Perm, get_user_home_page_url, ralph_permission from ralph.app import RalphModule @@ -1343,6 +1344,14 @@ def bulk_update(devices, fields, data, user): else: # for checkboxes un-checking values[name] = data.get(name, False) + if 'service' in fields: + values['service'] = ServiceCatalog.objects.get( + id=int(values['service']) + ) + if 'device_environment' in fields: + values['device_environment'] = DeviceEnvironment.objects.get( + id=int(values['device_environment']) + ) for device in devices: if 'venture' in fields: device.venture_role = None From bec1abf0d44732560b1f36098fcad0ad0cd71069 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Py=C5=BCalski?= Date: Tue, 3 Mar 2015 14:22:56 +0000 Subject: [PATCH 170/189] Validation for IPWithHostnameField The IPWithHostname field should always require IP Address, or else the IPAddress object creation will fail. Also the 'Management IP' form was corrected to use bob and to be displayed when it's invalid. --- src/ralph/ui/forms/addresses.py | 5 +++++ .../ui/templates/ui/device_addresses.html | 2 +- src/ralph/ui/tests/unit/test_fields.py | 20 +++++++++++++++++++ src/ralph/ui/views/common.py | 7 ++++--- 4 files changed, 30 insertions(+), 4 deletions(-) create mode 100644 src/ralph/ui/tests/unit/test_fields.py diff --git a/src/ralph/ui/forms/addresses.py b/src/ralph/ui/forms/addresses.py index 9b9431a508..d918238b34 100644 --- a/src/ralph/ui/forms/addresses.py +++ b/src/ralph/ui/forms/addresses.py @@ -8,6 +8,7 @@ from bob.forms import AutocompleteWidget from django import forms +from django.core.exceptions import ValidationError from django.template import Context from django.template.loader import get_template from django.utils.translation import ugettext_lazy as _ @@ -388,6 +389,10 @@ def __init__(self, *args, **kwargs): widget = IPWithHostWidget() def compress(self, value): + if value: + hostname, ip_number = value + if not ip_number: + raise ValidationError(_('IP Address is required')) return value diff --git a/src/ralph/ui/templates/ui/device_addresses.html b/src/ralph/ui/templates/ui/device_addresses.html index a3894a5526..c16d8a1777 100644 --- a/src/ralph/ui/templates/ui/device_addresses.html +++ b/src/ralph/ui/templates/ui/device_addresses.html @@ -114,7 +114,7 @@

Management address

{% csrf_token %} - {{ ip_management_form }} + {% form ip_management_form %} {% if canedit %}
diff --git a/src/ralph/ui/tests/unit/test_fields.py b/src/ralph/ui/tests/unit/test_fields.py new file mode 100644 index 0000000000..7df1e5df7a --- /dev/null +++ b/src/ralph/ui/tests/unit/test_fields.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +"""Tests for special fields.""" + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals + +from django.forms import ValidationError +from django.test import TestCase + +from ralph.ui.forms.addresses import IPWithHostField + + +class TestIPWithHostnameField(TestCase): + """Tests for ``IPWithHostField``""" + + def test_field_invalid_when_ip_number_missing(self): + with self.assertRaises(ValidationError): + IPWithHostField().compress(['example.com', '']) diff --git a/src/ralph/ui/views/common.py b/src/ralph/ui/views/common.py index 15997641f5..10c93fc566 100644 --- a/src/ralph/ui/views/common.py +++ b/src/ralph/ui/views/common.py @@ -941,9 +941,10 @@ def post(self, *args, **kwargs): else: messages.error(self.request, "Errors in the addresses form.") elif 'management' in self.request.POST: - form = IPManagementForm(self.request.POST) - if form.is_valid(): - self.object.management_ip = form.cleaned_data['management_ip'] + self.ip_management_form = IPManagementForm(self.request.POST) + if self.ip_management_form.is_valid(): + self.object.management_ip =\ + self.ip_management_form.cleaned_data['management_ip'] self.object.save() messages.success( self.request, "Management IP address updated." From d77ddf7b18bc290d74367d03519c013f7965f787 Mon Sep 17 00:00:00 2001 From: Slawomir Sawicki Date: Thu, 12 Mar 2015 09:33:31 +0100 Subject: [PATCH 171/189] Restore pagination on CMDB changes subpage --- src/ralph/cmdb/templates/cmdb/report_changes.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ralph/cmdb/templates/cmdb/report_changes.html b/src/ralph/cmdb/templates/cmdb/report_changes.html index 49bdbd9f57..96b5992ca8 100644 --- a/src/ralph/cmdb/templates/cmdb/report_changes.html +++ b/src/ralph/cmdb/templates/cmdb/report_changes.html @@ -52,5 +52,5 @@

{{ title }}

Service Catalog{{ activeSlot ? activeSlot.service : '-' }}
Hostname{{ activeSlot ? activeSlot.hostname : '-' }}
Model {{ activeSlot ? activeSlot.model : '-' }}
Remarks{{ activeItem ? activeItem.remarks : '-' }} + {{ activeItem ? activeItem.remarks : '-' }} + Edit +
Links
- {% pagination page url_query=url_query show_all=0 show_csv=0 fugue_icons=1 %} + {% pagination bob_page url_query=url_query show_all=0 show_csv=0 fugue_icons=1 %} {% endblock %} From a34d4477d8c24dbde26acef9b7774f775b18e078 Mon Sep 17 00:00:00 2001 From: Slawomir Sawicki Date: Tue, 17 Mar 2015 10:34:38 +0100 Subject: [PATCH 172/189] Fix install python-graph --- Makefile | 1 + 1 file changed, 1 insertion(+) diff --git a/Makefile b/Makefile index 7773179527..fed6b8b989 100644 --- a/Makefile +++ b/Makefile @@ -8,6 +8,7 @@ runserver: ralph runserver install: + pip install https://github.com/chapmanb/python-graph/releases/download/1.8.2/python-graph-core-1.8.2.tar.gz pip install -e . --use-mirrors --allow-all-external --allow-unverified ipaddr --allow-unverified postmarkup --allow-unverified python-graph-core --allow-unverified pysphere test-unittests: From edaf4d95f21db14ef60f7ad2be9576e5c0aace33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Py=C5=BCalski?= Date: Tue, 17 Mar 2015 10:56:42 +0000 Subject: [PATCH 173/189] Various tweaks for visualization * The racks get narrower on narrow monitors * The hover/activation is uniformly done with a white border * The borders in info table are corrected (removed extra border under last element. --- .../ui/static/partials/data_center/rack.html | 4 +- .../partials/rack/device_types/asset.html | 2 +- .../ui/static/partials/rack/rack_view.html | 8 +- src/ralph/ui/static/ui/css/data_center.css | 4 + src/ralph/ui/static/ui/css/data_center.less | 3 + src/ralph/ui/static/ui/css/rack.css | 78 ++++++++++----- src/ralph/ui/static/ui/css/rack.less | 97 ++++++++++++++----- src/ralph/ui/static/ui/css/variables.less | 9 ++ .../ui/js/app/data_center/controllers.js | 4 + .../ui/js/app/data_center/directives.js | 2 +- .../ui/static/ui/js/app/rack/directives.js | 1 + 11 files changed, 160 insertions(+), 52 deletions(-) diff --git a/src/ralph/ui/static/partials/data_center/rack.html b/src/ralph/ui/static/partials/data_center/rack.html index 1dd87b998b..e666280116 100644 --- a/src/ralph/ui/static/partials/data_center/rack.html +++ b/src/ralph/ui/static/partials/data_center/rack.html @@ -1,6 +1,6 @@ -
Links - Show in assets
- Show in core + Asset information
+ Configuration information
-
Links - Show in assets
- Show in core + Asset information
+ Configuration information
-
- - + +
diff --git a/src/ralph/ui/static/ui/css/data_center.css b/src/ralph/ui/static/ui/css/data_center.css index 9aaead208f..2b99dcdaeb 100644 --- a/src/ralph/ui/static/ui/css/data_center.css +++ b/src/ralph/ui/static/ui/css/data_center.css @@ -972,5 +972,5 @@ } .data-center .grid .grid_wrapper .rack:hover, .data-center .grid .grid_wrapper .rack.active { - border: solid 1px white; + border: 1px solid white; } diff --git a/src/ralph/ui/static/ui/css/rack.css b/src/ralph/ui/static/ui/css/rack.css index 0e16da5a94..6787ca5f53 100644 --- a/src/ralph/ui/static/ui/css/rack.css +++ b/src/ralph/ui/static/ui/css/rack.css @@ -833,25 +833,50 @@ background: rgba(0, 0, 0, 0.3); padding: 2px; } -.racks .rack .wrapper .devices .device.height-u-3 .children [class*="slot-"] .slot_no { +.racks .rack.front .wrapper .devices .device.height-u-3 .children [class*="slot-"] .slot_no { display: inline-block; } -.racks .rack .wrapper .devices .device.rows-1.cols-2 [class*="slot-"] { +.racks .rack.front .wrapper .devices .device.rows-1.cols-2 [class*="slot-"] { width: 200px; } -.racks .rack .wrapper .devices .device.rows-2.cols-4 [class*="slot-"] { +.racks .rack.front .wrapper .devices .device.rows-2.cols-4 [class*="slot-"] { width: 100px; } -.racks .rack .wrapper .devices .device.rows-2.cols-6 [class*="slot-"] { +.racks .rack.front .wrapper .devices .device.rows-2.cols-6 [class*="slot-"] { width: 66.66666667px; } -.racks .rack .wrapper .devices .device.rows-2.cols-8 [class*="slot-"] { +.racks .rack.front .wrapper .devices .device.rows-2.cols-8 [class*="slot-"] { width: 50px; } -.racks .rack .wrapper .devices .device.rows-2.cols-8.half-slots [class$="A"], -.racks .rack .wrapper .devices .device.rows-2.cols-8.half-slots [class$="B"] { +.racks .rack.front .wrapper .devices .device.rows-2.cols-8.half-slots [class$="A"], +.racks .rack.front .wrapper .devices .device.rows-2.cols-8.half-slots [class$="B"] { width: 25px; } +.racks .rack.front .wrapper .devices .device.rows-4.cols-2 [class*="slot-"] { + width: 200px; +} +.racks .rack.back .wrapper .devices .device.height-u-3 .children [class*="slot-"] .slot_no { + display: inline-block; +} +.racks .rack.back .wrapper .devices .device.rows-1.cols-2 [class*="slot-"] { + width: 150px; +} +.racks .rack.back .wrapper .devices .device.rows-2.cols-4 [class*="slot-"] { + width: 75px; +} +.racks .rack.back .wrapper .devices .device.rows-2.cols-6 [class*="slot-"] { + width: 50px; +} +.racks .rack.back .wrapper .devices .device.rows-2.cols-8 [class*="slot-"] { + width: 37.5px; +} +.racks .rack.back .wrapper .devices .device.rows-2.cols-8.half-slots [class$="A"], +.racks .rack.back .wrapper .devices .device.rows-2.cols-8.half-slots [class$="B"] { + width: 18.75px; +} +.racks .rack.back .wrapper .devices .device.rows-4.cols-2 [class*="slot-"] { + width: 150px; +} @media all and (max-width: 1500px) { .racks .rack { width: 350px; @@ -862,26 +887,54 @@ .racks .rack.front .wrapper .devices .device { width: 300px; } - .racks .rack .wrapper .devices { - width: 200px; - } - .racks .rack .wrapper .devices .device { - width: 200px; + .racks .rack.front .wrapper .devices .device.height-u-3 .children [class*="slot-"] .slot_no { + display: inline-block; } - .racks .rack .wrapper .devices .device.rows-1.cols-2 [class*="slot-"] { + .racks .rack.front .wrapper .devices .device.rows-1.cols-2 [class*="slot-"] { width: 150px; } - .racks .rack .wrapper .devices .device.rows-2.cols-4 [class*="slot-"] { + .racks .rack.front .wrapper .devices .device.rows-2.cols-4 [class*="slot-"] { width: 75px; } - .racks .rack .wrapper .devices .device.rows-2.cols-6 [class*="slot-"] { + .racks .rack.front .wrapper .devices .device.rows-2.cols-6 [class*="slot-"] { width: 50px; } - .racks .rack .wrapper .devices .device.rows-2.cols-8 [class*="slot-"] { + .racks .rack.front .wrapper .devices .device.rows-2.cols-8 [class*="slot-"] { width: 37.5px; } - .racks .rack .wrapper .devices .device.rows-2.cols-8.half-slots [class$="A"], - .racks .rack .wrapper .devices .device.rows-2.cols-8.half-slots [class$="B"] { + .racks .rack.front .wrapper .devices .device.rows-2.cols-8.half-slots [class$="A"], + .racks .rack.front .wrapper .devices .device.rows-2.cols-8.half-slots [class$="B"] { width: 18.75px; } + .racks .rack.front .wrapper .devices .device.rows-4.cols-2 [class*="slot-"] { + width: 150px; + } + .racks .rack.back .wrapper .devices { + width: 200px; + } + .racks .rack.back .wrapper .devices .device { + width: 200px; + } + .racks .rack.back .wrapper .devices .device.height-u-3 .children [class*="slot-"] .slot_no { + display: inline-block; + } + .racks .rack.back .wrapper .devices .device.rows-1.cols-2 [class*="slot-"] { + width: 100px; + } + .racks .rack.back .wrapper .devices .device.rows-2.cols-4 [class*="slot-"] { + width: 50px; + } + .racks .rack.back .wrapper .devices .device.rows-2.cols-6 [class*="slot-"] { + width: 33.33333333px; + } + .racks .rack.back .wrapper .devices .device.rows-2.cols-8 [class*="slot-"] { + width: 25px; + } + .racks .rack.back .wrapper .devices .device.rows-2.cols-8.half-slots [class$="A"], + .racks .rack.back .wrapper .devices .device.rows-2.cols-8.half-slots [class$="B"] { + width: 12.5px; + } + .racks .rack.back .wrapper .devices .device.rows-4.cols-2 [class*="slot-"] { + width: 100px; + } } diff --git a/src/ralph/ui/static/ui/css/rack.less b/src/ralph/ui/static/ui/css/rack.less index d7695cfc86..9e8338f22e 100644 --- a/src/ralph/ui/static/ui/css/rack.less +++ b/src/ralph/ui/static/ui/css/rack.less @@ -361,7 +361,7 @@ } // Layouts -.racks .rack .wrapper .devices .device { +.generate-layouts(@device-width) { &.height-u-3 .children { [class*="slot-"] { .slot_no {display: inline-block;} @@ -369,63 +369,58 @@ } &.rows-1.cols-2 { @cols: 2; - [class*="slot-"] {width: @devices-width-front / @cols;} + [class*="slot-"] {width: @device-width / @cols;} } &.rows-2.cols-4 { @cols: 4; - [class*="slot-"] {width: @devices-width-front / @cols;} + [class*="slot-"] {width: @device-width / @cols;} } &.rows-2.cols-6 { @cols: 6; - [class*="slot-"] {width: @devices-width-front / @cols;} + [class*="slot-"] {width: @device-width / @cols;} } &.rows-2.cols-8 { @cols: 8; - [class*="slot-"] {width: @devices-width-front / @cols;} + [class*="slot-"] {width: @device-width / @cols;} &.half-slots { [class$="A"], [class$="B"] { - width: @devices-width-front / (@cols * 2); + width: @device-width / (@cols * 2); } } } + &.rows-4.cols-2 { + @cols: 2; + [class*="slot-"] {width: @device-width / @cols;} + } +} + +.racks .rack.front .wrapper .devices .device { + .generate-layouts(@devices-width-front); +} +.racks .rack.back .wrapper .devices .device { + .generate-layouts(@devices-width-back); } @media all and (max-width: 1500px) { .racks { .rack { - &.front .wrapper .devices { - width: @devices-width-front-small; - .device {width: @devices-width-front-small;} - } width: @rack-width-small; - .wrapper { + &.front .wrapper { + .devices { + width: @devices-width-front-small; + .device { + width: @devices-width-front-small; + .generate-layouts(@devices-width-front-small) + } + } + } + &.back .wrapper { .devices { width: @devices-width-back-small; .device { width: @devices-width-back-small; - &.rows-1.cols-2 { - @cols: 2; - [class*="slot-"] {width: @devices-width-front-small / @cols;} - } - &.rows-2.cols-4 { - @cols: 4; - [class*="slot-"] {width: @devices-width-front-small / @cols;} - } - &.rows-2.cols-6 { - @cols: 6; - [class*="slot-"] {width: @devices-width-front-small / @cols;} - } - &.rows-2.cols-8 { - @cols: 8; - [class*="slot-"] {width: @devices-width-front-small / @cols;} - &.half-slots { - [class$="A"], - [class$="B"] { - width: @devices-width-front-small / (@cols * 2); - } - } - } + .generate-layouts(@devices-width-back-small) } } } diff --git a/src/ralph/ui/static/ui/js/app/rack/directives.js b/src/ralph/ui/static/ui/js/app/rack/directives.js index 36a4978128..99f56e9ed0 100644 --- a/src/ralph/ui/static/ui/js/app/rack/directives.js +++ b/src/ralph/ui/static/ui/js/app/rack/directives.js @@ -14,18 +14,25 @@ angular templateUrl: '/static/partials/rack/rack.html', }; }) - .directive('device', function () { + .directive('deviceItem', function () { return { restrict: 'E', - templateUrl: '/static/partials/rack/device.html', - controller: function($scope) { - $scope.setActiveItem = function(item) { + scope: { + device: '=', + side: '=' + }, + templateUrl: '/static/partials/rack/device_item.html', + link: function(scope) { + scope.setActiveItem = function(item) { item.active = true; - $scope.$emit('change_active_item', item); + scope.$emit('change_active_item', item); }; - $scope.setActiveSlot = function(slot) { - $scope.$emit('change_active_slot', slot); + scope.setActiveSlot = function(slot) { + scope.$emit('change_active_slot', slot); }; + scope.getLayout = function() { + return scope.device[scope.side + '_layout'] + } } }; }) @@ -33,17 +40,17 @@ angular return { restrict: 'E', templateUrl: '/static/partials/rack/listing.html', - link: function ($scope) { - $scope.u_range = []; - $scope.$on('change_active_item', function(event, item){ - $scope.u_range = []; + link: function (scope) { + scope.u_range = []; + scope.$on('change_active_item', function(event, item){ + scope.u_range = []; if (typeof item !== 'undefined' && item !== null) { var itemHeight = 1; // for accessories... if (typeof(item.height) !== 'undefined') { itemHeight = item.height; } for (var i = item.position; i <= item.position+itemHeight-1; i++) { - $scope.u_range.push(i); + scope.u_range.push(i); } } }); @@ -56,14 +63,13 @@ angular scope: { pdu: '=' }, - require: '^rack', templateUrl: '/static/partials/rack/pdu_item.html', - controller: function($scope) { - $scope.setActiveItem = function(item) { - $scope.$emit('change_active_item', item); + link: function(scope) { + scope.setActiveItem = function(item) { + scope.$emit('change_active_item', item); }; - $scope.setActiveSlot = function(slot) { - $scope.$emit('change_active_slot', slot); + scope.setActiveSlot = function(slot) { + scope.$emit('change_active_slot', slot); }; } }; From e70d62041e534dc5f1ee988989368b2025973226 Mon Sep 17 00:00:00 2001 From: Arkadiusz Adamski Date: Thu, 26 Mar 2015 11:19:53 +0100 Subject: [PATCH 180/189] changed function name to setActiveItem instead of setActiveDevice This mistake caused by refactoring (PR #1335). --- src/ralph/ui/static/partials/rack/device_types/asset.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ralph/ui/static/partials/rack/device_types/asset.html b/src/ralph/ui/static/partials/rack/device_types/asset.html index f00c22be67..4fa16b76a1 100644 --- a/src/ralph/ui/static/partials/rack/device_types/asset.html +++ b/src/ralph/ui/static/partials/rack/device_types/asset.html @@ -1,6 +1,6 @@
From bb70aa7108621f4a80438d4082293de3baca3436 Mon Sep 17 00:00:00 2001 From: Tymoteusz Jankowski Date: Thu, 26 Mar 2015 11:41:14 +0100 Subject: [PATCH 181/189] Fixed 'Management address' in form of Addresses tab. Removed redundant button in the form + added submit name to the form so that view can save the form (view checks for specific name in request to save it) --- src/ralph/ui/templates/ui/device_addresses.html | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/src/ralph/ui/templates/ui/device_addresses.html b/src/ralph/ui/templates/ui/device_addresses.html index c16d8a1777..ba34e4285a 100644 --- a/src/ralph/ui/templates/ui/device_addresses.html +++ b/src/ralph/ui/templates/ui/device_addresses.html @@ -111,20 +111,7 @@

DHCP Entries

Management address

- - {% csrf_token %} - - {% form ip_management_form %} - - {% if canedit %} -
- {% spaceless %} - - {% endspaceless %} -
- {% endif %} - + {% form ip_management_form fugue_icons='fugue-tick' submit_name='management' %}

Detected addresses

From 2ef6c0dbb25d23c61cd29e23a518d546718a86d0 Mon Sep 17 00:00:00 2001 From: Piotr Jarolewski Date: Wed, 25 Mar 2015 09:47:05 +0100 Subject: [PATCH 182/189] Change Ralph install docker documentation --- doc/installation.rst | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/doc/installation.rst b/doc/installation.rst index 82e38d63c9..3cfa934043 100644 --- a/doc/installation.rst +++ b/doc/installation.rst @@ -13,21 +13,15 @@ We decided to push new images from time to time when we decide it's stable enoug docker run -i -t -name mysql_data -v /var/lib/mysql -v /home/ralph/.ralph busybox /bin/sh -c "chown default /home/ralph; chown default /home/ralph/.ralph" -3. Initialize config file and empty mysql database with default login and password:: +3. Initialize config file and empty mysql database with default login and password and collect static files:: - docker run -u ralph -P -t -i -volumes-from mysql_data vi4m/ralph:latest /home/ralph/bin/ralph makeconf - docker run -P -t -i -volumes-from mysql_data vi4m/ralph:latest /bin/bash /home/ralph/init_mysql.sh + docker run -P -t -i -volumes-from mysql_data allegrogroup/ralph:latest /bin/bash /home/ralph/init.sh -4. Collect static files:: +4. Now, run ralph:: - docker run -P -t -i -volumes-from mysql_data vi4m/ralph:latest /home/ralph/bin/ralph collectstatic + docker run -P -p 8000:8000 -t -i -volumes-from mysql_data allegrogroup/ralph:latest -5. Now, run ralph:: - - docker run -P -t -i -volumes-from mysql_data vi4m/ralph:latest - -6. Check your instance port by typing ``docker ps -l``, for example 53914, and point your browser to: ``http://YOUR_DOCKER_IP:53914``. -Log in using username ``ralph`` and password ``ralph``. That's all! For more information read Docker manuals. Enjoy! +5. Open your browser to: ``http://YOUR_DOCKER_IP:8000``. That's all! For more information read Docker manuals. Enjoy! Upgrading an existing installation From 28d3cf36762991b4cb823d704b597bfb5f22ebe7 Mon Sep 17 00:00:00 2001 From: quamilek Date: Thu, 26 Mar 2015 23:14:44 +0100 Subject: [PATCH 183/189] Remove workaround for installation python-graph-core Now python-graph-core package is hosted on https://pypi.python.org/ --- Makefile | 3 +-- doc/installation.rst | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index fed6b8b989..dec677dc9c 100644 --- a/Makefile +++ b/Makefile @@ -8,8 +8,7 @@ runserver: ralph runserver install: - pip install https://github.com/chapmanb/python-graph/releases/download/1.8.2/python-graph-core-1.8.2.tar.gz - pip install -e . --use-mirrors --allow-all-external --allow-unverified ipaddr --allow-unverified postmarkup --allow-unverified python-graph-core --allow-unverified pysphere + pip install -e . --use-mirrors --allow-all-external --allow-unverified ipaddr --allow-unverified postmarkup --allow-unverified pysphere test-unittests: DJANGO_SETTINGS_PROFILE=test-ralph coverage run --source=ralph --omit='*migrations*,*tests*,*__init__*,*wsgi.py,*__main__*,*settings*,*manage.py' '$(VIRTUAL_ENV)/bin/ralph' test ralph diff --git a/doc/installation.rst b/doc/installation.rst index 3cfa934043..657225e079 100644 --- a/doc/installation.rst +++ b/doc/installation.rst @@ -321,7 +321,7 @@ If you have pip 1.3.x or 1.4.x use this command:: In case you have newer pip (1.5.x or newer) use slightly longer command:: - (ralph)$ pip install ralph --use-mirrors --allow-all-external --allow-unverified ipaddr --allow-unverified postmarkup --allow-unverified python-graph-core --allow-unverified pysphere + (ralph)$ pip install ralph --use-mirrors --allow-all-external --allow-unverified ipaddr --allow-unverified postmarkup --allow-unverified pysphere That's it. From 105ed5af871007ed321948669eddc87f79eb47f3 Mon Sep 17 00:00:00 2001 From: Piotr Jarolewski Date: Fri, 27 Mar 2015 11:03:03 +0100 Subject: [PATCH 184/189] Add mac address to ralph docker documentation --- doc/installation.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/installation.rst b/doc/installation.rst index 3cfa934043..b9d660fc07 100644 --- a/doc/installation.rst +++ b/doc/installation.rst @@ -15,11 +15,11 @@ We decided to push new images from time to time when we decide it's stable enoug 3. Initialize config file and empty mysql database with default login and password and collect static files:: - docker run -P -t -i -volumes-from mysql_data allegrogroup/ralph:latest /bin/bash /home/ralph/init.sh + docker run -P -t -i --volumes-from mysql_data allegrogroup/ralph:latest /bin/bash /home/ralph/init.sh 4. Now, run ralph:: - docker run -P -p 8000:8000 -t -i -volumes-from mysql_data allegrogroup/ralph:latest + docker run -P -p 8000:8000 -t -i --mac-address=02:42:ac:11:ff:ff --volumes-from mysql_data allegrogroup/ralph:latest 5. Open your browser to: ``http://YOUR_DOCKER_IP:8000``. That's all! For more information read Docker manuals. Enjoy! From b6d265a6124b9af0c85856990e7cd6a0076cfb95 Mon Sep 17 00:00:00 2001 From: Tymoteusz Jankowski Date: Fri, 27 Mar 2015 15:49:23 +0100 Subject: [PATCH 185/189] Truncate too long surnames when synchronising users with ldap. Before, ldap users with surnames longer then 30 (Django's default) chars break synching. Now such surnames are truncated to 30 chars. --- .../account/management/commands/ldap_sync.py | 12 +++++++ src/ralph/account/tests/test_models.py | 33 +++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/src/ralph/account/management/commands/ldap_sync.py b/src/ralph/account/management/commands/ldap_sync.py index 334571bcb9..5843924d3a 100644 --- a/src/ralph/account/management/commands/ldap_sync.py +++ b/src/ralph/account/management/commands/ldap_sync.py @@ -4,6 +4,7 @@ import logging from django.conf import settings +from django.contrib.auth.models import User from django.core.management.base import BaseCommand from ldap.controls import SimplePagedResultsControl @@ -22,6 +23,16 @@ ldap_module_exists = False +def _truncate_surname(ldap_dict): + """ + Truncate user's surname when it's longer then default django value, which is + 30 chars. + """ + if 'sn' in ldap_dict: + max_length = User._meta.get_field('last_name').max_length + ldap_dict['sn'] = [surname[:max_length] for surname in ldap_dict['sn']] + + class Command(BaseCommand): """Refresh info about users from ldap.""" @@ -128,6 +139,7 @@ def populate_users(self): """Load users from ldap and populate them. Returns number of users.""" synced = 0 for user_dn, ldap_dict in self._get_users(): + _truncate_surname(ldap_dict) self._create_or_update_user(user_dn, ldap_dict) synced += 1 return synced diff --git a/src/ralph/account/tests/test_models.py b/src/ralph/account/tests/test_models.py index 78d7e3ca0f..cc81381ddb 100644 --- a/src/ralph/account/tests/test_models.py +++ b/src/ralph/account/tests/test_models.py @@ -5,6 +5,8 @@ from __future__ import print_function from __future__ import unicode_literals +import unittest + from django.test import TestCase from django.conf import settings @@ -12,6 +14,13 @@ from ralph.account.tests import utils +try: + from ralph.account.management.commands.ldap_sync import _truncate_surname + NO_LDAP_MODULE = False +except ImportError: + NO_LDAP_MODULE = True + + class ModelsTest(TestCase): def test_getting_default_region(self): @@ -38,3 +47,27 @@ def test_if_region_is_granted_or_not(self): self.assertFalse(user_profile.is_region_granted(nld_region)) user_profile.region_set.add(nld_region) self.assertTrue(user_profile.is_region_granted(nld_region)) + + +@unittest.skipIf(NO_LDAP_MODULE, "'ldap' module is not installed") +class LdapSyncTest(TestCase): + + def test_long_surname_is_truncated(self): + too_long_surname = 'this-is-to-long-surname-so-it-should-be-truncated' + ldap_dict = {'sn': [too_long_surname]} + default_django_surname_length = 30 + _truncate_surname(ldap_dict) + self.assertEqual( + ldap_dict['sn'], + [too_long_surname[:default_django_surname_length]] + ) + + def test_short_surname_stays_unmodified(self): + short_surname = 'short-surname' + ldap_dict = {'sn': [short_surname]} + _truncate_surname(ldap_dict) + self.assertEqual(ldap_dict['sn'], [short_surname]) + + def test_truncate_works_when_no_sn(self): + ldap_dict = {} + _truncate_surname(ldap_dict) From 7d5508a31cdedff4149a1f4b738c32c2c920ed33 Mon Sep 17 00:00:00 2001 From: Tymoteusz Jankowski Date: Tue, 31 Mar 2015 15:17:04 +0200 Subject: [PATCH 186/189] Fixed highlighting elements on hover in visualization. Removed side borders, left only top and bottom borders - highlight is still visible but no flaw. --- src/ralph/ui/static/ui/css/rack.css | 7 +++---- src/ralph/ui/static/ui/css/rack.less | 8 +++----- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/src/ralph/ui/static/ui/css/rack.css b/src/ralph/ui/static/ui/css/rack.css index 6787ca5f53..6b41ce22a2 100644 --- a/src/ralph/ui/static/ui/css/rack.css +++ b/src/ralph/ui/static/ui/css/rack.css @@ -535,11 +535,10 @@ border-top: 1px solid #aae15d; background: #93d932; } -.racks .rack .wrapper .devices .device:hover { - border: 1px solid white; -} +.racks .rack .wrapper .devices .device:hover, .racks .rack .wrapper .devices .device.active { - border: 1px solid white; + border-bottom: 1px solid white; + border-top: 1px solid white; } .racks .rack .wrapper .devices .device.accessory { background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAK0lEQVQYV2NkIBIwEqmOAVnhfxyawGrQFaLbANI82BRi8w+GG/GGFNHhCAARkQoLdFrYLgAAAABJRU5ErkJggg==) repeat-x; diff --git a/src/ralph/ui/static/ui/css/rack.less b/src/ralph/ui/static/ui/css/rack.less index 9e8338f22e..5b1c560050 100644 --- a/src/ralph/ui/static/ui/css/rack.less +++ b/src/ralph/ui/static/ui/css/rack.less @@ -220,11 +220,9 @@ border-bottom: 1px solid darken(@device-background, 10%); border-top: 1px solid lighten(@device-background, 10%); background: @device-background; - &:hover { - .active-border(); - } - &.active { - .active-border(); + &:hover, &.active{ + border-bottom: 1px solid white; + border-top: 1px solid white; } &.accessory { background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAK0lEQVQYV2NkIBIwEqmOAVnhfxyawGrQFaLbANI82BRi8w+GG/GGFNHhCAARkQoLdFrYLgAAAABJRU5ErkJggg==) repeat-x; From 3e185130033b2c6662de596913ec7e1491616311 Mon Sep 17 00:00:00 2001 From: Mateusz Kurek Date: Thu, 19 Feb 2015 15:39:46 +0100 Subject: [PATCH 187/189] updated changelog to 2.2.0 --- CHANGES.rst | 62 +++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 48 insertions(+), 14 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 8ba04e23df..960f19d92b 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,24 +1,58 @@ Change Log ---------- -dev -~~~ - -Released on TBA - -* SCAN: puppet plugin tries to get data firstly from puppetdb-api -* SCAN: fix for assets saving - 2.2.0 ~~~~~ -Released on November 5, 2014 +Released on TBA -* New look for menu. -* CORE: Added new plugin - proxmox. -* CORE: Added new resource to API - PowerDNSRecord. -* CORE: Added support for regions (used in Assets Module). -* Bugfixes. +New features +************ +* Added data center and racks visualization. +* Ralph requires ralph_assets package. +* Improved Xen detection. +* Better ssh communication with cisco switches. +* Added possibility to store databases in Ralph and better support for storing load balancers virtual servers. +* Updated docs for Don Pedro agent and Ralph's installation process. +* We've moved documentation to the rtd: http://ralph.readthedocs.org/en/latest/. +* Docker is the official way to easy install Ralph. + + +Minor improvements +****************** +* SCAN: + - Detaching logical subdevices not found by scan: Previously only physical subdevices not found by scan were detached. Extended this feature on logical children. + - Detect Dell family in scan, when headers not contains `Server` + - Puppet-plugin: added expanduser call on cert paths. + - Puppet plugin can get data from puppetdb-api. + - 'Force autoscan' is now triggered only when SNMP name is missing. +* DEPLOY: + - Added two missing features to deploy/clean plugin: The hostname is now copied from deployment and copying venture/role from deployment. + - Changed the required permission for VM creation - You need core access to create a VM via API. + - Added new entry point to API to provide possibility to change IP address after deployment. +* GUI: + - Added management IP in 'Addresses' tab (device view). + - Scan form - now it's possible to add some new components to the existing device. +* CMDB: + - Set CI.state default value to ACTIVE. +* OTHER: + - The design of top bar is now in line with the Scrooge UI. + - Scrooge API: Databases and Load balancer virtual servers added. + - Added department to search form. + - Functionality to inject custom tracking code. + - Admin panel: changing management and management ip addresses blocked. + - `ralph makeconf` set pluggable apps to the reasonable defaults. + - For devices with assigned Asset (only without is_blade flag) it is no longer possible to change position. + - You can now set user's country using LDAP attribute. + - Added Powerdns Record resource to REST API. + +Fixes +***** +* Fix for MAC detecting in IDRAC plugin. +* Parent and management fields are now readonly when an asset is assigned. Before this change we could't save form. +* Updated parse_lshw(..) to work with puppetdb_api. Used better msg when no facts found. +* Fixed docker installation. +* Scan form now creates IPAddress object when does not exist. 2.1.0 ~~~~~ From 6830d5e29b920461d812a03cc6e407c970746192 Mon Sep 17 00:00:00 2001 From: Tymoteusz Jankowski Date: Mon, 13 Apr 2015 15:19:56 +0200 Subject: [PATCH 188/189] Updated changelog & bumped versions. --- CHANGES.rst | 4 ++-- setup.py | 6 +++--- src/ralph/__init__.py | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 960f19d92b..f677b26795 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,10 +1,10 @@ Change Log ---------- -2.2.0 +2.3.0 ~~~~~ -Released on TBA +Released on April 13, 2015 New features ************ diff --git a/setup.py b/setup.py index 35178c721a..b22fc5f76d 100644 --- a/setup.py +++ b/setup.py @@ -32,9 +32,9 @@ package_dir={'': 'src'}, zip_safe=False, # because templates are loaded from file path install_requires=[ - 'bob-ajax-selects==1.4.1', + 'bob-ajax-selects==1.6', 'djangorestframework==2.4.3', - 'django-bob==1.10.0', + 'django-bob==1.11.0', 'django-powerdns-dnssec==0.9.3', 'django-tastypie==0.9.16', 'django-rq==0.4.5', @@ -62,7 +62,7 @@ 'python-graph-core==1.8.2', 'pytz==2013.6', 'pyzabbix>=0.1', - 'ralph_assets==2.4.0', + 'ralph_assets==2.5.0', 'requests>=0.14.2', 'RestKit==4.2.0', 'rq>=0.3.7', diff --git a/src/ralph/__init__.py b/src/ralph/__init__.py index e69a3603c2..0c58e2fd9c 100644 --- a/src/ralph/__init__.py +++ b/src/ralph/__init__.py @@ -1 +1 @@ -VERSION = ('2', '2', '0') +VERSION = ('2', '3', '0') From cb508708fdef5b66e499f3e044cc3c930e27f3ec Mon Sep 17 00:00:00 2001 From: Tymoteusz Jankowski Date: Tue, 14 Apr 2015 12:16:32 +0200 Subject: [PATCH 189/189] Changed source branches (in travis file) from 'develop' to 'master' --- .travis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 6fb3cdd4d0..215fe9edd5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,9 +10,9 @@ python: - "2.7" # command to install dependencies, e.g. pip install -r requirements.txt --use-mirrors install: - - pip install git+https://github.com/allegro/django-bob@develop - - pip install git+https://github.com/quamilek/bob-ajax-selects.git@develop - - pip install git+https://github.com/allegro/ralph_assets.git@develop + - pip install git+https://github.com/allegro/django-bob@master + - pip install git+https://github.com/quamilek/bob-ajax-selects.git@master + - pip install git+https://github.com/allegro/ralph_assets.git@master - make install - pip install coveralls - pip install flake8