Merge branch 'devel' into can_CRUD

This commit is contained in:
AlanCoding
2016-09-12 08:56:47 -04:00
40 changed files with 1400 additions and 11815 deletions

View File

@@ -170,7 +170,7 @@ endif
.DEFAULT_GOAL := build .DEFAULT_GOAL := build
.PHONY: clean clean-tmp rebase push requirements requirements_dev \ .PHONY: clean clean-tmp clean-venv rebase push requirements requirements_dev \
requirements_jenkins \ requirements_jenkins \
develop refresh adduser migrate dbchange dbshell runserver celeryd \ develop refresh adduser migrate dbchange dbshell runserver celeryd \
receiver test test_unit test_coverage coverage_html test_jenkins dev_build \ receiver test test_unit test_coverage coverage_html test_jenkins dev_build \
@@ -216,6 +216,9 @@ clean-ui:
clean-tmp: clean-tmp:
rm -rf tmp/ rm -rf tmp/
clean-venv:
rm -rf venv/
# Remove temporary build files, compiled Python files. # Remove temporary build files, compiled Python files.
clean: clean-rpm clean-deb clean-ui clean-tar clean-packer clean-bundle clean: clean-rpm clean-deb clean-ui clean-tar clean-packer clean-bundle
rm -rf awx/lib/site-packages rm -rf awx/lib/site-packages

View File

@@ -19,7 +19,7 @@ from awx.main.utils import get_object_or_400
logger = logging.getLogger('awx.api.permissions') logger = logging.getLogger('awx.api.permissions')
__all__ = ['ModelAccessPermission', 'JobTemplateCallbackPermission', __all__ = ['ModelAccessPermission', 'JobTemplateCallbackPermission',
'TaskPermission', 'ProjectUpdatePermission'] 'TaskPermission', 'ProjectUpdatePermission', 'UserPermission']
class ModelAccessPermission(permissions.BasePermission): class ModelAccessPermission(permissions.BasePermission):
''' '''
@@ -205,3 +205,10 @@ class ProjectUpdatePermission(ModelAccessPermission):
def check_post_permissions(self, request, view, obj=None): def check_post_permissions(self, request, view, obj=None):
project = get_object_or_400(view.model, pk=view.kwargs['pk']) project = get_object_or_400(view.model, pk=view.kwargs['pk'])
return check_user_access(request.user, view.model, 'start', project) return check_user_access(request.user, view.model, 'start', project)
class UserPermission(ModelAccessPermission):
def check_post_permissions(self, request, view, obj=None):
if request.user.is_superuser:
return True
raise PermissionDenied()

View File

@@ -60,6 +60,7 @@ project_urls = patterns('awx.api.views',
) )
project_update_urls = patterns('awx.api.views', project_update_urls = patterns('awx.api.views',
url(r'^$', 'project_update_list'),
url(r'^(?P<pk>[0-9]+)/$', 'project_update_detail'), url(r'^(?P<pk>[0-9]+)/$', 'project_update_detail'),
url(r'^(?P<pk>[0-9]+)/cancel/$', 'project_update_cancel'), url(r'^(?P<pk>[0-9]+)/cancel/$', 'project_update_cancel'),
url(r'^(?P<pk>[0-9]+)/stdout/$', 'project_update_stdout'), url(r'^(?P<pk>[0-9]+)/stdout/$', 'project_update_stdout'),
@@ -145,6 +146,7 @@ inventory_source_urls = patterns('awx.api.views',
) )
inventory_update_urls = patterns('awx.api.views', inventory_update_urls = patterns('awx.api.views',
url(r'^$', 'inventory_update_list'),
url(r'^(?P<pk>[0-9]+)/$', 'inventory_update_detail'), url(r'^(?P<pk>[0-9]+)/$', 'inventory_update_detail'),
url(r'^(?P<pk>[0-9]+)/cancel/$', 'inventory_update_cancel'), url(r'^(?P<pk>[0-9]+)/cancel/$', 'inventory_update_cancel'),
url(r'^(?P<pk>[0-9]+)/stdout/$', 'inventory_update_stdout'), url(r'^(?P<pk>[0-9]+)/stdout/$', 'inventory_update_stdout'),

View File

@@ -1,3 +1,4 @@
# Copyright (c) 2015 Ansible, Inc. # Copyright (c) 2015 Ansible, Inc.
# All Rights Reserved. # All Rights Reserved.
@@ -124,11 +125,13 @@ class ApiV1RootView(APIView):
data['organizations'] = reverse('api:organization_list') data['organizations'] = reverse('api:organization_list')
data['users'] = reverse('api:user_list') data['users'] = reverse('api:user_list')
data['projects'] = reverse('api:project_list') data['projects'] = reverse('api:project_list')
data['project_updates'] = reverse('api:project_update_list')
data['teams'] = reverse('api:team_list') data['teams'] = reverse('api:team_list')
data['credentials'] = reverse('api:credential_list') data['credentials'] = reverse('api:credential_list')
data['inventory'] = reverse('api:inventory_list') data['inventory'] = reverse('api:inventory_list')
data['inventory_scripts'] = reverse('api:inventory_script_list') data['inventory_scripts'] = reverse('api:inventory_script_list')
data['inventory_sources'] = reverse('api:inventory_source_list') data['inventory_sources'] = reverse('api:inventory_source_list')
data['inventory_updates'] = reverse('api:inventory_update_list')
data['groups'] = reverse('api:group_list') data['groups'] = reverse('api:group_list')
data['hosts'] = reverse('api:host_list') data['hosts'] = reverse('api:host_list')
data['job_templates'] = reverse('api:job_template_list') data['job_templates'] = reverse('api:job_template_list')
@@ -1107,6 +1110,11 @@ class ProjectUpdateView(RetrieveAPIView):
else: else:
return self.http_method_not_allowed(request, *args, **kwargs) return self.http_method_not_allowed(request, *args, **kwargs)
class ProjectUpdateList(ListAPIView):
model = ProjectUpdate
serializer_class = ProjectUpdateListSerializer
class ProjectUpdateDetail(RetrieveDestroyAPIView): class ProjectUpdateDetail(RetrieveDestroyAPIView):
model = ProjectUpdate model = ProjectUpdate
@@ -1158,6 +1166,7 @@ class UserList(ListCreateAPIView):
model = User model = User
serializer_class = UserSerializer serializer_class = UserSerializer
capabilities_prefetch = ['admin'] capabilities_prefetch = ['admin']
permission_classes = (UserPermission,)
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
ret = super(UserList, self).post( request, *args, **kwargs) ret = super(UserList, self).post( request, *args, **kwargs)
@@ -1323,7 +1332,7 @@ class UserDetail(RetrieveUpdateDestroyAPIView):
can_admin = request.user.can_access(User, 'admin', obj, request.data) can_admin = request.user.can_access(User, 'admin', obj, request.data)
su_only_edit_fields = ('is_superuser', 'is_system_auditor') su_only_edit_fields = ('is_superuser', 'is_system_auditor')
admin_only_edit_fields = ('last_name', 'first_name', 'username', 'is_active') admin_only_edit_fields = ('username', 'is_active')
fields_to_check = () fields_to_check = ()
if not request.user.is_superuser: if not request.user.is_superuser:
@@ -2172,6 +2181,11 @@ class InventorySourceUpdateView(RetrieveAPIView):
else: else:
return self.http_method_not_allowed(request, *args, **kwargs) return self.http_method_not_allowed(request, *args, **kwargs)
class InventoryUpdateList(ListAPIView):
model = InventoryUpdate
serializer_class = InventoryUpdateListSerializer
class InventoryUpdateDetail(RetrieveDestroyAPIView): class InventoryUpdateDetail(RetrieveDestroyAPIView):
model = InventoryUpdate model = InventoryUpdate
@@ -3010,7 +3024,7 @@ class JobJobTasksList(BaseJobEventsList):
# need stats on grandchildren, sorted by child. # need stats on grandchildren, sorted by child.
queryset = (JobEvent.objects.filter(parent__parent=parent_task, queryset = (JobEvent.objects.filter(parent__parent=parent_task,
parent__event__in=STARTING_EVENTS) parent__event__in=STARTING_EVENTS)
.values('parent__id', 'event', 'changed', 'failed') .values('parent__id', 'event', 'changed')
.annotate(num=Count('event')) .annotate(num=Count('event'))
.order_by('parent__id')) .order_by('parent__id'))
@@ -3071,13 +3085,10 @@ class JobJobTasksList(BaseJobEventsList):
# make appropriate changes to the task data. # make appropriate changes to the task data.
for child_data in data.get(task_start_event.id, []): for child_data in data.get(task_start_event.id, []):
if child_data['event'] == 'runner_on_failed': if child_data['event'] == 'runner_on_failed':
task_data['failed'] = True
task_data['host_count'] += child_data['num'] task_data['host_count'] += child_data['num']
task_data['reported_hosts'] += child_data['num'] task_data['reported_hosts'] += child_data['num']
if child_data['failed']: task_data['failed_count'] += child_data['num']
task_data['failed'] = True
task_data['failed_count'] += child_data['num']
else:
task_data['skipped_count'] += child_data['num']
elif child_data['event'] == 'runner_on_ok': elif child_data['event'] == 'runner_on_ok':
task_data['host_count'] += child_data['num'] task_data['host_count'] += child_data['num']
task_data['reported_hosts'] += child_data['num'] task_data['reported_hosts'] += child_data['num']

View File

@@ -54,10 +54,6 @@ class AutoOneToOneField(models.OneToOneField):
AutoSingleRelatedObjectDescriptor(related)) AutoSingleRelatedObjectDescriptor(related))
def resolve_role_field(obj, field): def resolve_role_field(obj, field):
ret = [] ret = []
@@ -71,8 +67,8 @@ def resolve_role_field(obj, field):
return [] return []
if len(field_components) == 1: if len(field_components) == 1:
Role_ = get_current_apps().get_model('main', 'Role') role_cls = str(get_current_apps().get_model('main', 'Role'))
if type(obj) is not Role_: if not str(type(obj)) == role_cls:
raise Exception(smart_text('{} refers to a {}, not a Role'.format(field, type(obj)))) raise Exception(smart_text('{} refers to a {}, not a Role'.format(field, type(obj))))
ret.append(obj.id) ret.append(obj.id)
else: else:

View File

@@ -22,6 +22,7 @@ import yaml
from django.conf import settings from django.conf import settings
from django.core.management.base import NoArgsCommand, CommandError from django.core.management.base import NoArgsCommand, CommandError
from django.db import connection, transaction from django.db import connection, transaction
from django.utils.encoding import smart_text
# AWX # AWX
from awx.main.models import * # noqa from awx.main.models import * # noqa
@@ -606,7 +607,7 @@ class Command(NoArgsCommand):
break break
instance_id = from_dict.get(key, default) instance_id = from_dict.get(key, default)
from_dict = instance_id from_dict = instance_id
return instance_id return smart_text(instance_id)
def _get_enabled(self, from_dict, default=None): def _get_enabled(self, from_dict, default=None):
''' '''

View File

@@ -25,5 +25,6 @@ class Migration(migrations.Migration):
name='use_role', name='use_role',
field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=[b'admin_role'], to='main.Role', null=b'True'), field=awx.main.fields.ImplicitRoleField(related_name='+', parent_role=[b'admin_role'], to='main.Role', null=b'True'),
), ),
migrations.RunPython(rbac.infer_credential_org_from_team),
migrations.RunPython(rbac.rebuild_role_hierarchy), migrations.RunPython(rbac.rebuild_role_hierarchy),
] ]

