diff --git a/.gitignore b/.gitignore index 801b7e1110..f074513228 100644 --- a/.gitignore +++ b/.gitignore @@ -52,6 +52,7 @@ tools/vagrant/local.yml # Setup setup/tower_setup_conf.yml setup/setup.log +setup/inventory # Other .tower_cycle diff --git a/awx/api/views.py b/awx/api/views.py index 67007f11a8..406e52bfdd 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -109,6 +109,8 @@ class ApiV1RootView(APIView): data['hosts'] = reverse('api:host_list') data['job_templates'] = reverse('api:job_template_list') data['jobs'] = reverse('api:job_list') + data['system_job_templates'] = reverse('api:system_job_template_list') + data['system_jobs'] = reverse('api:system_job_list') data['schedules'] = reverse('api:schedule_list') data['unified_job_templates'] = reverse('api:unified_job_template_list') data['unified_jobs'] = reverse('api:unified_job_list') @@ -1720,6 +1722,11 @@ class SystemJobTemplateList(ListAPIView): model = SystemJobTemplate serializer_class = SystemJobTemplateSerializer + def get(self, request, *args, **kwargs): + if not request.user.is_superuser: + return Response(status=status.HTTP_404_NOT_FOUND) + return super(SystemJobTemplateList, self).get(request, *args, **kwargs) + class SystemJobTemplateDetail(RetrieveAPIView): model = SystemJobTemplate @@ -1738,7 +1745,7 @@ class SystemJobTemplateLaunch(GenericAPIView): raise PermissionDenied() new_job = obj.create_unified_job() result = new_job.signal_start() - data = dict(job=new_job.id) + data = dict(system_job=new_job.id) return Response(data, status=status.HTTP_202_ACCEPTED) class SystemJobTemplateSchedulesList(SubListCreateAPIView): @@ -2174,6 +2181,12 @@ class SystemJobList(ListCreateAPIView): model = SystemJob serializer_class = SystemJobListSerializer + def get(self, request, *args, **kwargs): + if not request.user.is_superuser: + return Response(status=status.HTTP_404_NOT_FOUND) + return super(SystemJobList, self).get(request, *args, **kwargs) + + class SystemJobDetail(RetrieveAPIView): model = SystemJob diff --git a/awx/main/access.py b/awx/main/access.py index 9511c9a1b3..04d6991708 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -980,6 +980,8 @@ class JobAccess(BaseAccess): def can_add(self, data): if not data or '_method' in data: # So the browseable API will work? return True + if not self.user.is_superuser: + return False reader = TaskSerializer() validation_info = reader.from_file() @@ -995,8 +997,6 @@ class JobAccess(BaseAccess): if validation_info.get('free_instances', 0) < 0: raise PermissionDenied("Host Count exceeds available instances") - if self.user.is_superuser: - return True add_data = dict(data.items()) # If a job template is provided, the user should have read access to it. @@ -1012,9 +1012,6 @@ class JobAccess(BaseAccess): add_data.setdefault('credential', job_template.credential.pk) else: job_template = None - # Only admins can create jobs without job templates - if not self.user.is_superuser: - return False # Check that the user would be able to add a job template with the # same data. diff --git a/awx/main/management/commands/run_socketio_service.py b/awx/main/management/commands/run_socketio_service.py index 644ef5df75..e7bd1fd643 100644 --- a/awx/main/management/commands/run_socketio_service.py +++ b/awx/main/management/commands/run_socketio_service.py @@ -8,6 +8,7 @@ import logging import json import signal import time +import urllib from optparse import make_option from threading import Thread @@ -31,23 +32,71 @@ from socketio import socketio_manage from socketio.server import SocketIOServer from socketio.namespace import BaseNamespace -class TestNamespace(BaseNamespace): +class TowerBaseNamespace(BaseNamespace): + + def get_allowed_methods(self): + return [] + + def get_initial_acl(self): + print self + if self.valid_user() is not None: + return set(['recv_connect'] + self.get_allowed_methods()) + return set() + + def valid_user(self): + if 'HTTP_COOKIE' not in self.environ: + return False + else: + try: + all_keys = [e.strip() for e in self.environ['HTTP_COOKIE'].split(";")] + for each_key in all_keys: + k, v = each_key.split("=") + if k == "token": + token_actual = urllib.unquote_plus(v).decode().replace("\"","") + auth_token = AuthToken.objects.filter(key=token_actual) + if not auth_token.exists(): + return False + auth_token = auth_token[0] + if not auth_token.expired: + return auth_token.user + else: + return False + except Exception, e: + return False + +class TestNamespace(TowerBaseNamespace): def recv_connect(self): print("Received client connect for test namespace from %s" % str(self.environ['REMOTE_ADDR'])) self.emit('test', "If you see this then you are connected to the test socket endpoint") -class JobNamespace(BaseNamespace): +class JobNamespace(TowerBaseNamespace): + + def get_allowed_methods(self): + return ['summary_complete', 'status_changed'] def recv_connect(self): print("Received client connect for job namespace from %s" % str(self.environ['REMOTE_ADDR'])) -class JobEventNamespace(BaseNamespace): +class JobEventNamespace(TowerBaseNamespace): + def get_initial_acl(self): + valid_user = self.valid_user() + if valid_user is None or valid_user is False: + return set() + else: + user_jobs = get_user_queryset(valid_user, Job).filter(finished__isnull=True) + visible_jobs = set(['recv_connect'] + ["job_events-%s" % str(j.id) for j in user_jobs]) + print("Visible jobs: " + str(visible_jobs)) + return visible_jobs + def recv_connect(self): print("Received client connect for job event namespace from %s" % str(self.environ['REMOTE_ADDR'])) -class ScheduleNamespace(BaseNamespace): +class ScheduleNamespace(TowerBaseNamespace): + + def get_allowed_methods(self): + return ["schedule_changed"] def recv_connect(self): print("Received client connect for schedule namespace from %s" % str(self.environ['REMOTE_ADDR'])) diff --git a/awx/main/migrations/0059_v210_changes.py b/awx/main/migrations/0059_v210_changes.py new file mode 100644 index 0000000000..016e08a9fe --- /dev/null +++ b/awx/main/migrations/0059_v210_changes.py @@ -0,0 +1,494 @@ +# -*- coding: utf-8 -*- +from south.utils import datetime_utils as datetime +from south.db import db +from south.v2 import DataMigration +from django.db import models +from django.utils.timezone import now +from awx.main.models import * + +class Migration(DataMigration): + + def forwards(self, orm): + SystemJobTemplate(name='Delete Old Jobs', + description="Run a job to delete jobs that are older than a given number of days", + job_type="cleanup_jobs", + created=now(), + modified=now()).save() + SystemJobTemplate(name='Cleanup Deleted Data', + description="Run a job to cleanup any deleted objects that are older than a given number of days", + job_type="cleanup_deleted", + created=now(), + modified=now()).save() + SystemJobTemplate(name='Cleanup Activity Stream', + description="Run a job to purge activity stream data that's older than a given number of days", + job_type="cleanup_activitystream", + created=now(), + modified=now()).save() + + def backwards(self, orm): + orm.SystemJobTemplate.objects.all().delete() + + models = { + u'auth.group': { + 'Meta': {'object_name': 'Group'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + u'auth.permission': { + 'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + u'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Group']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Permission']"}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + u'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'main.activitystream': { + 'Meta': {'object_name': 'ActivityStream'}, + 'actor': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'activity_stream'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'changes': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'credential': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['main.Credential']", 'symmetrical': 'False', 'blank': 'True'}), + 'group': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['main.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'host': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['main.Host']", 'symmetrical': 'False', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'inventory': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['main.Inventory']", 'symmetrical': 'False', 'blank': 'True'}), + 'inventory_source': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['main.InventorySource']", 'symmetrical': 'False', 'blank': 'True'}), + 'inventory_update': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['main.InventoryUpdate']", 'symmetrical': 'False', 'blank': 'True'}), + 'job': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['main.Job']", 'symmetrical': 'False', 'blank': 'True'}), + 'job_template': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['main.JobTemplate']", 'symmetrical': 'False', 'blank': 'True'}), + 'object1': ('django.db.models.fields.TextField', [], {}), + 'object2': ('django.db.models.fields.TextField', [], {}), + 'object_relationship_type': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'operation': ('django.db.models.fields.CharField', [], {'max_length': '13'}), + 'organization': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['main.Organization']", 'symmetrical': 'False', 'blank': 'True'}), + 'permission': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['main.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'project': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['main.Project']", 'symmetrical': 'False', 'blank': 'True'}), + 'project_update': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['main.ProjectUpdate']", 'symmetrical': 'False', 'blank': 'True'}), + 'schedule': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['main.Schedule']", 'symmetrical': 'False', 'blank': 'True'}), + 'team': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['main.Team']", 'symmetrical': 'False', 'blank': 'True'}), + 'timestamp': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'unified_job': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'activity_stream_as_unified_job+'", 'blank': 'True', 'to': "orm['main.UnifiedJob']"}), + 'unified_job_template': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'activity_stream_as_unified_job_template+'", 'blank': 'True', 'to': "orm['main.UnifiedJobTemplate']"}), + 'user': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.User']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'main.authtoken': { + 'Meta': {'object_name': 'AuthToken'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'expires': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'key': ('django.db.models.fields.CharField', [], {'max_length': '40', 'primary_key': 'True'}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'request_hash': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '40', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'auth_tokens'", 'to': u"orm['auth.User']"}) + }, + 'main.credential': { + 'Meta': {'ordering': "('kind', 'name')", 'unique_together': "[('user', 'team', 'kind', 'name')]", 'object_name': 'Credential'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'cloud': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'credential\', \'app_label\': \'main\'}(class)s_created+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'host': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'kind': ('django.db.models.fields.CharField', [], {'default': "'ssh'", 'max_length': '32'}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'modified_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'credential\', \'app_label\': \'main\'}(class)s_modified+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '512'}), + 'password': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'project': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '100', 'blank': 'True'}), + 'ssh_key_data': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'ssh_key_unlock': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'su_password': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'su_username': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'sudo_password': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'sudo_username': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'team': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'credentials'", 'null': 'True', 'blank': 'True', 'to': "orm['main.Team']"}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'credentials'", 'null': 'True', 'blank': 'True', 'to': u"orm['auth.User']"}), + 'username': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'vault_password': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}) + }, + 'main.custominventoryscript': { + 'Meta': {'ordering': "('name',)", 'object_name': 'CustomInventoryScript'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'custominventoryscript\', \'app_label\': \'main\'}(class)s_created+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'modified_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'custominventoryscript\', \'app_label\': \'main\'}(class)s_modified+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '512'}), + 'script': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}) + }, + 'main.group': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('name', 'inventory'),)", 'object_name': 'Group'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'group\', \'app_label\': \'main\'}(class)s_created+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'groups_with_active_failures': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'has_active_failures': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'has_inventory_sources': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'hosts': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'groups'", 'blank': 'True', 'to': "orm['main.Host']"}), + 'hosts_with_active_failures': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'inventory': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'groups'", 'to': "orm['main.Inventory']"}), + 'inventory_sources': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'groups'", 'symmetrical': 'False', 'to': "orm['main.InventorySource']"}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'modified_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'group\', \'app_label\': \'main\'}(class)s_modified+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '512'}), + 'parents': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'children'", 'blank': 'True', 'to': "orm['main.Group']"}), + 'total_groups': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'total_hosts': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'variables': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}) + }, + 'main.host': { + 'Meta': {'ordering': "('inventory', 'name')", 'unique_together': "(('name', 'inventory'),)", 'object_name': 'Host'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'host\', \'app_label\': \'main\'}(class)s_created+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'has_active_failures': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'has_inventory_sources': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'instance_id': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '100', 'blank': 'True'}), + 'inventory': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'hosts'", 'to': "orm['main.Inventory']"}), + 'inventory_sources': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'hosts'", 'symmetrical': 'False', 'to': "orm['main.InventorySource']"}), + 'last_job': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'hosts_as_last_job+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Job']"}), + 'last_job_host_summary': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'hosts_as_last_job_summary+'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.JobHostSummary']", 'blank': 'True', 'null': 'True'}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'modified_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'host\', \'app_label\': \'main\'}(class)s_modified+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '512'}), + 'variables': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}) + }, + 'main.instance': { + 'Meta': {'object_name': 'Instance'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ip_address': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'primary': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'uuid': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '40'}) + }, + 'main.inventory': { + 'Meta': {'ordering': "('name',)", 'unique_together': "[('name', 'organization')]", 'object_name': 'Inventory'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'inventory\', \'app_label\': \'main\'}(class)s_created+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'groups_with_active_failures': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'has_active_failures': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'has_inventory_sources': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'hosts_with_active_failures': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'inventory_sources_with_failures': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'modified_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'inventory\', \'app_label\': \'main\'}(class)s_modified+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '512'}), + 'organization': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'inventories'", 'to': "orm['main.Organization']"}), + 'total_groups': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'total_hosts': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'total_inventory_sources': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'variables': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}) + }, + 'main.inventorysource': { + 'Meta': {'object_name': 'InventorySource', '_ormbases': ['main.UnifiedJobTemplate']}, + 'credential': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'inventorysources'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.Credential']", 'blank': 'True', 'null': 'True'}), + 'group': ('awx.main.fields.AutoOneToOneField', [], {'default': 'None', 'related_name': "'inventory_source'", 'unique': 'True', 'null': 'True', 'to': "orm['main.Group']"}), + 'inventory': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'inventory_sources'", 'null': 'True', 'to': "orm['main.Inventory']"}), + 'overwrite': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'overwrite_vars': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'source': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '32', 'blank': 'True'}), + 'source_path': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'source_regions': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'source_script': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['main.CustomInventoryScript']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}), + 'source_vars': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + u'unifiedjobtemplate_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['main.UnifiedJobTemplate']", 'unique': 'True', 'primary_key': 'True'}), + 'update_cache_timeout': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'update_on_launch': ('django.db.models.fields.BooleanField', [], {'default': 'False'}) + }, + 'main.inventoryupdate': { + 'Meta': {'object_name': 'InventoryUpdate', '_ormbases': ['main.UnifiedJob']}, + 'credential': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'inventoryupdates'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.Credential']", 'blank': 'True', 'null': 'True'}), + 'inventory_source': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'inventory_updates'", 'to': "orm['main.InventorySource']"}), + 'license_error': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'overwrite': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'overwrite_vars': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'source': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '32', 'blank': 'True'}), + 'source_path': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'source_regions': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'source_script': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['main.CustomInventoryScript']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}), + 'source_vars': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + u'unifiedjob_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['main.UnifiedJob']", 'unique': 'True', 'primary_key': 'True'}) + }, + 'main.job': { + 'Meta': {'ordering': "('id',)", 'object_name': 'Job', '_ormbases': ['main.UnifiedJob']}, + 'cloud_credential': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'jobs_as_cloud_credential+'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.Credential']", 'blank': 'True', 'null': 'True'}), + 'credential': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'jobs'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.Credential']", 'blank': 'True', 'null': 'True'}), + 'extra_vars': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'force_handlers': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'forks': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0', 'blank': 'True'}), + 'hosts': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'jobs'", 'symmetrical': 'False', 'through': "orm['main.JobHostSummary']", 'to': "orm['main.Host']"}), + 'inventory': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'jobs'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Inventory']"}), + 'job_tags': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'job_template': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'jobs'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.JobTemplate']", 'blank': 'True', 'null': 'True'}), + 'job_type': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'limit': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'playbook': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'jobs'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Project']"}), + 'skip_tags': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'start_at_task': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + u'unifiedjob_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['main.UnifiedJob']", 'unique': 'True', 'primary_key': 'True'}), + 'verbosity': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0', 'blank': 'True'}) + }, + 'main.jobevent': { + 'Meta': {'ordering': "('pk',)", 'object_name': 'JobEvent'}, + 'changed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'counter': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'event': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'event_data': ('jsonfield.fields.JSONField', [], {'default': '{}', 'blank': 'True'}), + 'failed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'host': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'job_events_as_primary_host'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Host']"}), + 'host_name': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024'}), + 'hosts': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'job_events'", 'symmetrical': 'False', 'to': "orm['main.Host']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'job': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'job_events'", 'to': "orm['main.Job']"}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'parent': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'children'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.JobEvent']"}), + 'play': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024'}), + 'role': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024'}), + 'task': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024'}) + }, + 'main.jobhostsummary': { + 'Meta': {'ordering': "('-pk',)", 'unique_together': "[('job', 'host_name')]", 'object_name': 'JobHostSummary'}, + 'changed': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'dark': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'failed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'failures': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'host': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'job_host_summaries'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Host']"}), + 'host_name': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'job': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'job_host_summaries'", 'to': "orm['main.Job']"}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'ok': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'processed': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'skipped': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}) + }, + 'main.joborigin': { + 'Meta': {'object_name': 'JobOrigin'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'instance': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['main.Instance']"}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'unified_job': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'job_origin'", 'unique': 'True', 'to': "orm['main.UnifiedJob']"}) + }, + 'main.jobtemplate': { + 'Meta': {'ordering': "('name',)", 'object_name': 'JobTemplate', '_ormbases': ['main.UnifiedJobTemplate']}, + 'ask_variables_on_launch': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'cloud_credential': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'jobtemplates_as_cloud_credential+'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.Credential']", 'blank': 'True', 'null': 'True'}), + 'credential': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'jobtemplates'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.Credential']", 'blank': 'True', 'null': 'True'}), + 'extra_vars': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'force_handlers': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'forks': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0', 'blank': 'True'}), + 'host_config_key': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'inventory': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'jobtemplates'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Inventory']"}), + 'job_tags': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'job_type': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'limit': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'playbook': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'jobtemplates'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Project']"}), + 'skip_tags': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'start_at_task': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'survey_enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'survey_spec': ('jsonfield.fields.JSONField', [], {'default': '{}', 'blank': 'True'}), + u'unifiedjobtemplate_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['main.UnifiedJobTemplate']", 'unique': 'True', 'primary_key': 'True'}), + 'verbosity': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0', 'blank': 'True'}) + }, + 'main.organization': { + 'Meta': {'ordering': "('name',)", 'object_name': 'Organization'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'admins': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'admin_of_organizations'", 'blank': 'True', 'to': u"orm['auth.User']"}), + 'created': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'organization\', \'app_label\': \'main\'}(class)s_created+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'modified_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'organization\', \'app_label\': \'main\'}(class)s_modified+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '512'}), + 'projects': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'organizations'", 'blank': 'True', 'to': "orm['main.Project']"}), + 'users': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'organizations'", 'blank': 'True', 'to': u"orm['auth.User']"}) + }, + 'main.permission': { + 'Meta': {'object_name': 'Permission'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'permission\', \'app_label\': \'main\'}(class)s_created+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'inventory': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'permissions'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Inventory']"}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'modified_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'permission\', \'app_label\': \'main\'}(class)s_modified+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '512'}), + 'permission_type': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'permissions'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Project']"}), + 'team': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'permissions'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Team']"}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'permissions'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}) + }, + 'main.profile': { + 'Meta': {'object_name': 'Profile'}, + 'created': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'ldap_dn': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024'}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'user': ('awx.main.fields.AutoOneToOneField', [], {'related_name': "'profile'", 'unique': 'True', 'to': u"orm['auth.User']"}) + }, + 'main.project': { + 'Meta': {'ordering': "('id',)", 'object_name': 'Project', '_ormbases': ['main.UnifiedJobTemplate']}, + 'credential': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'projects'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.Credential']", 'blank': 'True', 'null': 'True'}), + 'local_path': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'blank': 'True'}), + 'scm_branch': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '256', 'blank': 'True'}), + 'scm_clean': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'scm_delete_on_next_update': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'scm_delete_on_update': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'scm_type': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '8', 'blank': 'True'}), + 'scm_update_cache_timeout': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'scm_update_on_launch': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'scm_url': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + u'unifiedjobtemplate_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['main.UnifiedJobTemplate']", 'unique': 'True', 'primary_key': 'True'}) + }, + 'main.projectupdate': { + 'Meta': {'object_name': 'ProjectUpdate', '_ormbases': ['main.UnifiedJob']}, + 'credential': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'projectupdates'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.Credential']", 'blank': 'True', 'null': 'True'}), + 'local_path': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'blank': 'True'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'project_updates'", 'to': "orm['main.Project']"}), + 'scm_branch': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '256', 'blank': 'True'}), + 'scm_clean': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'scm_delete_on_update': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'scm_type': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '8', 'blank': 'True'}), + 'scm_url': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + u'unifiedjob_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['main.UnifiedJob']", 'unique': 'True', 'primary_key': 'True'}) + }, + 'main.schedule': { + 'Meta': {'ordering': "['-next_run']", 'object_name': 'Schedule'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'schedule\', \'app_label\': \'main\'}(class)s_created+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'dtend': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True'}), + 'dtstart': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True'}), + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'extra_data': ('jsonfield.fields.JSONField', [], {'default': '{}', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'modified_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'schedule\', \'app_label\': \'main\'}(class)s_modified+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '512'}), + 'next_run': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True'}), + 'rrule': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'unified_job_template': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'schedules'", 'to': "orm['main.UnifiedJobTemplate']"}) + }, + 'main.systemjob': { + 'Meta': {'ordering': "('id',)", 'object_name': 'SystemJob', '_ormbases': ['main.UnifiedJob']}, + 'extra_vars': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'job_type': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '32', 'blank': 'True'}), + 'system_job_template': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'jobs'", 'on_delete': 'models.SET_NULL', 'default': 'None', 'to': "orm['main.SystemJobTemplate']", 'blank': 'True', 'null': 'True'}), + u'unifiedjob_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['main.UnifiedJob']", 'unique': 'True', 'primary_key': 'True'}) + }, + 'main.systemjobtemplate': { + 'Meta': {'object_name': 'SystemJobTemplate', '_ormbases': ['main.UnifiedJobTemplate']}, + 'job_type': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '32', 'blank': 'True'}), + u'unifiedjobtemplate_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['main.UnifiedJobTemplate']", 'unique': 'True', 'primary_key': 'True'}) + }, + 'main.team': { + 'Meta': {'ordering': "('organization__name', 'name')", 'unique_together': "[('organization', 'name')]", 'object_name': 'Team'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'team\', \'app_label\': \'main\'}(class)s_created+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'modified_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'team\', \'app_label\': \'main\'}(class)s_modified+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '512'}), + 'organization': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'teams'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Organization']"}), + 'projects': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'teams'", 'blank': 'True', 'to': "orm['main.Project']"}), + 'users': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'teams'", 'blank': 'True', 'to': u"orm['auth.User']"}) + }, + 'main.unifiedjob': { + 'Meta': {'object_name': 'UnifiedJob'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'cancel_flag': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'celery_task_id': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '100', 'blank': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'unifiedjob\', \'app_label\': \'main\'}(class)s_created+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'dependent_jobs': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'dependent_jobs_rel_+'", 'to': "orm['main.UnifiedJob']"}), + 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'elapsed': ('django.db.models.fields.DecimalField', [], {'max_digits': '12', 'decimal_places': '3'}), + 'failed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'finished': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'job_args': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'job_cwd': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}), + 'job_env': ('jsonfield.fields.JSONField', [], {'default': '{}', 'blank': 'True'}), + 'job_explanation': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'launch_type': ('django.db.models.fields.CharField', [], {'default': "'manual'", 'max_length': '20'}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'modified_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'unifiedjob\', \'app_label\': \'main\'}(class)s_modified+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '512'}), + 'old_pk': ('django.db.models.fields.PositiveIntegerField', [], {'default': 'None', 'null': 'True'}), + 'polymorphic_ctype': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'polymorphic_main.unifiedjob_set'", 'null': 'True', 'to': u"orm['contenttypes.ContentType']"}), + 'result_stdout_file': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'result_stdout_text': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'result_traceback': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'schedule': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['main.Schedule']", 'null': 'True', 'on_delete': 'models.SET_NULL'}), + 'start_args': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'started': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True'}), + 'status': ('django.db.models.fields.CharField', [], {'default': "'new'", 'max_length': '20'}), + 'unified_job_template': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'unifiedjob_unified_jobs'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.UnifiedJobTemplate']"}) + }, + 'main.unifiedjobtemplate': { + 'Meta': {'unique_together': "[('polymorphic_ctype', 'name')]", 'object_name': 'UnifiedJobTemplate'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'unifiedjobtemplate\', \'app_label\': \'main\'}(class)s_created+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'current_job': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'unifiedjobtemplate_as_current_job+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.UnifiedJob']"}), + 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'has_schedules': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'last_job': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'unifiedjobtemplate_as_last_job+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.UnifiedJob']"}), + 'last_job_failed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_job_run': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True'}), + 'modified': ('django.db.models.fields.DateTimeField', [], {'default': 'None'}), + 'modified_by': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': '"{\'class\': \'unifiedjobtemplate\', \'app_label\': \'main\'}(class)s_modified+"', 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['auth.User']"}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '512'}), + 'next_job_run': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True'}), + 'next_schedule': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'unifiedjobtemplate_as_next_schedule+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['main.Schedule']"}), + 'old_pk': ('django.db.models.fields.PositiveIntegerField', [], {'default': 'None', 'null': 'True'}), + 'polymorphic_ctype': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'polymorphic_main.unifiedjobtemplate_set'", 'null': 'True', 'to': u"orm['contenttypes.ContentType']"}), + 'status': ('django.db.models.fields.CharField', [], {'default': "'ok'", 'max_length': '32'}) + } + } + + complete_apps = ['main'] + symmetrical = True diff --git a/awx/main/tasks.py b/awx/main/tasks.py index bb9a328892..0bb58efc97 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -957,7 +957,7 @@ class RunInventoryUpdate(BaseTask): ec2_opts['cache_path'] = cache_path ec2_opts.setdefault('cache_max_age', '300') for k,v in ec2_opts.items(): - cp.set(section, k, str(v)) + cp.set(section, k, unicode(v)) # Build pyrax creds INI for rax inventory script. elif inventory_update.source == 'rax': section = 'rackspace_cloud' @@ -967,6 +967,15 @@ class RunInventoryUpdate(BaseTask): cp.set(section, 'username', credential.username) cp.set(section, 'api_key', decrypt_field(credential, 'password')) + # Allow custom options to vmware inventory script. + elif inventory_update.source == 'vmware': + section = 'defaults' + cp.add_section(section) + vmware_opts = dict(inventory_update.source_vars_dict.items()) + vmware_opts.setdefault('guests_only', 'True') + for k,v in vmware_opts.items(): + cp.set(section, k, unicode(v)) + # Return INI content. if cp.sections(): f = cStringIO.StringIO() @@ -1027,6 +1036,7 @@ class RunInventoryUpdate(BaseTask): # complain about not being able to determine its version number. env['PBR_VERSION'] = '0.5.21' elif inventory_update.source == 'vmware': + env['VMWARE_INI'] = kwargs.get('private_data_file', '') env['VMWARE_HOST'] = passwords.get('source_host', '') env['VMWARE_USER'] = passwords.get('source_username', '') env['VMWARE_PASSWORD'] = passwords.get('source_password', '') @@ -1042,8 +1052,8 @@ class RunInventoryUpdate(BaseTask): pass elif inventory_update.source == 'custom': for env_k in inventory_update.source_vars_dict: - if env_k not in os.environ: - env[env_k] = unicode(inventory_update.source_vars_dict[env_k]) + if str(env_k) not in os.environ: + env[str(env_k)] = unicode(inventory_update.source_vars_dict[env_k]) return env def build_args(self, inventory_update, **kwargs): @@ -1146,6 +1156,13 @@ class RunSystemJob(BaseTask): json_vars = json.loads(system_job.extra_vars) if 'days' in json_vars: args.extend(['--days', str(json_vars['days'])]) + if system_job.job_type == 'cleanup_jobs': + if 'jobs' in json_vars and json_vars['jobs']: + args.extend(['--jobs']) + if 'project_updates' in json_vars and json_vars['project_updates']: + args.extend(['--project-updates']) + if 'inventory_updates' in json_vars and json_vars['inventory_updates']: + args.extend(['--inventory-updates']) except Exception, e: pass print args diff --git a/awx/main/tests/inventory.py b/awx/main/tests/inventory.py index f2090d8935..35ea310f82 100644 --- a/awx/main/tests/inventory.py +++ b/awx/main/tests/inventory.py @@ -1562,6 +1562,62 @@ class InventoryUpdatesTest(BaseTransactionTest): # its own child). self.assertTrue(self.group in self.inventory.root_groups) + def test_update_from_vmware(self): + source_host = getattr(settings, 'TEST_VMWARE_HOST', '') + source_username = getattr(settings, 'TEST_VMWARE_USER', '') + source_password = getattr(settings, 'TEST_VMWARE_PASSWORD', '') + if not all([source_host, source_username, source_password]): + self.skipTest('no test vmware credentials defined!') + self.create_test_license_file() + credential = Credential.objects.create(kind='vmware', + user=self.super_django_user, + username=source_username, + password=source_password, + host=source_host) + inventory_source = self.update_inventory_source(self.group, + source='vmware', credential=credential) + # Check first without instance_id set (to import by name only). + with self.settings(VMWARE_INSTANCE_ID_VAR=''): + self.check_inventory_source(inventory_source) + # Rename hosts and verify the import picks up the instance_id present + # in host variables. + for host in self.inventory.hosts.all(): + self.assertFalse(host.instance_id, host.instance_id) + if host.enabled: + self.assertTrue(host.variables_dict.get('ansible_ssh_host', '')) + # Test a field that should be present for host systems, not VMs. + self.assertFalse(host.variables_dict.get('vmware_product_name', '')) + host.name = 'updated-%s' % host.name + host.save() + old_host_pks = set(self.inventory.hosts.values_list('pk', flat=True)) + self.check_inventory_source(inventory_source, initial=False) + new_host_pks = set(self.inventory.hosts.values_list('pk', flat=True)) + self.assertEqual(old_host_pks, new_host_pks) + # Manually disable all hosts, verify a new update re-enables them. + # Also change the host name, and verify it is not deleted, but instead + # updated because the instance ID matches. + enabled_host_pks = set(self.inventory.hosts.filter(enabled=True).values_list('pk', flat=True)) + for host in self.inventory.hosts.all(): + host.enabled = False + host.name = 'changed-%s' % host.name + host.save() + old_host_pks = set(self.inventory.hosts.values_list('pk', flat=True)) + self.check_inventory_source(inventory_source, initial=False, enabled_host_pks=enabled_host_pks) + new_host_pks = set(self.inventory.hosts.values_list('pk', flat=True)) + self.assertEqual(old_host_pks, new_host_pks) + # Update again and include host systems in addition to guests. + inventory_source.source_vars = '---\n\nguests_only: false\n' + inventory_source.save() + old_host_pks = set(self.inventory.hosts.values_list('pk', flat=True)) + self.check_inventory_source(inventory_source, initial=False) + new_host_pks = set(self.inventory.hosts.values_list('pk', flat=True)) + self.assertTrue(new_host_pks > old_host_pks) + for host in self.inventory.hosts.filter(pk__in=(new_host_pks - old_host_pks)): + if host.enabled: + self.assertTrue(host.variables_dict.get('ansible_ssh_host', '')) + # Test a field only present for host systems. + self.assertTrue(host.variables_dict.get('vmware_product_name', '')) + def test_update_from_custom_script(self): # Create the inventory script self.create_test_license_file() diff --git a/awx/plugins/inventory/vmware.py b/awx/plugins/inventory/vmware.py index 51817f4129..85b38739b4 100755 --- a/awx/plugins/inventory/vmware.py +++ b/awx/plugins/inventory/vmware.py @@ -1,232 +1,414 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- ''' -VMWARE external inventory script -================================= +VMware Inventory Script +======================= -shamelessly copied from existing inventory scripts. +Retrieve information about virtual machines from a vCenter server or +standalone ESX host. When `group_by=false` (in the INI file), host systems +are also returned in addition to VMs. -This script and it's ini can be used more than once, +This script will attempt to read configuration from an INI file with the same +base filename if present, or `vmware.ini` if not. It is possible to create +symlinks to the inventory script to support multiple configurations, e.g.: -i.e vmware.py/vmware_colo.ini vmware_idf.py/vmware_idf.ini -(script can be link) +* `vmware.py` (this script) +* `vmware.ini` (default configuration, will be read by `vmware.py`) +* `vmware_test.py` (symlink to `vmware.py`) +* `vmware_test.ini` (test configuration, will be read by `vmware_test.py`) +* `vmware_other.py` (symlink to `vmware.py`, will read `vmware.ini` since no + `vmware_other.ini` exists) -so if you don't have clustered vcenter but multiple esx machines or -just diff clusters you can have a inventory per each and automatically -group hosts based on file name or specify a group in the ini. +The path to an INI file may also be specified via the `VMWARE_INI` environment +variable, in which case the filename matching rules above will not apply. + +Host and authentication parameters may be specified via the `VMWARE_HOST`, +`VMWARE_USER` and `VMWARE_PASSWORD` environment variables; these options will +take precedence over options present in the INI file. An INI file is not +required if these options are specified using environment variables. ''' +import collections +import json +import logging +import optparse import os import sys import time import ConfigParser -from psphere.client import Client -from psphere.managedobjects import HostSystem +# Disable logging message trigged by pSphere/suds. try: - import json + from logging import NullHandler except ImportError: - import simplejson as json + from logging import Handler + class NullHandler(Handler): + def emit(self, record): + pass +logging.getLogger('psphere').addHandler(NullHandler()) +logging.getLogger('suds').addHandler(NullHandler()) + +from psphere.client import Client +from psphere.errors import ObjectNotFoundError +from psphere.managedobjects import HostSystem, VirtualMachine, ManagedObject, Network +from suds.sudsobject import Object as SudsObject -def save_cache(cache_item, data, config): - ''' saves item to cache ''' +class VMwareInventory(object): + + def __init__(self, guests_only=None): + self.config = ConfigParser.SafeConfigParser() + if os.environ.get('VMWARE_INI', ''): + config_files = [os.environ['VMWARE_INI']] + else: + config_files = [os.path.abspath(sys.argv[0]).rstrip('.py') + '.ini', 'vmware.ini'] + for config_file in config_files: + if os.path.exists(config_file): + self.config.read(config_file) + break - # Sanity check: Is caching enabled? If not, don't cache. - if not config.has_option('defaults', 'cache_dir'): - return + # Retrieve only guest VMs, or include host systems? + if guests_only is not None: + self.guests_only = guests_only + elif self.config.has_option('defaults', 'guests_only'): + self.guests_only = self.config.getboolean('defaults', 'guests_only') + else: + self.guests_only = True - dpath = config.get('defaults', 'cache_dir') - try: - cache = open('/'.join([dpath,cache_item]), 'w') - cache.write(json.dumps(data)) - cache.close() - except IOError, e: - pass # not really sure what to do here + # Read authentication information from VMware environment variables + # (if set), otherwise from INI file. + auth_host = os.environ.get('VMWARE_HOST') + if not auth_host and self.config.has_option('auth', 'host'): + auth_host = self.config.get('auth', 'host') + auth_user = os.environ.get('VMWARE_USER') + if not auth_user and self.config.has_option('auth', 'user'): + auth_user = self.config.get('auth', 'user') + auth_password = os.environ.get('VMWARE_PASSWORD') + if not auth_password and self.config.has_option('auth', 'password'): + auth_password = self.config.get('auth', 'password') + # Create the VMware client connection. + self.client = Client(auth_host, auth_user, auth_password) -def get_cache(cache_item, config): - ''' returns cached item ''' + def _put_cache(self, name, value): + ''' + Saves the value to cache with the name given. + ''' + if self.config.has_option('defaults', 'cache_dir'): + cache_dir = self.config.get('defaults', 'cache_dir') + if not os.path.exists(cache_dir): + os.makedirs(cache_dir) + cache_file = os.path.join(cache_dir, name) + with open(cache_file, 'w') as cache: + json.dump(value, cache) - # Sanity check: Is caching enabled? If not, return None. - if not config.has_option('defaults', 'cache_dir'): - return + def _get_cache(self, name, default=None): + ''' + Retrieves the value from cache for the given name. + ''' + if self.config.has_option('defaults', 'cache_dir'): + cache_dir = self.config.get('defaults', 'cache_dir') + cache_file = os.path.join(cache_dir, name) + if os.path.exists(cache_file): + if self.config.has_option('defaults', 'cache_max_age'): + cache_max_age = self.config.getint('defaults', 'cache_max_age') + else: + cache_max_age = 0 + cache_stat = os.stat(cache_file) + if (cache_stat.st_mtime + cache_max_age) < time.time(): + with open(cache_file) as cache: + return json.load(cache) + return default - dpath = config.get('defaults', 'cache_dir') - inv = {} - try: - cache = open('/'.join([dpath,cache_item]), 'r') - inv = json.loads(cache.read()) - cache.close() - except IOError, e: - pass # not really sure what to do here + def _flatten_dict(self, d, parent_key='', sep='_'): + ''' + Flatten nested dicts by combining keys with a separator. Lists with + only string items are included as is; any other lists are discarded. + ''' + items = [] + for k, v in d.items(): + if k.startswith('_'): + continue + new_key = parent_key + sep + k if parent_key else k + if isinstance(v, collections.MutableMapping): + items.extend(self._flatten_dict(v, new_key, sep).items()) + elif isinstance(v, (list, tuple)): + if all([isinstance(x, basestring) for x in v]): + items.append((new_key, v)) + else: + items.append((new_key, v)) + return dict(items) - return inv - -def cache_available(cache_item, config): - ''' checks if we have a 'fresh' cache available for item requested ''' - - if config.has_option('defaults', 'cache_dir'): - dpath = config.get('defaults', 'cache_dir') + def _get_obj_info(self, obj, depth=99, seen=None): + ''' + Recursively build a data structure for the given pSphere object (depth + only applies to ManagedObject instances). + ''' + seen = seen or set() + if isinstance(obj, ManagedObject): + try: + obj_unicode = unicode(getattr(obj, 'name')) + except AttributeError: + obj_unicode = () + if obj in seen: + return obj_unicode + seen.add(obj) + if depth <= 0: + return obj_unicode + d = {} + for attr in dir(obj): + if attr.startswith('_'): + continue + try: + val = getattr(obj, attr) + obj_info = self._get_obj_info(val, depth - 1, seen) + if obj_info != (): + d[attr] = obj_info + except Exception, e: + pass + return d + elif isinstance(obj, SudsObject): + d = {} + for key, val in iter(obj): + obj_info = self._get_obj_info(val, depth, seen) + if obj_info != (): + d[key] = obj_info + return d + elif isinstance(obj, (list, tuple)): + l = [] + for val in iter(obj): + obj_info = self._get_obj_info(val, depth, seen) + if obj_info != (): + l.append(obj_info) + return l + elif isinstance(obj, (type(None), bool, int, long, float, basestring)): + return obj + else: + return () + def _get_host_info(self, host, prefix='vmware'): + ''' + Return a flattened dict with info about the given host system. + ''' + host_info = { + 'name': host.name, + 'tag': host.tag, + 'datastores': self._get_obj_info(host.datastore, depth=0), + 'networks': self._get_obj_info(host.network, depth=0), + 'vms': self._get_obj_info(host.vm, depth=0), + } + for k, v in self._get_obj_info(host.summary, depth=0).items(): + if isinstance(v, collections.MutableMapping): + for k2, v2 in v.items(): + host_info[k2] = v2 + elif k != 'host': + host_info[k] = v try: - existing = os.stat( '/'.join([dpath,cache_item])) - except: - # cache doesn't exist or isn't accessible - return False + host_info['ipAddress'] = host.config.network.vnic[0].spec.ip.ipAddress + except Exception, e: + print >> sys.stderr, e + host_info = self._flatten_dict(host_info, prefix) + if ('%s_ipAddress' % prefix) in host_info: + host_info['ansible_ssh_host'] = host_info['%s_ipAddress' % prefix] + return host_info - if config.has_option('defaults', 'cache_max_age'): - maxage = config.get('defaults', 'cache_max_age') + def _get_vm_info(self, vm, prefix='vmware'): + ''' + Return a flattened dict with info about the given virtual machine. + ''' + vm_info = { + 'name': vm.name, + 'tag': vm.tag, + 'datastores': self._get_obj_info(vm.datastore, depth=0), + 'networks': self._get_obj_info(vm.network, depth=0), + 'resourcePool': self._get_obj_info(vm.resourcePool, depth=0), + 'guestState': vm.guest.guestState, + } + for k, v in self._get_obj_info(vm.summary, depth=0).items(): + if isinstance(v, collections.MutableMapping): + for k2, v2 in v.items(): + if k2 == 'host': + k2 = 'hostSystem' + vm_info[k2] = v2 + elif k != 'vm': + vm_info[k] = v + vm_info = self._flatten_dict(vm_info, prefix) + if ('%s_ipAddress' % prefix) in vm_info: + vm_info['ansible_ssh_host'] = vm_info['%s_ipAddress' % prefix] + return vm_info - if (existing.st_mtime - int(time.time())) <= maxage: - return True + def _add_host(self, inv, parent_group, host_name): + ''' + Add the host to the parent group in the given inventory. + ''' + p_group = inv.setdefault(parent_group, []) + if isinstance(p_group, dict): + group_hosts = p_group.setdefault('hosts', []) + else: + group_hosts = p_group + if host_name not in group_hosts: + group_hosts.append(host_name) - return False + def _add_child(self, inv, parent_group, child_group): + ''' + Add a child group to a parent group in the given inventory. + ''' + if parent_group != 'all': + p_group = inv.setdefault(parent_group, {}) + if not isinstance(p_group, dict): + inv[parent_group] = {'hosts': p_group} + p_group = inv[parent_group] + group_children = p_group.setdefault('children', []) + if child_group not in group_children: + group_children.append(child_group) + inv.setdefault(child_group, []) -def get_host_info(host): - ''' Get variables about a specific host ''' + def get_inventory(self, meta_hostvars=True): + ''' + Reads the inventory from cache or VMware API via pSphere. + ''' + # Use different cache names for guests only vs. all hosts. + if self.guests_only: + cache_name = '__inventory_guests__' + else: + cache_name = '__inventory_all__' - hostinfo = { - 'vmware_name' : host.name, - 'vmware_tag' : host.tag, - 'vmware_parent': host.parent.name, - } - for k in host.capability.__dict__.keys(): - if k.startswith('_'): - continue - try: - hostinfo['vmware_' + k] = str(host.capability[k]) - except: - continue + inv = self._get_cache(cache_name, None) + if inv is not None: + return inv - return hostinfo + inv = {'all': {'hosts': []}} + if meta_hostvars: + inv['_meta'] = {'hostvars': {}} - -def get_inventory(client, config): - ''' Reads the inventory from cache or vmware api ''' - - if cache_available('inventory', config): - inv = get_cache('inventory',config) - else: - inv= { 'all': {'hosts': []}, '_meta': { 'hostvars': {} } } default_group = os.path.basename(sys.argv[0]).rstrip('.py') - if config.has_option('defaults', 'guests_only'): - guests_only = config.get('defaults', 'guests_only') - else: - guests_only = True - - if not guests_only: - if config.has_option('defaults','hw_group'): - hw_group = config.get('defaults','hw_group') + if not self.guests_only: + if self.config.has_option('defaults', 'hw_group'): + hw_group = self.config.get('defaults', 'hw_group') else: hw_group = default_group + '_hw' - inv[hw_group] = [] - if config.has_option('defaults','vm_group'): - vm_group = config.get('defaults','vm_group') + if self.config.has_option('defaults', 'vm_group'): + vm_group = self.config.get('defaults', 'vm_group') else: vm_group = default_group + '_vm' - inv[vm_group] = [] # Loop through physical hosts: - hosts = HostSystem.all(client) - for host in hosts: - if not guests_only: - inv['all']['hosts'].append(host.name) - inv[hw_group].append(host.name) - if host.tag: - taggroup = 'vmware_' + host.tag - if taggroup in inv: - inv[taggroup].append(host.name) - else: - inv[taggroup] = [ host.name ] + for host in HostSystem.all(self.client): - inv['_meta']['hostvars'][host.name] = get_host_info(host) - save_cache(vm.name, inv['_meta']['hostvars'][host.name], config) + if not self.guests_only: + self._add_host(inv, 'all', host.name) + self._add_host(inv, hw_group, host.name) + if host.tag: # FIXME: Is this always a string? + host_tag = 'vmware_%s' % host.tag + self._add_host(inv, host_tag, host.name) + host_info = self._get_host_info(host) + if meta_hostvars: + inv['_meta']['hostvars'][host.name] = host_info + self._put_cache(host.name, host_info) + + # Loop through all VMs on physical host. for vm in host.vm: - inv['all']['hosts'].append(vm.name) - inv[vm_group].append(vm.name) - for tag in vm.tag: - taggroup = 'vmware_' + tag.key.lower() - if taggroup in inv: - inv[taggroup].append(vm.name) - else: - inv[taggroup] = [ vm.name ] + self._add_host(inv, 'all', vm.name) + self._add_host(inv, vm_group, vm.name) + if vm.tag: # FIXME: Is this always a string? + vm_tag = 'vmware_%s' % vm.tag + self._add_host(inv, vm_tag, vm.name) + vm_info = self._get_vm_info(vm) + if meta_hostvars: + inv['_meta']['hostvars'][vm.name] = vm_info + self._put_cache(vm.name, vm_info) - inv['_meta']['hostvars'][vm.name] = get_host_info(host) - save_cache(vm.name, inv['_meta']['hostvars'][vm.name], config) + # Group by resource pool. + vm_resourcePool = vm_info.get('vmware_resourcePool', None) + if vm_resourcePool: + self._add_child(inv, vm_group, 'resource_pools') + self._add_child(inv, 'resource_pools', vm_resourcePool) + self._add_host(inv, vm_resourcePool, vm.name) - save_cache('inventory', inv, config) - return json.dumps(inv) + # Group by datastore. + for vm_datastore in vm_info.get('vmware_datastores', []): + self._add_child(inv, vm_group, 'datastores') + self._add_child(inv, 'datastores', vm_datastore) + self._add_host(inv, vm_datastore, vm.name) -def get_single_host(client, config, hostname): + # Group by network. + for vm_network in vm_info.get('vmware_networks', []): + self._add_child(inv, vm_group, 'networks') + self._add_child(inv, 'networks', vm_network) + self._add_host(inv, vm_network, vm.name) - inv = {} + # Group by guest OS. + vm_guestId = vm_info.get('vmware_guestId', None) + if vm_guestId: + self._add_child(inv, vm_group, 'guests') + self._add_child(inv, 'guests', vm_guestId) + self._add_host(inv, vm_guestId, vm.name) - if cache_available(hostname, config): - inv = get_cache(hostname,config) + self._put_cache(cache_name, inv) + return inv + + def get_host(self, hostname): + ''' + Read info about a specific host or VM from cache or VMware API. + ''' + inv = self._get_cache(hostname, None) + if inv is not None: + return inv + + if not self.guests_only: + try: + host = HostSystem.get(self.client, name=hostname) + inv = self._get_host_info(host) + except ObjectNotFoundError: + pass + + if inv is None: + try: + vm = VirtualMachine.get(self.client, name=hostname) + inv = self._get_vm_info(vm) + except ObjectNotFoundError: + pass + + if inv is not None: + self._put_cache(hostname, inv) + return inv or {} + + +def main(): + parser = optparse.OptionParser() + parser.add_option('--list', action='store_true', dest='list', + default=False, help='Output inventory groups and hosts') + parser.add_option('--host', dest='host', default=None, metavar='HOST', + help='Output variables only for the given hostname') + # Additional options for use when running the script standalone, but never + # used by Ansible. + parser.add_option('--pretty', action='store_true', dest='pretty', + default=False, help='Output nicely-formatted JSON') + parser.add_option('--include-host-systems', action='store_true', + dest='include_host_systems', default=False, + help='Include host systems in addition to VMs') + parser.add_option('--no-meta-hostvars', action='store_false', + dest='meta_hostvars', default=True, + help='Exclude [\'_meta\'][\'hostvars\'] with --list') + options, args = parser.parse_args() + + if options.include_host_systems: + vmware_inventory = VMwareInventory(guests_only=False) else: - hosts = HostSystem.all(client) #TODO: figure out single host getter - for host in hosts: - if hostname == host.name: - inv = get_host_info(host) - break - for vm in host.vm: - if hostname == vm.name: - inv = get_host_info(host) - break - save_cache(hostname,inv,config) + vmware_inventory = VMwareInventory() + if options.host is not None: + inventory = vmware_inventory.get_host(options.host) + else: + inventory = vmware_inventory.get_inventory(options.meta_hostvars) + + json_kwargs = {} + if options.pretty: + json_kwargs.update({'indent': 4, 'sort_keys': True}) + json.dump(inventory, sys.stdout, **json_kwargs) - return json.dumps(inv) if __name__ == '__main__': - inventory = {} - hostname = None - - if len(sys.argv) > 1: - if sys.argv[1] == "--host": - hostname = sys.argv[2] - - # Read config - config = ConfigParser.SafeConfigParser( - defaults={'host': '', 'user': '', 'password': ''}, - ) - for section in ('auth', 'defaults'): - config.add_section(section) - for configfilename in [os.path.abspath(sys.argv[0]).rstrip('.py') + '.ini', 'vmware.ini']: - if os.path.exists(configfilename): - config.read(configfilename) - break - - auth_host, auth_user, auth_password = None, None, None - - # Read our authentication information from the INI file, if it exists. - try: - auth_host = config.get('auth', 'host') - auth_user = config.get('auth', 'user') - auth_password = config.get('auth', 'password') - except Exception: - pass - - # If any of the VMware environment variables are set, they trump - # the INI configuration. - if 'VMWARE_HOST' in os.environ: - auth_host = os.environ['VMWARE_HOST'] - if 'VMWARE_USER' in os.environ: - auth_user = os.environ['VMWARE_USER'] - if 'VMWARE_PASSWORD' in os.environ: - auth_password = os.environ['VMWARE_PASSWORD'] - - # Create the VMware client. - client = Client(auth_host, auth_user, auth_password) - - # Actually do the work. - if hostname is None: - inventory = get_inventory(client, config) - else: - inventory = get_single_host(client, config, hostname) - - # Return to Ansible. - print inventory + main() diff --git a/awx/settings/defaults.py b/awx/settings/defaults.py index 778ea98015..7e5cf1b64a 100644 --- a/awx/settings/defaults.py +++ b/awx/settings/defaults.py @@ -399,11 +399,11 @@ VMWARE_REGIONS_BLACKLIST = [] # Inventory variable name/values for determining whether a host is # active in vSphere. -VMWARE_ENABLED_VAR = 'status' -VMWARE_ENABLED_VALUE = 'POWERED ON' +VMWARE_ENABLED_VAR = 'vmware_powerState' +VMWARE_ENABLED_VALUE = 'poweredOn' # Inventory variable name containing the unique instance ID. -VMWARE_INSTANCE_ID_VAR = 'guest_id' +VMWARE_INSTANCE_ID_VAR = 'vmware_uuid' # Filter for allowed group and host names when importing inventory # from EC2. diff --git a/awx/settings/local_settings.py.example b/awx/settings/local_settings.py.example index 31a827f25d..40b3f1d8d9 100644 --- a/awx/settings/local_settings.py.example +++ b/awx/settings/local_settings.py.example @@ -484,3 +484,8 @@ TEST_AWS_REGIONS = 'all' TEST_RACKSPACE_USERNAME = '' TEST_RACKSPACE_API_KEY = '' TEST_RACKSPACE_REGIONS = 'all' + +# VMware credentials +TEST_VMWARE_HOST = '' +TEST_VMWARE_USER = '' +TEST_VMWARE_PASSWORD = '' diff --git a/awx/ui/static/js/config.js b/awx/ui/static/js/config.js index abed710242..ce064c6eba 100644 --- a/awx/ui/static/js/config.js +++ b/awx/ui/static/js/config.js @@ -19,7 +19,7 @@ tooltip_delay: {show: 500, hide: 100}, // Default number of milliseconds to delay displaying/hiding tooltips - debug_mode: false, // Enable console logging messages + debug_mode: true, // Enable console logging messages password_strength: 45, // User password strength. Integer between 0 and 100, 100 being impossibly strong. // This value controls progress bar colors: diff --git a/awx/ui/static/js/controllers/JobTemplates.js b/awx/ui/static/js/controllers/JobTemplates.js index fa22ecc88e..54c4b376d6 100644 --- a/awx/ui/static/js/controllers/JobTemplates.js +++ b/awx/ui/static/js/controllers/JobTemplates.js @@ -16,7 +16,7 @@ function JobTemplatesList($scope, $rootScope, $location, $log, $routeParams, Rest, Alert, JobTemplateList, GenerateList, LoadBreadCrumbs, Prompt, SearchInit, PaginateInit, ReturnToCaller, ClearScope, ProcessErrors, - GetBasePath, JobTemplateForm, CredentialList, LookUpInit, PlaybookRun, Wait, Stream) { + GetBasePath, JobTemplateForm, CredentialList, LookUpInit, PlaybookRun, Wait, Stream, CreateDialog, $compile) { ClearScope(); @@ -97,6 +97,138 @@ function JobTemplatesList($scope, $rootScope, $location, $log, $routeParams, Res }); }; + $scope.copyJobTemplate = function(id, name){ + var element, + buttons = [{ + "label": "Cancel", + "onClick": function() { + $(this).dialog('close'); + }, + "icon": "fa-times", + "class": "btn btn-default", + "id": "copy-close-button" + },{ + "label": "Copy", + "onClick": function() { + copyAction(); + // setTimeout(function(){ + // scope.$apply(function(){ + // if(mode==='survey-taker'){ + // scope.$emit('SurveyTakerCompleted'); + // } else{ + // scope.saveSurvey(); + // } + // }); + // }); + }, + "icon": "fa-copy", + "class": "btn btn-primary", + "id": "job-copy-button" + }], + copyAction = function () { + // retrieve the copy of the job template object from the api, then overwrite the name and throw away the id + Wait('start'); + var url = defaultUrl + id + '/'; + Rest.setUrl(url); + Rest.get() + .success(function (data) { + data.name = $scope.new_copy_name; + delete data.id; + $scope.$emit('GoToCopy', data); + }) + .error(function (data) { + Wait('stop'); + ProcessErrors($scope, data, status, null, { hdr: 'Error!', + msg: 'Call to ' + url + ' failed. DELETE returned status: ' + status }); + }); + }; + + + CreateDialog({ + id: 'copy-job-modal' , + title: "Copy", + scope: $scope, + buttons: buttons, + width: 500, + height: 300, + minWidth: 200, + callback: 'CopyDialogReady' + }); + + $('#job_name').text(name); + $('#copy-job-modal').show(); + + + if ($scope.removeCopyDialogReady) { + $scope.removeCopyDialogReady(); + } + $scope.removeCopyDialogReady = $scope.$on('CopyDialogReady', function() { + $('#copy-job-modal').dialog('open'); + $('#job-copy-button').attr('ng-disabled', "!copy_form.$valid"); + element = angular.element(document.getElementById('job-copy-button')); + $compile(element)($scope); + }); + + if ($scope.removeGoToCopy) { + $scope.removeGoToCopy(); + } + $scope.removeGoToCopy = $scope.$on('GoToCopy', function(e, data) { + var url = defaultUrl, + old_survey_url = (data.related.survey_spec) ? data.related.survey_spec : "" ; + Rest.setUrl(url); + Rest.post(data) + .success(function (data) { + if(data.survey_enabled===true){ + $scope.$emit("CopySurvey", data, old_survey_url); + } + else { + $('#copy-job-modal').dialog('close'); + Wait('stop'); + $location.path($location.path() + '/' + data.id); + } + + }) + .error(function (data) { + Wait('stop'); + ProcessErrors($scope, data, status, null, { hdr: 'Error!', + msg: 'Call to ' + url + ' failed. DELETE returned status: ' + status }); + }); + }); + + if ($scope.removeCopySurvey) { + $scope.removeCopySurvey(); + } + $scope.removeCopySurvey = $scope.$on('CopySurvey', function(e, new_data, old_url) { + // var url = data.related.survey_spec; + Rest.setUrl(old_url); + Rest.get() + .success(function (survey_data) { + + Rest.setUrl(new_data.related.survey_spec); + Rest.post(survey_data) + .success(function () { + $('#copy-job-modal').dialog('close'); + Wait('stop'); + $location.path($location.path() + '/' + new_data.id); + }) + .error(function (data) { + Wait('stop'); + ProcessErrors($scope, data, status, null, { hdr: 'Error!', + msg: 'Call to ' + new_data.related.survey_spec + ' failed. DELETE returned status: ' + status }); + }); + + + }) + .error(function (data) { + Wait('stop'); + ProcessErrors($scope, data, status, null, { hdr: 'Error!', + msg: 'Call to ' + old_url + ' failed. DELETE returned status: ' + status }); + }); + + }); + + }; + $scope.submitJob = function (id) { PlaybookRun({ scope: $scope, id: id }); }; @@ -105,7 +237,7 @@ function JobTemplatesList($scope, $rootScope, $location, $log, $routeParams, Res JobTemplatesList.$inject = ['$scope', '$rootScope', '$location', '$log', '$routeParams', 'Rest', 'Alert', 'JobTemplateList', 'GenerateList', 'LoadBreadCrumbs', 'Prompt', 'SearchInit', 'PaginateInit', 'ReturnToCaller', 'ClearScope', 'ProcessErrors', 'GetBasePath', 'JobTemplateForm', 'CredentialList', 'LookUpInit', - 'PlaybookRun', 'Wait', 'Stream' + 'PlaybookRun', 'Wait', 'Stream', 'CreateDialog' , '$compile' ]; function JobTemplatesAdd($scope, $rootScope, $compile, $location, $log, $routeParams, JobTemplateForm, @@ -339,18 +471,21 @@ function JobTemplatesAdd($scope, $rootScope, $compile, $location, $log, $routePa .success(function(data) { $scope.$emit('templateSaveSuccess', data); - //once the job template information is saved we submit the survey info to the correct endpoint - var url = data.url+ 'survey_spec/'; - Rest.setUrl(url); - Rest.post({ name: $scope.survey_name, description: $scope.survey_description, spec: $scope.survey_questions }) - .success(function () { - Wait('stop'); + if(data.survey_enabled===true){ + //once the job template information is saved we submit the survey info to the correct endpoint + var url = data.url+ 'survey_spec/'; + Rest.setUrl(url); + Rest.post({ name: $scope.survey_name, description: $scope.survey_description, spec: $scope.survey_questions }) + .success(function () { + Wait('stop'); + + }) + .error(function (data, status) { + ProcessErrors($scope, data, status, form, { hdr: 'Error!', + msg: 'Failed to add new survey. Post returned status: ' + status }); + }); + } - }) - .error(function (data, status) { - ProcessErrors($scope, data, status, form, { hdr: 'Error!', - msg: 'Failed to add new survey. Post returned status: ' + status }); - }); }) .error(function (data, status) { @@ -413,8 +548,7 @@ function JobTemplatesEdit($scope, $rootScope, $compile, $location, $log, $routeP Alert, ProcessErrors, LoadBreadCrumbs, RelatedSearchInit, RelatedPaginateInit, ReturnToCaller, ClearScope, InventoryList, CredentialList, ProjectList, LookUpInit, GetBasePath, md5Setup, ParseTypeChange, JobStatusToolTip, FormatDate, Wait, Stream, Empty, Prompt, ParseVariableString, ToJSON, SchedulesControllerInit, JobsControllerInit, JobsListUpdate, - GetChoices, SchedulesListInit, SchedulesList, CallbackHelpInit, PlaybookRun, SurveyControllerInit) -{ + GetChoices, SchedulesListInit, SchedulesList, CallbackHelpInit, PlaybookRun, SurveyControllerInit){ ClearScope(); @@ -429,6 +563,7 @@ function JobTemplatesEdit($scope, $rootScope, $compile, $location, $log, $routeP checkSCMStatus, getPlaybooks, callback, choicesCount = 0; + CallbackHelpInit({ scope: $scope }); SchedulesList.well = false; @@ -703,122 +838,7 @@ function JobTemplatesEdit($scope, $rootScope, $compile, $location, $log, $routeP $scope.rmoveLoadJobs(); } $scope.removeLoadJobs = $scope.$on('LoadJobs', function() { - // Retrieve detail record and prepopulate the form - Rest.setUrl(defaultUrl + id); - Rest.get() - .success(function (data) { - var fld, i; - LoadBreadCrumbs({ path: '/job_templates/' + id, title: data.name }); - for (fld in form.fields) { - if (fld !== 'variables' && data[fld] !== null && data[fld] !== undefined) { - if (form.fields[fld].type === 'select') { - if ($scope[fld + '_options'] && $scope[fld + '_options'].length > 0) { - for (i = 0; i < $scope[fld + '_options'].length; i++) { - if (data[fld] === $scope[fld + '_options'][i].value) { - $scope[fld] = $scope[fld + '_options'][i]; - } - } - } else { - $scope[fld] = data[fld]; - } - } else { - $scope[fld] = data[fld]; - if(fld ==='survey_enabled'){ - // $scope.$emit('EnableSurvey', fld); - $('#job_templates_survey_enabled_chbox').attr('checked', $scope[fld]); - if(Empty(data.summary_fields.survey)) { - $('#job_templates_delete_survey_btn').hide(); - $('#job_templates_edit_survey_btn').hide(); - $('#job_templates_create_survey_btn').show(); - } - else{ - $('#job_templates_delete_survey_btn').show(); - $('#job_templates_edit_survey_btn').show(); - $('#job_templates_create_survey_btn').hide(); - $scope.survey_exists = true; - } - } - } - master[fld] = $scope[fld]; - } - if (fld === 'variables') { - // Parse extra_vars, converting to YAML. - $scope.variables = ParseVariableString(data.extra_vars); - master.variables = $scope.variables; - } - if (form.fields[fld].type === 'lookup' && data.summary_fields[form.fields[fld].sourceModel]) { - $scope[form.fields[fld].sourceModel + '_' + form.fields[fld].sourceField] = - data.summary_fields[form.fields[fld].sourceModel][form.fields[fld].sourceField]; - master[form.fields[fld].sourceModel + '_' + form.fields[fld].sourceField] = - $scope[form.fields[fld].sourceModel + '_' + form.fields[fld].sourceField]; - } - } - - $scope.url = data.url; - - $scope.ask_variables_on_launch = (data.ask_variables_on_launch) ? 'true' : 'false'; - master.ask_variables_on_launch = $scope.ask_variables_on_launch; - - relatedSets = form.relatedSets(data.related); - - if (data.host_config_key) { - $scope.example_config_key = data.host_config_key; - } - $scope.example_template_id = id; - $scope.setCallbackHelp(); - - $scope.callback_url = $scope.callback_server_path + ((data.related.callback) ? data.related.callback : - GetBasePath('job_templates') + id + '/callback/'); - master.callback_url = $scope.callback_url; - - LookUpInit({ - scope: $scope, - form: form, - current_item: data.inventory, - list: InventoryList, - field: 'inventory', - input_type: "radio" - }); - - LookUpInit({ - url: GetBasePath('credentials') + '?kind=ssh', - scope: $scope, - form: form, - current_item: data.credential, - list: CredentialList, - field: 'credential', - hdr: 'Select Machine Credential', - input_type: "radio" - }); - - LookUpInit({ - scope: $scope, - form: form, - current_item: data.project, - list: ProjectList, - field: 'project', - input_type: "radio" - }); - - RelatedSearchInit({ - scope: $scope, - form: form, - relatedSets: relatedSets - }); - - RelatedPaginateInit({ - scope: $scope, - relatedSets: relatedSets - }); - - $scope.$emit('jobTemplateLoaded', data.related.cloud_credential); - }) - .error(function (data, status) { - ProcessErrors($scope, data, status, form, { - hdr: 'Error!', - msg: 'Failed to retrieve job template: ' + $routeParams.template_id + '. GET status: ' + status - }); - }); + $scope.fillJobTemplate(); }); if ($scope.removeChoicesReady) { diff --git a/awx/ui/static/js/controllers/Jobs.js b/awx/ui/static/js/controllers/Jobs.js index 40049a8d5c..7f95a68d0b 100644 --- a/awx/ui/static/js/controllers/Jobs.js +++ b/awx/ui/static/js/controllers/Jobs.js @@ -79,6 +79,8 @@ function JobsListController ($rootScope, $log, $scope, $compile, $routeParams, C queued_scope.search('queued_job'); break; case 'successful': + completed_scope.search('completed_job'); + break; case 'failed': case 'error': case 'canceled': diff --git a/awx/ui/static/js/controllers/Portal.js b/awx/ui/static/js/controllers/Portal.js index 6623fc62c4..c936f7be4a 100644 --- a/awx/ui/static/js/controllers/Portal.js +++ b/awx/ui/static/js/controllers/Portal.js @@ -31,8 +31,7 @@ function PortalController($scope, $compile, $routeParams, $rootScope, $location, var html, e, - // winHeight, - // available_height, + jobs_scope, list = PortalJobTemplateList, view= GenerateList, defaultUrl = GetBasePath('job_templates'), @@ -112,12 +111,60 @@ function PortalController($scope, $compile, $routeParams, $rootScope, $location, if ($scope.removeWidgetLoaded) { $scope.removeWidgetLoaded(); } - $scope.removeWidgetLoaded = $scope.$on('WidgetLoaded', function () { + $scope.removeWidgetLoaded = $scope.$on('WidgetLoaded', function (e, label, jobscope) { + if(label==="portal_jobs"){ + jobs_scope = jobscope; + } $('.actions-column:eq(0)').text('Launch'); $('.actions-column:eq(1)').text('Details'); $('.list-well:eq(1)').css('margin-top' , '0px'); }); + // function processEvent(event) { + // switch(event.status) { + // case 'running': + // jobs_scope.search('running_job'); + // jobs_scope.search('queued_job'); + + // break; + // case 'new': + // case 'pending': + // case 'waiting': + // jobs_scope.search('queued_job'); + + // break; + // case 'successful': + // jobs_scope.search('completed_job'); + // case 'failed': + // case 'error': + // case 'canceled': + // jobs_scope.search('completed_job'); + // jobs_scope.search('running_job'); + // jobs_scope.search('queued_job'); + // } + // } + + if ($rootScope.removeJobStatusChange) { + $rootScope.removeJobStatusChange(); + } + $rootScope.removeJobStatusChange = $rootScope.$on('JobStatusChange', function() { + jobs_scope.refreshJobs(); + // if(data.status==='pending'){ + // // $scope.refresh(); + // $('#portal-jobs').empty(); + // // $rootScope.flashMessage = null; + // PortalJobsWidget({ + // scope: $scope, + // target: 'portal-jobs', + // searchSize: 'col-lg-6 col-md-6' + // }); + // } + + + //x`processEvent(data); + + }); + $scope.submitJob = function (id) { PlaybookRun({ scope: $scope, id: id }); }; diff --git a/awx/ui/static/js/helpers/JobSubmission.js b/awx/ui/static/js/helpers/JobSubmission.js index 855c3c1d43..411a1d85b8 100644 --- a/awx/ui/static/js/helpers/JobSubmission.js +++ b/awx/ui/static/js/helpers/JobSubmission.js @@ -760,7 +760,7 @@ function($location, Wait, GetBasePath, LookUpInit, JobTemplateForm, CredentialLi } scope.removePlaybookLaunchFinished = scope.$on('PlaybookLaunchFinished', function(e, data) { //var base = $location.path().replace(/^\//, '').split('/')[0]; - if(scope.portalMode===false){ + if(scope.portalMode===false || scope.$parent.portalMode===false){ $location.path('/jobs/' + data.job); } diff --git a/awx/ui/static/js/helpers/JobTemplates.js b/awx/ui/static/js/helpers/JobTemplates.js index 42dbd0435d..d5a772b4e9 100644 --- a/awx/ui/static/js/helpers/JobTemplates.js +++ b/awx/ui/static/js/helpers/JobTemplates.js @@ -19,10 +19,24 @@ angular.module('JobTemplatesHelper', ['Utilities']) * Add bits to $scope for handling callback url help * */ -.factory('CallbackHelpInit', ['$location', 'GetBasePath', function($location, GetBasePath) { + +.factory('CallbackHelpInit', ['$location', 'GetBasePath', 'Rest', 'JobTemplateForm', 'GenerateForm', '$routeParams', 'LoadBreadCrumbs', 'ProcessErrors', 'ParseTypeChange', + 'ParseVariableString', 'Empty', 'LookUpInit', 'InventoryList', 'CredentialList','ProjectList', 'RelatedSearchInit', 'RelatedPaginateInit', + function($location, GetBasePath, Rest, JobTemplateForm, GenerateForm, $routeParams, LoadBreadCrumbs, ProcessErrors,ParseTypeChange, + ParseVariableString, Empty, LookUpInit, InventoryList, CredentialList, ProjectList, RelatedSearchInit, RelatedPaginateInit) { return function(params) { - var scope = params.scope; + var scope = params.scope, + defaultUrl = GetBasePath('job_templates'), + // generator = GenerateForm, + form = JobTemplateForm(), + // loadingFinishedCount = 0, + // base = $location.path().replace(/^\//, '').split('/')[0], + master = {}, + id = $routeParams.template_id, + relatedSets = {}; + // checkSCMStatus, getPlaybooks, callback, + // choicesCount = 0; // The form uses awPopOverWatch directive to 'watch' scope.callback_help for changes. Each time the // popover is activated, a function checks the value of scope.callback_help before constructing the content. @@ -56,8 +70,132 @@ angular.module('JobTemplatesHelper', ['Utilities']) scope.example_config_key = '5a8ec154832b780b9bdef1061764ae5a'; scope.example_template_id = 'N'; scope.setCallbackHelp(); + + scope.fillJobTemplate = function(){ + // id = id || $rootScope.copy.id; + // Retrieve detail record and prepopulate the form + Rest.setUrl(defaultUrl + id); + Rest.get() + .success(function (data) { + var fld, i; + LoadBreadCrumbs({ path: '/job_templates/' + id, title: data.name }); + for (fld in form.fields) { + if (fld !== 'variables' && data[fld] !== null && data[fld] !== undefined) { + if (form.fields[fld].type === 'select') { + if (scope[fld + '_options'] && scope[fld + '_options'].length > 0) { + for (i = 0; i < scope[fld + '_options'].length; i++) { + if (data[fld] === scope[fld + '_options'][i].value) { + scope[fld] = scope[fld + '_options'][i]; + } + } + } else { + scope[fld] = data[fld]; + } + } else { + scope[fld] = data[fld]; + if(fld ==='survey_enabled'){ + // $scope.$emit('EnableSurvey', fld); + $('#job_templates_survey_enabled_chbox').attr('checked', scope[fld]); + if(Empty(data.summary_fields.survey)) { + $('#job_templates_delete_survey_btn').hide(); + $('#job_templates_edit_survey_btn').hide(); + $('#job_templates_create_survey_btn').show(); + } + else{ + $('#job_templates_delete_survey_btn').show(); + $('#job_templates_edit_survey_btn').show(); + $('#job_templates_create_survey_btn').hide(); + scope.survey_exists = true; + } + } + } + master[fld] = scope[fld]; + } + if (fld === 'variables') { + // Parse extra_vars, converting to YAML. + scope.variables = ParseVariableString(data.extra_vars); + master.variables = scope.variables; + } + if (form.fields[fld].type === 'lookup' && data.summary_fields[form.fields[fld].sourceModel]) { + scope[form.fields[fld].sourceModel + '_' + form.fields[fld].sourceField] = + data.summary_fields[form.fields[fld].sourceModel][form.fields[fld].sourceField]; + master[form.fields[fld].sourceModel + '_' + form.fields[fld].sourceField] = + scope[form.fields[fld].sourceModel + '_' + form.fields[fld].sourceField]; + } + } + + scope.url = data.url; + + scope.ask_variables_on_launch = (data.ask_variables_on_launch) ? 'true' : 'false'; + master.ask_variables_on_launch = scope.ask_variables_on_launch; + + relatedSets = form.relatedSets(data.related); + + if (data.host_config_key) { + scope.example_config_key = data.host_config_key; + } + scope.example_template_id = id; + scope.setCallbackHelp(); + + scope.callback_url = scope.callback_server_path + ((data.related.callback) ? data.related.callback : + GetBasePath('job_templates') + id + '/callback/'); + master.callback_url = scope.callback_url; + + LookUpInit({ + scope: scope, + form: form, + current_item: data.inventory, + list: InventoryList, + field: 'inventory', + input_type: "radio" + }); + + LookUpInit({ + url: GetBasePath('credentials') + '?kind=ssh', + scope: scope, + form: form, + current_item: data.credential, + list: CredentialList, + field: 'credential', + hdr: 'Select Machine Credential', + input_type: "radio" + }); + + LookUpInit({ + scope: scope, + form: form, + current_item: data.project, + list: ProjectList, + field: 'project', + input_type: "radio" + }); + + RelatedSearchInit({ + scope: scope, + form: form, + relatedSets: relatedSets + }); + + RelatedPaginateInit({ + scope: scope, + relatedSets: relatedSets + }); + + + scope.$emit('jobTemplateLoaded', data.related.cloud_credential); + }) + .error(function (data, status) { + ProcessErrors(scope, data, status, form, { + hdr: 'Error!', + msg: 'Failed to retrieve job template: ' + $routeParams.template_id + '. GET status: ' + status + }); + }); + }; + }; + + }]); diff --git a/awx/ui/static/js/helpers/Variables.js b/awx/ui/static/js/helpers/Variables.js index c9a613b7e7..70d74161d7 100644 --- a/awx/ui/static/js/helpers/Variables.js +++ b/awx/ui/static/js/helpers/Variables.js @@ -32,6 +32,7 @@ angular.module('VariablesHelper', ['Utilities']) json_obj = JSON.parse(variables); json_obj = SortVariables(json_obj); result = jsyaml.safeDump(json_obj); + } catch (e) { $log.info('Attempt to parse extra_vars as JSON failed. Attempting to parse as YAML'); @@ -56,6 +57,7 @@ angular.module('VariablesHelper', ['Utilities']) try { json_obj = SortVariables(variables); result = jsyaml.safeDump(json_obj); + // result = variables; } catch(e3) { ProcessErrors(null, variables, e3.message, null, { hdr: 'Error!', @@ -78,7 +80,11 @@ angular.module('VariablesHelper', ['Utilities']) **/ .factory('ToJSON', ['$log', 'ProcessErrors', function($log, ProcessErrors) { return function(parseType, variables, stringify, reviver) { - var json_data, result; + var json_data, + result; + // bracketVar, + // key, + // lines, i, newVars = []; if (parseType === 'json') { try { //parse a JSON string @@ -97,7 +103,30 @@ angular.module('VariablesHelper', ['Utilities']) } } else { try { + // if(variables.indexOf('{{')>-1){ + // lines = variables.split(/\n/); + // for(i=0; i-1){ + // lines[i] = lines[i].replace('{{', '"{{'); + // // lines[i] = lines[i].replace(lines[i].lastIndexOf('}}'), '}}"'); + // lines[i] = lines[i]+'"'; + // // newVars = newVars+ lines[i]; + // newVars.push(lines[i]) + + // } + // } + // json_data = jsyaml.load(newVars.toString()); + + // bracketVar = variables.substr(variables.indexOf('{{'), variables.indexOf('}}')); + // bracketVar = bracketVar.trimRight(); + // key = variables.substr(0, variables.indexOf(':')); + // json_data = jsyaml.load(variables); + // json_data[key] = bracketVar; + // } + // else json_data = jsyaml.load(variables); + + } catch(e) { json_data = {}; diff --git a/awx/ui/static/js/lists/JobTemplates.js b/awx/ui/static/js/lists/JobTemplates.js index 83301dceea..a17314e4fa 100644 --- a/awx/ui/static/js/lists/JobTemplates.js +++ b/awx/ui/static/js/lists/JobTemplates.js @@ -64,7 +64,6 @@ angular.module('JobTemplatesListDefinition', []) ngHref: '#/job_templates/{{ job_template.id }}/schedules', awToolTip: 'Schedule future job template runs', dataPlacement: 'top', - ngHide: 'portalMode===true' }, edit: { label: 'Edit', @@ -72,7 +71,6 @@ angular.module('JobTemplatesListDefinition', []) awToolTip: 'Edit template', "class": 'btn-default btn-xs', dataPlacement: 'top', - ngHide: 'portalMode===true' }, "delete": { label: 'Delete', @@ -80,7 +78,14 @@ angular.module('JobTemplatesListDefinition', []) "class": 'btn-danger btn-xs', awToolTip: 'Delete template', dataPlacement: 'top', - ngHide: 'portalMode===true' + }, + copy: { + label: 'Copy', + ngClick: "copyJobTemplate(job_template.id, job_template.name)", + "class": 'btn-danger btn-xs', + awToolTip: 'Copy template', + dataPlacement: 'top', + } } }); diff --git a/awx/ui/static/js/widgets/PortalJobs.js b/awx/ui/static/js/widgets/PortalJobs.js index 94a3a34fa0..3b2a65c7dd 100644 --- a/awx/ui/static/js/widgets/PortalJobs.js +++ b/awx/ui/static/js/widgets/PortalJobs.js @@ -19,6 +19,9 @@ angular.module('PortalJobsWidget', ['RestServices', 'Utilities']) choicesCount = 0, listCount = 0, jobs_scope = scope.$new(true), + // completed_scope = scope.$new(true), + // running_scope = scope.$new(true), + // queued_scope = scope.$new(true), // scheduled_scope = scope.$new(true), max_rows, html, e; @@ -56,7 +59,7 @@ angular.module('PortalJobsWidget', ['RestServices', 'Utilities']) listCount++; if (listCount === 1) { //api_complete = true; - scope.$emit('WidgetLoaded', jobs_scope); + scope.$emit('WidgetLoaded', "portal_jobs", jobs_scope); } }); @@ -75,16 +78,36 @@ angular.module('PortalJobsWidget', ['RestServices', 'Utilities']) id: 'active-jobs', url: GetBasePath('unified_jobs') + '?status__in=running,completed,failed,successful,error,canceled', pageSize: max_rows, - spinner: false + spinner: true }); - // LoadSchedulesScope({ + + // completed_scope.showJobType = true; + // LoadJobsScope({ // parent_scope: scope, - // scope: scheduled_scope, - // list: ScheduledJobsList, - // id: 'scheduled-jobs-tab', - // url: GetBasePath('schedules') + '?next_run__isnull=false', - // pageSize: max_rows, - // spinner: false + // scope: completed_scope, + // list: PortalJobsList, + // id: 'active-jobs', + // url: GetBasePath('unified_jobs') + '?or__status=successful&or__status=failed&or__status=error&or__status=canceled', + // // searchParams: search_params, + // pageSize: max_rows + // }); + + // LoadJobsScope({ + // parent_scope: scope, + // scope: running_scope, + // list: PortalJobsList, + // id: 'active-jobs', + // url: GetBasePath('unified_jobs') + '?status=running', + // pageSize: max_rows + // }); + + // LoadJobsScope({ + // parent_scope: scope, + // scope: queued_scope, + // list: PortalJobsList, + // id: 'active-jobs', + // url: GetBasePath('unified_jobs') + '?or__status=pending&or__status=waiting&or__status=new', + // pageSize: max_rows // }); $(window).resize(_.debounce(function() { diff --git a/awx/ui/static/lib/ansible/Socket.js b/awx/ui/static/lib/ansible/Socket.js index 0a01415b84..fc36d7ddea 100644 --- a/awx/ui/static/lib/ansible/Socket.js +++ b/awx/ui/static/lib/ansible/Socket.js @@ -27,7 +27,9 @@ angular.module('SocketIO', ['AuthService', 'Utilities']) host = $location.host(), endpoint = params.endpoint, protocol = $location.protocol(), - config, socketPort, url; + config, socketPort, + // handshakeData, + url; // Since some pages are opened in a new tab, we might get here before AnsibleConfig is available. // In that case, load from local storage. @@ -61,14 +63,23 @@ angular.module('SocketIO', ['AuthService', 'Utilities']) scope: scope, url: url, socket: null, + init: function() { var self = this, token = Authorization.getToken(); if (!$rootScope.sessionTimer || ($rootScope.sessionTimer && !$rootScope.sessionTimer.isExpired())) { - // We have a valid session token, so attmempt socket connection + // We have a valid session token, so attempt socket connection $log.debug('Socket connecting to: ' + url); self.scope.socket_url = url; - self.socket = io.connect(url, { headers: + // handshakeData = { + // headers: { + // 'Authorization': 'Token ' + token, + // 'X-Auth-Token': 'Token ' + token + // } + // } + + self.socket = io.connect(url, { + headers: { 'Authorization': 'Token ' + token, 'X-Auth-Token': 'Token ' + token @@ -77,7 +88,9 @@ angular.module('SocketIO', ['AuthService', 'Utilities']) 'try multiple transports': false, 'max reconneciton attemps': 3, 'reconnection limit': 3000, + 'force new connection': true, }); + self.socket.on('connection', function() { $log.debug('Socket connecting...'); self.scope.$apply(function () { diff --git a/awx/ui/static/lib/ansible/generator-helpers.js b/awx/ui/static/lib/ansible/generator-helpers.js index d55cb45f01..9036ff3911 100644 --- a/awx/ui/static/lib/ansible/generator-helpers.js +++ b/awx/ui/static/lib/ansible/generator-helpers.js @@ -159,7 +159,7 @@ angular.module('GeneratorHelpers', []) icon = 'fa-list-ul'; break; case 'copy': - icon = "fa-cut"; + icon = "fa-copy"; break; } icon += (size) ? " " + size : ""; diff --git a/awx/ui/static/partials/job_detail.html b/awx/ui/static/partials/job_detail.html index 1cb05da748..68142f2523 100644 --- a/awx/ui/static/partials/job_detail.html +++ b/awx/ui/static/partials/job_detail.html @@ -20,8 +20,8 @@ - - + + diff --git a/awx/ui/static/partials/job_templates.html b/awx/ui/static/partials/job_templates.html index 2d5e9ed45b..df51fc051c 100644 --- a/awx/ui/static/partials/job_templates.html +++ b/awx/ui/static/partials/job_templates.html @@ -3,4 +3,12 @@
+ + \ No newline at end of file