diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..374508c --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +*.img filter=lfs diff=lfs merge=lfs -text +*.qcow2 filter=lfs diff=lfs merge=lfs -text diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/.idea/dataSources.ids b/.idea/dataSources.ids new file mode 100644 index 0000000..58cd68d --- /dev/null +++ b/.idea/dataSources.ids @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/dataSources.local.xml b/.idea/dataSources.local.xml new file mode 100644 index 0000000..2515303 --- /dev/null +++ b/.idea/dataSources.local.xml @@ -0,0 +1,10 @@ + + + + + memory + true + magpaqnujuklfc + + + \ No newline at end of file diff --git a/.idea/dataSources.xml b/.idea/dataSources.xml new file mode 100644 index 0000000..b5a82e1 --- /dev/null +++ b/.idea/dataSources.xml @@ -0,0 +1,11 @@ + + + + + postgresql + true + org.postgresql.Driver + jdbc:postgresql://ec2-54-83-18-87.compute-1.amazonaws.com:5432/dcv14dl16ev288 + + + \ No newline at end of file diff --git a/.idea/dictionaries/abdul.xml b/.idea/dictionaries/abdul.xml new file mode 100644 index 0000000..2c3ab07 --- /dev/null +++ b/.idea/dictionaries/abdul.xml @@ -0,0 +1,9 @@ + + + + openstack + retreive + retriveing + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..71d7772 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,14 @@ + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..3b31283 --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index cd9d35b..44c30f0 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -11,4 +11,7 @@ + + \ No newline at end of file diff --git a/.idea/workspace.xml b/.idea/workspace.xml index 8a29066..709257b 100644 --- a/.idea/workspace.xml +++ b/.idea/workspace.xml @@ -2,13 +2,9 @@ - - - - - - - + + + @@ -21,9 +17,26 @@ + + + + + + @@ -33,61 +46,83 @@ - - + + + + + + + + + + + + - - + + - - + + + + + + + + + + + + + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + @@ -98,10 +133,10 @@ @@ -111,69 +146,71 @@ - - + + true + DEFINITION_ORDER - @@ -200,7 +237,6 @@ - @@ -209,6 +245,10 @@ + @@ -502,18 +763,63 @@ + + + + + + + + + + - - + + + + + + + + project @@ -617,6 +937,7 @@ + @@ -735,6 +1056,7 @@ - + + + + + + + + + + + + - @@ -766,6 +1098,7 @@ 1455703657914 - + - - - - - + + + + - - - - - - - - + + + + + + + + - - + + + + - - - - - - - - - - - - + + + - + + - - + + + + + + + + + + + @@ -841,395 +1176,380 @@ file:///usr/local/lib/python2.7/dist-packages/rest_framework/mixins.py 6 - - file://$PROJECT_DIR$/ntt_tool/static/js/apps/controllers/cloudcontroller.js - 25 - - + - - + + - + - - + + - - - - + - - + + - + - - + + - + - - + + - + - + - + - + - + - - + + - + - - + + - + - - + + - + - - + + - + - - + + - + - - + + - + - - + + - + - - + + - + - - + + + - + - - + + - + - - + + - + - - + + - + - - + + - + - - + + - + - - + + - + - - + + - - + + - + - - + + - + - - + + - + - - - + + - + - - - + + - + - - + + - + - - - + + - + - - - + + - + - - + + - + - - - + + - + - - + + - + - - - + + - + - - - + + - + - - - + + - + - - - + + - + - - - + + - + - - - + + - + - - - + + - + - - - + + - + - - - + + - + - - + + - + - - + + - + - - - + + + + + - + - - + + - + - - + + - + - - + + - + - - + + - + - - + + diff --git a/Makefile b/Makefile index a1c508a..72f168f 100644 --- a/Makefile +++ b/Makefile @@ -4,6 +4,12 @@ install: run: python manage.py runserver 0.0.0.0:8000 +makemigrations: + python manage.py makemigrations + +migrate: + python manage.py migrate + clear-cache: python manage.py clear_cache diff --git a/NTTT TESTCASES.xlsx b/NTTT TESTCASES.xlsx new file mode 100644 index 0000000..f9e90c2 Binary files /dev/null and b/NTTT TESTCASES.xlsx differ diff --git a/NTTT installation guide .docx b/NTTT installation guide .docx new file mode 100644 index 0000000..0a63d81 Binary files /dev/null and b/NTTT installation guide .docx differ diff --git a/NTTTDevelopersDocument.docx b/NTTTDevelopersDocument.docx new file mode 100644 index 0000000..18490f3 Binary files /dev/null and b/NTTTDevelopersDocument.docx differ diff --git a/NTTT_V0.2.docx b/NTTT_V0.2.docx new file mode 100644 index 0000000..492e41d Binary files /dev/null and b/NTTT_V0.2.docx differ diff --git a/ntt_tool/.gitignore b/ntt_tool/.gitignore new file mode 100644 index 0000000..e7c5e4b --- /dev/null +++ b/ntt_tool/.gitignore @@ -0,0 +1,132 @@ +# Created by .ignore support plugin (hsz.mobi) +### SublimeText template +# cache files for sublime text +*.tmlanguage.cache +*.tmPreferences.cache +*.stTheme.cache + +# workspace files are user-specific +*.sublime-workspace + +# project files should be checked into the repository, unless a significant +# proportion of contributors will probably not be using SublimeText +# *.sublime-project + +# sftp configuration file +sftp-config.json +### Python template +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*,cover + +# Translations +*.mo +*.pot + +# Django stuff: +*.log + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ +### VirtualEnv template +# Virtualenv +# http://iamzed.com/2009/05/07/a-primer-on-virtualenv/ +.Python +[Bb]in +[Ii]nclude +[Ll]ib +[Ss]cripts +pyvenv.cfg +pip-selfcheck.json +### JetBrains template +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio + +*.iml + +## Directory-based project format: +.idea/ +# if you remove the above rule, at least ignore the following: + +# User-specific stuff: +# .idea/workspace.xml +# .idea/tasks.xml +# .idea/dictionaries + +# Sensitive or high-churn files: +# .idea/dataSources.ids +# .idea/dataSources.xml +# .idea/sqlDataSources.xml +# .idea/dynamic.xml +# .idea/uiDesigner.xml + +# Gradle: +# .idea/gradle.xml +# .idea/libraries + +# Mongo Explorer plugin: +# .idea/mongoSettings.xml + +## File-based project format: +*.ipr +*.iws + +## Plugin-specific files: + +# IntelliJ +/out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties diff --git a/ntt_tool/apps/cloud/apps.py b/ntt_tool/apps/cloud/apps.py index f9da1d3..6f2b436 100644 --- a/ntt_tool/apps/cloud/apps.py +++ b/ntt_tool/apps/cloud/apps.py @@ -5,3 +5,4 @@ class CloudConfig(AppConfig): name = 'cloudtraffic' + diff --git a/ntt_tool/apps/cloud/migrations/0001_initial.py b/ntt_tool/apps/cloud/migrations/0001_initial.py index aebd03c..b54849d 100644 --- a/ntt_tool/apps/cloud/migrations/0001_initial.py +++ b/ntt_tool/apps/cloud/migrations/0001_initial.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Generated by Django 1.9.2 on 2016-02-25 15:52 +# Generated by Django 1.9.2 on 2016-04-27 07:22 from __future__ import unicode_literals from django.conf import settings @@ -27,13 +27,122 @@ class Migration(migrations.Migration): ('keystone_tenant_name', models.CharField(max_length=100)), ('created_on', models.DateTimeField(auto_now=True)), ('updated_on', models.DateTimeField(auto_now_add=True)), + ('creator', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), ], options={ - 'db_table': 'cloud_details', + 'db_table': 'clouds', }, ), migrations.CreateModel( - name='CloudTraffic', + name='Endpoint', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('endpoint_id', models.CharField(max_length=100)), + ('name', models.CharField(max_length=100)), + ('ip_address', models.CharField(max_length=30)), + ('status', models.CharField(max_length=30)), + ('is_selected', models.BooleanField(default=False)), + ('is_dirty', models.BooleanField(default=False)), + ], + options={ + 'db_table': 'endpoints', + }, + ), + migrations.CreateModel( + name='ICMPTestResults', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('src_tenant', models.CharField(max_length=500)), + ('dest_tenant', models.CharField(max_length=500)), + ('src_ep', models.TextField()), + ('dest_ep', models.TextField()), + ('status', models.CharField(max_length=20)), + ('rtt_min', models.FloatField()), + ('rtt_max', models.FloatField()), + ('rtt_avg', models.FloatField()), + ('packets_received', models.IntegerField()), + ('packets_transmitted', models.IntegerField()), + ('packet_loss_percent', models.FloatField()), + ], + options={ + 'db_table': 'icmp_test_results', + }, + ), + migrations.CreateModel( + name='Network', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('network_id', models.CharField(max_length=255)), + ('network_name', models.CharField(max_length=255)), + ('shared', models.BooleanField(default=False)), + ('status', models.CharField(max_length=25)), + ('is_selected', models.BooleanField(default=False)), + ('endpoint_count', models.IntegerField(default=True, null=True)), + ('is_dirty', models.BooleanField(default=False)), + ('created_on', models.DateTimeField(auto_now=True)), + ('updated_on', models.DateTimeField(auto_now_add=True)), + ('creator', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'db_table': 'networks', + }, + ), + migrations.CreateModel( + name='Subnet', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('subnet_id', models.CharField(max_length=255)), + ('subnet_name', models.CharField(max_length=255)), + ('cidr', models.CharField(max_length=255)), + ('allocation_pool_start', models.GenericIPAddressField(blank=True, null=True)), + ('ip_range_start', models.TextField(blank=True, max_length=15, null=True)), + ('ip_range_end', models.TextField(blank=True, max_length=15, null=True)), + ('allocation_pool_end', models.GenericIPAddressField(blank=True, null=True)), + ('is_dirty', models.BooleanField(default=False)), + ('network', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='subnets', to='cloud.Network')), + ], + options={ + 'db_table': 'subnets', + }, + ), + migrations.CreateModel( + name='TCPTestResults', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('src_tenant', models.CharField(max_length=500)), + ('dest_tenant', models.CharField(max_length=500)), + ('src_ep', models.TextField()), + ('dest_ep', models.TextField()), + ('status', models.CharField(max_length=20)), + ('retr', models.IntegerField()), + ('bandwidth', models.CharField(max_length=20)), + ('interval_time', models.CharField(max_length=20)), + ('transferred', models.CharField(max_length=20)), + ], + options={ + 'db_table': 'tcp_test_results', + }, + ), + migrations.CreateModel( + name='Tenant', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('tenant_id', models.CharField(max_length=100)), + ('tenant_name', models.CharField(max_length=256)), + ('description', models.CharField(blank=True, max_length=256, null=True)), + ('enabled', models.BooleanField(default=False)), + ('is_selected', models.BooleanField(default=False)), + ('is_dirty', models.BooleanField(default=False)), + ('created_on', models.DateTimeField(auto_now=True)), + ('updated_on', models.DateTimeField(auto_now_add=True)), + ('creator', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'db_table': 'tenants', + }, + ), + migrations.CreateModel( + name='Traffic', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('name', models.CharField(max_length=256)), @@ -44,29 +153,83 @@ class Migration(migrations.Migration): ('remote_pass', models.CharField(max_length=100)), ('test_method', models.CharField(max_length=100)), ('iperf_duration', models.IntegerField()), - ('tenant_type', models.CharField(choices=[('all', 'All'), ('intra-tenant', 'Intra Tenant'), ('inter-tenant', 'Inter Tenant'), ('south-north', 'South to North'), ('north-south', 'North to South')], default='all', max_length=20)), - ('step', models.IntegerField(default=1)), + ('test_type', models.CharField(choices=[('all', 'All'), ('intra-tenant', 'Intra Tenant'), ('inter-tenant', 'Inter Tenant'), ('south-north', 'South to North'), ('north-south', 'North to South')], default='all', max_length=20)), + ('external_host', models.CharField(blank=True, max_length=100, null=True)), + ('ssh_gateway', models.CharField(blank=True, max_length=100, null=True)), + ('test_environment', models.CharField(choices=[('dev', 'Development/Test'), ('prod', 'Production')], default='dev', max_length=20)), ('created_on', models.DateTimeField(auto_now=True)), ('updated_on', models.DateTimeField(auto_now_add=True)), - ('cloud', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cloud.Cloud')), + ('cloud', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='traffics', to='cloud.Cloud')), + ('creator', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), ], options={ - 'db_table': 'cloud_traffic', + 'db_table': 'traffics', }, ), migrations.CreateModel( - name='CloudTrafficTenants', + name='TrafficTestRun', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('tenant_name', models.CharField(max_length=256)), - ('tenant_value', models.CharField(blank=True, max_length=256, null=True)), - ('created_on', models.DateTimeField(auto_now=True)), - ('updated_on', models.DateTimeField(auto_now_add=True)), - ('cloud_traffic', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cloud.CloudTraffic')), - ('creator', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ('test_run_duration', models.IntegerField(default=1)), + ('started_on', models.DateTimeField(auto_now=True)), + ('status', models.CharField(choices=[('queued', 'Queued'), ('inprogress', 'In Progress'), ('completed', 'Completed'), ('stopped', 'Stopped'), ('error', 'Error')], default='queued', max_length=20)), + ('started_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ('traffic', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cloud.Traffic')), ], options={ - 'db_table': 'cloud_traffic_tenants', + 'db_table': 'traffic_test_run', }, ), + migrations.CreateModel( + name='UDPTestResults', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('src_tenant', models.CharField(max_length=500)), + ('dest_tenant', models.CharField(max_length=500)), + ('src_ep', models.TextField()), + ('dest_ep', models.TextField()), + ('status', models.CharField(max_length=20)), + ('jitter', models.CharField(max_length=20)), + ('bandwidth', models.CharField(max_length=20)), + ('bandwidth_loss_percent', models.CharField(max_length=20)), + ('interval_time', models.CharField(max_length=20)), + ('transferred', models.CharField(max_length=20)), + ('loss_datagram', models.IntegerField()), + ('total_datagram', models.IntegerField()), + ('traffic_test_run', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cloud.TrafficTestRun')), + ], + options={ + 'db_table': 'udp_test_results', + }, + ), + migrations.AddField( + model_name='tenant', + name='traffic', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='tenants', to='cloud.Traffic'), + ), + migrations.AddField( + model_name='tcptestresults', + name='traffic_test_run', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cloud.TrafficTestRun'), + ), + migrations.AddField( + model_name='network', + name='tenant', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='networks', to='cloud.Tenant'), + ), + migrations.AddField( + model_name='icmptestresults', + name='traffic_test_run', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cloud.TrafficTestRun'), + ), + migrations.AddField( + model_name='endpoint', + name='network', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cloud.Network'), + ), + migrations.AddField( + model_name='endpoint', + name='traffic', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cloud.Traffic'), + ), ] diff --git a/ntt_tool/apps/cloud/migrations/0002_auto_20160427_0922.py b/ntt_tool/apps/cloud/migrations/0002_auto_20160427_0922.py new file mode 100644 index 0000000..be4c2ee --- /dev/null +++ b/ntt_tool/apps/cloud/migrations/0002_auto_20160427_0922.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.2 on 2016-04-27 09:22 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('cloud', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='icmptestresults', + name='traffic_test_run', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='icmp_test_results', to='cloud.TrafficTestRun'), + ), + migrations.AlterField( + model_name='tcptestresults', + name='traffic_test_run', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tcp_test_results', to='cloud.TrafficTestRun'), + ), + migrations.AlterField( + model_name='udptestresults', + name='traffic_test_run', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='udp_test_results', to='cloud.TrafficTestRun'), + ), + ] diff --git a/ntt_tool/apps/cloud/migrations/0003_auto_20160225_1726.py b/ntt_tool/apps/cloud/migrations/0003_auto_20160225_1726.py deleted file mode 100644 index ae3334b..0000000 --- a/ntt_tool/apps/cloud/migrations/0003_auto_20160225_1726.py +++ /dev/null @@ -1,27 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.9.2 on 2016-02-25 17:26 -from __future__ import unicode_literals - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('cloud', '0002_auto_20160225_1631'), - ] - - operations = [ - migrations.AlterField( - model_name='cloudtraffictenants', - name='cloud_traffic', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='cloud_traffic_tenants', to='cloud.CloudTraffic'), - ), - migrations.AlterField( - model_name='cloudtraffictenants', - name='creator', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='cloud_traffic_tenants_creator', to=settings.AUTH_USER_MODEL), - ), - ] diff --git a/ntt_tool/apps/cloud/migrations/0003_auto_20160428_0721.py b/ntt_tool/apps/cloud/migrations/0003_auto_20160428_0721.py new file mode 100644 index 0000000..76781af --- /dev/null +++ b/ntt_tool/apps/cloud/migrations/0003_auto_20160428_0721.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.2 on 2016-04-28 07:21 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('cloud', '0002_auto_20160427_0922'), + ] + + operations = [ + migrations.RenameModel( + old_name='TrafficTestRun', + new_name='TestRun', + ), + ] diff --git a/ntt_tool/apps/cloud/migrations/0004_auto_20160226_1322.py b/ntt_tool/apps/cloud/migrations/0004_auto_20160226_1322.py deleted file mode 100644 index c09b75b..0000000 --- a/ntt_tool/apps/cloud/migrations/0004_auto_20160226_1322.py +++ /dev/null @@ -1,24 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.9.2 on 2016-02-26 13:22 -from __future__ import unicode_literals - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('cloud', '0003_auto_20160225_1726'), - ] - - operations = [ - migrations.RemoveField( - model_name='cloudtraffic', - name='step', - ), - migrations.AddField( - model_name='cloudtraffic', - name='external_host', - field=models.CharField(blank=True, max_length=100, null=True), - ), - ] diff --git a/ntt_tool/apps/cloud/migrations/0004_testrun_report_name.py b/ntt_tool/apps/cloud/migrations/0004_testrun_report_name.py new file mode 100644 index 0000000..bc92665 --- /dev/null +++ b/ntt_tool/apps/cloud/migrations/0004_testrun_report_name.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.2 on 2016-04-29 06:53 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('cloud', '0003_auto_20160428_0721'), + ] + + operations = [ + migrations.AddField( + model_name='testrun', + name='report_name', + field=models.CharField(default='report.csv', max_length=100), + preserve_default=False, + ), + ] diff --git a/ntt_tool/apps/cloud/migrations/0005_auto_20160429_0655.py b/ntt_tool/apps/cloud/migrations/0005_auto_20160429_0655.py new file mode 100644 index 0000000..01706f3 --- /dev/null +++ b/ntt_tool/apps/cloud/migrations/0005_auto_20160429_0655.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.2 on 2016-04-29 06:55 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('cloud', '0004_testrun_report_name'), + ] + + operations = [ + migrations.AlterField( + model_name='testrun', + name='report_name', + field=models.CharField(blank=True, max_length=100, null=True), + ), + ] diff --git a/ntt_tool/apps/cloud/migrations/0006_auto_20160503_0958.py b/ntt_tool/apps/cloud/migrations/0006_auto_20160503_0958.py new file mode 100644 index 0000000..c724244 --- /dev/null +++ b/ntt_tool/apps/cloud/migrations/0006_auto_20160503_0958.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.2 on 2016-05-03 09:58 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('cloud', '0005_auto_20160429_0655'), + ] + + operations = [ + migrations.AlterField( + model_name='traffic', + name='test_method', + field=models.CharField(blank=True, max_length=100, null=True), + ), + ] diff --git a/ntt_tool/apps/cloud/migrations/0007_auto_20160503_1002.py b/ntt_tool/apps/cloud/migrations/0007_auto_20160503_1002.py new file mode 100644 index 0000000..acf1f29 --- /dev/null +++ b/ntt_tool/apps/cloud/migrations/0007_auto_20160503_1002.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.2 on 2016-05-03 10:02 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('cloud', '0006_auto_20160503_0958'), + ] + + operations = [ + migrations.AlterField( + model_name='traffic', + name='test_method', + field=models.CharField(default='', max_length=100), + preserve_default=False, + ), + ] diff --git a/ntt_tool/apps/cloud/migrations/0008_testrun_completed_on.py b/ntt_tool/apps/cloud/migrations/0008_testrun_completed_on.py new file mode 100644 index 0000000..37014b9 --- /dev/null +++ b/ntt_tool/apps/cloud/migrations/0008_testrun_completed_on.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.2 on 2016-05-04 09:34 +from __future__ import unicode_literals + +import datetime +from django.db import migrations, models +from django.utils.timezone import utc + + +class Migration(migrations.Migration): + + dependencies = [ + ('cloud', '0007_auto_20160503_1002'), + ] + + operations = [ + migrations.AddField( + model_name='testrun', + name='completed_on', + field=models.DateTimeField(auto_now=True, default=datetime.datetime(2016, 5, 4, 9, 34, 2, 512239, tzinfo=utc)), + preserve_default=False, + ), + ] diff --git a/ntt_tool/apps/cloud/migrations/0009_traffic_udp_datagram_size.py b/ntt_tool/apps/cloud/migrations/0009_traffic_udp_datagram_size.py new file mode 100644 index 0000000..ed02e5f --- /dev/null +++ b/ntt_tool/apps/cloud/migrations/0009_traffic_udp_datagram_size.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.2 on 2016-05-04 11:05 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('cloud', '0008_testrun_completed_on'), + ] + + operations = [ + migrations.AddField( + model_name='traffic', + name='udp_datagram_size', + field=models.IntegerField(default=1), + ), + ] diff --git a/ntt_tool/apps/cloud/migrations/0010_auto_20160504_1126.py b/ntt_tool/apps/cloud/migrations/0010_auto_20160504_1126.py new file mode 100644 index 0000000..c580ce6 --- /dev/null +++ b/ntt_tool/apps/cloud/migrations/0010_auto_20160504_1126.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.2 on 2016-05-04 11:26 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('cloud', '0009_traffic_udp_datagram_size'), + ] + + operations = [ + migrations.AlterField( + model_name='udptestresults', + name='loss_datagram', + field=models.CharField(max_length=20), + ), + migrations.AlterField( + model_name='udptestresults', + name='total_datagram', + field=models.CharField(max_length=20), + ), + ] diff --git a/ntt_tool/apps/cloud/migrations/0011_auto_20160509_1144.py b/ntt_tool/apps/cloud/migrations/0011_auto_20160509_1144.py new file mode 100644 index 0000000..6b63724 --- /dev/null +++ b/ntt_tool/apps/cloud/migrations/0011_auto_20160509_1144.py @@ -0,0 +1,103 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.2 on 2016-05-09 11:44 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('cloud', '0010_auto_20160504_1126'), + ] + + operations = [ + migrations.RemoveField( + model_name='udptestresults', + name='bandwidth_loss_percent', + ), + migrations.RemoveField( + model_name='udptestresults', + name='interval_time', + ), + migrations.RemoveField( + model_name='udptestresults', + name='total_datagram', + ), + migrations.RemoveField( + model_name='udptestresults', + name='transferred', + ), + migrations.AddField( + model_name='udptestresults', + name='bytes_transferred', + field=models.IntegerField(default=0), + preserve_default=False, + ), + migrations.AddField( + model_name='udptestresults', + name='bytes_transferred_units', + field=models.CharField(default='bits', max_length=5), + ), + migrations.AddField( + model_name='udptestresults', + name='cpu_utilization_desc', + field=models.FloatField(default=0), + preserve_default=False, + ), + migrations.AddField( + model_name='udptestresults', + name='cpu_utilization_src', + field=models.FloatField(default=0), + preserve_default=False, + ), + migrations.AddField( + model_name='udptestresults', + name='datagrams', + field=models.IntegerField(default=0), + preserve_default=False, + ), + migrations.AddField( + model_name='udptestresults', + name='interval_time_end', + field=models.FloatField(default=0), + preserve_default=False, + ), + migrations.AddField( + model_name='udptestresults', + name='interval_time_start', + field=models.FloatField(default=0), + preserve_default=False, + ), + migrations.AddField( + model_name='udptestresults', + name='interval_time_units', + field=models.CharField(default='sec', max_length=5), + ), + migrations.AddField( + model_name='udptestresults', + name='jitter_units', + field=models.CharField(default='ms', max_length=5), + ), + migrations.AddField( + model_name='udptestresults', + name='loss_bandwidth', + field=models.FloatField(default=0), + preserve_default=False, + ), + migrations.AlterField( + model_name='udptestresults', + name='bandwidth', + field=models.FloatField(), + ), + migrations.AlterField( + model_name='udptestresults', + name='jitter', + field=models.FloatField(), + ), + migrations.AlterField( + model_name='udptestresults', + name='loss_datagram', + field=models.IntegerField(), + ), + ] diff --git a/ntt_tool/apps/cloud/migrations/0012_auto_20160509_1223.py b/ntt_tool/apps/cloud/migrations/0012_auto_20160509_1223.py new file mode 100644 index 0000000..ebc2644 --- /dev/null +++ b/ntt_tool/apps/cloud/migrations/0012_auto_20160509_1223.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.2 on 2016-05-09 12:23 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('cloud', '0011_auto_20160509_1144'), + ] + + operations = [ + migrations.RenameField( + model_name='udptestresults', + old_name='loss_bandwidth', + new_name='bandwidth_loss', + ), + ] diff --git a/ntt_tool/apps/cloud/migrations/0002_auto_20160225_1631.py b/ntt_tool/apps/cloud/migrations/0013_auto_20160509_1227.py similarity index 52% rename from ntt_tool/apps/cloud/migrations/0002_auto_20160225_1631.py rename to ntt_tool/apps/cloud/migrations/0013_auto_20160509_1227.py index b3fd221..79e113e 100644 --- a/ntt_tool/apps/cloud/migrations/0002_auto_20160225_1631.py +++ b/ntt_tool/apps/cloud/migrations/0013_auto_20160509_1227.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Generated by Django 1.9.2 on 2016-02-25 16:31 +# Generated by Django 1.9.2 on 2016-05-09 12:27 from __future__ import unicode_literals from django.db import migrations @@ -8,13 +8,13 @@ class Migration(migrations.Migration): dependencies = [ - ('cloud', '0001_initial'), + ('cloud', '0012_auto_20160509_1223'), ] operations = [ migrations.RenameField( - model_name='cloudtraffictenants', - old_name='tenant_value', - new_name='ssh_gateway', + model_name='udptestresults', + old_name='loss_datagram', + new_name='loss_datagrams', ), ] diff --git a/ntt_tool/apps/cloud/migrations/0014_auto_20160509_1236.py b/ntt_tool/apps/cloud/migrations/0014_auto_20160509_1236.py new file mode 100644 index 0000000..7132a8c --- /dev/null +++ b/ntt_tool/apps/cloud/migrations/0014_auto_20160509_1236.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.2 on 2016-05-09 12:36 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('cloud', '0013_auto_20160509_1227'), + ] + + operations = [ + migrations.RenameField( + model_name='udptestresults', + old_name='cpu_utilization_desc', + new_name='cpu_utilization_dest', + ), + ] diff --git a/ntt_tool/apps/cloud/migrations/0015_udptestresults_bandwidth_units.py b/ntt_tool/apps/cloud/migrations/0015_udptestresults_bandwidth_units.py new file mode 100644 index 0000000..4f766c4 --- /dev/null +++ b/ntt_tool/apps/cloud/migrations/0015_udptestresults_bandwidth_units.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.2 on 2016-05-09 12:46 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('cloud', '0014_auto_20160509_1236'), + ] + + operations = [ + migrations.AddField( + model_name='udptestresults', + name='bandwidth_units', + field=models.CharField(default='bits', max_length=5), + ), + ] diff --git a/ntt_tool/apps/cloud/migrations/0016_auto_20160510_0547.py b/ntt_tool/apps/cloud/migrations/0016_auto_20160510_0547.py new file mode 100644 index 0000000..7a243b2 --- /dev/null +++ b/ntt_tool/apps/cloud/migrations/0016_auto_20160510_0547.py @@ -0,0 +1,78 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.2 on 2016-05-10 05:47 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('cloud', '0015_udptestresults_bandwidth_units'), + ] + + operations = [ + migrations.RenameField( + model_name='tcptestresults', + old_name='retr', + new_name='bytes_transferred', + ), + migrations.RemoveField( + model_name='tcptestresults', + name='interval_time', + ), + migrations.RemoveField( + model_name='tcptestresults', + name='transferred', + ), + migrations.AddField( + model_name='tcptestresults', + name='bandwidth_units', + field=models.CharField(default='bits', max_length=5), + ), + migrations.AddField( + model_name='tcptestresults', + name='bytes_transferred_units', + field=models.CharField(default='bits', max_length=5), + ), + migrations.AddField( + model_name='tcptestresults', + name='cpu_utilization_dest', + field=models.FloatField(default=0), + preserve_default=False, + ), + migrations.AddField( + model_name='tcptestresults', + name='cpu_utilization_src', + field=models.FloatField(default=0), + preserve_default=False, + ), + migrations.AddField( + model_name='tcptestresults', + name='interval_time_end', + field=models.FloatField(default=0), + preserve_default=False, + ), + migrations.AddField( + model_name='tcptestresults', + name='interval_time_start', + field=models.FloatField(default=0), + preserve_default=False, + ), + migrations.AddField( + model_name='tcptestresults', + name='interval_time_units', + field=models.CharField(default='sec', max_length=5), + ), + migrations.AddField( + model_name='tcptestresults', + name='retransmits', + field=models.IntegerField(default=0), + preserve_default=False, + ), + migrations.AlterField( + model_name='tcptestresults', + name='bandwidth', + field=models.FloatField(), + ), + ] diff --git a/ntt_tool/apps/cloud/models.py b/ntt_tool/apps/cloud/models.py index 5835eb2..5272d30 100644 --- a/ntt_tool/apps/cloud/models.py +++ b/ntt_tool/apps/cloud/models.py @@ -9,15 +9,15 @@ class Cloud(models.Model): keystone_user = models.CharField(max_length=100) keystone_password = models.CharField(max_length=250) keystone_tenant_name = models.CharField(max_length=100) - # creator = models.ForeignKey(User) #ToDo: Facing some issues in serializer validation + creator = models.ForeignKey(User, blank=True, null=True) created_on = models.DateTimeField(auto_now=True) updated_on = models.DateTimeField(auto_now_add=True) class Meta: - db_table = "cloud_details" + db_table = "clouds" -class CloudTraffic(models.Model): +class Traffic(models.Model): TYPE_CHOICES = ( ('all', 'All'), ('intra-tenant', 'Intra Tenant'), @@ -25,33 +25,196 @@ class CloudTraffic(models.Model): ('south-north', 'South to North'), ('north-south', 'North to South'), ) - + TEST_ENVIRONMENT_CHOICES = ( + ('dev', 'Development/Test'), + ('prod', 'Production'), + ) + cloud = models.ForeignKey(Cloud, blank=True, null=True, related_name="traffics") name = models.CharField(max_length=256) - cloud = models.ForeignKey(Cloud) allowed_delta_percentage = models.FloatField() test_result_path = models.CharField(max_length=250) number_of_workers = models.IntegerField() remote_user = models.CharField(max_length=100) remote_pass = models.CharField(max_length=100) test_method = models.CharField(max_length=100) + udp_datagram_size = models.IntegerField(default=1) iperf_duration = models.IntegerField() - tenant_type = models.CharField(max_length=20, choices=TYPE_CHOICES, default='all') + test_type = models.CharField(max_length=20, choices=TYPE_CHOICES, default='all') external_host = models.CharField(max_length=100, blank=True, null=True) - # creator = models.ForeignKey(User) + ssh_gateway = models.CharField(max_length=100, blank=True, null=True) + test_environment = models.CharField(max_length=20, choices=TEST_ENVIRONMENT_CHOICES, default='dev') + creator = models.ForeignKey(User, blank=True, null=True) created_on = models.DateTimeField(auto_now=True) updated_on = models.DateTimeField(auto_now_add=True) + def __unicode__(self): + return "%s | %s | cloud:%s" % (self.id, self.name, self.cloud.name) + class Meta: - db_table = "cloud_traffic" + db_table = "traffics" -class CloudTrafficTenants(models.Model): - cloud_traffic = models.ForeignKey(CloudTraffic, related_name="cloud_traffic_tenants") +class Tenant(models.Model): + traffic = models.ForeignKey(Traffic, blank=True, null=True, related_name="tenants") + tenant_id = models.CharField(max_length=100) tenant_name = models.CharField(max_length=256) - ssh_gateway = models.CharField(max_length=256, blank=True, null=True) - creator = models.ForeignKey(User, related_name="cloud_traffic_tenants_creator") + description = models.CharField(max_length=256, blank=True, null=True) + enabled = models.BooleanField(default=False) + is_selected = models.BooleanField(default=False) + is_dirty = models.BooleanField(default=False) + creator = models.ForeignKey(User, blank=True, null=True) + created_on = models.DateTimeField(auto_now=True) + updated_on = models.DateTimeField(auto_now_add=True) + + def __unicode__(self): + return "%s | %s | traffic:%s" % (self.id, self.tenant_name, self.traffic.name) + + class Meta: + db_table = "tenants" + + +class Network(models.Model): + tenant = models.ForeignKey(Tenant, related_name="networks") + network_id = models.CharField(max_length=255) + network_name = models.CharField(max_length=255) + shared = models.BooleanField(default=False) + status = models.CharField(max_length=25) + is_selected = models.BooleanField(default=False) + endpoint_count = models.IntegerField(default=True, null=True) + is_dirty = models.BooleanField(default=False) + creator = models.ForeignKey(User, blank=True, null=True) created_on = models.DateTimeField(auto_now=True) updated_on = models.DateTimeField(auto_now_add=True) + def __unicode__(self): + return "%s | %s | tenant:%s" % (self.id, self.network_name, self.tenant.tenant_name) + + class Meta: + db_table = "networks" + + +class Subnet(models.Model): + network = models.ForeignKey(Network, related_name="subnets") + subnet_id = models.CharField(max_length=255) + subnet_name = models.CharField(max_length=255) + cidr = models.CharField(max_length=255) + allocation_pool_start = models.GenericIPAddressField(blank=True, null=True) + ip_range_start = models.TextField(max_length=15, blank=True, null=True) + ip_range_end = models.TextField(max_length=15, blank=True, null=True) + allocation_pool_end = models.GenericIPAddressField(blank=True, null=True) + is_dirty = models.BooleanField(default=False) + + class Meta: + db_table = "subnets" + + +class Endpoint(models.Model): + traffic = models.ForeignKey(Traffic) + network = models.ForeignKey(Network) + endpoint_id = models.CharField(max_length=100) + name = models.CharField(max_length=100) + ip_address = models.CharField(max_length=30) + status = models.CharField(max_length=30) + is_selected = models.BooleanField(default=False) + is_dirty = models.BooleanField(default=False) + + class Meta: + db_table = "endpoints" + + +class TestRun(models.Model): + TRAFFIC_TEST_RUN_STATUS = ( + ('queued', 'Queued'), + ('inprogress', 'In Progress'), + ('completed', 'Completed'), + ('stopped', 'Stopped'), + ('error', 'Error'), + ) + traffic = models.ForeignKey(Traffic) + report_name = models.CharField(max_length=100, blank=True, null=True) + test_run_duration = models.IntegerField(default=1) + started_on = models.DateTimeField(auto_now=True) + completed_on = models.DateTimeField(auto_now=True) + started_by = models.ForeignKey(User) + status = models.CharField(max_length=20, choices=TRAFFIC_TEST_RUN_STATUS, default='queued') + + def __unicode__(self): + return "%s | %s" % (self.id, self.traffic.name) + + class Meta: + db_table = "traffic_test_run" + + +class UDPTestResults(models.Model): + traffic_test_run = models.ForeignKey(TestRun, related_name="udp_test_results") + src_tenant = models.CharField(max_length=500) + dest_tenant = models.CharField(max_length=500) + src_ep = models.TextField() + dest_ep = models.TextField() + status = models.CharField(max_length=20) + jitter = models.FloatField() + jitter_units = models.CharField(max_length=5, default='ms') + bandwidth = models.FloatField() + bandwidth_loss = models.FloatField() + bandwidth_units = models.CharField(max_length=5, default='bits') + interval_time_start = models.FloatField() + interval_time_end = models.FloatField() + interval_time_units = models.CharField(max_length=5, default='sec') + bytes_transferred = models.IntegerField() + bytes_transferred_units = models.CharField(max_length=5, default='bits') + datagrams = models.IntegerField() + loss_datagrams = models.IntegerField() + cpu_utilization_src = models.FloatField() + cpu_utilization_dest = models.FloatField() + + def __unicode__(self): + return "%s | TestRun: %s" % (self.id, self.traffic_test_run.id) + + class Meta: + db_table = "udp_test_results" + + +class ICMPTestResults(models.Model): + traffic_test_run = models.ForeignKey(TestRun, related_name="icmp_test_results") + src_tenant = models.CharField(max_length=500) + dest_tenant = models.CharField(max_length=500) + src_ep = models.TextField() + dest_ep = models.TextField() + status = models.CharField(max_length=20) + rtt_min = models.FloatField() + rtt_max = models.FloatField() + rtt_avg = models.FloatField() + packets_received = models.IntegerField() + packets_transmitted = models.IntegerField() + packet_loss_percent = models.FloatField() + + def __unicode__(self): + return "%s | TestRun: %s" % (self.id, self.traffic_test_run.id) + + class Meta: + db_table = "icmp_test_results" + + +class TCPTestResults(models.Model): + traffic_test_run = models.ForeignKey(TestRun, related_name="tcp_test_results") + src_tenant = models.CharField(max_length=500) + dest_tenant = models.CharField(max_length=500) + src_ep = models.TextField() + dest_ep = models.TextField() + status = models.CharField(max_length=20) + retransmits = models.IntegerField() + bandwidth = models.FloatField() + bandwidth_units = models.CharField(max_length=5, default='bits') + interval_time_start = models.FloatField() + interval_time_end = models.FloatField() + interval_time_units = models.CharField(max_length=5, default='sec') + bytes_transferred = models.IntegerField() + bytes_transferred_units = models.CharField(max_length=5, default='bits') + cpu_utilization_src = models.FloatField() + cpu_utilization_dest = models.FloatField() + + def __unicode__(self): + return "%s | TestRun: %s" % (self.id, self.traffic_test_run.id) + class Meta: - db_table = "cloud_traffic_tenants" + db_table = "tcp_test_results" diff --git a/ntt_tool/apps/cloud/openstackscripts/__init__.py b/ntt_tool/apps/cloud/openstackscripts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ntt_tool/apps/cloud/openstackscripts/config.py b/ntt_tool/apps/cloud/openstackscripts/config.py new file mode 100644 index 0000000..e11cfb4 --- /dev/null +++ b/ntt_tool/apps/cloud/openstackscripts/config.py @@ -0,0 +1,27 @@ +import os +from django.conf import settings + +# NOVA CONFIGS +NOVA_FLOATING_IP_CREATION = True +NOVA_FLOATING_IP_POOL = 'public' + +NOVA_IMAGE_NAME = "trafficimage" + +NOVA_QUOTA_INSTANCES = -1 +NOVA_QUOTA_CORES = -1 +NOVA_QUOTA_RAM = -1 +NOVA_QUOTA_FIXED_IPS = -1 +NOVA_QUOTA_FLOATING_IPS = -1 + +NOVA_VM_FLAVOR = "traffic" +NOVA_VM_FLAVOR_RAM = 2048 # In Mega Bytes +NOVA_VM_FLAVOR_VCPUS = 1 +NOVA_VM_FLAVOR_DISK = 10 # In Giga Bytes + + +# GLANCE CONFIGS +GLANCE_IMAGE_NAME = NOVA_IMAGE_NAME +GLANCE_IMAGE_PATH = os.path.join(settings.MEDIA_ROOT, os.path.join("osimages", "ubuntu.img")) +GLANCE_IMAGE_DISK_FORMAT = "qcow2" +GLANCE_IMAGE_CONTAINER_FORMAT = "bare" + diff --git a/ntt_tool/apps/cloud/openstackscripts/credentials.py b/ntt_tool/apps/cloud/openstackscripts/credentials.py new file mode 100644 index 0000000..78bc8ba --- /dev/null +++ b/ntt_tool/apps/cloud/openstackscripts/credentials.py @@ -0,0 +1,49 @@ +from ntt_tool.apps.cloud.models import Cloud + + +def get_cloud_instance(cloud): + if not isinstance(cloud, Cloud): + return Cloud.objects.get(pk=cloud) + return cloud + + +def get_credentials(cloud): + try: + cloud = get_cloud_instance(cloud) + credentials = { + "username": cloud.keystone_user, + "password": cloud.keystone_password, + "auth_url": cloud.keystone_auth_url, + "tenant_name": cloud.keystone_tenant_name + } + return credentials + except Exception, e: + return {} + + +def get_nova_credentials(cloud): + try: + cloud = get_cloud_instance(cloud) + credentials = { + "username": cloud.keystone_user, + "api_key": cloud.keystone_password, + "auth_url": cloud.keystone_auth_url, + "project_id": cloud.keystone_tenant_name + } + return credentials + except Exception, e: + return {} + + +def get_nova_credentials_v3(cloud): + try: + cloud = get_cloud_instance(cloud) + credentials = { + "username": cloud.keystone_user, + "password": cloud.keystone_password, + "auth_url": cloud.keystone_auth_url, + "project_id": cloud.keystone_tenant_name, + } + return credentials + except Exception, e: + return {} \ No newline at end of file diff --git a/ntt_tool/apps/cloud/openstackscripts/endpoints.py b/ntt_tool/apps/cloud/openstackscripts/endpoints.py new file mode 100644 index 0000000..0a893bb --- /dev/null +++ b/ntt_tool/apps/cloud/openstackscripts/endpoints.py @@ -0,0 +1,88 @@ +import json +import time +from netaddr import IPRange +from django.db import transaction +from neutronclient.v2_0 import client as neutron_client +from novaclient.v1_1 import client as nova_client +from ntt_tool.apps.cloud.models import Network, Endpoint +from credentials import * +from novaclientutils import NovaClientUtils + + +class DiscoverEndpoints(object): + + def __init__(self, traffic, network): + self.traffic = traffic + self.network = network + self.neutron = neutron_client.Client(**get_credentials(self.traffic.cloud)) + self.nova = nova_client.Client(**get_nova_credentials(self.traffic.cloud)) + + def get_endpoints(self, subnet): + ip_range = IPRange(subnet.ip_range_start, subnet.ip_range_end) + ports = self.neutron.list_ports(network_id=self.network.network_id).get('ports') + endpoints = [] + with transaction.atomic(): + filters = { + "traffic_id": self.traffic.id, + "network_id": self.network.id, + } + Endpoint.objects.filter(**filters).update(is_dirty=False) + for port in ports: + if port.get("device_owner") in ["compute:compute", "compute:nova"]: + if port['fixed_ips'][0]['subnet_id'] == subnet.subnet_id: + endpoint = self.nova.servers.get(port['device_id'].encode('unicode_escape')) + if endpoint and endpoint.status == 'ACTIVE': + ip_address = port['fixed_ips'][0]['ip_address'].encode('unicode_escape') + if ip_address in ip_range: + endpoint_obj, created = Endpoint.objects.get_or_create(traffic_id=self.traffic.id, + network_id=self.network.id, + endpoint_id=endpoint.id) + endpoint_obj.name = endpoint.name + endpoint_obj.ip_address = ip_address + endpoint_obj.status = endpoint.status + endpoint_obj.is_dirty = True + endpoint_obj.save() + endpoints.append(endpoint_obj) + Endpoint.objects.filter(**filters).filter(is_dirty=False).delete() + return endpoints + + +class LaunchEndpoints(NovaClientUtils): + + def __init__(self, **credentials): + super(LaunchEndpoints, self).__init__(**credentials) + + def launch_endpoints(self, request, traffic): + endpoints = [] + with transaction.atomic(): + Network.objects.filter(tenant__traffic_id=traffic.id).update(is_selected=False) + for selected_network in json.loads(request.data.get("json", '[]')): + network = Network.objects.get(pk=selected_network.get("network_id")) + network.is_selected = True + network.endpoint_count = selected_network.get("endpoint_count") + network.save() + + endpoint_name = "-".join([network.tenant.tenant_name, + network.network_name, + time.strftime("%Y%m%d%H%M%S")]) + launched_endpoints = self.launch_endpoint(network.tenant.tenant_id, + network.network_id, + endpoint_name, + network.endpoint_count) + filters = { + "traffic_id": traffic.id, + "network_id": network.id, + } + Endpoint.objects.filter(**filters).update(is_dirty=False) + for endpoint in launched_endpoints: + endpoint_obj, created = Endpoint.objects.get_or_create(traffic_id=traffic.id, + network_id=network.id, + endpoint_id=endpoint.id) + endpoint_obj.name = endpoint.name + endpoint_obj.ip_address = endpoint.addresses.get(network.network_name)[0].get("addr") + endpoint_obj.status = endpoint.status + endpoint_obj.is_dirty = True + endpoint_obj.save() + endpoints.append(endpoint_obj) + Endpoint.objects.filter(**filters).filter(is_dirty=False).delete() + return endpoints diff --git a/ntt_tool/apps/cloud/openstackscripts/glanceclientutils.py b/ntt_tool/apps/cloud/openstackscripts/glanceclientutils.py new file mode 100644 index 0000000..0962340 --- /dev/null +++ b/ntt_tool/apps/cloud/openstackscripts/glanceclientutils.py @@ -0,0 +1,19 @@ +from glanceclient.v2 import client as glance_client +from openstackclientutils import OpenStackClientUtils +from config import * + + +class GlanceClientUtils(OpenStackClientUtils): + client_class = glance_client.Client + + def __init__(self, **credentials): + super(GlanceClientUtils, self).__init__(**credentials) + self.glance = self.get_client_instance() + + def upload_image(self): + image = self.glance.images.create( + disk_format=GLANCE_IMAGE_DISK_FORMAT, + container_format=GLANCE_IMAGE_CONTAINER_FORMAT, + name=GLANCE_IMAGE_NAME) + self.glance.images.upload(image.id, open(GLANCE_IMAGE_PATH, 'rb')) + return image diff --git a/ntt_tool/apps/cloud/openstackscripts/keystoneclientutils.py b/ntt_tool/apps/cloud/openstackscripts/keystoneclientutils.py new file mode 100644 index 0000000..5c7961b --- /dev/null +++ b/ntt_tool/apps/cloud/openstackscripts/keystoneclientutils.py @@ -0,0 +1,16 @@ +from keystoneclient.v2_0 import client as keystone_client +from openstackclientutils import OpenStackClientUtils + + +class KeystoneClientUtils(OpenStackClientUtils): + client_class = keystone_client.Client + + def __init__(self, **credentials): + super(KeystoneClientUtils, self).__init__(**credentials) + self.keystone = self.get_client_instance() + + def get_token(self): + return self.keystone.auth_ref.get("token", {}).get("id") + + def get_tenants(self): + return self.keystone.tenants.list() diff --git a/ntt_tool/apps/cloud/openstackscripts/neutronclientutils.py b/ntt_tool/apps/cloud/openstackscripts/neutronclientutils.py new file mode 100644 index 0000000..0d12a86 --- /dev/null +++ b/ntt_tool/apps/cloud/openstackscripts/neutronclientutils.py @@ -0,0 +1,19 @@ +from neutronclient.v2_0 import client as neutron_client +from openstackclientutils import OpenStackClientUtils + + +class NeutronClientUtils(OpenStackClientUtils): + client_class = neutron_client.Client + + def __init__(self, **credentials): + super(NeutronClientUtils, self).__init__(**credentials) + self.neutron = self.get_client_instance() + + def list_networks(self, **kwargs): + return self.neutron.list_networks(**kwargs) + + def list_subnets(self, **kwargs): + return self.neutron.list_subnets(**kwargs) + + def show_subnet(self, subnet_id): + return self.neutron.show_subnet(subnet_id).get("subnet") diff --git a/ntt_tool/apps/cloud/openstackscripts/novaclientutils.py b/ntt_tool/apps/cloud/openstackscripts/novaclientutils.py new file mode 100644 index 0000000..b76b73a --- /dev/null +++ b/ntt_tool/apps/cloud/openstackscripts/novaclientutils.py @@ -0,0 +1,104 @@ +import time +import os +from urlparse import urlparse +from novaclient.v1_1 import client as nova_client +from novaclient.exceptions import NotFound +from openstackclientutils import OpenStackClientUtils +from keystoneclientutils import KeystoneClientUtils +from glanceclientutils import GlanceClientUtils +from config import * + + +class NovaClientUtils(OpenStackClientUtils): + client_class = nova_client.Client + + def __init__(self, **credentials): + super(NovaClientUtils, self).__init__(**credentials) + self.nova = self.get_client_instance() + + def assign_floating_ip(self, instance): + if NOVA_FLOATING_IP_CREATION: + floating_ip = self.nova.floating_ips.create(NOVA_FLOATING_IP_POOL) + instance.add_floating_ip(floating_ip) + return True + return False + + def launch_endpoint(self, tenant_id, network_id, endpoint_name, min_count=1): + """ + Launches VM on given tenant and network + :param tenant_id: + :param network_id: + :param endpoint_name: + :return: VM instance + """ + self.nova.quotas.update(tenant_id, + instances=NOVA_QUOTA_INSTANCES, + cores=NOVA_QUOTA_CORES, + ram=NOVA_QUOTA_RAM, + fixed_ips=NOVA_QUOTA_FIXED_IPS, + floating_ips=NOVA_QUOTA_FLOATING_IPS) + + user_data = None + user_data_file_path = os.path.join(os.path.dirname(__file__), 'user.txt') + with open(user_data_file_path, 'r') as f: + user_data = f.read() + + image = None + try: + image = self.nova.images.find(name=NOVA_IMAGE_NAME) + except NotFound: + # Uploading image if does not exist. + keystone_credentials = { + "username": self.credentials.get("username"), + "password": self.credentials.get("api_key"), + "auth_url": self.credentials.get("auth_url"), + "tenant_name": self.credentials.get("project_id") + } + keystone = KeystoneClientUtils(**keystone_credentials) + url = urlparse(self.credentials.get("auth_url")) + glance_endpoint_url = "%s://%s:9292" % (url.scheme, url.hostname) + glance = GlanceClientUtils(endpoint=glance_endpoint_url, + token=keystone.get_token()) + image = glance.upload_image() + + # Creating flavor + flavor = None + try: + flavor = self.nova.flavors.find(name=NOVA_VM_FLAVOR) + except NotFound: + flavor = self.nova.flavors.create(name=NOVA_VM_FLAVOR, + ram=NOVA_VM_FLAVOR_RAM, + vcpus=NOVA_VM_FLAVOR_VCPUS, + disk=NOVA_VM_FLAVOR_DISK) + + # Launching endpoints + first_instance = self.nova.servers.create(name=endpoint_name, + image=image, + flavor=flavor, + key_name="admin", + nics=[{'net-id': network_id}], + userdata=user_data, + min_count=min_count, + availability_zone="compute:openstack-aio") + + instances = [] + # Todo: As of now nova does not returns all the instances which it launched. It is returning only first + # Todo: Refer openstack blueprint + if min_count > 1: + for endpoint_index in range(1, min_count+1): + instance_name = "%s-%d" % (first_instance.name.rsplit('-', 1)[0], endpoint_index) + instance = self.nova.servers.find(name=instance_name) + while instance.status == 'BUILD': + time.sleep(5) + instance = self.nova.servers.get(instance.id) + # Assigning floating IP to the instance + if self.assign_floating_ip(instance): + instances.append(instance) + + return instances + + + + + + diff --git a/ntt_tool/apps/cloud/openstackscripts/openstackclientutils.py b/ntt_tool/apps/cloud/openstackscripts/openstackclientutils.py new file mode 100644 index 0000000..1ee1fb2 --- /dev/null +++ b/ntt_tool/apps/cloud/openstackscripts/openstackclientutils.py @@ -0,0 +1,21 @@ +class OpenStackClientUtils(object): + + def __init__(self, **credentials): + self.credentials = credentials + + def get_credentials(self): + return self.credentials + + def get_client_instance(self): + credentials = self.get_credentials() + return self.client_class(**credentials) + + + + + + + + + + diff --git a/ntt_tool/apps/cloud/openstackscripts/openstackcredentials.py b/ntt_tool/apps/cloud/openstackscripts/openstackcredentials.py new file mode 100644 index 0000000..e69de29 diff --git a/ntt_tool/apps/cloud/openstackscripts/tenantnetworkdiscovery.py b/ntt_tool/apps/cloud/openstackscripts/tenantnetworkdiscovery.py new file mode 100644 index 0000000..762747e --- /dev/null +++ b/ntt_tool/apps/cloud/openstackscripts/tenantnetworkdiscovery.py @@ -0,0 +1,196 @@ +import logging +from django.db import transaction +from django.utils import timezone +# from neutronclientutils import NeutronClientUtils +from ntt_tool.apps.cloud.models import Tenant, Network, Subnet +# from ntt_tool.apps.cloud.serializers import TenantSerializer + + +logger = logging.getLogger(__name__) + + +class TenantsController(object): + + def __init__(self, traffic_id): + self.traffic_id = traffic_id + + def save(self, creator, tenants): + """ + Updating/Saving tenants in NTTT database. + """ + discovery_datetime = timezone.now() + with transaction.atomic(): + Tenant.objects.filter(traffic_id=self.traffic_id).update(is_dirty=False) + + tenant_objs = [] + for tenant in tenants: + filters = { + "tenant_id": tenant.id, + "traffic_id": self.traffic_id + } + tenant_obj, created = Tenant.objects.get_or_create(**filters) + tenant_obj.tenant_name = tenant.name + tenant_obj.description = tenant.description + tenant_obj.enabled = tenant.enabled + tenant_obj.creator = creator + tenant_obj.is_dirty = True + tenant_obj.updated_on = discovery_datetime + tenant_obj.save() + tenant_objs.append(tenant_obj) + + # Deleting tenants which are got deleted in openstack + Tenant.objects.filter(traffic_id=self.traffic_id)\ + .filter(is_dirty=False).delete() + + return tenant_objs + + +class NetworkController(object): + + def save(self, neutron_utils, creator, tenant, networks): + discovery_datetime = timezone.now() + + with transaction.atomic(): + # Making tenant as selected. + Tenant.objects.filter(traffic_id=tenant.traffic.id)\ + .update(is_selected=False) + tenant.is_selected = True + tenant.save() + + Network.objects.filter(tenant_id=tenant.id).update(is_dirty=False) + + network_objs = [] + for network in networks.get("networks", []): + filters = { + "tenant_id": tenant.id, + "network_id": network.get('id') + } + network_obj, created = Network.objects.get_or_create(**filters) + network_obj.network_name = network.get('name') + network_obj.shared = network.get('shared') + network_obj.status = network.get('status') + network_obj.is_dirty = True + network_obj.creator = creator + network_obj.created_on = discovery_datetime + network_obj.updated_on = discovery_datetime + network_obj.save() + network_objs.append(network_obj) + + # Setting all records belongs to tenant as not dirty/updated + Subnet.objects.filter(network_id=network_obj.id)\ + .update(is_dirty=False) + + subnets = neutron_utils.list_subnets(network_id=network_obj.network_id) + for subnet in subnets.get("subnets", []): + filters = { + "network_id": network_obj.id, + "subnet_id": subnet.get("id") + } + subnet_obj, created = Subnet.objects.get_or_create(**filters) + subnet_obj.subnet_name = subnet.get("name") + subnet_obj.cidr = subnet.get("cidr") + subnet_obj.allocation_pool_start = subnet.get("allocation_pools")[0].get("start") + subnet_obj.allocation_pool_end = subnet.get("allocation_pools")[0].get("end") + subnet_obj.is_dirty = True + subnet_obj.save() + + # Deleting subnets which are got changed after discovery + Subnet.objects.filter(network_id=network_obj.id)\ + .filter(is_dirty=False).delete() + + # Deleting networks which are not got deleted in openstack during discovery + Network.objects.filter(tenant_id=tenant.id)\ + .filter(is_dirty=False).delete() + + return network_objs + +# class NetworkSubnetDiscovery(NeutronClientUtils): +# +# def get_networks_and_subnets(self, user, cloud_id, tenants): +# # neutron = self.get_client_instance() +# +# discovery_result = [] +# with transaction.atomic(): +# discovery_datetime = timezone.now() +# +# Tenant.objects.filter(cloud_id=cloud_id)\ +# .update(is_dirty=False) +# +# for tenant in tenants: +# tenant_obj = None +# try: +# tenant_obj = Tenant.objects.filter(tenant_id=tenant.id).get() +# except Tenant.DoesNotExist: +# tenant_obj = Tenant() +# tenant_obj.cloud_id = cloud_id +# tenant_obj.tenant_id = tenant.id +# tenant_obj.tenant_name = tenant.name +# tenant_obj.description = tenant.description +# tenant_obj.enabled = tenant.enabled +# tenant_obj.creator = user +# tenant_obj.is_dirty = True +# tenant_obj.updated_on = discovery_datetime +# tenant_obj.save() +# +# # Setting all records belongs to tenant as not dirty/updated +# Network.objects.filter(tenant__tenant_id=tenant.id)\ +# .update(is_dirty=False) +# +# networks = self.list_networks(tenant_id=tenant.id) +# for network in networks.get("networks", []): +# network_obj = None +# try: +# filters = { +# "tenant__tenant_id": tenant.id, +# "network_name": network.get("name") +# } +# network_obj = Network.objects.filter(**filters).get() +# except Network.DoesNotExist: +# network_obj = Network() +# network_obj.tenant = tenant_obj +# network_obj.network_id = network.get("id") +# network_obj.network_name = network.get("name") +# network_obj.shared = network.get("shared") +# network_obj.status = network.get("status") +# network_obj.is_dirty = True +# network_obj.creator = user +# network_obj.updated_on = discovery_datetime +# network_obj.save() +# +# # Setting all records belongs to tenant as not dirty/updated +# Subnet.objects.filter(network__network_id=network.get("id"))\ +# .update(is_dirty=False) +# +# subnets = self.list_subnets(network_id=network.get("id")) +# for subnet in subnets.get("subnets"): +# subnet_obj = None +# try: +# filters = { +# "network__network_id": network.get("id"), +# "subnet_id": subnet.get("id") +# } +# subnet_obj = Subnet.objects.filter(**filters).get() +# except Subnet.DoesNotExist: +# subnet_obj = Subnet() +# subnet_obj.subnet_id = subnet.get("id") +# subnet_obj.network_id = network_obj.id +# subnet_obj.subnet_name = subnet.get("name") +# subnet_obj.cidr = subnet.get("cidr") +# subnet_obj.is_dirty = True +# subnet_obj.save() +# +# # Deleting subnets which are got changed after discovery +# Subnet.objects.filter(network__network_id=network.get("id"))\ +# .filter(is_dirty=False).delete() +# +# # Deleting networks which are not got changed after discovery +# Network.objects.filter(tenant__tenant_id=tenant_obj.id)\ +# .filter(is_dirty=False).delete() +# +# serializer = TenantSerializer(tenant_obj) +# discovery_result.append(serializer.data) +# +# # Deleting tenants which are got changed after discovery +# Tenant.objects.filter(cloud_id=cloud_id)\ +# .filter(is_dirty=False).delete() +# return discovery_result diff --git a/ntt_tool/apps/cloud/openstackscripts/traffictest/README.md b/ntt_tool/apps/cloud/openstackscripts/traffictest/README.md new file mode 100644 index 0000000..b55a188 --- /dev/null +++ b/ntt_tool/apps/cloud/openstackscripts/traffictest/README.md @@ -0,0 +1,45 @@ +# datapath-traffic-test + +Collection of tools and utility scripts to validate datapath traffic between endpoints governed by a contract + +**Traffic Test between Local VMs on VMware:** + +**Starting Traffic:** +$ python main.py --cfgfile=conf/config.ini --action=start -t sample_timestamp_name_to_track + +**Note:** [Optional parameter to load endpoints as JSON input file +--jsopninput=sample_endpoints.json + +**Stoping Traffic:** +$ python main.py --cfgfile=conf/config.ini --action=stop -t sample_timestamp_name_to_track + +**Note:** [Optional parameter to load endpoints as JSON input file +--jsopninput=sample_endpoints.json + +**Output:** + ++--------------+----------+--------------+----------+---------------------+------------------+---------------+---------+---------+---------+-------------+ +| src_tenant | src_ep | dest_tenant | dest_ep | packets_transmitted | packets received | packet_loss % | rtt_min | rtt_avg | rtt_max | test_status | ++--------------+----------+--------------+----------+---------------------+------------------+---------------+---------+---------+---------+-------------+ +| traffic_test | 1.1.1.16 | traffic_test | 2.2.2.6 | 164 | 164 | 0 | 0.3 | 2.9 | 5.1 | Success | +| traffic_test | 1.1.1.16 | traffic_test | 1.1.1.15 | 167 | 167 | 0 | 0.3 | 2.8 | 5.1 | Success | +| traffic_test | 1.1.1.16 | traffic_test | 2.2.2.5 | 163 | 163 | 0 | 0.4 | 2.8 | 5.1 | Success | +| traffic_test | 1.1.1.16 | traffic_test | 1.1.1.17 | 166 | 166 | 0 | 0.6 | 33.1 | 1004.7 | Success | +| traffic_test | 1.1.1.17 | traffic_test | 2.2.2.6 | 164 | 164 | 0 | 1.1 | 2.8 | 5.1 | Success | +| traffic_test | 1.1.1.17 | traffic_test | 1.1.1.15 | 165 | 165 | 0 | 1.1 | 3.0 | 6.2 | Success | +| traffic_test | 1.1.1.17 | traffic_test | 2.2.2.5 | 162 | 162 | 0 | 0.4 | 27.4 | 1003.3 | Success | +| traffic_test | 1.1.1.17 | traffic_test | 1.1.1.16 | 166 | 166 | 0 | 0.6 | 3.3 | 7.8 | Success | +| traffic_test | 2.2.2.5 | traffic_test | 1.1.1.16 | 166 | 166 | 0 | 0.4 | 2.8 | 5.2 | Success | +| traffic_test | 2.2.2.5 | traffic_test | 1.1.1.17 | 164 | 0 | 100 | 0.0 | 0.0 | 0.0 | Failed | +| traffic_test | 2.2.2.5 | traffic_test | 2.2.2.6 | 162 | 162 | 0 | 1.1 | 9.1 | 1001.1 | Success | +| traffic_test | 2.2.2.5 | traffic_test | 1.1.1.15 | 165 | 165 | 0 | 0.3 | 3.0 | 5.1 | Success | +| traffic_test | 1.1.1.15 | traffic_test | 1.1.1.17 | 165 | 165 | 0 | 1.1 | 2.9 | 5.2 | Success | +| traffic_test | 1.1.1.15 | traffic_test | 2.2.2.6 | 163 | 163 | 0 | 0.2 | 2.9 | 5.1 | Success | +| traffic_test | 1.1.1.15 | traffic_test | 2.2.2.5 | 162 | 162 | 0 | 1.1 | 3.0 | 5.2 | Success | +| traffic_test | 1.1.1.15 | traffic_test | 1.1.1.16 | 166 | 166 | 0 | 0.3 | 2.9 | 5.1 | Success | +| traffic_test | 2.2.2.6 | traffic_test | 1.1.1.16 | 168 | 168 | 0 | 1.1 | 2.9 | 7.0 | Success | +| traffic_test | 2.2.2.6 | traffic_test | 1.1.1.17 | 165 | 0 | 100 | 0.0 | 0.0 | 0.0 | Failed | +| traffic_test | 2.2.2.6 | traffic_test | 1.1.1.15 | 166 | 166 | 0 | 0.8 | 9.0 | 1003.4 | Success | +| traffic_test | 2.2.2.6 | traffic_test | 2.2.2.5 | 163 | 163 | 0 | 0.4 | 21.3 | 1004.6 | Success | ++--------------+----------+--------------+----------+---------------------+------------------+---------------+---------+---------+---------+-------------+ + diff --git a/ntt_tool/apps/cloud/openstackscripts/traffictest/__init__.py b/ntt_tool/apps/cloud/openstackscripts/traffictest/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ntt_tool/apps/cloud/openstackscripts/traffictest/conf/__init__.py b/ntt_tool/apps/cloud/openstackscripts/traffictest/conf/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ntt_tool/apps/cloud/openstackscripts/traffictest/conf/config.ini b/ntt_tool/apps/cloud/openstackscripts/traffictest/conf/config.ini new file mode 100644 index 0000000..1cb480e --- /dev/null +++ b/ntt_tool/apps/cloud/openstackscripts/traffictest/conf/config.ini @@ -0,0 +1,34 @@ +# Openstack environment config +[default] +keystone_auth_url='http://172.168.2.40:5000/v2.0' +keystone_user='admin' +keystone_password='admin' +keystone_tenant_name='admin' + +# Traffic test config +[traffic] +allowed_delta_percentage = 3 +test_results_path = '~/dp_test_results' +number_of_workers = 10 +remote_user = 'ubuntu' +remote_pass = 'ubuntu' +# icmp test through hping3 & tcp, udp test through iperf +test_method = 'icmp','tcp','udp' +# tcp & udp traffic generation duration in seconds +iperf_duration = 60 +# intra-tenant/inter-tenant/south-north/north-south/all +type = 'intra-tenant' + +# specify tenant name +[tenants] +tenants = 'admin', + +# specify SSH jump-host/gateway IP based on the tenant name +[tenant_ssh_gateway] +#traffic_test = '172.168.2.53' +admin= '172.168.2.35' + +# specify SSH External host IP for south-north & north-south test +[external_host] +host = '10.1.25.129' + diff --git a/ntt_tool/apps/cloud/openstackscripts/traffictest/conf/logging.ini b/ntt_tool/apps/cloud/openstackscripts/traffictest/conf/logging.ini new file mode 100644 index 0000000..5091a55 --- /dev/null +++ b/ntt_tool/apps/cloud/openstackscripts/traffictest/conf/logging.ini @@ -0,0 +1,33 @@ +[loggers] +keys=root + +[handlers] +keys=consoleHandler,fileHandler + +[formatters] +keys=simpleFormatter,fileFormatter + +[logger_root] +level=INFO +handlers=consoleHandler,fileHandler + +[handler_consoleHandler] +class=StreamHandler +level=DEBUG +formatter=simpleFormatter +args=(sys.stdout,) + +[handler_fileHandler] +class=FileHandler +level=DEBUG +formatter=fileFormatter +args=('logs/dptest.log',) + +[formatter_simpleFormatter] +format=%(asctime)s - %(name)s - %(levelname)s - %(message)s +datefmt= + +[formatter_fileFormatter] +format=%(asctime)s - %(name)s - %(levelname)s - %(message)s +datefmt= + diff --git a/ntt_tool/apps/cloud/openstackscripts/traffictest/conf/os_env_conf.py b/ntt_tool/apps/cloud/openstackscripts/traffictest/conf/os_env_conf.py new file mode 100644 index 0000000..24bbe90 --- /dev/null +++ b/ntt_tool/apps/cloud/openstackscripts/traffictest/conf/os_env_conf.py @@ -0,0 +1,16 @@ +# Openstack environment config + +keystone_auth_url='http://10.30.120.50:5000/v2.0' +keystone_user='admin' +keystone_password='noir0123' +keystone_tenant_name='admin' + +# Traffic test config + +allowed_delta_percentage = 3 +test_results_path = '~/dp_test_results' +ssh_gateway = '192.168.61.2' +number_of_workers = 10 +remote_user = 'root' +remote_pass = 'root123' +test_method = 'hping' diff --git a/ntt_tool/apps/cloud/openstackscripts/traffictest/libs/__init__.py b/ntt_tool/apps/cloud/openstackscripts/traffictest/libs/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ntt_tool/apps/cloud/openstackscripts/traffictest/libs/os_get_env.py b/ntt_tool/apps/cloud/openstackscripts/traffictest/libs/os_get_env.py new file mode 100644 index 0000000..857b06b --- /dev/null +++ b/ntt_tool/apps/cloud/openstackscripts/traffictest/libs/os_get_env.py @@ -0,0 +1,74 @@ +import logging +from keystoneclient.v2_0 import client as ksc +from neutronclient.neutron import client as nwc +from novaclient import client as novac +import pdb + +class OSUtils(object): + + def __init__(self, config): + self.logger = logging.getLogger(__name__) + self.config = config + self.ks_user = config['default']['keystone_user'] + self.ks_pass = config['default']['keystone_password'] + self.ks_auth_url = config['default']['keystone_auth_url'] + self.ks_tenant_name = config['default']['keystone_tenant_name'] + + self.ks = ksc.Client( + username=self.ks_user, + password=self.ks_pass, + tenant_name=self.ks_tenant_name, + auth_url=self.ks_auth_url) + self.logger.info('OSUtils intialized successfully') + + def get_neutron_client_by_tenant(self, tenant): + nwclient = nwc.Client( + '2.0', + username=self.ks_user, + password=self.ks_pass, + tenant_name=tenant, + auth_url=self.ks_auth_url) + return nwclient + + def get_nova_client_by_tenant(self, tenant): + novaclient = novac.Client( + '2', + username=self.ks_user, + api_key=self.ks_pass, + project_id=tenant, + auth_url=self.ks_auth_url) + return novaclient + + def get_tenants_list(self): + self.logger.info('Get tenant list from Keystone') + tenants = self.ks.tenants.list() + self.logger.info(tenants) + if self.config['traffic']['type'] == 'intra-tenant' or self.config['traffic']['type'] == 'all': + self.logger.info('\n\t\tPerforming Intra-tenant Test.\n') + if len(self.config['tenants']['tenants']) > 0: + self.logger.info('\n\t\tMore than one Tenant found. Processing first tenant alone by default.\n') + cfgtenants = self.config['tenants']['tenants'][0] + else: + cfgtenants = self.config['tenants']['tenants'] + if self.config['traffic']['type'] == 'inter-tenant' or self.config['traffic']['type'] == 'all': + self.logger.info('\n\n\t\tPerforming Inter-tenant Test.\n') + if len(self.config['tenants']['tenants']) < 2: + cfgtenants = '' + self.logger.info('\n\t\tMinimum Tenants count should be 2\n') + else: + cfgtenants = self.config['tenants']['tenants'] + if self.config['traffic']['type'] == 'south-north' or self.config['traffic']['type'] == 'all': + self.logger.info('\n\n\t\tPerforming south-north Test.\n') + if len(self.config['tenants']['tenants']) < 1: + cfgtenants = '' + else: + cfgtenants = self.config['tenants']['tenants'] + if self.config['traffic']['type'] == 'north-south' or self.config['traffic']['type'] == 'all': + self.logger.info('\n\n\t\tPerforming north-south Test.\n') + if len(self.config['tenants']['tenants']) < 1: + cfgtenants = '' + else: + cfgtenants = self.config['tenants']['tenants'] + + return {tenant.id: tenant.name for tenant in tenants if tenant.enabled and tenant.name in cfgtenants} + diff --git a/ntt_tool/apps/cloud/openstackscripts/traffictest/libs/tasks.py b/ntt_tool/apps/cloud/openstackscripts/traffictest/libs/tasks.py new file mode 100644 index 0000000..e69de29 diff --git a/ntt_tool/apps/cloud/openstackscripts/traffictest/libs/test_fabric.py b/ntt_tool/apps/cloud/openstackscripts/traffictest/libs/test_fabric.py new file mode 100644 index 0000000..9c26972 --- /dev/null +++ b/ntt_tool/apps/cloud/openstackscripts/traffictest/libs/test_fabric.py @@ -0,0 +1,54 @@ +from fabric.api import * +import fabric +from fabric.network import ssh +import sys + +#import pudb + +ssh.util.log_to_file("paramiko.log", 10) + +host_list = ['1.2.3.3', '1.2.3.2', '1.2.3.5'] +env.hosts = host_list +env.user = 'noiro' +env.password = 'noir0123' +env.gateway = '10.30.120.11' +#env.shell = "/bin/sh -c" + +@task +def get_hostname(): + out = run("hostname") + print out + +@task +@parallel +def start_traffic(env): + + for host in host_list: + if host != env.host_string: +# with cd("~/dp_test_results"): + sudo('hping3 %s --icmp --fast -q 2> sampletraffic-%s-%s.txt 1> /dev/null &' %(host,env.host_string.replace('.','_'), host.replace('.','_')), pty=False) + + +@task +@parallel +def stop_traffic(): +# with cd("~/dp_test_results"): + sudo("kill -SIGINT `pgrep hping3`") +# with cd("~/dp_test_results"): + for host in host_list: + if host != env.host_string: + out = sudo("cat sampletraffic-%s-%s.txt | grep loss" %(env.host_string.replace('.','_'), host.replace('.','_'))) + print out + +@task +@runs_once +def start_task(): + if sys.argv[1] == 'start': + execute(start_traffic, env) + elif sys.argv[1] == 'stop': + execute(stop_traffic, hosts = env.hosts) + else: + execute(get_hostname, hosts = env.hosts) + +if __name__ == '__main__': + start_task() diff --git a/ntt_tool/apps/cloud/openstackscripts/traffictest/libs/traf_tester.py b/ntt_tool/apps/cloud/openstackscripts/traffictest/libs/traf_tester.py new file mode 100644 index 0000000..31a528c --- /dev/null +++ b/ntt_tool/apps/cloud/openstackscripts/traffictest/libs/traf_tester.py @@ -0,0 +1,387 @@ +import json +import logging +import os +import re +import time +import fabtools + +from itertools import * +from operator import * +from fabric.api import * + + +traffic_test_script_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'scripts') +test_results_path = '~/dp_test_results' + +logger = logging.getLogger(__name__) + + +def unique_justseen(iterable, key=None): + "List unique elements, preserving order. Remember only the element just seen." + return imap(next, imap(itemgetter(1), groupby(iterable, key))) + + +def setup_env(config, endpoints): + env.hosts = endpoints.get("src_eps") + env.user = config.get('traffic', {}).get('remote_user') + env.password = config.get('traffic', {}).get('remote_pass') + env.skip_bad_hosts = True + if endpoints['test_type'] != 'north-south': + env.gateway = config.get('tenant_ssh_gateway', {}).get(endpoints.get('src_tenant')[0]) + else: + env.gateway = config.get('tenant_ssh_gateway', {}).get(endpoints.get('dest_tenant')[0]) + + +def install_hping(environment): + try: + out = run("python -c 'import platform;" + " print platform.linux_distribution()[0]'") + os_info = out + logger.debug("Host %s runs %s" % (env.host_string, os_info)) + if out.return_code == 0: + if os_info in ['CentOS', + 'Red Hat Enterprise Linux Server', + 'Fedora']: + out = sudo("yum -y install hping3", quiet=True) + if out.return_code == 0: + logger.info("Installed hping3 on %s" % (env.host_string)) + elif os_info in ['Ubuntu','LinuxMint']: + out = sudo("apt-get -y install hping3", quiet=True) + if out.return_code == 0: + logger.info("Installed hping3 on %s" % (env.host_string)) + out = run("mkdir %s" % (test_results_path)) + except SystemExit, e: + logger.warn("Exception while executing task: %s", str(e)) + + +def install_iperf(environment): + try: + os_distribution_id = fabtools.system.distrib_id() + logger.debug("Host %s runs %s" % (env.host_string, os_distribution_id)) + # if out.return_code == 0: + if os_distribution_id in ['CentOS','Red Hat Enterprise Linux Server', 'Fedora']: + out = sudo("yum -y install iperf3") + if out.return_code == 0: + logger.info("Installed iperf3 on %s" % (env.host_string)) + elif os_distribution_id in ['Ubuntu','LinuxMint']: + if not fabtools.deb.is_installed("iperf3"): + fabtools.deb.update_index() + fabtools.deb.install("iperf3") + # run("sudo apt-get update -y") + # out = sudo("apt-get -y install iperf3") + # if out.return_code == 0: + # logger.info("Installed iperf3 on %s" % (env.host_string)) + run("mkdir %s" % (test_results_path)) + except SystemExit, e: + logger.warn("Exception while executing task: %s", str(e)) + + +def setup_iperf_env(config, src_tenant, dest_tenant, endpoints, test_type): + env.hosts = endpoints + env.user = config.get('traffic', {}).get('remote_user') + env.password = config.get('traffic', {}).get('remote_pass') + env.skip_bad_hosts = True + if test_type != 'north-south': + env.gateway = config.get('tenant_ssh_gateway', {}).get(src_tenant[0]) + else: + env.gateway = config.get('tenant_ssh_gateway', {}).get(dest_tenant[0]) + + +@task +@parallel +def create_test_results_directory(environment): + try: + run("mkdir %s" % (test_results_path)) + except SystemExit, e: + logger.warn("Exception while executing task: %s", str(e)) + + +@task +@parallel +def test_ping(environment, config, endpoints, contract, timestamp): + try: + for dest_ep in endpoints['dest_eps']: + if dest_ep != env.host_string: + #sudo("hping3 %s --icmp --fast -q 2>" + # " testtraffic-%s-%s-%s.txt 1> /dev/null &" % + sudo("hping3 %s --icmp --fast -q >" + " testtraffic-%s-%s-%s.txt 2>&1 &" % + (dest_ep, + env.host_string.replace('.', '_'), + dest_ep.replace('.', '_'), + timestamp), + pty=False) + + except SystemExit, e: + logger.warn("Exception while executing task: %s", str(e)) + + +@task +@parallel +def test_tcp(environment, config, server, endpoints, contract, timestamp): + try: + if server == '': + print "TCP server execution" + for src_ep in endpoints: + if src_ep == env.host_string: + print src_ep + + sudo("iperf3 -s -p 5201 -i 1 > tcptesttrafficserver-%s-%s.txt 2>&1 &" % + (env.host_string.replace('.', '_'), + timestamp), + pty=False) + + else: + print "TCP client execution" + for dest_ep in endpoints: + if dest_ep == env.host_string: + print dest_ep + sudo("iperf3 -c %s -t %s -p 5201 -J > tcptesttrafficclient-%s-%s-%s.json" % + (server, + config['traffic']['iperf_duration'], + env.host_string.replace('.', '_'), + server.replace('.', '_'), + timestamp), + pty=False) + + except SystemExit, e: + logger.warn("Exception while executing task: %s", str(e)) + + +@task +@parallel +def test_udp(environment, config, server, endpoints, contract, timestamp): + try: + if server == '': + print "UDP server execution" + for src_ep in endpoints: + if src_ep == env.host_string: + print src_ep + sudo("iperf3 -s -p 5202 -i 1 > udptesttrafficserver-%s-%s.txt 2>&1 &" % + (env.host_string.replace('.', '_'), + timestamp), + pty=False) + else: + print "UDP client execution" + for dest_ep in endpoints: + if dest_ep == env.host_string: + sudo("iperf3 -c %s -u -n %sm -b 10m -p 5202 -J > udptesttrafficclient-%s-%s-%s.json" % + (server, + config.get("traffic", {}).get("udp_datagram_size", 1), + env.host_string.replace('.', '_'), + server.replace('.', '_'), + timestamp), + pty=False) + except SystemExit, e: + logger.warn("Exception while executing task: %s", str(e)) + + +def capture_output(): + pass + + +@task +@parallel +def stop_traffic(environment, endpoints, timestamp): + try: + try: + sudo("kill -SIGINT `pgrep hping3`") + except SystemExit, e: + logger.warn("Exception while executing task: %s", str(e)) + + print "dest_eps are.....", endpoints['dest_eps'] + put(os.path.join(traffic_test_script_dir, "get_ping_statistics.py"), "get_ping_statistics.py") + out = run("python get_ping_statistics.py %s" % (timestamp)) + out_dict = json.JSONDecoder().decode(out) + output = {'src_tenant': endpoints['src_tenant'], 'dest_tenant': endpoints['dest_tenant'], 'test_result': out_dict} + return output + except SystemExit, e: + logger.warn("Exception while executing task: %s", str(e)) + + +@task +@parallel +def stop_iperf_traffic(environment, traffic_type, src_ep, dest_ep, src_tenant, dest_tenant, timestamp): + try: + if src_ep == '': + try: + print "Killing iperf on destination enpoint (server)" + sudo("kill -SIGINT `pgrep iperf`") + except SystemExit, e: + logger.warn("Exception while executing task: %s", str(e)) + print "Destination endpoint (Server) is.....", dest_ep + return True + else: + try: + print "Killing iperf on client" + sudo("kill -SIGINT `pgrep iperf`") + except SystemExit, e: + logger.warn("Exception while executing task: %s", str(e)) + print "client endpoint is.....", src_ep + + out = None + if traffic_type == 'tcp': + put(os.path.join(traffic_test_script_dir, "get_iperf_tcp_statistics.py"), "get_iperf_tcp_statistics.py") + out = run("python get_iperf_tcp_statistics.py %s-%s-%s" % + (src_ep.replace('.', '_'), + dest_ep.replace('.', '_'), + timestamp)) + + if traffic_type == 'udp': + put(os.path.join(traffic_test_script_dir, "get_iperf_udp_statistics.py"), "get_iperf_udp_statistics.py") + out = run("python get_iperf_udp_statistics.py %s-%s-%s" % + (src_ep.replace('.', '_'), + dest_ep.replace('.', '_'), + timestamp)) + + key = "%stesttrafficclient-%s-%s-%s.json" % (traffic_type, + src_ep.replace('.', '_'), + dest_ep.replace('.', '_'), + timestamp) + response = json.JSONDecoder().decode(out) + response = response.get(key) + response['src_tenant'] = src_tenant + response['dest_tenant'] = dest_tenant + return response + except SystemExit, e: + logger.warn("Exception while executing task: %s", str(e)) + + +def format_icmp_test_results(data, allowed_delta_percentage): + status = None + test_results = [] + dest_ep_regex = ".*-*-(?P[0-9]+_[0-9]+_[0-9]+_[0-9]+)-.*" + for content in data: + for k, v in content.items(): + src_ep = k + src_tenant = v['src_tenant'] + dest_tenant = v['dest_tenant'] + test_result_files = v['test_result'].keys() + for test_result_file in test_result_files: + dest_ep_match = re.match(dest_ep_regex, test_result_file) + dest_ep = dest_ep_match.group('dest_ip') + packet_stats = v['test_result'][test_result_file]['packet_stats'] + packet_loss_percent = packet_stats['packet_loss'] # NOQA + try: + if packet_loss_percent <= int(allowed_delta_percentage): # NOQA + status = 'Success' + else: + status = 'Failed' + except ValueError: + status = 'Failed' + rtt_stats = v['test_result'][test_result_file]['rtt'] + test_results.append({ + "src_tenant": src_tenant, + "src_ep": src_ep, + "dest_tenant": dest_tenant, + "dest_ep": dest_ep.replace('_', '.'), + "packets_transmitted": packet_stats['packets_transmitted'], + "packets_received": packet_stats['packets_received'], + "packet_loss_percent": packet_loss_percent, + "rtt_min": rtt_stats['rtt_min'], + "rtt_avg": rtt_stats['rtt_avg'], + "rtt_max": rtt_stats['rtt_max'], + "status": status, + }) + return test_results + + +def start_task(config, endpoints_list, action, test_prefix=None): + timestamp = test_prefix + if not test_prefix: + timestamp = time.strftime("%Y-%m-%d-%H-%M-%S") + + output_table_data_list = [] + tcp_output_table_data_list = [] + udp_output_table_data_list = [] + table_data = {} + iperf_endpoint_list = list(unique_justseen(endpoints_list, key=itemgetter('test_type'))) + if action == 'start': + for endpoints in endpoints_list: + for contract in endpoints['contract']: + if contract['protocol'] == 'icmp': + setup_env(config, endpoints) + execute(create_test_results_directory, env) + execute(install_hping, env) + execute(test_ping, env, config, endpoints, contract, timestamp) + for fullendpoints in iperf_endpoint_list: + endpoints = {} + endpoints['contract'] = fullendpoints['contract'] + endpoints['src_tenant'] = fullendpoints['src_tenant'] + endpoints['src_eps'] = [fullendpoints['src_eps'][0]] + endpoints['dest_tenant'] = fullendpoints['dest_tenant'] + endpoints['dest_eps'] = [fullendpoints['dest_eps'][0]] + endpoints['test_type'] = fullendpoints['test_type'] + for contract in endpoints['contract']: + if contract['protocol'] == 'tcp': + server_ip = [endpoints['dest_eps'][0]] + client_ip = endpoints['src_eps'] + setup_iperf_env(config, endpoints['src_tenant'], endpoints['dest_tenant'], server_ip, endpoints['test_type']) + execute(create_test_results_directory, env) + execute(install_iperf, env) + server = '' + execute(test_tcp, env, config, server, server_ip, contract, timestamp) + setup_iperf_env(config, endpoints['src_tenant'], endpoints['dest_tenant'], client_ip, endpoints['test_type']) + execute(install_iperf, env) + server = server_ip[0] + execute(test_tcp, env, config, server, client_ip, contract, timestamp) + if contract['protocol'] == 'udp': + server_ip = [endpoints['dest_eps'][0]] + client_ip = endpoints['src_eps'] + setup_iperf_env(config, endpoints['src_tenant'], endpoints['dest_tenant'], server_ip, endpoints['test_type']) + execute(create_test_results_directory, env) + execute(install_iperf, env) + server = '' + execute(test_udp, env, config, server, server_ip, contract, timestamp) + setup_iperf_env(config, endpoints['src_tenant'], endpoints['dest_tenant'], client_ip, endpoints['test_type']) + execute(install_iperf, env) + server = server_ip[0] + execute(test_udp, env, config, server, client_ip, contract, timestamp) + + if action == 'stop': + for endpoints in endpoints_list: + for contract in endpoints['contract']: + if contract['protocol'] == 'icmp': + setup_env(config, endpoints) + table_data = execute(stop_traffic, env, endpoints, timestamp) + output_table_data_list.append(table_data) + for fullendpoints in iperf_endpoint_list: + endpoints = {} + endpoints['contract'] = fullendpoints['contract'] + endpoints['src_tenant'] = fullendpoints['src_tenant'] + endpoints['src_eps'] = [fullendpoints['src_eps'][0]] + endpoints['dest_tenant'] = fullendpoints['dest_tenant'] + endpoints['dest_eps'] = [fullendpoints['dest_eps'][0]] + endpoints['test_type'] = fullendpoints['test_type'] + for contract in endpoints['contract']: + if contract['protocol'] == 'tcp': + server_ip = [endpoints['dest_eps'][0]] + dest_ep = server_ip[0] + client_ip = endpoints['src_eps'] + setup_iperf_env(config, endpoints['src_tenant'], endpoints['dest_tenant'], server_ip, endpoints['test_type']) + src_ep = '' + execute(stop_iperf_traffic, env, 'tcp', src_ep, dest_ep, endpoints['src_tenant'], endpoints['dest_tenant'], timestamp) + setup_iperf_env(config, endpoints['src_tenant'], endpoints['dest_tenant'], client_ip, endpoints['test_type']) + server = server_ip[0] + for src_ep in client_ip: + table_data = execute(stop_iperf_traffic, env, 'tcp', src_ep, dest_ep, endpoints['src_tenant'], endpoints['dest_tenant'], timestamp) + tcp_output_table_data_list.append(table_data.get(src_ep)) + if contract['protocol'] == 'udp': + server_ip = [endpoints['dest_eps'][0]] + dest_ep = server_ip[0] + client_ip = endpoints['src_eps'] + setup_iperf_env(config, endpoints['src_tenant'], endpoints['dest_tenant'], server_ip, endpoints['test_type']) + src_ep = '' + execute(stop_iperf_traffic, env, 'udp', src_ep, dest_ep, endpoints['src_tenant'], endpoints['dest_tenant'], timestamp) + setup_iperf_env(config, endpoints['src_tenant'], endpoints['dest_tenant'], client_ip, endpoints['test_type']) + for src_ep in client_ip: + table_data = execute(stop_iperf_traffic, env, 'udp', src_ep, dest_ep, endpoints['src_tenant'], endpoints['dest_tenant'], timestamp) + udp_output_table_data_list.append(table_data.get(src_ep)) + + traffic_test_result = { + "udp": udp_output_table_data_list, + "tcp": tcp_output_table_data_list, + "icmp": format_icmp_test_results(output_table_data_list, config.get('traffic', {}).get('allowed_delta_percentage')), + } + return traffic_test_result + diff --git a/ntt_tool/apps/cloud/openstackscripts/traffictest/main.py b/ntt_tool/apps/cloud/openstackscripts/traffictest/main.py new file mode 100644 index 0000000..e81eaa4 --- /dev/null +++ b/ntt_tool/apps/cloud/openstackscripts/traffictest/main.py @@ -0,0 +1,429 @@ +import os +import logging +import logging.config +from libs import os_get_env as os_lib +from libs import traf_tester as remote_libs +from configobj import ConfigObj +from argparse import ArgumentParser +import pprint +import json +import pdb +from datetime import datetime, timedelta +from pytz import timezone + +if not os.path.exists('logs'): + os.makedirs('logs') +logging.config.fileConfig('conf/logging.ini') +logger = logging.getLogger(__name__) +logger.setLevel(logging.DEBUG) + +action_list = ['start', 'stop'] +parser = ArgumentParser(description="Datapath traffic testing") +parser.add_argument("-f", "--cfgfile", required=True, metavar="config_input_file") +parser.add_argument("-a", "--action", required=True) +parser.add_argument("-t", + "--tid", + required=True, + help="Timestamp ID to reference for labeling test output") +parser.add_argument("-i", "--jsoninput", required=False, metavar="endpoints_JSON_input_file") +args = parser.parse_args() +cfgfile = args.cfgfile +config = ConfigObj(cfgfile) +action = args.action +tid = args.tid if args.tid else None +json_input = args.jsoninput if args.jsoninput else None + +osutils = os_lib.OSUtils(config) + + +def get_routers(tenant): + neutron = osutils.get_neutron_client_by_tenant(tenant[1]) + routers = neutron.list_routers()['routers'] + routers = [router for router in routers if router['tenant_id'] == tenant[0]] + router_list = [] + for router in routers: + if router['name'] != 'PHYSICAL_GLOBAL_ROUTER_ID': + router_list.append(router) + return router_list + + +def get_ports(tenant): + neutron = osutils.get_neutron_client_by_tenant(tenant[1]) + ports = neutron.list_ports()['ports'] + port_list = [port for port in ports if port['tenant_id'] == tenant[0]] + return port_list + + +def get_subnet_detail(tenant, subnet): + neutron = osutils.get_neutron_client_by_tenant(tenant[1]) + return neutron.show_subnet(subnet) + + +def get_instance_detail(tenant, instance): + nova = osutils.get_nova_client_by_tenant(tenant[1]) + return nova.servers.get(instance) + + +def get_floatingips(tenant): + neutron = osutils.get_neutron_client_by_tenant(tenant[1]) + return neutron.list_floatingips() + + +def get_router_ports(router, ports): + """ + Returns a list of router interface ports for a given router + """ + router_port_list = [] + for port in ports: + if (port['device_id'] == router['id'] and + port['device_owner'] == 'network:router_interface'): + router_port_list.append(port) + return router_port_list + + +def get_port_subnets(tenant, port): + """ + Using the subnets for a tenant, obtain IP for a given port + """ + subnet = get_subnet_detail(tenant, port['fixed_ips'][0]['subnet_id'])['subnet'] + return {'id': port['fixed_ips'][0]['subnet_id'], 'name': subnet['name'], + 'ip_address': port['fixed_ips'][0]['ip_address']} + + +def get_instance_ports(ports): + """ + Returns a list of instance interface ports + """ + instance_port_list = [] + for port in ports: + if port['device_owner'] == 'compute:nova': + instance_port_list.append(port) + return instance_port_list + + +def get_subnet_endpoints(tenant, ports, subnet): + """ + Returns a list of vm ips for a a given subnet + """ + endpoints = [] + for port in ports: + endpoint_detail = {} + if port['fixed_ips'][0]['subnet_id'] == subnet['id']: + device_id = port['device_id'].encode('unicode_escape') + ip = port['fixed_ips'][0]['ip_address'].encode('unicode_escape') + endpoint_detail['device_id'] = device_id + endpoint_detail['ip_address'] = ip + endpoints.append(endpoint_detail) + return endpoints + + +def get_endpoint_floatingips(tenant, ip): + """ + Returns floating ip for a given fixed-ip. + """ + endpoint = '' + for entry in get_floatingips(tenant)['floatingips']: + if entry['fixed_ip_address'] == ip: + endpoint = entry['floating_ip_address'] + return endpoint.encode('unicode_escape') + + +def get_default_icmp_contract(): + contract = [{'name': 'allow_icmp', + 'protocol': 'icmp', + 'port': 'None', + 'direction': 'in', + 'action': 'allow'}] + return contract + + +def get_default_tcp_contract(): + contract = [{'name': 'allow_ssh', + 'protocol': 'tcp', + 'port': '22', + 'direction': 'in', + 'action': 'allow'}] + return contract + + +def get_default_udp_contract(): + contract = [{'name': 'allow_udp', + 'protocol': 'udp', + 'port': 'None', + 'direction': 'in', + 'action': 'allow'}] + return contract + + +def get_traffic_testing_endpoint(src_tenant, dest_tenant, + src_eps, dest_eps, contract, test_type): + endpoint = {'src_tenant': src_tenant, + 'dest_tenant': dest_tenant, + 'src_eps': src_eps, + 'dest_eps': dest_eps, + 'contract': contract, + 'test_type': test_type} + return endpoint + + +def discover_endpoints(): + logger.info('Discovering Live Endpoints') + logger.info('Initialize OSUtils') + endpoints_list = [] + endpoint_list = [] + logger.info('Get tenant details') + tenants = osutils.get_tenants_list() + tenant_data = {} + routers = {} + for tenant in tenants.items(): + router_data = {} + # retrieve all rotuers for a tenant + routers = get_routers(tenant) + ports = get_ports(tenant) + instance_ports = get_instance_ports(ports) + for router in routers: + router_detail = {} + router_detail['id'] = router['id'] + router_detail['ports'] = get_router_ports(router, ports) + subnets = [] + instance_list = [] + for port in router_detail['ports']: + subnets.append(get_port_subnets(tenant, port)) + router_detail['subnets'] = subnets + for subnet in router_detail['subnets']: + if subnet['id'] != '': + endpoint_detail = get_subnet_endpoints(tenant, instance_ports, subnet) + ip_list = [] + endpoint_list = [] + subnet['instances'] = endpoint_detail + for entry in endpoint_detail: + server = get_instance_detail(tenant, entry['device_id']) + if entry['device_id'] not in instance_list and \ + server.status == 'ACTIVE': + instance_list.append(entry['device_id']) + ip_list.append(entry['ip_address']) + endpoint_list.append({'instances': instance_list, + 'endpoints': ip_list}) + subnet['endpoints'] = ip_list + router_data[router['name'].encode('unicode_escape')] = router_detail + tenant_data[tenant[1]] = router_data + # print tenant_data + + test_method = config['traffic']['test_method'] + contract = [] + if 'icmp' in test_method or len(test_method) == 0: + contract.append(get_default_icmp_contract()[0]) + if 'tcp' in test_method: + contract.append(get_default_tcp_contract()[0]) + if 'udp' in test_method: + contract.append(get_default_udp_contract()[0]) + tenant = config['tenants']['tenants'] + pdb.set_trace() + if config['traffic']['type'] == 'intra-tenant' or config['traffic']['type'] == 'all': + # intra-tenant + for tenant in tenants.items(): + current_router = {} + current_net = {} + routers = get_routers(tenant) + for route in routers: + rsubnets = tenant_data[tenant[1]][route['name']]['subnets'] + if len(rsubnets) > 1: + current_net = {} + for entry in rsubnets: + tsrc = [] + tdest = [] + tsrc.append(entry['endpoints']) + for nextep in rsubnets: + if entry['name'] != nextep['name']: + tdest.append(nextep['endpoints']) + subnet_data = {'src_eps': [ep for eps in tsrc for ep in eps], + 'dest_eps': [ep for eps in tdest for ep in eps]} + current_net[entry['name']] = subnet_data + current_router[route['name']] = current_net + + for route in routers: + if route['name'] in current_router: + rsubnets = tenant_data[tenant[1]][route['name']]['subnets'] + for entry in rsubnets: + if entry['name'] in current_router[route['name']]: + net = current_router[route['name']][entry['name']] + if len(net['src_eps']) > 0 and \ + len(net['dest_eps']) > 0: + endpoints_list.append( + get_traffic_testing_endpoint(tenant[1], + tenant[1], + net['src_eps'], + net['dest_eps'], + contract, + 'intra-tenant')) + + if config['traffic']['type'] == 'inter-tenant' or config['traffic']['type'] == 'all': + # inter-tenant + for tenant in tenants.items(): + current_router = {} + current_net = {} + routers = get_routers(tenant) + for route in routers: + rsubnets = tenant_data[tenant[1]][route['name']]['subnets'] + if len(rsubnets) > 0: + current_net = {} + tsrc = [] + tdest = [] + for entry in rsubnets: + tsrc.append(entry['endpoints']) + src_eps = [ep for eps in tsrc for ep in eps] + for nexttenant in tenants.items(): + if tenant[1] != nexttenant[1]: + for nroute in tenant_data[nexttenant[1]]: + for entry in tenant_data[nexttenant[1]][nroute]['subnets']: + if len(rsubnets) > 0: + for ip in entry['endpoints']: + floating_ip = get_endpoint_floatingips(nexttenant, ip) + if floating_ip != '': + tdest.append(floating_ip) + dest_eps = tdest + if len(src_eps) > 0 and len(dest_eps) > 0: + endpoints_list.append( + get_traffic_testing_endpoint(tenant[1], + nexttenant[1], + src_eps, + dest_eps, + contract, + 'inter-tenant')) + + if config['traffic']['type'] == 'south-north' or config['traffic']['type'] == 'all': + # south-north + for tenant in tenants.items(): + current_router = {} + current_net = {} + routers = get_routers(tenant) + for route in routers: + rsubnets = tenant_data[tenant[1]][route['name']]['subnets'] + if len(rsubnets) > 0: + current_net = {} + tsrc = [] + for entry in rsubnets: + tsrc.append(entry['endpoints']) + src_eps = [ep for eps in tsrc for ep in eps] + if config['external_host']['host'] != '': + dest_eps = [config['external_host']['host']] + else: + dest_eps = [] + print "Please specify external host in the config" + if len(src_eps) > 0 and len(dest_eps) > 0: + endpoints_list.append( + get_traffic_testing_endpoint(tenant[1], + 'External Host', + src_eps, + dest_eps, + contract, + 'south-north')) + + if config['traffic']['type'] == 'north-south' or config['traffic']['type'] == 'all': + # north-south + for tenant in tenants.items(): + current_router = {} + current_net = {} + routers = get_routers(tenant) + for route in routers: + rsubnets = tenant_data[tenant[1]][route['name']]['subnets'] + if len(rsubnets) > 0: + current_net = {} + tdest = [] + if config['external_host']['host'] != '': + src_eps = [config['external_host']['host']] + else: + src_eps = [] + print "Please specify external host in the config" + for entry in rsubnets: + for ip in entry['endpoints']: + floating_ip = get_endpoint_floatingips(tenant, ip) + if floating_ip != '': + tdest.append(floating_ip) + dest_eps = tdest + if len(src_eps) > 0 and len(dest_eps) > 0: + endpoints_list.append( + get_traffic_testing_endpoint('External Host', + tenant[1], + src_eps, + dest_eps, + contract, + 'north-south')) + logger.debug("endpoints list = %s" % (pprint.pformat(endpoints_list))) + + + # #src_ip_list = ['192.168.61.131'] + # src_ip_list =['1.1.1.17'] + # dest_ip_list = ['192.168.61.229','192.168.61.132'] + # #dest_ip_list = ['192.168.61.129', '192.168.61.132'] + # dest_ip_list = ['10.1.25.128', '10.1.25.129', '10.1.25.130'] + # dest_ip_list = ['1.1.1.15','1.1.1.16','2.2.2.5','2.2.2.6'] + # contract = [{'name': 'allow_ssh', + # 'protocol': 'tcp', + # 'port': 22, + # 'direction': 'in', + # 'action': 'allow'}, + # {'name': 'allow_icmp', + # 'protocol': 'icmp', + # 'port': 'None', + # 'direction': 'in', + # 'action': 'allow'}] + + # endpoints = {'src_tenant': 'dummy_tenant', + # 'dest_tenant': 'dummy_tenant', + # 'src_eps': src_ip_list, + # 'dest_eps': dest_ip_list, + # 'contract': contract} + # endpoints_list = [endpoints] + + # # endpoints = {'src_tenant': 'sample', + # 'dest_tenant': 'sample', + # 'src_eps': src_ip_list, + # 'dest_eps': dest_ip_list, + # 'contract': contract} + # # endpoints_list = [endpoints] + + return endpoints_list + + +def main(): + logger.info('Start the program') + fmt = "%Y-%m-%d %H:%M:%S %Z%z" + start_time = datetime.now(timezone('US/Pacific')) + + if json_input: + # Passing preloaded Endpoints through JSON file + logger.info('Loading Endpoints from JSON Input File') + with open(json_input, 'r') as data_file: + endpoints_list = json.load(data_file) + logger.debug("JSON Input endpoints list = %s" % (pprint.pformat(endpoints_list))) + discovery_time = datetime.now(timezone('US/Pacific')) + else: + # Discovering Endpoints from Live Openstack setup + endpoints_list = discover_endpoints() + discovery_time = datetime.now(timezone('US/Pacific')) + + # Initiating Traffic Test on the target + if tid: + remote_libs.start_task(config, endpoints_list, action, tid) + else: + remote_libs.start_task(config, endpoints_list, action) + + # Traffic Test Summary + now_time = datetime.now(timezone('US/Pacific')) + print "-"*65 + print ("Traffic Test Time Summary :") + print "-"*65 + print ("Traffic Started Time:\t\t %s" % (start_time.strftime(fmt))) + print ("Endpoints Discovered Time:\t %s" % (discovery_time.strftime(fmt))) + print ("Current Time:\t\t\t %s" % (now_time.strftime(fmt))) + if action == 'start': + stop_time = now_time + timedelta(seconds=int(config['traffic']['iperf_duration'])) + print ("Estimated Traffic Stop Time:\t %s" % (stop_time.strftime(fmt))) + print "Status:\t\t\t\t\tTraffic Started !!" + else: + print "Status:\t\t\t\t\tTraffic Stopped !!" + print "-"*65 + +if __name__ == '__main__': + main() diff --git a/ntt_tool/apps/cloud/openstackscripts/traffictest/myendpoint.json b/ntt_tool/apps/cloud/openstackscripts/traffictest/myendpoint.json new file mode 100644 index 0000000..068fe00 --- /dev/null +++ b/ntt_tool/apps/cloud/openstackscripts/traffictest/myendpoint.json @@ -0,0 +1,22 @@ +[ +{"contract": [{"action": "allow", + "direction": "in", + "name": "allow_icmp", + "port": "None", + "protocol": "icmp"}, + {"action": "allow", + "direction": "in", + "name": "allow_ssh", + "port": "22", + "protocol": "tcp"}, + {"action": "allow", + "direction": "in", + "name": "allow_udp", + "port": "None", + "protocol": "udp"}], + "dest_eps": ["2.2.2.3"], + "dest_tenant": "admin", + "src_eps": ["3.3.3.3","1.1.1.5"], + "src_tenant": "admin", + "test_type": "intra-tenant" + }] diff --git a/ntt_tool/apps/cloud/openstackscripts/traffictest/requirements.txt b/ntt_tool/apps/cloud/openstackscripts/traffictest/requirements.txt new file mode 100644 index 0000000..17fd141 --- /dev/null +++ b/ntt_tool/apps/cloud/openstackscripts/traffictest/requirements.txt @@ -0,0 +1,42 @@ +Babel==1.3 +cliff==1.13.0 +cmd2==0.6.8 +configobj==5.0.6 +configparser==3.3.0.0r2 +debtcollector==0.5.0 +ecdsa==0.13 +Fabric==1.10.2 +futures==3.0.3 +iso8601==0.1.10 +logging==0.4.9.6 +monotonic==0.2 +msgpack-python==0.4.6 +multiprocessing==2.6.2.1 +netaddr==0.7.15 +netifaces==0.10.4 +oslo.config==1.15.0 +oslo.i18n==2.1.0 +oslo.serialization==1.7.0 +oslo.utils==1.9.0 +paramiko==1.15.2 +pbr==1.3.0 +pexpect==3.3 +prettytable==0.7.2 +pudb==2015.3 +pycrypto==2.6.1 +Pygments==2.0.2 +pyparsing==2.0.3 +python-group-based-policy-client==0.9.2 +python-heatclient==0.6.0 +python-keystoneclient==1.6.0 +python-neutronclient==2.6.0 +python-novaclient==2.17.0 +python-swiftclient==2.4.0 +pytz==2015.4 +PyYAML==3.11 +requests==2.7.0 +simplejson==3.7.3 +six==1.9.0 +stevedore==1.6.0 +urwid==1.3.0 +wrapt==1.10.5 diff --git a/ntt_tool/apps/cloud/openstackscripts/traffictest/sample_endpoints.json b/ntt_tool/apps/cloud/openstackscripts/traffictest/sample_endpoints.json new file mode 100644 index 0000000..c30cbbb --- /dev/null +++ b/ntt_tool/apps/cloud/openstackscripts/traffictest/sample_endpoints.json @@ -0,0 +1,86 @@ +[ +{"contract": [{"action": "allow", + "direction": "in", + "name": "allow_icmp", + "port": "None", + "protocol": "icmp"}, + {"action": "allow", + "direction": "in", + "name": "allow_ssh", + "port": "22", + "protocol": "tcp"}, + {"action": "allow", + "direction": "in", + "name": "allow_udp", + "port": "None", + "protocol": "udp"}], + "dest_eps": ["2.2.2.2"], + "dest_tenant": "admin", + "src_eps": ["1.1.1.2", "1.1.1.4"], + "src_tenant": "admin", + "test_type": "intra-tenant" + }, + {"contract": [{"action": "allow", + "direction": "in", + "name": "allow_icmp", + "port": "None", + "protocol": "icmp"}, + {"action": "allow", + "direction": "in", + "name": "allow_ssh", + "port": "22", + "protocol": "tcp"}, + {"action": "allow", + "direction": "in", + "name": "allow_udp", + "port": "None", + "protocol": "udp"}], + "dest_eps": ["10.1.65.107"], + "dest_tenant": "traffic_test", + "src_eps": ["1.1.1.2", "1.1.1.4", "2.2.2.2"], + "src_tenant": "admin", + "test_type": "inter-tenant" + }, + {"contract": [{"action": "allow", + "direction": "in", + "name": "allow_icmp", + "port": "None", + "protocol": "icmp"}, + {"action": "allow", + "direction": "in", + "name": "allow_ssh", + "port": "22", + "protocol": "tcp"}, + {"action": "allow", + "direction": "in", + "name": "allow_udp", + "port": "None", + "protocol": "udp"}], + "dest_eps": ["10.1.25.129"], + "dest_tenant": "External Host", + "src_eps": ["1.1.1.2", "1.1.1.4", "2.2.2.2"], + "src_tenant": "admin", + "test_type": "south-north" + }, + {"contract": [{"action": "allow", + "direction": "in", + "name": "allow_icmp", + "port": "None", + "protocol": "icmp"}, + {"action": "allow", + "direction": "in", + "name": "allow_ssh", + "port": "22", + "protocol": "tcp"}, + {"action": "allow", + "direction": "in", + "name": "allow_udp", + "port": "None", + "protocol": "udp"}], + "dest_eps": ["10.1.65.102"], + "dest_tenant": "admin", + "src_eps": ["10.1.25.129"], + "src_tenant": "External Host", + "test_type": "north-south" + } +] diff --git a/ntt_tool/apps/cloud/openstackscripts/traffictest/scripts/__init__.py b/ntt_tool/apps/cloud/openstackscripts/traffictest/scripts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ntt_tool/apps/cloud/openstackscripts/traffictest/scripts/get_iperf_tcp_statistics.py b/ntt_tool/apps/cloud/openstackscripts/traffictest/scripts/get_iperf_tcp_statistics.py new file mode 100644 index 0000000..d2cfe47 --- /dev/null +++ b/ntt_tool/apps/cloud/openstackscripts/traffictest/scripts/get_iperf_tcp_statistics.py @@ -0,0 +1,64 @@ +import json +import os +import re +import sys + + +fileid = sys.argv[1] +filedir = os.getcwd() + + +def get_list_of_filenames(): + file_list = [] + for file in os.listdir(filedir): + if file.startswith("tcptesttrafficclient") and file.endswith("%s.json" % (fileid)): + file_list.append(file) + return file_list + + +def process_files(file_list): + out = {} + for file in file_list: + out[file] = get_test_results(file) + return out + + +def get_test_results(file): + """ + For a given test output file, return a tuple of the following format + (bandwidth_loss dict wth keys interval_time, transferred, bandwidth, + jitter, loss_datagram, total_datagram, loss_percent) + """ + + try: + with open(file, 'r') as f: + out = json.loads(f.read()) + result = {} + result['src_endpoints'] = [x.get('local_host') for x in out.get('start', {}).get('connected', [])] + result['dest_endpoints'] = [x.get('remote_host') for x in out.get('start', {}).get('connected', [])] + result['retransmits'] = out.get("end", {}).get("sum_sent", {}).get("retransmits", 0) + result['bandwidth'] = out.get("end", {}).get("sum_sent", {}).get("bits_per_second", 0) + result['interval_time_start'] = out.get("end", {}).get("sum_sent", {}).get("start", 0) + result['interval_time_end'] = out.get("end", {}).get("sum_sent", {}).get("end", 0) + result['bytes_transferred'] = out.get("end", {}).get("sum_sent", {}).get("bytes", 0) + result['cpu_utilization_src'] = out.get("end", {}).get("cpu_utilization_percent", {}).get("host_total", 0) + result['cpu_utilization_dest'] = out.get("end", {}).get("cpu_utilization_percent", {}).get("remote_total", 0) + result['status'] = 'success' + return result + except IOError, e: + raise e + except Exception, e: + raise e + + +def main(): + file_list = get_list_of_filenames() + script_output = process_files(file_list) + json_output = json.JSONEncoder().encode(script_output) + # sample output + # {'test.txt': ({'packet_loss': '0%'}, + # {'rtt_min': '4.3', 'rtt_avg': '5.5', 'rtt_max': '6.3'})} + print json_output + +if __name__ == '__main__': + main() diff --git a/ntt_tool/apps/cloud/openstackscripts/traffictest/scripts/get_iperf_udp_statistics.py b/ntt_tool/apps/cloud/openstackscripts/traffictest/scripts/get_iperf_udp_statistics.py new file mode 100644 index 0000000..51b32ba --- /dev/null +++ b/ntt_tool/apps/cloud/openstackscripts/traffictest/scripts/get_iperf_udp_statistics.py @@ -0,0 +1,116 @@ +import json +import os +import re +import sys + + +fileid = sys.argv[1] +filedir = os.getcwd() + + +def get_list_of_filenames(): + file_list = [] + for file in os.listdir(filedir): + if file.startswith("udptesttrafficclient") and file.endswith("%s.json" % (fileid)): + file_list.append(file) + return file_list + + +def process_files(file_list): + out = {} + for file in file_list: + out[file] = get_test_results(file) + return out + + +def get_test_results(file): + """ + For a given test output file, return a tuple of the following format + (bandwidth_loss dict wth keys interval_time, transferred, bandwidth, + jitter, loss_datagram, total_datagram, loss_percent) + """ + + try: + with open(file, 'r') as f: + out = json.loads(f.read()) + result = {} + result['src_endpoints'] = [x.get('local_host') for x in out.get('start', {}).get('connected', [])] + result['dest_endpoints'] = [x.get('remote_host') for x in out.get('start', {}).get('connected', [])] + result['jitter'] = out.get("end", {}).get("sum", {}).get("jitter_ms", 0) + result['bandwidth'] = out.get("end", {}).get("sum", {}).get("bits_per_second", 0) + result['bandwidth_loss'] = out.get("end", {}).get("sum", {}).get("lost_percent", 0) + result['interval_time_start'] = out.get("end", {}).get("sum", {}).get("start", 0) + result['interval_time_end'] = out.get("end", {}).get("sum", {}).get("end", 0) + result['bytes_transferred'] = out.get("end", {}).get("sum", {}).get("bytes", 0) + result['datagrams'] = out.get("end", {}).get("sum", {}).get("packets", 0) + result['loss_datagrams'] = out.get("end", {}).get("sum", {}).get("loss_packets", 0) + result['cpu_utilization_src'] = out.get("end", {}).get("cpu_utilization_percent", {}).get("host_total", 0) + result['cpu_utilization_dest'] = out.get("end", {}).get("cpu_utilization_percent", {}).get("remote_total", 0) + result['status'] = 'success' + return result + except IOError, e: + raise e + except Exception, e: + raise e + + + # bandwidth_stats = \ + # {'interval_time': '', # NOQA + # 'transferred': '', # NOQA + # 'bandwidth': '', # NOQA + # 'jitter': '', # NOQA + # 'loss_datagram': '', # NOQA + # 'total_datagram': '', # NOQA + # 'loss_percent': ''} + # + # reportflag = False + # print "-" * 100 + # f = open(file, 'r') + # for line in f: + # line + # print "-" * 100 + # + # for line in f: + # if "- - " in line: + # reportflag = True + # if "[ ID]" in line and reportflag: + # report = f.next() + # report_data = report.split(']')[1].split(' ') + # # also want packets transmitted, packets received, % packet loss + # datagram = report.split('/')[2] + # total_datagram = datagram.split('(')[0] + # loss_percent = datagram.split('(')[1].split('%')[0] + # if str(report_data[2]) == 'sec': + # interval_time =str(report_data[1]) + " " + str(report_data[2]) + # loss_datagram = report_data[6].split('/')[0] + # else: + # interval_time =str(report_data[1]) + # loss_datagram = report_data[5].split('/')[0] + # bandwidth_stats = \ + # {'interval_time': interval_time, # NOQA + # 'transferred': str(report_data[3]), # NOQA + # 'bandwidth': str(report_data[4]), # NOQA + # 'jitter': str(report_data[5]), # NOQA + # 'loss_datagram': str(loss_datagram), # NOQA + # 'total_datagram': str(total_datagram), # NOQA + # 'loss_percent': str(loss_percent)} + # + # test_results = {'bandwidth_stats': bandwidth_stats} + # + # return test_results + + +def main(): + try: + file_list = get_list_of_filenames() + script_output = process_files(file_list) + json_output = json.JSONEncoder().encode(script_output) + # sample output + # {'test.txt': ({'packet_loss': '0%'}, + # {'rtt_min': '4.3', 'rtt_avg': '5.5', 'rtt_max': '6.3'})} + print json_output + except Exception, e: + raise e + +if __name__ == '__main__': + main() diff --git a/ntt_tool/apps/cloud/openstackscripts/traffictest/scripts/get_ping_statistics.py b/ntt_tool/apps/cloud/openstackscripts/traffictest/scripts/get_ping_statistics.py new file mode 100644 index 0000000..7e6809b --- /dev/null +++ b/ntt_tool/apps/cloud/openstackscripts/traffictest/scripts/get_ping_statistics.py @@ -0,0 +1,78 @@ +import json +import os +import re +import sys + + +fileid = sys.argv[1] +filedir = os.getcwd() + + +def get_list_of_filenames(): + file_list = [] + for file in os.listdir(filedir): + if file.startswith("test") and file.endswith("%s.txt" % (fileid)): + file_list.append(file) + return file_list + + +def process_files(file_list): + out = {} + for file in file_list: + out[file] = get_test_results(file) + return out + + +def get_test_results(file): + """ + For a given test output file, return a tuple of the following format + (packet_loss dict wth keys packet_loss, + rtt dictionary with keys, rtt_min, rtt_avg, rtt_max) + """ + f = open(file, 'r') + + rtt_regex = ".*= (?P[0-9]+\.[0-9]+)/" \ + "(?P[0-9]+\.[0-9]+)/" \ + "(?P[0-9]+\.[0-9]+) ms" + + packet_loss_regex = "(?P[0-9]+) packets transmitted, " \ + "(?P[0-9]+) packets received," \ + " (?P[0-9]+)%.*" + + for line in f: + if "loss" in line: + # also want packets transmitted, packets received, % packet loss + packet_stats_match = re.match(packet_loss_regex, line) + + packet_stats = \ + {'packets_transmitted': int(packet_stats_match.group('packets_sent')), # NOQA + 'packets_received': int(packet_stats_match.group('packets_received')), # NOQA + 'packet_loss': int(packet_stats_match.group('packet_loss'))} + + if "round-trip" in line: + match_results = re.match(rtt_regex, line) + if (match_results is not None): + rtt_min = match_results.group('rtt_min') + rtt_avg = match_results.group('rtt_avg') + rtt_max = match_results.group('rtt_max') + + rtt = {'rtt_min': rtt_min, + 'rtt_avg': rtt_avg, + 'rtt_max': rtt_max} + + test_results = {'packet_stats': packet_stats, 'rtt': rtt} + + return test_results + + +def main(): + file_list = get_list_of_filenames() + script_output = process_files(file_list) + json_output = json.JSONEncoder().encode(script_output) + # sample output + {'test.txt': ({'packet_loss': '0%'}, + {'rtt_min': '4.3', 'rtt_avg': '5.5', 'rtt_max': '6.3'})} + print json_output + +if __name__ == '__main__': + main() diff --git a/ntt_tool/apps/cloud/openstackscripts/traffictest/traffictest.py b/ntt_tool/apps/cloud/openstackscripts/traffictest/traffictest.py new file mode 100644 index 0000000..16b6f37 --- /dev/null +++ b/ntt_tool/apps/cloud/openstackscripts/traffictest/traffictest.py @@ -0,0 +1,171 @@ +import time +from django.utils import timezone +from ntt_tool.apps.cloud.models import * +from ntt_tool.apps.cloud.serializers import TestRunSerializer +from utils import * +from ntt_tool.apps.cloud.openstackscripts.traffictest.libs import traf_tester + + +class TrafficTest(object): + + def __init__(self, traffic_id): + self.traffic = Traffic.objects.get(pk=traffic_id) + + def run_test(self, started_by, duration=1): + test_run = TestRun() + test_run.traffic = self.traffic + test_run.test_run_duration = duration + test_run.started_by = started_by + test_run.save() + + test_run.report_name = "%s_%s_%s" % (self.traffic.cloud.name, self.traffic.name, test_run.id) + test_run.save() + + try: + endpoints_list = self.generate_endpoints_contract_list() + setup_config = self.generate_setup_config() + + traf_tester.start_task(setup_config, endpoints_list, "start", "_".join(self.traffic.name.split())) + time.sleep(60 * duration) + test_results = traf_tester.start_task(setup_config, endpoints_list, "stop", "_".join(self.traffic.name.split())) + + test_run.completed_on = timezone.now() + test_run.save() + + for test_method, results in test_results.iteritems(): + if test_method == "udp": + for udp_test_result in results: + udp_res_obj = UDPTestResults() + udp_res_obj.traffic_test_run = test_run + udp_res_obj.src_tenant = udp_test_result.get('src_tenant')[0] + udp_res_obj.dest_tenant = udp_test_result.get('dest_tenant')[0] + udp_res_obj.src_ep = udp_test_result.get('src_endpoints')[0] + udp_res_obj.dest_ep = udp_test_result.get('dest_endpoints')[0] + udp_res_obj.status = udp_test_result.get('status') + udp_res_obj.jitter = udp_test_result.get('jitter') + udp_res_obj.bandwidth = udp_test_result.get('bandwidth') + udp_res_obj.bandwidth_loss = udp_test_result.get('bandwidth_loss') + udp_res_obj.interval_time_start = udp_test_result.get('interval_time_start') + udp_res_obj.interval_time_end = udp_test_result.get('interval_time_end') + udp_res_obj.bytes_transferred = udp_test_result.get('bytes_transferred') + udp_res_obj.datagrams = udp_test_result.get('datagrams') + udp_res_obj.loss_datagrams = udp_test_result.get('loss_datagrams') + udp_res_obj.cpu_utilization_src = udp_test_result.get('cpu_utilization_src') + udp_res_obj.cpu_utilization_dest = udp_test_result.get('cpu_utilization_dest') + udp_res_obj.save() + elif test_method == "icmp": + for icmp_test_result in results: + icmp_res_obj = ICMPTestResults() + icmp_res_obj.traffic_test_run = test_run + icmp_res_obj.src_tenant = icmp_test_result.get('src_tenant')[0] + icmp_res_obj.dest_tenant = icmp_test_result.get('dest_tenant')[0] + icmp_res_obj.src_ep = icmp_test_result.get('src_ep') + icmp_res_obj.dest_ep = icmp_test_result.get('dest_ep') + icmp_res_obj.status = icmp_test_result.get('status') + icmp_res_obj.rtt_min = icmp_test_result.get('rtt_min') + icmp_res_obj.rtt_max = icmp_test_result.get('rtt_max') + icmp_res_obj.rtt_avg = icmp_test_result.get('rtt_avg') + icmp_res_obj.packets_received = icmp_test_result.get('packets_received') + icmp_res_obj.packets_transmitted = icmp_test_result.get('packets_transmitted') + icmp_res_obj.packet_loss_percent = icmp_test_result.get('packet_loss_percent') + icmp_res_obj.save() + elif test_method == "tcp": + for tcp_test_result in results: + tcp_res_obj = TCPTestResults() + tcp_res_obj.traffic_test_run = test_run + tcp_res_obj.src_tenant = tcp_test_result.get('src_tenant')[0] + tcp_res_obj.dest_tenant = tcp_test_result.get('dest_tenant')[0] + tcp_res_obj.src_ep = tcp_test_result.get('src_endpoints')[0] + tcp_res_obj.dest_ep = tcp_test_result.get('dest_endpoints')[0] + tcp_res_obj.status = tcp_test_result.get('status') + tcp_res_obj.retransmits = tcp_test_result.get('retransmits') + tcp_res_obj.bandwidth = tcp_test_result.get('bandwidth') + tcp_res_obj.interval_time_start = tcp_test_result.get('interval_time_start') + tcp_res_obj.interval_time_end = tcp_test_result.get('interval_time_end') + tcp_res_obj.bytes_transferred = tcp_test_result.get('bytes_transferred') + tcp_res_obj.cpu_utilization_src = tcp_test_result.get('cpu_utilization_src') + tcp_res_obj.cpu_utilization_dest = tcp_test_result.get('cpu_utilization_dest') + tcp_res_obj.save() + except Exception, e: + test_run.status = "error" + test_run.save() + raise e + test_run.status = "completed" + test_run.save() + + serializer = TestRunSerializer(test_run) + return serializer.data + + def generate_endpoints_contract_list(self): + endpoint_contract_configs = [] + if self.traffic.test_type == 'intra-tenant': + subnet_endpoints_map = {} + endpoints = Endpoint.objects.filter(traffic_id=self.traffic.id) + if self.traffic.test_environment == "dev": + endpoints = endpoints.filter(is_selected=True) + for endpoint in endpoints: + subnet = endpoint.network.subnets.first() + if subnet.subnet_name not in subnet_endpoints_map: + subnet_endpoints_map[subnet.subnet_name] = { + 'endpoints': [], + 'id': subnet.subnet_id, + 'name': subnet.subnet_name + } + subnet_endpoints_map[subnet.subnet_name]['endpoints'].append(endpoint.ip_address) + + net = {} + for entry in subnet_endpoints_map.values(): + tsrc = [] + tdest = [] + tsrc.append(entry['endpoints']) + for nextep in subnet_endpoints_map.values(): + if entry['name'] != nextep['name']: + tdest.append(nextep['endpoints']) + subnet_data = { + 'src_eps': [ep for eps in tsrc for ep in eps], + 'dest_eps': [ep for eps in tdest for ep in eps] + } + net[entry['name']] = subnet_data + + tenants = [x.tenant_name for x in self.traffic.tenants.filter(is_selected=True)] + for k, src_dst_eps in net.iteritems(): + config = { + 'contract': [], + 'test_type': self.traffic.test_type, + 'src_tenant': tenants, + 'dest_eps': src_dst_eps.get('dest_eps'), + 'src_eps': src_dst_eps.get('src_eps') + } + for test_method in self.traffic.test_method.split(','): + config.get('contract').append(test_method_contracts(test_method)) + config['dest_tenant'] = tenants + endpoint_contract_configs.append(config) + return endpoint_contract_configs + + def generate_setup_config(self): + setup_config = {} + cloud = self.traffic.cloud + setup_config['default'] = { + 'keystone_auth_url': cloud.keystone_auth_url, + 'keystone_user': cloud.keystone_user, + 'keystone_password': cloud.keystone_password, + 'keystone_tenant_name': cloud.keystone_tenant_name + } + setup_config['traffic'] = { + 'allowed_delta_percentage': self.traffic.allowed_delta_percentage, + 'test_results_path': self.traffic.test_result_path, + 'number_of_workers': self.traffic.number_of_workers, + 'remote_user': self.traffic.remote_user, + 'remote_pass': self.traffic.remote_pass, + 'test_method': self.traffic.test_method.split(','), + 'iperf_duration': self.traffic.iperf_duration, + 'type': self.traffic.test_type, + 'udp_datagram_size': self.traffic.udp_datagram_size, + } + setup_config['tenants'] = {'tenants': []} + setup_config['tenant_ssh_gateway'] = {} + for tenant in self.traffic.tenants.all(): + setup_config['tenants']['tenants'].append(tenant.tenant_name) + setup_config['tenant_ssh_gateway'][tenant.tenant_name] = self.traffic.ssh_gateway + setup_config['external_host'] = {'host': self.traffic.external_host} + return setup_config diff --git a/ntt_tool/apps/cloud/openstackscripts/traffictest/utils.py b/ntt_tool/apps/cloud/openstackscripts/traffictest/utils.py new file mode 100644 index 0000000..2f9b4d3 --- /dev/null +++ b/ntt_tool/apps/cloud/openstackscripts/traffictest/utils.py @@ -0,0 +1,43 @@ +def get_default_icmp_contract(): + contract = { + 'name': 'allow_icmp', + 'protocol': 'icmp', + 'port': 'None', + 'direction': 'in', + 'action': 'allow' + } + return contract + + +def get_default_tcp_contract(): + contract = { + 'name': 'allow_ssh', + 'protocol': 'tcp', + 'port': '22', + 'direction': 'in', + 'action': 'allow' + } + return contract + + +def get_default_udp_contract(): + contract = { + 'name': 'allow_udp', + 'protocol': 'udp', + 'port': 'None', + 'direction': 'in', + 'action': 'allow' + } + return contract + + +def test_method_contracts(test_method): + function_dict = { + "icmp": get_default_icmp_contract, + "tcp": get_default_tcp_contract, + "udp": get_default_udp_contract + } + return function_dict[test_method]() + + + diff --git a/ntt_tool/apps/cloud/openstackscripts/user.txt b/ntt_tool/apps/cloud/openstackscripts/user.txt new file mode 100644 index 0000000..294cbc2 --- /dev/null +++ b/ntt_tool/apps/cloud/openstackscripts/user.txt @@ -0,0 +1,7 @@ +#cloud-config +chpasswd: + list: | + root:ubuntu + ubuntu:ubuntu + expire: False +ssh_pwauth: True \ No newline at end of file diff --git a/ntt_tool/apps/cloud/serializers.py b/ntt_tool/apps/cloud/serializers.py index edbd1b7..9f4e07d 100644 --- a/ntt_tool/apps/cloud/serializers.py +++ b/ntt_tool/apps/cloud/serializers.py @@ -3,36 +3,100 @@ class CloudSerializer(serializers.ModelSerializer): - class Meta: model = Cloud - exclude = ('updated_on',) + exclude = ('creator', 'updated_on') -class CloudTrafficListSerializer(serializers.ModelSerializer): +class TrafficSerializer(serializers.ModelSerializer): + test_method = serializers.SerializerMethodField() class Meta: - model = CloudTraffic + model = Traffic + exclude = ('creator', 'updated_on',) + + def get_test_method(self, obj): + test_methods = {"icmp": False, "tcp": False, "udp": False} + for test_method in obj.test_method.split(","): + test_methods[test_method] = True + return test_methods -class CloudTrafficRetrieveSerializer(serializers.ModelSerializer): +class TrafficListSerializer(serializers.ModelSerializer): + test_method = serializers.SerializerMethodField() class Meta: - model = CloudTraffic - depth = 1 - exclude = ('created_on', 'updated_on') + model = Traffic + exclude = ('creator', 'updated_on',) + + def get_test_method(self, obj): + test_methods = {"icmp": False, "tcp": False, "udp": False} + for test_method in obj.test_method.split(","): + test_methods[test_method] = True + return test_methods + + +class TenantListSerializer(serializers.ModelSerializer): + class Meta: + model = Tenant + fields = ('id', 'tenant_id', 'tenant_name', 'enabled', 'is_selected') + + +class TenantSerializer(serializers.ModelSerializer): + class Meta: + model = Tenant + exclude = ('creator', 'created_on', 'updated_on',) + + +class SubnetSerializer(serializers.ModelSerializer): + class Meta: + model = Subnet + +class NetworkSerializer(serializers.ModelSerializer): + subnets = SubnetSerializer(many=True, read_only=True) + + class Meta: + model = Network + exclude = ('creator', 'created_on', 'updated_on',) + + +class EndpointSerializer(serializers.ModelSerializer): + network_id = serializers.IntegerField(source='network.id', read_only=True) + network_name = serializers.CharField(source="network.network_name", read_only=True) + + class Meta: + model = Endpoint + fields = ('id', 'network_id', 'network_name', 'endpoint_id', 'name', 'status', 'ip_address', 'is_selected') + + +class UDPTestResultSerializer(serializers.ModelSerializer): + class Meta: + model = UDPTestResults -class CloudTrafficSerializer(serializers.ModelSerializer): +class ICMPTestResultSerializer(serializers.ModelSerializer): class Meta: - model = CloudTraffic + model = ICMPTestResults -class CloudTrafficTenantSerializer(serializers.ModelSerializer): +class TCPTestResultSerializer(serializers.ModelSerializer): + class Meta: + model = TCPTestResults + + +class TestRunSerializer(serializers.ModelSerializer): + udp_test_results = UDPTestResultSerializer(many=True, read_only=True) + icmp_test_results = ICMPTestResultSerializer(many=True, read_only=True) + tcp_test_results = TCPTestResultSerializer(many=True, read_only=True) class Meta: - model = CloudTrafficTenants + model = TestRun + depth = 1 +class TestRunListSerializer(serializers.ModelSerializer): + traffic_name = serializers.CharField(source='traffic.name', read_only=True) + class Meta: + model = TestRun \ No newline at end of file diff --git a/ntt_tool/apps/cloud/urls.py b/ntt_tool/apps/cloud/urls.py index 91db229..e5b77dc 100644 --- a/ntt_tool/apps/cloud/urls.py +++ b/ntt_tool/apps/cloud/urls.py @@ -4,10 +4,10 @@ router = routers.SimpleRouter() router.register(r'cloud', CloudViewSet) -router.register(r'cloudtraffic', CloudTrafficViewSet) -router.register(r'cloudtraffictenant', CloudTrafficTenantsViewSet) +router.register(r'traffic', TrafficViewSet) +router.register(r'tenant', TenantViewSet) +router.register(r'network', NetworkViewSet) +router.register(r'endpoint', EndpointViewSet) -urlpatterns = [ - # url('^cloudtraffics/(?P.+)/$', CloudTrafficListView.as_view()), -] +urlpatterns = [] urlpatterns += router.urls diff --git a/ntt_tool/apps/cloud/views.py b/ntt_tool/apps/cloud/views.py index 17c21b9..676ff6c 100644 --- a/ntt_tool/apps/cloud/views.py +++ b/ntt_tool/apps/cloud/views.py @@ -1,29 +1,66 @@ +import json +from django.http import HttpResponse +from django.template.loader import get_template from rest_framework import viewsets -from rest_framework import generics -from rest_framework import status +from rest_framework.decorators import list_route, detail_route from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework_jwt.authentication import JSONWebTokenAuthentication -from models import * +from ntt_tool.utils.reportutils import render_to_pdf from serializers import * +from keystoneclient import exceptions + +from openstackscripts.keystoneclientutils import KeystoneClientUtils +from openstackscripts.neutronclientutils import NeutronClientUtils +from openstackscripts.tenantnetworkdiscovery import * +from openstackscripts.credentials import * +from openstackscripts.endpoints import DiscoverEndpoints, LaunchEndpoints +from openstackscripts.traffictest.traffictest import TrafficTest + +from rest_framework import status +from django.core.mail import EmailMessage +from django.conf import settings + class CloudViewSet(viewsets.ModelViewSet): queryset = Cloud.objects.all() serializer_class = CloudSerializer - authentication_classes = [JSONWebTokenAuthentication] - permission_classes = [IsAuthenticated] + authentication_classes = (JSONWebTokenAuthentication,) + permission_classes = (IsAuthenticated,) + + def perform_create(self, serializer): + serializer.save(creator=self.request.user) -class CloudTrafficViewSet(viewsets.ModelViewSet): - queryset = CloudTraffic.objects.all() - serializer_class = CloudTrafficSerializer +class TrafficViewSet(viewsets.ModelViewSet): + queryset = Traffic.objects.all() + serializer_class = TrafficSerializer authentication_classes = (JSONWebTokenAuthentication,) permission_classes = (IsAuthenticated,) + action_serializers = { + 'retrieve': TrafficSerializer, + 'list': TrafficListSerializer, + 'create': TrafficSerializer, + 'update': TrafficSerializer, + } + + def get_serializer_class(self): + """ + Overriding method to get custom serializer classes based on request method. + """ + if hasattr(self, 'action_serializers'): + if self.action in self.action_serializers: + return self.action_serializers[self.action] + return super(TrafficViewSet, self).get_serializer_class() def list(self, request, *args, **kwargs): - cloud_id = self.request.GET.get("cloud_id") - queryset = self.filter_queryset(CloudTraffic.objects.filter(cloud_id=cloud_id)) + """ + Overriding list method to write custom queryset for retriveing traffic related to cloud. + """ + cloud_id = self.request.GET.get('cloud_id') + queryset = self.filter_queryset( + Traffic.objects.filter(cloud_id=cloud_id)) page = self.paginate_queryset(queryset) if page is not None: @@ -33,16 +70,90 @@ def list(self, request, *args, **kwargs): serializer = self.get_serializer(queryset, many=True) return Response(serializer.data) + def perform_create(self, serializer): + """ + Overriding method to provide selected tenants, creator and cloud_id at the time of creating traffic. + """ + serializer.save( + test_method=self.request.data.get("test_method"), + creator=self.request.user, + cloud_id=self.request.data.get('cloud_id') + ) + + def perform_update(self, serializer): + serializer.save(test_method=self.request.data.get("test_method")) + + @detail_route(methods=['get'], url_path='run/test') + def run_traffic_test(self, request, pk=None): + traffic_test = TrafficTest(pk) + duration = int(request.GET.get('duration')) + test_result = traffic_test.run_test(request.user, duration=duration) + return Response(test_result) + + @list_route(methods=['get'], url_path='reports/(?P[-\w]+)') + def list_reports(self, request, pk): + test_runs = TestRun.objects.filter(traffic_id=pk).order_by("-started_on") + serializer = TestRunListSerializer(test_runs, many=True) + return Response(serializer.data) + + @list_route(methods=['get'], url_path='report/(?P[-\w]+)') + def report(self, request, test_run_id=None): + test_run = TestRun.objects.get(pk=test_run_id) + serializer = TestRunSerializer(test_run) + return Response(serializer.data) + + @list_route(methods=['get', 'post'], url_path='report/download/(?P[-\w]+)') + def download_report(self, request, test_run_id=None): + test_run = TestRun.objects.get(pk=test_run_id) + serializer = TestRunSerializer(test_run) + pdf = render_to_pdf('reports/traffic_test_report.html', serializer.data) + return HttpResponse(pdf.getvalue(), content_type='application/pdf') + + @list_route(methods=['get'], url_path='report/email/(?P[-\w]+)') + def email_report(self, request, test_run_id=None): + test_run = TestRun.objects.get(pk=test_run_id) + serializer = TestRunSerializer(test_run) + pdf = render_to_pdf('reports/traffic_test_report.html', serializer.data) + + context = {"user_full_name": request.user.get_full_name()} + email_content = get_template('email_templates/traffic_test_report.html').render(context) + mail = EmailMessage("Traffic Test Report", + email_content, + to=['abdulgaffar@onecloudinc.com', request.user.email, 'keerthiv@onecloudinc.com'], + from_email=settings.EMAIL_HOST_USER) + mail.content_subtype = 'html' + mail.attach("report.pdf", pdf.getvalue(), 'application/pdf') + mail.send() + return Response(True) + + @list_route(methods=["delete"], url_path="report/delete/(?P[-\w]+)") + def delete_report(self, request, test_run_id=None): + TestRun.objects.get(pk=test_run_id).delete() + return Response(True) -class CloudTrafficTenantsViewSet(viewsets.ModelViewSet): - queryset = CloudTrafficTenants.objects.all() - serializer_class = CloudTrafficTenantSerializer + +class TenantViewSet(viewsets.ModelViewSet): + queryset = Tenant.objects.all() + serializer_class = TenantSerializer authentication_classes = (JSONWebTokenAuthentication,) permission_classes = (IsAuthenticated,) + action_serializers = { + 'retrieve': TenantSerializer, + 'list': TenantSerializer, + 'create': TenantSerializer, + 'update': TenantSerializer, + } + + def get_serializer_class(self): + if hasattr(self, 'action_serializers'): + if self.action in self.action_serializers: + return self.action_serializers[self.action] + return super(TrafficViewSet, self).get_serializer_class() def list(self, request, *args, **kwargs): traffic_id = self.request.GET.get("traffic_id") - queryset = self.filter_queryset(CloudTrafficTenants.objects.filter(cloud_traffic_id=traffic_id)) + queryset = self.filter_queryset( + Tenant.objects.filter(traffic_id=traffic_id)) page = self.paginate_queryset(queryset) if page is not None: @@ -52,24 +163,105 @@ def list(self, request, *args, **kwargs): serializer = self.get_serializer(queryset, many=True) return Response(serializer.data) - def create(self, request, *args, **kwargs): - tenant = CloudTrafficTenants() - tenant.cloud_traffic_id = request.data.get("cloud_traffic_id") - tenant.tenant_name = request.data.get("tenant_name") - tenant.ssh_gateway = request.data.get("ssh_gateway") - tenant.creator = request.user - tenant.save() - - serializer = CloudTrafficTenantSerializer(tenant) - headers = self.get_success_headers(serializer.data) - return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) - - def update(self, request, *args, **kwargs): - import pdb - pdb.set_trace() - partial = kwargs.pop('partial', False) - instance = self.get_object() - serializer = self.get_serializer(instance, data=request.data, partial=partial) - serializer.is_valid(raise_exception=True) - self.perform_update(serializer) + @list_route(methods=["get"], url_path="discover") + def discover(self, request): + cloud = Cloud.objects.get(pk=request.GET.get("cloud_id"), creator=request.user) + try: + keystone_utils = KeystoneClientUtils(**get_credentials(cloud)) + except exceptions.Unauthorized, e: + return Response("Unauthorized: %s" % (e.message), status=status.HTTP_500_INTERNAL_SERVER_ERROR) + except exceptions.AuthorizationFailure, e: + return Response(e.message, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + tenants_ctrl = TenantsController(request.GET.get("traffic_id")) + tenants = tenants_ctrl.save(request.user, keystone_utils.get_tenants()) + serializer = TenantListSerializer(tenants, many=True) + return Response(serializer.data) + + +class NetworkViewSet(viewsets.ModelViewSet): + queryset = Network.objects.all() + serializer_class = NetworkSerializer + authentication_classes = (JSONWebTokenAuthentication,) + permission_classes = (IsAuthenticated,) + + def list(self, request, *args, **kwargs): + tenant_id = self.request.GET.get("tenant_id") + networks = Network.objects.filter(tenant_id=tenant_id) + + serializer = self.get_serializer(networks, many=True) + return Response(serializer.data) + + @list_route(methods=['get'], url_path="discover") + def discover(self, request): + tenant = Tenant.objects.get(pk=request.GET.get("tenant_id")) + neutron_credentials = get_credentials(tenant.traffic.cloud) + neutron_utils = NeutronClientUtils(**neutron_credentials) + networks = neutron_utils.list_networks(tenant_id=tenant.tenant_id) + + network_ctrl = NetworkController() + network_objs = network_ctrl.save(neutron_utils, request.user, tenant, networks) + + serializer = NetworkSerializer(network_objs, many=True) + return Response(serializer.data) + + +class EndpointViewSet(viewsets.ModelViewSet): + queryset = Endpoint.objects.all() + serializer_class = EndpointSerializer + authentication_classes = (JSONWebTokenAuthentication,) + permission_classes = (IsAuthenticated,) + + def list(self, request, *args, **kwargs): + queryset = self.filter_queryset(self.get_queryset()) + queryset = queryset.filter(traffic_id=self.request.GET.get('traffic_id')) + + page = self.paginate_queryset(queryset) + if page is not None: + serializer = self.get_serializer(page, many=True) + return self.get_paginated_response(serializer.data) + + serializer = self.get_serializer(queryset, many=True) + return Response(serializer.data) + + @list_route(methods=['post'], url_path='discover/(?P[-\w]+)') + def discover(self, request, traffic_id=None): + endpoints_list = [] + traffic = Traffic.objects.get(pk=traffic_id) + + with transaction.atomic(): + Network.objects.filter(tenant__traffic_id=traffic.id).update(is_selected=False) + for selected_network in json.loads(request.data.get("json", '[]')): + network = Network.objects.get(pk=selected_network.get("network_id")) + network.is_selected = True + network.save() + + first_subnet = network.subnets.first() + first_subnet.ip_range_start = selected_network.get("ip_range_start") + first_subnet.ip_range_end = selected_network.get("ip_range_end") + first_subnet.save() + + endpoint_discovery = DiscoverEndpoints(traffic, network) + endpoints = endpoint_discovery.get_endpoints(first_subnet) + endpoints_list.extend(endpoints) + + serializer = EndpointSerializer(endpoints_list, many=True) + return Response(serializer.data) + + @list_route(methods=['post'], url_path='launch/(?P[-\w]+)') + def launch(self, request, traffic_id=None): + traffic = Traffic.objects.get(pk=traffic_id) + nova_credentials = get_nova_credentials(traffic.cloud) + launch_ep_obj = LaunchEndpoints(**nova_credentials) + endpoints = launch_ep_obj.launch_endpoints(request, traffic) + + serializer = EndpointSerializer(endpoints, many=True) + return Response(serializer.data) + + @detail_route(methods=["get"], url_path="select") + def select(self, request, pk=None): + endpoint = Endpoint.objects.get(pk=pk) + endpoint.is_selected = json.loads(request.GET.get("is_selected")) + endpoint.save() + + serializer = EndpointSerializer(endpoint) return Response(serializer.data) diff --git a/ntt_tool/apps/user/jwtutils.py b/ntt_tool/apps/user/jwtutils.py new file mode 100644 index 0000000..475e43a --- /dev/null +++ b/ntt_tool/apps/user/jwtutils.py @@ -0,0 +1,8 @@ +from serializers import UserSerializer + + +def jwt_response_payload_handler(token, user=None, request=None): + return { + 'token': token, + 'user': UserSerializer(user).data + } diff --git a/ntt_tool/apps/user/serializers.py b/ntt_tool/apps/user/serializers.py index 44482c9..c39277a 100644 --- a/ntt_tool/apps/user/serializers.py +++ b/ntt_tool/apps/user/serializers.py @@ -4,4 +4,7 @@ class UserSerializer(serializers.ModelSerializer): class Meta: - model = User \ No newline at end of file + model = User + fields = ('username', 'first_name', 'last_name') + + diff --git a/ntt_tool/media/osimages/trafficimage.qcow2 b/ntt_tool/media/osimages/trafficimage.qcow2 new file mode 100644 index 0000000..385c6ff --- /dev/null +++ b/ntt_tool/media/osimages/trafficimage.qcow2 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8d2aac38a48cfe4728eba22614b6cb1c7eb99fa27fbdb35eb1b82bd6eacefac6 +size 1165950976 diff --git a/ntt_tool/media/osimages/ubuntu.img b/ntt_tool/media/osimages/ubuntu.img new file mode 100644 index 0000000..4264222 --- /dev/null +++ b/ntt_tool/media/osimages/ubuntu.img @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:32d142beddf51b7439eebb319b9185458e9823f75355bf9b60531186e8030657 +size 259260928 diff --git a/ntt_tool/ntt.sql b/ntt_tool/ntt.sql new file mode 100644 index 0000000..42234bb --- /dev/null +++ b/ntt_tool/ntt.sql @@ -0,0 +1,408 @@ +-- MySQL dump 10.13 Distrib 5.6.25, for debian-linux-gnu (x86_64) +-- +-- Host: 127.0.0.1 Database: ntt +-- ------------------------------------------------------ +-- Server version 5.6.25-0ubuntu0.15.04.1 + +/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; +/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; +/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; +/*!40101 SET NAMES utf8 */; +/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; +/*!40103 SET TIME_ZONE='+00:00' */; +/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; +/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; +/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; +/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; + +-- +-- Table structure for table `auth_group` +-- + +DROP TABLE IF EXISTS `auth_group`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `auth_group` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `name` varchar(80) NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `name` (`name`) +) ENGINE=InnoDB DEFAULT CHARSET=latin1; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `auth_group` +-- + +LOCK TABLES `auth_group` WRITE; +/*!40000 ALTER TABLE `auth_group` DISABLE KEYS */; +/*!40000 ALTER TABLE `auth_group` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `auth_group_permissions` +-- + +DROP TABLE IF EXISTS `auth_group_permissions`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `auth_group_permissions` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `group_id` int(11) NOT NULL, + `permission_id` int(11) NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `auth_group_permissions_group_id_0cd325b0_uniq` (`group_id`,`permission_id`), + KEY `auth_group_permissi_permission_id_84c5c92e_fk_auth_permission_id` (`permission_id`), + CONSTRAINT `auth_group_permissi_permission_id_84c5c92e_fk_auth_permission_id` FOREIGN KEY (`permission_id`) REFERENCES `auth_permission` (`id`), + CONSTRAINT `auth_group_permissions_group_id_b120cbf9_fk_auth_group_id` FOREIGN KEY (`group_id`) REFERENCES `auth_group` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=latin1; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `auth_group_permissions` +-- + +LOCK TABLES `auth_group_permissions` WRITE; +/*!40000 ALTER TABLE `auth_group_permissions` DISABLE KEYS */; +/*!40000 ALTER TABLE `auth_group_permissions` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `auth_permission` +-- + +DROP TABLE IF EXISTS `auth_permission`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `auth_permission` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `name` varchar(255) NOT NULL, + `content_type_id` int(11) NOT NULL, + `codename` varchar(100) NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `auth_permission_content_type_id_01ab375a_uniq` (`content_type_id`,`codename`), + CONSTRAINT `auth_permissi_content_type_id_2f476e4b_fk_django_content_type_id` FOREIGN KEY (`content_type_id`) REFERENCES `django_content_type` (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=28 DEFAULT CHARSET=latin1; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `auth_permission` +-- + +LOCK TABLES `auth_permission` WRITE; +/*!40000 ALTER TABLE `auth_permission` DISABLE KEYS */; +INSERT INTO `auth_permission` VALUES (1,'Can add log entry',1,'add_logentry'),(2,'Can change log entry',1,'change_logentry'),(3,'Can delete log entry',1,'delete_logentry'),(4,'Can add permission',2,'add_permission'),(5,'Can change permission',2,'change_permission'),(6,'Can delete permission',2,'delete_permission'),(7,'Can add group',3,'add_group'),(8,'Can change group',3,'change_group'),(9,'Can delete group',3,'delete_group'),(10,'Can add user',4,'add_user'),(11,'Can change user',4,'change_user'),(12,'Can delete user',4,'delete_user'),(13,'Can add content type',5,'add_contenttype'),(14,'Can change content type',5,'change_contenttype'),(15,'Can delete content type',5,'delete_contenttype'),(16,'Can add session',6,'add_session'),(17,'Can change session',6,'change_session'),(18,'Can delete session',6,'delete_session'),(19,'Can add cloud',7,'add_cloud'),(20,'Can change cloud',7,'change_cloud'),(21,'Can delete cloud',7,'delete_cloud'),(22,'Can add cloud traffic',8,'add_cloudtraffic'),(23,'Can change cloud traffic',8,'change_cloudtraffic'),(24,'Can delete cloud traffic',8,'delete_cloudtraffic'),(25,'Can add cloud traffic tenants',9,'add_cloudtraffictenants'),(26,'Can change cloud traffic tenants',9,'change_cloudtraffictenants'),(27,'Can delete cloud traffic tenants',9,'delete_cloudtraffictenants'); +/*!40000 ALTER TABLE `auth_permission` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `auth_user` +-- + +DROP TABLE IF EXISTS `auth_user`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `auth_user` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `password` varchar(128) NOT NULL, + `last_login` datetime(6) DEFAULT NULL, + `is_superuser` tinyint(1) NOT NULL, + `username` varchar(30) NOT NULL, + `first_name` varchar(30) NOT NULL, + `last_name` varchar(30) NOT NULL, + `email` varchar(254) NOT NULL, + `is_staff` tinyint(1) NOT NULL, + `is_active` tinyint(1) NOT NULL, + `date_joined` datetime(6) NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `username` (`username`) +) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=latin1; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `auth_user` +-- + +LOCK TABLES `auth_user` WRITE; +/*!40000 ALTER TABLE `auth_user` DISABLE KEYS */; +INSERT INTO `auth_user` VALUES (1,'pbkdf2_sha256$24000$z6Uj4Y0v3T7h$sGmO9zwP2Xxb/Zb1ecxR2gVMKuKGDUWJW1s7F0l00gg=',NULL,1,'admin','','','admin@gmail.com',1,1,'2016-02-27 05:34:15.827140'); +/*!40000 ALTER TABLE `auth_user` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `auth_user_groups` +-- + +DROP TABLE IF EXISTS `auth_user_groups`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `auth_user_groups` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `user_id` int(11) NOT NULL, + `group_id` int(11) NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `auth_user_groups_user_id_94350c0c_uniq` (`user_id`,`group_id`), + KEY `auth_user_groups_group_id_97559544_fk_auth_group_id` (`group_id`), + CONSTRAINT `auth_user_groups_group_id_97559544_fk_auth_group_id` FOREIGN KEY (`group_id`) REFERENCES `auth_group` (`id`), + CONSTRAINT `auth_user_groups_user_id_6a12ed8b_fk_auth_user_id` FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=latin1; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `auth_user_groups` +-- + +LOCK TABLES `auth_user_groups` WRITE; +/*!40000 ALTER TABLE `auth_user_groups` DISABLE KEYS */; +/*!40000 ALTER TABLE `auth_user_groups` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `auth_user_user_permissions` +-- + +DROP TABLE IF EXISTS `auth_user_user_permissions`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `auth_user_user_permissions` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `user_id` int(11) NOT NULL, + `permission_id` int(11) NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `auth_user_user_permissions_user_id_14a6b632_uniq` (`user_id`,`permission_id`), + KEY `auth_user_user_perm_permission_id_1fbb5f2c_fk_auth_permission_id` (`permission_id`), + CONSTRAINT `auth_user_user_perm_permission_id_1fbb5f2c_fk_auth_permission_id` FOREIGN KEY (`permission_id`) REFERENCES `auth_permission` (`id`), + CONSTRAINT `auth_user_user_permissions_user_id_a95ead1b_fk_auth_user_id` FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=latin1; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `auth_user_user_permissions` +-- + +LOCK TABLES `auth_user_user_permissions` WRITE; +/*!40000 ALTER TABLE `auth_user_user_permissions` DISABLE KEYS */; +/*!40000 ALTER TABLE `auth_user_user_permissions` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `cloud_details` +-- + +DROP TABLE IF EXISTS `cloud_details`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `cloud_details` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `name` varchar(256) NOT NULL, + `keystone_auth_url` varchar(2083) NOT NULL, + `keystone_user` varchar(100) NOT NULL, + `keystone_password` varchar(250) NOT NULL, + `keystone_tenant_name` varchar(100) NOT NULL, + `created_on` datetime(6) NOT NULL, + `updated_on` datetime(6) NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=latin1; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `cloud_details` +-- + +LOCK TABLES `cloud_details` WRITE; +/*!40000 ALTER TABLE `cloud_details` DISABLE KEYS */; +INSERT INTO `cloud_details` VALUES (1,'Onecloud','https://github.com/onecloud/ntt_tool_python','admin','admin','admin','2016-03-01 05:12:15.246958','2016-02-27 05:35:21.670275'),(2,'CloudUno','https://github.com/onecloud/ntt_tool_python','test','test','test','2016-02-27 05:58:02.993225','2016-02-27 05:58:02.993265'),(3,'Airtel','http://10.1.32.11:5000/v2.0','admin','oc123','admin','2016-02-29 09:56:59.959767','2016-02-29 09:56:59.959941'),(4,'Vodafone','http://h.com','hello','aadsfsaf','vodafone chennai','2016-02-29 10:35:12.595484','2016-02-29 10:35:12.595531'); +/*!40000 ALTER TABLE `cloud_details` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `cloud_traffic` +-- + +DROP TABLE IF EXISTS `cloud_traffic`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `cloud_traffic` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `name` varchar(256) NOT NULL, + `allowed_delta_percentage` double NOT NULL, + `test_result_path` varchar(250) NOT NULL, + `number_of_workers` int(11) NOT NULL, + `remote_user` varchar(100) NOT NULL, + `remote_pass` varchar(100) NOT NULL, + `test_method` varchar(100) NOT NULL, + `iperf_duration` int(11) NOT NULL, + `tenant_type` varchar(20) NOT NULL, + `created_on` datetime(6) NOT NULL, + `updated_on` datetime(6) NOT NULL, + `cloud_id` int(11) NOT NULL, + `external_host` varchar(100), + PRIMARY KEY (`id`), + KEY `cloud_traffic_cloud_id_ac74e46e_fk_cloud_details_id` (`cloud_id`), + CONSTRAINT `cloud_traffic_cloud_id_ac74e46e_fk_cloud_details_id` FOREIGN KEY (`cloud_id`) REFERENCES `cloud_details` (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=latin1; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `cloud_traffic` +-- + +LOCK TABLES `cloud_traffic` WRITE; +/*!40000 ALTER TABLE `cloud_traffic` DISABLE KEYS */; +INSERT INTO `cloud_traffic` VALUES (1,'Hello Traffic',1,'sadad',1000,'ads','asd','asdfas',1,'all','2016-02-29 13:14:13.484433','2016-02-27 05:40:15.840123',1,'192.168.12.9'),(2,'vodafone bangalore',3,'/home/',2,'admin','admin','true',60,'all','2016-02-29 10:51:36.858036','2016-02-29 10:51:36.858071',4,NULL),(3,'Hello Traffic',2,'/home/',1000,'admin','admin','admintest',1,'all','2016-02-29 13:37:28.179671','2016-02-29 13:37:28.179714',1,NULL); +/*!40000 ALTER TABLE `cloud_traffic` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `cloud_traffic_tenants` +-- + +DROP TABLE IF EXISTS `cloud_traffic_tenants`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `cloud_traffic_tenants` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `tenant_name` varchar(256) NOT NULL, + `ssh_gateway` varchar(256) DEFAULT NULL, + `created_on` datetime(6) NOT NULL, + `updated_on` datetime(6) NOT NULL, + `cloud_traffic_id` int(11) NOT NULL, + `creator_id` int(11) NOT NULL, + PRIMARY KEY (`id`), + KEY `cloud_traffic_tena_cloud_traffic_id_1a906c3d_fk_cloud_traffic_id` (`cloud_traffic_id`), + KEY `cloud_traffic_tenants_creator_id_36dea512_fk_auth_user_id` (`creator_id`), + CONSTRAINT `cloud_traffic_tena_cloud_traffic_id_1a906c3d_fk_cloud_traffic_id` FOREIGN KEY (`cloud_traffic_id`) REFERENCES `cloud_traffic` (`id`), + CONSTRAINT `cloud_traffic_tenants_creator_id_36dea512_fk_auth_user_id` FOREIGN KEY (`creator_id`) REFERENCES `auth_user` (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=latin1; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `cloud_traffic_tenants` +-- + +LOCK TABLES `cloud_traffic_tenants` WRITE; +/*!40000 ALTER TABLE `cloud_traffic_tenants` DISABLE KEYS */; +INSERT INTO `cloud_traffic_tenants` VALUES (2,'admin','192.168.12.9','2016-02-27 05:42:55.338374','2016-02-27 05:42:55.338482',1,1),(3,'172.168.2.44:8000','172.168.2.44:8000','2016-02-29 11:58:09.559724','2016-02-29 11:58:09.559757',2,1); +/*!40000 ALTER TABLE `cloud_traffic_tenants` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `django_admin_log` +-- + +DROP TABLE IF EXISTS `django_admin_log`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `django_admin_log` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `action_time` datetime(6) NOT NULL, + `object_id` longtext, + `object_repr` varchar(200) NOT NULL, + `action_flag` smallint(5) unsigned NOT NULL, + `change_message` longtext NOT NULL, + `content_type_id` int(11) DEFAULT NULL, + `user_id` int(11) NOT NULL, + PRIMARY KEY (`id`), + KEY `django_admin__content_type_id_c4bce8eb_fk_django_content_type_id` (`content_type_id`), + KEY `django_admin_log_user_id_c564eba6_fk_auth_user_id` (`user_id`), + CONSTRAINT `django_admin__content_type_id_c4bce8eb_fk_django_content_type_id` FOREIGN KEY (`content_type_id`) REFERENCES `django_content_type` (`id`), + CONSTRAINT `django_admin_log_user_id_c564eba6_fk_auth_user_id` FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=latin1; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `django_admin_log` +-- + +LOCK TABLES `django_admin_log` WRITE; +/*!40000 ALTER TABLE `django_admin_log` DISABLE KEYS */; +/*!40000 ALTER TABLE `django_admin_log` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `django_content_type` +-- + +DROP TABLE IF EXISTS `django_content_type`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `django_content_type` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `app_label` varchar(100) NOT NULL, + `model` varchar(100) NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `django_content_type_app_label_76bd3d3b_uniq` (`app_label`,`model`) +) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=latin1; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `django_content_type` +-- + +LOCK TABLES `django_content_type` WRITE; +/*!40000 ALTER TABLE `django_content_type` DISABLE KEYS */; +INSERT INTO `django_content_type` VALUES (1,'admin','logentry'),(3,'auth','group'),(2,'auth','permission'),(4,'auth','user'),(7,'cloud','cloud'),(8,'cloud','cloudtraffic'),(9,'cloud','cloudtraffictenants'),(5,'contenttypes','contenttype'),(6,'sessions','session'); +/*!40000 ALTER TABLE `django_content_type` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `django_migrations` +-- + +DROP TABLE IF EXISTS `django_migrations`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `django_migrations` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `app` varchar(255) NOT NULL, + `name` varchar(255) NOT NULL, + `applied` datetime(6) NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=17 DEFAULT CHARSET=latin1; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `django_migrations` +-- + +LOCK TABLES `django_migrations` WRITE; +/*!40000 ALTER TABLE `django_migrations` DISABLE KEYS */; +INSERT INTO `django_migrations` VALUES (1,'contenttypes','0001_initial','2016-02-27 05:30:29.318686'),(2,'auth','0001_initial','2016-02-27 05:30:30.564825'),(3,'admin','0001_initial','2016-02-27 05:30:30.919881'),(4,'admin','0002_logentry_remove_auto_add','2016-02-27 05:30:30.941389'),(5,'contenttypes','0002_remove_content_type_name','2016-02-27 05:30:31.229480'),(6,'auth','0002_alter_permission_name_max_length','2016-02-27 05:30:31.359888'),(7,'auth','0003_alter_user_email_max_length','2016-02-27 05:30:31.463488'),(8,'auth','0004_alter_user_username_opts','2016-02-27 05:30:31.476924'),(9,'auth','0005_alter_user_last_login_null','2016-02-27 05:30:31.600098'),(10,'auth','0006_require_contenttypes_0002','2016-02-27 05:30:31.605120'),(11,'auth','0007_alter_validators_add_error_messages','2016-02-27 05:30:31.616106'),(12,'cloud','0001_initial','2016-02-27 05:30:32.306419'),(13,'cloud','0002_auto_20160225_1631','2016-02-27 05:30:32.365825'),(14,'cloud','0003_auto_20160225_1726','2016-02-27 05:30:32.625484'),(15,'cloud','0004_auto_20160226_1322','2016-02-27 05:30:32.880957'),(16,'sessions','0001_initial','2016-02-27 05:30:32.969106'); +/*!40000 ALTER TABLE `django_migrations` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `django_session` +-- + +DROP TABLE IF EXISTS `django_session`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `django_session` ( + `session_key` varchar(40) NOT NULL, + `session_data` longtext NOT NULL, + `expire_date` datetime(6) NOT NULL, + PRIMARY KEY (`session_key`), + KEY `django_session_de54fa62` (`expire_date`) +) ENGINE=InnoDB DEFAULT CHARSET=latin1; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `django_session` +-- + +LOCK TABLES `django_session` WRITE; +/*!40000 ALTER TABLE `django_session` DISABLE KEYS */; +/*!40000 ALTER TABLE `django_session` ENABLE KEYS */; +UNLOCK TABLES; +/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; + +/*!40101 SET SQL_MODE=@OLD_SQL_MODE */; +/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; +/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; +/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; +/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; +/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; +/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; + +-- Dump completed on 2016-03-01 10:47:01 diff --git a/ntt_tool/settings.py b/ntt_tool/settings.py index a4facaf..efa5bca 100644 --- a/ntt_tool/settings.py +++ b/ntt_tool/settings.py @@ -127,8 +127,9 @@ STATIC_URL = '/static/' STATICFILES_DIRS = [ - os.path.join(BASE_DIR, "ntt_tool/static") + os.path.join(BASE_DIR, os.path.join("ntt_tool", "static")) ] +MEDIA_ROOT = os.path.join(BASE_DIR, os.path.join("ntt_tool", "media")) REST_FRAMEWORK = { # Use Django's standard `django.contrib.auth` permissions, @@ -141,13 +142,23 @@ # 'rest_framework.authentication.BasicAuthentication', 'rest_framework_jwt.authentication.JSONWebTokenAuthentication', ], + 'EXCEPTION_HANDLER': 'ntt_tool.utils.custom_exception_handler.custom_exception_handler', 'PAGINATE_BY': 10, } JWT_AUTH = { + 'JWT_RESPONSE_PAYLOAD_HANDLER': 'ntt_tool.apps.user.jwtutils.jwt_response_payload_handler', 'JWT_EXPIRATION_DELTA': datetime.timedelta(days=10), 'JWT_ALLOW_REFRESH': True, - 'JWT_REFRESH_EXPIRATION_DELTA': datetime.timedelta(days=10) + 'JWT_REFRESH_EXPIRATION_DELTA': datetime.timedelta(days=10), } +# JWT_RESPONSE_PAYLOAD_HANDLER = 'ntt_tool.apps.user.jwtutils.jwt_response_payload_handler', # LOGIN_REDIRECT_URL = '/api/profile/' + +# E-Mail Settings +EMAIL_HOST = 'smtp.gmail.com' +EMAIL_PORT = "587" +EMAIL_HOST_USER = 'abdulgaffar@onecloudinc.com' +EMAIL_HOST_PASSWORD = 'abdulgaffar' +EMAIL_USE_TLS = True \ No newline at end of file diff --git a/ntt_tool/static/css/custom.css b/ntt_tool/static/css/custom.css index c5994e9..5b9134d 100644 --- a/ntt_tool/static/css/custom.css +++ b/ntt_tool/static/css/custom.css @@ -1,10 +1,10 @@ -@import url('http://fonts.googleapis.com/css?family=Open+Sans'); +/*@import url('http://fonts.googleapis.com/css?family=Open+Sans');*/ +/*@import url('http://fonts.googleapis.com/css?family=Varela+Round');*/ body, html { padding: 0; margin: 0; } - body { font-family: "Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif; font-size: 13px; @@ -12,12 +12,34 @@ body { color: #555; background-color: #f6f6f6; } - .navbar { box-shadow: 0 2px 2px rgba(0,0,0,.05),0 1px 0 rgba(0,0,0,.05); border: none; border-radius: 0; - background: #fff; + background: #0E3D59; + +} +.navbar-brand{ + font-family: 'Varela Round', 'sans-serif'; + color: #e9e9df; +} +.navbar-default .navbar-brand { + color: #e9e9df; +} +.navbar-default .navbar-brand:hover, +.navbar-default .navbar-brand:focus { + color: #fff; +} +.navbar-default .navbar-text { + color: #e9e9df; +} +.navbar-default .navbar-nav > li > a { + color: #e9e9df; +} +.navbar-default .navbar-nav > li > a:hover, +.navbar-default .navbar-nav > li > a:focus { + color: #fff; + background-color: transparent; } .navbar-header a { text-transform: uppercase; @@ -25,14 +47,74 @@ body { font-weight: 400; } -.page-header { - background: #f6f6f6; - border-color: #e9e9e9; +/* Headings */ +.form_h2{ + color: #0E3D59; +} + +/* buttons */ + +/* table headings */ +th{ + color: #0E3D59; + text-align: center; + vertical-align: middle !important; +} + +td > a{ + color: #555; +} + + +/* utility classes */ +.bg-white { + background: #fff; +} + +.border { + border: 1px solid #ddd; +} + +.noborder{ + border: none !important; +} + +.underline{ + text-decoration: underline; } + +.no-margin-bottom { + margin-bottom: 0; +} + +.mb0 { + margin-bottom: 0; +} +/* end: utility classes */ + +.panel { + border-radius: 0; +} +.panel-default .panel-heading { + background: #dddddd; + border-radius: 0; +} + +.btn-primary{ + background-color: #0E3D59; + /*border: none;*/ +} + +/*.page-header {*/ + /*background: #f6f6f6;*/ + /*border-color: #e9e9e9;*/ +/*}*/ .page-header { border-bottom: 1px solid #e4e4e4; - margin: -18px -18px 18px -18px; - padding: 5px 18px; + margin-top: 0; + padding-left: 0; + padding-right: 0; + padding-bottom: 0; } .page-header h1 { color: #666; @@ -46,12 +128,20 @@ body { margin-top: 18px; } -.bg-white { - background: #fff; +.modal-body { + max-height: calc(100vh - 210px); + overflow-y: auto; +} +@media (min-width: 768px) { + .modal-xl { + width: 95%; + max-width:1300px; + } + .modal-xxl { + width: 90%; + max-width: 1400px; + } } - - - /* widgets */ .card { border-radius: 2px; @@ -76,4 +166,20 @@ body { .panel-heading .btn{ margin-top: 0; -} \ No newline at end of file +} +.breadcrumb { + padding: 0; + margin: 0; +} + +.list-group-item-heading-custom { + border-bottom: 1px solid #ddd; + margin-bottom: 15px; + padding-bottom: 10px; +} + +.tenants-list { + max-height: calc(100vh - 210px); + overflow-y: auto; +} + diff --git a/ntt_tool/static/css/ngToast-animations.min.css b/ntt_tool/static/css/ngToast-animations.min.css new file mode 100644 index 0000000..607552e --- /dev/null +++ b/ntt_tool/static/css/ngToast-animations.min.css @@ -0,0 +1,7 @@ +/*! + * ngToast v2.0.0 (http://tameraydin.github.io/ngToast) + * Copyright 2016 Tamer Aydin (http://tamerayd.in) + * Licensed under MIT (http://tameraydin.mit-license.org/) + */ + +.ng-toast--animate-fade .ng-enter,.ng-toast--animate-fade .ng-leave,.ng-toast--animate-fade .ng-move{transition-property:opacity;transition-duration:.3s;transition-timing-function:ease}.ng-toast--animate-fade .ng-enter{opacity:0}.ng-toast--animate-fade .ng-enter.ng-enter-active,.ng-toast--animate-fade .ng-leave{opacity:1}.ng-toast--animate-fade .ng-leave.ng-leave-active{opacity:0}.ng-toast--animate-fade .ng-move{opacity:.5}.ng-toast--animate-fade .ng-move.ng-move-active{opacity:1}.ng-toast--animate-slide .ng-enter,.ng-toast--animate-slide .ng-leave,.ng-toast--animate-slide .ng-move{position:relative;transition-duration:.3s;transition-timing-function:ease}.ng-toast--animate-slide.ng-toast--center.ng-toast--top .ng-toast__message{position:relative;transition-property:top,margin-top,opacity}.ng-toast--animate-slide.ng-toast--center.ng-toast--top .ng-toast__message.ng-enter{opacity:0;top:-100px}.ng-toast--animate-slide.ng-toast--center.ng-toast--top .ng-toast__message.ng-enter.ng-enter-active,.ng-toast--animate-slide.ng-toast--center.ng-toast--top .ng-toast__message.ng-leave{opacity:1;top:0}.ng-toast--animate-slide.ng-toast--center.ng-toast--top .ng-toast__message.ng-leave.ng-leave-active{opacity:0;margin-top:-72px}.ng-toast--animate-slide.ng-toast--center.ng-toast--bottom .ng-toast__message{position:relative;transition-property:bottom,margin-bottom,opacity}.ng-toast--animate-slide.ng-toast--center.ng-toast--bottom .ng-toast__message.ng-enter{opacity:0;bottom:-100px}.ng-toast--animate-slide.ng-toast--center.ng-toast--bottom .ng-toast__message.ng-enter.ng-enter-active,.ng-toast--animate-slide.ng-toast--center.ng-toast--bottom .ng-toast__message.ng-leave{opacity:1;bottom:0}.ng-toast--animate-slide.ng-toast--center.ng-toast--bottom .ng-toast__message.ng-leave.ng-leave-active{opacity:0;margin-bottom:-72px}.ng-toast--animate-slide.ng-toast--right{transition-property:right,margin-right,opacity}.ng-toast--animate-slide.ng-toast--right .ng-enter{opacity:0;right:-200%;margin-right:20px}.ng-toast--animate-slide.ng-toast--right .ng-enter.ng-enter-active,.ng-toast--animate-slide.ng-toast--right .ng-leave{opacity:1;right:0;margin-right:0}.ng-toast--animate-slide.ng-toast--right .ng-leave.ng-leave-active{opacity:0;right:-200%;margin-right:20px}.ng-toast--animate-slide.ng-toast--left{transition-property:left,margin-left,opacity}.ng-toast--animate-slide.ng-toast--left .ng-enter{opacity:0;left:-200%;margin-left:20px}.ng-toast--animate-slide.ng-toast--left .ng-enter.ng-enter-active,.ng-toast--animate-slide.ng-toast--left .ng-leave{opacity:1;left:0;margin-left:0}.ng-toast--animate-slide.ng-toast--left .ng-leave.ng-leave-active{opacity:0;left:-200%;margin-left:20px} \ No newline at end of file diff --git a/ntt_tool/static/css/ngToast.css b/ntt_tool/static/css/ngToast.css new file mode 100644 index 0000000..d4f209a --- /dev/null +++ b/ntt_tool/static/css/ngToast.css @@ -0,0 +1,96 @@ +/*! + * ngToast v2.0.0 (http://tameraydin.github.io/ngToast) + * Copyright 2016 Tamer Aydin (http://tamerayd.in) + * Licensed under MIT (http://tameraydin.mit-license.org/) + */ + +.ng-toast { + position: fixed; + z-index: 1080; + width: 100%; + height: 0; + margin-top: 20px; + text-align: center; +} + + +.ng-toast.ng-toast--top { + top: 50px; + bottom: auto; +} +.ng-toast.ng-toast--top .ng-toast__list { + top: 0; + bottom: auto; +} +.ng-toast.ng-toast--top.ng-toast--center .ng-toast__list { + position: static; } + .ng-toast.ng-toast--bottom { + top: auto; + bottom: 0; } + .ng-toast.ng-toast--bottom .ng-toast__list { + top: auto; + bottom: 0; } + .ng-toast.ng-toast--bottom.ng-toast--center .ng-toast__list { + pointer-events: none; } + .ng-toast.ng-toast--bottom.ng-toast--center .ng-toast__message .alert { + pointer-events: auto; } + .ng-toast.ng-toast--right .ng-toast__list { + left: auto; + right: 0; + margin-right: 20px; } + .ng-toast.ng-toast--right .ng-toast__message { + text-align: right; } + .ng-toast.ng-toast--left .ng-toast__list { + right: auto; + left: 0; + margin-left: 20px; } + .ng-toast.ng-toast--left .ng-toast__message { + text-align: left; } + .ng-toast .ng-toast__list { + display: inline-block; + position: absolute; + right: 0; + left: 0; + margin: 0 auto; + padding: 0; + list-style: none; } + .ng-toast .ng-toast__message { + display: block; + width: 100%; + text-align: center; } + .ng-toast .ng-toast__message .alert { + display: inline-block; } + .ng-toast .ng-toast__message__count { + display: inline-block; + margin: 0 15px 0 5px; } + + +.ng-toast .alert{ + box-shadow: 0 0 12px #ddd; + -moz-box-shadow: 0 0 12px #ddd; + -webkit-box-shadow: 0 0 12px #ddd; + color: #ffffff; + text-align: left; + min-width: 200px; +} + +.ng-toast .alert-success{ + border: 1px solid #51a351; + background-color: #51a351; +} + +.ng-toast .alert-info{ + border: 1px solid #2f96b4; + background-color: #2f96b4; +} + +.ng-toast .alert-warning{ + border: 1px solid #f89406; + background-color: #f89406; +} + +.ng-toast .alert-danger{ + border: 1px solid #bd362f; + background-color: #bd362f; +} + diff --git a/ntt_tool/static/css/ngToast.min.css b/ntt_tool/static/css/ngToast.min.css new file mode 100644 index 0000000..715f089 --- /dev/null +++ b/ntt_tool/static/css/ngToast.min.css @@ -0,0 +1,7 @@ +/*! + * ngToast v2.0.0 (http://tameraydin.github.io/ngToast) + * Copyright 2016 Tamer Aydin (http://tamerayd.in) + * Licensed under MIT (http://tameraydin.mit-license.org/) + */ + +.ng-toast{position:fixed;z-index:1080;width:100%;height:0;margin-top:20px;text-align:center}.ng-toast.ng-toast--top,.ng-toast.ng-toast--top .ng-toast__list{top:0;bottom:auto}.ng-toast.ng-toast--top.ng-toast--center .ng-toast__list{position:static}.ng-toast.ng-toast--bottom,.ng-toast.ng-toast--bottom .ng-toast__list{top:auto;bottom:0}.ng-toast.ng-toast--bottom.ng-toast--center .ng-toast__list{pointer-events:none}.ng-toast.ng-toast--bottom.ng-toast--center .ng-toast__message .alert{pointer-events:auto}.ng-toast.ng-toast--right .ng-toast__list{left:auto;right:0;margin-right:20px}.ng-toast.ng-toast--right .ng-toast__message{text-align:right}.ng-toast.ng-toast--left .ng-toast__list{right:auto;left:0;margin-left:20px}.ng-toast.ng-toast--left .ng-toast__message{text-align:left}.ng-toast .ng-toast__list{display:inline-block;position:absolute;right:0;left:0;margin:0 auto;padding:0;list-style:none}.ng-toast .ng-toast__message{display:block;width:100%;text-align:center}.ng-toast .ng-toast__message .alert{display:inline-block}.ng-toast .ng-toast__message__count{display:inline-block;margin:0 15px 0 5px} \ No newline at end of file diff --git a/ntt_tool/static/images/logo.png b/ntt_tool/static/images/logo.png new file mode 100644 index 0000000..3fd811a Binary files /dev/null and b/ntt_tool/static/images/logo.png differ diff --git a/ntt_tool/static/js/apps/app.js b/ntt_tool/static/js/apps/app.js index 011ff9f..f562ea7 100644 --- a/ntt_tool/static/js/apps/app.js +++ b/ntt_tool/static/js/apps/app.js @@ -1,10 +1,15 @@ var nttApp = angular.module('nttApp', [ 'ngRoute', 'ngCookies', + 'ngAnimate', + 'ngSanitize', 'http-auth-interceptor', + 'ui.bootstrap', + 'ngToast', + 'checklist-model', ]); -nttApp.config(['$httpProvider', '$interpolateProvider', function($httpProvider, $interpolateProvider){ +nttApp.config(['$httpProvider', '$interpolateProvider', function($httpProvider, $interpolateProvider, $window){ /* For compatibility with django template engine */ $interpolateProvider.startSymbol('{$'); $interpolateProvider.endSymbol('$}'); @@ -29,45 +34,36 @@ nttApp.config(['$routeProvider', function($routeProvider){ templateUrl: '/static/partials/dashboard.html', }); - $routeProvider.when('/clouds/', { - controller: 'CloudsCtrl', - templateUrl: '/static/partials/cloud/clouds.html', + /** + * URL patterns for cloud + */ + $routeProvider.when('/cloud/', { + controller: 'CloudListCtrl', + templateUrl: '/static/partials/cloud/cloud_list.html', }); - $routeProvider.when('/cloud/:cloudId/', { - controller: 'CloudCtrl', + $routeProvider.when('/cloud/view/:id/', { + controller: 'CloudViewCtrl', templateUrl: '/static/partials/cloud/cloud_view.html', }); - $routeProvider.when('/clouds/:event/', { - controller: 'CloudCtrl', - templateUrl: '/static/partials/cloud/cloud.html', - }); + /** + * URL patterns for cloud traffic + */ - $routeProvider.when('/clouds/:event/:cloudId/', { - controller: 'CloudCtrl', - templateUrl: '/static/partials/cloud/cloud.html', + $routeProvider.when('/cloud/:cloudId/traffic/:id/', { + controller: 'TrafficViewCtrl', + templateUrl: '/static/partials/cloud/traffic/traffic_view.html' }); - $routeProvider.when('/cloudtraffic/view/:id/', { - controller: 'CloudTrafficViewCtrl', - templateUrl: '/static/partials/cloud/cloudtraffic_view.html', - }); - $routeProvider.when('/cloudtraffic/:event/:cloudId/', { - controller: 'CloudTrafficCtrl', - templateUrl: '/static/partials/cloud/cloudtraffic_form.html', - }); +}]); + + + + + - $routeProvider.when('/cloudtraffic/:event/:cloudId/:trafficId/', { - controller: 'CloudTrafficCtrl', - templateUrl: '/static/partials/cloud/cloudtraffic_form.html', - }); - $routeProvider.when('/cloudtraffictest/', { - controller: 'CloudTrafficTestCtrl', - templateUrl: '/static/partials/cloud/cloudtraffic_test.html' - }); -}]); diff --git a/ntt_tool/static/js/apps/auth.js b/ntt_tool/static/js/apps/auth.js index 46f066a..697d7c3 100644 --- a/ntt_tool/static/js/apps/auth.js +++ b/ntt_tool/static/js/apps/auth.js @@ -1,25 +1,38 @@ -nttApp.run(function($rootScope, $window, $location){ +nttApp.run(function($rootScope, $window, $location, ngToast){ $rootScope.isLoggedin = false; + if($window.localStorage.isLoggedin){ $rootScope.isLoggedin = true; - }; - + $rootScope.user = JSON.parse($window.localStorage.getItem("user")); + } $rootScope.$on('event:auth-loginConfirmed', function(event, data){ $('#modal').modal('hide'); $rootScope.isLoggedin = true; + $rootScope.user = data.user; $window.localStorage['isLoggedin'] = true; $window.localStorage['token'] = data.token; - $location.path("clouds"); + $window.localStorage['user'] = angular.toJson(data.user); + ngToast.dismiss(); + ngToast.create("Welcome " + data.user.first_name + " " + data.user.last_name + ""); + $location.path("cloud"); }); $rootScope.$on('event:auth-loginRequired', function (event) { $rootScope.$broadcast('event:auth-logout'); + ngToast.create({ + className: 'warning', + content: 'Login required.', + }); }); $rootScope.$on('event:auth-logout', function (event) { $rootScope.isLoggedin = false; + $rootScope.user = {}; $window.localStorage.removeItem('isLoggedin') $window.localStorage.removeItem('token'); + $window.localStorage.removeItem('user'); + ngToast.dismiss(); + ngToast.create("Logged out successfully."); $location.path("/"); }); @@ -28,15 +41,29 @@ nttApp.run(function($rootScope, $window, $location){ } }); -nttApp.controller('LoginCtrl', function($scope, $http, authService){ +nttApp.controller('LoginCtrl', function($scope, $http, authService, ngToast){ $scope.credentials = {}; $scope.login = function(){ $http({ method: 'post', url: '/api/auth-token/', data: $.param($scope.credentials) - }).success(function(response){ - authService.loginConfirmed(response); + }) + .then(function successCallback(response) { + authService.loginConfirmed(response.data); + }, function errorCallback(response) { + if(Object.keys(response.data)[0] == "non_field_errors"){ + ngToast.create({ + className: "danger", + content: response.data.non_field_errors, + }); + } + else { + ngToast.create({ + className: 'danger', + content: 'Please enter Username and Password', + }); + } }); }; }); @@ -55,3 +82,11 @@ nttApp.factory('token-interceptor', function($rootScope, $q, $window, $location) }).config(['$httpProvider', function ($httpProvider) { $httpProvider.interceptors.push('token-interceptor'); }]); + + +nttApp.config(['ngToastProvider', function(ngToast) { + ngToast.configure({ + animation: 'slide', + combineDuplications: true + }); +}]); \ No newline at end of file diff --git a/ntt_tool/static/js/apps/controllers/cloudcontroller.js b/ntt_tool/static/js/apps/controllers/cloudcontroller.js index 16238d6..b4fe9f3 100644 --- a/ntt_tool/static/js/apps/controllers/cloudcontroller.js +++ b/ntt_tool/static/js/apps/controllers/cloudcontroller.js @@ -1,177 +1,82 @@ -nttApp.controller('CloudsCtrl', function($scope, cloudService){ - $scope.clouds = []; - $scope.getClouds = function(){ - cloudService.list().then(function(data){ - $scope.clouds = data; - }) - }; - $scope.getClouds(); +/** + * Controller to list all clouds and delete selected cloud + */ +nttApp.controller('CloudListCtrl', function($scope, ngToast, cloudService){ + $scope.cloudList = []; + + cloudService.list().then(function(data){ + $scope.cloudList = data; + }); $scope.delete = function($index){ if(confirm("Are you sure want to delete?")){ - cloudService.delete($scope.clouds[$index].id).then(function(data){ - $scope.clouds.splice($index, 1); - }); + var cloudName = $scope.cloudList[$index].name; + cloudService.delete($scope.cloudList[$index].id).then( + function(data){ + $scope.cloudList.splice($index, 1); + ngToast.create("" + cloudName + " cloud deleted successfully."); + }, + function(error) { + console.log(error); + ngToast.create({ + className: 'danger', + content: "Errors while deleting cloud " + cloudName + "." + }); + } + ); } }; -}); - -nttApp.controller('CloudCtrl', function($scope, $routeParams, $location, cloudService, cloudTrafficService){ - $scope.cloudId = $routeParams.cloudId; - $scope.event = $routeParams.event; $scope.cloud = {}; - $scope.cloudTrafficList = []; - - if ($scope.cloudId != undefined){ - cloudService.get($scope.cloudId).then(function(data){ - $scope.cloud = data; - }); + $scope.eventType = "add"; + $scope.addCloud = function () { + $scope.eventType = "add"; + $scope.event = event; + $scope.cloud = {}; + }; - cloudTrafficService.list($scope.cloudId).then(function (data) { - $scope.cloudTrafficList = data; - }) + $scope.editCloud = function ($index) { + $scope.eventType = "edit"; + $scope.cloud = angular.copy($scope.cloudList[$index]); + $scope.cloud["$index"] = $index; }; $scope.save = function(){ - if ($scope.event == 'add'){ - cloudService.create($scope.cloud).then(function (data) { - $location.path("cloudtraffic/add/"+ data.id +"/"); - }); + if ($scope.eventType == "add") { + cloudService.create($scope.cloud).then( + function (response) { + $scope.cloudList.push(response); + ngToast.create("" + response.name + " cloud created successfully."); + $("#cloudFormModal").modal('hide'); + }, + function(error) { + console.log(error) + } + ); } else { - cloudService.update($scope.cloud.id, $scope.cloud).then(function(data){ - $location.path("cloud/"+$scope.cloud.id+"/"); - }); - }; - - }; - - $scope.deleteTraffic = function($index){ - if(confirm("Are you sure want to delete?")) { - cloudTrafficService.delete($scope.cloudTrafficList[$index].id).then(function (data) { - $scope.cloudTrafficList.splice($index, 1); - }); + cloudService.update($scope.cloud.id, $scope.cloud).then( + function(response) { + $scope.cloudList[$scope.cloud.$index] = response; + ngToast.create("" + response.name + " cloud updated successfully."); + $("#cloudFormModal").modal('hide'); + }, + function(error) { + console.log(error) + } + ); } - } -}); - - - -nttApp.controller('CloudTrafficCtrl', function($scope, $routeParams, $location, cloudService, cloudTrafficService){ - $scope.cloud = {}; - $scope.event = $routeParams.event; - cloudService.get($routeParams.cloudId).then(function(data){ - $scope.cloud = data; - }); - - $scope.cloudTraffic = {}; - if($scope.event == 'edit'){ - cloudTrafficService.get($routeParams.trafficId).then(function(data){ - $scope.cloudTraffic = data; - }); }; - $scope.save = function(){ - if($scope.event == 'add') { - $scope.cloudTraffic["cloud"] = $scope.cloud.id; - cloudTrafficService.create($scope.cloudTraffic).then(function (data) { - $location.path("cloudtraffic/view/" + data.id + "/"); - }); - } - else{ - cloudTrafficService.update($scope.cloudTraffic.id, $scope.cloudTraffic).then(function (data) { - $location.path("cloudtraffic/view/" + data.id + "/"); - }); - } - }; }); -nttApp.controller('CloudTrafficViewCtrl', function($scope, $routeParams, $location, cloudService, cloudTrafficService, cloudTrafficTenantService){ - $scope.cloud = {}; - $scope.cloudTrafficId = $routeParams.id; - $scope.cloudTraffic = {}; - cloudTrafficService.get($scope.cloudTrafficId).then(function(data){ - $scope.cloudTraffic = data; - $scope.getCloud($scope.cloudTraffic.cloud); - }); - - $scope.getCloud = function(cloudId){ - cloudService.get(cloudId).then(function(data){ - $scope.cloud = data; - }); - } +nttApp.controller('CloudViewCtrl', function($scope, $routeParams, $location, cloudService){ + $scope.id = $routeParams.id; + $scope.event = $scope.id == undefined ? "add" : "edit"; + $scope.cloud = {}; - $scope.tenantEvent = "Add"; - $scope.tenants = []; - $scope.tenant = {}; - cloudTrafficTenantService.list($scope.cloudTrafficId).then(function(data){ - $scope.tenants = data; + cloudService.get($scope.id).then(function(response){ + $scope.cloud = response; }); - - $scope.addTenant = function(){ - $scope.tenantEvent = "Add"; - $scope.tenant["cloud_traffic_id"] = $scope.cloudTraffic.id; - cloudTrafficTenantService.create($scope.tenant).then(function(data){ - $scope.tenants.push(data); - $('#addTenantModal').modal('hide'); - }); - }; - - $scope.editTenant = function($index){ - $scope.tenantEvent = "Edit"; - $scope.tenant = angular.copy($scope.tenants[$index]); - cloudTrafficTenantService.update($scope.tenant.id, $scope.tenant).then(function(data){ - $scope.tenant[$index] = data; - $('#addTenantModal').modal('hide'); - }); - }; - - $scope.saveTenant = function(event, $index){ - cloudTrafficTenantService.save($scope.tenant).then(function(data){ - $scope.tenant = {}; - return data; - }); - } - - $scope.deleteTenant = function($index){ - if(confirm("Are you sure you want to delete?")){ - cloudTrafficTenantService.delete($scope.tenants[$index].id).then(function(data){ - $scope.tenants.splice($index, 1); - }); - } - }; - - $scope.saveExternalHost = function(){ - cloudTrafficService.update($scope.cloudTraffic.id, $scope.cloudTraffic).then(function (data) { - $location.path("cloudtraffictest"); - }); - }; }); - -nttApp.controller('CloudTrafficTestCtrl', function($scope, $location, cloudTrafficService){ - -}); - -//nttApp.controller('CloudTrafficTenantsCtrl', function ($scope, $routeParams, $location, CloudTrafficTenantService) { -// $scope.trafficId = $routeParams.trafficId; -// $scope.tenants = {}; -// -// $scope.save = function(){ -// $scope.tenants["cloud_traffic"] = $scope.trafficId; -// CloudTrafficTenantService.save($scope.tenants).then(function (data) { -// $location.path("cloudtraffictenants/sshgateway/"+ data.id +"/"); -// }); -// }; -//}); -// -// -//nttApp.controller('cloudTrafficTenantsSSHGatewayCtrl', function($scope, $routeParams, $location){ -// $scope.trafficId = $routeParams.trafficId; -// $scope.tenants = {}; -// -// CloudTrafficTenantService.list($scope.trafficId).then(function(data){ -// $scope.tenants = data; -// }); -//}); \ No newline at end of file diff --git a/ntt_tool/static/js/apps/controllers/maincontroller.js b/ntt_tool/static/js/apps/controllers/maincontroller.js index ef31ac7..96e9b9e 100644 --- a/ntt_tool/static/js/apps/controllers/maincontroller.js +++ b/ntt_tool/static/js/apps/controllers/maincontroller.js @@ -1,6 +1,6 @@ nttApp.controller('IndexCtrl', function($rootScope, $scope, $location){ if($rootScope.isLoggedin){ - $location.path('clouds'); + $location.path('cloud'); } }); diff --git a/ntt_tool/static/js/apps/controllers/tenantcontroller.js b/ntt_tool/static/js/apps/controllers/tenantcontroller.js new file mode 100644 index 0000000..0cf465d --- /dev/null +++ b/ntt_tool/static/js/apps/controllers/tenantcontroller.js @@ -0,0 +1,16 @@ +nttApp.controller('TenantListCtrl', function($scope, $routeParams, tenantService){ + $scope.cloudId = $routeParams.id; + $scope.tenants = []; + tenantService.list($scope.cloudId).then(function(response){ + $scope.tenants = response; + }); + + $scope.showLoading = false; + $scope.discover = function(){ + $scope.showLoading = true; + tenantService.discover($scope.cloudId).then(function(response){ + $scope.showLoading = false; + $scope.tenants = response; + }); + }; +}); \ No newline at end of file diff --git a/ntt_tool/static/js/apps/controllers/trafficcontroller.js b/ntt_tool/static/js/apps/controllers/trafficcontroller.js new file mode 100644 index 0000000..2d552f2 --- /dev/null +++ b/ntt_tool/static/js/apps/controllers/trafficcontroller.js @@ -0,0 +1,356 @@ +nttApp.controller('TrafficListCtrl', function($scope, $routeParams, ngToast, trafficService){ + $scope.cloudId = $routeParams.id; + $scope.trafficList = []; + trafficService.list($scope.cloudId).then(function(response){ + $scope.trafficList = response; + }); + + $scope.delete = function($index){ + if (confirm("Are you sure want to delete?")){ + var trafficName = $scope.trafficList[$index].name; + trafficService.delete($scope.trafficList[$index].id).then( + function(response){ + $scope.trafficList.splice($index, 1); + ngToast.create("" + trafficName + " cloud traffic deleted successfully."); + }, + function(error) { + console.log(error); + ngToast.create({ + className: 'danger', + content: "Errors while deleting cloud traffic " + trafficName + "." + }); + } + ); + } + }; + + $scope.eventType = "add"; + $scope.traffic = {}; + $scope.addTraffic = function () { + $scope.eventType = "add"; + $scope.traffic = {}; + $scope.traffic["cloud_id"] = $scope.cloudId; + $scope.traffic["test_type"] = "intra-tenant"; + $scope.traffic["test_environment"] = "dev"; + }; + + $scope.editTraffic = function ($index) { + $scope.eventType = "edit"; + $scope.traffic = {}; + $scope.traffic = angular.copy($scope.trafficList[$index]); + $scope.traffic["$index"] = $index; + }; + + $scope.save = function () { + var selectedTestMethods = []; + angular.forEach($scope.traffic.test_method, function (isSelected, testMethod) { + if(isSelected){ + selectedTestMethods.push(testMethod) + } + }); + + var trafficObj = angular.copy($scope.traffic); + trafficObj["test_method"] = selectedTestMethods.join(); + + if($scope.eventType == "add") { + trafficService.create(trafficObj).then( + function (response) { + $scope.trafficList.push(response); + ngToast.create("" + response.name + " cloud traffic created successfully."); + $("#trafficFormModal").modal('hide'); + }, + function(error) { + console.log(error); + ngToast.create({ + className: 'danger', + content: "Errors while creating cloud traffic " + trafficObj.name + "." + }); + } + ); + } + else { + trafficService.update(trafficObj.id, trafficObj).then( + function(response){ + $scope.trafficList[$scope.traffic.$index] = response; + ngToast.create("" + response.name + " cloud traffic updated successfully."); + $("#trafficFormModal").modal('hide'); + }, + function(error) { + console.log(error); + ngToast.create({ + className: 'danger', + content: "Errors while updating cloud traffic " + trafficObj.name + "." + }); + } + ); + } + } +}); + +nttApp.controller('TrafficViewCtrl', function($scope, $routeParams, ngToast, trafficService, tenantService, networkService, endpointService, $http){ + $scope.cloudId = $routeParams.cloudId; + $scope.id = $routeParams.id; + $scope.traffic = {}; + + trafficService.get($scope.id).then(function(response){ + $scope.traffic = response; + $scope.getTenants(); + $scope.getReports(); + }); + + $scope.tenants = []; + $scope.selectedTenant = {}; + $scope.showTenantsLoading = false; + $scope.getTenants = function () { + $scope.showTenantsLoading = true; + tenantService.list($scope.id).then(function (response) { + $scope.showTenantsLoading = false; + $scope.tenants = response; + angular.forEach($scope.tenants, function (tenant, i) { + if(tenant.is_selected){ + $scope.selectedTenant = tenant; + } + }); + $scope.getNetworks($scope.selectedTenant.id); + }); + }; + + $scope.showTenantsDiscovering = false; + $scope.discoverTenants = function () { + $scope.showTenantsDiscovering = true; + tenantService.discover({"cloud_id": $scope.cloudId, "traffic_id":$scope.id}).then( + function (response) { + $scope.showTenantsDiscovering = false; + $scope.tenants = response; + ngToast.create("Tenants has been discovered successfully."); + }, + function (error) { + $scope.showTenantsDiscovering = false; + ngToast.create({ + className: 'danger', + content: error.error, + timeout: 8000, + }); + } + ); + }; + + + $scope.networks = []; + $scope.showNetworkDiscovering = false; + $scope.discoverNetworks = function ($index) { + $scope.showNetworkDiscovering = true; + $scope.selectedTenant = $scope.tenants[$index]; + networkService.discover($scope.selectedTenant.id).then( + function (response) { + $scope.networks = response; + $scope.showNetworkDiscovering = false; + ngToast.create("Networks has been discovered successfully."); + }, + function (error) { + $scope.showNetworkDiscovering = false; + ngToast.create({ + className: 'danger', + content: error.error, + timeout: 8000, + }); + } + ); + }; + + $scope.getNetworks = function (tenantId) { + if(tenantId != undefined) { + networkService.list(tenantId).then(function (response) { + $scope.networks = response; + $scope.getEndpoints(); + }); + } + }; + + + $scope.enabledEndpointActionBtn = false; + $scope.$watch('networks', function(newValues, oldValue, scope){ + var flag = false; + angular.forEach(newValues, function(network, i){ + if(network.is_selected){ + flag = true + } + }); + $scope.enabledEndpointActionBtn = flag; + }, true); + + + $scope.endpoints = []; + $scope.showEndpointLoading = false; + $scope.discoverEndpoints = function () { + $scope.showEndpointLoading = true; + var selectedNetworks = []; + angular.forEach($scope.networks, function(network, i) { + if(network.is_selected){ + selectedNetworks.push({ + "network_id": network.id, + "ip_range_start": network.subnets[0].ip_range_start, + "ip_range_end": network.subnets[0].ip_range_end, + }); + } + }); + endpointService.discover($scope.traffic.id, selectedNetworks).then( + function (response) { + $scope.endpoints = response; + $scope.showEndpointLoading = false; + ngToast.create("Endpoints has been discovered successfully."); + }, + function (error) { + $scope.showEndpointLoading = false; + ngToast.create({ + className: 'danger', + content: error.error, + timeout: 8000, + }); + } + ); + }; + + $scope.showEndpointLaunching = false; + $scope.launchEndpoints = function () { + $scope.showEndpointLaunching = true; + var selectedNetworks = []; + angular.forEach($scope.networks, function(network, i) { + if(network.is_selected){ + selectedNetworks.push({ + "network_id": network.id, + "endpoint_count": network.endpoint_count + }); + } + }); + endpointService.launch($scope.traffic.id, selectedNetworks).then( + function (response) { + $scope.endpoints = response; + $scope.showEndpointLaunching = false; + ngToast.create("Endpoints has been launched successfully."); + }, + function (error) { + $scope.showEndpointLaunching = false; + ngToast.create({ + className: 'danger', + content: error.error, + timeout: 8000, + }); + } + ); + }; + + $scope.getEndpoints = function () { + endpointService.list($scope.traffic.id).then(function (response) { + $scope.endpoints = response; + }); + }; + + $scope.selectEndpoint = function ($index, id, isSelected) { + var endpoint = $scope.endpoints[$index]; + endpointService.select(id, isSelected).then(function (response) { + endpoint = response; + }); + }; + + $scope.selectedEndpointsCount = 0; + $scope.$watch('endpoints', function(newValues, oldValue, scope){ + var count = 0; + angular.forEach(newValues, function(endpoint, i){ + if(endpoint.is_selected){ + count++; + } + }); + $scope.selectedEndpointsCount = count; + }, true); + + + $scope.reports = []; + $scope.getReports = function () { + trafficService.reports($scope.traffic.id).then(function (response) { + $scope.reports = response; + }); + }; + + $scope.report = {}; + $scope.trafficTestDuration = "1"; + $scope.trafficTestRunning = false; + $scope.runTrafficTest = function () { + $scope.report = {}; + $scope.trafficTestRunning = true; + trafficService.runTrafficTest($scope.traffic.id, $scope.trafficTestDuration).then( + function (response) { + $scope.report = response; + $scope.trafficTestRunning = false; + $scope.reports.push($scope.report); + $('#trafficTestFormModal').modal('hide'); + $('#viewReportModal').modal('show'); + }, + function (error) { + $scope.trafficTestRunning = false; + ngToast.create({ + className: 'danger', + content: error.error, + timeout: 8000, + }); + } + ); + }; + + $scope.showLoadingReport = false; + $scope.viewReport = function (testRunId) { + $scope.report = {}; + $scope.showLoadingReport = true; + trafficService.report(testRunId).then(function (response) { + $scope.report = response; + $scope.showLoadingReport = false; + }); + }; + + $scope.downloadReport = function (testRunId, reportName) { + var url = '/api/traffic/report/download/' + testRunId + '/'; + $http.post(url, {}, {responseType: 'arraybuffer'}).then(function (response) { + var headers = response.headers(); + var blob = new Blob([response.data],{type:headers['content-type']}); + var link = document.createElement('a'); + link.href = window.URL.createObjectURL(blob); + link.download = reportName + ".pdf"; + link.click(); + }); + }; + + + $scope.deleteReport = function ($index, testRunId) { + if(confirm("Are you sure want to delete?")) { + trafficService.deleteReport(testRunId).then( + function (response) { + $scope.reports.splice($index, 1); + ngToast.create("Test run has been deleted successfully."); + }, + function (error) { + ngToast.create({ + className: 'danger', + content: error.error, + timeout: 8000, + }); + } + ); + } + }; + + + $scope.emailReport = function (testRunId) { + trafficService.emailReport(testRunId).then( + function (response) { + ngToast.create("Report has been sent to email successfully."); + }, + function (error) { + ngToast.create({ + className: 'danger', + content: error.error, + timeout: 8000, + }); + } + ); + } +}); diff --git a/ntt_tool/static/js/apps/directives/directives.js b/ntt_tool/static/js/apps/directives/directives.js new file mode 100644 index 0000000..2a548e4 --- /dev/null +++ b/ntt_tool/static/js/apps/directives/directives.js @@ -0,0 +1,3 @@ +/** + * Created by abdul on 24/3/16. + */ diff --git a/ntt_tool/static/js/apps/filters/filters.js b/ntt_tool/static/js/apps/filters/filters.js index 3e86b53..1522f72 100644 --- a/ntt_tool/static/js/apps/filters/filters.js +++ b/ntt_tool/static/js/apps/filters/filters.js @@ -4,4 +4,14 @@ nttApp.filter('capitalize', function () { return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase(); }) : ''; } +}); + +nttApp.filter('isEmptyObject', function(){ + return function (obj) { + for(var prop in obj) { + if(obj.hasOwnProperty(prop)) + return false; + } + return true; + }; }); \ No newline at end of file diff --git a/ntt_tool/static/js/apps/services/cloudservice.js b/ntt_tool/static/js/apps/services/cloudservice.js index 3c06680..5ad56ca 100644 --- a/ntt_tool/static/js/apps/services/cloudservice.js +++ b/ntt_tool/static/js/apps/services/cloudservice.js @@ -6,7 +6,7 @@ nttApp.service('cloudService', function(dataService){ return dataService.get('/api/cloud/'+id+'/'); }; this.create = function(params){ - return dataService.post('/api/cloud/1/', params); + return dataService.post('/api/cloud/', params); }; this.update = function(pk, params){ return dataService.put('/api/cloud/'+pk+'/', params); @@ -16,38 +16,3 @@ nttApp.service('cloudService', function(dataService){ }; }); -nttApp.service('cloudTrafficService', function(dataService){ - this.list = function(cloudId){ - return dataService.get('/api/cloudtraffic/?cloud_id='+cloudId); - }; - this.get = function(id){ - return dataService.get('/api/cloudtraffic/'+id+'/'); - }; - this.create = function(params){ - return dataService.post('/api/cloudtraffic/', params); - }; - this.update = function(pk, params){ - return dataService.put('/api/cloudtraffic/'+pk+'/', params); - }; - this.delete = function(pk){ - return dataService.delete('/api/cloudtraffic/'+pk+'/'); - }; -}); - -nttApp.service('cloudTrafficTenantService', function(dataService){ - this.list = function(trafficId){ - return dataService.get('/api/cloudtraffictenant/?traffic_id='+trafficId); - }; - this.get = function(trafficId) { - return dataService.get('/api/cloudtraffictenant/'+trafficId+'/'); - }; - this.create = function(params){ - return dataService.post('/api/cloudtraffictenant/', params); - }; - this.update = function(pk, params){ - return dataService.put('/api/cloudtraffictenant/'+pk+'/', params); - }; - this.delete = function(pk){ - return dataService.delete('/api/cloudtraffictenant/'+pk+'/'); - }; -}); \ No newline at end of file diff --git a/ntt_tool/static/js/apps/services/dataservice.js b/ntt_tool/static/js/apps/services/dataservice.js index d67ab08..1dc146d 100644 --- a/ntt_tool/static/js/apps/services/dataservice.js +++ b/ntt_tool/static/js/apps/services/dataservice.js @@ -7,8 +7,9 @@ nttApp.service('dataService', ['$http', '$q', function ($http, $q) { request.success(function (data, status, headers, config) { deferred.resolve(data); }); - request.error(function (data, status) { - deferred.reject(data); + request.error(function (err, status) { + var error = {"status": status, "error": err}; + deferred.reject(error); }); return deferred.promise; }; @@ -23,8 +24,50 @@ nttApp.service('dataService', ['$http', '$q', function ($http, $q) { request.success(function (data, status, headers, config) { deferred.resolve(data) }); - request.error(function (response) { - deferred.reject(response); + request.error(function (err, status) { + var error = {"status": status, "error": err}; + deferred.reject(error); + }); + return deferred.promise; + }; + + this.getJSON = function(url, params) { + var deferred = $q.defer(); + var request = $http({ + method: "get", + url: url, + data: $.param({"json": angular.toJson(params)}), + headers: { + 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' + } + }); + request.success(function (data, status, headers, config) { + deferred.resolve(data) + }); + request.error(function (err, status) { + var error = {"status": status, "error": err}; + deferred.reject(error); + }); + return deferred.promise; + }; + + + this.postJSON = function(url, params) { + var deferred = $q.defer(); + var request = $http({ + method: "post", + url: url, + data: $.param({"json": angular.toJson(params)}), + headers: { + 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' + } + }); + request.success(function (data, status, headers, config) { + deferred.resolve(data) + }); + request.error(function (err, status) { + var error = {"status": status, "error": err}; + deferred.reject(error); }); return deferred.promise; }; @@ -39,8 +82,9 @@ nttApp.service('dataService', ['$http', '$q', function ($http, $q) { request.success(function (data, status, headers, config) { deferred.resolve(data) }); - request.error(function (response) { - deferred.reject(response); + request.error(function (err, status) { + var error = {"status": status, "error": err}; + deferred.reject(error); }); return deferred.promise; }; @@ -54,8 +98,9 @@ nttApp.service('dataService', ['$http', '$q', function ($http, $q) { request.success(function (data, status, headers, config) { deferred.resolve(data) }); - request.error(function (response) { - deferred.reject(response); + request.error(function (err, status) { + var error = {"status": status, "error": err}; + deferred.reject(error); }); return deferred.promise; }; @@ -66,8 +111,9 @@ nttApp.service('dataService', ['$http', '$q', function ($http, $q) { request.success(function (data, status, headers, config) { deferred.resolve(data) }); - request.error(function (response) { - deferred.reject(response); + request.error(function (err, status) { + var error = {"status": status, "error": err}; + deferred.reject(error); }); return deferred.promise; } diff --git a/ntt_tool/static/js/apps/services/tenantservice.js b/ntt_tool/static/js/apps/services/tenantservice.js new file mode 100644 index 0000000..d5838a6 --- /dev/null +++ b/ntt_tool/static/js/apps/services/tenantservice.js @@ -0,0 +1,43 @@ +nttApp.service('tenantService', function (dataService) { + this.discover = function(params){ + return dataService.get('/api/tenant/discover/?'+$.param(params)); + }; + + this.list = function(trafficId){ + return dataService.get('/api/tenant/?traffic_id='+trafficId); + }; + + this.save = function(params){ + return dataService.post('/api/tenant/', params); + }; +}); + + +nttApp.service('networkService', function (dataService) { + this.discover = function (tenantId) { + return dataService.get('/api/network/discover/?tenant_id='+tenantId); + }; + + this.list = function (tenantId) { + return dataService.get('/api/network/?tenant_id='+tenantId); + }; +}); + + +nttApp.service('endpointService', function (dataService) { + this.list = function (trafficId) { + return dataService.get('/api/endpoint/?traffic_id=' + trafficId); + }; + + this.discover = function (trafficId, params) { + return dataService.postJSON('/api/endpoint/discover/' + trafficId + '/', params); + }; + + this.launch = function (trafficId, params) { + return dataService.postJSON('/api/endpoint/launch/' + trafficId + '/', params); + }; + + this.select = function (pk, isSelected) { + return dataService.get('/api/endpoint/' + pk + '/select/?is_selected=' + JSON.stringify(isSelected)); + }; +}); diff --git a/ntt_tool/static/js/apps/services/trafficservice.js b/ntt_tool/static/js/apps/services/trafficservice.js new file mode 100644 index 0000000..5b4a4ba --- /dev/null +++ b/ntt_tool/static/js/apps/services/trafficservice.js @@ -0,0 +1,47 @@ +nttApp.service('trafficService', function(dataService){ + this.list = function(cloudId){ + return dataService.get('/api/traffic/?cloud_id=' + cloudId); + }; + + this.get = function(pk) { + return dataService.get('/api/traffic/' + pk + '/'); + }; + + this.create = function(params){ + return dataService.post('/api/traffic/', params); + }; + + this.update = function(pk, params){ + return dataService.put('/api/traffic/' + pk + '/', params); + }; + + this.delete = function(pk){ + return dataService.delete('/api/traffic/' + pk + '/'); + }; + + this.reports = function (pk) { + return dataService.get('/api/traffic/reports/' + pk + '/'); + }; + + this.report = function (testRunId) { + return dataService.get('/api/traffic/report/' + testRunId + '/'); + }; + + this.runTrafficTest = function(pk, testDuration){ + return dataService.get('/api/traffic/' + pk + '/run/test/?duration='+testDuration); + }; + + this.emailReport = function (testRunId) { + return dataService.get('/api/traffic/report/email/' + testRunId + '/'); + }; + + this.deleteReport = function (testRunId) { + return dataService.delete('/api/traffic/report/delete/' + testRunId + '/'); + }; +}); + + + + + + diff --git a/ntt_tool/static/js/libs/angular/angular-animate.js b/ntt_tool/static/js/libs/angular/angular-animate.js new file mode 100644 index 0000000..1aa4ed8 --- /dev/null +++ b/ntt_tool/static/js/libs/angular/angular-animate.js @@ -0,0 +1,4147 @@ +/** + * @license AngularJS v1.5.5 + * (c) 2010-2016 Google, Inc. http://angularjs.org + * License: MIT + */ +(function(window, angular) {'use strict'; + +/* jshint ignore:start */ +var noop = angular.noop; +var copy = angular.copy; +var extend = angular.extend; +var jqLite = angular.element; +var forEach = angular.forEach; +var isArray = angular.isArray; +var isString = angular.isString; +var isObject = angular.isObject; +var isUndefined = angular.isUndefined; +var isDefined = angular.isDefined; +var isFunction = angular.isFunction; +var isElement = angular.isElement; + +var ELEMENT_NODE = 1; +var COMMENT_NODE = 8; + +var ADD_CLASS_SUFFIX = '-add'; +var REMOVE_CLASS_SUFFIX = '-remove'; +var EVENT_CLASS_PREFIX = 'ng-'; +var ACTIVE_CLASS_SUFFIX = '-active'; +var PREPARE_CLASS_SUFFIX = '-prepare'; + +var NG_ANIMATE_CLASSNAME = 'ng-animate'; +var NG_ANIMATE_CHILDREN_DATA = '$$ngAnimateChildren'; + +// Detect proper transitionend/animationend event names. +var CSS_PREFIX = '', TRANSITION_PROP, TRANSITIONEND_EVENT, ANIMATION_PROP, ANIMATIONEND_EVENT; + +// If unprefixed events are not supported but webkit-prefixed are, use the latter. +// Otherwise, just use W3C names, browsers not supporting them at all will just ignore them. +// Note: Chrome implements `window.onwebkitanimationend` and doesn't implement `window.onanimationend` +// but at the same time dispatches the `animationend` event and not `webkitAnimationEnd`. +// Register both events in case `window.onanimationend` is not supported because of that, +// do the same for `transitionend` as Safari is likely to exhibit similar behavior. +// Also, the only modern browser that uses vendor prefixes for transitions/keyframes is webkit +// therefore there is no reason to test anymore for other vendor prefixes: +// http://caniuse.com/#search=transition +if (isUndefined(window.ontransitionend) && isDefined(window.onwebkittransitionend)) { + CSS_PREFIX = '-webkit-'; + TRANSITION_PROP = 'WebkitTransition'; + TRANSITIONEND_EVENT = 'webkitTransitionEnd transitionend'; +} else { + TRANSITION_PROP = 'transition'; + TRANSITIONEND_EVENT = 'transitionend'; +} + +if (isUndefined(window.onanimationend) && isDefined(window.onwebkitanimationend)) { + CSS_PREFIX = '-webkit-'; + ANIMATION_PROP = 'WebkitAnimation'; + ANIMATIONEND_EVENT = 'webkitAnimationEnd animationend'; +} else { + ANIMATION_PROP = 'animation'; + ANIMATIONEND_EVENT = 'animationend'; +} + +var DURATION_KEY = 'Duration'; +var PROPERTY_KEY = 'Property'; +var DELAY_KEY = 'Delay'; +var TIMING_KEY = 'TimingFunction'; +var ANIMATION_ITERATION_COUNT_KEY = 'IterationCount'; +var ANIMATION_PLAYSTATE_KEY = 'PlayState'; +var SAFE_FAST_FORWARD_DURATION_VALUE = 9999; + +var ANIMATION_DELAY_PROP = ANIMATION_PROP + DELAY_KEY; +var ANIMATION_DURATION_PROP = ANIMATION_PROP + DURATION_KEY; +var TRANSITION_DELAY_PROP = TRANSITION_PROP + DELAY_KEY; +var TRANSITION_DURATION_PROP = TRANSITION_PROP + DURATION_KEY; + +var isPromiseLike = function(p) { + return p && p.then ? true : false; +}; + +var ngMinErr = angular.$$minErr('ng'); +function assertArg(arg, name, reason) { + if (!arg) { + throw ngMinErr('areq', "Argument '{0}' is {1}", (name || '?'), (reason || "required")); + } + return arg; +} + +function mergeClasses(a,b) { + if (!a && !b) return ''; + if (!a) return b; + if (!b) return a; + if (isArray(a)) a = a.join(' '); + if (isArray(b)) b = b.join(' '); + return a + ' ' + b; +} + +function packageStyles(options) { + var styles = {}; + if (options && (options.to || options.from)) { + styles.to = options.to; + styles.from = options.from; + } + return styles; +} + +function pendClasses(classes, fix, isPrefix) { + var className = ''; + classes = isArray(classes) + ? classes + : classes && isString(classes) && classes.length + ? classes.split(/\s+/) + : []; + forEach(classes, function(klass, i) { + if (klass && klass.length > 0) { + className += (i > 0) ? ' ' : ''; + className += isPrefix ? fix + klass + : klass + fix; + } + }); + return className; +} + +function removeFromArray(arr, val) { + var index = arr.indexOf(val); + if (val >= 0) { + arr.splice(index, 1); + } +} + +function stripCommentsFromElement(element) { + if (element instanceof jqLite) { + switch (element.length) { + case 0: + return []; + break; + + case 1: + // there is no point of stripping anything if the element + // is the only element within the jqLite wrapper. + // (it's important that we retain the element instance.) + if (element[0].nodeType === ELEMENT_NODE) { + return element; + } + break; + + default: + return jqLite(extractElementNode(element)); + break; + } + } + + if (element.nodeType === ELEMENT_NODE) { + return jqLite(element); + } +} + +function extractElementNode(element) { + if (!element[0]) return element; + for (var i = 0; i < element.length; i++) { + var elm = element[i]; + if (elm.nodeType == ELEMENT_NODE) { + return elm; + } + } +} + +function $$addClass($$jqLite, element, className) { + forEach(element, function(elm) { + $$jqLite.addClass(elm, className); + }); +} + +function $$removeClass($$jqLite, element, className) { + forEach(element, function(elm) { + $$jqLite.removeClass(elm, className); + }); +} + +function applyAnimationClassesFactory($$jqLite) { + return function(element, options) { + if (options.addClass) { + $$addClass($$jqLite, element, options.addClass); + options.addClass = null; + } + if (options.removeClass) { + $$removeClass($$jqLite, element, options.removeClass); + options.removeClass = null; + } + } +} + +function prepareAnimationOptions(options) { + options = options || {}; + if (!options.$$prepared) { + var domOperation = options.domOperation || noop; + options.domOperation = function() { + options.$$domOperationFired = true; + domOperation(); + domOperation = noop; + }; + options.$$prepared = true; + } + return options; +} + +function applyAnimationStyles(element, options) { + applyAnimationFromStyles(element, options); + applyAnimationToStyles(element, options); +} + +function applyAnimationFromStyles(element, options) { + if (options.from) { + element.css(options.from); + options.from = null; + } +} + +function applyAnimationToStyles(element, options) { + if (options.to) { + element.css(options.to); + options.to = null; + } +} + +function mergeAnimationDetails(element, oldAnimation, newAnimation) { + var target = oldAnimation.options || {}; + var newOptions = newAnimation.options || {}; + + var toAdd = (target.addClass || '') + ' ' + (newOptions.addClass || ''); + var toRemove = (target.removeClass || '') + ' ' + (newOptions.removeClass || ''); + var classes = resolveElementClasses(element.attr('class'), toAdd, toRemove); + + if (newOptions.preparationClasses) { + target.preparationClasses = concatWithSpace(newOptions.preparationClasses, target.preparationClasses); + delete newOptions.preparationClasses; + } + + // noop is basically when there is no callback; otherwise something has been set + var realDomOperation = target.domOperation !== noop ? target.domOperation : null; + + extend(target, newOptions); + + // TODO(matsko or sreeramu): proper fix is to maintain all animation callback in array and call at last,but now only leave has the callback so no issue with this. + if (realDomOperation) { + target.domOperation = realDomOperation; + } + + if (classes.addClass) { + target.addClass = classes.addClass; + } else { + target.addClass = null; + } + + if (classes.removeClass) { + target.removeClass = classes.removeClass; + } else { + target.removeClass = null; + } + + oldAnimation.addClass = target.addClass; + oldAnimation.removeClass = target.removeClass; + + return target; +} + +function resolveElementClasses(existing, toAdd, toRemove) { + var ADD_CLASS = 1; + var REMOVE_CLASS = -1; + + var flags = {}; + existing = splitClassesToLookup(existing); + + toAdd = splitClassesToLookup(toAdd); + forEach(toAdd, function(value, key) { + flags[key] = ADD_CLASS; + }); + + toRemove = splitClassesToLookup(toRemove); + forEach(toRemove, function(value, key) { + flags[key] = flags[key] === ADD_CLASS ? null : REMOVE_CLASS; + }); + + var classes = { + addClass: '', + removeClass: '' + }; + + forEach(flags, function(val, klass) { + var prop, allow; + if (val === ADD_CLASS) { + prop = 'addClass'; + allow = !existing[klass]; + } else if (val === REMOVE_CLASS) { + prop = 'removeClass'; + allow = existing[klass]; + } + if (allow) { + if (classes[prop].length) { + classes[prop] += ' '; + } + classes[prop] += klass; + } + }); + + function splitClassesToLookup(classes) { + if (isString(classes)) { + classes = classes.split(' '); + } + + var obj = {}; + forEach(classes, function(klass) { + // sometimes the split leaves empty string values + // incase extra spaces were applied to the options + if (klass.length) { + obj[klass] = true; + } + }); + return obj; + } + + return classes; +} + +function getDomNode(element) { + return (element instanceof angular.element) ? element[0] : element; +} + +function applyGeneratedPreparationClasses(element, event, options) { + var classes = ''; + if (event) { + classes = pendClasses(event, EVENT_CLASS_PREFIX, true); + } + if (options.addClass) { + classes = concatWithSpace(classes, pendClasses(options.addClass, ADD_CLASS_SUFFIX)); + } + if (options.removeClass) { + classes = concatWithSpace(classes, pendClasses(options.removeClass, REMOVE_CLASS_SUFFIX)); + } + if (classes.length) { + options.preparationClasses = classes; + element.addClass(classes); + } +} + +function clearGeneratedClasses(element, options) { + if (options.preparationClasses) { + element.removeClass(options.preparationClasses); + options.preparationClasses = null; + } + if (options.activeClasses) { + element.removeClass(options.activeClasses); + options.activeClasses = null; + } +} + +function blockTransitions(node, duration) { + // we use a negative delay value since it performs blocking + // yet it doesn't kill any existing transitions running on the + // same element which makes this safe for class-based animations + var value = duration ? '-' + duration + 's' : ''; + applyInlineStyle(node, [TRANSITION_DELAY_PROP, value]); + return [TRANSITION_DELAY_PROP, value]; +} + +function blockKeyframeAnimations(node, applyBlock) { + var value = applyBlock ? 'paused' : ''; + var key = ANIMATION_PROP + ANIMATION_PLAYSTATE_KEY; + applyInlineStyle(node, [key, value]); + return [key, value]; +} + +function applyInlineStyle(node, styleTuple) { + var prop = styleTuple[0]; + var value = styleTuple[1]; + node.style[prop] = value; +} + +function concatWithSpace(a,b) { + if (!a) return b; + if (!b) return a; + return a + ' ' + b; +} + +var $$rAFSchedulerFactory = ['$$rAF', function($$rAF) { + var queue, cancelFn; + + function scheduler(tasks) { + // we make a copy since RAFScheduler mutates the state + // of the passed in array variable and this would be difficult + // to track down on the outside code + queue = queue.concat(tasks); + nextTick(); + } + + queue = scheduler.queue = []; + + /* waitUntilQuiet does two things: + * 1. It will run the FINAL `fn` value only when an uncanceled RAF has passed through + * 2. It will delay the next wave of tasks from running until the quiet `fn` has run. + * + * The motivation here is that animation code can request more time from the scheduler + * before the next wave runs. This allows for certain DOM properties such as classes to + * be resolved in time for the next animation to run. + */ + scheduler.waitUntilQuiet = function(fn) { + if (cancelFn) cancelFn(); + + cancelFn = $$rAF(function() { + cancelFn = null; + fn(); + nextTick(); + }); + }; + + return scheduler; + + function nextTick() { + if (!queue.length) return; + + var items = queue.shift(); + for (var i = 0; i < items.length; i++) { + items[i](); + } + + if (!cancelFn) { + $$rAF(function() { + if (!cancelFn) nextTick(); + }); + } + } +}]; + +/** + * @ngdoc directive + * @name ngAnimateChildren + * @restrict AE + * @element ANY + * + * @description + * + * ngAnimateChildren allows you to specify that children of this element should animate even if any + * of the children's parents are currently animating. By default, when an element has an active `enter`, `leave`, or `move` + * (structural) animation, child elements that also have an active structural animation are not animated. + * + * Note that even if `ngAnimteChildren` is set, no child animations will run when the parent element is removed from the DOM (`leave` animation). + * + * + * @param {string} ngAnimateChildren If the value is empty, `true` or `on`, + * then child animations are allowed. If the value is `false`, child animations are not allowed. + * + * @example + * + +
+ + +
+
+
+ List of items: +
Item {{item}}
+
+
+
+
+ + + .container.ng-enter, + .container.ng-leave { + transition: all ease 1.5s; + } + + .container.ng-enter, + .container.ng-leave-active { + opacity: 0; + } + + .container.ng-leave, + .container.ng-enter-active { + opacity: 1; + } + + .item { + background: firebrick; + color: #FFF; + margin-bottom: 10px; + } + + .item.ng-enter, + .item.ng-leave { + transition: transform 1.5s ease; + } + + .item.ng-enter { + transform: translateX(50px); + } + + .item.ng-enter-active { + transform: translateX(0); + } + + + angular.module('ngAnimateChildren', ['ngAnimate']) + .controller('mainController', function() { + this.animateChildren = false; + this.enterElement = false; + }); + +
+ */ +var $$AnimateChildrenDirective = ['$interpolate', function($interpolate) { + return { + link: function(scope, element, attrs) { + var val = attrs.ngAnimateChildren; + if (angular.isString(val) && val.length === 0) { //empty attribute + element.data(NG_ANIMATE_CHILDREN_DATA, true); + } else { + // Interpolate and set the value, so that it is available to + // animations that run right after compilation + setData($interpolate(val)(scope)); + attrs.$observe('ngAnimateChildren', setData); + } + + function setData(value) { + value = value === 'on' || value === 'true'; + element.data(NG_ANIMATE_CHILDREN_DATA, value); + } + } + }; +}]; + +var ANIMATE_TIMER_KEY = '$$animateCss'; + +/** + * @ngdoc service + * @name $animateCss + * @kind object + * + * @description + * The `$animateCss` service is a useful utility to trigger customized CSS-based transitions/keyframes + * from a JavaScript-based animation or directly from a directive. The purpose of `$animateCss` is NOT + * to side-step how `$animate` and ngAnimate work, but the goal is to allow pre-existing animations or + * directives to create more complex animations that can be purely driven using CSS code. + * + * Note that only browsers that support CSS transitions and/or keyframe animations are capable of + * rendering animations triggered via `$animateCss` (bad news for IE9 and lower). + * + * ## Usage + * Once again, `$animateCss` is designed to be used inside of a registered JavaScript animation that + * is powered by ngAnimate. It is possible to use `$animateCss` directly inside of a directive, however, + * any automatic control over cancelling animations and/or preventing animations from being run on + * child elements will not be handled by Angular. For this to work as expected, please use `$animate` to + * trigger the animation and then setup a JavaScript animation that injects `$animateCss` to trigger + * the CSS animation. + * + * The example below shows how we can create a folding animation on an element using `ng-if`: + * + * ```html + * + *
+ * This element will go BOOM + *
+ * + * ``` + * + * Now we create the **JavaScript animation** that will trigger the CSS transition: + * + * ```js + * ngModule.animation('.fold-animation', ['$animateCss', function($animateCss) { + * return { + * enter: function(element, doneFn) { + * var height = element[0].offsetHeight; + * return $animateCss(element, { + * from: { height:'0px' }, + * to: { height:height + 'px' }, + * duration: 1 // one second + * }); + * } + * } + * }]); + * ``` + * + * ## More Advanced Uses + * + * `$animateCss` is the underlying code that ngAnimate uses to power **CSS-based animations** behind the scenes. Therefore CSS hooks + * like `.ng-EVENT`, `.ng-EVENT-active`, `.ng-EVENT-stagger` are all features that can be triggered using `$animateCss` via JavaScript code. + * + * This also means that just about any combination of adding classes, removing classes, setting styles, dynamically setting a keyframe animation, + * applying a hardcoded duration or delay value, changing the animation easing or applying a stagger animation are all options that work with + * `$animateCss`. The service itself is smart enough to figure out the combination of options and examine the element styling properties in order + * to provide a working animation that will run in CSS. + * + * The example below showcases a more advanced version of the `.fold-animation` from the example above: + * + * ```js + * ngModule.animation('.fold-animation', ['$animateCss', function($animateCss) { + * return { + * enter: function(element, doneFn) { + * var height = element[0].offsetHeight; + * return $animateCss(element, { + * addClass: 'red large-text pulse-twice', + * easing: 'ease-out', + * from: { height:'0px' }, + * to: { height:height + 'px' }, + * duration: 1 // one second + * }); + * } + * } + * }]); + * ``` + * + * Since we're adding/removing CSS classes then the CSS transition will also pick those up: + * + * ```css + * /* since a hardcoded duration value of 1 was provided in the JavaScript animation code, + * the CSS classes below will be transitioned despite them being defined as regular CSS classes */ + * .red { background:red; } + * .large-text { font-size:20px; } + * + * /* we can also use a keyframe animation and $animateCss will make it work alongside the transition */ + * .pulse-twice { + * animation: 0.5s pulse linear 2; + * -webkit-animation: 0.5s pulse linear 2; + * } + * + * @keyframes pulse { + * from { transform: scale(0.5); } + * to { transform: scale(1.5); } + * } + * + * @-webkit-keyframes pulse { + * from { -webkit-transform: scale(0.5); } + * to { -webkit-transform: scale(1.5); } + * } + * ``` + * + * Given this complex combination of CSS classes, styles and options, `$animateCss` will figure everything out and make the animation happen. + * + * ## How the Options are handled + * + * `$animateCss` is very versatile and intelligent when it comes to figuring out what configurations to apply to the element to ensure the animation + * works with the options provided. Say for example we were adding a class that contained a keyframe value and we wanted to also animate some inline + * styles using the `from` and `to` properties. + * + * ```js + * var animator = $animateCss(element, { + * from: { background:'red' }, + * to: { background:'blue' } + * }); + * animator.start(); + * ``` + * + * ```css + * .rotating-animation { + * animation:0.5s rotate linear; + * -webkit-animation:0.5s rotate linear; + * } + * + * @keyframes rotate { + * from { transform: rotate(0deg); } + * to { transform: rotate(360deg); } + * } + * + * @-webkit-keyframes rotate { + * from { -webkit-transform: rotate(0deg); } + * to { -webkit-transform: rotate(360deg); } + * } + * ``` + * + * The missing pieces here are that we do not have a transition set (within the CSS code nor within the `$animateCss` options) and the duration of the animation is + * going to be detected from what the keyframe styles on the CSS class are. In this event, `$animateCss` will automatically create an inline transition + * style matching the duration detected from the keyframe style (which is present in the CSS class that is being added) and then prepare both the transition + * and keyframe animations to run in parallel on the element. Then when the animation is underway the provided `from` and `to` CSS styles will be applied + * and spread across the transition and keyframe animation. + * + * ## What is returned + * + * `$animateCss` works in two stages: a preparation phase and an animation phase. Therefore when `$animateCss` is first called it will NOT actually + * start the animation. All that is going on here is that the element is being prepared for the animation (which means that the generated CSS classes are + * added and removed on the element). Once `$animateCss` is called it will return an object with the following properties: + * + * ```js + * var animator = $animateCss(element, { ... }); + * ``` + * + * Now what do the contents of our `animator` variable look like: + * + * ```js + * { + * // starts the animation + * start: Function, + * + * // ends (aborts) the animation + * end: Function + * } + * ``` + * + * To actually start the animation we need to run `animation.start()` which will then return a promise that we can hook into to detect when the animation ends. + * If we choose not to run the animation then we MUST run `animation.end()` to perform a cleanup on the element (since some CSS classes and styles may have been + * applied to the element during the preparation phase). Note that all other properties such as duration, delay, transitions and keyframes are just properties + * and that changing them will not reconfigure the parameters of the animation. + * + * ### runner.done() vs runner.then() + * It is documented that `animation.start()` will return a promise object and this is true, however, there is also an additional method available on the + * runner called `.done(callbackFn)`. The done method works the same as `.finally(callbackFn)`, however, it does **not trigger a digest to occur**. + * Therefore, for performance reasons, it's always best to use `runner.done(callback)` instead of `runner.then()`, `runner.catch()` or `runner.finally()` + * unless you really need a digest to kick off afterwards. + * + * Keep in mind that, to make this easier, ngAnimate has tweaked the JS animations API to recognize when a runner instance is returned from $animateCss + * (so there is no need to call `runner.done(doneFn)` inside of your JavaScript animation code). + * Check the {@link ngAnimate.$animateCss#usage animation code above} to see how this works. + * + * @param {DOMElement} element the element that will be animated + * @param {object} options the animation-related options that will be applied during the animation + * + * * `event` - The DOM event (e.g. enter, leave, move). When used, a generated CSS class of `ng-EVENT` and `ng-EVENT-active` will be applied + * to the element during the animation. Multiple events can be provided when spaces are used as a separator. (Note that this will not perform any DOM operation.) + * * `structural` - Indicates that the `ng-` prefix will be added to the event class. Setting to `false` or omitting will turn `ng-EVENT` and + * `ng-EVENT-active` in `EVENT` and `EVENT-active`. Unused if `event` is omitted. + * * `easing` - The CSS easing value that will be applied to the transition or keyframe animation (or both). + * * `transitionStyle` - The raw CSS transition style that will be used (e.g. `1s linear all`). + * * `keyframeStyle` - The raw CSS keyframe animation style that will be used (e.g. `1s my_animation linear`). + * * `from` - The starting CSS styles (a key/value object) that will be applied at the start of the animation. + * * `to` - The ending CSS styles (a key/value object) that will be applied across the animation via a CSS transition. + * * `addClass` - A space separated list of CSS classes that will be added to the element and spread across the animation. + * * `removeClass` - A space separated list of CSS classes that will be removed from the element and spread across the animation. + * * `duration` - A number value representing the total duration of the transition and/or keyframe (note that a value of 1 is 1000ms). If a value of `0` + * is provided then the animation will be skipped entirely. + * * `delay` - A number value representing the total delay of the transition and/or keyframe (note that a value of 1 is 1000ms). If a value of `true` is + * used then whatever delay value is detected from the CSS classes will be mirrored on the elements styles (e.g. by setting delay true then the style value + * of the element will be `transition-delay: DETECTED_VALUE`). Using `true` is useful when you want the CSS classes and inline styles to all share the same + * CSS delay value. + * * `stagger` - A numeric time value representing the delay between successively animated elements + * ({@link ngAnimate#css-staggering-animations Click here to learn how CSS-based staggering works in ngAnimate.}) + * * `staggerIndex` - The numeric index representing the stagger item (e.g. a value of 5 is equal to the sixth item in the stagger; therefore when a + * `stagger` option value of `0.1` is used then there will be a stagger delay of `600ms`) + * * `applyClassesEarly` - Whether or not the classes being added or removed will be used when detecting the animation. This is set by `$animate` when enter/leave/move animations are fired to ensure that the CSS classes are resolved in time. (Note that this will prevent any transitions from occurring on the classes being added and removed.) + * * `cleanupStyles` - Whether or not the provided `from` and `to` styles will be removed once + * the animation is closed. This is useful for when the styles are used purely for the sake of + * the animation and do not have a lasting visual effect on the element (e.g. a collapse and open animation). + * By default this value is set to `false`. + * + * @return {object} an object with start and end methods and details about the animation. + * + * * `start` - The method to start the animation. This will return a `Promise` when called. + * * `end` - This method will cancel the animation and remove all applied CSS classes and styles. + */ +var ONE_SECOND = 1000; +var BASE_TEN = 10; + +var ELAPSED_TIME_MAX_DECIMAL_PLACES = 3; +var CLOSING_TIME_BUFFER = 1.5; + +var DETECT_CSS_PROPERTIES = { + transitionDuration: TRANSITION_DURATION_PROP, + transitionDelay: TRANSITION_DELAY_PROP, + transitionProperty: TRANSITION_PROP + PROPERTY_KEY, + animationDuration: ANIMATION_DURATION_PROP, + animationDelay: ANIMATION_DELAY_PROP, + animationIterationCount: ANIMATION_PROP + ANIMATION_ITERATION_COUNT_KEY +}; + +var DETECT_STAGGER_CSS_PROPERTIES = { + transitionDuration: TRANSITION_DURATION_PROP, + transitionDelay: TRANSITION_DELAY_PROP, + animationDuration: ANIMATION_DURATION_PROP, + animationDelay: ANIMATION_DELAY_PROP +}; + +function getCssKeyframeDurationStyle(duration) { + return [ANIMATION_DURATION_PROP, duration + 's']; +} + +function getCssDelayStyle(delay, isKeyframeAnimation) { + var prop = isKeyframeAnimation ? ANIMATION_DELAY_PROP : TRANSITION_DELAY_PROP; + return [prop, delay + 's']; +} + +function computeCssStyles($window, element, properties) { + var styles = Object.create(null); + var detectedStyles = $window.getComputedStyle(element) || {}; + forEach(properties, function(formalStyleName, actualStyleName) { + var val = detectedStyles[formalStyleName]; + if (val) { + var c = val.charAt(0); + + // only numerical-based values have a negative sign or digit as the first value + if (c === '-' || c === '+' || c >= 0) { + val = parseMaxTime(val); + } + + // by setting this to null in the event that the delay is not set or is set directly as 0 + // then we can still allow for negative values to be used later on and not mistake this + // value for being greater than any other negative value. + if (val === 0) { + val = null; + } + styles[actualStyleName] = val; + } + }); + + return styles; +} + +function parseMaxTime(str) { + var maxValue = 0; + var values = str.split(/\s*,\s*/); + forEach(values, function(value) { + // it's always safe to consider only second values and omit `ms` values since + // getComputedStyle will always handle the conversion for us + if (value.charAt(value.length - 1) == 's') { + value = value.substring(0, value.length - 1); + } + value = parseFloat(value) || 0; + maxValue = maxValue ? Math.max(value, maxValue) : value; + }); + return maxValue; +} + +function truthyTimingValue(val) { + return val === 0 || val != null; +} + +function getCssTransitionDurationStyle(duration, applyOnlyDuration) { + var style = TRANSITION_PROP; + var value = duration + 's'; + if (applyOnlyDuration) { + style += DURATION_KEY; + } else { + value += ' linear all'; + } + return [style, value]; +} + +function createLocalCacheLookup() { + var cache = Object.create(null); + return { + flush: function() { + cache = Object.create(null); + }, + + count: function(key) { + var entry = cache[key]; + return entry ? entry.total : 0; + }, + + get: function(key) { + var entry = cache[key]; + return entry && entry.value; + }, + + put: function(key, value) { + if (!cache[key]) { + cache[key] = { total: 1, value: value }; + } else { + cache[key].total++; + } + } + }; +} + +// we do not reassign an already present style value since +// if we detect the style property value again we may be +// detecting styles that were added via the `from` styles. +// We make use of `isDefined` here since an empty string +// or null value (which is what getPropertyValue will return +// for a non-existing style) will still be marked as a valid +// value for the style (a falsy value implies that the style +// is to be removed at the end of the animation). If we had a simple +// "OR" statement then it would not be enough to catch that. +function registerRestorableStyles(backup, node, properties) { + forEach(properties, function(prop) { + backup[prop] = isDefined(backup[prop]) + ? backup[prop] + : node.style.getPropertyValue(prop); + }); +} + +var $AnimateCssProvider = ['$animateProvider', function($animateProvider) { + var gcsLookup = createLocalCacheLookup(); + var gcsStaggerLookup = createLocalCacheLookup(); + + this.$get = ['$window', '$$jqLite', '$$AnimateRunner', '$timeout', + '$$forceReflow', '$sniffer', '$$rAFScheduler', '$$animateQueue', + function($window, $$jqLite, $$AnimateRunner, $timeout, + $$forceReflow, $sniffer, $$rAFScheduler, $$animateQueue) { + + var applyAnimationClasses = applyAnimationClassesFactory($$jqLite); + + var parentCounter = 0; + function gcsHashFn(node, extraClasses) { + var KEY = "$$ngAnimateParentKey"; + var parentNode = node.parentNode; + var parentID = parentNode[KEY] || (parentNode[KEY] = ++parentCounter); + return parentID + '-' + node.getAttribute('class') + '-' + extraClasses; + } + + function computeCachedCssStyles(node, className, cacheKey, properties) { + var timings = gcsLookup.get(cacheKey); + + if (!timings) { + timings = computeCssStyles($window, node, properties); + if (timings.animationIterationCount === 'infinite') { + timings.animationIterationCount = 1; + } + } + + // we keep putting this in multiple times even though the value and the cacheKey are the same + // because we're keeping an internal tally of how many duplicate animations are detected. + gcsLookup.put(cacheKey, timings); + return timings; + } + + function computeCachedCssStaggerStyles(node, className, cacheKey, properties) { + var stagger; + + // if we have one or more existing matches of matching elements + // containing the same parent + CSS styles (which is how cacheKey works) + // then staggering is possible + if (gcsLookup.count(cacheKey) > 0) { + stagger = gcsStaggerLookup.get(cacheKey); + + if (!stagger) { + var staggerClassName = pendClasses(className, '-stagger'); + + $$jqLite.addClass(node, staggerClassName); + + stagger = computeCssStyles($window, node, properties); + + // force the conversion of a null value to zero incase not set + stagger.animationDuration = Math.max(stagger.animationDuration, 0); + stagger.transitionDuration = Math.max(stagger.transitionDuration, 0); + + $$jqLite.removeClass(node, staggerClassName); + + gcsStaggerLookup.put(cacheKey, stagger); + } + } + + return stagger || {}; + } + + var cancelLastRAFRequest; + var rafWaitQueue = []; + function waitUntilQuiet(callback) { + rafWaitQueue.push(callback); + $$rAFScheduler.waitUntilQuiet(function() { + gcsLookup.flush(); + gcsStaggerLookup.flush(); + + // DO NOT REMOVE THIS LINE OR REFACTOR OUT THE `pageWidth` variable. + // PLEASE EXAMINE THE `$$forceReflow` service to understand why. + var pageWidth = $$forceReflow(); + + // we use a for loop to ensure that if the queue is changed + // during this looping then it will consider new requests + for (var i = 0; i < rafWaitQueue.length; i++) { + rafWaitQueue[i](pageWidth); + } + rafWaitQueue.length = 0; + }); + } + + function computeTimings(node, className, cacheKey) { + var timings = computeCachedCssStyles(node, className, cacheKey, DETECT_CSS_PROPERTIES); + var aD = timings.animationDelay; + var tD = timings.transitionDelay; + timings.maxDelay = aD && tD + ? Math.max(aD, tD) + : (aD || tD); + timings.maxDuration = Math.max( + timings.animationDuration * timings.animationIterationCount, + timings.transitionDuration); + + return timings; + } + + return function init(element, initialOptions) { + // all of the animation functions should create + // a copy of the options data, however, if a + // parent service has already created a copy then + // we should stick to using that + var options = initialOptions || {}; + if (!options.$$prepared) { + options = prepareAnimationOptions(copy(options)); + } + + var restoreStyles = {}; + var node = getDomNode(element); + if (!node + || !node.parentNode + || !$$animateQueue.enabled()) { + return closeAndReturnNoopAnimator(); + } + + var temporaryStyles = []; + var classes = element.attr('class'); + var styles = packageStyles(options); + var animationClosed; + var animationPaused; + var animationCompleted; + var runner; + var runnerHost; + var maxDelay; + var maxDelayTime; + var maxDuration; + var maxDurationTime; + var startTime; + var events = []; + + if (options.duration === 0 || (!$sniffer.animations && !$sniffer.transitions)) { + return closeAndReturnNoopAnimator(); + } + + var method = options.event && isArray(options.event) + ? options.event.join(' ') + : options.event; + + var isStructural = method && options.structural; + var structuralClassName = ''; + var addRemoveClassName = ''; + + if (isStructural) { + structuralClassName = pendClasses(method, EVENT_CLASS_PREFIX, true); + } else if (method) { + structuralClassName = method; + } + + if (options.addClass) { + addRemoveClassName += pendClasses(options.addClass, ADD_CLASS_SUFFIX); + } + + if (options.removeClass) { + if (addRemoveClassName.length) { + addRemoveClassName += ' '; + } + addRemoveClassName += pendClasses(options.removeClass, REMOVE_CLASS_SUFFIX); + } + + // there may be a situation where a structural animation is combined together + // with CSS classes that need to resolve before the animation is computed. + // However this means that there is no explicit CSS code to block the animation + // from happening (by setting 0s none in the class name). If this is the case + // we need to apply the classes before the first rAF so we know to continue if + // there actually is a detected transition or keyframe animation + if (options.applyClassesEarly && addRemoveClassName.length) { + applyAnimationClasses(element, options); + } + + var preparationClasses = [structuralClassName, addRemoveClassName].join(' ').trim(); + var fullClassName = classes + ' ' + preparationClasses; + var activeClasses = pendClasses(preparationClasses, ACTIVE_CLASS_SUFFIX); + var hasToStyles = styles.to && Object.keys(styles.to).length > 0; + var containsKeyframeAnimation = (options.keyframeStyle || '').length > 0; + + // there is no way we can trigger an animation if no styles and + // no classes are being applied which would then trigger a transition, + // unless there a is raw keyframe value that is applied to the element. + if (!containsKeyframeAnimation + && !hasToStyles + && !preparationClasses) { + return closeAndReturnNoopAnimator(); + } + + var cacheKey, stagger; + if (options.stagger > 0) { + var staggerVal = parseFloat(options.stagger); + stagger = { + transitionDelay: staggerVal, + animationDelay: staggerVal, + transitionDuration: 0, + animationDuration: 0 + }; + } else { + cacheKey = gcsHashFn(node, fullClassName); + stagger = computeCachedCssStaggerStyles(node, preparationClasses, cacheKey, DETECT_STAGGER_CSS_PROPERTIES); + } + + if (!options.$$skipPreparationClasses) { + $$jqLite.addClass(element, preparationClasses); + } + + var applyOnlyDuration; + + if (options.transitionStyle) { + var transitionStyle = [TRANSITION_PROP, options.transitionStyle]; + applyInlineStyle(node, transitionStyle); + temporaryStyles.push(transitionStyle); + } + + if (options.duration >= 0) { + applyOnlyDuration = node.style[TRANSITION_PROP].length > 0; + var durationStyle = getCssTransitionDurationStyle(options.duration, applyOnlyDuration); + + // we set the duration so that it will be picked up by getComputedStyle later + applyInlineStyle(node, durationStyle); + temporaryStyles.push(durationStyle); + } + + if (options.keyframeStyle) { + var keyframeStyle = [ANIMATION_PROP, options.keyframeStyle]; + applyInlineStyle(node, keyframeStyle); + temporaryStyles.push(keyframeStyle); + } + + var itemIndex = stagger + ? options.staggerIndex >= 0 + ? options.staggerIndex + : gcsLookup.count(cacheKey) + : 0; + + var isFirst = itemIndex === 0; + + // this is a pre-emptive way of forcing the setup classes to be added and applied INSTANTLY + // without causing any combination of transitions to kick in. By adding a negative delay value + // it forces the setup class' transition to end immediately. We later then remove the negative + // transition delay to allow for the transition to naturally do it's thing. The beauty here is + // that if there is no transition defined then nothing will happen and this will also allow + // other transitions to be stacked on top of each other without any chopping them out. + if (isFirst && !options.skipBlocking) { + blockTransitions(node, SAFE_FAST_FORWARD_DURATION_VALUE); + } + + var timings = computeTimings(node, fullClassName, cacheKey); + var relativeDelay = timings.maxDelay; + maxDelay = Math.max(relativeDelay, 0); + maxDuration = timings.maxDuration; + + var flags = {}; + flags.hasTransitions = timings.transitionDuration > 0; + flags.hasAnimations = timings.animationDuration > 0; + flags.hasTransitionAll = flags.hasTransitions && timings.transitionProperty == 'all'; + flags.applyTransitionDuration = hasToStyles && ( + (flags.hasTransitions && !flags.hasTransitionAll) + || (flags.hasAnimations && !flags.hasTransitions)); + flags.applyAnimationDuration = options.duration && flags.hasAnimations; + flags.applyTransitionDelay = truthyTimingValue(options.delay) && (flags.applyTransitionDuration || flags.hasTransitions); + flags.applyAnimationDelay = truthyTimingValue(options.delay) && flags.hasAnimations; + flags.recalculateTimingStyles = addRemoveClassName.length > 0; + + if (flags.applyTransitionDuration || flags.applyAnimationDuration) { + maxDuration = options.duration ? parseFloat(options.duration) : maxDuration; + + if (flags.applyTransitionDuration) { + flags.hasTransitions = true; + timings.transitionDuration = maxDuration; + applyOnlyDuration = node.style[TRANSITION_PROP + PROPERTY_KEY].length > 0; + temporaryStyles.push(getCssTransitionDurationStyle(maxDuration, applyOnlyDuration)); + } + + if (flags.applyAnimationDuration) { + flags.hasAnimations = true; + timings.animationDuration = maxDuration; + temporaryStyles.push(getCssKeyframeDurationStyle(maxDuration)); + } + } + + if (maxDuration === 0 && !flags.recalculateTimingStyles) { + return closeAndReturnNoopAnimator(); + } + + if (options.delay != null) { + var delayStyle; + if (typeof options.delay !== "boolean") { + delayStyle = parseFloat(options.delay); + // number in options.delay means we have to recalculate the delay for the closing timeout + maxDelay = Math.max(delayStyle, 0); + } + + if (flags.applyTransitionDelay) { + temporaryStyles.push(getCssDelayStyle(delayStyle)); + } + + if (flags.applyAnimationDelay) { + temporaryStyles.push(getCssDelayStyle(delayStyle, true)); + } + } + + // we need to recalculate the delay value since we used a pre-emptive negative + // delay value and the delay value is required for the final event checking. This + // property will ensure that this will happen after the RAF phase has passed. + if (options.duration == null && timings.transitionDuration > 0) { + flags.recalculateTimingStyles = flags.recalculateTimingStyles || isFirst; + } + + maxDelayTime = maxDelay * ONE_SECOND; + maxDurationTime = maxDuration * ONE_SECOND; + if (!options.skipBlocking) { + flags.blockTransition = timings.transitionDuration > 0; + flags.blockKeyframeAnimation = timings.animationDuration > 0 && + stagger.animationDelay > 0 && + stagger.animationDuration === 0; + } + + if (options.from) { + if (options.cleanupStyles) { + registerRestorableStyles(restoreStyles, node, Object.keys(options.from)); + } + applyAnimationFromStyles(element, options); + } + + if (flags.blockTransition || flags.blockKeyframeAnimation) { + applyBlocking(maxDuration); + } else if (!options.skipBlocking) { + blockTransitions(node, false); + } + + // TODO(matsko): for 1.5 change this code to have an animator object for better debugging + return { + $$willAnimate: true, + end: endFn, + start: function() { + if (animationClosed) return; + + runnerHost = { + end: endFn, + cancel: cancelFn, + resume: null, //this will be set during the start() phase + pause: null + }; + + runner = new $$AnimateRunner(runnerHost); + + waitUntilQuiet(start); + + // we don't have access to pause/resume the animation + // since it hasn't run yet. AnimateRunner will therefore + // set noop functions for resume and pause and they will + // later be overridden once the animation is triggered + return runner; + } + }; + + function endFn() { + close(); + } + + function cancelFn() { + close(true); + } + + function close(rejected) { // jshint ignore:line + // if the promise has been called already then we shouldn't close + // the animation again + if (animationClosed || (animationCompleted && animationPaused)) return; + animationClosed = true; + animationPaused = false; + + if (!options.$$skipPreparationClasses) { + $$jqLite.removeClass(element, preparationClasses); + } + $$jqLite.removeClass(element, activeClasses); + + blockKeyframeAnimations(node, false); + blockTransitions(node, false); + + forEach(temporaryStyles, function(entry) { + // There is only one way to remove inline style properties entirely from elements. + // By using `removeProperty` this works, but we need to convert camel-cased CSS + // styles down to hyphenated values. + node.style[entry[0]] = ''; + }); + + applyAnimationClasses(element, options); + applyAnimationStyles(element, options); + + if (Object.keys(restoreStyles).length) { + forEach(restoreStyles, function(value, prop) { + value ? node.style.setProperty(prop, value) + : node.style.removeProperty(prop); + }); + } + + // the reason why we have this option is to allow a synchronous closing callback + // that is fired as SOON as the animation ends (when the CSS is removed) or if + // the animation never takes off at all. A good example is a leave animation since + // the element must be removed just after the animation is over or else the element + // will appear on screen for one animation frame causing an overbearing flicker. + if (options.onDone) { + options.onDone(); + } + + if (events && events.length) { + // Remove the transitionend / animationend listener(s) + element.off(events.join(' '), onAnimationProgress); + } + + //Cancel the fallback closing timeout and remove the timer data + var animationTimerData = element.data(ANIMATE_TIMER_KEY); + if (animationTimerData) { + $timeout.cancel(animationTimerData[0].timer); + element.removeData(ANIMATE_TIMER_KEY); + } + + // if the preparation function fails then the promise is not setup + if (runner) { + runner.complete(!rejected); + } + } + + function applyBlocking(duration) { + if (flags.blockTransition) { + blockTransitions(node, duration); + } + + if (flags.blockKeyframeAnimation) { + blockKeyframeAnimations(node, !!duration); + } + } + + function closeAndReturnNoopAnimator() { + runner = new $$AnimateRunner({ + end: endFn, + cancel: cancelFn + }); + + // should flush the cache animation + waitUntilQuiet(noop); + close(); + + return { + $$willAnimate: false, + start: function() { + return runner; + }, + end: endFn + }; + } + + function onAnimationProgress(event) { + event.stopPropagation(); + var ev = event.originalEvent || event; + + // we now always use `Date.now()` due to the recent changes with + // event.timeStamp in Firefox, Webkit and Chrome (see #13494 for more info) + var timeStamp = ev.$manualTimeStamp || Date.now(); + + /* Firefox (or possibly just Gecko) likes to not round values up + * when a ms measurement is used for the animation */ + var elapsedTime = parseFloat(ev.elapsedTime.toFixed(ELAPSED_TIME_MAX_DECIMAL_PLACES)); + + /* $manualTimeStamp is a mocked timeStamp value which is set + * within browserTrigger(). This is only here so that tests can + * mock animations properly. Real events fallback to event.timeStamp, + * or, if they don't, then a timeStamp is automatically created for them. + * We're checking to see if the timeStamp surpasses the expected delay, + * but we're using elapsedTime instead of the timeStamp on the 2nd + * pre-condition since animationPauseds sometimes close off early */ + if (Math.max(timeStamp - startTime, 0) >= maxDelayTime && elapsedTime >= maxDuration) { + // we set this flag to ensure that if the transition is paused then, when resumed, + // the animation will automatically close itself since transitions cannot be paused. + animationCompleted = true; + close(); + } + } + + function start() { + if (animationClosed) return; + if (!node.parentNode) { + close(); + return; + } + + // even though we only pause keyframe animations here the pause flag + // will still happen when transitions are used. Only the transition will + // not be paused since that is not possible. If the animation ends when + // paused then it will not complete until unpaused or cancelled. + var playPause = function(playAnimation) { + if (!animationCompleted) { + animationPaused = !playAnimation; + if (timings.animationDuration) { + var value = blockKeyframeAnimations(node, animationPaused); + animationPaused + ? temporaryStyles.push(value) + : removeFromArray(temporaryStyles, value); + } + } else if (animationPaused && playAnimation) { + animationPaused = false; + close(); + } + }; + + // checking the stagger duration prevents an accidentally cascade of the CSS delay style + // being inherited from the parent. If the transition duration is zero then we can safely + // rely that the delay value is an intentional stagger delay style. + var maxStagger = itemIndex > 0 + && ((timings.transitionDuration && stagger.transitionDuration === 0) || + (timings.animationDuration && stagger.animationDuration === 0)) + && Math.max(stagger.animationDelay, stagger.transitionDelay); + if (maxStagger) { + $timeout(triggerAnimationStart, + Math.floor(maxStagger * itemIndex * ONE_SECOND), + false); + } else { + triggerAnimationStart(); + } + + // this will decorate the existing promise runner with pause/resume methods + runnerHost.resume = function() { + playPause(true); + }; + + runnerHost.pause = function() { + playPause(false); + }; + + function triggerAnimationStart() { + // just incase a stagger animation kicks in when the animation + // itself was cancelled entirely + if (animationClosed) return; + + applyBlocking(false); + + forEach(temporaryStyles, function(entry) { + var key = entry[0]; + var value = entry[1]; + node.style[key] = value; + }); + + applyAnimationClasses(element, options); + $$jqLite.addClass(element, activeClasses); + + if (flags.recalculateTimingStyles) { + fullClassName = node.className + ' ' + preparationClasses; + cacheKey = gcsHashFn(node, fullClassName); + + timings = computeTimings(node, fullClassName, cacheKey); + relativeDelay = timings.maxDelay; + maxDelay = Math.max(relativeDelay, 0); + maxDuration = timings.maxDuration; + + if (maxDuration === 0) { + close(); + return; + } + + flags.hasTransitions = timings.transitionDuration > 0; + flags.hasAnimations = timings.animationDuration > 0; + } + + if (flags.applyAnimationDelay) { + relativeDelay = typeof options.delay !== "boolean" && truthyTimingValue(options.delay) + ? parseFloat(options.delay) + : relativeDelay; + + maxDelay = Math.max(relativeDelay, 0); + timings.animationDelay = relativeDelay; + delayStyle = getCssDelayStyle(relativeDelay, true); + temporaryStyles.push(delayStyle); + node.style[delayStyle[0]] = delayStyle[1]; + } + + maxDelayTime = maxDelay * ONE_SECOND; + maxDurationTime = maxDuration * ONE_SECOND; + + if (options.easing) { + var easeProp, easeVal = options.easing; + if (flags.hasTransitions) { + easeProp = TRANSITION_PROP + TIMING_KEY; + temporaryStyles.push([easeProp, easeVal]); + node.style[easeProp] = easeVal; + } + if (flags.hasAnimations) { + easeProp = ANIMATION_PROP + TIMING_KEY; + temporaryStyles.push([easeProp, easeVal]); + node.style[easeProp] = easeVal; + } + } + + if (timings.transitionDuration) { + events.push(TRANSITIONEND_EVENT); + } + + if (timings.animationDuration) { + events.push(ANIMATIONEND_EVENT); + } + + startTime = Date.now(); + var timerTime = maxDelayTime + CLOSING_TIME_BUFFER * maxDurationTime; + var endTime = startTime + timerTime; + + var animationsData = element.data(ANIMATE_TIMER_KEY) || []; + var setupFallbackTimer = true; + if (animationsData.length) { + var currentTimerData = animationsData[0]; + setupFallbackTimer = endTime > currentTimerData.expectedEndTime; + if (setupFallbackTimer) { + $timeout.cancel(currentTimerData.timer); + } else { + animationsData.push(close); + } + } + + if (setupFallbackTimer) { + var timer = $timeout(onAnimationExpired, timerTime, false); + animationsData[0] = { + timer: timer, + expectedEndTime: endTime + }; + animationsData.push(close); + element.data(ANIMATE_TIMER_KEY, animationsData); + } + + if (events.length) { + element.on(events.join(' '), onAnimationProgress); + } + + if (options.to) { + if (options.cleanupStyles) { + registerRestorableStyles(restoreStyles, node, Object.keys(options.to)); + } + applyAnimationToStyles(element, options); + } + } + + function onAnimationExpired() { + var animationsData = element.data(ANIMATE_TIMER_KEY); + + // this will be false in the event that the element was + // removed from the DOM (via a leave animation or something + // similar) + if (animationsData) { + for (var i = 1; i < animationsData.length; i++) { + animationsData[i](); + } + element.removeData(ANIMATE_TIMER_KEY); + } + } + } + }; + }]; +}]; + +var $$AnimateCssDriverProvider = ['$$animationProvider', function($$animationProvider) { + $$animationProvider.drivers.push('$$animateCssDriver'); + + var NG_ANIMATE_SHIM_CLASS_NAME = 'ng-animate-shim'; + var NG_ANIMATE_ANCHOR_CLASS_NAME = 'ng-anchor'; + + var NG_OUT_ANCHOR_CLASS_NAME = 'ng-anchor-out'; + var NG_IN_ANCHOR_CLASS_NAME = 'ng-anchor-in'; + + function isDocumentFragment(node) { + return node.parentNode && node.parentNode.nodeType === 11; + } + + this.$get = ['$animateCss', '$rootScope', '$$AnimateRunner', '$rootElement', '$sniffer', '$$jqLite', '$document', + function($animateCss, $rootScope, $$AnimateRunner, $rootElement, $sniffer, $$jqLite, $document) { + + // only browsers that support these properties can render animations + if (!$sniffer.animations && !$sniffer.transitions) return noop; + + var bodyNode = $document[0].body; + var rootNode = getDomNode($rootElement); + + var rootBodyElement = jqLite( + // this is to avoid using something that exists outside of the body + // we also special case the doc fragment case because our unit test code + // appends the $rootElement to the body after the app has been bootstrapped + isDocumentFragment(rootNode) || bodyNode.contains(rootNode) ? rootNode : bodyNode + ); + + var applyAnimationClasses = applyAnimationClassesFactory($$jqLite); + + return function initDriverFn(animationDetails) { + return animationDetails.from && animationDetails.to + ? prepareFromToAnchorAnimation(animationDetails.from, + animationDetails.to, + animationDetails.classes, + animationDetails.anchors) + : prepareRegularAnimation(animationDetails); + }; + + function filterCssClasses(classes) { + //remove all the `ng-` stuff + return classes.replace(/\bng-\S+\b/g, ''); + } + + function getUniqueValues(a, b) { + if (isString(a)) a = a.split(' '); + if (isString(b)) b = b.split(' '); + return a.filter(function(val) { + return b.indexOf(val) === -1; + }).join(' '); + } + + function prepareAnchoredAnimation(classes, outAnchor, inAnchor) { + var clone = jqLite(getDomNode(outAnchor).cloneNode(true)); + var startingClasses = filterCssClasses(getClassVal(clone)); + + outAnchor.addClass(NG_ANIMATE_SHIM_CLASS_NAME); + inAnchor.addClass(NG_ANIMATE_SHIM_CLASS_NAME); + + clone.addClass(NG_ANIMATE_ANCHOR_CLASS_NAME); + + rootBodyElement.append(clone); + + var animatorIn, animatorOut = prepareOutAnimation(); + + // the user may not end up using the `out` animation and + // only making use of the `in` animation or vice-versa. + // In either case we should allow this and not assume the + // animation is over unless both animations are not used. + if (!animatorOut) { + animatorIn = prepareInAnimation(); + if (!animatorIn) { + return end(); + } + } + + var startingAnimator = animatorOut || animatorIn; + + return { + start: function() { + var runner; + + var currentAnimation = startingAnimator.start(); + currentAnimation.done(function() { + currentAnimation = null; + if (!animatorIn) { + animatorIn = prepareInAnimation(); + if (animatorIn) { + currentAnimation = animatorIn.start(); + currentAnimation.done(function() { + currentAnimation = null; + end(); + runner.complete(); + }); + return currentAnimation; + } + } + // in the event that there is no `in` animation + end(); + runner.complete(); + }); + + runner = new $$AnimateRunner({ + end: endFn, + cancel: endFn + }); + + return runner; + + function endFn() { + if (currentAnimation) { + currentAnimation.end(); + } + } + } + }; + + function calculateAnchorStyles(anchor) { + var styles = {}; + + var coords = getDomNode(anchor).getBoundingClientRect(); + + // we iterate directly since safari messes up and doesn't return + // all the keys for the coords object when iterated + forEach(['width','height','top','left'], function(key) { + var value = coords[key]; + switch (key) { + case 'top': + value += bodyNode.scrollTop; + break; + case 'left': + value += bodyNode.scrollLeft; + break; + } + styles[key] = Math.floor(value) + 'px'; + }); + return styles; + } + + function prepareOutAnimation() { + var animator = $animateCss(clone, { + addClass: NG_OUT_ANCHOR_CLASS_NAME, + delay: true, + from: calculateAnchorStyles(outAnchor) + }); + + // read the comment within `prepareRegularAnimation` to understand + // why this check is necessary + return animator.$$willAnimate ? animator : null; + } + + function getClassVal(element) { + return element.attr('class') || ''; + } + + function prepareInAnimation() { + var endingClasses = filterCssClasses(getClassVal(inAnchor)); + var toAdd = getUniqueValues(endingClasses, startingClasses); + var toRemove = getUniqueValues(startingClasses, endingClasses); + + var animator = $animateCss(clone, { + to: calculateAnchorStyles(inAnchor), + addClass: NG_IN_ANCHOR_CLASS_NAME + ' ' + toAdd, + removeClass: NG_OUT_ANCHOR_CLASS_NAME + ' ' + toRemove, + delay: true + }); + + // read the comment within `prepareRegularAnimation` to understand + // why this check is necessary + return animator.$$willAnimate ? animator : null; + } + + function end() { + clone.remove(); + outAnchor.removeClass(NG_ANIMATE_SHIM_CLASS_NAME); + inAnchor.removeClass(NG_ANIMATE_SHIM_CLASS_NAME); + } + } + + function prepareFromToAnchorAnimation(from, to, classes, anchors) { + var fromAnimation = prepareRegularAnimation(from, noop); + var toAnimation = prepareRegularAnimation(to, noop); + + var anchorAnimations = []; + forEach(anchors, function(anchor) { + var outElement = anchor['out']; + var inElement = anchor['in']; + var animator = prepareAnchoredAnimation(classes, outElement, inElement); + if (animator) { + anchorAnimations.push(animator); + } + }); + + // no point in doing anything when there are no elements to animate + if (!fromAnimation && !toAnimation && anchorAnimations.length === 0) return; + + return { + start: function() { + var animationRunners = []; + + if (fromAnimation) { + animationRunners.push(fromAnimation.start()); + } + + if (toAnimation) { + animationRunners.push(toAnimation.start()); + } + + forEach(anchorAnimations, function(animation) { + animationRunners.push(animation.start()); + }); + + var runner = new $$AnimateRunner({ + end: endFn, + cancel: endFn // CSS-driven animations cannot be cancelled, only ended + }); + + $$AnimateRunner.all(animationRunners, function(status) { + runner.complete(status); + }); + + return runner; + + function endFn() { + forEach(animationRunners, function(runner) { + runner.end(); + }); + } + } + }; + } + + function prepareRegularAnimation(animationDetails) { + var element = animationDetails.element; + var options = animationDetails.options || {}; + + if (animationDetails.structural) { + options.event = animationDetails.event; + options.structural = true; + options.applyClassesEarly = true; + + // we special case the leave animation since we want to ensure that + // the element is removed as soon as the animation is over. Otherwise + // a flicker might appear or the element may not be removed at all + if (animationDetails.event === 'leave') { + options.onDone = options.domOperation; + } + } + + // We assign the preparationClasses as the actual animation event since + // the internals of $animateCss will just suffix the event token values + // with `-active` to trigger the animation. + if (options.preparationClasses) { + options.event = concatWithSpace(options.event, options.preparationClasses); + } + + var animator = $animateCss(element, options); + + // the driver lookup code inside of $$animation attempts to spawn a + // driver one by one until a driver returns a.$$willAnimate animator object. + // $animateCss will always return an object, however, it will pass in + // a flag as a hint as to whether an animation was detected or not + return animator.$$willAnimate ? animator : null; + } + }]; +}]; + +// TODO(matsko): use caching here to speed things up for detection +// TODO(matsko): add documentation +// by the time... + +var $$AnimateJsProvider = ['$animateProvider', function($animateProvider) { + this.$get = ['$injector', '$$AnimateRunner', '$$jqLite', + function($injector, $$AnimateRunner, $$jqLite) { + + var applyAnimationClasses = applyAnimationClassesFactory($$jqLite); + // $animateJs(element, 'enter'); + return function(element, event, classes, options) { + var animationClosed = false; + + // the `classes` argument is optional and if it is not used + // then the classes will be resolved from the element's className + // property as well as options.addClass/options.removeClass. + if (arguments.length === 3 && isObject(classes)) { + options = classes; + classes = null; + } + + options = prepareAnimationOptions(options); + if (!classes) { + classes = element.attr('class') || ''; + if (options.addClass) { + classes += ' ' + options.addClass; + } + if (options.removeClass) { + classes += ' ' + options.removeClass; + } + } + + var classesToAdd = options.addClass; + var classesToRemove = options.removeClass; + + // the lookupAnimations function returns a series of animation objects that are + // matched up with one or more of the CSS classes. These animation objects are + // defined via the module.animation factory function. If nothing is detected then + // we don't return anything which then makes $animation query the next driver. + var animations = lookupAnimations(classes); + var before, after; + if (animations.length) { + var afterFn, beforeFn; + if (event == 'leave') { + beforeFn = 'leave'; + afterFn = 'afterLeave'; // TODO(matsko): get rid of this + } else { + beforeFn = 'before' + event.charAt(0).toUpperCase() + event.substr(1); + afterFn = event; + } + + if (event !== 'enter' && event !== 'move') { + before = packageAnimations(element, event, options, animations, beforeFn); + } + after = packageAnimations(element, event, options, animations, afterFn); + } + + // no matching animations + if (!before && !after) return; + + function applyOptions() { + options.domOperation(); + applyAnimationClasses(element, options); + } + + function close() { + animationClosed = true; + applyOptions(); + applyAnimationStyles(element, options); + } + + var runner; + + return { + $$willAnimate: true, + end: function() { + if (runner) { + runner.end(); + } else { + close(); + runner = new $$AnimateRunner(); + runner.complete(true); + } + return runner; + }, + start: function() { + if (runner) { + return runner; + } + + runner = new $$AnimateRunner(); + var closeActiveAnimations; + var chain = []; + + if (before) { + chain.push(function(fn) { + closeActiveAnimations = before(fn); + }); + } + + if (chain.length) { + chain.push(function(fn) { + applyOptions(); + fn(true); + }); + } else { + applyOptions(); + } + + if (after) { + chain.push(function(fn) { + closeActiveAnimations = after(fn); + }); + } + + runner.setHost({ + end: function() { + endAnimations(); + }, + cancel: function() { + endAnimations(true); + } + }); + + $$AnimateRunner.chain(chain, onComplete); + return runner; + + function onComplete(success) { + close(success); + runner.complete(success); + } + + function endAnimations(cancelled) { + if (!animationClosed) { + (closeActiveAnimations || noop)(cancelled); + onComplete(cancelled); + } + } + } + }; + + function executeAnimationFn(fn, element, event, options, onDone) { + var args; + switch (event) { + case 'animate': + args = [element, options.from, options.to, onDone]; + break; + + case 'setClass': + args = [element, classesToAdd, classesToRemove, onDone]; + break; + + case 'addClass': + args = [element, classesToAdd, onDone]; + break; + + case 'removeClass': + args = [element, classesToRemove, onDone]; + break; + + default: + args = [element, onDone]; + break; + } + + args.push(options); + + var value = fn.apply(fn, args); + if (value) { + if (isFunction(value.start)) { + value = value.start(); + } + + if (value instanceof $$AnimateRunner) { + value.done(onDone); + } else if (isFunction(value)) { + // optional onEnd / onCancel callback + return value; + } + } + + return noop; + } + + function groupEventedAnimations(element, event, options, animations, fnName) { + var operations = []; + forEach(animations, function(ani) { + var animation = ani[fnName]; + if (!animation) return; + + // note that all of these animations will run in parallel + operations.push(function() { + var runner; + var endProgressCb; + + var resolved = false; + var onAnimationComplete = function(rejected) { + if (!resolved) { + resolved = true; + (endProgressCb || noop)(rejected); + runner.complete(!rejected); + } + }; + + runner = new $$AnimateRunner({ + end: function() { + onAnimationComplete(); + }, + cancel: function() { + onAnimationComplete(true); + } + }); + + endProgressCb = executeAnimationFn(animation, element, event, options, function(result) { + var cancelled = result === false; + onAnimationComplete(cancelled); + }); + + return runner; + }); + }); + + return operations; + } + + function packageAnimations(element, event, options, animations, fnName) { + var operations = groupEventedAnimations(element, event, options, animations, fnName); + if (operations.length === 0) { + var a,b; + if (fnName === 'beforeSetClass') { + a = groupEventedAnimations(element, 'removeClass', options, animations, 'beforeRemoveClass'); + b = groupEventedAnimations(element, 'addClass', options, animations, 'beforeAddClass'); + } else if (fnName === 'setClass') { + a = groupEventedAnimations(element, 'removeClass', options, animations, 'removeClass'); + b = groupEventedAnimations(element, 'addClass', options, animations, 'addClass'); + } + + if (a) { + operations = operations.concat(a); + } + if (b) { + operations = operations.concat(b); + } + } + + if (operations.length === 0) return; + + // TODO(matsko): add documentation + return function startAnimation(callback) { + var runners = []; + if (operations.length) { + forEach(operations, function(animateFn) { + runners.push(animateFn()); + }); + } + + runners.length ? $$AnimateRunner.all(runners, callback) : callback(); + + return function endFn(reject) { + forEach(runners, function(runner) { + reject ? runner.cancel() : runner.end(); + }); + }; + }; + } + }; + + function lookupAnimations(classes) { + classes = isArray(classes) ? classes : classes.split(' '); + var matches = [], flagMap = {}; + for (var i=0; i < classes.length; i++) { + var klass = classes[i], + animationFactory = $animateProvider.$$registeredAnimations[klass]; + if (animationFactory && !flagMap[klass]) { + matches.push($injector.get(animationFactory)); + flagMap[klass] = true; + } + } + return matches; + } + }]; +}]; + +var $$AnimateJsDriverProvider = ['$$animationProvider', function($$animationProvider) { + $$animationProvider.drivers.push('$$animateJsDriver'); + this.$get = ['$$animateJs', '$$AnimateRunner', function($$animateJs, $$AnimateRunner) { + return function initDriverFn(animationDetails) { + if (animationDetails.from && animationDetails.to) { + var fromAnimation = prepareAnimation(animationDetails.from); + var toAnimation = prepareAnimation(animationDetails.to); + if (!fromAnimation && !toAnimation) return; + + return { + start: function() { + var animationRunners = []; + + if (fromAnimation) { + animationRunners.push(fromAnimation.start()); + } + + if (toAnimation) { + animationRunners.push(toAnimation.start()); + } + + $$AnimateRunner.all(animationRunners, done); + + var runner = new $$AnimateRunner({ + end: endFnFactory(), + cancel: endFnFactory() + }); + + return runner; + + function endFnFactory() { + return function() { + forEach(animationRunners, function(runner) { + // at this point we cannot cancel animations for groups just yet. 1.5+ + runner.end(); + }); + }; + } + + function done(status) { + runner.complete(status); + } + } + }; + } else { + return prepareAnimation(animationDetails); + } + }; + + function prepareAnimation(animationDetails) { + // TODO(matsko): make sure to check for grouped animations and delegate down to normal animations + var element = animationDetails.element; + var event = animationDetails.event; + var options = animationDetails.options; + var classes = animationDetails.classes; + return $$animateJs(element, event, classes, options); + } + }]; +}]; + +var NG_ANIMATE_ATTR_NAME = 'data-ng-animate'; +var NG_ANIMATE_PIN_DATA = '$ngAnimatePin'; +var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) { + var PRE_DIGEST_STATE = 1; + var RUNNING_STATE = 2; + var ONE_SPACE = ' '; + + var rules = this.rules = { + skip: [], + cancel: [], + join: [] + }; + + function makeTruthyCssClassMap(classString) { + if (!classString) { + return null; + } + + var keys = classString.split(ONE_SPACE); + var map = Object.create(null); + + forEach(keys, function(key) { + map[key] = true; + }); + return map; + } + + function hasMatchingClasses(newClassString, currentClassString) { + if (newClassString && currentClassString) { + var currentClassMap = makeTruthyCssClassMap(currentClassString); + return newClassString.split(ONE_SPACE).some(function(className) { + return currentClassMap[className]; + }); + } + } + + function isAllowed(ruleType, element, currentAnimation, previousAnimation) { + return rules[ruleType].some(function(fn) { + return fn(element, currentAnimation, previousAnimation); + }); + } + + function hasAnimationClasses(animation, and) { + var a = (animation.addClass || '').length > 0; + var b = (animation.removeClass || '').length > 0; + return and ? a && b : a || b; + } + + rules.join.push(function(element, newAnimation, currentAnimation) { + // if the new animation is class-based then we can just tack that on + return !newAnimation.structural && hasAnimationClasses(newAnimation); + }); + + rules.skip.push(function(element, newAnimation, currentAnimation) { + // there is no need to animate anything if no classes are being added and + // there is no structural animation that will be triggered + return !newAnimation.structural && !hasAnimationClasses(newAnimation); + }); + + rules.skip.push(function(element, newAnimation, currentAnimation) { + // why should we trigger a new structural animation if the element will + // be removed from the DOM anyway? + return currentAnimation.event == 'leave' && newAnimation.structural; + }); + + rules.skip.push(function(element, newAnimation, currentAnimation) { + // if there is an ongoing current animation then don't even bother running the class-based animation + return currentAnimation.structural && currentAnimation.state === RUNNING_STATE && !newAnimation.structural; + }); + + rules.cancel.push(function(element, newAnimation, currentAnimation) { + // there can never be two structural animations running at the same time + return currentAnimation.structural && newAnimation.structural; + }); + + rules.cancel.push(function(element, newAnimation, currentAnimation) { + // if the previous animation is already running, but the new animation will + // be triggered, but the new animation is structural + return currentAnimation.state === RUNNING_STATE && newAnimation.structural; + }); + + rules.cancel.push(function(element, newAnimation, currentAnimation) { + // cancel the animation if classes added / removed in both animation cancel each other out, + // but only if the current animation isn't structural + + if (currentAnimation.structural) return false; + + var nA = newAnimation.addClass; + var nR = newAnimation.removeClass; + var cA = currentAnimation.addClass; + var cR = currentAnimation.removeClass; + + // early detection to save the global CPU shortage :) + if ((isUndefined(nA) && isUndefined(nR)) || (isUndefined(cA) && isUndefined(cR))) { + return false; + } + + return hasMatchingClasses(nA, cR) || hasMatchingClasses(nR, cA); + }); + + this.$get = ['$$rAF', '$rootScope', '$rootElement', '$document', '$$HashMap', + '$$animation', '$$AnimateRunner', '$templateRequest', '$$jqLite', '$$forceReflow', + function($$rAF, $rootScope, $rootElement, $document, $$HashMap, + $$animation, $$AnimateRunner, $templateRequest, $$jqLite, $$forceReflow) { + + var activeAnimationsLookup = new $$HashMap(); + var disabledElementsLookup = new $$HashMap(); + var animationsEnabled = null; + + function postDigestTaskFactory() { + var postDigestCalled = false; + return function(fn) { + // we only issue a call to postDigest before + // it has first passed. This prevents any callbacks + // from not firing once the animation has completed + // since it will be out of the digest cycle. + if (postDigestCalled) { + fn(); + } else { + $rootScope.$$postDigest(function() { + postDigestCalled = true; + fn(); + }); + } + }; + } + + // Wait until all directive and route-related templates are downloaded and + // compiled. The $templateRequest.totalPendingRequests variable keeps track of + // all of the remote templates being currently downloaded. If there are no + // templates currently downloading then the watcher will still fire anyway. + var deregisterWatch = $rootScope.$watch( + function() { return $templateRequest.totalPendingRequests === 0; }, + function(isEmpty) { + if (!isEmpty) return; + deregisterWatch(); + + // Now that all templates have been downloaded, $animate will wait until + // the post digest queue is empty before enabling animations. By having two + // calls to $postDigest calls we can ensure that the flag is enabled at the + // very end of the post digest queue. Since all of the animations in $animate + // use $postDigest, it's important that the code below executes at the end. + // This basically means that the page is fully downloaded and compiled before + // any animations are triggered. + $rootScope.$$postDigest(function() { + $rootScope.$$postDigest(function() { + // we check for null directly in the event that the application already called + // .enabled() with whatever arguments that it provided it with + if (animationsEnabled === null) { + animationsEnabled = true; + } + }); + }); + } + ); + + var callbackRegistry = {}; + + // remember that the classNameFilter is set during the provider/config + // stage therefore we can optimize here and setup a helper function + var classNameFilter = $animateProvider.classNameFilter(); + var isAnimatableClassName = !classNameFilter + ? function() { return true; } + : function(className) { + return classNameFilter.test(className); + }; + + var applyAnimationClasses = applyAnimationClassesFactory($$jqLite); + + function normalizeAnimationDetails(element, animation) { + return mergeAnimationDetails(element, animation, {}); + } + + // IE9-11 has no method "contains" in SVG element and in Node.prototype. Bug #10259. + var contains = window.Node.prototype.contains || function(arg) { + // jshint bitwise: false + return this === arg || !!(this.compareDocumentPosition(arg) & 16); + // jshint bitwise: true + }; + + function findCallbacks(parent, element, event) { + var targetNode = getDomNode(element); + var targetParentNode = getDomNode(parent); + + var matches = []; + var entries = callbackRegistry[event]; + if (entries) { + forEach(entries, function(entry) { + if (contains.call(entry.node, targetNode)) { + matches.push(entry.callback); + } else if (event === 'leave' && contains.call(entry.node, targetParentNode)) { + matches.push(entry.callback); + } + }); + } + + return matches; + } + + function filterFromRegistry(list, matchContainer, matchCallback) { + var containerNode = extractElementNode(matchContainer); + return list.filter(function(entry) { + var isMatch = entry.node === containerNode && + (!matchCallback || entry.callback === matchCallback); + return !isMatch; + }); + } + + function cleanupEventListeners(phase, element) { + if (phase === 'close' && !element[0].parentNode) { + // If the element is not attached to a parentNode, it has been removed by + // the domOperation, and we can safely remove the event callbacks + $animate.off(element); + } + } + + var $animate = { + on: function(event, container, callback) { + var node = extractElementNode(container); + callbackRegistry[event] = callbackRegistry[event] || []; + callbackRegistry[event].push({ + node: node, + callback: callback + }); + + // Remove the callback when the element is removed from the DOM + jqLite(container).on('$destroy', function() { + var animationDetails = activeAnimationsLookup.get(node); + + if (!animationDetails) { + // If there's an animation ongoing, the callback calling code will remove + // the event listeners. If we'd remove here, the callbacks would be removed + // before the animation ends + $animate.off(event, container, callback); + } + }); + }, + + off: function(event, container, callback) { + if (arguments.length === 1 && !angular.isString(arguments[0])) { + container = arguments[0]; + for (var eventType in callbackRegistry) { + callbackRegistry[eventType] = filterFromRegistry(callbackRegistry[eventType], container); + } + + return; + } + + var entries = callbackRegistry[event]; + if (!entries) return; + + callbackRegistry[event] = arguments.length === 1 + ? null + : filterFromRegistry(entries, container, callback); + }, + + pin: function(element, parentElement) { + assertArg(isElement(element), 'element', 'not an element'); + assertArg(isElement(parentElement), 'parentElement', 'not an element'); + element.data(NG_ANIMATE_PIN_DATA, parentElement); + }, + + push: function(element, event, options, domOperation) { + options = options || {}; + options.domOperation = domOperation; + return queueAnimation(element, event, options); + }, + + // this method has four signatures: + // () - global getter + // (bool) - global setter + // (element) - element getter + // (element, bool) - element setter + enabled: function(element, bool) { + var argCount = arguments.length; + + if (argCount === 0) { + // () - Global getter + bool = !!animationsEnabled; + } else { + var hasElement = isElement(element); + + if (!hasElement) { + // (bool) - Global setter + bool = animationsEnabled = !!element; + } else { + var node = getDomNode(element); + var recordExists = disabledElementsLookup.get(node); + + if (argCount === 1) { + // (element) - Element getter + bool = !recordExists; + } else { + // (element, bool) - Element setter + disabledElementsLookup.put(node, !bool); + } + } + } + + return bool; + } + }; + + return $animate; + + function queueAnimation(element, event, initialOptions) { + // we always make a copy of the options since + // there should never be any side effects on + // the input data when running `$animateCss`. + var options = copy(initialOptions); + + var node, parent; + element = stripCommentsFromElement(element); + if (element) { + node = getDomNode(element); + parent = element.parent(); + } + + options = prepareAnimationOptions(options); + + // we create a fake runner with a working promise. + // These methods will become available after the digest has passed + var runner = new $$AnimateRunner(); + + // this is used to trigger callbacks in postDigest mode + var runInNextPostDigestOrNow = postDigestTaskFactory(); + + if (isArray(options.addClass)) { + options.addClass = options.addClass.join(' '); + } + + if (options.addClass && !isString(options.addClass)) { + options.addClass = null; + } + + if (isArray(options.removeClass)) { + options.removeClass = options.removeClass.join(' '); + } + + if (options.removeClass && !isString(options.removeClass)) { + options.removeClass = null; + } + + if (options.from && !isObject(options.from)) { + options.from = null; + } + + if (options.to && !isObject(options.to)) { + options.to = null; + } + + // there are situations where a directive issues an animation for + // a jqLite wrapper that contains only comment nodes... If this + // happens then there is no way we can perform an animation + if (!node) { + close(); + return runner; + } + + var className = [node.className, options.addClass, options.removeClass].join(' '); + if (!isAnimatableClassName(className)) { + close(); + return runner; + } + + var isStructural = ['enter', 'move', 'leave'].indexOf(event) >= 0; + + var documentHidden = $document[0].hidden; + + // this is a hard disable of all animations for the application or on + // the element itself, therefore there is no need to continue further + // past this point if not enabled + // Animations are also disabled if the document is currently hidden (page is not visible + // to the user), because browsers slow down or do not flush calls to requestAnimationFrame + var skipAnimations = !animationsEnabled || documentHidden || disabledElementsLookup.get(node); + var existingAnimation = (!skipAnimations && activeAnimationsLookup.get(node)) || {}; + var hasExistingAnimation = !!existingAnimation.state; + + // there is no point in traversing the same collection of parent ancestors if a followup + // animation will be run on the same element that already did all that checking work + if (!skipAnimations && (!hasExistingAnimation || existingAnimation.state != PRE_DIGEST_STATE)) { + skipAnimations = !areAnimationsAllowed(element, parent, event); + } + + if (skipAnimations) { + // Callbacks should fire even if the document is hidden (regression fix for issue #14120) + if (documentHidden) notifyProgress(runner, event, 'start'); + close(); + if (documentHidden) notifyProgress(runner, event, 'close'); + return runner; + } + + if (isStructural) { + closeChildAnimations(element); + } + + var newAnimation = { + structural: isStructural, + element: element, + event: event, + addClass: options.addClass, + removeClass: options.removeClass, + close: close, + options: options, + runner: runner + }; + + if (hasExistingAnimation) { + var skipAnimationFlag = isAllowed('skip', element, newAnimation, existingAnimation); + if (skipAnimationFlag) { + if (existingAnimation.state === RUNNING_STATE) { + close(); + return runner; + } else { + mergeAnimationDetails(element, existingAnimation, newAnimation); + return existingAnimation.runner; + } + } + var cancelAnimationFlag = isAllowed('cancel', element, newAnimation, existingAnimation); + if (cancelAnimationFlag) { + if (existingAnimation.state === RUNNING_STATE) { + // this will end the animation right away and it is safe + // to do so since the animation is already running and the + // runner callback code will run in async + existingAnimation.runner.end(); + } else if (existingAnimation.structural) { + // this means that the animation is queued into a digest, but + // hasn't started yet. Therefore it is safe to run the close + // method which will call the runner methods in async. + existingAnimation.close(); + } else { + // this will merge the new animation options into existing animation options + mergeAnimationDetails(element, existingAnimation, newAnimation); + + return existingAnimation.runner; + } + } else { + // a joined animation means that this animation will take over the existing one + // so an example would involve a leave animation taking over an enter. Then when + // the postDigest kicks in the enter will be ignored. + var joinAnimationFlag = isAllowed('join', element, newAnimation, existingAnimation); + if (joinAnimationFlag) { + if (existingAnimation.state === RUNNING_STATE) { + normalizeAnimationDetails(element, newAnimation); + } else { + applyGeneratedPreparationClasses(element, isStructural ? event : null, options); + + event = newAnimation.event = existingAnimation.event; + options = mergeAnimationDetails(element, existingAnimation, newAnimation); + + //we return the same runner since only the option values of this animation will + //be fed into the `existingAnimation`. + return existingAnimation.runner; + } + } + } + } else { + // normalization in this case means that it removes redundant CSS classes that + // already exist (addClass) or do not exist (removeClass) on the element + normalizeAnimationDetails(element, newAnimation); + } + + // when the options are merged and cleaned up we may end up not having to do + // an animation at all, therefore we should check this before issuing a post + // digest callback. Structural animations will always run no matter what. + var isValidAnimation = newAnimation.structural; + if (!isValidAnimation) { + // animate (from/to) can be quickly checked first, otherwise we check if any classes are present + isValidAnimation = (newAnimation.event === 'animate' && Object.keys(newAnimation.options.to || {}).length > 0) + || hasAnimationClasses(newAnimation); + } + + if (!isValidAnimation) { + close(); + clearElementAnimationState(element); + return runner; + } + + // the counter keeps track of cancelled animations + var counter = (existingAnimation.counter || 0) + 1; + newAnimation.counter = counter; + + markElementAnimationState(element, PRE_DIGEST_STATE, newAnimation); + + $rootScope.$$postDigest(function() { + var animationDetails = activeAnimationsLookup.get(node); + var animationCancelled = !animationDetails; + animationDetails = animationDetails || {}; + + // if addClass/removeClass is called before something like enter then the + // registered parent element may not be present. The code below will ensure + // that a final value for parent element is obtained + var parentElement = element.parent() || []; + + // animate/structural/class-based animations all have requirements. Otherwise there + // is no point in performing an animation. The parent node must also be set. + var isValidAnimation = parentElement.length > 0 + && (animationDetails.event === 'animate' + || animationDetails.structural + || hasAnimationClasses(animationDetails)); + + // this means that the previous animation was cancelled + // even if the follow-up animation is the same event + if (animationCancelled || animationDetails.counter !== counter || !isValidAnimation) { + // if another animation did not take over then we need + // to make sure that the domOperation and options are + // handled accordingly + if (animationCancelled) { + applyAnimationClasses(element, options); + applyAnimationStyles(element, options); + } + + // if the event changed from something like enter to leave then we do + // it, otherwise if it's the same then the end result will be the same too + if (animationCancelled || (isStructural && animationDetails.event !== event)) { + options.domOperation(); + runner.end(); + } + + // in the event that the element animation was not cancelled or a follow-up animation + // isn't allowed to animate from here then we need to clear the state of the element + // so that any future animations won't read the expired animation data. + if (!isValidAnimation) { + clearElementAnimationState(element); + } + + return; + } + + // this combined multiple class to addClass / removeClass into a setClass event + // so long as a structural event did not take over the animation + event = !animationDetails.structural && hasAnimationClasses(animationDetails, true) + ? 'setClass' + : animationDetails.event; + + markElementAnimationState(element, RUNNING_STATE); + var realRunner = $$animation(element, event, animationDetails.options); + + // this will update the runner's flow-control events based on + // the `realRunner` object. + runner.setHost(realRunner); + notifyProgress(runner, event, 'start', {}); + + realRunner.done(function(status) { + close(!status); + var animationDetails = activeAnimationsLookup.get(node); + if (animationDetails && animationDetails.counter === counter) { + clearElementAnimationState(getDomNode(element)); + } + notifyProgress(runner, event, 'close', {}); + }); + }); + + return runner; + + function notifyProgress(runner, event, phase, data) { + runInNextPostDigestOrNow(function() { + var callbacks = findCallbacks(parent, element, event); + if (callbacks.length) { + // do not optimize this call here to RAF because + // we don't know how heavy the callback code here will + // be and if this code is buffered then this can + // lead to a performance regression. + $$rAF(function() { + forEach(callbacks, function(callback) { + callback(element, phase, data); + }); + cleanupEventListeners(phase, element); + }); + } else { + cleanupEventListeners(phase, element); + } + }); + runner.progress(event, phase, data); + } + + function close(reject) { // jshint ignore:line + clearGeneratedClasses(element, options); + applyAnimationClasses(element, options); + applyAnimationStyles(element, options); + options.domOperation(); + runner.complete(!reject); + } + } + + function closeChildAnimations(element) { + var node = getDomNode(element); + var children = node.querySelectorAll('[' + NG_ANIMATE_ATTR_NAME + ']'); + forEach(children, function(child) { + var state = parseInt(child.getAttribute(NG_ANIMATE_ATTR_NAME)); + var animationDetails = activeAnimationsLookup.get(child); + if (animationDetails) { + switch (state) { + case RUNNING_STATE: + animationDetails.runner.end(); + /* falls through */ + case PRE_DIGEST_STATE: + activeAnimationsLookup.remove(child); + break; + } + } + }); + } + + function clearElementAnimationState(element) { + var node = getDomNode(element); + node.removeAttribute(NG_ANIMATE_ATTR_NAME); + activeAnimationsLookup.remove(node); + } + + function isMatchingElement(nodeOrElmA, nodeOrElmB) { + return getDomNode(nodeOrElmA) === getDomNode(nodeOrElmB); + } + + /** + * This fn returns false if any of the following is true: + * a) animations on any parent element are disabled, and animations on the element aren't explicitly allowed + * b) a parent element has an ongoing structural animation, and animateChildren is false + * c) the element is not a child of the body + * d) the element is not a child of the $rootElement + */ + function areAnimationsAllowed(element, parentElement, event) { + var bodyElement = jqLite($document[0].body); + var bodyElementDetected = isMatchingElement(element, bodyElement) || element[0].nodeName === 'HTML'; + var rootElementDetected = isMatchingElement(element, $rootElement); + var parentAnimationDetected = false; + var animateChildren; + var elementDisabled = disabledElementsLookup.get(getDomNode(element)); + + var parentHost = jqLite.data(element[0], NG_ANIMATE_PIN_DATA); + if (parentHost) { + parentElement = parentHost; + } + + parentElement = getDomNode(parentElement); + + while (parentElement) { + if (!rootElementDetected) { + // angular doesn't want to attempt to animate elements outside of the application + // therefore we need to ensure that the rootElement is an ancestor of the current element + rootElementDetected = isMatchingElement(parentElement, $rootElement); + } + + if (parentElement.nodeType !== ELEMENT_NODE) { + // no point in inspecting the #document element + break; + } + + var details = activeAnimationsLookup.get(parentElement) || {}; + // either an enter, leave or move animation will commence + // therefore we can't allow any animations to take place + // but if a parent animation is class-based then that's ok + if (!parentAnimationDetected) { + var parentElementDisabled = disabledElementsLookup.get(parentElement); + + if (parentElementDisabled === true && elementDisabled !== false) { + // disable animations if the user hasn't explicitly enabled animations on the + // current element + elementDisabled = true; + // element is disabled via parent element, no need to check anything else + break; + } else if (parentElementDisabled === false) { + elementDisabled = false; + } + parentAnimationDetected = details.structural; + } + + if (isUndefined(animateChildren) || animateChildren === true) { + var value = jqLite.data(parentElement, NG_ANIMATE_CHILDREN_DATA); + if (isDefined(value)) { + animateChildren = value; + } + } + + // there is no need to continue traversing at this point + if (parentAnimationDetected && animateChildren === false) break; + + if (!bodyElementDetected) { + // we also need to ensure that the element is or will be a part of the body element + // otherwise it is pointless to even issue an animation to be rendered + bodyElementDetected = isMatchingElement(parentElement, bodyElement); + } + + if (bodyElementDetected && rootElementDetected) { + // If both body and root have been found, any other checks are pointless, + // as no animation data should live outside the application + break; + } + + if (!rootElementDetected) { + // If no rootElement is detected, check if the parentElement is pinned to another element + parentHost = jqLite.data(parentElement, NG_ANIMATE_PIN_DATA); + if (parentHost) { + // The pin target element becomes the next parent element + parentElement = getDomNode(parentHost); + continue; + } + } + + parentElement = parentElement.parentNode; + } + + var allowAnimation = (!parentAnimationDetected || animateChildren) && elementDisabled !== true; + return allowAnimation && rootElementDetected && bodyElementDetected; + } + + function markElementAnimationState(element, state, details) { + details = details || {}; + details.state = state; + + var node = getDomNode(element); + node.setAttribute(NG_ANIMATE_ATTR_NAME, state); + + var oldValue = activeAnimationsLookup.get(node); + var newValue = oldValue + ? extend(oldValue, details) + : details; + activeAnimationsLookup.put(node, newValue); + } + }]; +}]; + +var $$AnimationProvider = ['$animateProvider', function($animateProvider) { + var NG_ANIMATE_REF_ATTR = 'ng-animate-ref'; + + var drivers = this.drivers = []; + + var RUNNER_STORAGE_KEY = '$$animationRunner'; + + function setRunner(element, runner) { + element.data(RUNNER_STORAGE_KEY, runner); + } + + function removeRunner(element) { + element.removeData(RUNNER_STORAGE_KEY); + } + + function getRunner(element) { + return element.data(RUNNER_STORAGE_KEY); + } + + this.$get = ['$$jqLite', '$rootScope', '$injector', '$$AnimateRunner', '$$HashMap', '$$rAFScheduler', + function($$jqLite, $rootScope, $injector, $$AnimateRunner, $$HashMap, $$rAFScheduler) { + + var animationQueue = []; + var applyAnimationClasses = applyAnimationClassesFactory($$jqLite); + + function sortAnimations(animations) { + var tree = { children: [] }; + var i, lookup = new $$HashMap(); + + // this is done first beforehand so that the hashmap + // is filled with a list of the elements that will be animated + for (i = 0; i < animations.length; i++) { + var animation = animations[i]; + lookup.put(animation.domNode, animations[i] = { + domNode: animation.domNode, + fn: animation.fn, + children: [] + }); + } + + for (i = 0; i < animations.length; i++) { + processNode(animations[i]); + } + + return flatten(tree); + + function processNode(entry) { + if (entry.processed) return entry; + entry.processed = true; + + var elementNode = entry.domNode; + var parentNode = elementNode.parentNode; + lookup.put(elementNode, entry); + + var parentEntry; + while (parentNode) { + parentEntry = lookup.get(parentNode); + if (parentEntry) { + if (!parentEntry.processed) { + parentEntry = processNode(parentEntry); + } + break; + } + parentNode = parentNode.parentNode; + } + + (parentEntry || tree).children.push(entry); + return entry; + } + + function flatten(tree) { + var result = []; + var queue = []; + var i; + + for (i = 0; i < tree.children.length; i++) { + queue.push(tree.children[i]); + } + + var remainingLevelEntries = queue.length; + var nextLevelEntries = 0; + var row = []; + + for (i = 0; i < queue.length; i++) { + var entry = queue[i]; + if (remainingLevelEntries <= 0) { + remainingLevelEntries = nextLevelEntries; + nextLevelEntries = 0; + result.push(row); + row = []; + } + row.push(entry.fn); + entry.children.forEach(function(childEntry) { + nextLevelEntries++; + queue.push(childEntry); + }); + remainingLevelEntries--; + } + + if (row.length) { + result.push(row); + } + + return result; + } + } + + // TODO(matsko): document the signature in a better way + return function(element, event, options) { + options = prepareAnimationOptions(options); + var isStructural = ['enter', 'move', 'leave'].indexOf(event) >= 0; + + // there is no animation at the current moment, however + // these runner methods will get later updated with the + // methods leading into the driver's end/cancel methods + // for now they just stop the animation from starting + var runner = new $$AnimateRunner({ + end: function() { close(); }, + cancel: function() { close(true); } + }); + + if (!drivers.length) { + close(); + return runner; + } + + setRunner(element, runner); + + var classes = mergeClasses(element.attr('class'), mergeClasses(options.addClass, options.removeClass)); + var tempClasses = options.tempClasses; + if (tempClasses) { + classes += ' ' + tempClasses; + options.tempClasses = null; + } + + var prepareClassName; + if (isStructural) { + prepareClassName = 'ng-' + event + PREPARE_CLASS_SUFFIX; + $$jqLite.addClass(element, prepareClassName); + } + + animationQueue.push({ + // this data is used by the postDigest code and passed into + // the driver step function + element: element, + classes: classes, + event: event, + structural: isStructural, + options: options, + beforeStart: beforeStart, + close: close + }); + + element.on('$destroy', handleDestroyedElement); + + // we only want there to be one function called within the post digest + // block. This way we can group animations for all the animations that + // were apart of the same postDigest flush call. + if (animationQueue.length > 1) return runner; + + $rootScope.$$postDigest(function() { + var animations = []; + forEach(animationQueue, function(entry) { + // the element was destroyed early on which removed the runner + // form its storage. This means we can't animate this element + // at all and it already has been closed due to destruction. + if (getRunner(entry.element)) { + animations.push(entry); + } else { + entry.close(); + } + }); + + // now any future animations will be in another postDigest + animationQueue.length = 0; + + var groupedAnimations = groupAnimations(animations); + var toBeSortedAnimations = []; + + forEach(groupedAnimations, function(animationEntry) { + toBeSortedAnimations.push({ + domNode: getDomNode(animationEntry.from ? animationEntry.from.element : animationEntry.element), + fn: function triggerAnimationStart() { + // it's important that we apply the `ng-animate` CSS class and the + // temporary classes before we do any driver invoking since these + // CSS classes may be required for proper CSS detection. + animationEntry.beforeStart(); + + var startAnimationFn, closeFn = animationEntry.close; + + // in the event that the element was removed before the digest runs or + // during the RAF sequencing then we should not trigger the animation. + var targetElement = animationEntry.anchors + ? (animationEntry.from.element || animationEntry.to.element) + : animationEntry.element; + + if (getRunner(targetElement)) { + var operation = invokeFirstDriver(animationEntry); + if (operation) { + startAnimationFn = operation.start; + } + } + + if (!startAnimationFn) { + closeFn(); + } else { + var animationRunner = startAnimationFn(); + animationRunner.done(function(status) { + closeFn(!status); + }); + updateAnimationRunners(animationEntry, animationRunner); + } + } + }); + }); + + // we need to sort each of the animations in order of parent to child + // relationships. This ensures that the child classes are applied at the + // right time. + $$rAFScheduler(sortAnimations(toBeSortedAnimations)); + }); + + return runner; + + // TODO(matsko): change to reference nodes + function getAnchorNodes(node) { + var SELECTOR = '[' + NG_ANIMATE_REF_ATTR + ']'; + var items = node.hasAttribute(NG_ANIMATE_REF_ATTR) + ? [node] + : node.querySelectorAll(SELECTOR); + var anchors = []; + forEach(items, function(node) { + var attr = node.getAttribute(NG_ANIMATE_REF_ATTR); + if (attr && attr.length) { + anchors.push(node); + } + }); + return anchors; + } + + function groupAnimations(animations) { + var preparedAnimations = []; + var refLookup = {}; + forEach(animations, function(animation, index) { + var element = animation.element; + var node = getDomNode(element); + var event = animation.event; + var enterOrMove = ['enter', 'move'].indexOf(event) >= 0; + var anchorNodes = animation.structural ? getAnchorNodes(node) : []; + + if (anchorNodes.length) { + var direction = enterOrMove ? 'to' : 'from'; + + forEach(anchorNodes, function(anchor) { + var key = anchor.getAttribute(NG_ANIMATE_REF_ATTR); + refLookup[key] = refLookup[key] || {}; + refLookup[key][direction] = { + animationID: index, + element: jqLite(anchor) + }; + }); + } else { + preparedAnimations.push(animation); + } + }); + + var usedIndicesLookup = {}; + var anchorGroups = {}; + forEach(refLookup, function(operations, key) { + var from = operations.from; + var to = operations.to; + + if (!from || !to) { + // only one of these is set therefore we can't have an + // anchor animation since all three pieces are required + var index = from ? from.animationID : to.animationID; + var indexKey = index.toString(); + if (!usedIndicesLookup[indexKey]) { + usedIndicesLookup[indexKey] = true; + preparedAnimations.push(animations[index]); + } + return; + } + + var fromAnimation = animations[from.animationID]; + var toAnimation = animations[to.animationID]; + var lookupKey = from.animationID.toString(); + if (!anchorGroups[lookupKey]) { + var group = anchorGroups[lookupKey] = { + structural: true, + beforeStart: function() { + fromAnimation.beforeStart(); + toAnimation.beforeStart(); + }, + close: function() { + fromAnimation.close(); + toAnimation.close(); + }, + classes: cssClassesIntersection(fromAnimation.classes, toAnimation.classes), + from: fromAnimation, + to: toAnimation, + anchors: [] // TODO(matsko): change to reference nodes + }; + + // the anchor animations require that the from and to elements both have at least + // one shared CSS class which effectively marries the two elements together to use + // the same animation driver and to properly sequence the anchor animation. + if (group.classes.length) { + preparedAnimations.push(group); + } else { + preparedAnimations.push(fromAnimation); + preparedAnimations.push(toAnimation); + } + } + + anchorGroups[lookupKey].anchors.push({ + 'out': from.element, 'in': to.element + }); + }); + + return preparedAnimations; + } + + function cssClassesIntersection(a,b) { + a = a.split(' '); + b = b.split(' '); + var matches = []; + + for (var i = 0; i < a.length; i++) { + var aa = a[i]; + if (aa.substring(0,3) === 'ng-') continue; + + for (var j = 0; j < b.length; j++) { + if (aa === b[j]) { + matches.push(aa); + break; + } + } + } + + return matches.join(' '); + } + + function invokeFirstDriver(animationDetails) { + // we loop in reverse order since the more general drivers (like CSS and JS) + // may attempt more elements, but custom drivers are more particular + for (var i = drivers.length - 1; i >= 0; i--) { + var driverName = drivers[i]; + if (!$injector.has(driverName)) continue; // TODO(matsko): remove this check + + var factory = $injector.get(driverName); + var driver = factory(animationDetails); + if (driver) { + return driver; + } + } + } + + function beforeStart() { + element.addClass(NG_ANIMATE_CLASSNAME); + if (tempClasses) { + $$jqLite.addClass(element, tempClasses); + } + if (prepareClassName) { + $$jqLite.removeClass(element, prepareClassName); + prepareClassName = null; + } + } + + function updateAnimationRunners(animation, newRunner) { + if (animation.from && animation.to) { + update(animation.from.element); + update(animation.to.element); + } else { + update(animation.element); + } + + function update(element) { + getRunner(element).setHost(newRunner); + } + } + + function handleDestroyedElement() { + var runner = getRunner(element); + if (runner && (event !== 'leave' || !options.$$domOperationFired)) { + runner.end(); + } + } + + function close(rejected) { // jshint ignore:line + element.off('$destroy', handleDestroyedElement); + removeRunner(element); + + applyAnimationClasses(element, options); + applyAnimationStyles(element, options); + options.domOperation(); + + if (tempClasses) { + $$jqLite.removeClass(element, tempClasses); + } + + element.removeClass(NG_ANIMATE_CLASSNAME); + runner.complete(!rejected); + } + }; + }]; +}]; + +/** + * @ngdoc directive + * @name ngAnimateSwap + * @restrict A + * @scope + * + * @description + * + * ngAnimateSwap is a animation-oriented directive that allows for the container to + * be removed and entered in whenever the associated expression changes. A + * common usecase for this directive is a rotating banner or slider component which + * contains one image being present at a time. When the active image changes + * then the old image will perform a `leave` animation and the new element + * will be inserted via an `enter` animation. + * + * @animations + * | Animation | Occurs | + * |----------------------------------|--------------------------------------| + * | {@link ng.$animate#enter enter} | when the new element is inserted to the DOM | + * | {@link ng.$animate#leave leave} | when the old element is removed from the DOM | + * + * @example + * + * + *
+ *
+ * {{ number }} + *
+ *
+ *
+ * + * angular.module('ngAnimateSwapExample', ['ngAnimate']) + * .controller('AppCtrl', ['$scope', '$interval', function($scope, $interval) { + * $scope.number = 0; + * $interval(function() { + * $scope.number++; + * }, 1000); + * + * var colors = ['red','blue','green','yellow','orange']; + * $scope.colorClass = function(number) { + * return colors[number % colors.length]; + * }; + * }]); + * + * + * .container { + * height:250px; + * width:250px; + * position:relative; + * overflow:hidden; + * border:2px solid black; + * } + * .container .cell { + * font-size:150px; + * text-align:center; + * line-height:250px; + * position:absolute; + * top:0; + * left:0; + * right:0; + * border-bottom:2px solid black; + * } + * .swap-animation.ng-enter, .swap-animation.ng-leave { + * transition:0.5s linear all; + * } + * .swap-animation.ng-enter { + * top:-250px; + * } + * .swap-animation.ng-enter-active { + * top:0px; + * } + * .swap-animation.ng-leave { + * top:0px; + * } + * .swap-animation.ng-leave-active { + * top:250px; + * } + * .red { background:red; } + * .green { background:green; } + * .blue { background:blue; } + * .yellow { background:yellow; } + * .orange { background:orange; } + * + *
+ */ +var ngAnimateSwapDirective = ['$animate', '$rootScope', function($animate, $rootScope) { + return { + restrict: 'A', + transclude: 'element', + terminal: true, + priority: 600, // we use 600 here to ensure that the directive is caught before others + link: function(scope, $element, attrs, ctrl, $transclude) { + var previousElement, previousScope; + scope.$watchCollection(attrs.ngAnimateSwap || attrs['for'], function(value) { + if (previousElement) { + $animate.leave(previousElement); + } + if (previousScope) { + previousScope.$destroy(); + previousScope = null; + } + if (value || value === 0) { + previousScope = scope.$new(); + $transclude(previousScope, function(element) { + previousElement = element; + $animate.enter(element, null, $element); + }); + } + }); + } + }; +}]; + +/* global angularAnimateModule: true, + + ngAnimateSwapDirective, + $$AnimateAsyncRunFactory, + $$rAFSchedulerFactory, + $$AnimateChildrenDirective, + $$AnimateQueueProvider, + $$AnimationProvider, + $AnimateCssProvider, + $$AnimateCssDriverProvider, + $$AnimateJsProvider, + $$AnimateJsDriverProvider, +*/ + +/** + * @ngdoc module + * @name ngAnimate + * @description + * + * The `ngAnimate` module provides support for CSS-based animations (keyframes and transitions) as well as JavaScript-based animations via + * callback hooks. Animations are not enabled by default, however, by including `ngAnimate` the animation hooks are enabled for an Angular app. + * + *
+ * + * # Usage + * Simply put, there are two ways to make use of animations when ngAnimate is used: by using **CSS** and **JavaScript**. The former works purely based + * using CSS (by using matching CSS selectors/styles) and the latter triggers animations that are registered via `module.animation()`. For + * both CSS and JS animations the sole requirement is to have a matching `CSS class` that exists both in the registered animation and within + * the HTML element that the animation will be triggered on. + * + * ## Directive Support + * The following directives are "animation aware": + * + * | Directive | Supported Animations | + * |----------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------| + * | {@link ng.directive:ngRepeat#animations ngRepeat} | enter, leave and move | + * | {@link ngRoute.directive:ngView#animations ngView} | enter and leave | + * | {@link ng.directive:ngInclude#animations ngInclude} | enter and leave | + * | {@link ng.directive:ngSwitch#animations ngSwitch} | enter and leave | + * | {@link ng.directive:ngIf#animations ngIf} | enter and leave | + * | {@link ng.directive:ngClass#animations ngClass} | add and remove (the CSS class(es) present) | + * | {@link ng.directive:ngShow#animations ngShow} & {@link ng.directive:ngHide#animations ngHide} | add and remove (the ng-hide class value) | + * | {@link ng.directive:form#animation-hooks form} & {@link ng.directive:ngModel#animation-hooks ngModel} | add and remove (dirty, pristine, valid, invalid & all other validations) | + * | {@link module:ngMessages#animations ngMessages} | add and remove (ng-active & ng-inactive) | + * | {@link module:ngMessages#animations ngMessage} | enter and leave | + * + * (More information can be found by visiting each the documentation associated with each directive.) + * + * ## CSS-based Animations + * + * CSS-based animations with ngAnimate are unique since they require no JavaScript code at all. By using a CSS class that we reference between our HTML + * and CSS code we can create an animation that will be picked up by Angular when an the underlying directive performs an operation. + * + * The example below shows how an `enter` animation can be made possible on an element using `ng-if`: + * + * ```html + *
+ * Fade me in out + *
+ * + * + * ``` + * + * Notice the CSS class **fade**? We can now create the CSS transition code that references this class: + * + * ```css + * /* The starting CSS styles for the enter animation */ + * .fade.ng-enter { + * transition:0.5s linear all; + * opacity:0; + * } + * + * /* The finishing CSS styles for the enter animation */ + * .fade.ng-enter.ng-enter-active { + * opacity:1; + * } + * ``` + * + * The key thing to remember here is that, depending on the animation event (which each of the directives above trigger depending on what's going on) two + * generated CSS classes will be applied to the element; in the example above we have `.ng-enter` and `.ng-enter-active`. For CSS transitions, the transition + * code **must** be defined within the starting CSS class (in this case `.ng-enter`). The destination class is what the transition will animate towards. + * + * If for example we wanted to create animations for `leave` and `move` (ngRepeat triggers move) then we can do so using the same CSS naming conventions: + * + * ```css + * /* now the element will fade out before it is removed from the DOM */ + * .fade.ng-leave { + * transition:0.5s linear all; + * opacity:1; + * } + * .fade.ng-leave.ng-leave-active { + * opacity:0; + * } + * ``` + * + * We can also make use of **CSS Keyframes** by referencing the keyframe animation within the starting CSS class: + * + * ```css + * /* there is no need to define anything inside of the destination + * CSS class since the keyframe will take charge of the animation */ + * .fade.ng-leave { + * animation: my_fade_animation 0.5s linear; + * -webkit-animation: my_fade_animation 0.5s linear; + * } + * + * @keyframes my_fade_animation { + * from { opacity:1; } + * to { opacity:0; } + * } + * + * @-webkit-keyframes my_fade_animation { + * from { opacity:1; } + * to { opacity:0; } + * } + * ``` + * + * Feel free also mix transitions and keyframes together as well as any other CSS classes on the same element. + * + * ### CSS Class-based Animations + * + * Class-based animations (animations that are triggered via `ngClass`, `ngShow`, `ngHide` and some other directives) have a slightly different + * naming convention. Class-based animations are basic enough that a standard transition or keyframe can be referenced on the class being added + * and removed. + * + * For example if we wanted to do a CSS animation for `ngHide` then we place an animation on the `.ng-hide` CSS class: + * + * ```html + *
+ * Show and hide me + *
+ * + * + * + * ``` + * + * All that is going on here with ngShow/ngHide behind the scenes is the `.ng-hide` class is added/removed (when the hidden state is valid). Since + * ngShow and ngHide are animation aware then we can match up a transition and ngAnimate handles the rest. + * + * In addition the addition and removal of the CSS class, ngAnimate also provides two helper methods that we can use to further decorate the animation + * with CSS styles. + * + * ```html + *
+ * Highlight this box + *
+ * + * + * + * ``` + * + * We can also make use of CSS keyframes by placing them within the CSS classes. + * + * + * ### CSS Staggering Animations + * A Staggering animation is a collection of animations that are issued with a slight delay in between each successive operation resulting in a + * curtain-like effect. The ngAnimate module (versions >=1.2) supports staggering animations and the stagger effect can be + * performed by creating a **ng-EVENT-stagger** CSS class and attaching that class to the base CSS class used for + * the animation. The style property expected within the stagger class can either be a **transition-delay** or an + * **animation-delay** property (or both if your animation contains both transitions and keyframe animations). + * + * ```css + * .my-animation.ng-enter { + * /* standard transition code */ + * transition: 1s linear all; + * opacity:0; + * } + * .my-animation.ng-enter-stagger { + * /* this will have a 100ms delay between each successive leave animation */ + * transition-delay: 0.1s; + * + * /* As of 1.4.4, this must always be set: it signals ngAnimate + * to not accidentally inherit a delay property from another CSS class */ + * transition-duration: 0s; + * } + * .my-animation.ng-enter.ng-enter-active { + * /* standard transition styles */ + * opacity:1; + * } + * ``` + * + * Staggering animations work by default in ngRepeat (so long as the CSS class is defined). Outside of ngRepeat, to use staggering animations + * on your own, they can be triggered by firing multiple calls to the same event on $animate. However, the restrictions surrounding this + * are that each of the elements must have the same CSS className value as well as the same parent element. A stagger operation + * will also be reset if one or more animation frames have passed since the multiple calls to `$animate` were fired. + * + * The following code will issue the **ng-leave-stagger** event on the element provided: + * + * ```js + * var kids = parent.children(); + * + * $animate.leave(kids[0]); //stagger index=0 + * $animate.leave(kids[1]); //stagger index=1 + * $animate.leave(kids[2]); //stagger index=2 + * $animate.leave(kids[3]); //stagger index=3 + * $animate.leave(kids[4]); //stagger index=4 + * + * window.requestAnimationFrame(function() { + * //stagger has reset itself + * $animate.leave(kids[5]); //stagger index=0 + * $animate.leave(kids[6]); //stagger index=1 + * + * $scope.$digest(); + * }); + * ``` + * + * Stagger animations are currently only supported within CSS-defined animations. + * + * ### The `ng-animate` CSS class + * + * When ngAnimate is animating an element it will apply the `ng-animate` CSS class to the element for the duration of the animation. + * This is a temporary CSS class and it will be removed once the animation is over (for both JavaScript and CSS-based animations). + * + * Therefore, animations can be applied to an element using this temporary class directly via CSS. + * + * ```css + * .zipper.ng-animate { + * transition:0.5s linear all; + * } + * .zipper.ng-enter { + * opacity:0; + * } + * .zipper.ng-enter.ng-enter-active { + * opacity:1; + * } + * .zipper.ng-leave { + * opacity:1; + * } + * .zipper.ng-leave.ng-leave-active { + * opacity:0; + * } + * ``` + * + * (Note that the `ng-animate` CSS class is reserved and it cannot be applied on an element directly since ngAnimate will always remove + * the CSS class once an animation has completed.) + * + * + * ### The `ng-[event]-prepare` class + * + * This is a special class that can be used to prevent unwanted flickering / flash of content before + * the actual animation starts. The class is added as soon as an animation is initialized, but removed + * before the actual animation starts (after waiting for a $digest). + * It is also only added for *structural* animations (`enter`, `move`, and `leave`). + * + * In practice, flickering can appear when nesting elements with structural animations such as `ngIf` + * into elements that have class-based animations such as `ngClass`. + * + * ```html + *
+ *
+ *
+ *
+ *
+ * ``` + * + * It is possible that during the `enter` animation, the `.message` div will be briefly visible before it starts animating. + * In that case, you can add styles to the CSS that make sure the element stays hidden before the animation starts: + * + * ```css + * .message.ng-enter-prepare { + * opacity: 0; + * } + * + * ``` + * + * ## JavaScript-based Animations + * + * ngAnimate also allows for animations to be consumed by JavaScript code. The approach is similar to CSS-based animations (where there is a shared + * CSS class that is referenced in our HTML code) but in addition we need to register the JavaScript animation on the module. By making use of the + * `module.animation()` module function we can register the animation. + * + * Let's see an example of a enter/leave animation using `ngRepeat`: + * + * ```html + *
+ * {{ item }} + *
+ * ``` + * + * See the **slide** CSS class? Let's use that class to define an animation that we'll structure in our module code by using `module.animation`: + * + * ```js + * myModule.animation('.slide', [function() { + * return { + * // make note that other events (like addClass/removeClass) + * // have different function input parameters + * enter: function(element, doneFn) { + * jQuery(element).fadeIn(1000, doneFn); + * + * // remember to call doneFn so that angular + * // knows that the animation has concluded + * }, + * + * move: function(element, doneFn) { + * jQuery(element).fadeIn(1000, doneFn); + * }, + * + * leave: function(element, doneFn) { + * jQuery(element).fadeOut(1000, doneFn); + * } + * } + * }]); + * ``` + * + * The nice thing about JS-based animations is that we can inject other services and make use of advanced animation libraries such as + * greensock.js and velocity.js. + * + * If our animation code class-based (meaning that something like `ngClass`, `ngHide` and `ngShow` triggers it) then we can still define + * our animations inside of the same registered animation, however, the function input arguments are a bit different: + * + * ```html + *
+ * this box is moody + *
+ * + * + * + * ``` + * + * ```js + * myModule.animation('.colorful', [function() { + * return { + * addClass: function(element, className, doneFn) { + * // do some cool animation and call the doneFn + * }, + * removeClass: function(element, className, doneFn) { + * // do some cool animation and call the doneFn + * }, + * setClass: function(element, addedClass, removedClass, doneFn) { + * // do some cool animation and call the doneFn + * } + * } + * }]); + * ``` + * + * ## CSS + JS Animations Together + * + * AngularJS 1.4 and higher has taken steps to make the amalgamation of CSS and JS animations more flexible. However, unlike earlier versions of Angular, + * defining CSS and JS animations to work off of the same CSS class will not work anymore. Therefore the example below will only result in **JS animations taking + * charge of the animation**: + * + * ```html + *
+ * Slide in and out + *
+ * ``` + * + * ```js + * myModule.animation('.slide', [function() { + * return { + * enter: function(element, doneFn) { + * jQuery(element).slideIn(1000, doneFn); + * } + * } + * }]); + * ``` + * + * ```css + * .slide.ng-enter { + * transition:0.5s linear all; + * transform:translateY(-100px); + * } + * .slide.ng-enter.ng-enter-active { + * transform:translateY(0); + * } + * ``` + * + * Does this mean that CSS and JS animations cannot be used together? Do JS-based animations always have higher priority? We can make up for the + * lack of CSS animations by using the `$animateCss` service to trigger our own tweaked-out, CSS-based animations directly from + * our own JS-based animation code: + * + * ```js + * myModule.animation('.slide', ['$animateCss', function($animateCss) { + * return { + * enter: function(element) { +* // this will trigger `.slide.ng-enter` and `.slide.ng-enter-active`. + * return $animateCss(element, { + * event: 'enter', + * structural: true + * }); + * } + * } + * }]); + * ``` + * + * The nice thing here is that we can save bandwidth by sticking to our CSS-based animation code and we don't need to rely on a 3rd-party animation framework. + * + * The `$animateCss` service is very powerful since we can feed in all kinds of extra properties that will be evaluated and fed into a CSS transition or + * keyframe animation. For example if we wanted to animate the height of an element while adding and removing classes then we can do so by providing that + * data into `$animateCss` directly: + * + * ```js + * myModule.animation('.slide', ['$animateCss', function($animateCss) { + * return { + * enter: function(element) { + * return $animateCss(element, { + * event: 'enter', + * structural: true, + * addClass: 'maroon-setting', + * from: { height:0 }, + * to: { height: 200 } + * }); + * } + * } + * }]); + * ``` + * + * Now we can fill in the rest via our transition CSS code: + * + * ```css + * /* the transition tells ngAnimate to make the animation happen */ + * .slide.ng-enter { transition:0.5s linear all; } + * + * /* this extra CSS class will be absorbed into the transition + * since the $animateCss code is adding the class */ + * .maroon-setting { background:red; } + * ``` + * + * And `$animateCss` will figure out the rest. Just make sure to have the `done()` callback fire the `doneFn` function to signal when the animation is over. + * + * To learn more about what's possible be sure to visit the {@link ngAnimate.$animateCss $animateCss service}. + * + * ## Animation Anchoring (via `ng-animate-ref`) + * + * ngAnimate in AngularJS 1.4 comes packed with the ability to cross-animate elements between + * structural areas of an application (like views) by pairing up elements using an attribute + * called `ng-animate-ref`. + * + * Let's say for example we have two views that are managed by `ng-view` and we want to show + * that there is a relationship between two components situated in within these views. By using the + * `ng-animate-ref` attribute we can identify that the two components are paired together and we + * can then attach an animation, which is triggered when the view changes. + * + * Say for example we have the following template code: + * + * ```html + * + *
+ *
+ * + * + * + * + * + * + * + * + * ``` + * + * Now, when the view changes (once the link is clicked), ngAnimate will examine the + * HTML contents to see if there is a match reference between any components in the view + * that is leaving and the view that is entering. It will scan both the view which is being + * removed (leave) and inserted (enter) to see if there are any paired DOM elements that + * contain a matching ref value. + * + * The two images match since they share the same ref value. ngAnimate will now create a + * transport element (which is a clone of the first image element) and it will then attempt + * to animate to the position of the second image element in the next view. For the animation to + * work a special CSS class called `ng-anchor` will be added to the transported element. + * + * We can now attach a transition onto the `.banner.ng-anchor` CSS class and then + * ngAnimate will handle the entire transition for us as well as the addition and removal of + * any changes of CSS classes between the elements: + * + * ```css + * .banner.ng-anchor { + * /* this animation will last for 1 second since there are + * two phases to the animation (an `in` and an `out` phase) */ + * transition:0.5s linear all; + * } + * ``` + * + * We also **must** include animations for the views that are being entered and removed + * (otherwise anchoring wouldn't be possible since the new view would be inserted right away). + * + * ```css + * .view-animation.ng-enter, .view-animation.ng-leave { + * transition:0.5s linear all; + * position:fixed; + * left:0; + * top:0; + * width:100%; + * } + * .view-animation.ng-enter { + * transform:translateX(100%); + * } + * .view-animation.ng-leave, + * .view-animation.ng-enter.ng-enter-active { + * transform:translateX(0%); + * } + * .view-animation.ng-leave.ng-leave-active { + * transform:translateX(-100%); + * } + * ``` + * + * Now we can jump back to the anchor animation. When the animation happens, there are two stages that occur: + * an `out` and an `in` stage. The `out` stage happens first and that is when the element is animated away + * from its origin. Once that animation is over then the `in` stage occurs which animates the + * element to its destination. The reason why there are two animations is to give enough time + * for the enter animation on the new element to be ready. + * + * The example above sets up a transition for both the in and out phases, but we can also target the out or + * in phases directly via `ng-anchor-out` and `ng-anchor-in`. + * + * ```css + * .banner.ng-anchor-out { + * transition: 0.5s linear all; + * + * /* the scale will be applied during the out animation, + * but will be animated away when the in animation runs */ + * transform: scale(1.2); + * } + * + * .banner.ng-anchor-in { + * transition: 1s linear all; + * } + * ``` + * + * + * + * + * ### Anchoring Demo + * + + + Home +
+
+
+
+
+ + angular.module('anchoringExample', ['ngAnimate', 'ngRoute']) + .config(['$routeProvider', function($routeProvider) { + $routeProvider.when('/', { + templateUrl: 'home.html', + controller: 'HomeController as home' + }); + $routeProvider.when('/profile/:id', { + templateUrl: 'profile.html', + controller: 'ProfileController as profile' + }); + }]) + .run(['$rootScope', function($rootScope) { + $rootScope.records = [ + { id:1, title: "Miss Beulah Roob" }, + { id:2, title: "Trent Morissette" }, + { id:3, title: "Miss Ava Pouros" }, + { id:4, title: "Rod Pouros" }, + { id:5, title: "Abdul Rice" }, + { id:6, title: "Laurie Rutherford Sr." }, + { id:7, title: "Nakia McLaughlin" }, + { id:8, title: "Jordon Blanda DVM" }, + { id:9, title: "Rhoda Hand" }, + { id:10, title: "Alexandrea Sauer" } + ]; + }]) + .controller('HomeController', [function() { + //empty + }]) + .controller('ProfileController', ['$rootScope', '$routeParams', function($rootScope, $routeParams) { + var index = parseInt($routeParams.id, 10); + var record = $rootScope.records[index - 1]; + + this.title = record.title; + this.id = record.id; + }]); + + +

Welcome to the home page

+

Please click on an element

+ + {{ record.title }} + +
+ +
+ {{ profile.title }} +
+
+ + .record { + display:block; + font-size:20px; + } + .profile { + background:black; + color:white; + font-size:100px; + } + .view-container { + position:relative; + } + .view-container > .view.ng-animate { + position:absolute; + top:0; + left:0; + width:100%; + min-height:500px; + } + .view.ng-enter, .view.ng-leave, + .record.ng-anchor { + transition:0.5s linear all; + } + .view.ng-enter { + transform:translateX(100%); + } + .view.ng-enter.ng-enter-active, .view.ng-leave { + transform:translateX(0%); + } + .view.ng-leave.ng-leave-active { + transform:translateX(-100%); + } + .record.ng-anchor-out { + background:red; + } + +
+ * + * ### How is the element transported? + * + * When an anchor animation occurs, ngAnimate will clone the starting element and position it exactly where the starting + * element is located on screen via absolute positioning. The cloned element will be placed inside of the root element + * of the application (where ng-app was defined) and all of the CSS classes of the starting element will be applied. The + * element will then animate into the `out` and `in` animations and will eventually reach the coordinates and match + * the dimensions of the destination element. During the entire animation a CSS class of `.ng-animate-shim` will be applied + * to both the starting and destination elements in order to hide them from being visible (the CSS styling for the class + * is: `visibility:hidden`). Once the anchor reaches its destination then it will be removed and the destination element + * will become visible since the shim class will be removed. + * + * ### How is the morphing handled? + * + * CSS Anchoring relies on transitions and keyframes and the internal code is intelligent enough to figure out + * what CSS classes differ between the starting element and the destination element. These different CSS classes + * will be added/removed on the anchor element and a transition will be applied (the transition that is provided + * in the anchor class). Long story short, ngAnimate will figure out what classes to add and remove which will + * make the transition of the element as smooth and automatic as possible. Be sure to use simple CSS classes that + * do not rely on DOM nesting structure so that the anchor element appears the same as the starting element (since + * the cloned element is placed inside of root element which is likely close to the body element). + * + * Note that if the root element is on the `` element then the cloned node will be placed inside of body. + * + * + * ## Using $animate in your directive code + * + * So far we've explored how to feed in animations into an Angular application, but how do we trigger animations within our own directives in our application? + * By injecting the `$animate` service into our directive code, we can trigger structural and class-based hooks which can then be consumed by animations. Let's + * imagine we have a greeting box that shows and hides itself when the data changes + * + * ```html + * Hi there + * ``` + * + * ```js + * ngModule.directive('greetingBox', ['$animate', function($animate) { + * return function(scope, element, attrs) { + * attrs.$observe('active', function(value) { + * value ? $animate.addClass(element, 'on') : $animate.removeClass(element, 'on'); + * }); + * }); + * }]); + * ``` + * + * Now the `on` CSS class is added and removed on the greeting box component. Now if we add a CSS class on top of the greeting box element + * in our HTML code then we can trigger a CSS or JS animation to happen. + * + * ```css + * /* normally we would create a CSS class to reference on the element */ + * greeting-box.on { transition:0.5s linear all; background:green; color:white; } + * ``` + * + * The `$animate` service contains a variety of other methods like `enter`, `leave`, `animate` and `setClass`. To learn more about what's + * possible be sure to visit the {@link ng.$animate $animate service API page}. + * + * + * ## Callbacks and Promises + * + * When `$animate` is called it returns a promise that can be used to capture when the animation has ended. Therefore if we were to trigger + * an animation (within our directive code) then we can continue performing directive and scope related activities after the animation has + * ended by chaining onto the returned promise that animation method returns. + * + * ```js + * // somewhere within the depths of the directive + * $animate.enter(element, parent).then(function() { + * //the animation has completed + * }); + * ``` + * + * (Note that earlier versions of Angular prior to v1.4 required the promise code to be wrapped using `$scope.$apply(...)`. This is not the case + * anymore.) + * + * In addition to the animation promise, we can also make use of animation-related callbacks within our directives and controller code by registering + * an event listener using the `$animate` service. Let's say for example that an animation was triggered on our view + * routing controller to hook into that: + * + * ```js + * ngModule.controller('HomePageController', ['$animate', function($animate) { + * $animate.on('enter', ngViewElement, function(element) { + * // the animation for this route has completed + * }]); + * }]) + * ``` + * + * (Note that you will need to trigger a digest within the callback to get angular to notice any scope-related changes.) + */ + +/** + * @ngdoc service + * @name $animate + * @kind object + * + * @description + * The ngAnimate `$animate` service documentation is the same for the core `$animate` service. + * + * Click here {@link ng.$animate to learn more about animations with `$animate`}. + */ +angular.module('ngAnimate', []) + .directive('ngAnimateSwap', ngAnimateSwapDirective) + + .directive('ngAnimateChildren', $$AnimateChildrenDirective) + .factory('$$rAFScheduler', $$rAFSchedulerFactory) + + .provider('$$animateQueue', $$AnimateQueueProvider) + .provider('$$animation', $$AnimationProvider) + + .provider('$animateCss', $AnimateCssProvider) + .provider('$$animateCssDriver', $$AnimateCssDriverProvider) + + .provider('$$animateJs', $$AnimateJsProvider) + .provider('$$animateJsDriver', $$AnimateJsDriverProvider); + + +})(window, window.angular); diff --git a/ntt_tool/static/js/libs/angular/angular-sanitize.js b/ntt_tool/static/js/libs/angular/angular-sanitize.js new file mode 100644 index 0000000..6561868 --- /dev/null +++ b/ntt_tool/static/js/libs/angular/angular-sanitize.js @@ -0,0 +1,717 @@ +/** + * @license AngularJS v1.5.5 + * (c) 2010-2016 Google, Inc. http://angularjs.org + * License: MIT + */ +(function(window, angular) {'use strict'; + +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * Any commits to this file should be reviewed with security in mind. * + * Changes to this file can potentially create security vulnerabilities. * + * An approval from 2 Core members with history of modifying * + * this file is required. * + * * + * Does the change somehow allow for arbitrary javascript to be executed? * + * Or allows for someone to change the prototype of built-in objects? * + * Or gives undesired access to variables likes document or window? * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +var $sanitizeMinErr = angular.$$minErr('$sanitize'); + +/** + * @ngdoc module + * @name ngSanitize + * @description + * + * # ngSanitize + * + * The `ngSanitize` module provides functionality to sanitize HTML. + * + * + *
+ * + * See {@link ngSanitize.$sanitize `$sanitize`} for usage. + */ + +/** + * @ngdoc service + * @name $sanitize + * @kind function + * + * @description + * Sanitizes an html string by stripping all potentially dangerous tokens. + * + * The input is sanitized by parsing the HTML into tokens. All safe tokens (from a whitelist) are + * then serialized back to properly escaped html string. This means that no unsafe input can make + * it into the returned string. + * + * The whitelist for URL sanitization of attribute values is configured using the functions + * `aHrefSanitizationWhitelist` and `imgSrcSanitizationWhitelist` of {@link ng.$compileProvider + * `$compileProvider`}. + * + * The input may also contain SVG markup if this is enabled via {@link $sanitizeProvider}. + * + * @param {string} html HTML input. + * @returns {string} Sanitized HTML. + * + * @example + + + +
+ Snippet: + + + + + + + + + + + + + + + + + + + + + + + + + +
DirectiveHowSourceRendered
ng-bind-htmlAutomatically uses $sanitize
<div ng-bind-html="snippet">
</div>
ng-bind-htmlBypass $sanitize by explicitly trusting the dangerous value +
<div ng-bind-html="deliberatelyTrustDangerousSnippet()">
+</div>
+
ng-bindAutomatically escapes
<div ng-bind="snippet">
</div>
+
+
+ + it('should sanitize the html snippet by default', function() { + expect(element(by.css('#bind-html-with-sanitize div')).getInnerHtml()). + toBe('

an html\nclick here\nsnippet

'); + }); + + it('should inline raw snippet if bound to a trusted value', function() { + expect(element(by.css('#bind-html-with-trust div')).getInnerHtml()). + toBe("

an html\n" + + "click here\n" + + "snippet

"); + }); + + it('should escape snippet without any filter', function() { + expect(element(by.css('#bind-default div')).getInnerHtml()). + toBe("<p style=\"color:blue\">an html\n" + + "<em onmouseover=\"this.textContent='PWN3D!'\">click here</em>\n" + + "snippet</p>"); + }); + + it('should update', function() { + element(by.model('snippet')).clear(); + element(by.model('snippet')).sendKeys('new text'); + expect(element(by.css('#bind-html-with-sanitize div')).getInnerHtml()). + toBe('new text'); + expect(element(by.css('#bind-html-with-trust div')).getInnerHtml()).toBe( + 'new text'); + expect(element(by.css('#bind-default div')).getInnerHtml()).toBe( + "new <b onclick=\"alert(1)\">text</b>"); + }); +
+
+ */ + + +/** + * @ngdoc provider + * @name $sanitizeProvider + * + * @description + * Creates and configures {@link $sanitize} instance. + */ +function $SanitizeProvider() { + var svgEnabled = false; + + this.$get = ['$$sanitizeUri', function($$sanitizeUri) { + if (svgEnabled) { + angular.extend(validElements, svgElements); + } + return function(html) { + var buf = []; + htmlParser(html, htmlSanitizeWriter(buf, function(uri, isImage) { + return !/^unsafe:/.test($$sanitizeUri(uri, isImage)); + })); + return buf.join(''); + }; + }]; + + + /** + * @ngdoc method + * @name $sanitizeProvider#enableSvg + * @kind function + * + * @description + * Enables a subset of svg to be supported by the sanitizer. + * + *
+ *

By enabling this setting without taking other precautions, you might expose your + * application to click-hijacking attacks. In these attacks, sanitized svg elements could be positioned + * outside of the containing element and be rendered over other elements on the page (e.g. a login + * link). Such behavior can then result in phishing incidents.

+ * + *

To protect against these, explicitly setup `overflow: hidden` css rule for all potential svg + * tags within the sanitized content:

+ * + *
+ * + *

+   *   .rootOfTheIncludedContent svg {
+   *     overflow: hidden !important;
+   *   }
+   *   
+ *
+ * + * @param {boolean=} regexp New regexp to whitelist urls with. + * @returns {boolean|ng.$sanitizeProvider} Returns the currently configured value if called + * without an argument or self for chaining otherwise. + */ + this.enableSvg = function(enableSvg) { + if (angular.isDefined(enableSvg)) { + svgEnabled = enableSvg; + return this; + } else { + return svgEnabled; + } + }; +} + +function sanitizeText(chars) { + var buf = []; + var writer = htmlSanitizeWriter(buf, angular.noop); + writer.chars(chars); + return buf.join(''); +} + + +// Regular Expressions for parsing tags and attributes +var SURROGATE_PAIR_REGEXP = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g, + // Match everything outside of normal chars and " (quote character) + NON_ALPHANUMERIC_REGEXP = /([^\#-~ |!])/g; + + +// Good source of info about elements and attributes +// http://dev.w3.org/html5/spec/Overview.html#semantics +// http://simon.html5.org/html-elements + +// Safe Void Elements - HTML5 +// http://dev.w3.org/html5/spec/Overview.html#void-elements +var voidElements = toMap("area,br,col,hr,img,wbr"); + +// Elements that you can, intentionally, leave open (and which close themselves) +// http://dev.w3.org/html5/spec/Overview.html#optional-tags +var optionalEndTagBlockElements = toMap("colgroup,dd,dt,li,p,tbody,td,tfoot,th,thead,tr"), + optionalEndTagInlineElements = toMap("rp,rt"), + optionalEndTagElements = angular.extend({}, + optionalEndTagInlineElements, + optionalEndTagBlockElements); + +// Safe Block Elements - HTML5 +var blockElements = angular.extend({}, optionalEndTagBlockElements, toMap("address,article," + + "aside,blockquote,caption,center,del,dir,div,dl,figure,figcaption,footer,h1,h2,h3,h4,h5," + + "h6,header,hgroup,hr,ins,map,menu,nav,ol,pre,section,table,ul")); + +// Inline Elements - HTML5 +var inlineElements = angular.extend({}, optionalEndTagInlineElements, toMap("a,abbr,acronym,b," + + "bdi,bdo,big,br,cite,code,del,dfn,em,font,i,img,ins,kbd,label,map,mark,q,ruby,rp,rt,s," + + "samp,small,span,strike,strong,sub,sup,time,tt,u,var")); + +// SVG Elements +// https://wiki.whatwg.org/wiki/Sanitization_rules#svg_Elements +// Note: the elements animate,animateColor,animateMotion,animateTransform,set are intentionally omitted. +// They can potentially allow for arbitrary javascript to be executed. See #11290 +var svgElements = toMap("circle,defs,desc,ellipse,font-face,font-face-name,font-face-src,g,glyph," + + "hkern,image,linearGradient,line,marker,metadata,missing-glyph,mpath,path,polygon,polyline," + + "radialGradient,rect,stop,svg,switch,text,title,tspan"); + +// Blocked Elements (will be stripped) +var blockedElements = toMap("script,style"); + +var validElements = angular.extend({}, + voidElements, + blockElements, + inlineElements, + optionalEndTagElements); + +//Attributes that have href and hence need to be sanitized +var uriAttrs = toMap("background,cite,href,longdesc,src,xlink:href"); + +var htmlAttrs = toMap('abbr,align,alt,axis,bgcolor,border,cellpadding,cellspacing,class,clear,' + + 'color,cols,colspan,compact,coords,dir,face,headers,height,hreflang,hspace,' + + 'ismap,lang,language,nohref,nowrap,rel,rev,rows,rowspan,rules,' + + 'scope,scrolling,shape,size,span,start,summary,tabindex,target,title,type,' + + 'valign,value,vspace,width'); + +// SVG attributes (without "id" and "name" attributes) +// https://wiki.whatwg.org/wiki/Sanitization_rules#svg_Attributes +var svgAttrs = toMap('accent-height,accumulate,additive,alphabetic,arabic-form,ascent,' + + 'baseProfile,bbox,begin,by,calcMode,cap-height,class,color,color-rendering,content,' + + 'cx,cy,d,dx,dy,descent,display,dur,end,fill,fill-rule,font-family,font-size,font-stretch,' + + 'font-style,font-variant,font-weight,from,fx,fy,g1,g2,glyph-name,gradientUnits,hanging,' + + 'height,horiz-adv-x,horiz-origin-x,ideographic,k,keyPoints,keySplines,keyTimes,lang,' + + 'marker-end,marker-mid,marker-start,markerHeight,markerUnits,markerWidth,mathematical,' + + 'max,min,offset,opacity,orient,origin,overline-position,overline-thickness,panose-1,' + + 'path,pathLength,points,preserveAspectRatio,r,refX,refY,repeatCount,repeatDur,' + + 'requiredExtensions,requiredFeatures,restart,rotate,rx,ry,slope,stemh,stemv,stop-color,' + + 'stop-opacity,strikethrough-position,strikethrough-thickness,stroke,stroke-dasharray,' + + 'stroke-dashoffset,stroke-linecap,stroke-linejoin,stroke-miterlimit,stroke-opacity,' + + 'stroke-width,systemLanguage,target,text-anchor,to,transform,type,u1,u2,underline-position,' + + 'underline-thickness,unicode,unicode-range,units-per-em,values,version,viewBox,visibility,' + + 'width,widths,x,x-height,x1,x2,xlink:actuate,xlink:arcrole,xlink:role,xlink:show,xlink:title,' + + 'xlink:type,xml:base,xml:lang,xml:space,xmlns,xmlns:xlink,y,y1,y2,zoomAndPan', true); + +var validAttrs = angular.extend({}, + uriAttrs, + svgAttrs, + htmlAttrs); + +function toMap(str, lowercaseKeys) { + var obj = {}, items = str.split(','), i; + for (i = 0; i < items.length; i++) { + obj[lowercaseKeys ? angular.lowercase(items[i]) : items[i]] = true; + } + return obj; +} + +var inertBodyElement; +(function(window) { + var doc; + if (window.document && window.document.implementation) { + doc = window.document.implementation.createHTMLDocument("inert"); + } else { + throw $sanitizeMinErr('noinert', "Can't create an inert html document"); + } + var docElement = doc.documentElement || doc.getDocumentElement(); + var bodyElements = docElement.getElementsByTagName('body'); + + // usually there should be only one body element in the document, but IE doesn't have any, so we need to create one + if (bodyElements.length === 1) { + inertBodyElement = bodyElements[0]; + } else { + var html = doc.createElement('html'); + inertBodyElement = doc.createElement('body'); + html.appendChild(inertBodyElement); + doc.appendChild(html); + } +})(window); + +/** + * @example + * htmlParser(htmlString, { + * start: function(tag, attrs) {}, + * end: function(tag) {}, + * chars: function(text) {}, + * comment: function(text) {} + * }); + * + * @param {string} html string + * @param {object} handler + */ +function htmlParser(html, handler) { + if (html === null || html === undefined) { + html = ''; + } else if (typeof html !== 'string') { + html = '' + html; + } + inertBodyElement.innerHTML = html; + + //mXSS protection + var mXSSAttempts = 5; + do { + if (mXSSAttempts === 0) { + throw $sanitizeMinErr('uinput', "Failed to sanitize html because the input is unstable"); + } + mXSSAttempts--; + + // strip custom-namespaced attributes on IE<=11 + if (window.document.documentMode) { + stripCustomNsAttrs(inertBodyElement); + } + html = inertBodyElement.innerHTML; //trigger mXSS + inertBodyElement.innerHTML = html; + } while (html !== inertBodyElement.innerHTML); + + var node = inertBodyElement.firstChild; + while (node) { + switch (node.nodeType) { + case 1: // ELEMENT_NODE + handler.start(node.nodeName.toLowerCase(), attrToMap(node.attributes)); + break; + case 3: // TEXT NODE + handler.chars(node.textContent); + break; + } + + var nextNode; + if (!(nextNode = node.firstChild)) { + if (node.nodeType == 1) { + handler.end(node.nodeName.toLowerCase()); + } + nextNode = node.nextSibling; + if (!nextNode) { + while (nextNode == null) { + node = node.parentNode; + if (node === inertBodyElement) break; + nextNode = node.nextSibling; + if (node.nodeType == 1) { + handler.end(node.nodeName.toLowerCase()); + } + } + } + } + node = nextNode; + } + + while (node = inertBodyElement.firstChild) { + inertBodyElement.removeChild(node); + } +} + +function attrToMap(attrs) { + var map = {}; + for (var i = 0, ii = attrs.length; i < ii; i++) { + var attr = attrs[i]; + map[attr.name] = attr.value; + } + return map; +} + + +/** + * Escapes all potentially dangerous characters, so that the + * resulting string can be safely inserted into attribute or + * element text. + * @param value + * @returns {string} escaped text + */ +function encodeEntities(value) { + return value. + replace(/&/g, '&'). + replace(SURROGATE_PAIR_REGEXP, function(value) { + var hi = value.charCodeAt(0); + var low = value.charCodeAt(1); + return '&#' + (((hi - 0xD800) * 0x400) + (low - 0xDC00) + 0x10000) + ';'; + }). + replace(NON_ALPHANUMERIC_REGEXP, function(value) { + return '&#' + value.charCodeAt(0) + ';'; + }). + replace(//g, '>'); +} + +/** + * create an HTML/XML writer which writes to buffer + * @param {Array} buf use buf.join('') to get out sanitized html string + * @returns {object} in the form of { + * start: function(tag, attrs) {}, + * end: function(tag) {}, + * chars: function(text) {}, + * comment: function(text) {} + * } + */ +function htmlSanitizeWriter(buf, uriValidator) { + var ignoreCurrentElement = false; + var out = angular.bind(buf, buf.push); + return { + start: function(tag, attrs) { + tag = angular.lowercase(tag); + if (!ignoreCurrentElement && blockedElements[tag]) { + ignoreCurrentElement = tag; + } + if (!ignoreCurrentElement && validElements[tag] === true) { + out('<'); + out(tag); + angular.forEach(attrs, function(value, key) { + var lkey=angular.lowercase(key); + var isImage = (tag === 'img' && lkey === 'src') || (lkey === 'background'); + if (validAttrs[lkey] === true && + (uriAttrs[lkey] !== true || uriValidator(value, isImage))) { + out(' '); + out(key); + out('="'); + out(encodeEntities(value)); + out('"'); + } + }); + out('>'); + } + }, + end: function(tag) { + tag = angular.lowercase(tag); + if (!ignoreCurrentElement && validElements[tag] === true && voidElements[tag] !== true) { + out(''); + } + if (tag == ignoreCurrentElement) { + ignoreCurrentElement = false; + } + }, + chars: function(chars) { + if (!ignoreCurrentElement) { + out(encodeEntities(chars)); + } + } + }; +} + + +/** + * When IE9-11 comes across an unknown namespaced attribute e.g. 'xlink:foo' it adds 'xmlns:ns1' attribute to declare + * ns1 namespace and prefixes the attribute with 'ns1' (e.g. 'ns1:xlink:foo'). This is undesirable since we don't want + * to allow any of these custom attributes. This method strips them all. + * + * @param node Root element to process + */ +function stripCustomNsAttrs(node) { + if (node.nodeType === window.Node.ELEMENT_NODE) { + var attrs = node.attributes; + for (var i = 0, l = attrs.length; i < l; i++) { + var attrNode = attrs[i]; + var attrName = attrNode.name.toLowerCase(); + if (attrName === 'xmlns:ns1' || attrName.indexOf('ns1:') === 0) { + node.removeAttributeNode(attrNode); + i--; + l--; + } + } + } + + var nextNode = node.firstChild; + if (nextNode) { + stripCustomNsAttrs(nextNode); + } + + nextNode = node.nextSibling; + if (nextNode) { + stripCustomNsAttrs(nextNode); + } +} + + + +// define ngSanitize module and register $sanitize service +angular.module('ngSanitize', []).provider('$sanitize', $SanitizeProvider); + +/* global sanitizeText: false */ + +/** + * @ngdoc filter + * @name linky + * @kind function + * + * @description + * Finds links in text input and turns them into html links. Supports `http/https/ftp/mailto` and + * plain email address links. + * + * Requires the {@link ngSanitize `ngSanitize`} module to be installed. + * + * @param {string} text Input text. + * @param {string} target Window (`_blank|_self|_parent|_top`) or named frame to open links in. + * @param {object|function(url)} [attributes] Add custom attributes to the link element. + * + * Can be one of: + * + * - `object`: A map of attributes + * - `function`: Takes the url as a parameter and returns a map of attributes + * + * If the map of attributes contains a value for `target`, it overrides the value of + * the target parameter. + * + * + * @returns {string} Html-linkified and {@link $sanitize sanitized} text. + * + * @usage + + * + * @example + + +
+ Snippet: + + + + + + + + + + + + + + + + + + + + + + + + + + +
FilterSourceRendered
linky filter +
<div ng-bind-html="snippet | linky">
</div>
+
+
+
linky target +
<div ng-bind-html="snippetWithSingleURL | linky:'_blank'">
</div>
+
+
+
linky custom attributes +
<div ng-bind-html="snippetWithSingleURL | linky:'_self':{rel: 'nofollow'}">
</div>
+
+
+
no filter
<div ng-bind="snippet">
</div>
+ + + angular.module('linkyExample', ['ngSanitize']) + .controller('ExampleController', ['$scope', function($scope) { + $scope.snippet = + 'Pretty text with some links:\n'+ + 'http://angularjs.org/,\n'+ + 'mailto:us@somewhere.org,\n'+ + 'another@somewhere.org,\n'+ + 'and one more: ftp://127.0.0.1/.'; + $scope.snippetWithSingleURL = 'http://angularjs.org/'; + }]); + + + it('should linkify the snippet with urls', function() { + expect(element(by.id('linky-filter')).element(by.binding('snippet | linky')).getText()). + toBe('Pretty text with some links: http://angularjs.org/, us@somewhere.org, ' + + 'another@somewhere.org, and one more: ftp://127.0.0.1/.'); + expect(element.all(by.css('#linky-filter a')).count()).toEqual(4); + }); + + it('should not linkify snippet without the linky filter', function() { + expect(element(by.id('escaped-html')).element(by.binding('snippet')).getText()). + toBe('Pretty text with some links: http://angularjs.org/, mailto:us@somewhere.org, ' + + 'another@somewhere.org, and one more: ftp://127.0.0.1/.'); + expect(element.all(by.css('#escaped-html a')).count()).toEqual(0); + }); + + it('should update', function() { + element(by.model('snippet')).clear(); + element(by.model('snippet')).sendKeys('new http://link.'); + expect(element(by.id('linky-filter')).element(by.binding('snippet | linky')).getText()). + toBe('new http://link.'); + expect(element.all(by.css('#linky-filter a')).count()).toEqual(1); + expect(element(by.id('escaped-html')).element(by.binding('snippet')).getText()) + .toBe('new http://link.'); + }); + + it('should work with the target property', function() { + expect(element(by.id('linky-target')). + element(by.binding("snippetWithSingleURL | linky:'_blank'")).getText()). + toBe('http://angularjs.org/'); + expect(element(by.css('#linky-target a')).getAttribute('target')).toEqual('_blank'); + }); + + it('should optionally add custom attributes', function() { + expect(element(by.id('linky-custom-attributes')). + element(by.binding("snippetWithSingleURL | linky:'_self':{rel: 'nofollow'}")).getText()). + toBe('http://angularjs.org/'); + expect(element(by.css('#linky-custom-attributes a')).getAttribute('rel')).toEqual('nofollow'); + }); + + + */ +angular.module('ngSanitize').filter('linky', ['$sanitize', function($sanitize) { + var LINKY_URL_REGEXP = + /((ftp|https?):\/\/|(www\.)|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s.;,(){}<>"\u201d\u2019]/i, + MAILTO_REGEXP = /^mailto:/i; + + var linkyMinErr = angular.$$minErr('linky'); + var isString = angular.isString; + + return function(text, target, attributes) { + if (text == null || text === '') return text; + if (!isString(text)) throw linkyMinErr('notstring', 'Expected string but received: {0}', text); + + var match; + var raw = text; + var html = []; + var url; + var i; + while ((match = raw.match(LINKY_URL_REGEXP))) { + // We can not end in these as they are sometimes found at the end of the sentence + url = match[0]; + // if we did not match ftp/http/www/mailto then assume mailto + if (!match[2] && !match[4]) { + url = (match[3] ? 'http://' : 'mailto:') + url; + } + i = match.index; + addText(raw.substr(0, i)); + addLink(url, match[0].replace(MAILTO_REGEXP, '')); + raw = raw.substring(i + match[0].length); + } + addText(raw); + return $sanitize(html.join('')); + + function addText(text) { + if (!text) { + return; + } + html.push(sanitizeText(text)); + } + + function addLink(url, text) { + var key; + html.push(''); + addText(text); + html.push(''); + } + }; +}]); + + +})(window, window.angular); diff --git a/ntt_tool/static/js/libs/angular/checklist-model.js b/ntt_tool/static/js/libs/angular/checklist-model.js new file mode 100644 index 0000000..391d831 --- /dev/null +++ b/ntt_tool/static/js/libs/angular/checklist-model.js @@ -0,0 +1,148 @@ +/** + * Checklist-model + * AngularJS directive for list of checkboxes + * https://github.com/vitalets/checklist-model + * License: MIT http://opensource.org/licenses/MIT + */ + +angular.module('checklist-model', []) +.directive('checklistModel', ['$parse', '$compile', function($parse, $compile) { + // contains + function contains(arr, item, comparator) { + if (angular.isArray(arr)) { + for (var i = arr.length; i--;) { + if (comparator(arr[i], item)) { + return true; + } + } + } + return false; + } + + // add + function add(arr, item, comparator) { + arr = angular.isArray(arr) ? arr : []; + if(!contains(arr, item, comparator)) { + arr.push(item); + } + return arr; + } + + // remove + function remove(arr, item, comparator) { + if (angular.isArray(arr)) { + for (var i = arr.length; i--;) { + if (comparator(arr[i], item)) { + arr.splice(i, 1); + break; + } + } + } + return arr; + } + + // http://stackoverflow.com/a/19228302/1458162 + function postLinkFn(scope, elem, attrs) { + // exclude recursion, but still keep the model + var checklistModel = attrs.checklistModel; + attrs.$set("checklistModel", null); + // compile with `ng-model` pointing to `checked` + $compile(elem)(scope); + attrs.$set("checklistModel", checklistModel); + + // getter / setter for original model + var getter = $parse(checklistModel); + var setter = getter.assign; + var checklistChange = $parse(attrs.checklistChange); + var checklistBeforeChange = $parse(attrs.checklistBeforeChange); + + // value added to list + var value = attrs.checklistValue ? $parse(attrs.checklistValue)(scope.$parent) : attrs.value; + + + var comparator = angular.equals; + + if (attrs.hasOwnProperty('checklistComparator')){ + if (attrs.checklistComparator[0] == '.') { + var comparatorExpression = attrs.checklistComparator.substring(1); + comparator = function (a, b) { + return a[comparatorExpression] === b[comparatorExpression]; + }; + + } else { + comparator = $parse(attrs.checklistComparator)(scope.$parent); + } + } + + // watch UI checked change + scope.$watch(attrs.ngModel, function(newValue, oldValue) { + if (newValue === oldValue) { + return; + } + + if (checklistBeforeChange && (checklistBeforeChange(scope) === false)) { + scope[attrs.ngModel] = contains(getter(scope.$parent), value, comparator); + return; + } + + setValueInChecklistModel(value, newValue); + + if (checklistChange) { + checklistChange(scope); + } + }); + + function setValueInChecklistModel(value, checked) { + var current = getter(scope.$parent); + if (angular.isFunction(setter)) { + if (checked === true) { + setter(scope.$parent, add(current, value, comparator)); + } else { + setter(scope.$parent, remove(current, value, comparator)); + } + } + + } + + // declare one function to be used for both $watch functions + function setChecked(newArr, oldArr) { + if (checklistBeforeChange && (checklistBeforeChange(scope) === false)) { + setValueInChecklistModel(value, scope[attrs.ngModel]); + return; + } + scope[attrs.ngModel] = contains(newArr, value, comparator); + } + + // watch original model change + // use the faster $watchCollection method if it's available + if (angular.isFunction(scope.$parent.$watchCollection)) { + scope.$parent.$watchCollection(checklistModel, setChecked); + } else { + scope.$parent.$watch(checklistModel, setChecked, true); + } + } + + return { + restrict: 'A', + priority: 1000, + terminal: true, + scope: true, + compile: function(tElement, tAttrs) { + if ((tElement[0].tagName !== 'INPUT' || tAttrs.type !== 'checkbox') && (tElement[0].tagName !== 'MD-CHECKBOX') && (!tAttrs.btnCheckbox)) { + throw 'checklist-model should be applied to `input[type="checkbox"]` or `md-checkbox`.'; + } + + if (!tAttrs.checklistValue && !tAttrs.value) { + throw 'You should provide `value` or `checklist-value`.'; + } + + // by default ngModel is 'checked', so we set it if not specified + if (!tAttrs.ngModel) { + // local scope var storing individual checkbox model + tAttrs.$set("ngModel", "checked"); + } + + return postLinkFn; + } + }; +}]); diff --git a/ntt_tool/static/js/libs/angular/ngtoast/ngToast.min.js b/ntt_tool/static/js/libs/angular/ngtoast/ngToast.min.js new file mode 100644 index 0000000..ecd44bf --- /dev/null +++ b/ntt_tool/static/js/libs/angular/ngtoast/ngToast.min.js @@ -0,0 +1,7 @@ +/*! + * ngToast v2.0.0 (http://tameraydin.github.io/ngToast) + * Copyright 2016 Tamer Aydin (http://tamerayd.in) + * Licensed under MIT (http://tameraydin.mit-license.org/) + */ + +!function(a,b,c){"use strict";b.module("ngToast.provider",[]).provider("ngToast",[function(){function a(a){for(var d=Math.floor(1e3*Math.random());c.indexOf(d)>-1;)d=Math.floor(1e3*Math.random());this.id=d,this.count=0,this.animation=e.animation,this.className=e.className,this.additionalClasses=e.additionalClasses,this.dismissOnTimeout=e.dismissOnTimeout,this.timeout=e.timeout,this.dismissButton=e.dismissButton,this.dismissButtonHtml=e.dismissButtonHtml,this.dismissOnClick=e.dismissOnClick,this.onDismiss=e.onDismiss,this.compileContent=e.compileContent,b.extend(this,a)}var c=[],d=[],e={animation:!1,className:"success",additionalClasses:null,dismissOnTimeout:!0,timeout:4e3,dismissButton:!1,dismissButtonHtml:"×",dismissOnClick:!0,onDismiss:null,compileContent:!1,combineDuplications:!1,horizontalPosition:"right",verticalPosition:"top",maxNumber:0,newestOnTop:!0};this.configure=function(a){b.extend(e,a)},this.$get=[function(){var b=function(a,b){return b="object"==typeof b?b:{content:b},b.className=a,this.create(b)};return{settings:e,messages:c,dismiss:function(a){if(a){for(var b=c.length-1;b>=0;b--)if(c[b].id===a)return c.splice(b,1),void d.splice(d.indexOf(a),1)}else{for(;c.length>0;)c.pop();d=[]}},create:function(b){if(b="object"==typeof b?b:{content:b},e.combineDuplications)for(var f=d.length-1;f>=0;f--){var g=c[f],h=b.className||"success";if(g.content===b.content&&g.className===h)return void c[f].count++}e.maxNumber>0&&d.length>=e.maxNumber&&this.dismiss(d[0]);var i=new a(b);return c[e.newestOnTop?"unshift":"push"](i),d.push(i.id),i.id},success:function(a){return b.call(this,"success",a)},info:function(a){return b.call(this,"info",a)},warning:function(a){return b.call(this,"warning",a)},danger:function(a){return b.call(this,"danger",a)}}}]}])}(window,window.angular),function(a,b){"use strict";b.module("ngToast.directives",["ngToast.provider"]).run(["$templateCache",function(a){a.put("ngToast/toast.html",'
'),a.put("ngToast/toastMessage.html",'
  • {{count + 1}}
  • ')}]).directive("toast",["ngToast","$templateCache","$log",function(a,b,c){return{replace:!0,restrict:"EA",templateUrl:"ngToast/toast.html",compile:function(d,e){if(e.template){var f=b.get(e.template);f?d.replaceWith(f):c.warn("ngToast: Provided template could not be loaded. Please be sure that it is populated before the element is represented.")}return function(b){b.hPos=a.settings.horizontalPosition,b.vPos=a.settings.verticalPosition,b.animation=a.settings.animation,b.messages=a.messages}}}}]).directive("toastMessage",["$timeout","$compile","ngToast",function(a,b,c){return{replace:!0,transclude:!0,restrict:"EA",scope:{message:"=",count:"="},controller:["$scope","ngToast",function(a,b){a.dismiss=function(){b.dismiss(a.message.id)}}],templateUrl:"ngToast/toastMessage.html",link:function(d,e,f,g,h){e.attr("data-message-id",d.message.id);var i,j=d.message.compileContent;if(d.cancelTimeout=function(){a.cancel(i)},d.startTimeout=function(){d.message.dismissOnTimeout&&(i=a(function(){c.dismiss(d.message.id)},d.message.timeout))},d.onMouseEnter=function(){d.cancelTimeout()},d.onMouseLeave=function(){d.startTimeout()},j){var k;h(d,function(a){k=a,e.children().append(k)}),a(function(){b(k.contents())("boolean"==typeof j?d.$parent:j,function(a){k.replaceWith(a)})},0)}d.startTimeout(),d.message.dismissOnClick&&e.bind("click",function(){c.dismiss(d.message.id),d.$apply()}),d.message.onDismiss&&d.$on("$destroy",d.message.onDismiss.bind(d.message))}}}])}(window,window.angular),function(a,b){"use strict";b.module("ngToast",["ngSanitize","ngToast.directives","ngToast.provider"])}(window,window.angular); \ No newline at end of file diff --git a/ntt_tool/static/js/libs/angular/ui-bootstrap-tpls-1.2.4.min.js b/ntt_tool/static/js/libs/angular/ui-bootstrap-tpls-1.2.4.min.js new file mode 100644 index 0000000..0eab296 --- /dev/null +++ b/ntt_tool/static/js/libs/angular/ui-bootstrap-tpls-1.2.4.min.js @@ -0,0 +1,10 @@ +/* + * angular-ui-bootstrap + * http://angular-ui.github.io/bootstrap/ + + * Version: 1.2.4 - 2016-03-06 + * License: MIT + */angular.module("ui.bootstrap",["ui.bootstrap.tpls","ui.bootstrap.collapse","ui.bootstrap.accordion","ui.bootstrap.alert","ui.bootstrap.buttons","ui.bootstrap.carousel","ui.bootstrap.dateparser","ui.bootstrap.isClass","ui.bootstrap.position","ui.bootstrap.datepicker","ui.bootstrap.debounce","ui.bootstrap.dropdown","ui.bootstrap.stackedMap","ui.bootstrap.modal","ui.bootstrap.paging","ui.bootstrap.pager","ui.bootstrap.pagination","ui.bootstrap.tooltip","ui.bootstrap.popover","ui.bootstrap.progressbar","ui.bootstrap.rating","ui.bootstrap.tabs","ui.bootstrap.timepicker","ui.bootstrap.typeahead"]),angular.module("ui.bootstrap.tpls",["uib/template/accordion/accordion-group.html","uib/template/accordion/accordion.html","uib/template/alert/alert.html","uib/template/carousel/carousel.html","uib/template/carousel/slide.html","uib/template/datepicker/datepicker.html","uib/template/datepicker/day.html","uib/template/datepicker/month.html","uib/template/datepicker/popup.html","uib/template/datepicker/year.html","uib/template/modal/backdrop.html","uib/template/modal/window.html","uib/template/pager/pager.html","uib/template/pagination/pagination.html","uib/template/tooltip/tooltip-html-popup.html","uib/template/tooltip/tooltip-popup.html","uib/template/tooltip/tooltip-template-popup.html","uib/template/popover/popover-html.html","uib/template/popover/popover-template.html","uib/template/popover/popover.html","uib/template/progressbar/bar.html","uib/template/progressbar/progress.html","uib/template/progressbar/progressbar.html","uib/template/rating/rating.html","uib/template/tabs/tab.html","uib/template/tabs/tabset.html","uib/template/timepicker/timepicker.html","uib/template/typeahead/typeahead-match.html","uib/template/typeahead/typeahead-popup.html"]),angular.module("ui.bootstrap.collapse",[]).directive("uibCollapse",["$animate","$q","$parse","$injector",function(a,b,c,d){var e=d.has("$animateCss")?d.get("$animateCss"):null;return{link:function(d,f,g){function h(){f.hasClass("collapse")&&f.hasClass("in")||b.resolve(l(d)).then(function(){f.removeClass("collapse").addClass("collapsing").attr("aria-expanded",!0).attr("aria-hidden",!1),e?e(f,{addClass:"in",easing:"ease",to:{height:f[0].scrollHeight+"px"}}).start()["finally"](i):a.addClass(f,"in",{to:{height:f[0].scrollHeight+"px"}}).then(i)})}function i(){f.removeClass("collapsing").addClass("collapse").css({height:"auto"}),m(d)}function j(){return f.hasClass("collapse")||f.hasClass("in")?void b.resolve(n(d)).then(function(){f.css({height:f[0].scrollHeight+"px"}).removeClass("collapse").addClass("collapsing").attr("aria-expanded",!1).attr("aria-hidden",!0),e?e(f,{removeClass:"in",to:{height:"0"}}).start()["finally"](k):a.removeClass(f,"in",{to:{height:"0"}}).then(k)}):k()}function k(){f.css({height:"0"}),f.removeClass("collapsing").addClass("collapse"),o(d)}var l=c(g.expanding),m=c(g.expanded),n=c(g.collapsing),o=c(g.collapsed);d.$eval(g.uibCollapse)||f.addClass("in").addClass("collapse").attr("aria-expanded",!0).attr("aria-hidden",!1).css({height:"auto"}),d.$watch(g.uibCollapse,function(a){a?j():h()})}}}]),angular.module("ui.bootstrap.accordion",["ui.bootstrap.collapse"]).constant("uibAccordionConfig",{closeOthers:!0}).controller("UibAccordionController",["$scope","$attrs","uibAccordionConfig",function(a,b,c){this.groups=[],this.closeOthers=function(d){var e=angular.isDefined(b.closeOthers)?a.$eval(b.closeOthers):c.closeOthers;e&&angular.forEach(this.groups,function(a){a!==d&&(a.isOpen=!1)})},this.addGroup=function(a){var b=this;this.groups.push(a),a.$on("$destroy",function(c){b.removeGroup(a)})},this.removeGroup=function(a){var b=this.groups.indexOf(a);-1!==b&&this.groups.splice(b,1)}}]).directive("uibAccordion",function(){return{controller:"UibAccordionController",controllerAs:"accordion",transclude:!0,templateUrl:function(a,b){return b.templateUrl||"uib/template/accordion/accordion.html"}}}).directive("uibAccordionGroup",function(){return{require:"^uibAccordion",transclude:!0,replace:!0,templateUrl:function(a,b){return b.templateUrl||"uib/template/accordion/accordion-group.html"},scope:{heading:"@",isOpen:"=?",isDisabled:"=?"},controller:function(){this.setHeading=function(a){this.heading=a}},link:function(a,b,c,d){d.addGroup(a),a.openClass=c.openClass||"panel-open",a.panelClass=c.panelClass||"panel-default",a.$watch("isOpen",function(c){b.toggleClass(a.openClass,!!c),c&&d.closeOthers(a)}),a.toggleOpen=function(b){a.isDisabled||b&&32!==b.which||(a.isOpen=!a.isOpen)};var e="accordiongroup-"+a.$id+"-"+Math.floor(1e4*Math.random());a.headingId=e+"-tab",a.panelId=e+"-panel"}}}).directive("uibAccordionHeading",function(){return{transclude:!0,template:"",replace:!0,require:"^uibAccordionGroup",link:function(a,b,c,d,e){d.setHeading(e(a,angular.noop))}}}).directive("uibAccordionTransclude",function(){return{require:"^uibAccordionGroup",link:function(a,b,c,d){a.$watch(function(){return d[c.uibAccordionTransclude]},function(a){if(a){var c=angular.element(b[0].querySelector("[uib-accordion-header]"));c.html(""),c.append(a)}})}}}),angular.module("ui.bootstrap.alert",[]).controller("UibAlertController",["$scope","$attrs","$interpolate","$timeout",function(a,b,c,d){a.closeable=!!b.close;var e=angular.isDefined(b.dismissOnTimeout)?c(b.dismissOnTimeout)(a.$parent):null;e&&d(function(){a.close()},parseInt(e,10))}]).directive("uibAlert",function(){return{controller:"UibAlertController",controllerAs:"alert",templateUrl:function(a,b){return b.templateUrl||"uib/template/alert/alert.html"},transclude:!0,replace:!0,scope:{type:"@",close:"&"}}}),angular.module("ui.bootstrap.buttons",[]).constant("uibButtonConfig",{activeClass:"active",toggleEvent:"click"}).controller("UibButtonsController",["uibButtonConfig",function(a){this.activeClass=a.activeClass||"active",this.toggleEvent=a.toggleEvent||"click"}]).directive("uibBtnRadio",["$parse",function(a){return{require:["uibBtnRadio","ngModel"],controller:"UibButtonsController",controllerAs:"buttons",link:function(b,c,d,e){var f=e[0],g=e[1],h=a(d.uibUncheckable);c.find("input").css({display:"none"}),g.$render=function(){c.toggleClass(f.activeClass,angular.equals(g.$modelValue,b.$eval(d.uibBtnRadio)))},c.on(f.toggleEvent,function(){if(!d.disabled){var a=c.hasClass(f.activeClass);(!a||angular.isDefined(d.uncheckable))&&b.$apply(function(){g.$setViewValue(a?null:b.$eval(d.uibBtnRadio)),g.$render()})}}),d.uibUncheckable&&b.$watch(h,function(a){d.$set("uncheckable",a?"":void 0)})}}}]).directive("uibBtnCheckbox",function(){return{require:["uibBtnCheckbox","ngModel"],controller:"UibButtonsController",controllerAs:"button",link:function(a,b,c,d){function e(){return g(c.btnCheckboxTrue,!0)}function f(){return g(c.btnCheckboxFalse,!1)}function g(b,c){return angular.isDefined(b)?a.$eval(b):c}var h=d[0],i=d[1];b.find("input").css({display:"none"}),i.$render=function(){b.toggleClass(h.activeClass,angular.equals(i.$modelValue,e()))},b.on(h.toggleEvent,function(){c.disabled||a.$apply(function(){i.$setViewValue(b.hasClass(h.activeClass)?f():e()),i.$render()})})}}}),angular.module("ui.bootstrap.carousel",[]).controller("UibCarouselController",["$scope","$element","$interval","$timeout","$animate",function(a,b,c,d,e){function f(){for(;t.length;)t.shift()}function g(a){for(var b=0;b1){q[d].element.data(r,c.direction);var j=p.getCurrentIndex();angular.isNumber(j)&&q[j].element&&q[j].element.data(r,c.direction),a.$currentTransition=!0,e.on("addClass",q[d].element,function(b,c){if("close"===c&&(a.$currentTransition=null,e.off("addClass",b),t.length)){var d=t.pop().slide,g=d.index,i=g>p.getCurrentIndex()?"next":"prev";f(),h(d,g,i)}})}a.active=c.index,s=c.index,g(d),l()}}function i(a){for(var b=0;b0&&(n=c(m,b))}function m(){var b=+a.interval;o&&!isNaN(b)&&b>0&&q.length?a.next():a.pause()}var n,o,p=this,q=p.slides=a.slides=[],r="uib-slideDirection",s=a.active,t=[],u=!1;p.addSlide=function(b,c){q.push({slide:b,element:c}),q.sort(function(a,b){return+a.slide.index>+b.slide.index}),(b.index===a.active||1===q.length&&!angular.isNumber(a.active))&&(a.$currentTransition&&(a.$currentTransition=null),s=b.index,a.active=b.index,g(s),p.select(q[i(b)]),1===q.length&&a.play())},p.getCurrentIndex=function(){for(var a=0;a0&&s===c?c>=q.length?(s=q.length-1,a.active=s,g(s),p.select(q[q.length-1])):(s=c,a.active=s,g(s),p.select(q[c])):s>c&&(s--,a.active=s),0===q.length&&(s=null,a.active=null,f())},p.select=a.select=function(b,c){var d=i(b.slide);void 0===c&&(c=d>p.getCurrentIndex()?"next":"prev"),b.slide.index===s||a.$currentTransition?b&&b.slide.index!==s&&a.$currentTransition&&t.push(q[d]):h(b.slide,d,c)},a.indexOfSlide=function(a){return+a.slide.index},a.isActive=function(b){return a.active===b.slide.index},a.pause=function(){a.noPause||(o=!1,j())},a.play=function(){o||(o=!0,l())},a.$on("$destroy",function(){u=!0,j()}),a.$watch("noTransition",function(a){e.enabled(b,!a)}),a.$watch("interval",l),a.$watchCollection("slides",k),a.$watch("active",function(a){if(angular.isNumber(a)&&s!==a){for(var b=0;b-1){var g=!1;a=a.split("");for(var h=f;h-1){a=a.split(""),e[f]="("+d.regex+")",a[f]="$";for(var g=f+1,h=f+d.key.length;h>g;g++)e[g]="",a[g]="$";a=a.join(""),c.push({index:f,key:d.key,apply:d[b],matcher:d.regex})}}),{regex:new RegExp("^"+e.join("")+"$"),map:d(c,"index")}}function f(a,b,c){return 1>c?!1:1===b&&c>28?29===c&&(a%4===0&&a%100!==0||a%400===0):3===b||5===b||8===b||10===b?31>c:!0}function g(a){return parseInt(a,10)}function h(a,b){return a&&b?l(a,b):a}function i(a,b){return a&&b?l(a,b,!0):a}function j(a,b){var c=Date.parse("Jan 01, 1970 00:00:00 "+a)/6e4;return isNaN(c)?b:c}function k(a,b){return a=new Date(a.getTime()),a.setMinutes(a.getMinutes()+b),a}function l(a,b,c){c=c?-1:1;var d=j(b,a.getTimezoneOffset());return k(a,c*(d-a.getTimezoneOffset()))}var m,n,o=/[\\\^\$\*\+\?\|\[\]\(\)\.\{\}]/g;this.init=function(){m=b.id,this.parsers={},this.formatters={},n=[{key:"yyyy",regex:"\\d{4}",apply:function(a){this.year=+a},formatter:function(a){var b=new Date;return b.setFullYear(Math.abs(a.getFullYear())),c(b,"yyyy")}},{key:"yy",regex:"\\d{2}",apply:function(a){this.year=+a+2e3},formatter:function(a){var b=new Date;return b.setFullYear(Math.abs(a.getFullYear())),c(b,"yy")}},{key:"y",regex:"\\d{1,4}",apply:function(a){this.year=+a},formatter:function(a){var b=new Date;return b.setFullYear(Math.abs(a.getFullYear())),c(b,"y")}},{key:"M!",regex:"0?[1-9]|1[0-2]",apply:function(a){this.month=a-1},formatter:function(a){var b=a.getMonth();return/^[0-9]$/.test(b)?c(a,"MM"):c(a,"M")}},{key:"MMMM",regex:b.DATETIME_FORMATS.MONTH.join("|"),apply:function(a){this.month=b.DATETIME_FORMATS.MONTH.indexOf(a)},formatter:function(a){return c(a,"MMMM")}},{key:"MMM",regex:b.DATETIME_FORMATS.SHORTMONTH.join("|"),apply:function(a){this.month=b.DATETIME_FORMATS.SHORTMONTH.indexOf(a)},formatter:function(a){return c(a,"MMM")}},{key:"MM",regex:"0[1-9]|1[0-2]",apply:function(a){this.month=a-1},formatter:function(a){return c(a,"MM")}},{key:"M",regex:"[1-9]|1[0-2]",apply:function(a){this.month=a-1},formatter:function(a){return c(a,"M")}},{key:"d!",regex:"[0-2]?[0-9]{1}|3[0-1]{1}",apply:function(a){this.date=+a},formatter:function(a){var b=a.getDate();return/^[1-9]$/.test(b)?c(a,"dd"):c(a,"d")}},{key:"dd",regex:"[0-2][0-9]{1}|3[0-1]{1}",apply:function(a){this.date=+a},formatter:function(a){return c(a,"dd")}},{key:"d",regex:"[1-2]?[0-9]{1}|3[0-1]{1}",apply:function(a){this.date=+a},formatter:function(a){return c(a,"d")}},{key:"EEEE",regex:b.DATETIME_FORMATS.DAY.join("|"),formatter:function(a){return c(a,"EEEE")}},{key:"EEE",regex:b.DATETIME_FORMATS.SHORTDAY.join("|"),formatter:function(a){return c(a,"EEE")}},{key:"HH",regex:"(?:0|1)[0-9]|2[0-3]",apply:function(a){this.hours=+a},formatter:function(a){return c(a,"HH")}},{key:"hh",regex:"0[0-9]|1[0-2]",apply:function(a){this.hours=+a},formatter:function(a){return c(a,"hh")}},{key:"H",regex:"1?[0-9]|2[0-3]",apply:function(a){this.hours=+a},formatter:function(a){return c(a,"H")}},{key:"h",regex:"[0-9]|1[0-2]",apply:function(a){this.hours=+a},formatter:function(a){return c(a,"h")}},{key:"mm",regex:"[0-5][0-9]",apply:function(a){this.minutes=+a},formatter:function(a){return c(a,"mm")}},{key:"m",regex:"[0-9]|[1-5][0-9]",apply:function(a){this.minutes=+a},formatter:function(a){return c(a,"m")}},{key:"sss",regex:"[0-9][0-9][0-9]",apply:function(a){this.milliseconds=+a},formatter:function(a){return c(a,"sss")}},{key:"ss",regex:"[0-5][0-9]",apply:function(a){this.seconds=+a},formatter:function(a){return c(a,"ss")}},{key:"s",regex:"[0-9]|[1-5][0-9]",apply:function(a){this.seconds=+a},formatter:function(a){return c(a,"s")}},{key:"a",regex:b.DATETIME_FORMATS.AMPMS.join("|"),apply:function(a){12===this.hours&&(this.hours=0),"PM"===a&&(this.hours+=12)},formatter:function(a){return c(a,"a")}},{key:"Z",regex:"[+-]\\d{4}",apply:function(a){var b=a.match(/([+-])(\d{2})(\d{2})/),c=b[1],d=b[2],e=b[3];this.hours+=g(c+d),this.minutes+=g(c+e)},formatter:function(a){return c(a,"Z")}},{key:"ww",regex:"[0-4][0-9]|5[0-3]",formatter:function(a){return c(a,"ww")}},{key:"w",regex:"[0-9]|[1-4][0-9]|5[0-3]",formatter:function(a){return c(a,"w")}},{key:"GGGG",regex:b.DATETIME_FORMATS.ERANAMES.join("|").replace(/\s/g,"\\s"),formatter:function(a){return c(a,"GGGG")}},{key:"GGG",regex:b.DATETIME_FORMATS.ERAS.join("|"),formatter:function(a){return c(a,"GGG")}},{key:"GG",regex:b.DATETIME_FORMATS.ERAS.join("|"),formatter:function(a){return c(a,"GG")}},{key:"G",regex:b.DATETIME_FORMATS.ERAS.join("|"),formatter:function(a){return c(a,"G")}}]},this.init(),this.filter=function(a,c){if(!angular.isDate(a)||isNaN(a)||!c)return"";c=b.DATETIME_FORMATS[c]||c,b.id!==m&&this.init(),this.formatters[c]||(this.formatters[c]=e(c,"formatter"));var d=this.formatters[c],f=d.map,g=c;return f.reduce(function(b,c,d){var e=g.match(new RegExp("(.*)"+c.key));e&&angular.isString(e[1])&&(b+=e[1],g=g.replace(e[1]+c.key,""));var h=d===f.length-1?g:"";return c.apply?b+c.apply.call(null,a)+h:b+h},"")},this.parse=function(c,d,g){if(!angular.isString(c)||!d)return c;d=b.DATETIME_FORMATS[d]||d,d=d.replace(o,"\\$&"),b.id!==m&&this.init(),this.parsers[d]||(this.parsers[d]=e(d,"apply"));var h=this.parsers[d],i=h.regex,j=h.map,k=c.match(i),l=!1;if(k&&k.length){var n,p;angular.isDate(g)&&!isNaN(g.getTime())?n={year:g.getFullYear(),month:g.getMonth(),date:g.getDate(),hours:g.getHours(),minutes:g.getMinutes(),seconds:g.getSeconds(),milliseconds:g.getMilliseconds()}:(g&&a.warn("dateparser:","baseDate is not a valid date"),n={year:1900,month:0,date:1,hours:0,minutes:0,seconds:0,milliseconds:0});for(var q=1,r=k.length;r>q;q++){var s=j[q-1];"Z"===s.matcher&&(l=!0),s.apply&&s.apply.call(n,k[q])}var t=l?Date.prototype.setUTCFullYear:Date.prototype.setFullYear,u=l?Date.prototype.setUTCHours:Date.prototype.setHours;return f(n.year,n.month,n.date)&&(!angular.isDate(g)||isNaN(g.getTime())||l?(p=new Date(0),t.call(p,n.year,n.month,n.date),u.call(p,n.hours||0,n.minutes||0,n.seconds||0,n.milliseconds||0)):(p=new Date(g),t.call(p,n.year,n.month,n.date),u.call(p,n.hours,n.minutes,n.seconds,n.milliseconds))),p}},this.toTimezone=h,this.fromTimezone=i,this.timezoneToOffset=j,this.addDateMinutes=k,this.convertTimezoneToLocal=l}]),angular.module("ui.bootstrap.isClass",[]).directive("uibIsClass",["$animate",function(a){var b=/^\s*([\s\S]+?)\s+on\s+([\s\S]+?)\s*$/,c=/^\s*([\s\S]+?)\s+for\s+([\s\S]+?)\s*$/;return{restrict:"A",compile:function(d,e){function f(a,b,c){i.push(a),j.push({scope:a,element:b}),o.forEach(function(b,c){g(b,a)}),a.$on("$destroy",h)}function g(b,d){var e=b.match(c),f=d.$eval(e[1]),g=e[2],h=k[b];if(!h){var i=function(b){var c=null;j.some(function(a){var d=a.scope.$eval(m);return d===b?(c=a,!0):void 0}),h.lastActivated!==c&&(h.lastActivated&&a.removeClass(h.lastActivated.element,f),c&&a.addClass(c.element,f),h.lastActivated=c)};k[b]=h={lastActivated:null,scope:d,watchFn:i,compareWithExp:g,watcher:d.$watch(g,i)}}h.watchFn(d.$eval(g))}function h(a){var b=a.targetScope,c=i.indexOf(b);if(i.splice(c,1),j.splice(c,1),i.length){var d=i[0];angular.forEach(k,function(a){a.scope===b&&(a.watcher=d.$watch(a.compareWithExp,a.watchFn),a.scope=d)})}else k={}}var i=[],j=[],k={},l=e.uibIsClass.match(b),m=l[2],n=l[1],o=n.split(",");return f}}}]),angular.module("ui.bootstrap.position",[]).factory("$uibPosition",["$document","$window",function(a,b){var c,d={normal:/(auto|scroll)/,hidden:/(auto|scroll|hidden)/},e={auto:/\s?auto?\s?/i,primary:/^(top|bottom|left|right)$/,secondary:/^(top|bottom|left|right|center)$/,vertical:/^(top|bottom)$/};return{getRawNode:function(a){return a.nodeName?a:a[0]||a},parseStyle:function(a){return a=parseFloat(a),isFinite(a)?a:0},offsetParent:function(c){function d(a){return"static"===(b.getComputedStyle(a).position||"static")}c=this.getRawNode(c);for(var e=c.offsetParent||a[0].documentElement;e&&e!==a[0].documentElement&&d(e);)e=e.offsetParent;return e||a[0].documentElement},scrollbarWidth:function(){if(angular.isUndefined(c)){var b=angular.element('
    ');a.find("body").append(b),c=b[0].offsetWidth-b[0].clientWidth,c=isFinite(c)?c:0,b.remove()}return c},isScrollable:function(a,c){a=this.getRawNode(a);var e=c?d.hidden:d.normal,f=b.getComputedStyle(a);return e.test(f.overflow+f.overflowY+f.overflowX)},scrollParent:function(c,e){c=this.getRawNode(c);var f=e?d.hidden:d.normal,g=a[0].documentElement,h=b.getComputedStyle(c),i="absolute"===h.position,j=c.parentElement||g;if(j===g||"fixed"===h.position)return g;for(;j.parentElement&&j!==g;){var k=b.getComputedStyle(j);if(i&&"static"!==k.position&&(i=!1),!i&&f.test(k.overflow+k.overflowY+k.overflowX))break;j=j.parentElement}return j},position:function(c,d){c=this.getRawNode(c);var e=this.offset(c);if(d){var f=b.getComputedStyle(c);e.top-=this.parseStyle(f.marginTop),e.left-=this.parseStyle(f.marginLeft)}var g=this.offsetParent(c),h={top:0,left:0};return g!==a[0].documentElement&&(h=this.offset(g),h.top+=g.clientTop-g.scrollTop,h.left+=g.clientLeft-g.scrollLeft),{width:Math.round(angular.isNumber(e.width)?e.width:c.offsetWidth),height:Math.round(angular.isNumber(e.height)?e.height:c.offsetHeight),top:Math.round(e.top-h.top),left:Math.round(e.left-h.left)}},offset:function(c){c=this.getRawNode(c);var d=c.getBoundingClientRect();return{width:Math.round(angular.isNumber(d.width)?d.width:c.offsetWidth),height:Math.round(angular.isNumber(d.height)?d.height:c.offsetHeight),top:Math.round(d.top+(b.pageYOffset||a[0].documentElement.scrollTop)),left:Math.round(d.left+(b.pageXOffset||a[0].documentElement.scrollLeft))}},viewportOffset:function(c,d,e){c=this.getRawNode(c),e=e!==!1?!0:!1;var f=c.getBoundingClientRect(),g={top:0,left:0,bottom:0,right:0},h=d?a[0].documentElement:this.scrollParent(c),i=h.getBoundingClientRect();if(g.top=i.top+h.clientTop,g.left=i.left+h.clientLeft,h===a[0].documentElement&&(g.top+=b.pageYOffset,g.left+=b.pageXOffset),g.bottom=g.top+h.clientHeight,g.right=g.left+h.clientWidth,e){var j=b.getComputedStyle(h);g.top+=this.parseStyle(j.paddingTop),g.bottom-=this.parseStyle(j.paddingBottom),g.left+=this.parseStyle(j.paddingLeft),g.right-=this.parseStyle(j.paddingRight)}return{top:Math.round(f.top-g.top),bottom:Math.round(g.bottom-f.bottom),left:Math.round(f.left-g.left),right:Math.round(g.right-f.right)}},parsePlacement:function(a){var b=e.auto.test(a);return b&&(a=a.replace(e.auto,"")),a=a.split("-"),a[0]=a[0]||"top",e.primary.test(a[0])||(a[0]="top"),a[1]=a[1]||"center",e.secondary.test(a[1])||(a[1]="center"),b?a[2]=!0:a[2]=!1,a},positionElements:function(a,c,d,f){a=this.getRawNode(a),c=this.getRawNode(c);var g=angular.isDefined(c.offsetWidth)?c.offsetWidth:c.prop("offsetWidth"),h=angular.isDefined(c.offsetHeight)?c.offsetHeight:c.prop("offsetHeight");d=this.parsePlacement(d);var i=f?this.offset(a):this.position(a),j={top:0,left:0,placement:""};if(d[2]){var k=this.viewportOffset(a,f),l=b.getComputedStyle(c),m={width:g+Math.round(Math.abs(this.parseStyle(l.marginLeft)+this.parseStyle(l.marginRight))),height:h+Math.round(Math.abs(this.parseStyle(l.marginTop)+this.parseStyle(l.marginBottom)))};if(d[0]="top"===d[0]&&m.height>k.top&&m.height<=k.bottom?"bottom":"bottom"===d[0]&&m.height>k.bottom&&m.height<=k.top?"top":"left"===d[0]&&m.width>k.left&&m.width<=k.right?"right":"right"===d[0]&&m.width>k.right&&m.width<=k.left?"left":d[0],d[1]="top"===d[1]&&m.height-i.height>k.bottom&&m.height-i.height<=k.top?"bottom":"bottom"===d[1]&&m.height-i.height>k.top&&m.height-i.height<=k.bottom?"top":"left"===d[1]&&m.width-i.width>k.right&&m.width-i.width<=k.left?"right":"right"===d[1]&&m.width-i.width>k.left&&m.width-i.width<=k.right?"left":d[1],"center"===d[1])if(e.vertical.test(d[0])){var n=i.width/2-g/2;k.left+n<0&&m.width-i.width<=k.right?d[1]="left":k.right+n<0&&m.width-i.width<=k.left&&(d[1]="right")}else{var o=i.height/2-m.height/2;k.top+o<0&&m.height-i.height<=k.bottom?d[1]="top":k.bottom+o<0&&m.height-i.height<=k.top&&(d[1]="bottom")}}switch(d[0]){case"top":j.top=i.top-h;break;case"bottom":j.top=i.top+i.height;break;case"left":j.left=i.left-g;break;case"right":j.left=i.left+i.width}switch(d[1]){case"top":j.top=i.top;break;case"bottom":j.top=i.top+i.height-h;break;case"left":j.left=i.left;break;case"right":j.left=i.left+i.width-g;break;case"center":e.vertical.test(d[0])?j.left=i.left+i.width/2-g/2:j.top=i.top+i.height/2-h/2}return j.top=Math.round(j.top),j.left=Math.round(j.left),j.placement="center"===d[1]?d[0]:d[0]+"-"+d[1],j},positionArrow:function(a,c){a=this.getRawNode(a);var d=a.querySelector(".tooltip-inner, .popover-inner");if(d){var f=angular.element(d).hasClass("tooltip-inner"),g=f?a.querySelector(".tooltip-arrow"):a.querySelector(".arrow");if(g){var h={top:"",bottom:"",left:"",right:""};if(c=this.parsePlacement(c),"center"===c[1])return void angular.element(g).css(h);var i="border-"+c[0]+"-width",j=b.getComputedStyle(g)[i],k="border-";k+=e.vertical.test(c[0])?c[0]+"-"+c[1]:c[1]+"-"+c[0],k+="-radius";var l=b.getComputedStyle(f?d:a)[k];switch(c[0]){case"top":h.bottom=f?"0":"-"+j;break;case"bottom":h.top=f?"0":"-"+j;break;case"left":h.right=f?"0":"-"+j;break;case"right":h.left=f?"0":"-"+j}h[c[1]]=l,angular.element(g).css(h)}}}}}]),angular.module("ui.bootstrap.datepicker",["ui.bootstrap.dateparser","ui.bootstrap.isClass","ui.bootstrap.position"]).value("$datepickerSuppressError",!1).value("uibDatepickerAttributeWarning",!0).constant("uibDatepickerConfig",{datepickerMode:"day",formatDay:"dd",formatMonth:"MMMM",formatYear:"yyyy",formatDayHeader:"EEE",formatDayTitle:"MMMM yyyy",formatMonthTitle:"yyyy",maxDate:null,maxMode:"year",minDate:null,minMode:"day",ngModelOptions:{},shortcutPropagation:!1,showWeeks:!0,yearColumns:5,yearRows:4}).controller("UibDatepickerController",["$scope","$attrs","$parse","$interpolate","$locale","$log","dateFilter","uibDatepickerConfig","$datepickerSuppressError","uibDatepickerAttributeWarning","uibDateParser",function(a,b,c,d,e,f,g,h,i,j,k){function l(b){a.datepickerMode=b,q&&(a.datepickerOptions.datepickerMode=b)}var m=this,n={$setViewValue:angular.noop},o={},p=[],q=!!b.datepickerOptions;if(this.modes=["day","month","year"],q)["customClass","dateDisabled","datepickerMode","formatDay","formatDayHeader","formatDayTitle","formatMonth","formatMonthTitle","formatYear","initDate","maxDate","maxMode","minDate","minMode","showWeeks","shortcutPropagation","startingDay","yearColumns","yearRows"].forEach(function(b){switch(b){case"customClass":case"dateDisabled":a[b]=a.datepickerOptions[b]||angular.noop;break;case"datepickerMode":a.datepickerMode=angular.isDefined(a.datepickerOptions.datepickerMode)?a.datepickerOptions.datepickerMode:h.datepickerMode;break;case"formatDay":case"formatDayHeader":case"formatDayTitle":case"formatMonth":case"formatMonthTitle":case"formatYear":m[b]=angular.isDefined(a.datepickerOptions[b])?d(a.datepickerOptions[b])(a.$parent):h[b];break;case"showWeeks":case"shortcutPropagation":case"yearColumns":case"yearRows":m[b]=angular.isDefined(a.datepickerOptions[b])?a.datepickerOptions[b]:h[b];break;case"startingDay":angular.isDefined(a.datepickerOptions.startingDay)?m.startingDay=a.datepickerOptions.startingDay:angular.isNumber(h.startingDay)?m.startingDay=h.startingDay:m.startingDay=(e.DATETIME_FORMATS.FIRSTDAYOFWEEK+8)%7;break;case"maxDate":case"minDate":a.datepickerOptions[b]?a.$watch(function(){return a.datepickerOptions[b]},function(a){a?angular.isDate(a)?m[b]=k.fromTimezone(new Date(a),o.timezone):m[b]=new Date(g(a,"medium")):m[b]=null,m.refreshView()}):m[b]=h[b]?k.fromTimezone(new Date(h[b]),o.timezone):null;break;case"maxMode":case"minMode":a.datepickerOptions[b]?a.$watch(function(){return a.datepickerOptions[b]},function(c){m[b]=a[b]=angular.isDefined(c)?c:datepickerOptions[b],("minMode"===b&&m.modes.indexOf(a.datepickerOptions.datepickerMode)m.modes.indexOf(m[b]))&&(a.datepickerMode=m[b],a.datepickerOptions.datepickerMode=m[b])}):m[b]=a[b]=h[b]||null;break;case"initDate":a.datepickerOptions.initDate?(m.activeDate=k.fromTimezone(a.datepickerOptions.initDate,o.timezone)||new Date,a.$watch(function(){return a.datepickerOptions.initDate},function(a){a&&(n.$isEmpty(n.$modelValue)||n.$invalid)&&(m.activeDate=k.fromTimezone(a,o.timezone),m.refreshView())})):m.activeDate=new Date}});else{if(angular.forEach(["formatDay","formatMonth","formatYear","formatDayHeader","formatDayTitle","formatMonthTitle"],function(c){m[c]=angular.isDefined(b[c])?d(b[c])(a.$parent):h[c],angular.isDefined(b[c])&&j&&f.warn("uib-datepicker "+c+" attribute usage is deprecated, use datepicker-options attribute instead")}),angular.forEach(["showWeeks","yearRows","yearColumns","shortcutPropagation"],function(c){m[c]=angular.isDefined(b[c])?a.$parent.$eval(b[c]):h[c],angular.isDefined(b[c])&&j&&f.warn("uib-datepicker "+c+" attribute usage is deprecated, use datepicker-options attribute instead")}),angular.forEach(["dateDisabled","customClass"],function(a){angular.isDefined(b[a])&&j&&f.warn("uib-datepicker "+a+" attribute usage is deprecated, use datepicker-options attribute instead")}),angular.isDefined(b.startingDay)?(j&&f.warn("uib-datepicker startingDay attribute usage is deprecated, use datepicker-options attribute instead"),m.startingDay=a.$parent.$eval(b.startingDay)):angular.isNumber(h.startingDay)?m.startingDay=h.startingDay:m.startingDay=(e.DATETIME_FORMATS.FIRSTDAYOFWEEK+8)%7,angular.forEach(["minDate","maxDate"],function(c){b[c]?(j&&f.warn("uib-datepicker "+c+" attribute usage is deprecated, use datepicker-options attribute instead"),p.push(a.$parent.$watch(b[c],function(a){a?angular.isDate(a)?m[c]=k.fromTimezone(new Date(a),o.timezone):m[c]=new Date(g(a,"medium")):m[c]=null,m.refreshView()}))):m[c]=h[c]?k.fromTimezone(new Date(h[c]),o.timezone):null}),angular.forEach(["minMode","maxMode"],function(c){b[c]?(j&&f.warn("uib-datepicker "+c+" attribute usage is deprecated, use datepicker-options attribute instead"),p.push(a.$parent.$watch(b[c],function(d){m[c]=a[c]=angular.isDefined(d)?d:b[c],("minMode"===c&&m.modes.indexOf(a.datepickerMode)m.modes.indexOf(m[c]))&&(a.datepickerMode=m[c])}))):m[c]=a[c]=h[c]||null}),angular.isDefined(b.initDate)){j&&f.warn("uib-datepicker initDate attribute usage is deprecated, use datepicker-options attribute instead");var r=k.fromTimezone(a.$parent.$eval(b.initDate),o.timezone);this.activeDate=isNaN(r)?new Date:r,p.push(a.$parent.$watch(b.initDate,function(a){a&&(n.$isEmpty(n.$modelValue)||n.$invalid)&&(a=k.fromTimezone(a,o.timezone),m.activeDate=isNaN(a)?new Date:a,m.refreshView())}))}else this.activeDate=new Date;b.datepickerMode&&j&&f.warn("uib-datepicker datepickerMode attribute usage is deprecated, use datepicker-options attribute instead"),a.datepickerMode=a.datepickerMode||h.datepickerMode}a.uniqueId="datepicker-"+a.$id+"-"+Math.floor(1e4*Math.random()),a.disabled=angular.isDefined(b.disabled)||!1,angular.isDefined(b.ngDisabled)&&p.push(a.$parent.$watch(b.ngDisabled,function(b){a.disabled=b,m.refreshView()})),a.isActive=function(b){return 0===m.compare(b.date,m.activeDate)?(a.activeDateId=b.uid,!0):!1},this.init=function(a){n=a,o=a.$options||h.ngModelOptions,this.activeDate=n.$modelValue||new Date,n.$render=function(){m.render()}},this.render=function(){if(n.$viewValue){var a=new Date(n.$viewValue),b=!isNaN(a);b?this.activeDate=k.fromTimezone(a,o.timezone):i||f.error('Datepicker directive: "ng-model" value must be a Date object')}this.refreshView()},this.refreshView=function(){if(this.element){a.selectedDt=null,this._refreshView(),a.activeDt&&(a.activeDateId=a.activeDt.uid);var b=n.$viewValue?new Date(n.$viewValue):null;b=k.fromTimezone(b,o.timezone),n.$setValidity("dateDisabled",!b||this.element&&!this.isDisabled(b))}},this.createDateObject=function(b,c){var d=n.$viewValue?new Date(n.$viewValue):null;d=k.fromTimezone(d,o.timezone);var e=new Date;e=k.fromTimezone(e,o.timezone);var f=this.compare(b,e),g={date:b,label:k.filter(b,c),selected:d&&0===this.compare(b,d),disabled:this.isDisabled(b),past:0>f,current:0===f,future:f>0,customClass:this.customClass(b)||null};return d&&0===this.compare(b,d)&&(a.selectedDt=g),m.activeDate&&0===this.compare(g.date,m.activeDate)&&(a.activeDt=g),g},this.isDisabled=function(b){return a.disabled||this.minDate&&this.compare(b,this.minDate)<0||this.maxDate&&this.compare(b,this.maxDate)>0||a.dateDisabled&&a.dateDisabled({date:b,mode:a.datepickerMode})},this.customClass=function(b){return a.customClass({date:b,mode:a.datepickerMode})},this.split=function(a,b){for(var c=[];a.length>0;)c.push(a.splice(0,b));return c; +},a.select=function(b){if(a.datepickerMode===m.minMode){var c=n.$viewValue?k.fromTimezone(new Date(n.$viewValue),o.timezone):new Date(0,0,0,0,0,0,0);c.setFullYear(b.getFullYear(),b.getMonth(),b.getDate()),c=k.toTimezone(c,o.timezone),n.$setViewValue(c),n.$render()}else m.activeDate=b,l(m.modes[m.modes.indexOf(a.datepickerMode)-1]),a.$emit("uib:datepicker.mode")},a.move=function(a){var b=m.activeDate.getFullYear()+a*(m.step.years||0),c=m.activeDate.getMonth()+a*(m.step.months||0);m.activeDate.setFullYear(b,c,1),m.refreshView()},a.toggleMode=function(b){b=b||1,a.datepickerMode===m.maxMode&&1===b||a.datepickerMode===m.minMode&&-1===b||(l(m.modes[m.modes.indexOf(a.datepickerMode)+b]),a.$emit("uib:datepicker.mode"))},a.keys={13:"enter",32:"space",33:"pageup",34:"pagedown",35:"end",36:"home",37:"left",38:"up",39:"right",40:"down"};var s=function(){m.element[0].focus()};a.$on("uib:datepicker.focus",s),a.keydown=function(b){var c=a.keys[b.which];if(c&&!b.shiftKey&&!b.altKey&&!a.disabled)if(b.preventDefault(),m.shortcutPropagation||b.stopPropagation(),"enter"===c||"space"===c){if(m.isDisabled(m.activeDate))return;a.select(m.activeDate)}else!b.ctrlKey||"up"!==c&&"down"!==c?(m.handleKeyDown(c,b),m.refreshView()):a.toggleMode("up"===c?1:-1)},a.$on("$destroy",function(){for(;p.length;)p.shift()()})}]).controller("UibDaypickerController",["$scope","$element","dateFilter",function(a,b,c){function d(a,b){return 1!==b||a%4!==0||a%100===0&&a%400!==0?f[b]:29}function e(a){var b=new Date(a);b.setDate(b.getDate()+4-(b.getDay()||7));var c=b.getTime();return b.setMonth(0),b.setDate(1),Math.floor(Math.round((c-b)/864e5)/7)+1}var f=[31,28,31,30,31,30,31,31,30,31,30,31];this.step={months:1},this.element=b,this.init=function(b){angular.extend(b,this),a.showWeeks=b.showWeeks,b.refreshView()},this.getDates=function(a,b){for(var c,d=new Array(b),e=new Date(a),f=0;b>f;)c=new Date(e),d[f++]=c,e.setDate(e.getDate()+1);return d},this._refreshView=function(){var b=this.activeDate.getFullYear(),d=this.activeDate.getMonth(),f=new Date(this.activeDate);f.setFullYear(b,d,1);var g=this.startingDay-f.getDay(),h=g>0?7-g:-g,i=new Date(f);h>0&&i.setDate(-h+1);for(var j=this.getDates(i,42),k=0;42>k;k++)j[k]=angular.extend(this.createDateObject(j[k],this.formatDay),{secondary:j[k].getMonth()!==d,uid:a.uniqueId+"-"+k});a.labels=new Array(7);for(var l=0;7>l;l++)a.labels[l]={abbr:c(j[l].date,this.formatDayHeader),full:c(j[l].date,"EEEE")};if(a.title=c(this.activeDate,this.formatDayTitle),a.rows=this.split(j,7),a.showWeeks){a.weekNumbers=[];for(var m=(11-this.startingDay)%7,n=a.rows.length,o=0;n>o;o++)a.weekNumbers.push(e(a.rows[o][m].date))}},this.compare=function(a,b){var c=new Date(a.getFullYear(),a.getMonth(),a.getDate()),d=new Date(b.getFullYear(),b.getMonth(),b.getDate());return c.setFullYear(a.getFullYear()),d.setFullYear(b.getFullYear()),c-d},this.handleKeyDown=function(a,b){var c=this.activeDate.getDate();if("left"===a)c-=1;else if("up"===a)c-=7;else if("right"===a)c+=1;else if("down"===a)c+=7;else if("pageup"===a||"pagedown"===a){var e=this.activeDate.getMonth()+("pageup"===a?-1:1);this.activeDate.setMonth(e,1),c=Math.min(d(this.activeDate.getFullYear(),this.activeDate.getMonth()),c)}else"home"===a?c=1:"end"===a&&(c=d(this.activeDate.getFullYear(),this.activeDate.getMonth()));this.activeDate.setDate(c)}}]).controller("UibMonthpickerController",["$scope","$element","dateFilter",function(a,b,c){this.step={years:1},this.element=b,this.init=function(a){angular.extend(a,this),a.refreshView()},this._refreshView=function(){for(var b,d=new Array(12),e=this.activeDate.getFullYear(),f=0;12>f;f++)b=new Date(this.activeDate),b.setFullYear(e,f,1),d[f]=angular.extend(this.createDateObject(b,this.formatMonth),{uid:a.uniqueId+"-"+f});a.title=c(this.activeDate,this.formatMonthTitle),a.rows=this.split(d,3)},this.compare=function(a,b){var c=new Date(a.getFullYear(),a.getMonth()),d=new Date(b.getFullYear(),b.getMonth());return c.setFullYear(a.getFullYear()),d.setFullYear(b.getFullYear()),c-d},this.handleKeyDown=function(a,b){var c=this.activeDate.getMonth();if("left"===a)c-=1;else if("up"===a)c-=3;else if("right"===a)c+=1;else if("down"===a)c+=3;else if("pageup"===a||"pagedown"===a){var d=this.activeDate.getFullYear()+("pageup"===a?-1:1);this.activeDate.setFullYear(d)}else"home"===a?c=0:"end"===a&&(c=11);this.activeDate.setMonth(c)}}]).controller("UibYearpickerController",["$scope","$element","dateFilter",function(a,b,c){function d(a){return parseInt((a-1)/f,10)*f+1}var e,f;this.element=b,this.yearpickerInit=function(){e=this.yearColumns,f=this.yearRows*e,this.step={years:f}},this._refreshView=function(){for(var b,c=new Array(f),g=0,h=d(this.activeDate.getFullYear());f>g;g++)b=new Date(this.activeDate),b.setFullYear(h+g,0,1),c[g]=angular.extend(this.createDateObject(b,this.formatYear),{uid:a.uniqueId+"-"+g});a.title=[c[0].label,c[f-1].label].join(" - "),a.rows=this.split(c,e),a.columns=e},this.compare=function(a,b){return a.getFullYear()-b.getFullYear()},this.handleKeyDown=function(a,b){var c=this.activeDate.getFullYear();"left"===a?c-=1:"up"===a?c-=e:"right"===a?c+=1:"down"===a?c+=e:"pageup"===a||"pagedown"===a?c+=("pageup"===a?-1:1)*f:"home"===a?c=d(this.activeDate.getFullYear()):"end"===a&&(c=d(this.activeDate.getFullYear())+f-1),this.activeDate.setFullYear(c)}}]).directive("uibDatepicker",function(){return{replace:!0,templateUrl:function(a,b){return b.templateUrl||"uib/template/datepicker/datepicker.html"},scope:{datepickerMode:"=?",datepickerOptions:"=?",dateDisabled:"&",customClass:"&",shortcutPropagation:"&?"},require:["uibDatepicker","^ngModel"],controller:"UibDatepickerController",controllerAs:"datepicker",link:function(a,b,c,d){var e=d[0],f=d[1];e.init(f)}}}).directive("uibDaypicker",function(){return{replace:!0,templateUrl:function(a,b){return b.templateUrl||"uib/template/datepicker/day.html"},require:["^uibDatepicker","uibDaypicker"],controller:"UibDaypickerController",link:function(a,b,c,d){var e=d[0],f=d[1];f.init(e)}}}).directive("uibMonthpicker",function(){return{replace:!0,templateUrl:function(a,b){return b.templateUrl||"uib/template/datepicker/month.html"},require:["^uibDatepicker","uibMonthpicker"],controller:"UibMonthpickerController",link:function(a,b,c,d){var e=d[0],f=d[1];f.init(e)}}}).directive("uibYearpicker",function(){return{replace:!0,templateUrl:function(a,b){return b.templateUrl||"uib/template/datepicker/year.html"},require:["^uibDatepicker","uibYearpicker"],controller:"UibYearpickerController",link:function(a,b,c,d){var e=d[0];angular.extend(e,d[1]),e.yearpickerInit(),e.refreshView()}}}).value("uibDatepickerPopupAttributeWarning",!0).constant("uibDatepickerPopupConfig",{altInputFormats:[],appendToBody:!1,clearText:"Clear",closeOnDateSelection:!0,closeText:"Done",currentText:"Today",datepickerPopup:"yyyy-MM-dd",datepickerPopupTemplateUrl:"uib/template/datepicker/popup.html",datepickerTemplateUrl:"uib/template/datepicker/datepicker.html",html5Types:{date:"yyyy-MM-dd","datetime-local":"yyyy-MM-ddTHH:mm:ss.sss",month:"yyyy-MM"},onOpenFocus:!0,showButtonBar:!0,placement:"auto bottom-left"}).controller("UibDatepickerPopupController",["$scope","$element","$attrs","$compile","$log","$parse","$window","$document","$rootScope","$uibPosition","dateFilter","uibDateParser","uibDatepickerPopupConfig","$timeout","uibDatepickerConfig","uibDatepickerPopupAttributeWarning",function(a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p){function q(a){return a.replace(/([A-Z])/g,function(a){return"-"+a.toLowerCase()})}function r(b){var c=l.parse(b,x,a.date);if(isNaN(c))for(var d=0;d
    "),a.ngModelOptions=angular.copy(H),a.ngModelOptions.timezone=null,a.ngModelOptions.updateOnDefault===!0&&(a.ngModelOptions.updateOn=a.ngModelOptions.updateOn?a.ngModelOptions.updateOn+" default":"default"),D.attr({"ng-model":"date","ng-model-options":"ngModelOptions","ng-change":"dateSelection(date)","template-url":B}),E=angular.element(D.children()[0]),E.attr("template-url",C),L&&"month"===c.type&&(E.attr("datepicker-mode",'"month"'),E.attr("min-mode","month")),a.datepickerOptions&&E.attr("datepicker-options","datepickerOptions"),angular.forEach(["minMode","maxMode","datepickerMode","shortcutPropagation"],function(b){if(c[b]){p&&e.warn("uib-datepicker settings via uib-datepicker-popup attributes are deprecated and will be removed in UI Bootstrap 1.3, use datepicker-options attribute instead");var d=f(c[b]),g={get:function(){return d(a.$parent)}};if(E.attr(q(b),"watchData."+b),"datepickerMode"===b){var h=d.assign;g.set=function(b){h(a.$parent,b)}}Object.defineProperty(a.watchData,b,g)}}),angular.forEach(["minDate","maxDate","initDate"],function(b){if(c[b]){p&&e.warn("uib-datepicker settings via uib-datepicker-popup attributes are deprecated and will be removed in UI Bootstrap 1.3, use datepicker-options attribute instead");var d=f(c[b]);M.push(a.$parent.$watch(d,function(c){if("minDate"===b||"maxDate"===b)null===c?K[b]=null:angular.isDate(c)?K[b]=l.fromTimezone(new Date(c),H.timezone):K[b]=new Date(k(c,"medium")),a.watchData[b]=null===c?null:K[b];else{var d=c?new Date(c):new Date;a.watchData[b]=l.fromTimezone(d,H.timezone)}})),E.attr(q(b),"watchData."+b)}}),c.dateDisabled&&(p&&e.warn("uib-datepicker settings via uib-datepicker-popup attributes are deprecated and will be removed in UI Bootstrap 1.3, use datepicker-options attribute instead"),E.attr("date-disabled","dateDisabled({ date: date, mode: mode })")),angular.forEach(["formatDay","formatMonth","formatYear","formatDayHeader","formatDayTitle","formatMonthTitle","showWeeks","startingDay","yearRows","yearColumns"],function(a){angular.isDefined(c[a])&&(p&&e.warn("uib-datepicker settings via uib-datepicker-popup attributes are deprecated and will be removed in UI Bootstrap 1.3, use datepicker-options attribute instead"),E.attr(q(a),c[a]))}),c.customClass&&(p&&e.warn("uib-datepicker settings via uib-datepicker-popup attributes are deprecated and will be removed in UI Bootstrap 1.3, use datepicker-options attribute instead"),E.attr("custom-class","customClass({ date: date, mode: mode })")),L?G.$formatters.push(function(b){return a.date=l.fromTimezone(b,H.timezone),b}):(G.$$parserName="date",G.$validators.date=t,G.$parsers.unshift(s),G.$formatters.push(function(b){return G.$isEmpty(b)?(a.date=b,b):(a.date=l.fromTimezone(b,H.timezone),angular.isNumber(a.date)&&(a.date=new Date(a.date)),l.filter(a.date,x))})),G.$viewChangeListeners.push(function(){a.date=r(G.$viewValue)}),b.on("keydown",v),I=d(D)(a),D.remove(),z?h.find("body").append(I):b.after(I),a.$on("$destroy",function(){for(a.isOpen===!0&&(i.$$phase||a.$apply(function(){a.isOpen=!1})),I.remove(),b.off("keydown",v),h.off("click",u),F&&F.off("scroll",w),angular.element(g).off("resize",w);M.length;)M.shift()()})},a.getText=function(b){return a[b+"Text"]||m[b+"Text"]},a.isDisabled=function(b){return"today"===b&&(b=new Date),a.watchData.minDate&&a.compare(b,K.minDate)<0||a.watchData.maxDate&&a.compare(b,K.maxDate)>0},a.compare=function(a,b){return new Date(a.getFullYear(),a.getMonth(),a.getDate())-new Date(b.getFullYear(),b.getMonth(),b.getDate())},a.dateSelection=function(c){angular.isDefined(c)&&(a.date=c);var d=a.date?l.filter(a.date,x):null;b.val(d),G.$setViewValue(d),y&&(a.isOpen=!1,b[0].focus())},a.keydown=function(c){27===c.which&&(c.stopPropagation(),a.isOpen=!1,b[0].focus())},a.select=function(b,c){if(c.stopPropagation(),"today"===b){var d=new Date;angular.isDate(a.date)?(b=new Date(a.date),b.setFullYear(d.getFullYear(),d.getMonth(),d.getDate())):b=new Date(d.setHours(0,0,0,0))}a.dateSelection(b)},a.close=function(c){c.stopPropagation(),a.isOpen=!1,b[0].focus()},a.disabled=angular.isDefined(c.disabled)||!1,c.ngDisabled&&M.push(a.$parent.$watch(f(c.ngDisabled),function(b){a.disabled=b})),a.$watch("isOpen",function(d){d?a.disabled?a.isOpen=!1:n(function(){w(),A&&a.$broadcast("uib:datepicker.focus"),h.on("click",u);var d=c.popupPlacement?c.popupPlacement:m.placement;z||j.parsePlacement(d)[2]?(F=F||angular.element(j.scrollParent(b)),F&&F.on("scroll",w)):F=null,angular.element(g).on("resize",w)},0,!1):(h.off("click",u),F&&F.off("scroll",w),angular.element(g).off("resize",w))}),a.$on("uib:datepicker.mode",function(){n(w,0,!1)})}]).directive("uibDatepickerPopup",function(){return{require:["ngModel","uibDatepickerPopup"],controller:"UibDatepickerPopupController",scope:{datepickerOptions:"=?",isOpen:"=?",currentText:"@",clearText:"@",closeText:"@",dateDisabled:"&",customClass:"&"},link:function(a,b,c,d){var e=d[0],f=d[1];f.init(e)}}}).directive("uibDatepickerPopupWrap",function(){return{replace:!0,transclude:!0,templateUrl:function(a,b){return b.templateUrl||"uib/template/datepicker/popup.html"}}}),angular.module("ui.bootstrap.debounce",[]).factory("$$debounce",["$timeout",function(a){return function(b,c){var d;return function(){var e=this,f=Array.prototype.slice.call(arguments);d&&a.cancel(d),d=a(function(){b.apply(e,f)},c)}}}]),angular.module("ui.bootstrap.dropdown",["ui.bootstrap.position"]).constant("uibDropdownConfig",{appendToOpenClass:"uib-dropdown-open",openClass:"open"}).service("uibDropdownService",["$document","$rootScope",function(a,b){var c=null;this.open=function(b){c||(a.on("click",d),a.on("keydown",e)),c&&c!==b&&(c.isOpen=!1),c=b},this.close=function(b){c===b&&(c=null,a.off("click",d),a.off("keydown",e))};var d=function(a){if(c&&!(a&&"disabled"===c.getAutoClose()||a&&3===a.which)){var d=c.getToggleElement();if(!(a&&d&&d[0].contains(a.target))){var e=c.getDropdownElement();a&&"outsideClick"===c.getAutoClose()&&e&&e[0].contains(a.target)||(c.isOpen=!1,b.$$phase||c.$apply())}}},e=function(a){27===a.which?(c.focusToggleElement(),d()):c.isKeynavEnabled()&&-1!==[38,40].indexOf(a.which)&&c.isOpen&&(a.preventDefault(),a.stopPropagation(),c.focusDropdownEntry(a.which))}}]).controller("UibDropdownController",["$scope","$element","$attrs","$parse","uibDropdownConfig","uibDropdownService","$animate","$uibPosition","$document","$compile","$templateRequest",function(a,b,c,d,e,f,g,h,i,j,k){var l,m,n=this,o=a.$new(),p=e.appendToOpenClass,q=e.openClass,r=angular.noop,s=c.onToggle?d(c.onToggle):angular.noop,t=!1,u=null,v=!1,w=i.find("body");b.addClass("dropdown"),this.init=function(){if(c.isOpen&&(m=d(c.isOpen),r=m.assign,a.$watch(m,function(a){o.isOpen=!!a})),angular.isDefined(c.dropdownAppendTo)){var e=d(c.dropdownAppendTo)(o);e&&(u=angular.element(e))}t=angular.isDefined(c.dropdownAppendToBody),v=angular.isDefined(c.keyboardNav),t&&!u&&(u=w),u&&n.dropdownMenu&&(u.append(n.dropdownMenu),b.on("$destroy",function(){n.dropdownMenu.remove()}))},this.toggle=function(a){return o.isOpen=arguments.length?!!a:!o.isOpen,angular.isFunction(r)&&r(o,o.isOpen),o.isOpen},this.isOpen=function(){return o.isOpen},o.getToggleElement=function(){return n.toggleElement},o.getAutoClose=function(){return c.autoClose||"always"},o.getElement=function(){return b},o.isKeynavEnabled=function(){return v},o.focusDropdownEntry=function(a){var c=n.dropdownMenu?angular.element(n.dropdownMenu).find("a"):b.find("ul").eq(0).find("a");switch(a){case 40:angular.isNumber(n.selectedOption)?n.selectedOption=n.selectedOption===c.length-1?n.selectedOption:n.selectedOption+1:n.selectedOption=0;break;case 38:angular.isNumber(n.selectedOption)?n.selectedOption=0===n.selectedOption?0:n.selectedOption-1:n.selectedOption=c.length-1}c[n.selectedOption].focus()},o.getDropdownElement=function(){return n.dropdownMenu},o.focusToggleElement=function(){n.toggleElement&&n.toggleElement[0].focus()},o.$watch("isOpen",function(c,d){if(u&&n.dropdownMenu){var e,i,m=h.positionElements(b,n.dropdownMenu,"bottom-left",!0);if(e={top:m.top+"px",display:c?"block":"none"},i=n.dropdownMenu.hasClass("dropdown-menu-right"),i?(e.left="auto",e.right=window.innerWidth-(m.left+b.prop("offsetWidth"))+"px"):(e.left=m.left+"px",e.right="auto"),!t){var v=h.offset(u);e.top=m.top-v.top+"px",i?e.right=window.innerWidth-(m.left-v.left+b.prop("offsetWidth"))+"px":e.left=m.left-v.left+"px"}n.dropdownMenu.css(e)}var w=u?u:b,x=w.hasClass(u?p:q);if(x===!c&&g[c?"addClass":"removeClass"](w,u?p:q).then(function(){angular.isDefined(c)&&c!==d&&s(a,{open:!!c})}),c)n.dropdownMenuTemplateUrl&&k(n.dropdownMenuTemplateUrl).then(function(a){l=o.$new(),j(a.trim())(l,function(a){var b=a;n.dropdownMenu.replaceWith(b),n.dropdownMenu=b})}),o.focusToggleElement(),f.open(o);else{if(n.dropdownMenuTemplateUrl){l&&l.$destroy();var y=angular.element('');n.dropdownMenu.replaceWith(y),n.dropdownMenu=y}f.close(o),n.selectedOption=null}angular.isFunction(r)&&r(a,c)}),a.$on("$locationChangeSuccess",function(){"disabled"!==o.getAutoClose()&&(o.isOpen=!1)})}]).directive("uibDropdown",function(){return{controller:"UibDropdownController",link:function(a,b,c,d){d.init()}}}).directive("uibDropdownMenu",function(){return{restrict:"A",require:"?^uibDropdown",link:function(a,b,c,d){if(d&&!angular.isDefined(c.dropdownNested)){b.addClass("dropdown-menu");var e=c.templateUrl;e&&(d.dropdownMenuTemplateUrl=e),d.dropdownMenu||(d.dropdownMenu=b)}}}}).directive("uibDropdownToggle",function(){return{require:"?^uibDropdown",link:function(a,b,c,d){if(d){b.addClass("dropdown-toggle"),d.toggleElement=b;var e=function(e){e.preventDefault(),b.hasClass("disabled")||c.disabled||a.$apply(function(){d.toggle()})};b.bind("click",e),b.attr({"aria-haspopup":!0,"aria-expanded":!1}),a.$watch(d.isOpen,function(a){b.attr("aria-expanded",!!a)}),a.$on("$destroy",function(){b.unbind("click",e)})}}}}),angular.module("ui.bootstrap.stackedMap",[]).factory("$$stackedMap",function(){return{createNew:function(){var a=[];return{add:function(b,c){a.push({key:b,value:c})},get:function(b){for(var c=0;c0&&(b=t.top().value,b.modalDomEl.toggleClass(b.windowTopClass||"",a))}function l(){if(p&&-1===i()){var a=q;m(p,q,function(){a=null}),p=void 0,q=void 0}}function m(b,c,d,e){function g(){g.done||(g.done=!0,a.leave(b).then(function(){b.remove(),e&&e.resolve()}),c.$destroy(),d&&d())}var h,i=null,j=function(){return h||(h=f.defer(),i=h.promise),function(){h.resolve()}};return c.$broadcast(v.NOW_CLOSING_EVENT,j),f.when(i).then(g)}function n(a){if(a.isDefaultPrevented())return a;var b=t.top();if(b)switch(a.which){case 27:b.value.keyboard&&(a.preventDefault(),e.$apply(function(){v.dismiss(b.key,"escape key press")}));break;case 9:v.loadFocusElementList(b);var c=!1;a.shiftKey?(v.isFocusInFirstItem(a)||v.isModalFocused(a,b))&&(c=v.focusLastFocusableElement()):v.isFocusInLastItem(a)&&(c=v.focusFirstFocusableElement()),c&&(a.preventDefault(),a.stopPropagation())}}function o(a,b,c){return!a.value.modalScope.$broadcast("modal.closing",b,c).defaultPrevented}var p,q,r,s="modal-open",t=h.createNew(),u=g.createNew(),v={NOW_CLOSING_EVENT:"modal.stack.now-closing"},w=0,x="a[href], area[href], input:not([disabled]), button:not([disabled]),select:not([disabled]), textarea:not([disabled]), iframe, object, embed, *[tabindex], *[contenteditable=true]";return e.$watch(i,function(a){q&&(q.index=a)}),c.on("keydown",n),e.$on("$destroy",function(){c.off("keydown",n)}),v.open=function(b,f){var g=c[0].activeElement,h=f.openedClass||s;k(!1),t.add(b,{deferred:f.deferred,renderDeferred:f.renderDeferred,closedDeferred:f.closedDeferred,modalScope:f.scope,backdrop:f.backdrop,keyboard:f.keyboard,openedClass:f.openedClass,windowTopClass:f.windowTopClass,animation:f.animation,appendTo:f.appendTo}),u.put(h,b);var j=f.appendTo,l=i();if(!j.length)throw new Error("appendTo element not found. Make sure that the element passed is in DOM.");l>=0&&!p&&(q=e.$new(!0),q.modalOptions=f,q.index=l,p=angular.element('
    '),p.attr("backdrop-class",f.backdropClass),f.animation&&p.attr("modal-animation","true"),d(p)(q),a.enter(p,j));var m=angular.element('
    ');m.attr({"template-url":f.windowTemplateUrl,"window-class":f.windowClass,"window-top-class":f.windowTopClass,size:f.size,index:t.length()-1,animate:"animate"}).html(f.content),f.animation&&m.attr("modal-animation","true"),a.enter(d(m)(f.scope),j).then(function(){f.scope.$$uibDestructionScheduled||a.addClass(j,h)}),t.top().value.modalDomEl=m,t.top().value.modalOpener=g,v.clearFocusListCache()},v.close=function(a,b){var c=t.get(a);return c&&o(c,b,!0)?(c.value.modalScope.$$uibDestructionScheduled=!0,c.value.deferred.resolve(b),j(a,c.value.modalOpener),!0):!c},v.dismiss=function(a,b){var c=t.get(a);return c&&o(c,b,!1)?(c.value.modalScope.$$uibDestructionScheduled=!0,c.value.deferred.reject(b),j(a,c.value.modalOpener),!0):!c},v.dismissAll=function(a){for(var b=this.getTop();b&&this.dismiss(b.key,a);)b=this.getTop()},v.getTop=function(){return t.top()},v.modalRendered=function(a){var b=t.get(a);b&&b.value.renderDeferred.resolve()},v.focusFirstFocusableElement=function(){return r.length>0?(r[0].focus(),!0):!1},v.focusLastFocusableElement=function(){return r.length>0?(r[r.length-1].focus(),!0):!1},v.isModalFocused=function(a,b){if(a&&b){var c=b.value.modalDomEl;if(c&&c.length)return(a.target||a.srcElement)===c[0]}return!1},v.isFocusInFirstItem=function(a){return r.length>0?(a.target||a.srcElement)===r[0]:!1},v.isFocusInLastItem=function(a){return r.length>0?(a.target||a.srcElement)===r[r.length-1]:!1},v.clearFocusListCache=function(){r=[],w=0},v.loadFocusElementList=function(a){if((void 0===r||!r.length)&&a){var b=a.value.modalDomEl;b&&b.length&&(r=b[0].querySelectorAll(x))}},v}]).provider("$uibModal",function(){var a={options:{animation:!0,backdrop:!0,keyboard:!0},$get:["$rootScope","$q","$document","$templateRequest","$controller","$uibResolve","$uibModalStack",function(b,c,d,e,f,g,h){function i(a){return a.template?c.when(a.template):e(angular.isFunction(a.templateUrl)?a.templateUrl():a.templateUrl)}var j={},k=null;return j.getPromiseChain=function(){return k},j.open=function(e){function j(){return r}var l=c.defer(),m=c.defer(),n=c.defer(),o=c.defer(),p={result:l.promise,opened:m.promise,closed:n.promise,rendered:o.promise,close:function(a){return h.close(p,a)},dismiss:function(a){return h.dismiss(p,a)}};if(e=angular.extend({},a.options,e),e.resolve=e.resolve||{},e.appendTo=e.appendTo||d.find("body").eq(0),!e.template&&!e.templateUrl)throw new Error("One of template or templateUrl options is required.");var q,r=c.all([i(e),g.resolve(e.resolve,{},null,null)]);return q=k=c.all([k]).then(j,j).then(function(a){var c=e.scope||b,d=c.$new();d.$close=p.close,d.$dismiss=p.dismiss,d.$on("$destroy",function(){d.$$uibDestructionScheduled||d.$dismiss("$uibUnscheduledDestruction")});var g,i,j={};e.controller&&(j.$scope=d,j.$uibModalInstance=p,angular.forEach(a[1],function(a,b){j[b]=a}),i=f(e.controller,j,!0),e.controllerAs?(g=i.instance,e.bindToController&&(g.$close=d.$close,g.$dismiss=d.$dismiss,angular.extend(g,c)),g=i(),d[e.controllerAs]=g):g=i(),angular.isFunction(g.$onInit)&&g.$onInit()),h.open(p,{scope:d,deferred:l,renderDeferred:o,closedDeferred:n,content:a[0],animation:e.animation,backdrop:e.backdrop,keyboard:e.keyboard,backdropClass:e.backdropClass,windowTopClass:e.windowTopClass,windowClass:e.windowClass,windowTemplateUrl:e.windowTemplateUrl,size:e.size,openedClass:e.openedClass,appendTo:e.appendTo}),m.resolve(!0)},function(a){m.reject(a),l.reject(a)})["finally"](function(){k===q&&(k=null)}),p},j}]};return a}),angular.module("ui.bootstrap.paging",[]).factory("uibPaging",["$parse",function(a){return{create:function(b,c,d){b.setNumPages=d.numPages?a(d.numPages).assign:angular.noop,b.ngModelCtrl={$setViewValue:angular.noop},b._watchers=[],b.init=function(a,e){b.ngModelCtrl=a,b.config=e,a.$render=function(){b.render()},d.itemsPerPage?b._watchers.push(c.$parent.$watch(d.itemsPerPage,function(a){b.itemsPerPage=parseInt(a,10),c.totalPages=b.calculateTotalPages(),b.updatePage()})):b.itemsPerPage=e.itemsPerPage,c.$watch("totalItems",function(a,d){(angular.isDefined(a)||a!==d)&&(c.totalPages=b.calculateTotalPages(),b.updatePage())})},b.calculateTotalPages=function(){var a=b.itemsPerPage<1?1:Math.ceil(c.totalItems/b.itemsPerPage);return Math.max(a||0,1)},b.render=function(){c.page=parseInt(b.ngModelCtrl.$viewValue,10)||1},c.selectPage=function(a,d){d&&d.preventDefault();var e=!c.ngDisabled||!d;e&&c.page!==a&&a>0&&a<=c.totalPages&&(d&&d.target&&d.target.blur(),b.ngModelCtrl.$setViewValue(a),b.ngModelCtrl.$render())},c.getText=function(a){return c[a+"Text"]||b.config[a+"Text"]},c.noPrevious=function(){return 1===c.page},c.noNext=function(){return c.page===c.totalPages},b.updatePage=function(){b.setNumPages(c.$parent,c.totalPages),c.page>c.totalPages?c.selectPage(c.totalPages):b.ngModelCtrl.$render()},c.$on("$destroy",function(){for(;b._watchers.length;)b._watchers.shift()()})}}}]),angular.module("ui.bootstrap.pager",["ui.bootstrap.paging"]).controller("UibPagerController",["$scope","$attrs","uibPaging","uibPagerConfig",function(a,b,c,d){a.align=angular.isDefined(b.align)?a.$parent.$eval(b.align):d.align,c.create(this,a,b)}]).constant("uibPagerConfig",{itemsPerPage:10,previousText:"« Previous",nextText:"Next »",align:!0}).directive("uibPager",["uibPagerConfig",function(a){return{scope:{totalItems:"=",previousText:"@",nextText:"@",ngDisabled:"="},require:["uibPager","?ngModel"],controller:"UibPagerController",controllerAs:"pager",templateUrl:function(a,b){return b.templateUrl||"uib/template/pager/pager.html"},replace:!0,link:function(b,c,d,e){var f=e[0],g=e[1];g&&f.init(g,a)}}}]),angular.module("ui.bootstrap.pagination",["ui.bootstrap.paging"]).controller("UibPaginationController",["$scope","$attrs","$parse","uibPaging","uibPaginationConfig",function(a,b,c,d,e){function f(a,b,c){return{number:a,text:b,active:c}}function g(a,b){var c=[],d=1,e=b,g=angular.isDefined(i)&&b>i;g&&(j?(d=Math.max(a-Math.floor(i/2),1),e=d+i-1,e>b&&(e=b,d=e-i+1)):(d=(Math.ceil(a/i)-1)*i+1,e=Math.min(d+i-1,b)));for(var h=d;e>=h;h++){var n=f(h,m(h),h===a);c.push(n)}if(g&&i>0&&(!j||k||l)){if(d>1){if(!l||d>3){var o=f(d-1,"...",!1);c.unshift(o)}if(l){if(3===d){var p=f(2,"2",!1);c.unshift(p)}var q=f(1,"1",!1); +c.unshift(q)}}if(b>e){if(!l||b-2>e){var r=f(e+1,"...",!1);c.push(r)}if(l){if(e===b-2){var s=f(b-1,b-1,!1);c.push(s)}var t=f(b,b,!1);c.push(t)}}}return c}var h=this,i=angular.isDefined(b.maxSize)?a.$parent.$eval(b.maxSize):e.maxSize,j=angular.isDefined(b.rotate)?a.$parent.$eval(b.rotate):e.rotate,k=angular.isDefined(b.forceEllipses)?a.$parent.$eval(b.forceEllipses):e.forceEllipses,l=angular.isDefined(b.boundaryLinkNumbers)?a.$parent.$eval(b.boundaryLinkNumbers):e.boundaryLinkNumbers,m=angular.isDefined(b.pageLabel)?function(c){return a.$parent.$eval(b.pageLabel,{$page:c})}:angular.identity;a.boundaryLinks=angular.isDefined(b.boundaryLinks)?a.$parent.$eval(b.boundaryLinks):e.boundaryLinks,a.directionLinks=angular.isDefined(b.directionLinks)?a.$parent.$eval(b.directionLinks):e.directionLinks,d.create(this,a,b),b.maxSize&&h._watchers.push(a.$parent.$watch(c(b.maxSize),function(a){i=parseInt(a,10),h.render()}));var n=this.render;this.render=function(){n(),a.page>0&&a.page<=a.totalPages&&(a.pages=g(a.page,a.totalPages))}}]).constant("uibPaginationConfig",{itemsPerPage:10,boundaryLinks:!1,boundaryLinkNumbers:!1,directionLinks:!0,firstText:"First",previousText:"Previous",nextText:"Next",lastText:"Last",rotate:!0,forceEllipses:!1}).directive("uibPagination",["$parse","uibPaginationConfig",function(a,b){return{scope:{totalItems:"=",firstText:"@",previousText:"@",nextText:"@",lastText:"@",ngDisabled:"="},require:["uibPagination","?ngModel"],controller:"UibPaginationController",controllerAs:"pagination",templateUrl:function(a,b){return b.templateUrl||"uib/template/pagination/pagination.html"},replace:!0,link:function(a,c,d,e){var f=e[0],g=e[1];g&&f.init(g,b)}}}]),angular.module("ui.bootstrap.tooltip",["ui.bootstrap.position","ui.bootstrap.stackedMap"]).provider("$uibTooltip",function(){function a(a){var b=/[A-Z]/g,c="-";return a.replace(b,function(a,b){return(b?c:"")+a.toLowerCase()})}var b={placement:"top",placementClassPrefix:"",animation:!0,popupDelay:0,popupCloseDelay:0,useContentExp:!1},c={mouseenter:"mouseleave",click:"click",outsideClick:"outsideClick",focus:"blur",none:""},d={};this.options=function(a){angular.extend(d,a)},this.setTriggers=function(a){angular.extend(c,a)},this.$get=["$window","$compile","$timeout","$document","$uibPosition","$interpolate","$rootScope","$parse","$$stackedMap",function(e,f,g,h,i,j,k,l,m){function n(a){if(27===a.which){var b=o.top();b&&(b.value.close(),o.removeTop(),b=null)}}var o=m.createNew();return h.on("keypress",n),k.$on("$destroy",function(){h.off("keypress",n)}),function(e,k,m,n){function p(a){var b=(a||n.trigger||m).split(" "),d=b.map(function(a){return c[a]||a});return{show:b,hide:d}}n=angular.extend({},b,d,n);var q=a(e),r=j.startSymbol(),s=j.endSymbol(),t="
    ';return{compile:function(a,b){var c=f(t);return function(a,b,d,f){function j(){N.isOpen?q():m()}function m(){(!M||a.$eval(d[k+"Enable"]))&&(u(),x(),N.popupDelay?G||(G=g(r,N.popupDelay,!1)):r())}function q(){s(),N.popupCloseDelay?H||(H=g(t,N.popupCloseDelay,!1)):t()}function r(){return s(),u(),N.content?(v(),void N.$evalAsync(function(){N.isOpen=!0,y(!0),S()})):angular.noop}function s(){G&&(g.cancel(G),G=null),I&&(g.cancel(I),I=null)}function t(){N&&N.$evalAsync(function(){N&&(N.isOpen=!1,y(!1),N.animation?F||(F=g(w,150,!1)):w())})}function u(){H&&(g.cancel(H),H=null),F&&(g.cancel(F),F=null)}function v(){D||(E=N.$new(),D=c(E,function(a){K?h.find("body").append(a):b.after(a)}),z())}function w(){s(),u(),A(),D&&(D.remove(),D=null),E&&(E.$destroy(),E=null)}function x(){N.title=d[k+"Title"],Q?N.content=Q(a):N.content=d[e],N.popupClass=d[k+"Class"],N.placement=angular.isDefined(d[k+"Placement"])?d[k+"Placement"]:n.placement;var b=i.parsePlacement(N.placement);J=b[1]?b[0]+"-"+b[1]:b[0];var c=parseInt(d[k+"PopupDelay"],10),f=parseInt(d[k+"PopupCloseDelay"],10);N.popupDelay=isNaN(c)?n.popupDelay:c,N.popupCloseDelay=isNaN(f)?n.popupCloseDelay:f}function y(b){P&&angular.isFunction(P.assign)&&P.assign(a,b)}function z(){R.length=0,Q?(R.push(a.$watch(Q,function(a){N.content=a,!a&&N.isOpen&&t()})),R.push(E.$watch(function(){O||(O=!0,E.$$postDigest(function(){O=!1,N&&N.isOpen&&S()}))}))):R.push(d.$observe(e,function(a){N.content=a,!a&&N.isOpen?t():S()})),R.push(d.$observe(k+"Title",function(a){N.title=a,N.isOpen&&S()})),R.push(d.$observe(k+"Placement",function(a){N.placement=a?a:n.placement;var b=i.parsePlacement(N.placement);J=b[1]?b[0]+"-"+b[1]:b[0],N.isOpen&&S()}))}function A(){R.length&&(angular.forEach(R,function(a){a()}),R.length=0)}function B(a){N&&N.isOpen&&D&&(b[0].contains(a.target)||D[0].contains(a.target)||q())}function C(){var a=d[k+"Trigger"];T(),L=p(a),"none"!==L.show&&L.show.forEach(function(a,c){"outsideClick"===a?(b.on("click",j),h.on("click",B)):a===L.hide[c]?b.on(a,j):a&&(b.on(a,m),b.on(L.hide[c],q)),b.on("keypress",function(a){27===a.which&&q()})})}var D,E,F,G,H,I,J,K=angular.isDefined(n.appendToBody)?n.appendToBody:!1,L=p(void 0),M=angular.isDefined(d[k+"Enable"]),N=a.$new(!0),O=!1,P=angular.isDefined(d[k+"IsOpen"])?l(d[k+"IsOpen"]):!1,Q=n.useContentExp?l(d[e]):!1,R=[],S=function(){D&&D.html()&&(I||(I=g(function(){var a=i.positionElements(b,D,N.placement,K);D.css({top:a.top+"px",left:a.left+"px"}),D.hasClass(a.placement.split("-")[0])||(D.removeClass(J.split("-")[0]),D.addClass(a.placement.split("-")[0])),D.hasClass(n.placementClassPrefix+a.placement)||(D.removeClass(n.placementClassPrefix+J),D.addClass(n.placementClassPrefix+a.placement)),D.hasClass("uib-position-measure")?(i.positionArrow(D,a.placement),D.removeClass("uib-position-measure")):J!==a.placement&&i.positionArrow(D,a.placement),J=a.placement,I=null},0,!1)))};N.origScope=a,N.isOpen=!1,o.add(N,{close:t}),N.contentExp=function(){return N.content},d.$observe("disabled",function(a){a&&s(),a&&N.isOpen&&t()}),P&&a.$watch(P,function(a){N&&!a===N.isOpen&&j()});var T=function(){L.show.forEach(function(a){"outsideClick"===a?b.off("click",j):(b.off(a,m),b.off(a,j))}),L.hide.forEach(function(a){"outsideClick"===a?h.off("click",B):b.off(a,q)})};C();var U=a.$eval(d[k+"Animation"]);N.animation=angular.isDefined(U)?!!U:n.animation;var V,W=k+"AppendToBody";V=W in d&&void 0===d[W]?!0:a.$eval(d[W]),K=angular.isDefined(V)?V:K,a.$on("$destroy",function(){T(),w(),o.remove(N),N=null})}}}}}]}).directive("uibTooltipTemplateTransclude",["$animate","$sce","$compile","$templateRequest",function(a,b,c,d){return{link:function(e,f,g){var h,i,j,k=e.$eval(g.tooltipTemplateTranscludeScope),l=0,m=function(){i&&(i.remove(),i=null),h&&(h.$destroy(),h=null),j&&(a.leave(j).then(function(){i=null}),i=j,j=null)};e.$watch(b.parseAsResourceUrl(g.uibTooltipTemplateTransclude),function(b){var g=++l;b?(d(b,!0).then(function(d){if(g===l){var e=k.$new(),i=d,n=c(i)(e,function(b){m(),a.enter(b,f)});h=e,j=n,h.$emit("$includeContentLoaded",b)}},function(){g===l&&(m(),e.$emit("$includeContentError",b))}),e.$emit("$includeContentRequested",b)):m()}),e.$on("$destroy",m)}}}]).directive("uibTooltipClasses",["$uibPosition",function(a){return{restrict:"A",link:function(b,c,d){if(b.placement){var e=a.parsePlacement(b.placement);c.addClass(e[0])}b.popupClass&&c.addClass(b.popupClass),b.animation()&&c.addClass(d.tooltipAnimationClass)}}}]).directive("uibTooltipPopup",function(){return{replace:!0,scope:{content:"@",placement:"@",popupClass:"@",animation:"&",isOpen:"&"},templateUrl:"uib/template/tooltip/tooltip-popup.html"}}).directive("uibTooltip",["$uibTooltip",function(a){return a("uibTooltip","tooltip","mouseenter")}]).directive("uibTooltipTemplatePopup",function(){return{replace:!0,scope:{contentExp:"&",placement:"@",popupClass:"@",animation:"&",isOpen:"&",originScope:"&"},templateUrl:"uib/template/tooltip/tooltip-template-popup.html"}}).directive("uibTooltipTemplate",["$uibTooltip",function(a){return a("uibTooltipTemplate","tooltip","mouseenter",{useContentExp:!0})}]).directive("uibTooltipHtmlPopup",function(){return{replace:!0,scope:{contentExp:"&",placement:"@",popupClass:"@",animation:"&",isOpen:"&"},templateUrl:"uib/template/tooltip/tooltip-html-popup.html"}}).directive("uibTooltipHtml",["$uibTooltip",function(a){return a("uibTooltipHtml","tooltip","mouseenter",{useContentExp:!0})}]),angular.module("ui.bootstrap.popover",["ui.bootstrap.tooltip"]).directive("uibPopoverTemplatePopup",function(){return{replace:!0,scope:{title:"@",contentExp:"&",placement:"@",popupClass:"@",animation:"&",isOpen:"&",originScope:"&"},templateUrl:"uib/template/popover/popover-template.html"}}).directive("uibPopoverTemplate",["$uibTooltip",function(a){return a("uibPopoverTemplate","popover","click",{useContentExp:!0})}]).directive("uibPopoverHtmlPopup",function(){return{replace:!0,scope:{contentExp:"&",title:"@",placement:"@",popupClass:"@",animation:"&",isOpen:"&"},templateUrl:"uib/template/popover/popover-html.html"}}).directive("uibPopoverHtml",["$uibTooltip",function(a){return a("uibPopoverHtml","popover","click",{useContentExp:!0})}]).directive("uibPopoverPopup",function(){return{replace:!0,scope:{title:"@",content:"@",placement:"@",popupClass:"@",animation:"&",isOpen:"&"},templateUrl:"uib/template/popover/popover.html"}}).directive("uibPopover",["$uibTooltip",function(a){return a("uibPopover","popover","click")}]),angular.module("ui.bootstrap.progressbar",[]).constant("uibProgressConfig",{animate:!0,max:100}).controller("UibProgressController",["$scope","$attrs","uibProgressConfig",function(a,b,c){function d(){return angular.isDefined(a.maxParam)?a.maxParam:c.max}var e=this,f=angular.isDefined(b.animate)?a.$parent.$eval(b.animate):c.animate;this.bars=[],a.max=d(),this.addBar=function(a,b,c){f||b.css({transition:"none"}),this.bars.push(a),a.max=d(),a.title=c&&angular.isDefined(c.title)?c.title:"progressbar",a.$watch("value",function(b){a.recalculatePercentage()}),a.recalculatePercentage=function(){var b=e.bars.reduce(function(a,b){return b.percent=+(100*b.value/b.max).toFixed(2),a+b.percent},0);b>100&&(a.percent-=b-100)},a.$on("$destroy",function(){b=null,e.removeBar(a)})},this.removeBar=function(a){this.bars.splice(this.bars.indexOf(a),1),this.bars.forEach(function(a){a.recalculatePercentage()})},a.$watch("maxParam",function(a){e.bars.forEach(function(a){a.max=d(),a.recalculatePercentage()})})}]).directive("uibProgress",function(){return{replace:!0,transclude:!0,controller:"UibProgressController",require:"uibProgress",scope:{maxParam:"=?max"},templateUrl:"uib/template/progressbar/progress.html"}}).directive("uibBar",function(){return{replace:!0,transclude:!0,require:"^uibProgress",scope:{value:"=",type:"@"},templateUrl:"uib/template/progressbar/bar.html",link:function(a,b,c,d){d.addBar(a,b,c)}}}).directive("uibProgressbar",function(){return{replace:!0,transclude:!0,controller:"UibProgressController",scope:{value:"=",maxParam:"=?max",type:"@"},templateUrl:"uib/template/progressbar/progressbar.html",link:function(a,b,c,d){d.addBar(a,angular.element(b.children()[0]),{title:c.title})}}}),angular.module("ui.bootstrap.rating",[]).constant("uibRatingConfig",{max:5,stateOn:null,stateOff:null,titles:["one","two","three","four","five"]}).controller("UibRatingController",["$scope","$attrs","uibRatingConfig",function(a,b,c){var d={$setViewValue:angular.noop},e=this;this.init=function(e){d=e,d.$render=this.render,d.$formatters.push(function(a){return angular.isNumber(a)&&a<<0!==a&&(a=Math.round(a)),a}),this.stateOn=angular.isDefined(b.stateOn)?a.$parent.$eval(b.stateOn):c.stateOn,this.stateOff=angular.isDefined(b.stateOff)?a.$parent.$eval(b.stateOff):c.stateOff;var f=angular.isDefined(b.titles)?a.$parent.$eval(b.titles):c.titles;this.titles=angular.isArray(f)&&f.length>0?f:c.titles;var g=angular.isDefined(b.ratingStates)?a.$parent.$eval(b.ratingStates):new Array(angular.isDefined(b.max)?a.$parent.$eval(b.max):c.max);a.range=this.buildTemplateObjects(g)},this.buildTemplateObjects=function(a){for(var b=0,c=a.length;c>b;b++)a[b]=angular.extend({index:b},{stateOn:this.stateOn,stateOff:this.stateOff,title:this.getTitle(b)},a[b]);return a},this.getTitle=function(a){return a>=this.titles.length?a+1:this.titles[a]},a.rate=function(b){!a.readonly&&b>=0&&b<=a.range.length&&(d.$setViewValue(d.$viewValue===b?0:b),d.$render())},a.enter=function(b){a.readonly||(a.value=b),a.onHover({value:b})},a.reset=function(){a.value=d.$viewValue,a.onLeave()},a.onKeydown=function(b){/(37|38|39|40)/.test(b.which)&&(b.preventDefault(),b.stopPropagation(),a.rate(a.value+(38===b.which||39===b.which?1:-1)))},this.render=function(){a.value=d.$viewValue,a.title=e.getTitle(a.value-1)}}]).directive("uibRating",function(){return{require:["uibRating","ngModel"],scope:{readonly:"=?readOnly",onHover:"&",onLeave:"&"},controller:"UibRatingController",templateUrl:"uib/template/rating/rating.html",replace:!0,link:function(a,b,c,d){var e=d[0],f=d[1];e.init(f)}}}),angular.module("ui.bootstrap.tabs",[]).controller("UibTabsetController",["$scope",function(a){function b(a){for(var b=0;bb.index?1:a.index0&&13>b:b>=0&&24>b;return c?(a.showMeridian&&(12===b&&(b=0),a.meridian===u[1]&&(b+=12)),b):void 0}function i(){var b=+a.minutes;return b>=0&&60>b?b:void 0}function j(){var b=+a.seconds;return b>=0&&60>b?b:void 0}function k(a){return null===a?"":angular.isDefined(a)&&a.toString().length<2?"0"+a:a.toString()}function l(a){m(),t.$setViewValue(new Date(r)),n(a)}function m(){t.$setValidity("time",!0),a.invalidHours=!1,a.invalidMinutes=!1,a.invalidSeconds=!1}function n(b){if(t.$modelValue){var c=r.getHours(),d=r.getMinutes(),e=r.getSeconds();a.showMeridian&&(c=0===c||12===c?12:c%12),a.hours="h"===b?c:k(c),"m"!==b&&(a.minutes=k(d)),a.meridian=r.getHours()<12?u[0]:u[1],"s"!==b&&(a.seconds=k(e)),a.meridian=r.getHours()<12?u[0]:u[1]}else a.hours=null,a.minutes=null,a.seconds=null,a.meridian=u[0]}function o(a){r=q(r,a),l()}function p(a,b){return q(a,60*b)}function q(a,b){var c=new Date(a.getTime()+1e3*b),d=new Date(a);return d.setHours(c.getHours(),c.getMinutes(),c.getSeconds()),d}var r=new Date,s=[],t={$setViewValue:angular.noop},u=angular.isDefined(c.meridians)?a.$parent.$eval(c.meridians):g.meridians||f.DATETIME_FORMATS.AMPMS;a.tabindex=angular.isDefined(c.tabindex)?c.tabindex:0,b.removeAttr("tabindex"),this.init=function(b,d){t=b,t.$render=this.render,t.$formatters.unshift(function(a){return a?new Date(a):null});var e=d.eq(0),f=d.eq(1),h=d.eq(2),i=angular.isDefined(c.mousewheel)?a.$parent.$eval(c.mousewheel):g.mousewheel;i&&this.setupMousewheelEvents(e,f,h);var j=angular.isDefined(c.arrowkeys)?a.$parent.$eval(c.arrowkeys):g.arrowkeys;j&&this.setupArrowkeyEvents(e,f,h),a.readonlyInput=angular.isDefined(c.readonlyInput)?a.$parent.$eval(c.readonlyInput):g.readonlyInput,this.setupInputEvents(e,f,h)};var v=g.hourStep;c.hourStep&&s.push(a.$parent.$watch(d(c.hourStep),function(a){v=+a}));var w=g.minuteStep;c.minuteStep&&s.push(a.$parent.$watch(d(c.minuteStep),function(a){w=+a}));var x;s.push(a.$parent.$watch(d(c.min),function(a){var b=new Date(a);x=isNaN(b)?void 0:b}));var y;s.push(a.$parent.$watch(d(c.max),function(a){var b=new Date(a);y=isNaN(b)?void 0:b}));var z=!1;c.ngDisabled&&s.push(a.$parent.$watch(d(c.ngDisabled),function(a){z=a})),a.noIncrementHours=function(){var a=p(r,60*v);return z||a>y||r>a&&x>a},a.noDecrementHours=function(){var a=p(r,60*-v);return z||x>a||a>r&&a>y},a.noIncrementMinutes=function(){var a=p(r,w);return z||a>y||r>a&&x>a},a.noDecrementMinutes=function(){var a=p(r,-w);return z||x>a||a>r&&a>y},a.noIncrementSeconds=function(){var a=q(r,A);return z||a>y||r>a&&x>a},a.noDecrementSeconds=function(){var a=q(r,-A);return z||x>a||a>r&&a>y},a.noToggleMeridian=function(){return r.getHours()<12?z||p(r,720)>y:z||p(r,-720)0};b.bind("mousewheel wheel",function(b){z||a.$apply(e(b)?a.incrementHours():a.decrementHours()),b.preventDefault()}),c.bind("mousewheel wheel",function(b){z||a.$apply(e(b)?a.incrementMinutes():a.decrementMinutes()),b.preventDefault()}),d.bind("mousewheel wheel",function(b){z||a.$apply(e(b)?a.incrementSeconds():a.decrementSeconds()),b.preventDefault()})},this.setupArrowkeyEvents=function(b,c,d){b.bind("keydown",function(b){z||(38===b.which?(b.preventDefault(),a.incrementHours(),a.$apply()):40===b.which&&(b.preventDefault(),a.decrementHours(),a.$apply()))}),c.bind("keydown",function(b){z||(38===b.which?(b.preventDefault(),a.incrementMinutes(),a.$apply()):40===b.which&&(b.preventDefault(),a.decrementMinutes(),a.$apply()))}),d.bind("keydown",function(b){z||(38===b.which?(b.preventDefault(),a.incrementSeconds(),a.$apply()):40===b.which&&(b.preventDefault(),a.decrementSeconds(),a.$apply()))})},this.setupInputEvents=function(b,c,d){if(a.readonlyInput)return a.updateHours=angular.noop,a.updateMinutes=angular.noop,void(a.updateSeconds=angular.noop);var e=function(b,c,d){t.$setViewValue(null),t.$setValidity("time",!1),angular.isDefined(b)&&(a.invalidHours=b),angular.isDefined(c)&&(a.invalidMinutes=c),angular.isDefined(d)&&(a.invalidSeconds=d)};a.updateHours=function(){var a=h(),b=i();t.$setDirty(),angular.isDefined(a)&&angular.isDefined(b)?(r.setHours(a),r.setMinutes(b),x>r||r>y?e(!0):l("h")):e(!0)},b.bind("blur",function(b){t.$setTouched(),null===a.hours||""===a.hours?e(!0):!a.invalidHours&&a.hours<10&&a.$apply(function(){a.hours=k(a.hours)})}),a.updateMinutes=function(){var a=i(),b=h();t.$setDirty(),angular.isDefined(a)&&angular.isDefined(b)?(r.setHours(b),r.setMinutes(a),x>r||r>y?e(void 0,!0):l("m")):e(void 0,!0)},c.bind("blur",function(b){t.$setTouched(),null===a.minutes?e(void 0,!0):!a.invalidMinutes&&a.minutes<10&&a.$apply(function(){a.minutes=k(a.minutes)})}),a.updateSeconds=function(){var a=j();t.$setDirty(),angular.isDefined(a)?(r.setSeconds(a),l("s")):e(void 0,void 0,!0)},d.bind("blur",function(b){!a.invalidSeconds&&a.seconds<10&&a.$apply(function(){a.seconds=k(a.seconds)})})},this.render=function(){var b=t.$viewValue;isNaN(b)?(t.$setValidity("time",!1),e.error('Timepicker directive: "ng-model" value must be a Date object, a number of milliseconds since 01.01.1970 or a string representing an RFC2822 or ISO 8601 date.')):(b&&(r=b),x>r||r>y?(t.$setValidity("time",!1),a.invalidHours=!0,a.invalidMinutes=!0):m(),n())},a.showSpinners=angular.isDefined(c.showSpinners)?a.$parent.$eval(c.showSpinners):g.showSpinners,a.incrementHours=function(){a.noIncrementHours()||o(60*v*60)},a.decrementHours=function(){a.noDecrementHours()||o(60*-v*60)},a.incrementMinutes=function(){a.noIncrementMinutes()||o(60*w)},a.decrementMinutes=function(){a.noDecrementMinutes()||o(60*-w)},a.incrementSeconds=function(){a.noIncrementSeconds()||o(A)},a.decrementSeconds=function(){a.noDecrementSeconds()||o(-A)},a.toggleMeridian=function(){var b=i(),c=h();a.noToggleMeridian()||(angular.isDefined(b)&&angular.isDefined(c)?o(720*(r.getHours()<12?60:-60)):a.meridian=a.meridian===u[0]?u[1]:u[0])},a.blur=function(){t.$setTouched()},a.$on("$destroy",function(){for(;s.length;)s.shift()()})}]).directive("uibTimepicker",["uibTimepickerConfig",function(a){return{require:["uibTimepicker","?^ngModel"],controller:"UibTimepickerController",controllerAs:"timepicker",replace:!0,scope:{},templateUrl:function(b,c){return c.templateUrl||a.templateUrl},link:function(a,b,c,d){var e=d[0],f=d[1];f&&e.init(f,b.find("input"))}}}]),angular.module("ui.bootstrap.typeahead",["ui.bootstrap.debounce","ui.bootstrap.position"]).factory("uibTypeaheadParser",["$parse",function(a){var b=/^\s*([\s\S]+?)(?:\s+as\s+([\s\S]+?))?\s+for\s+(?:([\$\w][\$\w\d]*))\s+in\s+([\s\S]+?)$/;return{parse:function(c){var d=c.match(b);if(!d)throw new Error('Expected typeahead specification in form of "_modelValue_ (as _label_)? for _item_ in _collection_" but got "'+c+'".');return{itemName:d[3],source:a(d[4]),viewMapper:a(d[2]||d[1]),modelMapper:a(d[1])}}}}]).controller("UibTypeaheadController",["$scope","$element","$attrs","$compile","$parse","$q","$timeout","$document","$window","$rootScope","$$debounce","$uibPosition","uibTypeaheadParser",function(a,b,c,d,e,f,g,h,i,j,k,l,m){function n(){N.moveInProgress||(N.moveInProgress=!0,N.$digest()),Y()}function o(){N.position=D?l.offset(b):l.position(b),N.position.top+=b.prop("offsetHeight")}var p,q,r=[9,13,27,38,40],s=200,t=a.$eval(c.typeaheadMinLength);t||0===t||(t=1),a.$watch(c.typeaheadMinLength,function(a){t=a||0===a?a:1});var u=a.$eval(c.typeaheadWaitMs)||0,v=a.$eval(c.typeaheadEditable)!==!1;a.$watch(c.typeaheadEditable,function(a){v=a!==!1});var w,x,y=e(c.typeaheadLoading).assign||angular.noop,z=e(c.typeaheadOnSelect),A=angular.isDefined(c.typeaheadSelectOnBlur)?a.$eval(c.typeaheadSelectOnBlur):!1,B=e(c.typeaheadNoResults).assign||angular.noop,C=c.typeaheadInputFormatter?e(c.typeaheadInputFormatter):void 0,D=c.typeaheadAppendToBody?a.$eval(c.typeaheadAppendToBody):!1,E=c.typeaheadAppendTo?a.$eval(c.typeaheadAppendTo):null,F=a.$eval(c.typeaheadFocusFirst)!==!1,G=c.typeaheadSelectOnExact?a.$eval(c.typeaheadSelectOnExact):!1,H=e(c.typeaheadIsOpen).assign||angular.noop,I=a.$eval(c.typeaheadShowHint)||!1,J=e(c.ngModel),K=e(c.ngModel+"($$$p)"),L=function(b,c){return angular.isFunction(J(a))&&q&&q.$options&&q.$options.getterSetter?K(b,{$$$p:c}):J.assign(b,c)},M=m.parse(c.uibTypeahead),N=a.$new(),O=a.$on("$destroy",function(){N.$destroy()});N.$on("$destroy",O);var P="typeahead-"+N.$id+"-"+Math.floor(1e4*Math.random());b.attr({"aria-autocomplete":"list","aria-expanded":!1,"aria-owns":P});var Q,R;I&&(Q=angular.element("
    "),Q.css("position","relative"),b.after(Q),R=b.clone(),R.attr("placeholder",""),R.attr("tabindex","-1"),R.val(""),R.css({position:"absolute",top:"0px",left:"0px","border-color":"transparent","box-shadow":"none",opacity:1,background:"none 0% 0% / auto repeat scroll padding-box border-box rgb(255, 255, 255)",color:"#999"}),b.css({position:"relative","vertical-align":"top","background-color":"transparent"}),Q.append(R),R.after(b));var S=angular.element("
    ");S.attr({id:P,matches:"matches",active:"activeIdx",select:"select(activeIdx, evt)","move-in-progress":"moveInProgress",query:"query",position:"position","assign-is-open":"assignIsOpen(isOpen)",debounce:"debounceUpdate"}),angular.isDefined(c.typeaheadTemplateUrl)&&S.attr("template-url",c.typeaheadTemplateUrl),angular.isDefined(c.typeaheadPopupTemplateUrl)&&S.attr("popup-template-url",c.typeaheadPopupTemplateUrl);var T=function(){I&&R.val("")},U=function(){N.matches=[],N.activeIdx=-1,b.attr("aria-expanded",!1),T()},V=function(a){return P+"-option-"+a};N.$watch("activeIdx",function(a){0>a?b.removeAttr("aria-activedescendant"):b.attr("aria-activedescendant",V(a))});var W=function(a,b){return N.matches.length>b&&a?a.toUpperCase()===N.matches[b].label.toUpperCase():!1},X=function(c,d){var e={$viewValue:c};y(a,!0),B(a,!1),f.when(M.source(a,e)).then(function(f){var g=c===p.$viewValue;if(g&&w)if(f&&f.length>0){N.activeIdx=F?0:-1,B(a,!1),N.matches.length=0;for(var h=0;h0&&i.slice(0,c.length).toUpperCase()===c.toUpperCase()?R.val(c+i.slice(c.length)):R.val("")}}else U(),B(a,!0);g&&y(a,!1)},function(){U(),y(a,!1),B(a,!0)})};D&&(angular.element(i).on("resize",n),h.find("body").on("scroll",n));var Y=k(function(){N.matches.length&&o(),N.moveInProgress=!1},s);N.moveInProgress=!1,N.query=void 0;var Z,$=function(a){Z=g(function(){X(a)},u)},_=function(){Z&&g.cancel(Z)};U(),N.assignIsOpen=function(b){H(a,b)},N.select=function(d,e){var f,h,i={};x=!0,i[M.itemName]=h=N.matches[d].model,f=M.modelMapper(a,i),L(a,f),p.$setValidity("editable",!0),p.$setValidity("parse",!0),z(a,{$item:h,$model:f,$label:M.viewMapper(a,i),$event:e}),U(),N.$eval(c.typeaheadFocusOnSelect)!==!1&&g(function(){b[0].focus()},0,!1)},b.on("keydown",function(b){if(0!==N.matches.length&&-1!==r.indexOf(b.which)){if(-1===N.activeIdx&&(9===b.which||13===b.which)||9===b.which&&b.shiftKey)return U(),void N.$digest();b.preventDefault();var c;switch(b.which){case 9:case 13:N.$apply(function(){angular.isNumber(N.debounceUpdate)||angular.isObject(N.debounceUpdate)?k(function(){N.select(N.activeIdx,b)},angular.isNumber(N.debounceUpdate)?N.debounceUpdate:N.debounceUpdate["default"]):N.select(N.activeIdx,b)});break;case 27:b.stopPropagation(),U(),a.$digest();break;case 38:N.activeIdx=(N.activeIdx>0?N.activeIdx:N.matches.length)-1,N.$digest(),c=S.find("li")[N.activeIdx],c.parentNode.scrollTop=c.offsetTop;break;case 40:N.activeIdx=(N.activeIdx+1)%N.matches.length,N.$digest(),c=S.find("li")[N.activeIdx],c.parentNode.scrollTop=c.offsetTop}}}),b.bind("focus",function(a){w=!0,0!==t||p.$viewValue||g(function(){X(p.$viewValue,a)},0)}),b.bind("blur",function(a){A&&N.matches.length&&-1!==N.activeIdx&&!x&&(x=!0,N.$apply(function(){angular.isObject(N.debounceUpdate)&&angular.isNumber(N.debounceUpdate.blur)?k(function(){N.select(N.activeIdx,a)},N.debounceUpdate.blur):N.select(N.activeIdx,a)})),!v&&p.$error.editable&&(p.$viewValue="",b.val("")),w=!1,x=!1});var aa=function(c){b[0]!==c.target&&3!==c.which&&0!==N.matches.length&&(U(),j.$$phase||a.$digest())};h.on("click",aa),a.$on("$destroy",function(){h.off("click",aa),(D||E)&&ba.remove(),D&&(angular.element(i).off("resize",n),h.find("body").off("scroll",n)),S.remove(),I&&Q.remove()});var ba=d(S)(N);D?h.find("body").append(ba):E?angular.element(E).eq(0).append(ba):b.after(ba),this.init=function(b,c){p=b,q=c,N.debounceUpdate=p.$options&&e(p.$options.debounce)(a),p.$parsers.unshift(function(b){return w=!0,0===t||b&&b.length>=t?u>0?(_(),$(b)):X(b):(y(a,!1),_(),U()),v?b:b?void p.$setValidity("editable",!1):(p.$setValidity("editable",!0),null)}),p.$formatters.push(function(b){var c,d,e={};return v||p.$setValidity("editable",!0),C?(e.$model=b,C(a,e)):(e[M.itemName]=b,c=M.viewMapper(a,e),e[M.itemName]=void 0,d=M.viewMapper(a,e),c!==d?c:b)})}}]).directive("uibTypeahead",function(){return{controller:"UibTypeaheadController",require:["ngModel","^?ngModelOptions","uibTypeahead"],link:function(a,b,c,d){d[2].init(d[0],d[1])}}}).directive("uibTypeaheadPopup",["$$debounce",function(a){return{scope:{matches:"=",query:"=",active:"=",position:"&",moveInProgress:"=",select:"&",assignIsOpen:"&",debounce:"&"},replace:!0,templateUrl:function(a,b){return b.popupTemplateUrl||"uib/template/typeahead/typeahead-popup.html"},link:function(b,c,d){b.templateUrl=d.templateUrl,b.isOpen=function(){var a=b.matches.length>0;return b.assignIsOpen({isOpen:a}),a},b.isActive=function(a){return b.active===a},b.selectActive=function(a){b.active=a},b.selectMatch=function(c,d){var e=b.debounce();angular.isNumber(e)||angular.isObject(e)?a(function(){b.select({activeIdx:c,evt:d})},angular.isNumber(e)?e:e["default"]):b.select({activeIdx:c,evt:d})}}}}]).directive("uibTypeaheadMatch",["$templateRequest","$compile","$parse",function(a,b,c){return{scope:{index:"=",match:"=",query:"="},link:function(d,e,f){var g=c(f.templateUrl)(d.$parent)||"uib/template/typeahead/typeahead-match.html";a(g).then(function(a){var c=angular.element(a.trim());e.replaceWith(c),b(c)(d)})}}}]).filter("uibTypeaheadHighlight",["$sce","$injector","$log",function(a,b,c){function d(a){return a.replace(/([.?*+^$[\]\\(){}|-])/g,"\\$1")}function e(a){return/<.*>/g.test(a)}var f;return f=b.has("$sanitize"),function(b,g){return!f&&e(b)&&c.warn("Unsafe use of typeahead please use ngSanitize"),b=g?(""+b).replace(new RegExp(d(g),"gi"),"$&"):b,f||(b=a.trustAsHtml(b)),b}}]),angular.module("uib/template/accordion/accordion-group.html",[]).run(["$templateCache",function(a){a.put("uib/template/accordion/accordion-group.html",'
    \n \n
    \n
    \n
    \n
    \n'); +}]),angular.module("uib/template/accordion/accordion.html",[]).run(["$templateCache",function(a){a.put("uib/template/accordion/accordion.html",'
    ')}]),angular.module("uib/template/alert/alert.html",[]).run(["$templateCache",function(a){a.put("uib/template/alert/alert.html",'\n')}]),angular.module("uib/template/carousel/carousel.html",[]).run(["$templateCache",function(a){a.put("uib/template/carousel/carousel.html",'\n')}]),angular.module("uib/template/carousel/slide.html",[]).run(["$templateCache",function(a){a.put("uib/template/carousel/slide.html",'
    \n')}]),angular.module("uib/template/datepicker/datepicker.html",[]).run(["$templateCache",function(a){a.put("uib/template/datepicker/datepicker.html",'
    \n \n \n \n
    \n')}]),angular.module("uib/template/datepicker/day.html",[]).run(["$templateCache",function(a){a.put("uib/template/datepicker/day.html",'\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
    {{::label.abbr}}
    {{ weekNumbers[$index] }}\n \n
    \n')}]),angular.module("uib/template/datepicker/month.html",[]).run(["$templateCache",function(a){a.put("uib/template/datepicker/month.html",'\n \n \n \n \n \n \n \n \n \n \n \n \n
    \n \n
    \n')}]),angular.module("uib/template/datepicker/popup.html",[]).run(["$templateCache",function(a){a.put("uib/template/datepicker/popup.html",'
    \n \n
    \n')}]),angular.module("uib/template/datepicker/year.html",[]).run(["$templateCache",function(a){a.put("uib/template/datepicker/year.html",'\n \n \n \n \n \n \n \n \n \n \n \n \n
    \n \n
    \n')}]),angular.module("uib/template/modal/backdrop.html",[]).run(["$templateCache",function(a){a.put("uib/template/modal/backdrop.html",'\n')}]),angular.module("uib/template/modal/window.html",[]).run(["$templateCache",function(a){a.put("uib/template/modal/window.html",'\n')}]),angular.module("uib/template/pager/pager.html",[]).run(["$templateCache",function(a){a.put("uib/template/pager/pager.html",'\n')}]),angular.module("uib/template/pagination/pager.html",[]).run(["$templateCache",function(a){a.put("uib/template/pagination/pager.html",'\n')}]),angular.module("uib/template/pagination/pagination.html",[]).run(["$templateCache",function(a){a.put("uib/template/pagination/pagination.html",'\n')}]),angular.module("uib/template/tooltip/tooltip-html-popup.html",[]).run(["$templateCache",function(a){a.put("uib/template/tooltip/tooltip-html-popup.html",'
    \n
    \n
    \n
    \n')}]),angular.module("uib/template/tooltip/tooltip-popup.html",[]).run(["$templateCache",function(a){a.put("uib/template/tooltip/tooltip-popup.html",'
    \n
    \n
    \n
    \n')}]),angular.module("uib/template/tooltip/tooltip-template-popup.html",[]).run(["$templateCache",function(a){a.put("uib/template/tooltip/tooltip-template-popup.html",'
    \n
    \n
    \n
    \n')}]),angular.module("uib/template/popover/popover-html.html",[]).run(["$templateCache",function(a){a.put("uib/template/popover/popover-html.html",'
    \n
    \n\n
    \n

    \n
    \n
    \n
    \n')}]),angular.module("uib/template/popover/popover-template.html",[]).run(["$templateCache",function(a){a.put("uib/template/popover/popover-template.html",'
    \n
    \n\n
    \n

    \n
    \n
    \n
    \n')}]),angular.module("uib/template/popover/popover.html",[]).run(["$templateCache",function(a){a.put("uib/template/popover/popover.html",'
    \n
    \n\n
    \n

    \n
    \n
    \n
    \n')}]),angular.module("uib/template/progressbar/bar.html",[]).run(["$templateCache",function(a){a.put("uib/template/progressbar/bar.html",'
    \n')}]),angular.module("uib/template/progressbar/progress.html",[]).run(["$templateCache",function(a){a.put("uib/template/progressbar/progress.html",'
    ')}]),angular.module("uib/template/progressbar/progressbar.html",[]).run(["$templateCache",function(a){a.put("uib/template/progressbar/progressbar.html",'
    \n
    \n
    \n')}]),angular.module("uib/template/rating/rating.html",[]).run(["$templateCache",function(a){a.put("uib/template/rating/rating.html",'\n ({{ $index < value ? \'*\' : \' \' }})\n \n\n')}]),angular.module("uib/template/tabs/tab.html",[]).run(["$templateCache",function(a){a.put("uib/template/tabs/tab.html",'\n')}]),angular.module("uib/template/tabs/tabset.html",[]).run(["$templateCache",function(a){a.put("uib/template/tabs/tabset.html",'
    \n \n
    \n
    \n
    \n
    \n
    \n')}]),angular.module("uib/template/timepicker/timepicker.html",[]).run(["$templateCache",function(a){a.put("uib/template/timepicker/timepicker.html",'\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
      
    \n \n :\n \n :\n \n
      
    \n')}]),angular.module("uib/template/typeahead/typeahead-match.html",[]).run(["$templateCache",function(a){a.put("uib/template/typeahead/typeahead-match.html",'\n')}]),angular.module("uib/template/typeahead/typeahead-popup.html",[]).run(["$templateCache",function(a){a.put("uib/template/typeahead/typeahead-popup.html",'\n')}]),angular.module("ui.bootstrap.carousel").run(function(){!angular.$$csp().noInlineStyle&&!angular.$$uibCarouselCss&&angular.element(document).find("head").prepend(''),angular.$$uibCarouselCss=!0}),angular.module("ui.bootstrap.position").run(function(){!angular.$$csp().noInlineStyle&&!angular.$$uibPositionCss&&angular.element(document).find("head").prepend(''),angular.$$uibPositionCss=!0}),angular.module("ui.bootstrap.datepicker").run(function(){!angular.$$csp().noInlineStyle&&!angular.$$uibDatepickerCss&&angular.element(document).find("head").prepend(''),angular.$$uibDatepickerCss=!0}),angular.module("ui.bootstrap.tooltip").run(function(){!angular.$$csp().noInlineStyle&&!angular.$$uibTooltipCss&&angular.element(document).find("head").prepend(''),angular.$$uibTooltipCss=!0}),angular.module("ui.bootstrap.timepicker").run(function(){!angular.$$csp().noInlineStyle&&!angular.$$uibTimepickerCss&&angular.element(document).find("head").prepend(''),angular.$$uibTimepickerCss=!0}),angular.module("ui.bootstrap.typeahead").run(function(){!angular.$$csp().noInlineStyle&&!angular.$$uibTypeaheadCss&&angular.element(document).find("head").prepend(''),angular.$$uibTypeaheadCss=!0}); \ No newline at end of file diff --git a/ntt_tool/static/partials/cloud/cloud.html b/ntt_tool/static/partials/cloud/cloud.html deleted file mode 100644 index f7b20e2..0000000 --- a/ntt_tool/static/partials/cloud/cloud.html +++ /dev/null @@ -1,33 +0,0 @@ - - -
    -
    -
    -

    {$ event | capitalize:true $} Cloud

    -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    - Cancel - -
    -
    -
    -
    \ No newline at end of file diff --git a/ntt_tool/static/partials/cloud/cloud_form.html b/ntt_tool/static/partials/cloud/cloud_form.html new file mode 100644 index 0000000..b330ce2 --- /dev/null +++ b/ntt_tool/static/partials/cloud/cloud_form.html @@ -0,0 +1,56 @@ + + + diff --git a/ntt_tool/static/partials/cloud/clouds.html b/ntt_tool/static/partials/cloud/cloud_list.html similarity index 54% rename from ntt_tool/static/partials/cloud/clouds.html rename to ntt_tool/static/partials/cloud/cloud_list.html index e73f56f..0ce7f99 100644 --- a/ntt_tool/static/partials/cloud/clouds.html +++ b/ntt_tool/static/partials/cloud/cloud_list.html @@ -4,13 +4,11 @@
    - +
    @@ -19,20 +17,23 @@

    Clouds

    - - + + -
    Name Keystone Auth URL Created On Actions
    {$ cloud.name $}
    {$ cloud.name $} {$ cloud.keystone_auth_url $} {$ cloud.keystone_user $} {$ cloud.keystone_tenant_name $} {$ cloud.created_on | date:"MM/dd/yyyy hh:mm a" $} - + +
    -
    Data not found.
    +
    Data not found.
    - \ No newline at end of file + + + + \ No newline at end of file diff --git a/ntt_tool/static/partials/cloud/cloud_view.html b/ntt_tool/static/partials/cloud/cloud_view.html index fdc4859..f878723 100644 --- a/ntt_tool/static/partials/cloud/cloud_view.html +++ b/ntt_tool/static/partials/cloud/cloud_view.html @@ -3,61 +3,38 @@
    -
    -
    Cloud Details
    - - - - - - - - - - - - - -
    Name : {$ cloud.name $}Keystone Auth URL : {$ cloud.keystone_auth_url $}
    Keystone Tenant Name : {$ cloud.keystone_tenant_name $}Keystone User : {$ cloud.keystone_user $}
    -
    +
    +
    - - +
    - - - - - - - - - + + + + - - - - - - - - - - + + + + +
    NameAllowed Delta PercentageTest Result PathNumber Of WorkersRemote UserTest MethodIPERF DurationTypeActionsName : {$ cloud.name $}Keystone Auth URL : {$ cloud.keystone_auth_url $}
    {$ cloudTraffic.name $}{$ cloudTraffic.allowed_delta_percentage $}{$ cloudTraffic.test_result_path $}{$ cloudTraffic.number_of_workers $}{$ cloudTraffic.remote_user $}{$ cloudTraffic.test_method $}{$ cloudTraffic.iperf_duration $}{$ cloudTraffic.tenant_type $} - - -
    Keystone Tenant Name : {$ cloud.keystone_tenant_name $}Keystone User : {$ cloud.keystone_user $}
    + +
    +
    + +
    +
    \ No newline at end of file diff --git a/ntt_tool/static/partials/cloud/cloudtraffic_form.html b/ntt_tool/static/partials/cloud/cloudtraffic_form.html deleted file mode 100644 index 6aad14f..0000000 --- a/ntt_tool/static/partials/cloud/cloudtraffic_form.html +++ /dev/null @@ -1,53 +0,0 @@ - - -
    -
    -
    {$ cloud | json $}
    -
    -
    -
    -

    {$ event | capitalize:true $} Cloud Traffic

    -

    Cloud > Traffic

    -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    - Cancel - -
    -
    -
    -
    \ No newline at end of file diff --git a/ntt_tool/static/partials/cloud/cloudtraffic_test.html b/ntt_tool/static/partials/cloud/cloudtraffic_test.html deleted file mode 100644 index ad54e0a..0000000 --- a/ntt_tool/static/partials/cloud/cloudtraffic_test.html +++ /dev/null @@ -1,15 +0,0 @@ - - -
    -
    - -
    - -
    - - -
    -
    -
    \ No newline at end of file diff --git a/ntt_tool/static/partials/cloud/cloudtraffic_view.html b/ntt_tool/static/partials/cloud/cloudtraffic_view.html deleted file mode 100644 index f65ddf3..0000000 --- a/ntt_tool/static/partials/cloud/cloudtraffic_view.html +++ /dev/null @@ -1,126 +0,0 @@ - - -
    -
    - - - - \ No newline at end of file diff --git a/ntt_tool/static/partials/cloud/tenants/tenants_discovery.html b/ntt_tool/static/partials/cloud/tenants/tenants_discovery.html new file mode 100644 index 0000000..64d60aa --- /dev/null +++ b/ntt_tool/static/partials/cloud/tenants/tenants_discovery.html @@ -0,0 +1,57 @@ + + \ No newline at end of file diff --git a/ntt_tool/static/partials/cloud/tenants/tenants_list.html b/ntt_tool/static/partials/cloud/tenants/tenants_list.html new file mode 100644 index 0000000..4bb52d9 --- /dev/null +++ b/ntt_tool/static/partials/cloud/tenants/tenants_list.html @@ -0,0 +1,49 @@ + + +
    + Discovering tenants and networks Please wait... +
    +
    +
    +
    + +
    +
    +
    + {$ tenant.tenant_name $} + + {$ tenant.enabled === true ? "Enabled" : "Disabled" $} + +
    + + + + + + + + + + + +
    NetworkSubnet AssociatedStatus
    {$ network.network_name $} +
    + {$ subnet.subnet_name $} : {$ subnet.cidr $} +
    +
    {$ network.status $}
    + +
    + No networks. +
    +
    + +

    No tenants discovered. Please click on discover tenants

    +
    + diff --git a/ntt_tool/static/partials/cloud/traffic/traffic_form.html b/ntt_tool/static/partials/cloud/traffic/traffic_form.html new file mode 100644 index 0000000..e8a29cc --- /dev/null +++ b/ntt_tool/static/partials/cloud/traffic/traffic_form.html @@ -0,0 +1,161 @@ + + + \ No newline at end of file diff --git a/ntt_tool/static/partials/cloud/traffic/traffic_list.html b/ntt_tool/static/partials/cloud/traffic/traffic_list.html new file mode 100644 index 0000000..88d0917 --- /dev/null +++ b/ntt_tool/static/partials/cloud/traffic/traffic_list.html @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameTest EnvironmentTest TypeTest MethodsIPERF Duration# WorkersAllowed Delta %Test Result PathActions
    {$ cloudTraffic.name | limitTo: 20 $}{$ cloudTraffic.test_environment $}{$ cloudTraffic.test_type $} + TCP + UDP + ICMP + {$ cloudTraffic.iperf_duration $} sec{$ cloudTraffic.number_of_workers $}{$ cloudTraffic.allowed_delta_percentage $} %{$ cloudTraffic.test_result_path $} + + +
    + +

    No data found.

    + + + + + + + + + + diff --git a/ntt_tool/static/partials/cloud/traffic/traffic_test.html b/ntt_tool/static/partials/cloud/traffic/traffic_test.html new file mode 100644 index 0000000..7c053f9 --- /dev/null +++ b/ntt_tool/static/partials/cloud/traffic/traffic_test.html @@ -0,0 +1,48 @@ + + +
    +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    +
    +

    {$ status $}

    +
    +
    +
    +
    + +
    +
    {$ status $}
    + + + + + + + + + +
    VM NameVM Status
    {$ instance.instance_name $}{$ instance.instance_status $}
    +
    +
    +
    + + +
    \ No newline at end of file diff --git a/ntt_tool/static/partials/cloud/traffic/traffic_view.html b/ntt_tool/static/partials/cloud/traffic/traffic_view.html new file mode 100644 index 0000000..9d502be --- /dev/null +++ b/ntt_tool/static/partials/cloud/traffic/traffic_view.html @@ -0,0 +1,505 @@ + + +
    +
    +
    + +
    +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Name : {$ traffic.name $}Allowed delta percentage : {$ traffic.allowed_delta_percentage $} %
    Remote User : {$ traffic.remote_user $}Number of workers : {$ traffic.number_of_workers $}
    Test environment : {$ traffic.test_environment $}IPERF duration : {$ traffic.iperf_duration $} sec
    SSH gateway : {$ traffic.ssh_gateway $}Test Type : {$ traffic.test_type $}
    Test result path : {$ traffic.test_result_path $}Test Methods : + TCP + UDP + ICMP +
    +
    +
    + + +
    +
    + + +
    + Loading tenants Please wait... +
    +
    +
    +
    + +
    + Discovering tenants Please wait... +
    +
    +
    +
    +
    + +
    +
    + + + + + + + + + +
    Name
    {$ tenant.tenant_name $} {$ tenant.enabled $} + +
    + +

    No tenants found. Click here to discover.

    +
    + +
    + + + +
    +
    +
    Please select endpoints to start traffic test
    +
    Please select more than 1 endpoints to start traffic test
    +
    + +
    +
    +
    + + + +
    + + + + + + + + + + + + + + + + + + + +
    Test Run IdTest Run DurationStarted OnCompleted OnStatusActions
    {$ report.report_name $}{$ report.test_run_duration $} min{$ report.started_on | date:'medium' $}{$ report.completed_on | date:'medium' $} + Error + In Progress + Queued + Completed + + + + + + + +
    +
    + +
    +
    + + + + + + + + + + + + + + + + + diff --git a/ntt_tool/static/partials/cloud/traffic/traffic_view_tenants_dev_env.html b/ntt_tool/static/partials/cloud/traffic/traffic_view_tenants_dev_env.html new file mode 100644 index 0000000..aabcab3 --- /dev/null +++ b/ntt_tool/static/partials/cloud/traffic/traffic_view_tenants_dev_env.html @@ -0,0 +1,112 @@ + +
    +
    + Discovering Networks Please wait... +
    +
    +
    +
    + +
    +
    +
    + + + + + + + + + + + + + + + + + + + + + + +
    Network NameSubnets AssociatedAllocation PoolSharedStatus
    {$ network.network_name $} +
    + {$ subnet.subnet_name $} {$ subnet.cidr $} +
    +
    + {$ network.subnets[0].allocation_pool_start $} - {$ network.subnets[0].allocation_pool_end $} +
    +
    + + - + +
    + Note: Enter ip range between allocation pool. +
    +
    {$ network.shared $}{$ network.status $} + +
    + Note: Please select atleast 2 networks + +
    +
    + + +
    + Discovering Endpoints Please wait... +
    +
    +
    +
    + +
    + + + + + + + + + + + + + + + + +
    NetworkEndpoint NameIP AddressStatus
    {$ endpoint.network_name $}{$ endpoint.name $}{$ endpoint.ip_address $}{$ endpoint.status $} + +
    +
    + +
    +

    No tenant selected. Please select a tenant to discover networks

    +
    +
    + \ No newline at end of file diff --git a/ntt_tool/static/partials/cloud/traffic/traffic_view_tenants_prod_env.html b/ntt_tool/static/partials/cloud/traffic/traffic_view_tenants_prod_env.html new file mode 100644 index 0000000..dd57f18 --- /dev/null +++ b/ntt_tool/static/partials/cloud/traffic/traffic_view_tenants_prod_env.html @@ -0,0 +1,95 @@ + +
    +
    + Discovering Networks Please wait... +
    +
    +
    +
    + +
    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + +
    Network NameSubnets AssociatedSharedStatusNo. of Endpoints To Be Launched
    {$ network.network_name $} +
    + {$ subnet.subnet_name $} {$ subnet.cidr $} +
    +
    {$ network.shared $}{$ network.status $} + + + +
    + Note: Please select atleast 2 networks + +
    +
    + + + +
    + Launching Endpoints Please wait... +
    +
    +
    +
    + +
    + + + + + + + + + + + + + + +
    NetworkEndpoint NameIP AddressStatus
    {$ endpoint.network_name $}{$ endpoint.name $}{$ endpoint.ip_address $}{$ endpoint.status $}
    +
    + +
    +

    No tenant selected. Please select a tenant to discover networks

    +
    +
    + \ No newline at end of file diff --git a/ntt_tool/static/partials/common/header.html b/ntt_tool/static/partials/common/header.html index bfd31a0..a4bf779 100644 --- a/ntt_tool/static/partials/common/header.html +++ b/ntt_tool/static/partials/common/header.html @@ -2,12 +2,17 @@ - \ No newline at end of file + diff --git a/ntt_tool/static/partials/index.html b/ntt_tool/static/partials/index.html index 3b38d67..276dfec 100644 --- a/ntt_tool/static/partials/index.html +++ b/ntt_tool/static/partials/index.html @@ -1,10 +1,10 @@
    -

    Network Traffic Testing Tool

    +
    - +

    Login to your Account


    @@ -13,11 +13,12 @@
    - - +
    +

    You are logged in.

    + diff --git a/ntt_tool/templates/base.html b/ntt_tool/templates/base.html index 41661c1..e040d81 100644 --- a/ntt_tool/templates/base.html +++ b/ntt_tool/templates/base.html @@ -7,24 +7,37 @@ + + {% block css %}{% endblock %} + + + + + + + + + + {% block js %}{% endblock %}
    + \ No newline at end of file diff --git a/ntt_tool/templates/email_templates/plain.txt b/ntt_tool/templates/email_templates/plain.txt new file mode 100644 index 0000000..e69de29 diff --git a/ntt_tool/templates/email_templates/traffic_test_report.html b/ntt_tool/templates/email_templates/traffic_test_report.html new file mode 100644 index 0000000..6fd299a --- /dev/null +++ b/ntt_tool/templates/email_templates/traffic_test_report.html @@ -0,0 +1,143 @@ + + + + + + + + OneCloud Consulting - Traffic Test Report + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + + + + + + + + + + + + + + + +
    + + + Traffic Test Report
    +
    +

    +
    +
    Hi {{ user_full_name }},
    +
    +
    Please find attached traffic test report.
    +
    +
    Regards,
    +
    OneCloud Consulting
    +

    +
    + ©OneCloud Consulting 2015 +
    + + + \ No newline at end of file diff --git a/ntt_tool/templates/reports/traffic_test_report.html b/ntt_tool/templates/reports/traffic_test_report.html new file mode 100644 index 0000000..9ef7cec --- /dev/null +++ b/ntt_tool/templates/reports/traffic_test_report.html @@ -0,0 +1,207 @@ + + + + + + + +

    Traffic Test Report

    + + + + + + + + + + + + + + + + + + + + + + +
    Name : {{ data.traffic.name }}Allowed delta percentage : {{ data.traffic.allowed_delta_percentage }} %
    Remote User : {{ data.traffic.remote_user }}Number of workers : {{ data.traffic.number_of_workers }}
    Test environment : {{ data.traffic.test_environment }}IPERF duration : {{ data.traffic.iperf_duration }} sec
    SSH gateway : {{ data.traffic.ssh_gateway }}Test Type : {{ data.traffic.test_type }}
    Test result path : {{ data.traffic.test_result_path }}Test Methods : {{ data.traffic.test_method }}
    + +
    + + + {% if data.icmp_test_results %} +

    ICMP Test Result

    + + + + + + + + + + + + + + + {% for res in data.icmp_test_results %} + + + + + + + + + + + + + + {% endfor %} +
    Source TenantDestination TenantSource EndpointsDestination Endpointsrtt minrtt maxrtt avgPackets RecievedPackets TransmittedPackets Loss PercetageStatus
    {{ res.src_tenant }}{{ res.dest_tenant }}{{ res.src_ep }}{{ res.dest_ep }}{{ res.rtt_min }}{{ res.rtt_max }}{{ res.rtt_avg }}{{ res.packets_received }}{{ res.packets_transmitted }}{{ res.packet_loss_percent }}{{ res.status }}
    + {% endif %} + + +
    +
    + + + {% if data.udp_test_results %} +

    UDP Test Result

    + + + + + + + + + + + + + + + + + + {% for res in data.udp_test_results %} + + + + + + + + + + + + + + + + + {% endfor %} +
    Source TenantDestination TenantSource EndpointsDestination EndpointsJitterBandwidthLoss BandwidthInterval TimeBytes TransferredDatagramsLoss DatagramsCPU Utilization at SourceCPU Utilization at DestinationStatus
    {{ res.src_tenant }}{{ res.dest_tenant }}{{ res.src_ep }}{{ res.dest_ep }}{{ res.jitter }}{{ res.bandwidth }} {{ res.bandwidth_units }}{{ res.bandwidth_loss }} {{ res.bandwidth_units }}{{ res.interval_time_start }} - {{ res.interval_time_end }} {{ res.interval_time_units }} {{ res.bytes_transferred }} {{ res.bytes_transferred_units }}{{ res.datagrams }}{{ res.loss_datagrams }}{{ res.cpu_utilization_src }}{{ res.cpu_utilization_dest }}{{ res.status }}
    + {% endif %} + + + +
    +
    + + + {% if data.tcp_test_results %} +

    TCP Test Result

    + + + + + + + + + + + + + + + {% for res in data.tcp_test_results %} + + + + + + + + + + + + + + {% endfor %} +
    Source TenantDestination TenantSource EndpointsDestination EndpointsRetransmitsBandwidthInterval TimeTransferredCPU Utilization at SourceCPU Utilization at DestinationStatus
    {{ res.src_tenant }}{{ res.dest_tenant }}{{ res.src_ep }}{{ res.dest_ep }}{{ res.retransmits }}{{ res.bandwidth }} {{ res.bandwidth_units }}{{ res.interval_time_start }} - {{ res.interval_time_end }} {{ res.interval_time_units }} {{ res.bytes_transferred }} {{ res.bytes_transferred_units }}{{ res.cpu_utilization_src }}{{ res.cpu_utilization_dest }}{{ res.status }}
    + {% endif %} + + + + + + \ No newline at end of file diff --git a/ntt_tool/utils/__init__.py b/ntt_tool/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ntt_tool/utils/custom_exception_handler.py b/ntt_tool/utils/custom_exception_handler.py new file mode 100644 index 0000000..030364a --- /dev/null +++ b/ntt_tool/utils/custom_exception_handler.py @@ -0,0 +1,19 @@ +from rest_framework.views import exception_handler + + +def custom_exception_handler(exc, context): + # Call REST framework's default exception handler first, + # to get the standard error response. + response = exception_handler(exc, context) + + # Now add the HTTP status code to the response. + if response is not None: + response.data = {} + errors = [] + for field, value in response.data.items(): + errors.append("{} : {}".format(field, " ".join(value))) + response.data['errors'] = errors + response.data['status'] = False + response.data['exception'] = str(exc) + + return response diff --git a/ntt_tool/utils/reportutils.py b/ntt_tool/utils/reportutils.py new file mode 100644 index 0000000..6261352 --- /dev/null +++ b/ntt_tool/utils/reportutils.py @@ -0,0 +1,11 @@ +import cStringIO as StringIO +from django.template.loader import get_template +from xhtml2pdf import pisa + + +def render_to_pdf(template_path, context): + template = get_template(template_path) + html = template.render({'data': context}) + pdf_result = StringIO.StringIO() + pisa.pisaDocument(StringIO.StringIO(html.encode("ISO-8859-1")), pdf_result) + return pdf_result diff --git a/requirements.txt b/requirements.txt index 642f6ad..0150954 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,3 +9,26 @@ djangorestframework==3.3.2 djangorestframework-jwt==1.7.2 six==1.10.0 wsgiref==0.1.2 +python-heatclient==0.6.0 +python-keystoneclient==1.6.0 +python-neutronclient==2.6.0 +python-novaclient==2.17.0 +python-swiftclient==2.4.0 +python-glanceclient==2.0.0 + + +# Following Packages used in traffic test scripts +configobj==5.0.6 +# configparser==3.3.0.0r2 # package not installed +ecdsa==0.13 +Fabric==1.10.2 +fabtools==0.19.0 +logging==0.4.9.6 +multiprocessing==2.6.2.1 +paramiko==1.15.2 +pexpect==3.3 +pudb==2015.3 +pycrypto==2.6.1 +Pygments==2.0.2 +python-group-based-policy-client==0.9.2 +urwid==1.3.0