View File

@@ -2,7 +2,9 @@ import logging
from time import time from time import time
from django.utils.encoding import smart_text from django.utils.encoding import smart_text
from django.db import transaction
from django.db.models import Q from django.db.models import Q
from django.db.utils import IntegrityError
from collections import defaultdict from collections import defaultdict
from awx.main.utils import getattrd from awx.main.utils import getattrd
@@ -490,3 +492,11 @@ def rebuild_role_hierarchy(apps, schema_editor):
logger.info('Done.') logger.info('Done.')
def infer_credential_org_from_team(apps, schema_editor):
Credential = apps.get_model('main', "Credential")
for cred in Credential.objects.exclude(deprecated_team__isnull=True):
try:
with transaction.atomic():
_update_credential_parents(cred.deprecated_team.organization, cred)
except IntegrityError:
logger.info("Organization<{}> credential for old Team<{}> credential already created".format(cred.deprecated_team.organization.pk, cred.pk))

View File

@@ -23,13 +23,14 @@ from awx.main.models.base import * # noqa
from awx.main.models.unified_jobs import * # noqa from awx.main.models.unified_jobs import * # noqa
from awx.main.utils import decrypt_field from awx.main.utils import decrypt_field
from awx.main.conf import tower_settings from awx.main.conf import tower_settings
from awx.main.models.notifications import JobNotificationMixin
logger = logging.getLogger('awx.main.models.ad_hoc_commands') logger = logging.getLogger('awx.main.models.ad_hoc_commands')
__all__ = ['AdHocCommand', 'AdHocCommandEvent'] __all__ = ['AdHocCommand', 'AdHocCommandEvent']
class AdHocCommand(UnifiedJob): class AdHocCommand(UnifiedJob, JobNotificationMixin):
class Meta(object): class Meta(object):
app_label = 'main' app_label = 'main'
@@ -237,6 +238,14 @@ class AdHocCommand(UnifiedJob):
update_fields.append('name') update_fields.append('name')
super(AdHocCommand, self).save(*args, **kwargs) super(AdHocCommand, self).save(*args, **kwargs)
'''
JobNotificationMixin
'''
def get_notification_templates(self):
return self.notification_templates
def get_notification_friendly_name(self):
return "AdHoc Command"
class AdHocCommandEvent(CreatedModifiedModel): class AdHocCommandEvent(CreatedModifiedModel):
''' '''

View File

@@ -25,7 +25,10 @@ from awx.main.models.base import * # noqa
from awx.main.models.jobs import Job from awx.main.models.jobs import Job
from awx.main.models.unified_jobs import * # noqa from awx.main.models.unified_jobs import * # noqa
from awx.main.models.mixins import ResourceMixin from awx.main.models.mixins import ResourceMixin
from awx.main.models.notifications import NotificationTemplate from awx.main.models.notifications import (
NotificationTemplate,
JobNotificationMixin,
)
from awx.main.utils import _inventory_updates from awx.main.utils import _inventory_updates
from awx.main.conf import tower_settings from awx.main.conf import tower_settings
@@ -1192,7 +1195,7 @@ class InventorySource(UnifiedJobTemplate, InventorySourceOptions):
return source return source
class InventoryUpdate(UnifiedJob, InventorySourceOptions): class InventoryUpdate(UnifiedJob, InventorySourceOptions, JobNotificationMixin):
''' '''
Internal job for tracking inventory updates from external sources. Internal job for tracking inventory updates from external sources.
''' '''
@@ -1268,6 +1271,15 @@ class InventoryUpdate(UnifiedJob, InventorySourceOptions):
return False return False
return True return True
'''
JobNotificationMixin
'''
def get_notification_templates(self):
return self.inventory_source.notification_templates
def get_notification_friendly_name(self):
return "Inventory Update"
class CustomInventoryScript(CommonModelNameNotUnique, ResourceMixin): class CustomInventoryScript(CommonModelNameNotUnique, ResourceMixin):

View File

@@ -24,7 +24,10 @@ from jsonfield import JSONField
from awx.main.constants import CLOUD_PROVIDERS from awx.main.constants import CLOUD_PROVIDERS
from awx.main.models.base import * # noqa from awx.main.models.base import * # noqa
from awx.main.models.unified_jobs import * # noqa from awx.main.models.unified_jobs import * # noqa
from awx.main.models.notifications import NotificationTemplate from awx.main.models.notifications import (
NotificationTemplate,
JobNotificationMixin,
)
from awx.main.utils import decrypt_field, ignore_inventory_computed_fields from awx.main.utils import decrypt_field, ignore_inventory_computed_fields
from awx.main.utils import emit_websocket_notification from awx.main.utils import emit_websocket_notification
from awx.main.redact import PlainTextCleaner from awx.main.redact import PlainTextCleaner
@@ -499,7 +502,7 @@ class JobTemplate(UnifiedJobTemplate, JobOptions, ResourceMixin):
any_notification_templates = set(any_notification_templates + list(base_notification_templates.filter(organization_notification_templates_for_any=self.project.organization))) any_notification_templates = set(any_notification_templates + list(base_notification_templates.filter(organization_notification_templates_for_any=self.project.organization)))
return dict(error=list(error_notification_templates), success=list(success_notification_templates), any=list(any_notification_templates)) return dict(error=list(error_notification_templates), success=list(success_notification_templates), any=list(any_notification_templates))
class Job(UnifiedJob, JobOptions): class Job(UnifiedJob, JobOptions, JobNotificationMixin):
''' '''
A job applies a project (with playbook) to an inventory source with a given A job applies a project (with playbook) to an inventory source with a given
credential. It represents a single invocation of ansible-playbook with the credential. It represents a single invocation of ansible-playbook with the
@@ -792,6 +795,15 @@ class Job(UnifiedJob, JobOptions):
return True return True
'''
JobNotificationMixin
'''
def get_notification_templates(self):
return self.job_template.notification_templates
def get_notification_friendly_name(self):
return "Job"
class JobHostSummary(CreatedModifiedModel): class JobHostSummary(CreatedModifiedModel):
''' '''
Per-host statistics for each job. Per-host statistics for each job.
@@ -1315,7 +1327,7 @@ class SystemJobTemplate(UnifiedJobTemplate, SystemJobOptions):
any=list(any_notification_templates)) any=list(any_notification_templates))
class SystemJob(UnifiedJob, SystemJobOptions): class SystemJob(UnifiedJob, SystemJobOptions, JobNotificationMixin):
class Meta: class Meta:
app_label = 'main' app_label = 'main'
@@ -1378,3 +1390,13 @@ class SystemJob(UnifiedJob, SystemJobOptions):
@property @property
def task_impact(self): def task_impact(self):
return 150 return 150
'''
JobNotificationMixin
'''
def get_notification_templates(self):
return self.system_job_template.notification_templates
def get_notification_friendly_name(self):
return "System Job"

View File

@@ -171,3 +171,27 @@ class Notification(CreatedModifiedModel):
def get_absolute_url(self): def get_absolute_url(self):
return reverse('api:notification_detail', args=(self.pk,)) return reverse('api:notification_detail', args=(self.pk,))
class JobNotificationMixin(object):
def get_notification_templates(self):
raise RuntimeError("Define me")
def get_notification_friendly_name(self):
raise RuntimeError("Define me")
def _build_notification_message(self, status_str):
notification_body = self.notification_data()
notification_subject = "{} #{} '{}' {} on Ansible Tower: {}".format(self.get_notification_friendly_name(),
self.id,
self.name,
status_str,
notification_body['url'])
notification_body['friendly_name'] = self.get_notification_friendly_name()
return (notification_subject, notification_body)
def build_notification_succeeded_message(self):
return self._build_notification_message('succeeded')
def build_notification_failed_message(self):
return self._build_notification_message('failed')

View File

@@ -20,7 +20,10 @@ from django.utils.timezone import now, make_aware, get_default_timezone
from awx.lib.compat import slugify from awx.lib.compat import slugify
from awx.main.models.base import * # noqa from awx.main.models.base import * # noqa
from awx.main.models.jobs import Job from awx.main.models.jobs import Job
from awx.main.models.notifications import NotificationTemplate from awx.main.models.notifications import (
NotificationTemplate,
JobNotificationMixin,
)
from awx.main.models.unified_jobs import * # noqa from awx.main.models.unified_jobs import * # noqa
from awx.main.models.mixins import ResourceMixin from awx.main.models.mixins import ResourceMixin
from awx.main.utils import update_scm_url from awx.main.utils import update_scm_url
@@ -372,8 +375,7 @@ class Project(UnifiedJobTemplate, ProjectOptions, ResourceMixin):
def get_absolute_url(self): def get_absolute_url(self):
return reverse('api:project_detail', args=(self.pk,)) return reverse('api:project_detail', args=(self.pk,))
class ProjectUpdate(UnifiedJob, ProjectOptions, JobNotificationMixin):
class ProjectUpdate(UnifiedJob, ProjectOptions):
''' '''
Internal job for tracking project updates from SCM. Internal job for tracking project updates from SCM.
''' '''
@@ -443,3 +445,12 @@ class ProjectUpdate(UnifiedJob, ProjectOptions):
if 'scm_delete_on_next_update' not in update_fields: if 'scm_delete_on_next_update' not in update_fields:
update_fields.append('scm_delete_on_next_update') update_fields.append('scm_delete_on_next_update')
parent_instance.save(update_fields=update_fields) parent_instance.save(update_fields=update_fields)
'''
JobNotificationMixin
'''
def get_notification_templates(self):
return self.project.notification_templates
def get_notification_friendly_name(self):
return "Project Update"

View File

@@ -18,6 +18,7 @@ from django.core.exceptions import NON_FIELD_ERRORS
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.utils.timezone import now from django.utils.timezone import now
from django.utils.encoding import smart_text from django.utils.encoding import smart_text
from django.apps import apps
# Django-JSONField # Django-JSONField
from jsonfield import JSONField from jsonfield import JSONField
@@ -360,8 +361,30 @@ class UnifiedJobTemplate(PolymorphicModel, CommonModelNameNotUnique, Notificatio
dest_field.add(*list(src_field_value.all().values_list('id', flat=True))) dest_field.add(*list(src_field_value.all().values_list('id', flat=True)))
return unified_job return unified_job
class UnifiedJobTypeStringMixin(object):
@classmethod
def _underscore_to_camel(cls, word):
return ''.join(x.capitalize() or '_' for x in word.split('_'))
class UnifiedJob(PolymorphicModel, PasswordFieldsModel, CommonModelNameNotUnique): @classmethod
def _model_type(cls, job_type):
# Django >= 1.9
#app = apps.get_app_config('main')
model_str = cls._underscore_to_camel(job_type)
try:
return apps.get_model('main', model_str)
except LookupError:
print("Lookup model error")
return None
@classmethod
def get_instance_by_type(cls, job_type, job_id):
model = cls._model_type(job_type)
if not model:
return None
return model.objects.get(id=job_id)
class UnifiedJob(PolymorphicModel, PasswordFieldsModel, CommonModelNameNotUnique, UnifiedJobTypeStringMixin):
''' '''
Concrete base class for unified job run by the task engine. Concrete base class for unified job run by the task engine.
''' '''

View File

@@ -185,114 +185,61 @@ def notify_task_runner(metadata_dict):
queue = FifoQueue('tower_task_manager') queue = FifoQueue('tower_task_manager')
queue.push(metadata_dict) queue.push(metadata_dict)
def _send_notification_templates(instance, status_str):
if status_str not in ['succeeded', 'failed']:
raise ValueError("status_str must be either succeeded or failed")
print("Instance has some shit in it %s" % instance)
notification_templates = instance.get_notification_templates()
if notification_templates:
all_notification_templates = set(notification_templates.get('success', []) + notification_templates.get('any', []))
if len(all_notification_templates):
try:
(notification_subject, notification_body) = getattr(instance, 'build_notification_%s_message' % status_str)()
except AttributeError:
raise NotImplementedError("build_notification_%s_message() does not exist" % status_str)
send_notifications.delay([n.generate_notification(notification_subject, notification_body).id
for n in all_notification_templates],
job_id=instance.id)
@task(bind=True) @task(bind=True)
def handle_work_success(self, result, task_actual): def handle_work_success(self, result, task_actual):
if task_actual['type'] == 'project_update': instance = UnifiedJob.get_instance_by_type(task_actual['type'], task_actual['id'])
instance = ProjectUpdate.objects.get(id=task_actual['id']) if not instance:
instance_name = instance.name
notification_templates = instance.project.notification_templates
friendly_name = "Project Update"
elif task_actual['type'] == 'inventory_update':
instance = InventoryUpdate.objects.get(id=task_actual['id'])
instance_name = instance.name
notification_templates = instance.inventory_source.notification_templates
friendly_name = "Inventory Update"
elif task_actual['type'] == 'job':
instance = Job.objects.get(id=task_actual['id'])
instance_name = instance.job_template.name
notification_templates = instance.job_template.notification_templates
friendly_name = "Job"
elif task_actual['type'] == 'ad_hoc_command':
instance = AdHocCommand.objects.get(id=task_actual['id'])
instance_name = instance.module_name
notification_templates = instance.notification_templates
friendly_name = "AdHoc Command"
elif task_actual['type'] == 'system_job':
instance = SystemJob.objects.get(id=task_actual['id'])
instance_name = instance.system_job_template.name
notification_templates = instance.system_job_template.notification_templates
friendly_name = "System Job"
else:
return return
all_notification_templates = set(notification_templates.get('success', []) + notification_templates.get('any', [])) _send_notification_templates(instance, 'succeeded')
if len(all_notification_templates):
notification_body = instance.notification_data()
notification_subject = "{} #{} '{}' succeeded on Ansible Tower: {}".format(friendly_name,
task_actual['id'],
smart_str(instance_name),
notification_body['url'])
notification_body['friendly_name'] = friendly_name
send_notifications.delay([n.generate_notification(notification_subject, notification_body).id
for n in all_notification_templates],
job_id=task_actual['id'])
@task(bind=True) @task(bind=True)
def handle_work_error(self, task_id, subtasks=None): def handle_work_error(self, task_id, subtasks=None):
print('Executing error task id %s, subtasks: %s' % print('Executing error task id %s, subtasks: %s' %
(str(self.request.id), str(subtasks))) (str(self.request.id), str(subtasks)))
first_task = None first_instance = None
first_task_id = None first_instance_type = ''
first_task_type = ''
first_task_name = ''
if subtasks is not None: if subtasks is not None:
for each_task in subtasks: for each_task in subtasks:
instance_name = '' instance = UnifiedJob.get_instance_by_type(each_task['type'], each_task['id'])
if each_task['type'] == 'project_update': if not instance:
instance = ProjectUpdate.objects.get(id=each_task['id'])
instance_name = instance.name
notification_templates = instance.project.notification_templates
friendly_name = "Project Update"
elif each_task['type'] == 'inventory_update':
instance = InventoryUpdate.objects.get(id=each_task['id'])
instance_name = instance.name
notification_templates = instance.inventory_source.notification_templates
friendly_name = "Inventory Update"
elif each_task['type'] == 'job':
instance = Job.objects.get(id=each_task['id'])
instance_name = instance.job_template.name
notification_templates = instance.job_template.notification_templates
friendly_name = "Job"
elif each_task['type'] == 'ad_hoc_command':
instance = AdHocCommand.objects.get(id=each_task['id'])
instance_name = instance.module_name
notification_templates = instance.notification_templates
friendly_name = "AdHoc Command"
elif each_task['type'] == 'system_job':
instance = SystemJob.objects.get(id=each_task['id'])
instance_name = instance.system_job_template.name
notification_templates = instance.system_job_template.notification_templates
friendly_name = "System Job"
else:
# Unknown task type # Unknown task type
logger.warn("Unknown task type: {}".format(each_task['type'])) logger.warn("Unknown task type: {}".format(each_task['type']))
continue continue
if first_task is None:
first_task = instance if first_instance is None:
first_task_id = instance.id first_instance = instance
first_task_type = each_task['type'] first_instance_type = each_task['type']
first_task_name = instance_name
first_task_friendly_name = friendly_name
if instance.celery_task_id != task_id: if instance.celery_task_id != task_id:
instance.status = 'failed' instance.status = 'failed'
instance.failed = True instance.failed = True
instance.job_explanation = 'Previous Task Failed: {"job_type": "%s", "job_name": "%s", "job_id": "%s"}' % \ instance.job_explanation = 'Previous Task Failed: {"job_type": "%s", "job_name": "%s", "job_id": "%s"}' % \
(first_task_type, first_task_name, first_task_id) (first_instance_type, first_instance.name, first_instance.id)
instance.save() instance.save()
instance.socketio_emit_status("failed") instance.socketio_emit_status("failed")
all_notification_templates = set(notification_templates.get('error', []) + notification_templates.get('any', [])) if first_instance:
if len(all_notification_templates): print("Instance type is %s" % first_instance_type)
notification_body = first_task.notification_data() print("Instance passing along %s" % first_instance.name)
notification_subject = "{} #{} '{}' failed on Ansible Tower: {}".format(first_task_friendly_name, _send_notification_templates(first_instance, 'failed')
first_task_id,
smart_str(first_task_name),
notification_body['url'])
notification_body['friendly_name'] = first_task_friendly_name
send_notifications.delay([n.generate_notification(notification_subject, notification_body).id
for n in all_notification_templates],
job_id=first_task_id)
@task() @task()
def update_inventory_computed_fields(inventory_id, should_update_hosts=True): def update_inventory_computed_fields(inventory_id, should_update_hosts=True):
@@ -1306,9 +1253,14 @@ class RunInventoryUpdate(BaseTask):
credential = inventory_update.credential credential = inventory_update.credential
if credential: if credential:
cp.set(section, 'hostname', credential.host) cp.set(section, 'url', credential.host)
cp.set(section, 'username', credential.username) cp.set(section, 'username', credential.username)
cp.set(section, 'password', decrypt_field(credential, 'password')) cp.set(section, 'password', decrypt_field(credential, 'password'))
cp.set(section, 'ssl_verify', "false")
section = 'cache'
cp.add_section(section)
cp.set(section, 'max_age', "0")
elif inventory_update.source == 'azure_rm': elif inventory_update.source == 'azure_rm':
section = 'azure' section = 'azure'
@@ -1710,3 +1662,4 @@ class RunSystemJob(BaseTask):
def build_cwd(self, instance, **kwargs): def build_cwd(self, instance, **kwargs):
return settings.BASE_DIR return settings.BASE_DIR

View File

@@ -71,7 +71,6 @@ def test_create_user_credential_via_user_credentials_list_xfail(post, alice, bob
def test_create_team_credential(post, get, team, organization, org_admin, team_member): def test_create_team_credential(post, get, team, organization, org_admin, team_member):
response = post(reverse('api:credential_list'), { response = post(reverse('api:credential_list'), {
'team': team.id, 'team': team.id,
'organization': organization.id,
'name': 'Some name', 'name': 'Some name',
'username': 'someusername' 'username': 'someusername'
}, org_admin) }, org_admin)
@@ -81,6 +80,9 @@ def test_create_team_credential(post, get, team, organization, org_admin, team_m
assert response.status_code == 200 assert response.status_code == 200
assert response.data['count'] == 1 assert response.data['count'] == 1
# Assure that credential's organization is implictly set to team's org
assert response.data['results'][0]['summary_fields']['organization']['id'] == team.organization.id
@pytest.mark.django_db @pytest.mark.django_db
def test_create_team_credential_via_team_credentials_list(post, get, team, org_admin, team_member): def test_create_team_credential_via_team_credentials_list(post, get, team, org_admin, team_member):
response = post(reverse('api:team_credentials_list', args=(team.pk,)), { response = post(reverse('api:team_credentials_list', args=(team.pk,)), {

View File

@@ -54,21 +54,40 @@ def test_credential_migration_team_member(credential, team, user, permissions):
rbac.migrate_credential(apps, None) rbac.migrate_credential(apps, None)
# Admin permissions post migration # User permissions post migration
assert u in credential.use_role assert u in credential.use_role
assert u not in credential.admin_role
@pytest.mark.django_db @pytest.mark.django_db
def test_credential_migration_team_admin(credential, team, user, permissions): def test_credential_migration_team_admin(credential, team, user, permissions):
u = user('user', False) u = user('user', False)
team.member_role.members.add(u) team.admin_role.members.add(u)
credential.deprecated_team = team credential.deprecated_team = team
credential.save() credential.save()
assert u not in credential.use_role assert u not in credential.use_role
# Usage permissions post migration # Admin permissions post migration
rbac.migrate_credential(apps, None) rbac.migrate_credential(apps, None)
assert u in credential.use_role assert u in credential.admin_role
@pytest.mark.django_db
def test_credential_migration_org_auditor(credential, team, org_auditor):
# Team's organization is the org_auditor's org
credential.deprecated_team = team
credential.save()
# No permissions pre-migration (this happens automatically so we patch this)
team.admin_role.children.remove(credential.admin_role)
team.member_role.children.remove(credential.use_role)
assert org_auditor not in credential.read_role
rbac.migrate_credential(apps, None)
rbac.infer_credential_org_from_team(apps, None)
# Read permissions post migration
assert org_auditor not in credential.use_role
assert org_auditor in credential.read_role
def test_credential_access_superuser(): def test_credential_access_superuser():
u = User(username='admin', is_superuser=True) u = User(username='admin', is_superuser=True)

View File

@@ -192,8 +192,12 @@ class UsersTest(BaseTest):
self.post(url, expect=403, data=new_user, auth=self.get_other_credentials()) self.post(url, expect=403, data=new_user, auth=self.get_other_credentials())
self.post(url, expect=201, data=new_user, auth=self.get_super_credentials()) self.post(url, expect=201, data=new_user, auth=self.get_super_credentials())
self.post(url, expect=400, data=new_user, auth=self.get_super_credentials()) self.post(url, expect=400, data=new_user, auth=self.get_super_credentials())
self.post(url, expect=201, data=new_user2, auth=self.get_normal_credentials()) # org admin cannot create orphaned users
self.post(url, expect=400, data=new_user2, auth=self.get_normal_credentials()) self.post(url, expect=403, data=new_user2, auth=self.get_normal_credentials())
# org admin can create org users
org_url = reverse('api:organization_users_list', args=(self.organizations[0].pk,))
self.post(org_url, expect=201, data=new_user2, auth=self.get_normal_credentials())
self.post(org_url, expect=400, data=new_user2, auth=self.get_normal_credentials())
# Normal user cannot add users after his org is marked inactive. # Normal user cannot add users after his org is marked inactive.
self.organizations[0].delete() self.organizations[0].delete()
new_user3 = dict(username='blippy3') new_user3 = dict(username='blippy3')
@@ -325,9 +329,9 @@ class UsersTest(BaseTest):
detail_url = reverse('api:user_detail', args=(self.other_django_user.pk,)) detail_url = reverse('api:user_detail', args=(self.other_django_user.pk,))
data = self.get(detail_url, expect=200, auth=self.get_other_credentials()) data = self.get(detail_url, expect=200, auth=self.get_other_credentials())
# can't change first_name, last_name, etc # can change first_name, last_name, etc
data['last_name'] = "NewLastName" data['last_name'] = "NewLastName"
self.put(detail_url, data, expect=403, auth=self.get_other_credentials()) self.put(detail_url, data, expect=200, auth=self.get_other_credentials())
# can't change username # can't change username
data['username'] = 'newUsername' data['username'] = 'newUsername'
@@ -367,23 +371,20 @@ class UsersTest(BaseTest):
url = reverse('api:user_list') url = reverse('api:user_list')
data = dict(username='username', password='password') data = dict(username='username', password='password')
data2 = dict(username='username2', password='password2') data2 = dict(username='username2', password='password2')
data = self.post(url, expect=201, data=data, auth=self.get_normal_credentials())
# but a regular user cannot create users
self.post(url, expect=403, data=data2, auth=self.get_other_credentials())
# org admins cannot create orphaned users
self.post(url, expect=403, data=data2, auth=self.get_normal_credentials())
# a super user can create new users
self.post(url, expect=201, data=data, auth=self.get_super_credentials())
# verify that the login works... # verify that the login works...
self.get(url, expect=200, auth=('username', 'password')) self.get(url, expect=200, auth=('username', 'password'))
# but a regular user cannot
data = self.post(url, expect=403, data=data2, auth=self.get_other_credentials())
# a super user can also create new users
data = self.post(url, expect=201, data=data2, auth=self.get_super_credentials())
# verify that the login works
self.get(url, expect=200, auth=('username2', 'password2'))
# verify that if you post a user with a pk, you do not alter that user's password info # verify that if you post a user with a pk, you do not alter that user's password info
mod = dict(id=self.super_django_user.pk, username='change', password='change') mod = dict(id=self.super_django_user.pk, username='change', password='change')
data = self.post(url, expect=201, data=mod, auth=self.get_super_credentials()) self.post(url, expect=201, data=mod, auth=self.get_super_credentials())
orig = User.objects.get(pk=self.super_django_user.pk) orig = User.objects.get(pk=self.super_django_user.pk)
self.assertTrue(orig.username != 'change') self.assertTrue(orig.username != 'change')

View File

@@ -19,7 +19,6 @@ from awx.main.models import (
Role, Role,
) )
@pytest.mark.skip(reason="Seeing pk error, suspect weirdness in mocking requests")
@pytest.mark.parametrize("pk, err", [ @pytest.mark.parametrize("pk, err", [
(111, "not change the membership"), (111, "not change the membership"),
(1, "may not perform"), (1, "may not perform"),
@@ -38,18 +37,17 @@ def test_user_roles_list_user_admin_role(pk, err):
factory = APIRequestFactory() factory = APIRequestFactory()
view = UserRolesList.as_view() view = UserRolesList.as_view()
user = User(username="root", is_superuser=True) user = User(username="root", is_superuser=True, pk=1, id=1)
request = factory.post("/user/1/roles", {'id':pk}, format="json") request = factory.post("/user/1/roles", {'id':pk}, format="json")
force_authenticate(request, user) force_authenticate(request, user)
response = view(request) response = view(request, pk=user.pk)
response.render() response.render()
assert response.status_code == 403 assert response.status_code == 403
assert err in response.content assert err in response.content
@pytest.mark.skip(reason="db access or mocking needed for new tests in role assignment code")
@pytest.mark.parametrize("admin_role, err", [ @pytest.mark.parametrize("admin_role, err", [
(True, "may not perform"), (True, "may not perform"),
(False, "not change the membership"), (False, "not change the membership"),
@@ -70,10 +68,13 @@ def test_role_users_list_other_user_admin_role(admin_role, err):
view = RoleUsersList.as_view() view = RoleUsersList.as_view()
user = User(username="root", is_superuser=True, pk=1, id=1) user = User(username="root", is_superuser=True, pk=1, id=1)
queried_user = User(username="maynard")
request = factory.post("/role/1/users", {'id':1}, format="json") request = factory.post("/role/1/users", {'id':1}, format="json")
force_authenticate(request, user) force_authenticate(request, user)
response = view(request) with mock.patch('awx.api.views.get_object_or_400', return_value=queried_user):
response = view(request)
response.render() response.render()
assert response.status_code == 403 assert response.status_code == 403

View File

@@ -1,144 +1,462 @@
#!/usr/bin/python #!/usr/bin/python
# vim: set fileencoding=utf-8 :
#
# Copyright (C) 2016 Guido Günther <agx@sigxcpu.org>
#
# This script is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with it. If not, see <http://www.gnu.org/licenses/>.
#
# This is loosely based on the foreman inventory script
# -- Josh Preston <jpreston@redhat.com>
#
''' from __future__ import print_function
CloudForms external inventory script
==================================================
Generates inventory that Ansible can understand by making API request to CloudForms.
Modeled after https://raw.githubusercontent.com/ansible/ansible/stable-1.9/plugins/inventory/ec2.py
jlabocki <at> redhat.com or @jameslabocki on twitter
'''
import os
import argparse import argparse
import ConfigParser import ConfigParser
import os
import re
from time import time
import requests import requests
import json from requests.auth import HTTPBasicAuth
import warnings
# This disables warnings and is not a good idea, but hey, this is a demo try:
# http://urllib3.readthedocs.org/en/latest/security.html#disabling-warnings import json
requests.packages.urllib3.disable_warnings() except ImportError:
import simplejson as json
class CloudFormsInventory(object): class CloudFormsInventory(object):
def _empty_inventory(self):
return {"_meta": {"hostvars": {}}}
def __init__(self): def __init__(self):
''' Main execution path ''' """
Main execution path
"""
self.inventory = dict() # A list of groups and the hosts in that group
self.hosts = dict() # Details about hosts in the inventory
# Inventory grouped by instance IDs, tags, security groups, regions, # Parse CLI arguments
# and availability zones
self.inventory = self._empty_inventory()
# Index of hostname (address) to instance ID
self.index = {}
# Read CLI arguments
self.read_settings()
self.parse_cli_args() self.parse_cli_args()
# Get Hosts # Read settings
if self.args.list: self.read_settings()
self.get_hosts()
# This doesn't exist yet and needs to be added # Cache
if self.args.refresh_cache or not self.is_cache_valid():
self.update_cache()
else:
self.load_inventory_from_cache()
self.load_hosts_from_cache()
data_to_print = ""
# Data to print
if self.args.host: if self.args.host:
data2 = {} if self.args.debug:
print json.dumps(data2, indent=2) print("Fetching host [%s]" % self.args.host)
data_to_print += self.get_host_info(self.args.host)
else:
self.inventory['_meta'] = {'hostvars': {}}
for hostname in self.hosts:
self.inventory['_meta']['hostvars'][hostname] = {
'cloudforms': self.hosts[hostname],
}
# include the ansible_ssh_host in the top level
if 'ansible_ssh_host' in self.hosts[hostname]:
self.inventory['_meta']['hostvars'][hostname]['ansible_ssh_host'] = self.hosts[hostname]['ansible_ssh_host']
def parse_cli_args(self): data_to_print += self.json_format_dict(self.inventory, self.args.pretty)
''' Command line argument processing '''
parser = argparse.ArgumentParser(description='Produce an Ansible Inventory file based on CloudForms') print(data_to_print)
parser.add_argument('--list', action='store_true', default=False,
help='List instances (default: False)') def is_cache_valid(self):
parser.add_argument('--host', action='store', """
help='Get all the variables about a specific instance') Determines if the cache files have expired, or if it is still valid
self.args = parser.parse_args() """
if self.args.debug:
print("Determining if cache [%s] is still valid (< %s seconds old)" % (self.cache_path_hosts, self.cache_max_age))
if os.path.isfile(self.cache_path_hosts):
mod_time = os.path.getmtime(self.cache_path_hosts)
current_time = time()
if (mod_time + self.cache_max_age) > current_time:
if os.path.isfile(self.cache_path_inventory):
if self.args.debug:
print("Cache is still valid!")
return True
if self.args.debug:
print("Cache is stale or does not exist.")
return False
def read_settings(self): def read_settings(self):
''' Reads the settings from the cloudforms.ini file ''' """
Reads the settings from the cloudforms.ini file
"""
config = ConfigParser.SafeConfigParser() config = ConfigParser.SafeConfigParser()
config_paths = [ config_paths = [
os.path.join(os.path.dirname(os.path.realpath(__file__)), 'cloudforms.ini'), os.path.dirname(os.path.realpath(__file__)) + '/cloudforms.ini',
"/opt/rh/cloudforms.ini", "/etc/ansible/cloudforms.ini",
] ]
env_value = os.environ.get('CLOUDFORMS_INI_PATH') env_value = os.environ.get('CLOUDFORMS_INI_PATH')
if env_value is not None: if env_value is not None:
config_paths.append(os.path.expanduser(os.path.expandvars(env_value))) config_paths.append(os.path.expanduser(os.path.expandvars(env_value)))
if self.args.debug:
for config_path in config_paths:
print("Reading from configuration file [%s]" % config_path)
config.read(config_paths) config.read(config_paths)
# Version # CloudForms API related
if config.has_option('cloudforms', 'version'): if config.has_option('cloudforms', 'url'):
self.cloudforms_version = config.get('cloudforms', 'version') self.cloudforms_url = config.get('cloudforms', 'url')
else: else:
self.cloudforms_version = "none" self.cloudforms_url = None
# CloudForms Endpoint if not self.cloudforms_url:
if config.has_option('cloudforms', 'hostname'): warnings.warn("No url specified, expected something like 'https://cfme.example.com'")
self.cloudforms_hostname = config.get('cloudforms', 'hostname')
else:
self.cloudforms_hostname = None
# CloudForms Username
if config.has_option('cloudforms', 'username'): if config.has_option('cloudforms', 'username'):
self.cloudforms_username = config.get('cloudforms', 'username') self.cloudforms_username = config.get('cloudforms', 'username')
else: else:
self.cloudforms_username = "none" self.cloudforms_username = None
if not self.cloudforms_username:
warnings.warn("No username specified, you need to specify a CloudForms username.")
# CloudForms Password
if config.has_option('cloudforms', 'password'): if config.has_option('cloudforms', 'password'):
self.cloudforms_password = config.get('cloudforms', 'password') self.cloudforms_pw = config.get('cloudforms', 'password')
else: else:
self.cloudforms_password = "none" self.cloudforms_pw = None
def get_hosts(self): if not self.cloudforms_pw:
''' Gets host from CloudForms ''' warnings.warn("No password specified, you need to specify a password for the CloudForms user.")
r = requests.get("https://{0}/api/vms?expand=resources&attributes=all".format(self.cloudforms_hostname),
auth=(self.cloudforms_username, self.cloudforms_password), verify=False)
obj = r.json()
# Create groups+hosts based on host data if config.has_option('cloudforms', 'ssl_verify'):
for resource in obj.get('resources', []): self.cloudforms_ssl_verify = config.getboolean('cloudforms', 'ssl_verify')
else:
self.cloudforms_ssl_verify = True
# Maintain backwards compat by creating `Dynamic_CloudForms` group if config.has_option('cloudforms', 'version'):
if 'Dynamic_CloudForms' not in self.inventory: self.cloudforms_version = config.get('cloudforms', 'version')
self.inventory['Dynamic_CloudForms'] = [] else:
self.inventory['Dynamic_CloudForms'].append(resource['name']) self.cloudforms_version = None
# Add host to desired groups if config.has_option('cloudforms', 'limit'):
for key in ('vendor', 'type', 'location'): self.cloudforms_limit = config.getint('cloudforms', 'limit')
if key in resource: else:
# Create top-level group self.cloudforms_limit = 100
if key not in self.inventory:
self.inventory[key] = dict(children=[], vars={}, hosts=[])
# if resource['name'] not in self.inventory[key]['hosts']:
# self.inventory[key]['hosts'].append(resource['name'])
# Create sub-group if config.has_option('cloudforms', 'purge_actions'):
if resource[key] not in self.inventory: self.cloudforms_purge_actions = config.getboolean('cloudforms', 'purge_actions')
self.inventory[resource[key]] = dict(children=[], vars={}, hosts=[]) else:
# self.inventory[resource[key]]['hosts'].append(resource['name']) self.cloudforms_purge_actions = True
# Add sub-group, as a child of top-level if config.has_option('cloudforms', 'clean_group_keys'):
if resource[key] not in self.inventory[key]['children']: self.cloudforms_clean_group_keys = config.getboolean('cloudforms', 'clean_group_keys')
self.inventory[key]['children'].append(resource[key]) else:
self.cloudforms_clean_group_keys = True
if config.has_option('cloudforms', 'nest_tags'):
self.cloudforms_nest_tags = config.getboolean('cloudforms', 'nest_tags')
else:
self.cloudforms_nest_tags = False
# Ansible related
try:
group_patterns = config.get('ansible', 'group_patterns')
except (ConfigParser.NoOptionError, ConfigParser.NoSectionError):
group_patterns = "[]"
self.group_patterns = eval(group_patterns)
# Cache related
try:
cache_path = os.path.expanduser(config.get('cache', 'path'))
except (ConfigParser.NoOptionError, ConfigParser.NoSectionError):
cache_path = '.'
(script, ext) = os.path.splitext(os.path.basename(__file__))
self.cache_path_hosts = cache_path + "/%s.hosts" % script
self.cache_path_inventory = cache_path + "/%s.inventory" % script
self.cache_max_age = config.getint('cache', 'max_age')
if self.args.debug:
print("CloudForms settings:")
print("cloudforms_url = %s" % self.cloudforms_url)
print("cloudforms_username = %s" % self.cloudforms_username)
print("cloudforms_pw = %s" % self.cloudforms_pw)
print("cloudforms_ssl_verify = %s" % self.cloudforms_ssl_verify)
print("cloudforms_version = %s" % self.cloudforms_version)
print("cloudforms_limit = %s" % self.cloudforms_limit)
print("cloudforms_purge_actions = %s" % self.cloudforms_purge_actions)
print("Cache settings:")
print("cache_max_age = %s" % self.cache_max_age)
print("cache_path_hosts = %s" % self.cache_path_hosts)
print("cache_path_inventory = %s" % self.cache_path_inventory)
def parse_cli_args(self):
"""
Command line argument processing
"""
parser = argparse.ArgumentParser(description='Produce an Ansible Inventory file based on CloudForms managed VMs')
parser.add_argument('--list', action='store_true', default=True, help='List instances (default: True)')
parser.add_argument('--host', action='store', help='Get all the variables about a specific instance')
parser.add_argument('--pretty', action='store_true', default=False, help='Pretty print JSON output (default: False)')
parser.add_argument('--refresh-cache', action='store_true', default=False,
help='Force refresh of cache by making API requests to CloudForms (default: False - use cache files)')
parser.add_argument('--debug', action='store_true', default=False, help='Show debug output while running (default: False)')
self.args = parser.parse_args()
def _get_json(self, url):
"""
Make a request and return the JSON
"""
results = []
ret = requests.get(url,
auth=HTTPBasicAuth(self.cloudforms_username, self.cloudforms_pw),
verify=self.cloudforms_ssl_verify)
ret.raise_for_status()
try:
results = json.loads(ret.text)
except ValueError:
warnings.warn("Unexpected response from {0} ({1}): {2}".format(self.cloudforms_url, ret.status_code, ret.reason))
results = {}
if self.args.debug:
print("=======================================================================")
print("=======================================================================")
print("=======================================================================")
print(ret.text)
print("=======================================================================")
print("=======================================================================")
print("=======================================================================")
return results
def _get_hosts(self):
"""
Get all hosts by paging through the results
"""
limit = self.cloudforms_limit
page = 0
last_page = False
results = []
while not last_page:
offset = page * limit
ret = self._get_json("%s/api/vms?offset=%s&limit=%s&expand=resources,tags,hosts,&attributes=ipaddresses" % (self.cloudforms_url, offset, limit))
results += ret['resources']
if ret['subcount'] < limit:
last_page = True
page += 1
return results
def update_cache(self):
"""
Make calls to cloudforms and save the output in a cache
"""
self.groups = dict()
self.hosts = dict()
if self.args.debug:
print("Updating cache...")
for host in self._get_hosts():
# Ignore VMs that are not powered on
if host['power_state'] != 'on':
if self.args.debug:
print("Skipping %s because power_state = %s" % (host['name'], host['power_state']))
continue
# purge actions
if self.cloudforms_purge_actions and 'actions' in host:
del host['actions']
# Create ansible groups for tags
if 'tags' in host:
# Create top-level group
if 'tags' not in self.inventory:
self.inventory['tags'] = dict(children=[], vars={}, hosts=[])
if not self.cloudforms_nest_tags:
# don't expand tags, just use them in a safe way
for group in host['tags']:
# Add sub-group, as a child of top-level
safe_key = self.to_safe(group['name'])
if safe_key:
if self.args.debug:
print("Adding sub-group '%s' to parent 'tags'" % safe_key)
if safe_key not in self.inventory['tags']['children']:
self.push(self.inventory['tags'], 'children', safe_key)
self.push(self.inventory, safe_key, host['name'])
if self.args.debug:
print("Found tag [%s] for host which will be mapped to [%s]" % (group['name'], safe_key))
else:
# expand the tags into nested groups / sub-groups
# Create nested groups for tags
safe_parent_tag_name = 'tags'
for tag in host['tags']:
tag_hierarchy = tag['name'][1:].split('/')
if self.args.debug:
print("Working on list %s" % tag_hierarchy)
for tag_name in tag_hierarchy:
if self.args.debug:
print("Working on tag_name = %s" % tag_name)
safe_tag_name = self.to_safe(tag_name)
if self.args.debug:
print("Using sanitized name %s" % safe_tag_name)
# Create sub-group
if safe_tag_name not in self.inventory:
self.inventory[safe_tag_name] = dict(children=[], vars={}, hosts=[])
# Add sub-group, as a child of top-level
if safe_parent_tag_name:
if self.args.debug:
print("Adding sub-group '%s' to parent '%s'" % (safe_tag_name, safe_parent_tag_name))
if safe_tag_name not in self.inventory[safe_parent_tag_name]['children']:
self.push(self.inventory[safe_parent_tag_name], 'children', safe_tag_name)
# Make sure the next one uses this one as it's parent
safe_parent_tag_name = safe_tag_name
# Add the host to the last tag
self.push(self.inventory[safe_parent_tag_name], 'hosts', host['name'])
# Set ansible_ssh_host to the first available ip address
if 'ipaddresses' in host and host['ipaddresses'] and isinstance(host['ipaddresses'], list):
host['ansible_ssh_host'] = host['ipaddresses'][0]
# Create additional groups
for key in ('location', 'type', 'vendor'):
safe_key = self.to_safe(host[key])
# Create top-level group
if key not in self.inventory:
self.inventory[key] = dict(children=[], vars={}, hosts=[])
# Create sub-group
if safe_key not in self.inventory:
self.inventory[safe_key] = dict(children=[], vars={}, hosts=[])
# Add sub-group, as a child of top-level
if safe_key not in self.inventory[key]['children']:
self.push(self.inventory[key], 'children', safe_key)
if key in host:
# Add host to sub-group # Add host to sub-group
if resource['name'] not in self.inventory[resource[key]]: self.push(self.inventory[safe_key], 'hosts', host['name'])
self.inventory[resource[key]]['hosts'].append(resource['name'])
# Delete 'actions' key self.hosts[host['name']] = host
del resource['actions'] self.push(self.inventory, 'all', host['name'])
# Add _meta hostvars if self.args.debug:
self.inventory['_meta']['hostvars'][resource['name']] = resource print("Saving cached data")
print json.dumps(self.inventory, indent=2) self.write_to_cache(self.hosts, self.cache_path_hosts)
self.write_to_cache(self.inventory, self.cache_path_inventory)
def get_host_info(self, host):
"""
Get variables about a specific host
"""
if not self.hosts or len(self.hosts) == 0:
# Need to load cache from cache
self.load_hosts_from_cache()
if host not in self.hosts:
if self.args.debug:
print("[%s] not found in cache." % host)
# try updating the cache
self.update_cache()
if host not in self.hosts:
if self.args.debug:
print("[%s] does not exist after cache update." % host)
# host might not exist anymore
return self.json_format_dict({}, self.args.pretty)
return self.json_format_dict(self.hosts[host], self.args.pretty)
def push(self, d, k, v):
"""
Safely puts a new entry onto an array.
"""
if k in d:
d[k].append(v)
else:
d[k] = [v]
def load_inventory_from_cache(self):
"""
Reads the inventory from the cache file sets self.inventory
"""
cache = open(self.cache_path_inventory, 'r')
json_inventory = cache.read()
self.inventory = json.loads(json_inventory)
def load_hosts_from_cache(self):
"""
Reads the cache from the cache file sets self.hosts
"""
cache = open(self.cache_path_hosts, 'r')
json_cache = cache.read()
self.hosts = json.loads(json_cache)
def write_to_cache(self, data, filename):
"""
Writes data in JSON format to a file
"""
json_data = self.json_format_dict(data, True)
cache = open(filename, 'w')
cache.write(json_data)
cache.close()
def to_safe(self, word):
"""
Converts 'bad' characters in a string to underscores so they can be used as Ansible groups
"""
if self.cloudforms_clean_group_keys:
regex = "[^A-Za-z0-9\_]"
return re.sub(regex, "_", word.replace(" ", ""))
else:
return word
def json_format_dict(self, data, pretty=False):
"""
Converts a dict to a JSON object and dumps it as a formatted string
"""
if pretty:
return json.dumps(data, sort_keys=True, indent=2)
else:
return json.dumps(data)
# Run the script
CloudFormsInventory() CloudFormsInventory()

View File

@@ -1,8 +1,6 @@
#!/usr/bin/python #!/usr/bin/env python
# vim: set fileencoding=utf-8 : # vim: set fileencoding=utf-8 :
# #
# NOTE FOR TOWER: change foreman_ to sattelite_ for the group prefix
#
# Copyright (C) 2016 Guido Günther <agx@sigxcpu.org> # Copyright (C) 2016 Guido Günther <agx@sigxcpu.org>
# #
# This script is free software: you can redistribute it and/or modify # This script is free software: you can redistribute it and/or modify
@@ -41,6 +39,7 @@ class ForemanInventory(object):
self.inventory = dict() # A list of groups and the hosts in that group self.inventory = dict() # A list of groups and the hosts in that group
self.cache = dict() # Details about hosts in the inventory self.cache = dict() # Details about hosts in the inventory
self.params = dict() # Params of each host self.params = dict() # Params of each host
self.facts = dict() # Facts of each host
self.hostgroups = dict() # host groups self.hostgroups = dict() # host groups
# Read settings and parse CLI arguments # Read settings and parse CLI arguments
@@ -55,6 +54,7 @@ class ForemanInventory(object):
else: else:
self.load_inventory_from_cache() self.load_inventory_from_cache()
self.load_params_from_cache() self.load_params_from_cache()
self.load_facts_from_cache()
self.load_cache_from_cache() self.load_cache_from_cache()
data_to_print = "" data_to_print = ""
@@ -69,6 +69,9 @@ class ForemanInventory(object):
'foreman': self.cache[hostname], 'foreman': self.cache[hostname],
'foreman_params': self.params[hostname], 'foreman_params': self.params[hostname],
} }
if self.want_facts:
self.inventory['_meta']['hostvars'][hostname]['foreman_facts'] = self.facts[hostname]
data_to_print += self.json_format_dict(self.inventory, True) data_to_print += self.json_format_dict(self.inventory, True)
print(data_to_print) print(data_to_print)
@@ -81,7 +84,8 @@ class ForemanInventory(object):
current_time = time() current_time = time()
if (mod_time + self.cache_max_age) > current_time: if (mod_time + self.cache_max_age) > current_time:
if (os.path.isfile(self.cache_path_inventory) and if (os.path.isfile(self.cache_path_inventory) and
os.path.isfile(self.cache_path_params)): os.path.isfile(self.cache_path_params) and
os.path.isfile(self.cache_path_facts)):
return True return True
return False return False
@@ -114,6 +118,16 @@ class ForemanInventory(object):
self.group_patterns = eval(group_patterns) self.group_patterns = eval(group_patterns)
try:
self.group_prefix = config.get('ansible', 'group_prefix')
except (ConfigParser.NoOptionError, ConfigParser.NoSectionError):
self.group_prefix = "foreman_"
try:
self.want_facts = config.getboolean('ansible', 'want_facts')
except (ConfigParser.NoOptionError, ConfigParser.NoSectionError):
self.want_facts = True
# Cache related # Cache related
try: try:
cache_path = os.path.expanduser(config.get('cache', 'path')) cache_path = os.path.expanduser(config.get('cache', 'path'))
@@ -123,6 +137,7 @@ class ForemanInventory(object):
self.cache_path_cache = cache_path + "/%s.cache" % script self.cache_path_cache = cache_path + "/%s.cache" % script
self.cache_path_inventory = cache_path + "/%s.index" % script self.cache_path_inventory = cache_path + "/%s.index" % script
self.cache_path_params = cache_path + "/%s.params" % script self.cache_path_params = cache_path + "/%s.params" % script
self.cache_path_facts = cache_path + "/%s.facts" % script
self.cache_max_age = config.getint('cache', 'max_age') self.cache_max_age = config.getint('cache', 'max_age')
def parse_cli_args(self): def parse_cli_args(self):
@@ -135,7 +150,7 @@ class ForemanInventory(object):
help='Force refresh of cache by making API requests to foreman (default: False - use cache files)') help='Force refresh of cache by making API requests to foreman (default: False - use cache files)')
self.args = parser.parse_args() self.args = parser.parse_args()
def _get_json(self, url): def _get_json(self, url, ignore_errors=None):
page = 1 page = 1
results = [] results = []
while True: while True:
@@ -143,10 +158,14 @@ class ForemanInventory(object):
auth=HTTPBasicAuth(self.foreman_user, self.foreman_pw), auth=HTTPBasicAuth(self.foreman_user, self.foreman_pw),
verify=self.foreman_ssl_verify, verify=self.foreman_ssl_verify,
params={'page': page, 'per_page': 250}) params={'page': page, 'per_page': 250})
if ignore_errors and ret.status_code in ignore_errors:
break
ret.raise_for_status() ret.raise_for_status()
json = ret.json() json = ret.json()
if not json.has_key('results'): if not json.has_key('results'):
return json return json
if type(json['results']) == type({}):
return json['results']
results = results + json['results'] results = results + json['results']
if len(results) >= json['total']: if len(results) >= json['total']:
break break
@@ -162,38 +181,44 @@ class ForemanInventory(object):
self.hostgroups[hid] = self._get_json(url) self.hostgroups[hid] = self._get_json(url)
return self.hostgroups[hid] return self.hostgroups[hid]
def _get_params_by_id(self, hid): def _get_all_params_by_id(self, hid):
url = "%s/api/v2/hosts/%s/parameters" % (self.foreman_url, hid) url = "%s/api/v2/hosts/%s" % (self.foreman_url, hid)
ret = self._get_json(url, [404])
if ret == []: ret = {}
return ret.get('all_parameters', {})
def _get_facts_by_id(self, hid):
url = "%s/api/v2/hosts/%s/facts" % (self.foreman_url, hid)
return self._get_json(url) return self._get_json(url)
def _resolve_params(self, host): def _resolve_params(self, host):
""" """
Resolve all host group params of the host using the top level Fetch host params and convert to dict
hostgroup and the ancestry.
""" """
hostgroup_id = host['hostgroup_id']
paramgroups = []
params = {} params = {}
if hostgroup_id: for param in self._get_all_params_by_id(host['id']):
hostgroup = self._get_hostgroup_by_id(hostgroup_id) name = param['name']
ancestry_path = hostgroup.get('ancestry', '') params[name] = param['value']
ancestry = ancestry_path.split('/') if ancestry_path is not None else []
# Append top level hostgroup last to overwrite lower levels
# values
ancestry.append(hostgroup_id)
paramgroups = [self._get_hostgroup_by_id(hostgroup_id)['parameters']
for hostgroup_id in ancestry]
paramgroups += [self._get_params_by_id(host['id'])]
for paramgroup in paramgroups:
for param in paramgroup:
name = param['name']
params[name] = param['value']
return params return params
def _get_facts(self, host):
"""
Fetch all host facts of the host
"""
if not self.want_facts:
return {}
ret = self._get_facts_by_id(host['id'])
if len(ret.values()) == 0:
facts = {}
elif len(ret.values()) == 1:
facts = ret.values()[0]
else:
raise ValueError("More than one set of facts returned for '%s'" % host)
return facts
def update_cache(self): def update_cache(self):
"""Make calls to foreman and save the output in a cache""" """Make calls to foreman and save the output in a cache"""
@@ -203,11 +228,17 @@ class ForemanInventory(object):
for host in self._get_hosts(): for host in self._get_hosts():
dns_name = host['name'] dns_name = host['name']
# Create ansible groups for hostgroup, location and organization # Create ansible groups for hostgroup, environment, location and organization
for group in ['hostgroup', 'location', 'organization']: for group in ['hostgroup', 'environment', 'location', 'organization']:
val = host.get('%s_name' % group) val = host.get('%s_name' % group)
if val: if val:
safe_key = self.to_safe('satellite_%s_%s' % (group, val.lower())) safe_key = self.to_safe('%s%s_%s' % (self.group_prefix, group, val.lower()))
self.push(self.inventory, safe_key, dns_name)
for group in ['lifecycle_environment', 'content_view']:
val = host.get('content_facet_attributes', {}).get('%s_name' % group)
if val:
safe_key = self.to_safe('%s%s_%s' % (self.group_prefix, group, val.lower()))
self.push(self.inventory, safe_key, dns_name) self.push(self.inventory, safe_key, dns_name)
params = self._resolve_params(host) params = self._resolve_params(host)
@@ -231,11 +262,13 @@ class ForemanInventory(object):
self.cache[dns_name] = host self.cache[dns_name] = host
self.params[dns_name] = params self.params[dns_name] = params
self.facts[dns_name] = self._get_facts(host)
self.push(self.inventory, 'all', dns_name) self.push(self.inventory, 'all', dns_name)
self.write_to_cache(self.cache, self.cache_path_cache) self.write_to_cache(self.cache, self.cache_path_cache)
self.write_to_cache(self.inventory, self.cache_path_inventory) self.write_to_cache(self.inventory, self.cache_path_inventory)
self.write_to_cache(self.params, self.cache_path_params) self.write_to_cache(self.params, self.cache_path_params)
self.write_to_cache(self.facts, self.cache_path_facts)
def get_host_info(self): def get_host_info(self):
""" Get variables about a specific host """ """ Get variables about a specific host """
@@ -274,6 +307,14 @@ class ForemanInventory(object):
json_params = cache.read() json_params = cache.read()
self.params = json.loads(json_params) self.params = json.loads(json_params)
def load_facts_from_cache(self):
""" Reads the index from the cache file sets self.index """
if not self.want_facts:
return
cache = open(self.cache_path_facts, 'r')
json_facts = cache.read()
self.facts = json.loads(json_facts)
def load_cache_from_cache(self): def load_cache_from_cache(self):
""" Reads the cache from the cache file sets self.cache """ """ Reads the cache from the cache file sets self.cache """
@@ -301,4 +342,7 @@ class ForemanInventory(object):
else: else:
return json.dumps(data) return json.dumps(data)
ForemanInventory() if __name__ == '__main__':
ForemanInventory()

View File

@@ -351,7 +351,7 @@ CELERYBEAT_SCHEDULE = {
}, },
} }
# Use Redis as cache backend (except when testing). # Django Caching Configuration
if is_testing(): if is_testing():
CACHES = { CACHES = {
'default': { 'default': {
@@ -361,8 +361,8 @@ if is_testing():
else: else:
CACHES = { CACHES = {
'default': { 'default': {
'BACKEND': 'redis_cache.RedisCache', 'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache',
'LOCATION': BROKER_URL, 'LOCATION': 'memcached:11211',
}, },
} }

View File

@@ -7,18 +7,31 @@
import sys import sys
import traceback import traceback
# Centos-7 doesn't include the svg mime type
# /usr/lib64/python/mimetypes.py
import mimetypes
# Django Split Settings # Django Split Settings
from split_settings.tools import optional, include from split_settings.tools import optional, include
# Load default settings. # Load default settings.
from defaults import * # NOQA from defaults import * # NOQA
mimetypes.add_type("image/svg+xml", ".svg", True)
mimetypes.add_type("image/svg+xml", ".svgz", True)
MONGO_HOST = '127.0.0.1' MONGO_HOST = '127.0.0.1'
MONGO_PORT = 27017 MONGO_PORT = 27017
MONGO_USERNAME = None MONGO_USERNAME = None
MONGO_PASSWORD = None MONGO_PASSWORD = None
MONGO_DB = 'system_tracking_dev' MONGO_DB = 'system_tracking_dev'
# Override django.template.loaders.cached.Loader in defaults.py
TEMPLATE_LOADERS = (
'django.template.loaders.filesystem.Loader',
'django.template.loaders.app_directories.Loader',
)
# Disable capturing all SQL queries when running celeryd in development. # Disable capturing all SQL queries when running celeryd in development.
if 'celeryd' in sys.argv: if 'celeryd' in sys.argv:
SQL_DEBUG = False SQL_DEBUG = False
@@ -71,9 +84,9 @@ include(optional('/etc/tower/settings.py'), scope=locals())
include(optional('/etc/tower/conf.d/*.py'), scope=locals()) include(optional('/etc/tower/conf.d/*.py'), scope=locals())
ANSIBLE_USE_VENV = True ANSIBLE_USE_VENV = True
ANSIBLE_VENV_PATH = "/tower_devel/venv/ansible" ANSIBLE_VENV_PATH = "/venv/ansible"
TOWER_USE_VENV = True TOWER_USE_VENV = True
TOWER_VENV_PATH = "/tower_devel/venv/tower" TOWER_VENV_PATH = "/venv/tower"
# If any local_*.py files are present in awx/settings/, use them to override # If any local_*.py files are present in awx/settings/, use them to override
# default settings for development. If not present, we can still run using # default settings for development. If not present, we can still run using

View File

@@ -48,23 +48,8 @@ if is_testing(sys.argv):
MONGO_DB = 'system_tracking_test' MONGO_DB = 'system_tracking_test'
# Django Caching Configuration
if is_testing():
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
},
}
else:
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache',
'LOCATION': 'memcached:11211',
},
}
# Celery AMQP configuration. # Celery AMQP configuration.
BROKER_URL = 'qpid://qpid:5672' BROKER_URL = 'amqp://guest:guest@rabbitmq//'
# Mongo host configuration # Mongo host configuration
MONGO_HOST = NotImplemented MONGO_HOST = NotImplemented

View File

@@ -1,2 +1,21 @@
# Copyright (c) 2015 Ansible, Inc. # Copyright (c) 2015 Ansible, Inc.
# All Rights Reserved. # All Rights Reserved.
# Python
import threading
# Monkeypatch xmlsec.initialize() to only run once (https://github.com/ansible/ansible-tower/issues/3241).
xmlsec_init_lock = threading.Lock()
xmlsec_initialized = False
import dm.xmlsec.binding
original_xmlsec_initialize = dm.xmlsec.binding.initialize
def xmlsec_initialize(*args, **kwargs):
global xmlsec_init_lock, xmlsec_initialized, original_xmlsec_initialize
with xmlsec_init_lock:
if not xmlsec_initialized:
original_xmlsec_initialize(*args, **kwargs)
xmlsec_initialized = True
dm.xmlsec.binding.initialize = xmlsec_initialize

View File

@@ -14,7 +14,6 @@ import 'jquery.resize';
import 'codemirror'; import 'codemirror';
import 'js-yaml'; import 'js-yaml';
import 'select2'; import 'select2';
import 'rrule';
// Configuration dependencies // Configuration dependencies
global.$AnsibleConfig = null; global.$AnsibleConfig = null;

View File

@@ -38,9 +38,6 @@
<label class="Form-inputLabel"> <label class="Form-inputLabel">
<span class="red-text">*</span> <span class="red-text">*</span>
Start Date Start Date
<span class="fmt-help">
(mm/dd/yyyy)
</span>
</label> </label>
<div class="input-group Form-inputGroup SchedulerForm-inputGroup--date"> <div class="input-group Form-inputGroup SchedulerForm-inputGroup--date">
<scheduler-date-picker date="schedulerStartDt"> <scheduler-date-picker date="schedulerStartDt">
@@ -487,9 +484,6 @@
<label class="Form-inputLabel"> <label class="Form-inputLabel">
<span class="red-text">*</span> <span class="red-text">*</span>
End Date End Date
<span class="fmt-help">
(mm/dd/yyyy)
</span>
</label> </label>
<div class="input-group Form-inputGroup SchedulerForm-inputGroup--date"> <div class="input-group Form-inputGroup SchedulerForm-inputGroup--date">
<scheduler-date-picker date="$parent.schedulerEndDt"> <scheduler-date-picker date="$parent.schedulerEndDt">

View File

@@ -38,9 +38,6 @@
<label class="Form-inputLabel"> <label class="Form-inputLabel">
<span class="red-text">*</span> <span class="red-text">*</span>
Start Date Start Date
<span class="fmt-help">
(mm/dd/yyyy)
</span>
</label> </label>
<div class="input-group Form-inputGroup SchedulerForm-inputGroup--date"> <div class="input-group Form-inputGroup SchedulerForm-inputGroup--date">
<scheduler-date-picker date="schedulerStartDt"> <scheduler-date-picker date="schedulerStartDt">
@@ -469,9 +466,6 @@
<label class="Form-inputLabel"> <label class="Form-inputLabel">
<span class="red-text">*</span> <span class="red-text">*</span>
End Date End Date
<span class="fmt-help">
(mm/dd/yyyy)
</span>
</label> </label>
<div class="input-group Form-inputGroup SchedulerForm-inputGroup--date"> <div class="input-group Form-inputGroup SchedulerForm-inputGroup--date">
<scheduler-date-picker date="$parent.schedulerEndDt"> <scheduler-date-picker date="$parent.schedulerEndDt">

View File

@@ -5,7 +5,10 @@ module.exports = {
http: { http: {
bsFiles: { bsFiles: {
src: [ src: [
'static/**/*' 'static/**/*',
'!static/tower.vendor.js',
'!static/tower.vendor.map.js',
'!static/tower.js.map'
] ]
}, },
options: { options: {

View File

@@ -74,7 +74,6 @@ module.exports = function(config) {
test: /\.angular.js$/, test: /\.angular.js$/,
loader: 'expose?angular' loader: 'expose?angular'
}, },
{ {
test: /\.js$/, test: /\.js$/,
loader: 'babel-loader', loader: 'babel-loader',
@@ -85,11 +84,12 @@ module.exports = function(config) {
} }
}, { }, {
test: /\.js$/, test: /\.js$/,
loader: 'babel-istanbul', loader: 'babel-loader',
include: [path.resolve() + '/client/src/'], include: [path.resolve() + '/client/src/'],
exclude: '/(node_modules)/', exclude: '/(node_modules)/',
query: { query: {
presets: ['es2015'] presets: ['es2015'],
plugins: ['istanbul']
} }
} }
] ]

View File

@@ -58,11 +58,12 @@ module.exports = function(config) {
} }
}, { }, {
test: /\.js$/, test: /\.js$/,
loader: 'babel-istanbul', loader: 'babel-loader',
include: [path.resolve() + '/client/src/'], include: [path.resolve() + '/client/src/'],
exclude: '/(node_modules)/', exclude: '/(node_modules)/',
query: { query: {
presets: ['es2015'] presets: ['es2015'],
plugins: ['istanbul']
} }
} }
] ]

File diff suppressed because it is too large Load Diff

View File

@@ -29,8 +29,8 @@
"angular-mocks": "^1.5.8", "angular-mocks": "^1.5.8",
"babel-core": "^6.11.4", "babel-core": "^6.11.4",
"babel-istanbul": "^0.11.0", "babel-istanbul": "^0.11.0",
"babel-istanbul-loader": "^0.1.0",
"babel-loader": "^6.2.4", "babel-loader": "^6.2.4",
"babel-plugin-istanbul": "^2.0.0",
"babel-preset-es2015": "^6.9.0", "babel-preset-es2015": "^6.9.0",
"browser-sync": "^2.14.0", "browser-sync": "^2.14.0",
"expose-loader": "^0.7.1", "expose-loader": "^0.7.1",
@@ -54,6 +54,7 @@
"karma-chrome-launcher": "^1.0.1", "karma-chrome-launcher": "^1.0.1",
"karma-coverage": "^1.1.1", "karma-coverage": "^1.1.1",
"karma-firefox-launcher": "^1.0.0", "karma-firefox-launcher": "^1.0.0",
"karma-html2js-preprocessor": "^1.0.0",
"karma-jasmine": "^1.0.2", "karma-jasmine": "^1.0.2",
"karma-sauce-launcher": "^1.0.0", "karma-sauce-launcher": "^1.0.0",
"karma-sourcemap-loader": "^0.3.7", "karma-sourcemap-loader": "^0.3.7",
@@ -61,7 +62,6 @@
"less-plugin-autoprefix": "^1.4.2", "less-plugin-autoprefix": "^1.4.2",
"load-grunt-configs": "^1.0.0", "load-grunt-configs": "^1.0.0",
"load-grunt-tasks": "^3.5.0", "load-grunt-tasks": "^3.5.0",
"stats-webpack-plugin": "^0.4.0",
"time-grunt": "^1.4.0", "time-grunt": "^1.4.0",
"webpack": "^1.13.1", "webpack": "^1.13.1",
"webpack-dev-server": "^1.14.1" "webpack-dev-server": "^1.14.1"
@@ -76,7 +76,7 @@
"angular-moment": "^0.10.1", "angular-moment": "^0.10.1",
"angular-resource": "^1.4.3", "angular-resource": "^1.4.3",
"angular-sanitize": "^1.4.3", "angular-sanitize": "^1.4.3",
"angular-scheduler": "chouseknecht/angular-scheduler#0.0.20", "angular-scheduler": "chouseknecht/angular-scheduler#0.1.0",
"angular-tz-extensions": "chouseknecht/angular-tz-extensions#0.3.11", "angular-tz-extensions": "chouseknecht/angular-tz-extensions#0.3.11",
"angular-ui-router": "^0.2.15", "angular-ui-router": "^0.2.15",
"bootstrap": "^3.1.1", "bootstrap": "^3.1.1",
@@ -95,7 +95,6 @@
"moment": "^2.10.2", "moment": "^2.10.2",
"ng-toast": "leigh-johnson/ngToast#2.0.1", "ng-toast": "leigh-johnson/ngToast#2.0.1",
"nvd3": "leigh-johnson/nvd3#1.7.1", "nvd3": "leigh-johnson/nvd3#1.7.1",
"rrule": "jkbrzt/rrule#4ff63b2f8524fd6d5ba6e80db770953b5cd08a0c",
"select2": "^4.0.2", "select2": "^4.0.2",
"socket.io-client": "^0.9.17" "socket.io-client": "^0.9.17"
} }

View File

@@ -1,6 +1,5 @@
var path = require('path'), var path = require('path'),
webpack = require('webpack'), webpack = require('webpack');
StatsPlugin = require('stats-webpack-plugin');
var vendorPkgs = [ var vendorPkgs = [
'angular', 'angular',
@@ -51,13 +50,9 @@ var dev = {
'CodeMirror': 'codemirror', 'CodeMirror': 'codemirror',
'jsyaml': 'js-yaml', 'jsyaml': 'js-yaml',
'jsonlint': 'codemirror.jsonlint', 'jsonlint': 'codemirror.jsonlint',
'RRule': 'rrule'
}), }),
// (chunkName, outfileName) // (chunkName, outfileName)
new webpack.optimize.CommonsChunkPlugin('vendor', 'tower.vendor.js'), new webpack.optimize.CommonsChunkPlugin('vendor', 'tower.vendor.js'),
new StatsPlugin('stats.json', {
chunkModules: true
})
], ],
module: { module: {
preLoaders: [{ preLoaders: [{
@@ -70,15 +65,6 @@ var dev = {
} }
}], }],
loaders: [ loaders: [
{ // expose RRule global for nlp module, whose AMD/CJS loading methods are broken
test: /\.rrule.js$/,
loader: 'expose?RRule'
},
{
test: /\.nlp.js$/,
// disable CommonJS & AMD loading (broken in this lib)
loader: 'imports?require=>false&define=>false'
},
{ {
// disable AMD loading (broken in this lib) and default to CommonJS (not broken) // disable AMD loading (broken in this lib) and default to CommonJS (not broken)
test: /\.angular-tz-extensions.js$/, test: /\.angular-tz-extensions.js$/,
@@ -91,9 +77,6 @@ var dev = {
query: { query: {
presets: ['es2015'] presets: ['es2015']
} }
}, {
test: /\.nlp.js$/,
loader: 'imports?RRule=rrule'
}] }]
}, },
resolve: { resolve: {
@@ -134,15 +117,7 @@ var release = {
}) })
], ],
module: { module: {
loaders: [{ loaders: [
test: /\.rrule.js$/,
loader: 'expose?RRule'
},
{
test: /\.nlp.js$/,
// disable CommonJS (broken in this lib)
loader: 'imports?require=>false'
},
{ {
// disable AMD loading (broken in this lib) and default to CommonJS (not broken) // disable AMD loading (broken in this lib) and default to CommonJS (not broken)
test: /\.angular-tz-extensions.js$/, test: /\.angular-tz-extensions.js$/,

11232
npm-shrinkwrap.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
[pytest] [pytest]
DJANGO_SETTINGS_MODULE = awx.settings.development DJANGO_SETTINGS_MODULE = awx.settings.development
python_paths = venv/tower/lib/python2.7/site-packages python_paths = /venv/tower/lib/python2.7/site-packages
site_dirs = venv/tower/lib/python2.7/site-packages site_dirs = /venv/tower/lib/python2.7/site-packages
python_files = *.py python_files = *.py
addopts = --reuse-db --nomigrations --tb=native addopts = --reuse-db --nomigrations --tb=native
markers = markers =

View File

@@ -21,8 +21,6 @@ django-extensions==1.5.9
git+https://github.com/chrismeyersfsu/django-jsonbfield@fix-sqlite_serialization#egg=jsonbfield git+https://github.com/chrismeyersfsu/django-jsonbfield@fix-sqlite_serialization#egg=jsonbfield
django-polymorphic==0.7.2 django-polymorphic==0.7.2
django-radius==1.0.0 django-radius==1.0.0
# NOTE: Remove when we transition packaging
django-redis-cache==1.6.5
djangorestframework==3.3.2 djangorestframework==3.3.2
djangorestframework-yaml==1.0.2 djangorestframework-yaml==1.0.2
django-split-settings==0.1.1 django-split-settings==0.1.1
@@ -110,13 +108,10 @@ python-troveclient==1.4.0
pytz==2015.7 pytz==2015.7
PyYAML==3.11 PyYAML==3.11
pyzmq==14.5.0 pyzmq==14.5.0
qpid-python==0.32.1
rackspace-auth-openstack==1.3 rackspace-auth-openstack==1.3
rackspace-novaclient==1.5 rackspace-novaclient==1.5
rax-default-network-flags-python-novaclient-ext==0.3.2 rax-default-network-flags-python-novaclient-ext==0.3.2
rax-scheduled-images-python-novaclient-ext==0.3.1 rax-scheduled-images-python-novaclient-ext==0.3.1
# NOTE: Remove this when we transition packaging
redis==2.10.3
requests-oauthlib==0.5.0 requests-oauthlib==0.5.0
requests==2.9.1 requests==2.9.1
requestsexceptions==1.1.1 requestsexceptions==1.1.1

View File

@@ -10,4 +10,3 @@ pytest-cov
pytest-django pytest-django
pytest-pythonpath pytest-pythonpath
pytest-mock pytest-mock
qpid-tools

View File

@@ -9,7 +9,7 @@ services:
links: links:
- postgres - postgres
- memcached - memcached
- qpid - rabbitmq
# - sync # - sync
# volumes_from: # volumes_from:
# - sync # - sync
@@ -23,9 +23,8 @@ services:
memcached: memcached:
image: memcached:alpine image: memcached:alpine
qpid: rabbitmq:
image: fedora/qpid:latest image: rabbitmq:3-management
entrypoint: qpidd --auth=no
# Source Code Synchronization Container # Source Code Synchronization Container
# sync: # sync:

View File

@@ -4,7 +4,7 @@ set +x
# Wait for the databases to come up # Wait for the databases to come up
ansible -i "127.0.0.1," -c local -v -m wait_for -a "host=postgres port=5432" all ansible -i "127.0.0.1," -c local -v -m wait_for -a "host=postgres port=5432" all
ansible -i "127.0.0.1," -c local -v -m wait_for -a "host=memcached port=11211" all ansible -i "127.0.0.1," -c local -v -m wait_for -a "host=memcached port=11211" all
ansible -i "127.0.0.1," -c local -v -m wait_for -a "host=qpid port=5672" all ansible -i "127.0.0.1," -c local -v -m wait_for -a "host=rabbitmq port=5672" all
# In case Tower in the container wants to connect to itself, use "docker exec" to attach to the container otherwise # In case Tower in the container wants to connect to itself, use "docker exec" to attach to the container otherwise
# TODO: FIX # TODO: FIX