From 98883e771ffd5f6741de95fee9952092ea8831d2 Mon Sep 17 00:00:00 2001 From: Chris Church Date: Mon, 4 Nov 2013 15:44:43 -0500 Subject: [PATCH] Moved API code into separate Django app. --- awx/{middleware => api}/__init__.py | 0 awx/{main => api}/authentication.py | 0 awx/{main => api}/filters.py | 0 awx/{main/base_views.py => api/generics.py} | 2 +- awx/api/models.py | 4 + awx/{main => api}/pagination.py | 0 awx/{main => api}/permissions.py | 2 +- awx/{main => api}/renderers.py | 0 awx/{main => api}/serializers.py | 190 +-- .../templates/api}/_list_common.md | 2 +- .../main => api/templates/api}/_new_in_awx.md | 0 .../templates/api}/_result_fields_common.md | 0 .../templates/api}/api_root_view.md | 0 .../templates/api}/api_v1_config_view.md | 0 .../templates/api}/api_v1_root_view.md | 0 awx/api/templates/api/api_view.md | 3 + .../templates/api}/auth_token_view.md | 0 .../templates/api}/base_variable_data.md | 0 .../templates/api}/group_all_hosts_list.md | 2 +- .../api}/group_potential_children_list.md | 4 +- .../templates/api}/host_all_groups_list.md | 2 +- .../api}/inventory_root_groups_list.md | 2 +- .../templates/api}/inventory_script_view.md | 0 .../templates/api}/inventory_source_cancel.md | 2 +- .../api}/inventory_source_update_view.md | 2 +- .../templates/api}/inventory_tree_view.md | 4 +- .../main => api/templates/api}/job_cancel.md | 0 .../main => api/templates/api}/job_list.md | 2 +- .../main => api/templates/api}/job_start.md | 0 .../templates/api}/job_template_callback.md | 0 .../templates/api}/job_template_jobs_list.md | 2 +- .../templates/api}/list_api_view.md | 4 +- .../templates/api}/list_create_api_view.md | 6 +- .../api}/organization_admins_list.md | 2 +- .../templates/api}/project_playbooks.md | 0 .../templates/api}/project_update_cancel.md | 2 +- .../templates/api}/project_update_view.md | 2 +- .../templates/api}/retrieve_api_view.md | 4 +- .../api}/retrieve_update_api_view.md | 6 +- .../api}/retrieve_update_destroy_api_view.md | 6 +- .../templates/api}/sub_list_api_view.md | 4 +- .../api}/sub_list_create_api_view.md | 6 +- .../api}/user_admin_of_organizations_list.md | 2 +- .../templates/api}/user_me_list.md | 2 +- awx/api/urls.py | 170 +++ awx/api/views.py | 1059 +++++++++++++++++ awx/lib/__init__.py | 2 + awx/{main => lib}/compat.py | 0 awx/main/admin.py | 2 +- awx/main/models/__init__.py | 32 +- awx/main/templates/main/api_view.md | 3 - awx/main/tests/inventory.py | 80 +- awx/main/tests/jobs.py | 62 +- awx/main/tests/organizations.py | 14 +- awx/main/tests/projects.py | 66 +- awx/main/tests/users.py | 72 +- awx/main/urls.py | 169 +-- awx/main/views.py | 1055 +--------------- awx/middleware/exceptions.py | 13 - awx/settings/defaults.py | 27 +- awx/urls.py | 2 +- 61 files changed, 1553 insertions(+), 1546 deletions(-) rename awx/{middleware => api}/__init__.py (100%) rename awx/{main => api}/authentication.py (100%) rename awx/{main => api}/filters.py (100%) rename awx/{main/base_views.py => api/generics.py} (99%) create mode 100644 awx/api/models.py rename awx/{main => api}/pagination.py (100%) rename awx/{main => api}/permissions.py (99%) rename awx/{main => api}/renderers.py (100%) rename awx/{main => api}/serializers.py (82%) rename awx/{main/templates/main => api/templates/api}/_list_common.md (98%) rename awx/{main/templates/main => api/templates/api}/_new_in_awx.md (100%) rename awx/{main/templates/main => api/templates/api}/_result_fields_common.md (100%) rename awx/{main/templates/main => api/templates/api}/api_root_view.md (100%) rename awx/{main/templates/main => api/templates/api}/api_v1_config_view.md (100%) rename awx/{main/templates/main => api/templates/api}/api_v1_root_view.md (100%) create mode 100644 awx/api/templates/api/api_view.md rename awx/{main/templates/main => api/templates/api}/auth_token_view.md (100%) rename awx/{main/templates/main => api/templates/api}/base_variable_data.md (100%) rename awx/{main/templates/main => api/templates/api}/group_all_hosts_list.md (87%) rename awx/{main/templates/main => api/templates/api}/group_potential_children_list.md (77%) rename awx/{main/templates/main => api/templates/api}/host_all_groups_list.md (88%) rename awx/{main/templates/main => api/templates/api}/inventory_root_groups_list.md (87%) rename awx/{main/templates/main => api/templates/api}/inventory_script_view.md (100%) rename awx/{main/templates/main => api/templates/api}/inventory_source_cancel.md (92%) rename awx/{main/templates/main => api/templates/api}/inventory_source_update_view.md (95%) rename awx/{main/templates/main => api/templates/api}/inventory_tree_view.md (82%) rename awx/{main/templates/main => api/templates/api}/job_cancel.md (100%) rename awx/{main/templates/main => api/templates/api}/job_list.md (79%) rename awx/{main/templates/main => api/templates/api}/job_start.md (100%) rename awx/{main/templates/main => api/templates/api}/job_template_callback.md (100%) rename awx/{main/templates/main => api/templates/api}/job_template_jobs_list.md (78%) rename awx/{main/templates/main => api/templates/api}/list_api_view.md (66%) rename awx/{main/templates/main => api/templates/api}/list_create_api_view.md (64%) rename awx/{main/templates/main => api/templates/api}/organization_admins_list.md (66%) rename awx/{main/templates/main => api/templates/api}/project_playbooks.md (100%) rename awx/{main/templates/main => api/templates/api}/project_update_cancel.md (92%) rename awx/{main/templates/main => api/templates/api}/project_update_view.md (95%) rename awx/{main/templates/main => api/templates/api}/retrieve_api_view.md (67%) rename awx/{main/templates/main => api/templates/api}/retrieve_update_api_view.md (73%) rename awx/{main/templates/main => api/templates/api}/retrieve_update_destroy_api_view.md (78%) rename awx/{main/templates/main => api/templates/api}/sub_list_api_view.md (77%) rename awx/{main/templates/main => api/templates/api}/sub_list_create_api_view.md (90%) rename awx/{main/templates/main => api/templates/api}/user_admin_of_organizations_list.md (87%) rename awx/{main/templates/main => api/templates/api}/user_me_list.md (82%) create mode 100644 awx/api/urls.py create mode 100644 awx/api/views.py create mode 100644 awx/lib/__init__.py rename awx/{main => lib}/compat.py (100%) delete mode 100644 awx/main/templates/main/api_view.md delete mode 100644 awx/middleware/exceptions.py diff --git a/awx/middleware/__init__.py b/awx/api/__init__.py similarity index 100% rename from awx/middleware/__init__.py rename to awx/api/__init__.py diff --git a/awx/main/authentication.py b/awx/api/authentication.py similarity index 100% rename from awx/main/authentication.py rename to awx/api/authentication.py diff --git a/awx/main/filters.py b/awx/api/filters.py similarity index 100% rename from awx/main/filters.py rename to awx/api/filters.py diff --git a/awx/main/base_views.py b/awx/api/generics.py similarity index 99% rename from awx/main/base_views.py rename to awx/api/generics.py index 0b1d75a621..89230de9b0 100644 --- a/awx/main/base_views.py +++ b/awx/api/generics.py @@ -98,7 +98,7 @@ class APIView(views.APIView): template_list = [] for klass in inspect.getmro(type(self)): template_basename = camelcase_to_underscore(klass.__name__) - template_list.append('main/%s.md' % template_basename) + template_list.append('api/%s.md' % template_basename) context = self.get_description_context() return render_to_string(template_list, context) diff --git a/awx/api/models.py b/awx/api/models.py new file mode 100644 index 0000000000..f30c0d520a --- /dev/null +++ b/awx/api/models.py @@ -0,0 +1,4 @@ +# Copyright (c) 2013 AnsibleWorks, Inc. +# All Rights Reserved. + +# Empty models file. diff --git a/awx/main/pagination.py b/awx/api/pagination.py similarity index 100% rename from awx/main/pagination.py rename to awx/api/pagination.py diff --git a/awx/main/permissions.py b/awx/api/permissions.py similarity index 99% rename from awx/main/permissions.py rename to awx/api/permissions.py index 68b760b986..4d0c3263be 100644 --- a/awx/main/permissions.py +++ b/awx/api/permissions.py @@ -16,7 +16,7 @@ from awx.main.access import * from awx.main.models import * from awx.main.utils import get_object_or_400 -logger = logging.getLogger('awx.main.permissions') +logger = logging.getLogger('awx.api.permissions') __all__ = ['ModelAccessPermission', 'JobTemplateCallbackPermission', 'JobTaskPermission'] diff --git a/awx/main/renderers.py b/awx/api/renderers.py similarity index 100% rename from awx/main/renderers.py rename to awx/api/renderers.py diff --git a/awx/main/serializers.py b/awx/api/serializers.py similarity index 82% rename from awx/main/serializers.py rename to awx/api/serializers.py index 7ec7f34bf5..f3b0bb077e 100644 --- a/awx/main/serializers.py +++ b/awx/api/serializers.py @@ -132,14 +132,14 @@ class BaseSerializer(serializers.ModelSerializer): if obj is None: return '' elif isinstance(obj, User): - return reverse('main:user_detail', args=(obj.pk,)) + return reverse('api:user_detail', args=(obj.pk,)) else: return obj.get_absolute_url() def get_related(self, obj): res = SortedDict() if getattr(obj, 'created_by', None): - res['created_by'] = reverse('main:user_detail', args=(obj.created_by.pk,)) + res['created_by'] = reverse('api:user_detail', args=(obj.created_by.pk,)) return res def get_summary_fields(self, obj): @@ -237,12 +237,12 @@ class UserSerializer(BaseSerializer): return {} res = super(UserSerializer, self).get_related(obj) res.update(dict( - teams = reverse('main:user_teams_list', args=(obj.pk,)), - organizations = reverse('main:user_organizations_list', args=(obj.pk,)), - admin_of_organizations = reverse('main:user_admin_of_organizations_list', args=(obj.pk,)), - projects = reverse('main:user_projects_list', args=(obj.pk,)), - credentials = reverse('main:user_credentials_list', args=(obj.pk,)), - permissions = reverse('main:user_permissions_list', args=(obj.pk,)), + teams = reverse('api:user_teams_list', args=(obj.pk,)), + organizations = reverse('api:user_organizations_list', args=(obj.pk,)), + admin_of_organizations = reverse('api:user_admin_of_organizations_list', args=(obj.pk,)), + projects = reverse('api:user_projects_list', args=(obj.pk,)), + credentials = reverse('api:user_credentials_list', args=(obj.pk,)), + permissions = reverse('api:user_permissions_list', args=(obj.pk,)), )) return res @@ -286,13 +286,13 @@ class OrganizationSerializer(BaseSerializer): return {} res = super(OrganizationSerializer, self).get_related(obj) res.update(dict( - #audit_trail = reverse('main:organization_audit_trail_list', args=(obj.pk,)), - projects = reverse('main:organization_projects_list', args=(obj.pk,)), - inventories = reverse('main:organization_inventories_list', args=(obj.pk,)), - users = reverse('main:organization_users_list', args=(obj.pk,)), - admins = reverse('main:organization_admins_list', args=(obj.pk,)), - #tags = reverse('main:organization_tags_list', args=(obj.pk,)), - teams = reverse('main:organization_teams_list', args=(obj.pk,)), + #audit_trail = reverse('api:organization_audit_trail_list', args=(obj.pk,)), + projects = reverse('api:organization_projects_list', args=(obj.pk,)), + inventories = reverse('api:organization_inventories_list', args=(obj.pk,)), + users = reverse('api:organization_users_list', args=(obj.pk,)), + admins = reverse('api:organization_admins_list', args=(obj.pk,)), + #tags = reverse('api:organization_tags_list', args=(obj.pk,)), + teams = reverse('api:organization_teams_list', args=(obj.pk,)), )) return res @@ -314,20 +314,20 @@ class ProjectSerializer(BaseSerializer): return {} res = super(ProjectSerializer, self).get_related(obj) res.update(dict( - organizations = reverse('main:project_organizations_list', args=(obj.pk,)), - teams = reverse('main:project_teams_list', args=(obj.pk,)), - playbooks = reverse('main:project_playbooks', args=(obj.pk,)), - update = reverse('main:project_update_view', args=(obj.pk,)), - project_updates = reverse('main:project_updates_list', args=(obj.pk,)), + organizations = reverse('api:project_organizations_list', args=(obj.pk,)), + teams = reverse('api:project_teams_list', args=(obj.pk,)), + playbooks = reverse('api:project_playbooks', args=(obj.pk,)), + update = reverse('api:project_update_view', args=(obj.pk,)), + project_updates = reverse('api:project_updates_list', args=(obj.pk,)), )) if obj.credential: - res['credential'] = reverse('main:credential_detail', + res['credential'] = reverse('api:credential_detail', args=(obj.credential.pk,)) if obj.current_update: - res['current_update'] = reverse('main:project_update_detail', + res['current_update'] = reverse('api:project_update_detail', args=(obj.current_update.pk,)) if obj.last_update: - res['last_update'] = reverse('main:project_update_detail', + res['last_update'] = reverse('api:project_update_detail', args=(obj.last_update.pk,)) return res @@ -433,8 +433,8 @@ class ProjectUpdateSerializer(BaseSerializer): return {} res = super(ProjectUpdateSerializer, self).get_related(obj) res.update(dict( - project = reverse('main:project_detail', args=(obj.project.pk,)), - cancel = reverse('main:project_update_cancel', args=(obj.pk,)), + project = reverse('api:project_detail', args=(obj.project.pk,)), + cancel = reverse('api:project_update_cancel', args=(obj.pk,)), )) return res @@ -467,14 +467,14 @@ class InventorySerializer(BaseSerializerWithVariables): return {} res = super(InventorySerializer, self).get_related(obj) res.update(dict( - hosts = reverse('main:inventory_hosts_list', args=(obj.pk,)), - groups = reverse('main:inventory_groups_list', args=(obj.pk,)), - root_groups = reverse('main:inventory_root_groups_list', args=(obj.pk,)), - variable_data = reverse('main:inventory_variable_data', args=(obj.pk,)), - script = reverse('main:inventory_script_view', args=(obj.pk,)), - tree = reverse('main:inventory_tree_view', args=(obj.pk,)), - organization = reverse('main:organization_detail', args=(obj.organization.pk,)), - inventory_sources = reverse('main:inventory_inventory_sources_list', args=(obj.pk,)), + hosts = reverse('api:inventory_hosts_list', args=(obj.pk,)), + groups = reverse('api:inventory_groups_list', args=(obj.pk,)), + root_groups = reverse('api:inventory_root_groups_list', args=(obj.pk,)), + variable_data = reverse('api:inventory_variable_data', args=(obj.pk,)), + script = reverse('api:inventory_script_view', args=(obj.pk,)), + tree = reverse('api:inventory_tree_view', args=(obj.pk,)), + organization = reverse('api:organization_detail', args=(obj.organization.pk,)), + inventory_sources = reverse('api:inventory_inventory_sources_list', args=(obj.pk,)), )) return res @@ -491,18 +491,18 @@ class HostSerializer(BaseSerializerWithVariables): return {} res = super(HostSerializer, self).get_related(obj) res.update(dict( - variable_data = reverse('main:host_variable_data', args=(obj.pk,)), - inventory = reverse('main:inventory_detail', args=(obj.inventory.pk,)), - groups = reverse('main:host_groups_list', args=(obj.pk,)), - all_groups = reverse('main:host_all_groups_list', args=(obj.pk,)), - job_events = reverse('main:host_job_events_list', args=(obj.pk,)), - job_host_summaries = reverse('main:host_job_host_summaries_list', args=(obj.pk,)), - #inventory_sources = reverse('main:host_inventory_sources_list', args=(obj.pk,)), + variable_data = reverse('api:host_variable_data', args=(obj.pk,)), + inventory = reverse('api:inventory_detail', args=(obj.inventory.pk,)), + groups = reverse('api:host_groups_list', args=(obj.pk,)), + all_groups = reverse('api:host_all_groups_list', args=(obj.pk,)), + job_events = reverse('api:host_job_events_list', args=(obj.pk,)), + job_host_summaries = reverse('api:host_job_host_summaries_list', args=(obj.pk,)), + #inventory_sources = reverse('api:host_inventory_sources_list', args=(obj.pk,)), )) if obj.last_job: - res['last_job'] = reverse('main:job_detail', args=(obj.last_job.pk,)) + res['last_job'] = reverse('api:job_detail', args=(obj.last_job.pk,)) if obj.last_job_host_summary: - res['last_job_host_summary'] = reverse('main:job_host_summary_detail', args=(obj.last_job_host_summary.pk,)) + res['last_job_host_summary'] = reverse('api:job_host_summary_detail', args=(obj.last_job_host_summary.pk,)) return res def get_summary_fields(self, obj): @@ -589,16 +589,16 @@ class GroupSerializer(BaseSerializerWithVariables): return {} res = super(GroupSerializer, self).get_related(obj) res.update(dict( - variable_data = reverse('main:group_variable_data', args=(obj.pk,)), - hosts = reverse('main:group_hosts_list', args=(obj.pk,)), - potential_children = reverse('main:group_potential_children_list', args=(obj.pk,)), - children = reverse('main:group_children_list', args=(obj.pk,)), - all_hosts = reverse('main:group_all_hosts_list', args=(obj.pk,)), - inventory = reverse('main:inventory_detail', args=(obj.inventory.pk,)), - job_events = reverse('main:group_job_events_list', args=(obj.pk,)), - job_host_summaries = reverse('main:group_job_host_summaries_list', args=(obj.pk,)), - inventory_source = reverse('main:inventory_source_detail', args=(obj.inventory_source.pk,)), - #inventory_sources = reverse('main:group_inventory_sources_list', args=(obj.pk,)), + variable_data = reverse('api:group_variable_data', args=(obj.pk,)), + hosts = reverse('api:group_hosts_list', args=(obj.pk,)), + potential_children = reverse('api:group_potential_children_list', args=(obj.pk,)), + children = reverse('api:group_children_list', args=(obj.pk,)), + all_hosts = reverse('api:group_all_hosts_list', args=(obj.pk,)), + inventory = reverse('api:inventory_detail', args=(obj.inventory.pk,)), + job_events = reverse('api:group_job_events_list', args=(obj.pk,)), + job_host_summaries = reverse('api:group_job_host_summaries_list', args=(obj.pk,)), + inventory_source = reverse('api:inventory_source_detail', args=(obj.inventory_source.pk,)), + #inventory_sources = reverse('api:group_inventory_sources_list', args=(obj.pk,)), )) return res @@ -676,23 +676,23 @@ class InventorySourceSerializer(BaseSerializer): return {} res = super(InventorySourceSerializer, self).get_related(obj) res.update(dict( - update = reverse('main:inventory_source_update_view', args=(obj.pk,)), - inventory_updates = reverse('main:inventory_source_updates_list', args=(obj.pk,)), - #hosts = reverse('main:inventory_source_hosts_list', args=(obj.pk,)), - #groups = reverse('main:inventory_source_groups_list', args=(obj.pk,)), + update = reverse('api:inventory_source_update_view', args=(obj.pk,)), + inventory_updates = reverse('api:inventory_source_updates_list', args=(obj.pk,)), + #hosts = reverse('api:inventory_source_hosts_list', args=(obj.pk,)), + #groups = reverse('api:inventory_source_groups_list', args=(obj.pk,)), )) if obj.inventory: - res['inventory'] = reverse('main:inventory_detail', args=(obj.inventory.pk,)) + res['inventory'] = reverse('api:inventory_detail', args=(obj.inventory.pk,)) if obj.group: - res['group'] = reverse('main:group_detail', args=(obj.group.pk,)) + res['group'] = reverse('api:group_detail', args=(obj.group.pk,)) if obj.credential: - res['credential'] = reverse('main:credential_detail', + res['credential'] = reverse('api:credential_detail', args=(obj.credential.pk,)) if obj.current_update: - res['current_update'] = reverse('main:inventory_update_detail', + res['current_update'] = reverse('api:inventory_update_detail', args=(obj.current_update.pk,)) if obj.last_update: - res['last_update'] = reverse('main:inventory_update_detail', + res['last_update'] = reverse('api:inventory_update_detail', args=(obj.last_update.pk,)) return res @@ -741,8 +741,8 @@ class InventoryUpdateSerializer(BaseSerializer): return {} res = super(InventoryUpdateSerializer, self).get_related(obj) res.update(dict( - inventory_source = reverse('main:inventory_source_detail', args=(obj.inventory_source.pk,)), - cancel = reverse('main:inventory_update_cancel', args=(obj.pk,)), + inventory_source = reverse('api:inventory_source_detail', args=(obj.inventory_source.pk,)), + cancel = reverse('api:inventory_update_cancel', args=(obj.pk,)), )) return res @@ -757,11 +757,11 @@ class TeamSerializer(BaseSerializer): return {} res = super(TeamSerializer, self).get_related(obj) res.update(dict( - projects = reverse('main:team_projects_list', args=(obj.pk,)), - users = reverse('main:team_users_list', args=(obj.pk,)), - credentials = reverse('main:team_credentials_list', args=(obj.pk,)), - organization = reverse('main:organization_detail', args=(obj.organization.pk,)), - permissions = reverse('main:team_permissions_list', args=(obj.pk,)), + projects = reverse('api:team_projects_list', args=(obj.pk,)), + users = reverse('api:team_users_list', args=(obj.pk,)), + credentials = reverse('api:team_credentials_list', args=(obj.pk,)), + organization = reverse('api:organization_detail', args=(obj.organization.pk,)), + permissions = reverse('api:team_permissions_list', args=(obj.pk,)), )) return res @@ -777,13 +777,13 @@ class PermissionSerializer(BaseSerializer): return {} res = super(PermissionSerializer, self).get_related(obj) if obj.user: - res['user'] = reverse('main:user_detail', args=(obj.user.pk,)) + res['user'] = reverse('api:user_detail', args=(obj.user.pk,)) if obj.team: - res['team'] = reverse('main:team_detail', args=(obj.team.pk,)) + res['team'] = reverse('api:team_detail', args=(obj.team.pk,)) if obj.project: - res['project'] = reverse('main:project_detail', args=(obj.project.pk,)) + res['project'] = reverse('api:project_detail', args=(obj.project.pk,)) if obj.inventory: - res['inventory'] = reverse('main:inventory_detail', args=(obj.inventory.pk,)) + res['inventory'] = reverse('api:inventory_detail', args=(obj.inventory.pk,)) return res def validate(self, attrs): @@ -837,9 +837,9 @@ class CredentialSerializer(BaseSerializer): return {} res = super(CredentialSerializer, self).get_related(obj) if obj.user: - res['user'] = reverse('main:user_detail', args=(obj.user.pk,)) + res['user'] = reverse('api:user_detail', args=(obj.user.pk,)) if obj.team: - res['team'] = reverse('main:team_detail', args=(obj.team.pk,)) + res['team'] = reverse('api:team_detail', args=(obj.team.pk,)) return res class JobTemplateSerializer(BaseSerializer): @@ -855,14 +855,14 @@ class JobTemplateSerializer(BaseSerializer): return {} res = super(JobTemplateSerializer, self).get_related(obj) res.update(dict( - inventory = reverse('main:inventory_detail', args=(obj.inventory.pk,)), - project = reverse('main:project_detail', args=(obj.project.pk,)), - jobs = reverse('main:job_template_jobs_list', args=(obj.pk,)), + inventory = reverse('api:inventory_detail', args=(obj.inventory.pk,)), + project = reverse('api:project_detail', args=(obj.project.pk,)), + jobs = reverse('api:job_template_jobs_list', args=(obj.pk,)), )) if obj.credential: - res['credential'] = reverse('main:credential_detail', args=(obj.credential.pk,)) + res['credential'] = reverse('api:credential_detail', args=(obj.credential.pk,)) if obj.host_config_key: - res['callback'] = reverse('main:job_template_callback', args=(obj.pk,)) + res['callback'] = reverse('api:job_template_callback', args=(obj.pk,)) return res def validate_playbook(self, attrs, source): @@ -892,18 +892,18 @@ class JobSerializer(BaseSerializer): return {} res = super(JobSerializer, self).get_related(obj) res.update(dict( - inventory = reverse('main:inventory_detail', args=(obj.inventory.pk,)), - project = reverse('main:project_detail', args=(obj.project.pk,)), - credential = reverse('main:credential_detail', args=(obj.credential.pk,)), - job_events = reverse('main:job_job_events_list', args=(obj.pk,)), - job_host_summaries = reverse('main:job_job_host_summaries_list', args=(obj.pk,)), + inventory = reverse('api:inventory_detail', args=(obj.inventory.pk,)), + project = reverse('api:project_detail', args=(obj.project.pk,)), + credential = reverse('api:credential_detail', args=(obj.credential.pk,)), + job_events = reverse('api:job_job_events_list', args=(obj.pk,)), + job_host_summaries = reverse('api:job_job_host_summaries_list', args=(obj.pk,)), )) if obj.job_template: - res['job_template'] = reverse('main:job_template_detail', args=(obj.job_template.pk,)) + res['job_template'] = reverse('api:job_template_detail', args=(obj.job_template.pk,)) if obj.can_start or True: - res['start'] = reverse('main:job_start', args=(obj.pk,)) + res['start'] = reverse('api:job_start', args=(obj.pk,)) if obj.can_cancel or True: - res['cancel'] = reverse('main:job_cancel', args=(obj.pk,)) + res['cancel'] = reverse('api:job_cancel', args=(obj.pk,)) return res def from_native(self, data, files): @@ -942,8 +942,8 @@ class JobHostSummarySerializer(BaseSerializer): return {} res = super(JobHostSummarySerializer, self).get_related(obj) res.update(dict( - job=reverse('main:job_detail', args=(obj.job.pk,)), - host=reverse('main:host_detail', args=(obj.host.pk,)) + job=reverse('api:job_detail', args=(obj.job.pk,)), + host=reverse('api:host_detail', args=(obj.host.pk,)) )) return res @@ -975,17 +975,17 @@ class JobEventSerializer(BaseSerializer): return {} res = super(JobEventSerializer, self).get_related(obj) res.update(dict( - job = reverse('main:job_detail', args=(obj.job.pk,)), - #children = reverse('main:job_event_children_list', args=(obj.pk,)), + job = reverse('api:job_detail', args=(obj.job.pk,)), + #children = reverse('api:job_event_children_list', args=(obj.pk,)), )) if obj.parent: - res['parent'] = reverse('main:job_event_detail', args=(obj.parent.pk,)) + res['parent'] = reverse('api:job_event_detail', args=(obj.parent.pk,)) if obj.children.count(): - res['children'] = reverse('main:job_event_children_list', args=(obj.pk,)) + res['children'] = reverse('api:job_event_children_list', args=(obj.pk,)) if obj.host: - res['host'] = reverse('main:host_detail', args=(obj.host.pk,)) + res['host'] = reverse('api:host_detail', args=(obj.host.pk,)) if obj.hosts.count(): - res['hosts'] = reverse('main:job_event_hosts_list', args=(obj.pk,)) + res['hosts'] = reverse('api:job_event_hosts_list', args=(obj.pk,)) return res def get_summary_fields(self, obj): diff --git a/awx/main/templates/main/_list_common.md b/awx/api/templates/api/_list_common.md similarity index 98% rename from awx/main/templates/main/_list_common.md rename to awx/api/templates/api/_list_common.md index 7cacf39366..1118a31f74 100644 --- a/awx/main/templates/main/_list_common.md +++ b/awx/api/templates/api/_list_common.md @@ -18,7 +18,7 @@ additional results if there are more than will fit on a single page. The Each {{ model_verbose_name }} data structure includes the following fields: -{% include "main/_result_fields_common.md" %} +{% include "api/_result_fields_common.md" %} ## Sorting diff --git a/awx/main/templates/main/_new_in_awx.md b/awx/api/templates/api/_new_in_awx.md similarity index 100% rename from awx/main/templates/main/_new_in_awx.md rename to awx/api/templates/api/_new_in_awx.md diff --git a/awx/main/templates/main/_result_fields_common.md b/awx/api/templates/api/_result_fields_common.md similarity index 100% rename from awx/main/templates/main/_result_fields_common.md rename to awx/api/templates/api/_result_fields_common.md diff --git a/awx/main/templates/main/api_root_view.md b/awx/api/templates/api/api_root_view.md similarity index 100% rename from awx/main/templates/main/api_root_view.md rename to awx/api/templates/api/api_root_view.md diff --git a/awx/main/templates/main/api_v1_config_view.md b/awx/api/templates/api/api_v1_config_view.md similarity index 100% rename from awx/main/templates/main/api_v1_config_view.md rename to awx/api/templates/api/api_v1_config_view.md diff --git a/awx/main/templates/main/api_v1_root_view.md b/awx/api/templates/api/api_v1_root_view.md similarity index 100% rename from awx/main/templates/main/api_v1_root_view.md rename to awx/api/templates/api/api_v1_root_view.md diff --git a/awx/api/templates/api/api_view.md b/awx/api/templates/api/api_view.md new file mode 100644 index 0000000000..d716841f26 --- /dev/null +++ b/awx/api/templates/api/api_view.md @@ -0,0 +1,3 @@ +{{ docstring }} + +{% include "api/_new_in_awx.md" %} diff --git a/awx/main/templates/main/auth_token_view.md b/awx/api/templates/api/auth_token_view.md similarity index 100% rename from awx/main/templates/main/auth_token_view.md rename to awx/api/templates/api/auth_token_view.md diff --git a/awx/main/templates/main/base_variable_data.md b/awx/api/templates/api/base_variable_data.md similarity index 100% rename from awx/main/templates/main/base_variable_data.md rename to awx/api/templates/api/base_variable_data.md diff --git a/awx/main/templates/main/group_all_hosts_list.md b/awx/api/templates/api/group_all_hosts_list.md similarity index 87% rename from awx/main/templates/main/group_all_hosts_list.md rename to awx/api/templates/api/group_all_hosts_list.md index 8ce2b209ea..4c021634c7 100644 --- a/awx/main/templates/main/group_all_hosts_list.md +++ b/awx/api/templates/api/group_all_hosts_list.md @@ -4,4 +4,4 @@ Make a GET request to this resource to retrieve a list of all {{ model_verbose_name_plural }} directly or indirectly belonging to this {{ parent_model_verbose_name }}. -{% include "main/_list_common.md" %} +{% include "api/_list_common.md" %} diff --git a/awx/main/templates/main/group_potential_children_list.md b/awx/api/templates/api/group_potential_children_list.md similarity index 77% rename from awx/main/templates/main/group_potential_children_list.md rename to awx/api/templates/api/group_potential_children_list.md index 1ec20f401f..a22c10f3d9 100644 --- a/awx/main/templates/main/group_potential_children_list.md +++ b/awx/api/templates/api/group_potential_children_list.md @@ -4,6 +4,6 @@ Make a GET request to this resource to retrieve a list of {{ model_verbose_name_plural }} available to be added as children of the current {{ parent_model_verbose_name }}. -{% include "main/_list_common.md" %} +{% include "api/_list_common.md" %} -{% include "main/_new_in_awx.md" %} +{% include "api/_new_in_awx.md" %} diff --git a/awx/main/templates/main/host_all_groups_list.md b/awx/api/templates/api/host_all_groups_list.md similarity index 88% rename from awx/main/templates/main/host_all_groups_list.md rename to awx/api/templates/api/host_all_groups_list.md index 704f8aefd5..c4275e0158 100644 --- a/awx/main/templates/main/host_all_groups_list.md +++ b/awx/api/templates/api/host_all_groups_list.md @@ -4,4 +4,4 @@ Make a GET request to this resource to retrieve a list of all {{ model_verbose_name_plural }} of which the selected {{ parent_model_verbose_name }} is directly or indirectly a member. -{% include "main/_list_common.md" %} +{% include "api/_list_common.md" %} diff --git a/awx/main/templates/main/inventory_root_groups_list.md b/awx/api/templates/api/inventory_root_groups_list.md similarity index 87% rename from awx/main/templates/main/inventory_root_groups_list.md rename to awx/api/templates/api/inventory_root_groups_list.md index 025a5e3896..17c95e03ba 100644 --- a/awx/main/templates/main/inventory_root_groups_list.md +++ b/awx/api/templates/api/inventory_root_groups_list.md @@ -4,4 +4,4 @@ Make a GET request to this resource to retrieve a list of root (top-level) {{ model_verbose_name_plural }} associated with this {{ parent_model_verbose_name }}. -{% include "main/_list_common.md" %} +{% include "api/_list_common.md" %} diff --git a/awx/main/templates/main/inventory_script_view.md b/awx/api/templates/api/inventory_script_view.md similarity index 100% rename from awx/main/templates/main/inventory_script_view.md rename to awx/api/templates/api/inventory_script_view.md diff --git a/awx/main/templates/main/inventory_source_cancel.md b/awx/api/templates/api/inventory_source_cancel.md similarity index 92% rename from awx/main/templates/main/inventory_source_cancel.md rename to awx/api/templates/api/inventory_source_cancel.md index b5a212bbf6..945ca93011 100644 --- a/awx/main/templates/main/inventory_source_cancel.md +++ b/awx/api/templates/api/inventory_source_cancel.md @@ -10,4 +10,4 @@ Make a POST request to this resource to cancel a pending or running inventory update. The response status code will be 202 if successful, or 405 if the update cannot be canceled. -{% include "main/_new_in_awx.md" %} +{% include "api/_new_in_awx.md" %} diff --git a/awx/main/templates/main/inventory_source_update_view.md b/awx/api/templates/api/inventory_source_update_view.md similarity index 95% rename from awx/main/templates/main/inventory_source_update_view.md rename to awx/api/templates/api/inventory_source_update_view.md index b0eb5227f1..e88eede86c 100644 --- a/awx/main/templates/main/inventory_source_update_view.md +++ b/awx/api/templates/api/inventory_source_update_view.md @@ -15,4 +15,4 @@ If successful, the response status code will be 202. If any required passwords are not provided, a 400 status code will be returned. If the inventory source is not defined or cannot be updated, a 405 status code will be returned. -{% include "main/_new_in_awx.md" %} +{% include "api/_new_in_awx.md" %} diff --git a/awx/main/templates/main/inventory_tree_view.md b/awx/api/templates/api/inventory_tree_view.md similarity index 82% rename from awx/main/templates/main/inventory_tree_view.md rename to awx/api/templates/api/inventory_tree_view.md index d76dc468b0..9818b56880 100644 --- a/awx/main/templates/main/inventory_tree_view.md +++ b/awx/api/templates/api/inventory_tree_view.md @@ -10,6 +10,6 @@ also containing a list of its children. Each group data structure includes the following fields: -{% include "main/_result_fields_common.md" %} +{% include "api/_result_fields_common.md" %} -{% include "main/_new_in_awx.md" %} +{% include "api/_new_in_awx.md" %} diff --git a/awx/main/templates/main/job_cancel.md b/awx/api/templates/api/job_cancel.md similarity index 100% rename from awx/main/templates/main/job_cancel.md rename to awx/api/templates/api/job_cancel.md diff --git a/awx/main/templates/main/job_list.md b/awx/api/templates/api/job_list.md similarity index 79% rename from awx/main/templates/main/job_list.md rename to awx/api/templates/api/job_list.md index 77aecca1fb..c7cbad3c0a 100644 --- a/awx/main/templates/main/job_list.md +++ b/awx/api/templates/api/job_list.md @@ -1,4 +1,4 @@ -{% include "main/list_create_api_view.md" %} +{% include "api/list_create_api_view.md" %} If the `job_template` field is specified, any fields not explicitly provided for the new job (except `name` and `description`) will use the default values diff --git a/awx/main/templates/main/job_start.md b/awx/api/templates/api/job_start.md similarity index 100% rename from awx/main/templates/main/job_start.md rename to awx/api/templates/api/job_start.md diff --git a/awx/main/templates/main/job_template_callback.md b/awx/api/templates/api/job_template_callback.md similarity index 100% rename from awx/main/templates/main/job_template_callback.md rename to awx/api/templates/api/job_template_callback.md diff --git a/awx/main/templates/main/job_template_jobs_list.md b/awx/api/templates/api/job_template_jobs_list.md similarity index 78% rename from awx/main/templates/main/job_template_jobs_list.md rename to awx/api/templates/api/job_template_jobs_list.md index 64d8f7f488..3757db287a 100644 --- a/awx/main/templates/main/job_template_jobs_list.md +++ b/awx/api/templates/api/job_template_jobs_list.md @@ -1,4 +1,4 @@ -{% extends "main/sub_list_create_api_view.md" %} +{% extends "api/sub_list_create_api_view.md" %} {% block post_create %} Any fields not explicitly provided for the new job (except `name` and diff --git a/awx/main/templates/main/list_api_view.md b/awx/api/templates/api/list_api_view.md similarity index 66% rename from awx/main/templates/main/list_api_view.md rename to awx/api/templates/api/list_api_view.md index 080d1d2f65..c45b46c40f 100644 --- a/awx/main/templates/main/list_api_view.md +++ b/awx/api/templates/api/list_api_view.md @@ -3,6 +3,6 @@ Make a GET request to this resource to retrieve the list of {{ model_verbose_name_plural }}. -{% include "main/_list_common.md" %} +{% include "api/_list_common.md" %} -{% include "main/_new_in_awx.md" %} +{% include "api/_new_in_awx.md" %} diff --git a/awx/main/templates/main/list_create_api_view.md b/awx/api/templates/api/list_create_api_view.md similarity index 64% rename from awx/main/templates/main/list_create_api_view.md rename to awx/api/templates/api/list_create_api_view.md index 07344e5b2d..5839f0fbf6 100644 --- a/awx/main/templates/main/list_create_api_view.md +++ b/awx/api/templates/api/list_create_api_view.md @@ -1,4 +1,4 @@ -{% include "main/list_api_view.md" %} +{% include "api/list_api_view.md" %} # Create {{ model_verbose_name_plural|title }}: @@ -6,7 +6,7 @@ Make a POST request to this resource with the following {{ model_verbose_name }} fields to create a new {{ model_verbose_name }}: {% with write_only=1 %} -{% include "main/_result_fields_common.md" %} +{% include "api/_result_fields_common.md" %} {% endwith %} -{% include "main/_new_in_awx.md" %} +{% include "api/_new_in_awx.md" %} diff --git a/awx/main/templates/main/organization_admins_list.md b/awx/api/templates/api/organization_admins_list.md similarity index 66% rename from awx/main/templates/main/organization_admins_list.md rename to awx/api/templates/api/organization_admins_list.md index 7b055379c6..e0d5056f0e 100644 --- a/awx/main/templates/main/organization_admins_list.md +++ b/awx/api/templates/api/organization_admins_list.md @@ -1,3 +1,3 @@ {% with model_verbose_name="admin user" model_verbose_name_plural="admin users" %} -{% include "main/sub_list_create_api_view.md" %} +{% include "api/sub_list_create_api_view.md" %} {% endwith %} \ No newline at end of file diff --git a/awx/main/templates/main/project_playbooks.md b/awx/api/templates/api/project_playbooks.md similarity index 100% rename from awx/main/templates/main/project_playbooks.md rename to awx/api/templates/api/project_playbooks.md diff --git a/awx/main/templates/main/project_update_cancel.md b/awx/api/templates/api/project_update_cancel.md similarity index 92% rename from awx/main/templates/main/project_update_cancel.md rename to awx/api/templates/api/project_update_cancel.md index 3841996e0a..287c15d169 100644 --- a/awx/main/templates/main/project_update_cancel.md +++ b/awx/api/templates/api/project_update_cancel.md @@ -10,4 +10,4 @@ Make a POST request to this resource to cancel a pending or running project update. The response status code will be 202 if successful, or 405 if the update cannot be canceled. -{% include "main/_new_in_awx.md" %} +{% include "api/_new_in_awx.md" %} diff --git a/awx/main/templates/main/project_update_view.md b/awx/api/templates/api/project_update_view.md similarity index 95% rename from awx/main/templates/main/project_update_view.md rename to awx/api/templates/api/project_update_view.md index 8d051dd2d4..b378c13861 100644 --- a/awx/main/templates/main/project_update_view.md +++ b/awx/api/templates/api/project_update_view.md @@ -15,4 +15,4 @@ If successful, the response status code will be 202. If any required passwords are not provided, a 400 status code will be returned. If the project cannot be updated, a 405 status code will be returned. -{% include "main/_new_in_awx.md" %} +{% include "api/_new_in_awx.md" %} diff --git a/awx/main/templates/main/retrieve_api_view.md b/awx/api/templates/api/retrieve_api_view.md similarity index 67% rename from awx/main/templates/main/retrieve_api_view.md rename to awx/api/templates/api/retrieve_api_view.md index 73e4e0885f..a623f421e9 100644 --- a/awx/main/templates/main/retrieve_api_view.md +++ b/awx/api/templates/api/retrieve_api_view.md @@ -3,7 +3,7 @@ Make GET request to this resource to retrieve a single {{ model_verbose_name }} record containing the following fields: -{% include "main/_result_fields_common.md" %} +{% include "api/_result_fields_common.md" %} -{% include "main/_new_in_awx.md" %} +{% include "api/_new_in_awx.md" %} diff --git a/awx/main/templates/main/retrieve_update_api_view.md b/awx/api/templates/api/retrieve_update_api_view.md similarity index 73% rename from awx/main/templates/main/retrieve_update_api_view.md rename to awx/api/templates/api/retrieve_update_api_view.md index 27e2ca5990..2a0b5f6248 100644 --- a/awx/main/templates/main/retrieve_update_api_view.md +++ b/awx/api/templates/api/retrieve_update_api_view.md @@ -1,4 +1,4 @@ -{% include "main/retrieve_api_view.md" %} +{% include "api/retrieve_api_view.md" %} # Update {{ model_verbose_name|title }}: @@ -6,11 +6,11 @@ Make a PUT or PATCH request to this resource to update this {{ model_verbose_name }}. The following fields may be modified: {% with write_only=1 %} -{% include "main/_result_fields_common.md" %} +{% include "api/_result_fields_common.md" %} {% endwith %} For a PUT request, include **all** fields in the request. For a PATCH request, include only the fields that are being modified. -{% include "main/_new_in_awx.md" %} +{% include "api/_new_in_awx.md" %} diff --git a/awx/main/templates/main/retrieve_update_destroy_api_view.md b/awx/api/templates/api/retrieve_update_destroy_api_view.md similarity index 78% rename from awx/main/templates/main/retrieve_update_destroy_api_view.md rename to awx/api/templates/api/retrieve_update_destroy_api_view.md index b3ff94be77..b90fc4bbfd 100644 --- a/awx/main/templates/main/retrieve_update_destroy_api_view.md +++ b/awx/api/templates/api/retrieve_update_destroy_api_view.md @@ -1,4 +1,4 @@ -{% include "main/retrieve_api_view.md" %} +{% include "api/retrieve_api_view.md" %} # Update {{ model_verbose_name|title }}: @@ -6,7 +6,7 @@ Make a PUT or PATCH request to this resource to update this {{ model_verbose_name }}. The following fields may be modified: {% with write_only=1 %} -{% include "main/_result_fields_common.md" %} +{% include "api/_result_fields_common.md" %} {% endwith %} For a PUT request, include **all** fields in the request. @@ -17,4 +17,4 @@ For a PATCH request, include only the fields that are being modified. Make a DELETE request to this resource to delete this {{ model_verbose_name }}. -{% include "main/_new_in_awx.md" %} +{% include "api/_new_in_awx.md" %} diff --git a/awx/main/templates/main/sub_list_api_view.md b/awx/api/templates/api/sub_list_api_view.md similarity index 77% rename from awx/main/templates/main/sub_list_api_view.md rename to awx/api/templates/api/sub_list_api_view.md index 2bfaa88789..9993819bc3 100644 --- a/awx/main/templates/main/sub_list_api_view.md +++ b/awx/api/templates/api/sub_list_api_view.md @@ -4,6 +4,6 @@ Make a GET request to this resource to retrieve a list of {{ model_verbose_name_plural }} associated with the selected {{ parent_model_verbose_name }}. -{% include "main/_list_common.md" %} +{% include "api/_list_common.md" %} -{% include "main/_new_in_awx.md" %} +{% include "api/_new_in_awx.md" %} diff --git a/awx/main/templates/main/sub_list_create_api_view.md b/awx/api/templates/api/sub_list_create_api_view.md similarity index 90% rename from awx/main/templates/main/sub_list_create_api_view.md rename to awx/api/templates/api/sub_list_create_api_view.md index 5dd089e6cb..c313614f4e 100644 --- a/awx/main/templates/main/sub_list_create_api_view.md +++ b/awx/api/templates/api/sub_list_create_api_view.md @@ -1,4 +1,4 @@ -{% include "main/sub_list_api_view.md" %} +{% include "api/sub_list_api_view.md" %} # Create {{ model_verbose_name_plural|title }} for this {{ parent_model_verbose_name|title }}: @@ -7,7 +7,7 @@ fields to create a new {{ model_verbose_name }} associated with this {{ parent_model_verbose_name }}. {% with write_only=1 %} -{% include "main/_result_fields_common.md" %} +{% include "api/_result_fields_common.md" %} {% endwith %} {% block post_create %}{% endblock %} @@ -36,4 +36,4 @@ remove the {{ model_verbose_name }} from this {{ parent_model_verbose_name }} without deleting the {{ model_verbose_name }}. {% endif %} -{% include "main/_new_in_awx.md" %} +{% include "api/_new_in_awx.md" %} diff --git a/awx/main/templates/main/user_admin_of_organizations_list.md b/awx/api/templates/api/user_admin_of_organizations_list.md similarity index 87% rename from awx/main/templates/main/user_admin_of_organizations_list.md rename to awx/api/templates/api/user_admin_of_organizations_list.md index 9596982793..aa38eecb88 100644 --- a/awx/main/templates/main/user_admin_of_organizations_list.md +++ b/awx/api/templates/api/user_admin_of_organizations_list.md @@ -4,4 +4,4 @@ Make a GET request to this resource to retrieve a list of {{ model_verbose_name_plural }} of which the selected {{ parent_model_verbose_name }} is an admin. -{% include "main/_list_common.md" %} +{% include "api/_list_common.md" %} diff --git a/awx/main/templates/main/user_me_list.md b/awx/api/templates/api/user_me_list.md similarity index 82% rename from awx/main/templates/main/user_me_list.md rename to awx/api/templates/api/user_me_list.md index 577b890a72..50bea61bd8 100644 --- a/awx/main/templates/main/user_me_list.md +++ b/awx/api/templates/api/user_me_list.md @@ -2,6 +2,6 @@ Make a GET request to retrieve user information about the current user. One result should be returned containing the following fields: -{% include "main/_result_fields_common.md" %} +{% include "api/_result_fields_common.md" %} Use the primary URL for the user (/api/v1/users/N/) to modify the user. diff --git a/awx/api/urls.py b/awx/api/urls.py new file mode 100644 index 0000000000..e868610231 --- /dev/null +++ b/awx/api/urls.py @@ -0,0 +1,170 @@ +# Copyright (c) 2013 AnsibleWorks, Inc. +# All Rights Reserved. + +from django.conf.urls import include, patterns, url as original_url + +def url(regex, view, kwargs=None, name=None, prefix=''): + # Set default name from view name (if a string). + if isinstance(view, basestring) and name is None: + name = view + return original_url(regex, view, kwargs, name, prefix) + +organization_urls = patterns('awx.api.views', + url(r'^$', 'organization_list'), + url(r'^(?P[0-9]+)/$', 'organization_detail'), + url(r'^(?P[0-9]+)/users/$', 'organization_users_list'), + url(r'^(?P[0-9]+)/admins/$', 'organization_admins_list'), + url(r'^(?P[0-9]+)/inventories/$', 'organization_inventories_list'), + url(r'^(?P[0-9]+)/projects/$', 'organization_projects_list'), + url(r'^(?P[0-9]+)/teams/$', 'organization_teams_list'), +) + +user_urls = patterns('awx.api.views', + url(r'^$', 'user_list'), + url(r'^(?P[0-9]+)/$', 'user_detail'), + url(r'^(?P[0-9]+)/teams/$', 'user_teams_list'), + url(r'^(?P[0-9]+)/organizations/$', 'user_organizations_list'), + url(r'^(?P[0-9]+)/admin_of_organizations/$', 'user_admin_of_organizations_list'), + url(r'^(?P[0-9]+)/projects/$', 'user_projects_list'), + url(r'^(?P[0-9]+)/credentials/$', 'user_credentials_list'), + url(r'^(?P[0-9]+)/permissions/$', 'user_permissions_list'), +) + +project_urls = patterns('awx.api.views', + url(r'^$', 'project_list'), + url(r'^(?P[0-9]+)/$', 'project_detail'), + url(r'^(?P[0-9]+)/playbooks/$', 'project_playbooks'), + url(r'^(?P[0-9]+)/organizations/$', 'project_organizations_list'), + url(r'^(?P[0-9]+)/teams/$', 'project_teams_list'), + url(r'^(?P[0-9]+)/update/$', 'project_update_view'), + url(r'^(?P[0-9]+)/project_updates/$', 'project_updates_list'), +) + +project_update_urls = patterns('awx.api.views', + url(r'^(?P[0-9]+)/$', 'project_update_detail'), + url(r'^(?P[0-9]+)/cancel/$', 'project_update_cancel'), +) + +team_urls = patterns('awx.api.views', + url(r'^$', 'team_list'), + url(r'^(?P[0-9]+)/$', 'team_detail'), + url(r'^(?P[0-9]+)/projects/$', 'team_projects_list'), + url(r'^(?P[0-9]+)/users/$', 'team_users_list'), + url(r'^(?P[0-9]+)/credentials/$', 'team_credentials_list'), + url(r'^(?P[0-9]+)/permissions/$', 'team_permissions_list'), +) + +inventory_urls = patterns('awx.api.views', + url(r'^$', 'inventory_list'), + url(r'^(?P[0-9]+)/$', 'inventory_detail'), + url(r'^(?P[0-9]+)/hosts/$', 'inventory_hosts_list'), + url(r'^(?P[0-9]+)/groups/$', 'inventory_groups_list'), + url(r'^(?P[0-9]+)/root_groups/$', 'inventory_root_groups_list'), + url(r'^(?P[0-9]+)/variable_data/$', 'inventory_variable_data'), + url(r'^(?P[0-9]+)/script/$', 'inventory_script_view'), + url(r'^(?P[0-9]+)/tree/$', 'inventory_tree_view'), + url(r'^(?P[0-9]+)/inventory_sources/$', 'inventory_inventory_sources_list'), +) + +host_urls = patterns('awx.api.views', + url(r'^$', 'host_list'), + url(r'^(?P[0-9]+)/$', 'host_detail'), + url(r'^(?P[0-9]+)/variable_data/$', 'host_variable_data'), + url(r'^(?P[0-9]+)/groups/$', 'host_groups_list'), + url(r'^(?P[0-9]+)/all_groups/$', 'host_all_groups_list'), + url(r'^(?P[0-9]+)/job_events/', 'host_job_events_list'), + url(r'^(?P[0-9]+)/job_host_summaries/$', 'host_job_host_summaries_list'), + #url(r'^(?P[0-9]+)/inventory_sources/$', 'host_inventory_sources_list'), +) + +group_urls = patterns('awx.api.views', + url(r'^$', 'group_list'), + url(r'^(?P[0-9]+)/$', 'group_detail'), + url(r'^(?P[0-9]+)/children/$', 'group_children_list'), + url(r'^(?P[0-9]+)/hosts/$', 'group_hosts_list'), + url(r'^(?P[0-9]+)/all_hosts/$', 'group_all_hosts_list'), + url(r'^(?P[0-9]+)/variable_data/$', 'group_variable_data'), + url(r'^(?P[0-9]+)/job_events/$', 'group_job_events_list'), + url(r'^(?P[0-9]+)/job_host_summaries/$', 'group_job_host_summaries_list'), + url(r'^(?P[0-9]+)/potential_children/$', 'group_potential_children_list'), + #url(r'^(?P[0-9]+)/inventory_sources/$', 'group_inventory_sources_list'), +) + +inventory_source_urls = patterns('awx.api.views', + url(r'^$', 'inventory_source_list'), + url(r'^(?P[0-9]+)/$', 'inventory_source_detail'), + url(r'^(?P[0-9]+)/update/$', 'inventory_source_update_view'), + url(r'^(?P[0-9]+)/inventory_updates/$', 'inventory_source_updates_list'), + #url(r'^(?P[0-9]+)/groups/$', 'inventory_source_groups_list'), + #url(r'^(?P[0-9]+)/hosts/$', 'inventory_source_hosts_list'), +) + +inventory_update_urls = patterns('awx.api.views', + url(r'^(?P[0-9]+)/$', 'inventory_update_detail'), + url(r'^(?P[0-9]+)/cancel/$', 'inventory_update_cancel'), +) + +credential_urls = patterns('awx.api.views', + url(r'^$', 'credential_list'), + url(r'^(?P[0-9]+)/$', 'credential_detail'), + # See also credentials resources on users/teams. +) + +permission_urls = patterns('awx.api.views', + url(r'^(?P[0-9]+)/$', 'permission_detail'), +) + +job_template_urls = patterns('awx.api.views', + url(r'^$', 'job_template_list'), + url(r'^(?P[0-9]+)/$', 'job_template_detail'), + url(r'^(?P[0-9]+)/jobs/$', 'job_template_jobs_list'), + url(r'^(?P[0-9]+)/callback/$', 'job_template_callback'), +) + +job_urls = patterns('awx.api.views', + url(r'^$', 'job_list'), + url(r'^(?P[0-9]+)/$', 'job_detail'), + url(r'^(?P[0-9]+)/start/$', 'job_start'), + url(r'^(?P[0-9]+)/cancel/$', 'job_cancel'), + url(r'^(?P[0-9]+)/job_host_summaries/$', 'job_job_host_summaries_list'), + url(r'^(?P[0-9]+)/job_events/$', 'job_job_events_list'), +) + +job_host_summary_urls = patterns('awx.api.views', + url(r'^(?P[0-9]+)/$', 'job_host_summary_detail'), +) + +job_event_urls = patterns('awx.api.views', + url(r'^$', 'job_event_list'), + url(r'^(?P[0-9]+)/$', 'job_event_detail'), + url(r'^(?P[0-9]+)/children/$', 'job_event_children_list'), + url(r'^(?P[0-9]+)/hosts/$', 'job_event_hosts_list'), +) + +v1_urls = patterns('awx.api.views', + url(r'^$', 'api_v1_root_view'), + url(r'^config/$', 'api_v1_config_view'), + url(r'^authtoken/$', 'auth_token_view'), + url(r'^me/$', 'user_me_list'), + url(r'^organizations/', include(organization_urls)), + url(r'^users/', include(user_urls)), + url(r'^projects/', include(project_urls)), + url(r'^project_updates/', include(project_update_urls)), + url(r'^teams/', include(team_urls)), + url(r'^inventories/', include(inventory_urls)), + url(r'^hosts/', include(host_urls)), + url(r'^groups/', include(group_urls)), + url(r'^inventory_sources/', include(inventory_source_urls)), + url(r'^inventory_updates/', include(inventory_update_urls)), + url(r'^credentials/', include(credential_urls)), + url(r'^permissions/', include(permission_urls)), + url(r'^job_templates/', include(job_template_urls)), + url(r'^jobs/', include(job_urls)), + url(r'^job_host_summaries/', include(job_host_summary_urls)), + url(r'^job_events/', include(job_event_urls)), +) + +urlpatterns = patterns('awx.api.views', + url(r'^$', 'api_root_view'), + url(r'^v1/', include(v1_urls)), +) diff --git a/awx/api/views.py b/awx/api/views.py new file mode 100644 index 0000000000..272983cb2b --- /dev/null +++ b/awx/api/views.py @@ -0,0 +1,1059 @@ +# Copyright (c) 2013 AnsibleWorks, Inc. +# All Rights Reserved. + +# Python +import datetime +import re +import socket +import sys + +# Django +from django.conf import settings +from django.contrib.auth.models import User +from django.core.urlresolvers import reverse +from django.db.models import Q +from django.shortcuts import get_object_or_404 +from django.utils.datastructures import SortedDict +from django.utils.timezone import now + +# Django REST Framework +from rest_framework.authtoken.views import ObtainAuthToken +from rest_framework.exceptions import PermissionDenied +from rest_framework.parsers import YAMLParser +from rest_framework.permissions import AllowAny, IsAuthenticated +from rest_framework.renderers import YAMLRenderer +from rest_framework.response import Response +from rest_framework.settings import api_settings +from rest_framework import status + +# AWX +from awx.main.licenses import LicenseReader +from awx.main.models import * +from awx.main.utils import * +from awx.api.authentication import JobTaskAuthentication +from awx.api.permissions import * +from awx.api.serializers import * +from awx.api.generics import * + + +class ApiRootView(APIView): + + permission_classes = (AllowAny,) + view_name = 'REST API' + + def get(self, request, format=None): + ''' list supported API versions ''' + + current = reverse('api:api_v1_root_view', args=[]) + data = dict( + description = 'AWX REST API', + current_version = current, + available_versions = dict( + v1 = current + ) + ) + return Response(data) + +class ApiV1RootView(APIView): + + permission_classes = (AllowAny,) + view_name = 'Version 1' + + def get(self, request, format=None): + ''' list top level resources ''' + + data = SortedDict() + data['authtoken'] = reverse('api:auth_token_view') + data['config'] = reverse('api:api_v1_config_view') + data['me'] = reverse('api:user_me_list') + data['organizations'] = reverse('api:organization_list') + data['users'] = reverse('api:user_list') + data['projects'] = reverse('api:project_list') + data['teams'] = reverse('api:team_list') + data['credentials'] = reverse('api:credential_list') + data['inventory'] = reverse('api:inventory_list') + data['inventory_sources'] = reverse('api:inventory_source_list') + data['groups'] = reverse('api:group_list') + data['hosts'] = reverse('api:host_list') + data['job_templates'] = reverse('api:job_template_list') + data['jobs'] = reverse('api:job_list') + return Response(data) + +class ApiV1ConfigView(APIView): + + permission_classes = (IsAuthenticated,) + view_name = 'Configuration' + + def get(self, request, format=None): + '''Return various sitewide configuration settings.''' + + license_reader = LicenseReader() + license_data = license_reader.from_file() + + data = dict( + time_zone=settings.TIME_ZONE, + license_info=license_data, + version=get_awx_version(), + ansible_version=get_ansible_version(), + ) + + # If LDAP is enabled, user_ldap_fields will return a list of field + # names that are managed by LDAP and should be read-only for users with + # a non-empty ldap_dn attribute. + if getattr(settings, 'AUTH_LDAP_SERVER_URI', None): + user_ldap_fields = ['username', 'password'] + user_ldap_fields.extend(getattr(settings, 'AUTH_LDAP_USER_ATTR_MAP', {}).keys()) + user_ldap_fields.extend(getattr(settings, 'AUTH_LDAP_USER_FLAGS_BY_GROUP', {}).keys()) + data['user_ldap_fields'] = user_ldap_fields + + if request.user.is_superuser or request.user.admin_of_organizations.filter(active=True).count(): + data.update(dict( + project_base_dir = settings.PROJECTS_ROOT, + project_local_paths = Project.get_local_path_choices(), + )) + + return Response(data) + +class AuthTokenView(APIView): + + permission_classes = (AllowAny,) + serializer_class = AuthTokenSerializer + model = AuthToken + + def post(self, request): + serializer = self.serializer_class(data=request.DATA) + if serializer.is_valid(): + request_hash = AuthToken.get_request_hash(self.request) + try: + token = AuthToken.objects.filter(user=serializer.object['user'], + request_hash=request_hash, + expires__gt=now())[0] + token.refresh() + except IndexError: + token = AuthToken.objects.create(user=serializer.object['user'], + request_hash=request_hash) + return Response({'token': token.key, 'expires': token.expires}) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + +class OrganizationList(ListCreateAPIView): + + model = Organization + serializer_class = OrganizationSerializer + +class OrganizationDetail(RetrieveUpdateDestroyAPIView): + + model = Organization + serializer_class = OrganizationSerializer + +class OrganizationInventoriesList(SubListAPIView): + + model = Inventory + serializer_class = InventorySerializer + parent_model = Organization + relationship = 'inventories' + +class OrganizationUsersList(SubListCreateAPIView): + + model = User + serializer_class = UserSerializer + parent_model = Organization + relationship = 'users' + +class OrganizationAdminsList(SubListCreateAPIView): + + model = User + serializer_class = UserSerializer + parent_model = Organization + relationship = 'admins' + +class OrganizationProjectsList(SubListCreateAPIView): + + model = Project + serializer_class = ProjectSerializer + parent_model = Organization + relationship = 'projects' + +class OrganizationTeamsList(SubListCreateAPIView): + + model = Team + serializer_class = TeamSerializer + parent_model = Organization + relationship = 'teams' + parent_key = 'organization' + +class TeamList(ListCreateAPIView): + + model = Team + serializer_class = TeamSerializer + +class TeamDetail(RetrieveUpdateDestroyAPIView): + + model = Team + serializer_class = TeamSerializer + +class TeamUsersList(SubListCreateAPIView): + + model = User + serializer_class = UserSerializer + parent_model = Team + relationship = 'users' + +class TeamPermissionsList(SubListCreateAPIView): + + model = Permission + serializer_class = PermissionSerializer + parent_model = Team + relationship = 'permissions' + parent_key = 'team' + + def get_queryset(self): + # FIXME: Default get_queryset should handle this. + team = Team.objects.get(pk=self.kwargs['pk']) + base = Permission.objects.filter(team = team) + #if Team.can_user_administrate(self.request.user, team, None): + if self.request.user.can_access(Team, 'change', team, None): + return base + elif team.users.filter(pk=self.request.user.pk).count() > 0: + return base + raise PermissionDenied() + +class TeamProjectsList(SubListCreateAPIView): + + model = Project + serializer_class = ProjectSerializer + parent_model = Team + relationship = 'projects' + +class TeamCredentialsList(SubListCreateAPIView): + + model = Credential + serializer_class = CredentialSerializer + parent_model = Team + relationship = 'credentials' + parent_key = 'team' + +class ProjectList(ListCreateAPIView): + + model = Project + serializer_class = ProjectSerializer + + def get(self, request, *args, **kwargs): + # Not optimal, but make sure the project status and last_updated fields + # are up to date here... + projects_qs = Project.objects.filter(active=True) + projects_qs = projects_qs.select_related('current_update', 'last_updated') + for project in projects_qs: + project.set_status_and_last_updated() + return super(ProjectList, self).get(request, *args, **kwargs) + +class ProjectDetail(RetrieveUpdateDestroyAPIView): + + model = Project + serializer_class = ProjectSerializer + +class ProjectPlaybooks(RetrieveAPIView): + + model = Project + serializer_class = ProjectPlaybooksSerializer + +class ProjectOrganizationsList(SubListCreateAPIView): + + model = Organization + serializer_class = OrganizationSerializer + parent_model = Project + relationship = 'organizations' + +class ProjectTeamsList(SubListCreateAPIView): + + model = Team + serializer_class = TeamSerializer + parent_model = Project + relationship = 'teams' + +class ProjectUpdatesList(SubListAPIView): + + model = ProjectUpdate + serializer_class = ProjectUpdateSerializer + parent_model = Project + relationship = 'project_updates' + new_in_13 = True + +class ProjectUpdateView(GenericAPIView): + + model = Project + new_in_13 = True + + def get(self, request, *args, **kwargs): + obj = self.get_object() + data = dict( + can_update=obj.can_update, + ) + if obj.scm_type: + data['passwords_needed_to_update'] = obj.scm_passwords_needed + return Response(data) + + def post(self, request, *args, **kwargs): + obj = self.get_object() + if obj.can_update: + project_update = obj.update(**request.DATA) + if not project_update: + data = dict(passwords_needed_to_update=obj.scm_passwords_needed) + return Response(data, status=status.HTTP_400_BAD_REQUEST) + else: + headers = {'Location': project_update.get_absolute_url()} + return Response(status=status.HTTP_202_ACCEPTED, headers=headers) + else: + return self.http_method_not_allowed(request, *args, **kwargs) + +class ProjectUpdateDetail(RetrieveAPIView): + + model = ProjectUpdate + serializer_class = ProjectUpdateSerializer + new_in_13 = True + +class ProjectUpdateCancel(GenericAPIView): + + model = ProjectUpdate + is_job_cancel = True + new_in_13 = True + + def get(self, request, *args, **kwargs): + obj = self.get_object() + data = dict( + can_cancel=obj.can_cancel, + ) + return Response(data) + + def post(self, request, *args, **kwargs): + obj = self.get_object() + if obj.can_cancel: + result = obj.cancel() + return Response(status=status.HTTP_202_ACCEPTED) + else: + return self.http_method_not_allowed(request, *args, **kwargs) + +class UserList(ListCreateAPIView): + + model = User + serializer_class = UserSerializer + +class UserMeList(ListAPIView): + + model = User + serializer_class = UserSerializer + view_name = 'Me' + + def get_queryset(self): + return self.model.objects.filter(pk=self.request.user.pk) + +class UserTeamsList(SubListAPIView): + + model = Team + serializer_class = TeamSerializer + parent_model = User + relationship = 'teams' + +class UserPermissionsList(SubListCreateAPIView): + + model = Permission + serializer_class = PermissionSerializer + parent_model = User + relationship = 'permissions' + parent_key = 'user' + +class UserProjectsList(SubListAPIView): + + model = Project + serializer_class = ProjectSerializer + parent_model = User + relationship = 'projects' + + def get_queryset(self): + parent = self.get_parent_object() + self.check_parent_access(parent) + qs = self.request.user.get_queryset(self.model) + return qs.filter(teams__in=parent.teams.distinct()) + +class UserCredentialsList(SubListCreateAPIView): + + model = Credential + serializer_class = CredentialSerializer + parent_model = User + relationship = 'credentials' + parent_key = 'user' + +class UserOrganizationsList(SubListAPIView): + + model = Organization + serializer_class = OrganizationSerializer + parent_model = User + relationship = 'organizations' + +class UserAdminOfOrganizationsList(SubListAPIView): + + model = Organization + serializer_class = OrganizationSerializer + parent_model = User + relationship = 'admin_of_organizations' + +class UserDetail(RetrieveUpdateDestroyAPIView): + + model = User + serializer_class = UserSerializer + + def update_filter(self, request, *args, **kwargs): + ''' make sure non-read-only fields that can only be edited by admins, are only edited by admins ''' + obj = User.objects.get(pk=kwargs['pk']) + can_change = request.user.can_access(User, 'change', obj, request.DATA) + can_admin = request.user.can_access(User, 'admin', obj, request.DATA) + if can_change and not can_admin: + admin_only_edit_fields = ('last_name', 'first_name', 'username', + 'is_active', 'is_superuser') + changed = {} + for field in admin_only_edit_fields: + left = getattr(obj, field, None) + right = request.DATA.get(field, None) + if left is not None and right is not None and left != right: + changed[field] = (left, right) + if changed: + raise PermissionDenied('Cannot change %s' % ', '.join(changed.keys())) + +class CredentialList(ListCreateAPIView): + + model = Credential + serializer_class = CredentialSerializer + +class CredentialDetail(RetrieveUpdateDestroyAPIView): + + model = Credential + serializer_class = CredentialSerializer + +class PermissionDetail(RetrieveUpdateDestroyAPIView): + + model = Permission + serializer_class = PermissionSerializer + +class InventoryList(ListCreateAPIView): + + model = Inventory + serializer_class = InventorySerializer + +class InventoryDetail(RetrieveUpdateDestroyAPIView): + + model = Inventory + serializer_class = InventorySerializer + +class HostList(ListCreateAPIView): + + model = Host + serializer_class = HostSerializer + +class HostDetail(RetrieveUpdateDestroyAPIView): + + model = Host + serializer_class = HostSerializer + +class InventoryHostsList(SubListCreateAPIView): + + model = Host + serializer_class = HostSerializer + parent_model = Inventory + relationship = 'hosts' + parent_key = 'inventory' + +class HostGroupsList(SubListCreateAPIView): + ''' the list of groups a host is directly a member of ''' + + model = Group + serializer_class = GroupSerializer + parent_model = Host + relationship = 'groups' + +class HostAllGroupsList(SubListAPIView): + ''' the list of all groups of which the host is directly or indirectly a member ''' + + model = Group + serializer_class = GroupSerializer + parent_model = Host + relationship = 'groups' + + def get_queryset(self): + parent = self.get_parent_object() + self.check_parent_access(parent) + qs = self.request.user.get_queryset(self.model) + sublist_qs = parent.all_groups.distinct() + return qs & sublist_qs + +class GroupList(ListCreateAPIView): + + model = Group + serializer_class = GroupSerializer + +class GroupChildrenList(SubListCreateAPIView): + + model = Group + serializer_class = GroupSerializer + parent_model = Group + relationship = 'children' + + def _unattach(self, request, *args, **kwargs): # FIXME: Disabled for now for UI support. + ''' + Special case for disassociating a child group from the parent. If the + child group has no more parents, then automatically mark it inactive. + ''' + sub_id = request.DATA.get('id', None) + if not sub_id: + data = dict(msg='"id" is required to disassociate') + return Response(data, status=status.HTTP_400_BAD_REQUEST) + + parent = self.get_parent_object() + parent_key = getattr(self, 'parent_key', None) + relationship = getattr(parent, self.relationship) + sub = get_object_or_400(self.model, pk=sub_id) + + if not request.user.can_access(self.parent_model, 'unattach', parent, + sub, self.relationship): + raise PermissionDenied() + + if sub.parents.filter(active=True).exclude(pk=parent.pk).count() == 0: + sub.mark_inactive() + else: + relationship.remove(sub) + + return Response(status=status.HTTP_204_NO_CONTENT) + +class GroupPotentialChildrenList(SubListAPIView): + + model = Group + serializer_class = GroupSerializer + parent_model = Group + new_in_14 = True + + def get_queryset(self): + parent = self.get_parent_object() + self.check_parent_access(parent) + qs = self.request.user.get_queryset(self.model) + qs = qs.filter(inventory__pk=parent.inventory.pk) + except_pks = set([parent.pk]) + except_pks.update(parent.all_parents.values_list('pk', flat=True)) + except_pks.update(parent.all_children.values_list('pk', flat=True)) + return qs.exclude(pk__in=except_pks) + +class GroupHostsList(SubListCreateAPIView): + ''' the list of hosts directly below a group ''' + + model = Host + serializer_class = HostSerializer + parent_model = Group + relationship = 'hosts' + +class GroupAllHostsList(SubListAPIView): + ''' the list of all hosts below a group, even including subgroups ''' + + model = Host + serializer_class = HostSerializer + parent_model = Group + relationship = 'hosts' + + def get_queryset(self): + parent = self.get_parent_object() + self.check_parent_access(parent) + qs = self.request.user.get_queryset(self.model) + sublist_qs = parent.all_hosts.distinct() + return qs & sublist_qs + +class GroupDetail(RetrieveUpdateDestroyAPIView): + + model = Group + serializer_class = GroupSerializer + +class InventoryGroupsList(SubListCreateAPIView): + + model = Group + serializer_class = GroupSerializer + parent_model = Inventory + relationship = 'groups' + parent_key = 'inventory' + +class InventoryRootGroupsList(SubListCreateAPIView): + + model = Group + serializer_class = GroupSerializer + parent_model = Inventory + relationship = 'groups' + parent_key = 'inventory' + + def get_queryset(self): + parent = self.get_parent_object() + self.check_parent_access(parent) + qs = self.request.user.get_queryset(self.model) + return qs & parent.root_groups + +class BaseVariableData(RetrieveUpdateAPIView): + + parser_classes = api_settings.DEFAULT_PARSER_CLASSES + [YAMLParser] + renderer_classes = api_settings.DEFAULT_RENDERER_CLASSES + [YAMLRenderer] + is_variable_data = True # Special flag for permissions check. + +class InventoryVariableData(BaseVariableData): + + model = Inventory + serializer_class = InventoryVariableDataSerializer + +class HostVariableData(BaseVariableData): + + model = Host + serializer_class = HostVariableDataSerializer + +class GroupVariableData(BaseVariableData): + + model = Group + serializer_class = GroupVariableDataSerializer + +class InventoryScriptView(RetrieveAPIView): + + model = Inventory + authentication_classes = [JobTaskAuthentication] + \ + api_settings.DEFAULT_AUTHENTICATION_CLASSES + permission_classes = (JobTaskPermission,) + filter_backends = () + + def retrieve(self, request, *args, **kwargs): + self.object = self.get_object() + hostname = request.QUERY_PARAMS.get('host', '') + hostvars = bool(request.QUERY_PARAMS.get('hostvars', '')) + show_all = bool(request.QUERY_PARAMS.get('all', '')) + if show_all: + hosts_q = dict(active=True) + else: + hosts_q = dict(active=True, enabled=True) + if hostname: + host = get_object_or_404(self.object.hosts, name=hostname, **hosts_q) + data = host.variables_dict + else: + data = SortedDict() + if self.object.variables_dict: + data['all'] = SortedDict() + data['all']['vars'] = self.object.variables_dict + + for group in self.object.groups.filter(active=True): + hosts = group.hosts.filter(**hosts_q) + children = group.children.filter(active=True) + group_info = SortedDict() + group_info['hosts'] = list(hosts.values_list('name', flat=True)) + group_info['children'] = list(children.values_list('name', flat=True)) + group_info['vars'] = group.variables_dict + data[group.name] = group_info + + if hostvars: + data.setdefault('_meta', SortedDict()) + data['_meta'].setdefault('hostvars', SortedDict()) + for host in self.object.hosts.filter(**hosts_q): + data['_meta']['hostvars'][host.name] = host.variables_dict + + # workaround for Ansible inventory bug (github #3687), localhost + # must be explicitly listed in the all group for dynamic inventory + # scripts to pick it up. + localhost_names = ('localhost', '127.0.0.1', '::1') + localhosts_qs = self.object.hosts.filter(name__in=localhost_names, + **hosts_q) + localhosts = list(localhosts_qs.values_list('name', flat=True)) + if localhosts: + data.setdefault('all', SortedDict()) + data['all']['hosts'] = localhosts + + return Response(data) + +class InventoryTreeView(RetrieveAPIView): + + model = Inventory + filter_backends = () + new_in_13 = True + + def retrieve(self, request, *args, **kwargs): + inventory = self.get_object() + groups_qs = inventory.root_groups.filter(active=True) + data = GroupTreeSerializer(groups_qs, many=True).data + return Response(data) + + def get_description_context(self): + d = super(InventoryTreeView, self).get_description_context() + d.update({ + 'serializer_fields': GroupTreeSerializer().metadata(), + }) + return d + +class InventoryInventorySourcesList(SubListAPIView): + + model = InventorySource + serializer_class = InventorySourceSerializer + parent_model = Inventory + relationship = None # Not defined since using get_queryset(). + view_name = 'Inventory Source List' + new_in_14 = True + + def get_queryset(self): + parent = self.get_parent_object() + self.check_parent_access(parent) + qs = self.request.user.get_queryset(self.model) + return qs.filter(Q(inventory__pk=parent.pk) | + Q(group__inventory__pk=parent.pk)) + +class InventorySourceList(ListAPIView): + + model = InventorySource + serializer_class = InventorySourceSerializer + new_in_14 = True + +class InventorySourceDetail(RetrieveUpdateAPIView): + + model = InventorySource + serializer_class = InventorySourceSerializer + new_in_14 = True + +class InventorySourceUpdatesList(SubListAPIView): + + model = InventoryUpdate + serializer_class = InventoryUpdateSerializer + parent_model = InventorySource + relationship = 'inventory_updates' + new_in_14 = True + +class InventorySourceUpdateView(GenericAPIView): + + model = InventorySource + new_in_14 = True + + def get(self, request, *args, **kwargs): + obj = self.get_object() + data = dict( + can_update=obj.can_update, + ) + return Response(data) + + def post(self, request, *args, **kwargs): + obj = self.get_object() + if obj.can_update: + inventory_update = obj.update(**request.DATA) + if not inventory_update: + return Response({}, status=status.HTTP_400_BAD_REQUEST) + else: + headers = {'Location': inventory_update.get_absolute_url()} + return Response(status=status.HTTP_202_ACCEPTED, headers=headers) + else: + return self.http_method_not_allowed(request, *args, **kwargs) + +class InventoryUpdateDetail(RetrieveAPIView): + + model = InventoryUpdate + serializer_class = InventoryUpdateSerializer + new_in_14 = True + +class InventoryUpdateCancel(GenericAPIView): + + model = InventoryUpdate + is_job_cancel = True + new_in_14 = True + + def get(self, request, *args, **kwargs): + obj = self.get_object() + data = dict( + can_cancel=obj.can_cancel, + ) + return Response(data) + + def post(self, request, *args, **kwargs): + obj = self.get_object() + if obj.can_cancel: + result = obj.cancel() + return Response(status=status.HTTP_202_ACCEPTED) + else: + return self.http_method_not_allowed(request, *args, **kwargs) + +class JobTemplateList(ListCreateAPIView): + + model = JobTemplate + serializer_class = JobTemplateSerializer + +class JobTemplateDetail(RetrieveUpdateDestroyAPIView): + + model = JobTemplate + serializer_class = JobTemplateSerializer + +class JobTemplateCallback(GenericAPIView): + + model = JobTemplate + permission_classes = (JobTemplateCallbackPermission,) + + def find_matching_hosts(self): + ''' + Find the host(s) in the job template's inventory that match the remote + host for the current request. + ''' + # Find the list of remote host names/IPs to check. + remote_hosts = set() + for header in settings.REMOTE_HOST_HEADERS: + value = self.request.META.get(header, '').strip() + if value: + remote_hosts.add(value) + # Add the reverse lookup of IP addresses. + for rh in list(remote_hosts): + try: + result = socket.gethostbyaddr(rh) + except socket.herror: + continue + remote_hosts.add(result[0]) + remote_hosts.update(result[1]) + # Filter out any .arpa results. + for rh in list(remote_hosts): + if rh.endswith('.arpa'): + remote_hosts.remove(rh) + if not remote_hosts: + return set() + # Find the host objects to search for a match. + obj = self.get_object() + qs = obj.inventory.hosts.filter(active=True) + # First try for an exact match on the name. + try: + return set([qs.get(name__in=remote_hosts)]) + except (Host.DoesNotExist, Host.MultipleObjectsReturned): + pass + # Next, try matching based on name or ansible_ssh_host variable. + matches = set() + for host in qs: + ansible_ssh_host = host.variables_dict.get('ansible_ssh_host', '') + if ansible_ssh_host in remote_hosts: + matches.add(host) + # FIXME: Not entirely sure if this statement will ever be needed? + if host.name != ansible_ssh_host and host.name in remote_hosts: + matches.add(host) + if len(matches) == 1: + return matches + # Try to resolve forward addresses for each host to find matches. + for host in qs: + hostnames = set([host.name]) + ansible_ssh_host = host.variables_dict.get('ansible_ssh_host', '') + if ansible_ssh_host: + hostnames.add(ansible_ssh_host) + for hostname in hostnames: + try: + result = socket.getaddrinfo(hostname, None) + possible_ips = set(x[4][0] for x in result) + possible_ips.discard(hostname) + if possible_ips and possible_ips & remote_hosts: + matches.add(host) + except socket.gaierror: + pass + # Return all matches found. + return matches + + def get(self, request, *args, **kwargs): + job_template = self.get_object() + matching_hosts = self.find_matching_hosts() + data = dict( + host_config_key=job_template.host_config_key, + matching_hosts=[x.name for x in matching_hosts], + ) + if settings.DEBUG: + d = dict([(k,v) for k,v in request.META.items() + if k.startswith('HTTP_') or k.startswith('REMOTE_')]) + data['request_meta'] = d + return Response(data) + + def post(self, request, *args, **kwargs): + job_template = self.get_object() + # Permission class should have already validated host_config_key. + matching_hosts = self.find_matching_hosts() + if not matching_hosts: + data = dict(msg='No matching host could be found!') + # FIXME: Log! + return Response(data, status=status.HTTP_400_BAD_REQUEST) + elif len(matching_hosts) > 1: + data = dict(msg='Multiple hosts matched the request!') + # FIXME: Log! + return Response(data, status=status.HTTP_400_BAD_REQUEST) + else: + host = list(matching_hosts)[0] + if not job_template.can_start_without_user_input(): + data = dict(msg='Cannot start automatically, user input required!') + # FIXME: Log! + return Response(data, status=status.HTTP_400_BAD_REQUEST) + limit = ':'.join(filter(None, [job_template.limit, host.name])) + job = job_template.create_job(limit=limit, launch_type='callback') + result = job.start() + if not result: + data = dict(msg='Error starting job!') + return Response(data, status=status.HTTP_400_BAD_REQUEST) + else: + return Response(status=status.HTTP_202_ACCEPTED) + +class JobTemplateJobsList(SubListCreateAPIView): + + model = Job + serializer_class = JobSerializer + parent_model = JobTemplate + relationship = 'jobs' + parent_key = 'job_template' + +class JobList(ListCreateAPIView): + + model = Job + serializer_class = JobSerializer + +class JobDetail(RetrieveUpdateDestroyAPIView): + + model = Job + serializer_class = JobSerializer + + def update(self, request, *args, **kwargs): + obj = self.get_object() + # Only allow changes (PUT/PATCH) when job status is "new". + if obj.status != 'new': + return self.http_method_not_allowed(request, *args, **kwargs) + return super(JobDetail, self).update(request, *args, **kwargs) + +class JobStart(GenericAPIView): + + model = Job + is_job_start = True + + def get(self, request, *args, **kwargs): + obj = self.get_object() + data = dict( + can_start=obj.can_start, + ) + if obj.can_start: + data['passwords_needed_to_start'] = obj.passwords_needed_to_start + return Response(data) + + def post(self, request, *args, **kwargs): + obj = self.get_object() + if obj.can_start: + result = obj.start(**request.DATA) + if not result: + data = dict(passwords_needed_to_start=obj.passwords_needed_to_start) + return Response(data, status=status.HTTP_400_BAD_REQUEST) + else: + return Response(status=status.HTTP_202_ACCEPTED) + else: + return self.http_method_not_allowed(request, *args, **kwargs) + +class JobCancel(GenericAPIView): + + model = Job + is_job_cancel = True + + def get(self, request, *args, **kwargs): + obj = self.get_object() + data = dict( + can_cancel=obj.can_cancel, + ) + return Response(data) + + def post(self, request, *args, **kwargs): + obj = self.get_object() + if obj.can_cancel: + result = obj.cancel() + return Response(status=status.HTTP_202_ACCEPTED) + else: + return self.http_method_not_allowed(request, *args, **kwargs) + +class BaseJobHostSummariesList(SubListAPIView): + + model = JobHostSummary + serializer_class = JobHostSummarySerializer + parent_model = None # Subclasses must define this attribute. + relationship = 'job_host_summaries' + view_name = 'Job Host Summaries List' + +class HostJobHostSummariesList(BaseJobHostSummariesList): + + parent_model = Host + +class GroupJobHostSummariesList(BaseJobHostSummariesList): + + parent_model = Group + +class JobJobHostSummariesList(BaseJobHostSummariesList): + + parent_model = Job + +class JobHostSummaryDetail(RetrieveAPIView): + + model = JobHostSummary + serializer_class = JobHostSummarySerializer + +class JobEventList(ListAPIView): + + model = JobEvent + serializer_class = JobEventSerializer + +class JobEventDetail(RetrieveAPIView): + + model = JobEvent + serializer_class = JobEventSerializer + +class JobEventChildrenList(SubListAPIView): + + model = JobEvent + serializer_class = JobEventSerializer + parent_model = JobEvent + relationship = 'children' + view_name = 'Job Event Children List' + +class JobEventHostsList(SubListAPIView): + + model = Host + serializer_class = HostSerializer + parent_model = JobEvent + relationship = 'hosts' + view_name = 'Job Event Hosts List' + +class BaseJobEventsList(SubListAPIView): + + model = JobEvent + serializer_class = JobEventSerializer + parent_model = None # Subclasses must define this attribute. + relationship = 'job_events' + view_name = 'Job Events List' + +class HostJobEventsList(BaseJobEventsList): + + parent_model = Host + +class GroupJobEventsList(BaseJobEventsList): + + parent_model = Group + +class JobJobEventsList(BaseJobEventsList): + + parent_model = Job + authentication_classes = [JobTaskAuthentication] + \ + api_settings.DEFAULT_AUTHENTICATION_CLASSES + permission_classes = (JobTaskPermission,) + + # Post allowed for job event callback only. + def post(self, request, *args, **kwargs): + parent_obj = get_object_or_404(self.parent_model, pk=self.kwargs['pk']) + data = request.DATA.copy() + data['job'] = parent_obj.pk + serializer = self.get_serializer(data=data) + if serializer.is_valid(): + self.pre_save(serializer.object) + self.object = serializer.save(force_insert=True) + self.post_save(self.object, created=True) + headers = {'Location': serializer.data['url']} + return Response(serializer.data, status=status.HTTP_201_CREATED, + headers=headers) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + +# Create view functions for all of the class-based views to simplify inclusion +# in URL patterns and reverse URL lookups, converting CamelCase names to +# lowercase_with_underscore (e.g. MyView.as_view() becomes my_view). +this_module = sys.modules[__name__] +for attr, value in locals().items(): + if isinstance(value, type) and issubclass(value, APIView): + name = camelcase_to_underscore(attr) + view = value.as_view() + setattr(this_module, name, view) diff --git a/awx/lib/__init__.py b/awx/lib/__init__.py new file mode 100644 index 0000000000..bfec7ec6ea --- /dev/null +++ b/awx/lib/__init__.py @@ -0,0 +1,2 @@ +# Copyright (c) 2013 AnsibleWorks, Inc. +# All Rights Reserved. diff --git a/awx/main/compat.py b/awx/lib/compat.py similarity index 100% rename from awx/main/compat.py rename to awx/lib/compat.py diff --git a/awx/main/admin.py b/awx/main/admin.py index 7184d77405..1bf3b6d54e 100644 --- a/awx/main/admin.py +++ b/awx/main/admin.py @@ -15,7 +15,7 @@ from django.utils.translation import ugettext_lazy as _ from django.contrib.auth.models import User from django.contrib.auth.admin import UserAdmin -from awx.main.compat import format_html +from awx.lib.compat import format_html from awx.main.models import * from awx.main.forms import * diff --git a/awx/main/models/__init__.py b/awx/main/models/__init__.py index b53cbd5c12..d76258d926 100644 --- a/awx/main/models/__init__.py +++ b/awx/main/models/__init__.py @@ -35,7 +35,7 @@ from taggit.managers import TaggableManager from djcelery.models import TaskMeta # AWX -from awx.main.compat import slugify +from awx.lib.compat import slugify from awx.main.fields import AutoOneToOneField from awx.main.utils import encrypt_field, decrypt_field @@ -343,7 +343,7 @@ class Organization(CommonModel): projects = models.ManyToManyField('Project', blank=True, related_name='organizations') def get_absolute_url(self): - return reverse('main:organization_detail', args=(self.pk,)) + return reverse('api:organization_detail', args=(self.pk,)) def __unicode__(self): return self.name @@ -412,7 +412,7 @@ class Inventory(CommonModel): ) def get_absolute_url(self): - return reverse('main:inventory_detail', args=(self.pk,)) + return reverse('api:inventory_detail', args=(self.pk,)) def mark_inactive(self, save=True): ''' @@ -537,7 +537,7 @@ class Host(CommonModelNameNotUnique): return self.name def get_absolute_url(self): - return reverse('main:host_detail', args=(self.pk,)) + return reverse('api:host_detail', args=(self.pk,)) def mark_inactive(self, save=True): ''' @@ -667,7 +667,7 @@ class Group(CommonModelNameNotUnique): return self.name def get_absolute_url(self): - return reverse('main:group_detail', args=(self.pk,)) + return reverse('api:group_detail', args=(self.pk,)) def mark_inactive(self, save=True): ''' @@ -938,7 +938,7 @@ class InventorySource(PrimordialModel): return inventory_update def get_absolute_url(self): - return reverse('main:inventory_source_detail', args=(self.pk,)) + return reverse('api:inventory_source_detail', args=(self.pk,)) class InventoryUpdate(CommonTask): ''' @@ -972,7 +972,7 @@ class InventoryUpdate(CommonTask): return self.inventory_source def get_absolute_url(self): - return reverse('main:inventory_update_detail', args=(self.pk,)) + return reverse('api:inventory_update_detail', args=(self.pk,)) def _get_task_class(self): from awx.main.tasks import RunInventoryUpdate @@ -1087,7 +1087,7 @@ class Credential(CommonModelNameNotUnique): return needed def get_absolute_url(self): - return reverse('main:credential_detail', args=(self.pk,)) + return reverse('api:credential_detail', args=(self.pk,)) def clean(self): if self.user and self.team: @@ -1142,7 +1142,7 @@ class Team(CommonModelNameNotUnique): organization = models.ForeignKey('Organization', blank=False, null=True, on_delete=SET_NULL, related_name='teams') def get_absolute_url(self): - return reverse('main:team_detail', args=(self.pk,)) + return reverse('api:team_detail', args=(self.pk,)) class Project(CommonModel): ''' @@ -1377,7 +1377,7 @@ class Project(CommonModel): return project_update def get_absolute_url(self): - return reverse('main:project_detail', args=(self.pk,)) + return reverse('api:project_detail', args=(self.pk,)) def get_project_path(self, check_if_exists=True): local_path = os.path.basename(self.local_path) @@ -1435,7 +1435,7 @@ class ProjectUpdate(CommonTask): ) def get_absolute_url(self): - return reverse('main:project_update_detail', args=(self.pk,)) + return reverse('api:project_update_detail', args=(self.pk,)) def _get_parent_instance(self): return self.project @@ -1507,7 +1507,7 @@ class Permission(CommonModelNameNotUnique): )) def get_absolute_url(self): - return reverse('main:permission_detail', args=(self.pk,)) + return reverse('api:permission_detail', args=(self.pk,)) # TODO: other job types (later) @@ -1607,7 +1607,7 @@ class JobTemplate(CommonModel): return job def get_absolute_url(self): - return reverse('main:job_template_detail', args=(self.pk,)) + return reverse('api:job_template_detail', args=(self.pk,)) def can_start_without_user_input(self): ''' @@ -1719,7 +1719,7 @@ class Job(CommonTask): ) def get_absolute_url(self): - return reverse('main:job_detail', args=(self.pk,)) + return reverse('api:job_detail', args=(self.pk,)) extra_vars_dict = VarsDictProperty('extra_vars', True) @@ -1829,7 +1829,7 @@ class JobHostSummary(models.Model): self.processed, self.skipped) def get_absolute_url(self): - return reverse('main:job_host_summary_detail', args=(self.pk,)) + return reverse('api:job_host_summary_detail', args=(self.pk,)) def save(self, *args, **kwargs): self.failed = bool(self.dark or self.failures) @@ -1982,7 +1982,7 @@ class JobEvent(models.Model): ) def get_absolute_url(self): - return reverse('main:job_event_detail', args=(self.pk,)) + return reverse('api:job_event_detail', args=(self.pk,)) def __unicode__(self): return u'%s @ %s' % (self.get_event_display(), self.created.isoformat()) diff --git a/awx/main/templates/main/api_view.md b/awx/main/templates/main/api_view.md deleted file mode 100644 index 77c3bde798..0000000000 --- a/awx/main/templates/main/api_view.md +++ /dev/null @@ -1,3 +0,0 @@ -{{ docstring }} - -{% include "main/_new_in_awx.md" %} diff --git a/awx/main/tests/inventory.py b/awx/main/tests/inventory.py index 5d42d23041..54077de37e 100644 --- a/awx/main/tests/inventory.py +++ b/awx/main/tests/inventory.py @@ -44,7 +44,7 @@ class InventoryTest(BaseTest): ) def test_get_inventory_list(self): - url = reverse('main:inventory_list') + url = reverse('api:inventory_list') qs = Inventory.objects.filter(active=True).distinct() # Check list view with invalid authentication. @@ -66,7 +66,7 @@ class InventoryTest(BaseTest): self.check_get_list(url, self.nobody_django_user, nobody_qs) def test_post_inventory_list(self): - url = reverse('main:inventory_list') + url = reverse('api:inventory_list') # Check post to list view with invalid authentication. new_inv_0 = dict(name='inventory-c', description='baz', organization=self.organizations[0].pk) @@ -95,8 +95,8 @@ class InventoryTest(BaseTest): data = self.post(url, data=new_inv_denied, expect=403) def test_get_inventory_detail(self): - url_a = reverse('main:inventory_detail', args=(self.inventory_a.pk,)) - url_b = reverse('main:inventory_detail', args=(self.inventory_b.pk,)) + url_a = reverse('api:inventory_detail', args=(self.inventory_a.pk,)) + url_b = reverse('api:inventory_detail', args=(self.inventory_b.pk,)) # Check detail view with invalid authentication. self.check_invalid_auth(url_a) @@ -125,8 +125,8 @@ class InventoryTest(BaseTest): data = self.get(url_b, expect=403) def test_put_inventory_detail(self): - url_a = reverse('main:inventory_detail', args=(self.inventory_a.pk,)) - url_b = reverse('main:inventory_detail', args=(self.inventory_b.pk,)) + url_a = reverse('api:inventory_detail', args=(self.inventory_a.pk,)) + url_b = reverse('api:inventory_detail', args=(self.inventory_b.pk,)) # Check put to detail view with invalid authentication. self.check_invalid_auth(url_a, methods=('put',)) @@ -178,7 +178,7 @@ class InventoryTest(BaseTest): # Via AC-376: # Create an inventory. Leave the description empty. # Edit the new inventory, change the Name, click Save. - list_url = reverse('main:inventory_list') + list_url = reverse('api:inventory_list') new_data = dict(name='inventory-c', description='', organization=self.organizations[0].pk) new_id = max(Inventory.objects.values_list('pk', flat=True)) + 1 @@ -186,7 +186,7 @@ class InventoryTest(BaseTest): data = self.post(list_url, data=new_data, expect=201) self.assertEqual(data['id'], new_id) self.assertEqual(data['description'], '') - url_c = reverse('main:inventory_detail', args=(new_id,)) + url_c = reverse('api:inventory_detail', args=(new_id,)) data = self.get(url_c, expect=200) self.assertEqual(data['description'], '') data['description'] = None @@ -194,8 +194,8 @@ class InventoryTest(BaseTest): self.put(url_c, data, expect=200) def test_delete_inventory_detail(self): - url_a = reverse('main:inventory_detail', args=(self.inventory_a.pk,)) - url_b = reverse('main:inventory_detail', args=(self.inventory_b.pk,)) + url_a = reverse('api:inventory_detail', args=(self.inventory_a.pk,)) + url_b = reverse('api:inventory_detail', args=(self.inventory_b.pk,)) # Create test hosts and groups within each inventory. self.inventory_a.hosts.create(name='host-a') @@ -246,11 +246,11 @@ class InventoryTest(BaseTest): def test_main_line(self): # some basic URLs... - inventories = reverse('main:inventory_list') - inventories_1 = reverse('main:inventory_detail', args=(self.inventory_a.pk,)) - inventories_2 = reverse('main:inventory_detail', args=(self.inventory_b.pk,)) - hosts = reverse('main:host_list') - groups = reverse('main:group_list') + inventories = reverse('api:inventory_list') + inventories_1 = reverse('api:inventory_detail', args=(self.inventory_a.pk,)) + inventories_2 = reverse('api:inventory_detail', args=(self.inventory_b.pk,)) + hosts = reverse('api:host_list') + groups = reverse('api:group_list') # a super user can add hosts (but inventory ID is required) @@ -326,7 +326,7 @@ class InventoryTest(BaseTest): new_group_c = dict(name='web4', inventory=inv.pk) new_group_d = dict(name='web5', inventory=inv.pk) new_group_e = dict(name='web6', inventory=inv.pk) - groups = reverse('main:group_list') + groups = reverse('api:group_list') data0 = self.post(groups, data=invalid, expect=400, auth=self.get_super_credentials()) data0 = self.post(groups, data=new_group_a, expect=201, auth=self.get_super_credentials()) @@ -360,7 +360,7 @@ class InventoryTest(BaseTest): ################################################# # HOSTS->inventories POST via subcollection - url = reverse('main:inventory_hosts_list', args=(self.inventory_a.pk,)) + url = reverse('api:inventory_hosts_list', args=(self.inventory_a.pk,)) new_host_a = dict(name='web100.example.com') new_host_b = dict(name='web101.example.com') new_host_c = dict(name='web102.example.com') @@ -377,7 +377,7 @@ class InventoryTest(BaseTest): added_by_collection_c = self.post(url, data=new_host_c, expect=403, auth=self.get_nobody_credentials()) # a normal user with edit permission on the inventory can associate hosts with inventories - url5 = reverse('main:inventory_hosts_list', args=(inv.pk,)) + url5 = reverse('api:inventory_hosts_list', args=(inv.pk,)) added_by_collection_d = self.post(url5, data=new_host_d, expect=201, auth=self.get_other_credentials()) got = self.get(url5, expect=200, auth=self.get_other_credentials()) self.assertEquals(got['count'], 4) @@ -392,9 +392,9 @@ class InventoryTest(BaseTest): ################################################## # GROUPS->inventories POST via subcollection - root_groups = reverse('main:inventory_root_groups_list', args=(self.inventory_a.pk,)) + root_groups = reverse('api:inventory_root_groups_list', args=(self.inventory_a.pk,)) - url = reverse('main:inventory_groups_list', args=(self.inventory_a.pk,)) + url = reverse('api:inventory_groups_list', args=(self.inventory_a.pk,)) new_group_a = dict(name='web100') new_group_b = dict(name='web101') new_group_c = dict(name='web102') @@ -411,7 +411,7 @@ class InventoryTest(BaseTest): added_by_collection = self.post(url, data=new_group_c, expect=403, auth=self.get_nobody_credentials()) # a normal user with edit permissions on the inventory can associate groups with inventories - url5 = reverse('main:inventory_groups_list', args=(inv.pk,)) + url5 = reverse('api:inventory_groups_list', args=(inv.pk,)) added_by_collection = self.post(url5, data=new_group_d, expect=201, auth=self.get_other_credentials()) # make sure duplicates give 400s self.post(url5, data=new_group_d, expect=400, auth=self.get_other_credentials()) @@ -436,7 +436,7 @@ class InventoryTest(BaseTest): vars_c = dict(asdf=5555, dog='mouse', cat='mogwai', unstructured=dict(a=[3,0,3],b=dict(z=2600))) # attempting to get a variable object creates it, even though it does not already exist - vdata_url = reverse('main:host_variable_data', args=(added_by_collection_a['id'],)) + vdata_url = reverse('api:host_variable_data', args=(added_by_collection_a['id'],)) got = self.get(vdata_url, expect=200, auth=self.get_super_credentials()) self.assertEquals(got, {}) @@ -467,8 +467,8 @@ class InventoryTest(BaseTest): vars_c = dict(asdf=9999, dog='pluto', cat='five', unstructured=dict(a=[3,3,3],b=dict(z=5))) groups = Group.objects.all() - vdata1_url = reverse('main:group_variable_data', args=(groups[0].pk,)) - vdata2_url = reverse('main:group_variable_data', args=(groups[1].pk,)) + vdata1_url = reverse('api:group_variable_data', args=(groups[0].pk,)) + vdata2_url = reverse('api:group_variable_data', args=(groups[1].pk,)) # a super user can associate variable objects with groups got = self.get(vdata1_url, expect=200, auth=self.get_super_credentials()) @@ -493,7 +493,7 @@ class InventoryTest(BaseTest): vars_b = dict(asdf=2736, dog='benji', cat='garfield', unstructured=dict(a=[2,2,2],b=dict(x=3,y=4))) vars_c = dict(asdf=7692, dog='buck', cat='sylvester', unstructured=dict(a=[3,3,3],b=dict(z=5))) - vdata_url = reverse('main:inventory_variable_data', args=(self.inventory_a.pk,)) + vdata_url = reverse('api:inventory_variable_data', args=(self.inventory_a.pk,)) # a super user can associate variable objects with inventory got = self.get(vdata_url, expect=200, auth=self.get_super_credentials()) @@ -536,14 +536,14 @@ class InventoryTest(BaseTest): groups[0].save() # access - url1 = reverse('main:group_hosts_list', args=(groups[0].pk,)) + url1 = reverse('api:group_hosts_list', args=(groups[0].pk,)) data = self.get(url1, expect=200, auth=self.get_normal_credentials()) self.assertEquals(data['count'], 2) self.assertTrue(host1.pk in [x['id'] for x in data['results']]) self.assertTrue(host3.pk in [x['id'] for x in data['results']]) # addition - url = reverse('main:host_detail', args=(host2.pk,)) + url = reverse('api:host_detail', args=(host2.pk,)) got = self.get(url, expect=200, auth=self.get_normal_credentials()) self.assertEquals(got['id'], host2.pk) posted = self.post(url1, data=got, expect=204, auth=self.get_normal_credentials()) @@ -596,15 +596,15 @@ class InventoryTest(BaseTest): g2.save() # a super user can set subgroups - subgroups_url = reverse('main:group_children_list', + subgroups_url = reverse('api:group_children_list', args=(Group.objects.get(name='web2').pk,)) - child_url = reverse('main:group_detail', + child_url = reverse('api:group_detail', args=(Group.objects.get(name='web4').pk,)) - subgroups_url2 = reverse('main:group_children_list', + subgroups_url2 = reverse('api:group_children_list', args=(Group.objects.get(name='web6').pk,)) - subgroups_url3 = reverse('main:group_children_list', + subgroups_url3 = reverse('api:group_children_list', args=(Group.objects.get(name='web100').pk,)) - subgroups_url4 = reverse('main:group_children_list', + subgroups_url4 = reverse('api:group_children_list', args=(Group.objects.get(name='web101').pk,)) got = self.get(child_url, expect=200, auth=self.get_super_credentials()) self.post(subgroups_url, data=got, expect=204, auth=self.get_super_credentials()) @@ -625,7 +625,7 @@ class InventoryTest(BaseTest): # a group can't be it's own grandparent subsub = posted2['related']['children'] # this is the grandparent - original_url = reverse('main:group_detail', args=(Group.objects.get(name='web6').pk,)) + original_url = reverse('api:group_detail', args=(Group.objects.get(name='web6').pk,)) parent_data = self.get(original_url, expect=200, auth=self.get_super_credentials()) # now posting to kid's children collection... self.post(subsub, data=parent_data, expect=403, auth=self.get_super_credentials()) @@ -648,7 +648,7 @@ class InventoryTest(BaseTest): # slight detour # can see all hosts under a group, even if it has subgroups # this URL is NOT postable - all_hosts = reverse('main:group_all_hosts_list', + all_hosts = reverse('api:group_all_hosts_list', args=(Group.objects.get(name='web2').pk,)) self.assertEqual(Group.objects.get(name='web2').hosts.count(), 3) data = self.get(all_hosts, expect=200, auth=self.get_normal_credentials()) @@ -669,7 +669,7 @@ class InventoryTest(BaseTest): self.assertTrue(removed_group.parents.count()) self.assertTrue(removed_group.active) for parent in removed_group.parents.all(): - parent_children_url = reverse('main:group_children_list', args=(parent.pk,)) + parent_children_url = reverse('api:group_children_list', args=(parent.pk,)) data = {'id': removed_group.pk, 'disassociate': 1} self.post(parent_children_url, data, expect=204, auth=self.get_super_credentials()) removed_group = Group.objects.get(pk=result['id']) @@ -678,7 +678,7 @@ class InventoryTest(BaseTest): # Removing a group from a hierarchy should migrate its children to the # parent. The group itself will be deleted (marked inactive), and all # relationships removed. - url = reverse('main:group_children_list', args=(gx2.pk,)) + url = reverse('api:group_children_list', args=(gx2.pk,)) data = { 'id': gx3.pk, 'disassociate': 1, @@ -691,7 +691,7 @@ class InventoryTest(BaseTest): #self.assertTrue(gx4 in gx2.children.all()) # Try with invalid hostnames and invalid IPs. - hosts = reverse('main:host_list') + hosts = reverse('api:host_list') invalid_expect = 400 # hostname validation is disabled for now. data = dict(name='', inventory=inv.pk) with self.current_user(self.super_django_user): @@ -752,7 +752,7 @@ class InventoryTest(BaseTest): h_d.groups.add(g_d) # Old, slow 1.2 way. - url = reverse('main:inventory_script_view', args=(i_a.pk,)) + url = reverse('api:inventory_script_view', args=(i_a.pk,)) with self.current_user(self.super_django_user): response = self.get(url, expect=200) self.assertTrue('all' in response) @@ -772,7 +772,7 @@ class InventoryTest(BaseTest): self.assertEqual(response, h.variables_dict) # New 1.3 way. - url = reverse('main:inventory_script_view', args=(i_a.pk,)) + url = reverse('api:inventory_script_view', args=(i_a.pk,)) url = '%s?hostvars=1' % url with self.current_user(self.super_django_user): response = self.get(url, expect=200) @@ -792,7 +792,7 @@ class InventoryTest(BaseTest): g_d = self.inventory_a.groups.create(name='D') g_d.parents.add(g_c) - url = reverse('main:inventory_tree_view', args=(self.inventory_a.pk,)) + url = reverse('api:inventory_tree_view', args=(self.inventory_a.pk,)) with self.current_user(self.super_django_user): response = self.get(url, expect=200) diff --git a/awx/main/tests/jobs.py b/awx/main/tests/jobs.py index 25bb337a1f..36e050033c 100644 --- a/awx/main/tests/jobs.py +++ b/awx/main/tests/jobs.py @@ -436,7 +436,7 @@ class JobTemplateTest(BaseJobTestMixin, django.test.TestCase): 'job_tags', 'host_config_key',) def test_get_job_template_list(self): - url = reverse('main:job_template_list') + url = reverse('api:job_template_list') qs = JobTemplate.objects.distinct() fields = self.JOB_TEMPLATE_FIELDS @@ -474,7 +474,7 @@ class JobTemplateTest(BaseJobTestMixin, django.test.TestCase): # FIXME: Check with other credentials. def test_post_job_template_list(self): - url = reverse('main:job_template_list') + url = reverse('api:job_template_list') data = dict( name = 'new job template', job_type = PERM_INVENTORY_DEPLOY, @@ -489,7 +489,7 @@ class JobTemplateTest(BaseJobTestMixin, django.test.TestCase): # sue can always add job templates. with self.current_user(self.user_sue): response = self.post(url, data, expect=201) - detail_url = reverse('main:job_template_detail', + detail_url = reverse('api:job_template_detail', args=(response['id'],)) self.assertEquals(response['url'], detail_url) @@ -530,7 +530,7 @@ class JobTemplateTest(BaseJobTestMixin, django.test.TestCase): def test_get_job_template_detail(self): jt = self.jt_eng_run - url = reverse('main:job_template_detail', args=(jt.pk,)) + url = reverse('api:job_template_detail', args=(jt.pk,)) # Test with no auth and with invalid login. self.check_invalid_auth(url) @@ -551,7 +551,7 @@ class JobTemplateTest(BaseJobTestMixin, django.test.TestCase): def test_put_job_template_detail(self): jt = self.jt_eng_run - url = reverse('main:job_template_detail', args=(jt.pk,)) + url = reverse('api:job_template_detail', args=(jt.pk,)) # Test with no auth and with invalid login. self.check_invalid_auth(url, methods=('put',))# 'patch')) @@ -568,7 +568,7 @@ class JobTemplateTest(BaseJobTestMixin, django.test.TestCase): def test_get_job_template_job_list(self): jt = self.jt_eng_run - url = reverse('main:job_template_jobs_list', args=(jt.pk,)) + url = reverse('api:job_template_jobs_list', args=(jt.pk,)) # Test with no auth and with invalid login. self.check_invalid_auth(url) @@ -586,7 +586,7 @@ class JobTemplateTest(BaseJobTestMixin, django.test.TestCase): def test_post_job_template_job_list(self): jt = self.jt_eng_run - url = reverse('main:job_template_jobs_list', args=(jt.pk,)) + url = reverse('api:job_template_jobs_list', args=(jt.pk,)) data = dict( name='new job from template', credential=self.cred_bob.pk, @@ -604,7 +604,7 @@ class JobTemplateTest(BaseJobTestMixin, django.test.TestCase): class JobTest(BaseJobTestMixin, django.test.TestCase): def test_get_job_list(self): - url = reverse('main:job_list') + url = reverse('api:job_list') # Test with no auth and with invalid login. self.check_invalid_auth(url) @@ -622,7 +622,7 @@ class JobTest(BaseJobTestMixin, django.test.TestCase): # FIXME: Check with other credentials. def test_post_job_list(self): - url = reverse('main:job_list') + url = reverse('api:job_list') data = dict( name='new job without template', job_type=PERM_INVENTORY_DEPLOY, @@ -652,7 +652,7 @@ class JobTest(BaseJobTestMixin, django.test.TestCase): def test_get_job_detail(self): job = self.job_ops_east_run - url = reverse('main:job_detail', args=(job.pk,)) + url = reverse('api:job_detail', args=(job.pk,)) # Test with no auth and with invalid login. self.check_invalid_auth(url) @@ -668,7 +668,7 @@ class JobTest(BaseJobTestMixin, django.test.TestCase): def test_put_job_detail(self): job = self.job_ops_west_run - url = reverse('main:job_detail', args=(job.pk,)) + url = reverse('api:job_detail', args=(job.pk,)) # Test with no auth and with invalid login. self.check_invalid_auth(url, methods=('put',))# 'patch')) @@ -697,7 +697,7 @@ class JobTest(BaseJobTestMixin, django.test.TestCase): # FIXME: Check with other credentials and readonly fields. def _test_mainline(self): - url = reverse('main:job_list') + url = reverse('api:job_list') # job templates data = self.get('/api/v1/job_templates/', expect=401) @@ -769,7 +769,7 @@ class JobStartCancelTest(BaseJobTestMixin, django.test.LiveServerTestCase): def test_job_start(self): job = self.job_ops_east_run - url = reverse('main:job_start', args=(job.pk,)) + url = reverse('api:job_start', args=(job.pk,)) # Test with no auth and with invalid login. self.check_invalid_auth(url) @@ -797,7 +797,7 @@ class JobStartCancelTest(BaseJobTestMixin, django.test.LiveServerTestCase): # Test with a job that prompts for SSH and sudo passwords. job = self.job_sup_run - url = reverse('main:job_start', args=(job.pk,)) + url = reverse('api:job_start', args=(job.pk,)) with self.current_user(self.user_sue): response = self.get(url) self.assertTrue(response['can_start']) @@ -821,7 +821,7 @@ class JobStartCancelTest(BaseJobTestMixin, django.test.LiveServerTestCase): credential=self.cred_greg, created_by=self.user_sue, ) - url = reverse('main:job_start', args=(job.pk,)) + url = reverse('api:job_start', args=(job.pk,)) with self.current_user(self.user_sue): response = self.get(url) self.assertTrue(response['can_start']) @@ -841,7 +841,7 @@ class JobStartCancelTest(BaseJobTestMixin, django.test.LiveServerTestCase): credential=self.cred_greg, created_by=self.user_sue, ) - url = reverse('main:job_start', args=(job.pk,)) + url = reverse('api:job_start', args=(job.pk,)) with self.current_user(self.user_sue): response = self.get(url) self.assertTrue(response['can_start']) @@ -858,7 +858,7 @@ class JobStartCancelTest(BaseJobTestMixin, django.test.LiveServerTestCase): def test_job_cancel(self): job = self.job_ops_east_run - url = reverse('main:job_cancel', args=(job.pk,)) + url = reverse('api:job_cancel', args=(job.pk,)) # Test with no auth and with invalid login. self.check_invalid_auth(url) @@ -887,7 +887,7 @@ class JobStartCancelTest(BaseJobTestMixin, django.test.LiveServerTestCase): job.start() # Check that the job detail has been updated. - url = reverse('main:job_detail', args=(job.pk,)) + url = reverse('api:job_detail', args=(job.pk,)) with self.current_user(self.user_sue): response = self.get(url) self.assertEqual(response['status'], 'successful', @@ -895,7 +895,7 @@ class JobStartCancelTest(BaseJobTestMixin, django.test.LiveServerTestCase): self.assertTrue(response['result_stdout']) # Test job events for completed job. - url = reverse('main:job_job_events_list', args=(job.pk,)) + url = reverse('api:job_job_events_list', args=(job.pk,)) with self.current_user(self.user_sue): response = self.get(url) qs = job.job_events.all() @@ -908,13 +908,13 @@ class JobStartCancelTest(BaseJobTestMixin, django.test.LiveServerTestCase): for job_event in job.job_events.all(): if job_event.host: host_ids.add(job_event.host.pk) - url = reverse('main:job_event_detail', args=(job_event.pk,)) + url = reverse('api:job_event_detail', args=(job_event.pk,)) with self.current_user(self.user_sue): response = self.get(url) # Also test job event list for each host. for host in Host.objects.filter(pk__in=host_ids): - url = reverse('main:host_job_events_list', args=(host.pk,)) + url = reverse('api:host_job_events_list', args=(host.pk,)) with self.current_user(self.user_sue): response = self.get(url) qs = host.job_events.all() @@ -924,7 +924,7 @@ class JobStartCancelTest(BaseJobTestMixin, django.test.LiveServerTestCase): # Test job event list for groups. for group in self.inv_ops_east.groups.all(): - url = reverse('main:group_job_events_list', args=(group.pk,)) + url = reverse('api:group_job_events_list', args=(group.pk,)) with self.current_user(self.user_sue): response = self.get(url) qs = group.job_events.all() @@ -933,7 +933,7 @@ class JobStartCancelTest(BaseJobTestMixin, django.test.LiveServerTestCase): self.check_list_ids(response, qs) # Test global job event list. - url = reverse('main:job_event_list') + url = reverse('api:job_event_list') with self.current_user(self.user_sue): response = self.get(url) qs = JobEvent.objects.all() @@ -942,7 +942,7 @@ class JobStartCancelTest(BaseJobTestMixin, django.test.LiveServerTestCase): self.check_list_ids(response, qs) # Test job host summaries for completed job. - url = reverse('main:job_job_host_summaries_list', args=(job.pk,)) + url = reverse('api:job_job_host_summaries_list', args=(job.pk,)) with self.current_user(self.user_sue): response = self.get(url) qs = job.job_host_summaries.all() @@ -956,14 +956,14 @@ class JobStartCancelTest(BaseJobTestMixin, django.test.LiveServerTestCase): # Test individual job host summary records. for job_host_summary in job.job_host_summaries.all(): - url = reverse('main:job_host_summary_detail', + url = reverse('api:job_host_summary_detail', args=(job_host_summary.pk,)) with self.current_user(self.user_sue): response = self.get(url) # Test job host summaries for each host. for host in Host.objects.filter(pk__in=host_ids): - url = reverse('main:host_job_host_summaries_list', args=(host.pk,)) + url = reverse('api:host_job_host_summaries_list', args=(host.pk,)) with self.current_user(self.user_sue): response = self.get(url) qs = host.job_host_summaries.all() @@ -973,7 +973,7 @@ class JobStartCancelTest(BaseJobTestMixin, django.test.LiveServerTestCase): # Test job host summaries for groups. for group in self.inv_ops_east.groups.all(): - url = reverse('main:group_job_host_summaries_list', args=(group.pk,)) + url = reverse('api:group_job_host_summaries_list', args=(group.pk,)) with self.current_user(self.user_sue): response = self.get(url) qs = group.job_host_summaries.all() @@ -1115,7 +1115,7 @@ class JobTemplateCallbackTest(BaseJobTestMixin, django.test.LiveServerTestCase): job_template = jt break self.assertTrue(job_template) - url = reverse('main:job_template_callback', args=(job_template.pk,)) + url = reverse('api:job_template_callback', args=(job_template.pk,)) data = dict(host_config_key=job_template.host_config_key) # Test a POST to start a new job. @@ -1254,7 +1254,7 @@ class JobTemplateCallbackTest(BaseJobTestMixin, django.test.LiveServerTestCase): job_template = jt break self.assertTrue(job_template) - url = reverse('main:job_template_callback', args=(job_template.pk,)) + url = reverse('api:job_template_callback', args=(job_template.pk,)) data = dict(host_config_key=job_template.host_config_key) # Should get an error when multiple hosts match to the same IP. @@ -1278,7 +1278,7 @@ class JobTemplateCallbackTest(BaseJobTestMixin, django.test.LiveServerTestCase): job_template = jt break self.assertTrue(job_template) - url = reverse('main:job_template_callback', args=(job_template.pk,)) + url = reverse('api:job_template_callback', args=(job_template.pk,)) data = dict(host_config_key=job_template.host_config_key) # Test POST to start a new job when the template has no credential. @@ -1300,7 +1300,7 @@ class JobTemplateCallbackTest(BaseJobTestMixin, django.test.LiveServerTestCase): job_template = jt break self.assertTrue(job_template) - url = reverse('main:job_template_callback', args=(job_template.pk,)) + url = reverse('api:job_template_callback', args=(job_template.pk,)) data = dict(host_config_key=job_template.host_config_key) # Test POST to start a new job when the credential would require user diff --git a/awx/main/tests/organizations.py b/awx/main/tests/organizations.py index c90763c717..2f12715b13 100644 --- a/awx/main/tests/organizations.py +++ b/awx/main/tests/organizations.py @@ -14,7 +14,7 @@ from awx.main.tests.base import BaseTest class OrganizationsTest(BaseTest): def collection(self): - return reverse('main:organization_list') + return reverse('api:organization_list') def setUp(self): super(OrganizationsTest, self).setUp() @@ -53,7 +53,7 @@ class OrganizationsTest(BaseTest): self.organizations[1].admins.add(self.normal_django_user) def test_get_organization_list(self): - url = reverse('main:organization_list') + url = reverse('api:organization_list') # no credentials == 401 self.options(url, expect=401) @@ -280,7 +280,7 @@ class OrganizationsTest(BaseTest): def test_post_item_subobjects_users(self): - url = reverse('main:organization_users_list', args=(self.organizations[1].pk,)) + url = reverse('api:organization_users_list', args=(self.organizations[1].pk,)) users = self.get(url, expect=200, auth=self.get_normal_credentials()) self.assertEqual(users['count'], 2) self.post(url, dict(id=self.normal_django_user.pk), expect=204, auth=self.get_normal_credentials()) @@ -293,7 +293,7 @@ class OrganizationsTest(BaseTest): # post a completely new user to verify we can add users to the subcollection directly new_user = dict(username='NewUser9000') which_org = self.normal_django_user.admin_of_organizations.all()[0] - url = reverse('main:organization_users_list', args=(which_org.pk,)) + url = reverse('api:organization_users_list', args=(which_org.pk,)) posted = self.post(url, new_user, expect=201, auth=self.get_normal_credentials()) all_users = self.get(url, expect=200, auth=self.get_normal_credentials()) @@ -301,7 +301,7 @@ class OrganizationsTest(BaseTest): def test_post_item_subobjects_admins(self): - url = reverse('main:organization_admins_list', args=(self.organizations[1].pk,)) + url = reverse('api:organization_admins_list', args=(self.organizations[1].pk,)) admins = self.get(url, expect=200, auth=self.get_normal_credentials()) self.assertEqual(admins['count'], 2) self.post(url, dict(id=self.other_django_user.pk), expect=204, auth=self.get_normal_credentials()) @@ -395,7 +395,7 @@ class OrganizationsTest(BaseTest): self.delete(self.collection(), expect=405, auth=self.get_super_credentials()) def test_invalid_post_data(self): - url = reverse('main:organization_list') + url = reverse('api:organization_list') # API should gracefully handle data of an invalid type. self.post(url, expect=400, data=None, auth=self.get_super_credentials()) self.post(url, expect=400, data=99, auth=self.get_super_credentials()) @@ -403,7 +403,7 @@ class OrganizationsTest(BaseTest): self.post(url, expect=400, data=3.14, auth=self.get_super_credentials()) self.post(url, expect=400, data=True, auth=self.get_super_credentials()) self.post(url, expect=400, data=[1,2,3], auth=self.get_super_credentials()) - url = reverse('main:organization_users_list', args=(self.organizations[0].pk,)) + url = reverse('api:organization_users_list', args=(self.organizations[0].pk,)) self.post(url, expect=400, data=None, auth=self.get_super_credentials()) self.post(url, expect=400, data=99, auth=self.get_super_credentials()) self.post(url, expect=400, data='abcd', auth=self.get_super_credentials()) diff --git a/awx/main/tests/projects.py b/awx/main/tests/projects.py index 8305fc43f6..1b4af761ac 100644 --- a/awx/main/tests/projects.py +++ b/awx/main/tests/projects.py @@ -38,7 +38,7 @@ class ProjectsTest(BaseTest): # tests for users, projects, and teams def collection(self): - return reverse('main:project_list') + return reverse('api:project_list') def setUp(self): super(ProjectsTest, self).setUp() @@ -148,7 +148,7 @@ class ProjectsTest(BaseTest): def test_api_config(self): # superuser can read all config data. - url = reverse('main:api_v1_config_view') + url = reverse('api:api_v1_config_view') response = self.get(url, expect=200, auth=self.get_super_credentials()) self.assertTrue('project_base_dir' in response) self.assertEqual(response['project_base_dir'], settings.PROJECTS_ROOT) @@ -188,7 +188,7 @@ class ProjectsTest(BaseTest): # PROJECTS - LISTING # can get projects list - projects = reverse('main:project_list') + projects = reverse('api:project_list') # invalid auth self.get(projects, expect=401) self.get(projects, expect=401, auth=self.get_invalid_credentials()) @@ -217,7 +217,7 @@ class ProjectsTest(BaseTest): auth=self.get_super_credentials()) # can edit project using same local path. - project_detail = reverse('main:project_detail', args=(response['id'],)) + project_detail = reverse('api:project_detail', args=(response['id'],)) project_data = self.get(project_detail, expect=200, auth=self.get_super_credentials()) response = self.put(project_detail, project_data, expect=200, @@ -235,7 +235,7 @@ class ProjectsTest(BaseTest): # ===================================================================== # PROJECTS - ACCESS - project = reverse('main:project_detail', args=(self.projects[3].pk,)) + project = reverse('api:project_detail', args=(self.projects[3].pk,)) self.get(project, expect=200, auth=self.get_super_credentials()) self.get(project, expect=200, auth=self.get_normal_credentials()) self.get(project, expect=200, auth=self.get_other_credentials()) @@ -246,18 +246,18 @@ class ProjectsTest(BaseTest): self.get(project, expect=404, auth=self.get_normal_credentials()) # can list playbooks for projects - proj_playbooks = reverse('main:project_playbooks', args=(self.projects[2].pk,)) + proj_playbooks = reverse('api:project_playbooks', args=(self.projects[2].pk,)) got = self.get(proj_playbooks, expect=200, auth=self.get_super_credentials()) self.assertEqual(got, self.projects[2].playbooks) # can list member organizations for projects - proj_orgs = reverse('main:project_organizations_list', args=(self.projects[0].pk,)) + proj_orgs = reverse('api:project_organizations_list', args=(self.projects[0].pk,)) # only usable as superuser got = self.get(proj_orgs, expect=200, auth=self.get_normal_credentials()) got = self.get(proj_orgs, expect=200, auth=self.get_super_credentials()) self.get(proj_orgs, expect=403, auth=self.get_other_credentials()) self.assertEquals(got['count'], 1) - self.assertEquals(got['results'][0]['url'], reverse('main:organization_detail', args=(self.organizations[0].pk,))) + self.assertEquals(got['results'][0]['url'], reverse('api:organization_detail', args=(self.organizations[0].pk,))) # post to create new org associated with this project. self.post(proj_orgs, data={'name': 'New Org'}, expect=201, auth=self.get_super_credentials()) @@ -267,8 +267,8 @@ class ProjectsTest(BaseTest): # ===================================================================== # TEAMS - all_teams = reverse('main:team_list') - team1 = reverse('main:team_detail', args=(self.team1.pk,)) + all_teams = reverse('api:team_list') + team1 = reverse('api:team_detail', args=(self.team1.pk,)) # can list teams got = self.get(all_teams, expect=200, auth=self.get_super_credentials()) @@ -277,7 +277,7 @@ class ProjectsTest(BaseTest): # can get teams got = self.get(team1, expect=200, auth=self.get_super_credentials()) - self.assertEquals(got['url'], reverse('main:team_detail', args=(self.team1.pk,))) + self.assertEquals(got['url'], reverse('api:team_detail', args=(self.team1.pk,))) got = self.get(team1, expect=200, auth=self.get_normal_credentials()) got = self.get(team1, expect=403, auth=self.get_other_credentials()) self.team1.users.add(User.objects.get(username='other')) @@ -303,7 +303,7 @@ class ProjectsTest(BaseTest): url5 = posted1['url'] new_team = Team.objects.create(name='newTeam4', organization=self.organizations[1]) - url = reverse('main:team_detail', args=(new_team.pk,)) + url = reverse('api:team_detail', args=(new_team.pk,)) # can delete teams self.delete(url, expect=401) @@ -316,7 +316,7 @@ class ProjectsTest(BaseTest): # ORGANIZATION TEAMS # can list organization teams (filtered by user) -- this is an org admin function - org_teams = reverse('main:organization_teams_list', args=(self.organizations[1].pk,)) + org_teams = reverse('api:organization_teams_list', args=(self.organizations[1].pk,)) data1 = self.get(org_teams, expect=401) data2 = self.get(org_teams, expect=403, auth=self.get_nobody_credentials()) data3 = self.get(org_teams, expect=403, auth=self.get_other_credentials()) @@ -346,7 +346,7 @@ class ProjectsTest(BaseTest): # TEAM PROJECTS team = Team.objects.filter(active=True, organization__pk=self.organizations[1].pk)[0] - team_projects = reverse('main:team_projects_list', args=(team.pk,)) + team_projects = reverse('api:team_projects_list', args=(team.pk,)) p1 = self.projects[0] team.projects.add(p1) @@ -363,7 +363,7 @@ class ProjectsTest(BaseTest): # TEAMS USER MEMBERSHIP team = Team.objects.filter(active=True, organization__pk=self.organizations[1].pk)[0] - team_users = reverse('main:team_users_list', args=(team.pk,)) + team_users = reverse('api:team_users_list', args=(team.pk,)) for x in team.users.all(): team.users.remove(x) team.save() @@ -377,7 +377,7 @@ class ProjectsTest(BaseTest): self.get(team_users, expect=200, auth=self.get_super_credentials()) # can add users to teams (but only users I can see) - all_users = self.get(reverse('main:user_list'), expect=200, auth=self.get_normal_credentials()) + all_users = self.get(reverse('api:user_list'), expect=200, auth=self.get_normal_credentials()) for x in all_users['results']: self.post(team_users, data=x, expect=403, auth=self.get_nobody_credentials()) self.post(team_users, data=x, expect=204, auth=self.get_normal_credentials()) @@ -397,7 +397,7 @@ class ProjectsTest(BaseTest): # from a user, can see what teams they are on (related resource) other = User.objects.get(username = 'other') - url = reverse('main:user_teams_list', args=(other.pk,)) + url = reverse('api:user_teams_list', args=(other.pk,)) self.get(url, expect=401) self.get(url, expect=401, auth=self.get_invalid_credentials()) self.get(url, expect=403, auth=self.get_nobody_credentials()) @@ -413,7 +413,7 @@ class ProjectsTest(BaseTest): # ===================================================================== # USER PROJECTS - url = reverse('main:user_projects_list', args=(other.pk,)) + url = reverse('api:user_projects_list', args=(other.pk,)) # from a user, can see what projects they can see based on team association # though this resource doesn't do anything else @@ -427,8 +427,8 @@ class ProjectsTest(BaseTest): # ===================================================================== # CREDENTIALS - other_creds = reverse('main:user_credentials_list', args=(other.pk,)) - team_creds = reverse('main:team_credentials_list', args=(team.pk,)) + other_creds = reverse('api:user_credentials_list', args=(other.pk,)) + team_creds = reverse('api:team_credentials_list', args=(team.pk,)) new_credentials = dict( name = 'credential', @@ -476,7 +476,7 @@ class ProjectsTest(BaseTest): self.get(team_creds, expect=403, auth=self.get_nobody_credentials()) # Check /api/v1/credentials (GET) - url = reverse('main:credential_list') + url = reverse('api:credential_list') with self.current_user(self.super_django_user): self.options(url) self.head(url) @@ -498,8 +498,8 @@ class ProjectsTest(BaseTest): d_cred_user = dict(id=cred_user.pk, name='x', sudo_password='blippy', user=cred_user.user.pk) d_cred_user2 = dict(id=cred_user.pk, name='x', sudo_password='blippy', user=self.super_django_user.pk) d_cred_team = dict(id=cred_team.pk, name='x', sudo_password='blippy', team=cred_team.team.pk) - edit_creds1 = reverse('main:credential_detail', args=(cred_user.pk,)) - edit_creds2 = reverse('main:credential_detail', args=(cred_team.pk,)) + edit_creds1 = reverse('api:credential_detail', args=(cred_user.pk,)) + edit_creds2 = reverse('api:credential_detail', args=(cred_team.pk,)) self.put(edit_creds1, data=d_cred_user, expect=401) self.put(edit_creds1, data=d_cred_user, expect=401, auth=self.get_invalid_credentials()) @@ -517,13 +517,13 @@ class ProjectsTest(BaseTest): self.put(edit_creds2, data=d_cred_team, expect=403, auth=self.get_other_credentials()) cred_put_t['disassociate'] = 1 - team_url = reverse('main:team_credentials_list', args=(cred_put_t['team'],)) + team_url = reverse('api:team_credentials_list', args=(cred_put_t['team'],)) self.post(team_url, data=cred_put_t, expect=204, auth=self.get_normal_credentials()) # can remove credentials from a user (via disassociate) - this will delete the credential. cred_put_u['disassociate'] = 1 url = cred_put_u['url'] - user_url = reverse('main:user_credentials_list', args=(cred_put_u['user'],)) + user_url = reverse('api:user_credentials_list', args=(cred_put_u['user'],)) self.post(user_url, data=cred_put_u, expect=204, auth=self.get_normal_credentials()) # can delete a credential directly -- probably won't be used too often @@ -560,13 +560,13 @@ class ProjectsTest(BaseTest): permission_type=PERM_INVENTORY_DEPLOY ) - url = reverse('main:user_permissions_list', args=(user.pk,)) + url = reverse('api:user_permissions_list', args=(user.pk,)) posted = self.post(url, user_permission, expect=201, auth=self.get_super_credentials()) url2 = posted['url'] got = self.get(url2, expect=200, auth=self.get_other_credentials()) # cannot add permissions that apply to both team and user - url = reverse('main:user_permissions_list', args=(user.pk,)) + url = reverse('api:user_permissions_list', args=(user.pk,)) user_permission['name'] = 'user permission 2' user_permission['team'] = team.pk self.post(url, user_permission, expect=400, auth=self.get_super_credentials()) @@ -584,26 +584,26 @@ class ProjectsTest(BaseTest): self.post(url, user_permission, expect=400, auth=self.get_super_credentials()) # can add permissions on a team - url = reverse('main:team_permissions_list', args=(team.pk,)) + url = reverse('api:team_permissions_list', args=(team.pk,)) posted = self.post(url, team_permission, expect=201, auth=self.get_super_credentials()) url2 = posted['url'] # check we can get that permission back got = self.get(url2, expect=200, auth=self.get_other_credentials()) # cannot add permissions that apply to both team and user - url = reverse('main:team_permissions_list', args=(team.pk,)) + url = reverse('api:team_permissions_list', args=(team.pk,)) team_permission['name'] += '2' team_permission['user'] = user.pk self.post(url, team_permission, expect=400, auth=self.get_super_credentials()) # can list permissions on a user - url = reverse('main:user_permissions_list', args=(user.pk,)) + url = reverse('api:user_permissions_list', args=(user.pk,)) got = self.get(url, expect=200, auth=self.get_super_credentials()) got = self.get(url, expect=200, auth=self.get_other_credentials()) got = self.get(url, expect=403, auth=self.get_nobody_credentials()) # can list permissions on a team - url = reverse('main:team_permissions_list', args=(team.pk,)) + url = reverse('api:team_permissions_list', args=(team.pk,)) got = self.get(url, expect=200, auth=self.get_super_credentials()) got = self.get(url, expect=200, auth=self.get_other_credentials()) got = self.get(url, expect=403, auth=self.get_nobody_credentials()) @@ -1335,7 +1335,7 @@ class ProjectUpdatesTest(BaseTransactionTest): scm_username='nobody', scm_password='ASK', ) - url = reverse('main:project_update_view', args=(project.pk,)) + url = reverse('api:project_update_view', args=(project.pk,)) with self.current_user(self.super_django_user): response = self.get(url, expect=200) self.assertTrue(response['can_update']) @@ -1359,7 +1359,7 @@ class ProjectUpdatesTest(BaseTransactionTest): scm_key_data=TEST_SSH_KEY_DATA_LOCKED, scm_key_unlock='ASK', ) - url = reverse('main:project_update_view', args=(project.pk,)) + url = reverse('api:project_update_view', args=(project.pk,)) with self.current_user(self.super_django_user): response = self.get(url, expect=200) self.assertTrue(response['can_update']) diff --git a/awx/main/tests/users.py b/awx/main/tests/users.py index f34fdf3eff..7eef73f4e9 100644 --- a/awx/main/tests/users.py +++ b/awx/main/tests/users.py @@ -23,7 +23,7 @@ __all__ = ['UsersTest', 'LdapTest'] class UsersTest(BaseTest): def collection(self): - return reverse('main:user_list') + return reverse('api:user_list') def setUp(self): super(UsersTest, self).setUp() @@ -34,7 +34,7 @@ class UsersTest(BaseTest): self.organizations[0].users.add(self.normal_django_user) def test_only_super_user_or_org_admin_can_add_users(self): - url = reverse('main:user_list') + url = reverse('api:user_list') new_user = dict(username='blippy') new_user2 = dict(username='blippy2') self.post(url, expect=401, data=new_user, auth=None) @@ -46,7 +46,7 @@ class UsersTest(BaseTest): self.post(url, expect=400, data=new_user2, auth=self.get_normal_credentials()) def test_auth_token_login(self): - auth_token_url = reverse('main:auth_token_view') + auth_token_url = reverse('api:auth_token_view') # Always returns a 405 for any GET request, regardless of credentials. self.get(auth_token_url, expect=405, auth=None) @@ -69,7 +69,7 @@ class UsersTest(BaseTest): auth_token = response['token'] # Verify we can access our own user information with the auth token. - response = self.get(reverse('main:user_me_list'), expect=200, + response = self.get(reverse('api:user_me_list'), expect=200, auth=auth_token) self.assertEquals(response['results'][0]['username'], 'normal') self.assertEquals(response['count'], 1) @@ -77,7 +77,7 @@ class UsersTest(BaseTest): # If we simulate a different remote address, should not be able to use # the first auth token. remote_addr = '127.0.0.2' - response = self.get(reverse('main:user_me_list'), expect=401, + response = self.get(reverse('api:user_me_list'), expect=401, auth=auth_token, remote_addr=remote_addr) self.assertEqual(response['detail'], 'Invalid token') @@ -97,28 +97,28 @@ class UsersTest(BaseTest): # Verify we can access our own user information with the second auth # token from the other remote address. - response = self.get(reverse('main:user_me_list'), expect=200, + response = self.get(reverse('api:user_me_list'), expect=200, auth=auth_token2, remote_addr=remote_addr) self.assertEquals(response['results'][0]['username'], 'normal') self.assertEquals(response['count'], 1) # The second auth token also can't be used from the first address, but # the first auth token is still valid from its address. - response = self.get(reverse('main:user_me_list'), expect=401, + response = self.get(reverse('api:user_me_list'), expect=401, auth=auth_token2) self.assertEqual(response['detail'], 'Invalid token') response_header = response.response.get('WWW-Authenticate', '') self.assertEqual(response_header.split()[0], 'Token') - response = self.get(reverse('main:user_me_list'), expect=200, + response = self.get(reverse('api:user_me_list'), expect=200, auth=auth_token) # A request without authentication should ask for Basic by default. - response = self.get(reverse('main:user_me_list'), expect=401) + response = self.get(reverse('api:user_me_list'), expect=401) response_header = response.response.get('WWW-Authenticate', '') self.assertEqual(response_header.split()[0], 'Basic') # A request that attempts Basic auth should request Basic auth again. - response = self.get(reverse('main:user_me_list'), expect=401, + response = self.get(reverse('api:user_me_list'), expect=401, auth=('invalid', 'password')) response_header = response.response.get('WWW-Authenticate', '') self.assertEqual(response_header.split()[0], 'Basic') @@ -126,21 +126,21 @@ class UsersTest(BaseTest): # Invalidate a key (simulate expiration), now token auth should fail # with the first token, but still work with the second. self.normal_django_user.auth_tokens.get(key=auth_token).invalidate() - response = self.get(reverse('main:user_me_list'), expect=401, + response = self.get(reverse('api:user_me_list'), expect=401, auth=auth_token) self.assertEqual(response['detail'], 'Token is expired') - response = self.get(reverse('main:user_me_list'), expect=200, + response = self.get(reverse('api:user_me_list'), expect=200, auth=auth_token2, remote_addr=remote_addr) # Token auth should be denied if the user is inactive. self.normal_django_user.mark_inactive() - response = self.get(reverse('main:user_me_list'), expect=401, + response = self.get(reverse('api:user_me_list'), expect=401, auth=auth_token2, remote_addr=remote_addr) self.assertEqual(response['detail'], 'User inactive or deleted') def test_ordinary_user_can_modify_some_fields_about_himself_but_not_all_and_passwords_work(self): - detail_url = reverse('main: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()) # can't change first_name, last_name, etc @@ -182,7 +182,7 @@ class UsersTest(BaseTest): def test_user_created_with_password_can_login(self): # this is something an org admin can do... - url = reverse('main:user_list') + url = reverse('api:user_list') data = dict(username='username', password='password') data2 = dict(username='username2', password='password2') data = self.post(url, expect=201, data=data, auth=self.get_normal_credentials()) @@ -206,16 +206,16 @@ class UsersTest(BaseTest): self.assertTrue(orig.username != 'change') def test_password_not_shown_in_get_operations_for_list_or_detail(self): - url = reverse('main:user_detail', args=(self.super_django_user.pk,)) + url = reverse('api:user_detail', args=(self.super_django_user.pk,)) data = self.get(url, expect=200, auth=self.get_super_credentials()) self.assertTrue('password' not in data) - url = reverse('main:user_list') + url = reverse('api:user_list') data = self.get(url, expect=200, auth=self.get_super_credentials()) self.assertTrue('password' not in data['results'][0]) def test_user_list_filtered(self): - url = reverse('main:user_list') + url = reverse('api:user_list') data3 = self.get(url, expect=200, auth=self.get_super_credentials()) self.assertEquals(data3['count'], 4) data2 = self.get(url, expect=200, auth=self.get_normal_credentials()) @@ -225,22 +225,22 @@ class UsersTest(BaseTest): def test_super_user_can_delete_a_user_but_only_marked_inactive(self): user_pk = self.normal_django_user.pk - url = reverse('main:user_detail', args=(user_pk,)) + url = reverse('api:user_detail', args=(user_pk,)) data = self.delete(url, expect=204, auth=self.get_super_credentials()) data = self.get(url, expect=404, auth=self.get_super_credentials()) obj = User.objects.get(pk=user_pk) self.assertEquals(obj.is_active, False) def test_non_org_admin_user_cannot_delete_any_user_including_himself(self): - url1 = reverse('main:user_detail', args=(self.super_django_user.pk,)) - url2 = reverse('main:user_detail', args=(self.normal_django_user.pk,)) - url3 = reverse('main:user_detail', args=(self.other_django_user.pk,)) + url1 = reverse('api:user_detail', args=(self.super_django_user.pk,)) + url2 = reverse('api:user_detail', args=(self.normal_django_user.pk,)) + url3 = reverse('api:user_detail', args=(self.other_django_user.pk,)) data = self.delete(url1, expect=403, auth=self.get_other_credentials()) data = self.delete(url2, expect=403, auth=self.get_other_credentials()) data = self.delete(url3, expect=403, auth=self.get_other_credentials()) def test_there_exists_an_obvious_url_where_a_user_may_find_his_user_record(self): - url = reverse('main:user_me_list') + url = reverse('api:user_me_list') data = self.get(url, expect=401, auth=None) data = self.get(url, expect=401, auth=self.get_invalid_credentials()) data = self.get(url, expect=200, auth=self.get_normal_credentials()) @@ -254,7 +254,7 @@ class UsersTest(BaseTest): self.assertEquals(data['count'], 1) def test_superuser_can_change_admin_only_fields_about_himself(self): - url = reverse('main:user_detail', args=(self.super_django_user.pk,)) + url = reverse('api:user_detail', args=(self.super_django_user.pk,)) data = self.get(url, expect=200, auth=self.get_super_credentials()) data['username'] += '2' data['first_name'] += ' Awesome' @@ -267,7 +267,7 @@ class UsersTest(BaseTest): def test_user_related_resources(self): # organizations the user is a member of, should be 1 - url = reverse('main:user_organizations_list', + url = reverse('api:user_organizations_list', args=(self.normal_django_user.pk,)) data = self.get(url, expect=200, auth=self.get_normal_credentials()) self.assertEquals(data['count'], 1) @@ -280,7 +280,7 @@ class UsersTest(BaseTest): data = self.get(url, expect=403, auth=self.get_nobody_credentials()) # organizations the user is an admin of, should be 1 - url = reverse('main:user_admin_of_organizations_list', + url = reverse('api:user_admin_of_organizations_list', args=(self.normal_django_user.pk,)) data = self.get(url, expect=200, auth=self.get_normal_credentials()) self.assertEquals(data['count'], 1) @@ -293,7 +293,7 @@ class UsersTest(BaseTest): data = self.get(url, expect=403, auth=self.get_nobody_credentials()) # teams the user is on, should be 0 - url = reverse('main:user_teams_list', args=(self.normal_django_user.pk,)) + url = reverse('api:user_teams_list', args=(self.normal_django_user.pk,)) data = self.get(url, expect=200, auth=self.get_normal_credentials()) self.assertEquals(data['count'], 0) # also accessible via superuser @@ -305,15 +305,15 @@ class UsersTest(BaseTest): data = self.get(url, expect=403, auth=self.get_nobody_credentials()) # verify org admin can still read other user data too - url = reverse('main:user_organizations_list', + url = reverse('api:user_organizations_list', args=(self.other_django_user.pk,)) data = self.get(url, expect=200, auth=self.get_normal_credentials()) self.assertEquals(data['count'], 1) - url = reverse('main:user_admin_of_organizations_list', + url = reverse('api:user_admin_of_organizations_list', args=(self.other_django_user.pk,)) data = self.get(url, expect=200, auth=self.get_normal_credentials()) self.assertEquals(data['count'], 0) - url = reverse('main:user_teams_list', + url = reverse('api:user_teams_list', args=(self.other_django_user.pk,)) data = self.get(url, expect=200, auth=self.get_normal_credentials()) self.assertEquals(data['count'], 0) @@ -323,7 +323,7 @@ class UsersTest(BaseTest): # FIXME: add test that shows posting a projects w/o id to /organizations/2/projects/ can create a new one & associate def test_user_list_ordering(self): - base_url = reverse('main:user_list') + base_url = reverse('api:user_list') base_qs = User.objects.distinct() # Check list view with ordering by name. @@ -351,7 +351,7 @@ class UsersTest(BaseTest): def test_user_list_filtering(self): # Also serves as general-purpose testing for custom API filters. - base_url = reverse('main:user_list') + base_url = reverse('api:user_list') base_qs = User.objects.distinct() # Filter by username. @@ -651,7 +651,7 @@ class UsersTest(BaseTest): self.check_get_list(url, self.super_django_user, base_qs, expect=400) def test_user_list_pagination(self): - base_url = reverse('main:user_list') + base_url = reverse('api:user_list') base_qs = User.objects.distinct() # Check list view with page size of 1. @@ -682,7 +682,7 @@ class UsersTest(BaseTest): # limit=0) def test_user_list_searching(self): - base_url = reverse('main:user_list') + base_url = reverse('api:user_list') base_qs = User.objects.distinct() # Check search query parameter. @@ -826,12 +826,12 @@ class LdapTest(BaseTest): self.use_test_setting(name) user = self.check_login() self.setup_users() - url = reverse('main:api_v1_config_view') + url = reverse('api:api_v1_config_view') with self.current_user(self.super_django_user): response = self.get(url, expect=200) user_ldap_fields = response.get('user_ldap_fields', []) self.assertTrue(user_ldap_fields) - url = reverse('main:user_detail', args=(user.pk,)) + url = reverse('api:user_detail', args=(user.pk,)) for user_field in user_ldap_fields: with self.current_user(self.super_django_user): data = self.get(url, expect=200) diff --git a/awx/main/urls.py b/awx/main/urls.py index 3efc766fcd..e990efb3e7 100644 --- a/awx/main/urls.py +++ b/awx/main/urls.py @@ -1,170 +1,7 @@ # Copyright (c) 2013 AnsibleWorks, Inc. # All Rights Reserved. -from django.conf.urls import include, patterns, url as original_url +from django.conf import settings +from django.conf.urls import * -def url(regex, view, kwargs=None, name=None, prefix=''): - # Set default name from view name (if a string). - if isinstance(view, basestring) and name is None: - name = view - return original_url(regex, view, kwargs, name, prefix) - -organization_urls = patterns('awx.main.views', - url(r'^$', 'organization_list'), - url(r'^(?P[0-9]+)/$', 'organization_detail'), - url(r'^(?P[0-9]+)/users/$', 'organization_users_list'), - url(r'^(?P[0-9]+)/admins/$', 'organization_admins_list'), - url(r'^(?P[0-9]+)/inventories/$', 'organization_inventories_list'), - url(r'^(?P[0-9]+)/projects/$', 'organization_projects_list'), - url(r'^(?P[0-9]+)/teams/$', 'organization_teams_list'), -) - -user_urls = patterns('awx.main.views', - url(r'^$', 'user_list'), - url(r'^(?P[0-9]+)/$', 'user_detail'), - url(r'^(?P[0-9]+)/teams/$', 'user_teams_list'), - url(r'^(?P[0-9]+)/organizations/$', 'user_organizations_list'), - url(r'^(?P[0-9]+)/admin_of_organizations/$', 'user_admin_of_organizations_list'), - url(r'^(?P[0-9]+)/projects/$', 'user_projects_list'), - url(r'^(?P[0-9]+)/credentials/$', 'user_credentials_list'), - url(r'^(?P[0-9]+)/permissions/$', 'user_permissions_list'), -) - -project_urls = patterns('awx.main.views', - url(r'^$', 'project_list'), - url(r'^(?P[0-9]+)/$', 'project_detail'), - url(r'^(?P[0-9]+)/playbooks/$', 'project_playbooks'), - url(r'^(?P[0-9]+)/organizations/$', 'project_organizations_list'), - url(r'^(?P[0-9]+)/teams/$', 'project_teams_list'), - url(r'^(?P[0-9]+)/update/$', 'project_update_view'), - url(r'^(?P[0-9]+)/project_updates/$', 'project_updates_list'), -) - -project_update_urls = patterns('awx.main.views', - url(r'^(?P[0-9]+)/$', 'project_update_detail'), - url(r'^(?P[0-9]+)/cancel/$', 'project_update_cancel'), -) - -team_urls = patterns('awx.main.views', - url(r'^$', 'team_list'), - url(r'^(?P[0-9]+)/$', 'team_detail'), - url(r'^(?P[0-9]+)/projects/$', 'team_projects_list'), - url(r'^(?P[0-9]+)/users/$', 'team_users_list'), - url(r'^(?P[0-9]+)/credentials/$', 'team_credentials_list'), - url(r'^(?P[0-9]+)/permissions/$', 'team_permissions_list'), -) - -inventory_urls = patterns('awx.main.views', - url(r'^$', 'inventory_list'), - url(r'^(?P[0-9]+)/$', 'inventory_detail'), - url(r'^(?P[0-9]+)/hosts/$', 'inventory_hosts_list'), - url(r'^(?P[0-9]+)/groups/$', 'inventory_groups_list'), - url(r'^(?P[0-9]+)/root_groups/$', 'inventory_root_groups_list'), - url(r'^(?P[0-9]+)/variable_data/$', 'inventory_variable_data'), - url(r'^(?P[0-9]+)/script/$', 'inventory_script_view'), - url(r'^(?P[0-9]+)/tree/$', 'inventory_tree_view'), - url(r'^(?P[0-9]+)/inventory_sources/$', 'inventory_inventory_sources_list'), -) - -host_urls = patterns('awx.main.views', - url(r'^$', 'host_list'), - url(r'^(?P[0-9]+)/$', 'host_detail'), - url(r'^(?P[0-9]+)/variable_data/$', 'host_variable_data'), - url(r'^(?P[0-9]+)/groups/$', 'host_groups_list'), - url(r'^(?P[0-9]+)/all_groups/$', 'host_all_groups_list'), - url(r'^(?P[0-9]+)/job_events/', 'host_job_events_list'), - url(r'^(?P[0-9]+)/job_host_summaries/$', 'host_job_host_summaries_list'), - #url(r'^(?P[0-9]+)/inventory_sources/$', 'host_inventory_sources_list'), -) - -group_urls = patterns('awx.main.views', - url(r'^$', 'group_list'), - url(r'^(?P[0-9]+)/$', 'group_detail'), - url(r'^(?P[0-9]+)/children/$', 'group_children_list'), - url(r'^(?P[0-9]+)/hosts/$', 'group_hosts_list'), - url(r'^(?P[0-9]+)/all_hosts/$', 'group_all_hosts_list'), - url(r'^(?P[0-9]+)/variable_data/$', 'group_variable_data'), - url(r'^(?P[0-9]+)/job_events/$', 'group_job_events_list'), - url(r'^(?P[0-9]+)/job_host_summaries/$', 'group_job_host_summaries_list'), - url(r'^(?P[0-9]+)/potential_children/$', 'group_potential_children_list'), - #url(r'^(?P[0-9]+)/inventory_sources/$', 'group_inventory_sources_list'), -) - -inventory_source_urls = patterns('awx.main.views', - url(r'^$', 'inventory_source_list'), - url(r'^(?P[0-9]+)/$', 'inventory_source_detail'), - url(r'^(?P[0-9]+)/update/$', 'inventory_source_update_view'), - url(r'^(?P[0-9]+)/inventory_updates/$', 'inventory_source_updates_list'), - #url(r'^(?P[0-9]+)/groups/$', 'inventory_source_groups_list'), - #url(r'^(?P[0-9]+)/hosts/$', 'inventory_source_hosts_list'), -) - -inventory_update_urls = patterns('awx.main.views', - url(r'^(?P[0-9]+)/$', 'inventory_update_detail'), - url(r'^(?P[0-9]+)/cancel/$', 'inventory_update_cancel'), -) - -credential_urls = patterns('awx.main.views', - url(r'^$', 'credential_list'), - url(r'^(?P[0-9]+)/$', 'credential_detail'), - # See also credentials resources on users/teams. -) - -permission_urls = patterns('awx.main.views', - url(r'^(?P[0-9]+)/$', 'permission_detail'), -) - -job_template_urls = patterns('awx.main.views', - url(r'^$', 'job_template_list'), - url(r'^(?P[0-9]+)/$', 'job_template_detail'), - url(r'^(?P[0-9]+)/jobs/$', 'job_template_jobs_list'), - url(r'^(?P[0-9]+)/callback/$', 'job_template_callback'), -) - -job_urls = patterns('awx.main.views', - url(r'^$', 'job_list'), - url(r'^(?P[0-9]+)/$', 'job_detail'), - url(r'^(?P[0-9]+)/start/$', 'job_start'), - url(r'^(?P[0-9]+)/cancel/$', 'job_cancel'), - url(r'^(?P[0-9]+)/job_host_summaries/$', 'job_job_host_summaries_list'), - url(r'^(?P[0-9]+)/job_events/$', 'job_job_events_list'), -) - -job_host_summary_urls = patterns('awx.main.views', - url(r'^(?P[0-9]+)/$', 'job_host_summary_detail'), -) - -job_event_urls = patterns('awx.main.views', - url(r'^$', 'job_event_list'), - url(r'^(?P[0-9]+)/$', 'job_event_detail'), - url(r'^(?P[0-9]+)/children/$', 'job_event_children_list'), - url(r'^(?P[0-9]+)/hosts/$', 'job_event_hosts_list'), -) - -v1_urls = patterns('awx.main.views', - url(r'^$', 'api_v1_root_view'), - url(r'^config/$', 'api_v1_config_view'), - url(r'^authtoken/$', 'auth_token_view'), - url(r'^me/$', 'user_me_list'), - url(r'^organizations/', include(organization_urls)), - url(r'^users/', include(user_urls)), - url(r'^projects/', include(project_urls)), - url(r'^project_updates/', include(project_update_urls)), - url(r'^teams/', include(team_urls)), - url(r'^inventories/', include(inventory_urls)), - url(r'^hosts/', include(host_urls)), - url(r'^groups/', include(group_urls)), - url(r'^inventory_sources/', include(inventory_source_urls)), - url(r'^inventory_updates/', include(inventory_update_urls)), - url(r'^credentials/', include(credential_urls)), - url(r'^permissions/', include(permission_urls)), - url(r'^job_templates/', include(job_template_urls)), - url(r'^jobs/', include(job_urls)), - url(r'^job_host_summaries/', include(job_host_summary_urls)), - url(r'^job_events/', include(job_event_urls)), -) - -urlpatterns = patterns('awx.main.views', - url(r'^$', 'api_root_view'), - url(r'^v1/', include(v1_urls)), -) +urlpatterns = patterns() diff --git a/awx/main/views.py b/awx/main/views.py index 3524b3e641..ccced20438 100644 --- a/awx/main/views.py +++ b/awx/main/views.py @@ -1,40 +1,9 @@ # Copyright (c) 2013 AnsibleWorks, Inc. # All Rights Reserved. -# Python -import datetime -import re -import socket -import sys - # Django -from django.conf import settings -from django.contrib.auth.models import User -from django.core.urlresolvers import reverse -from django.db.models import Q -from django.shortcuts import get_object_or_404, render_to_response +from django.shortcuts import render_to_response from django.template import RequestContext -from django.utils.datastructures import SortedDict -from django.utils.timezone import now - -# Django REST Framework -from rest_framework.authtoken.views import ObtainAuthToken -from rest_framework.exceptions import PermissionDenied -from rest_framework.parsers import YAMLParser -from rest_framework.permissions import AllowAny, IsAuthenticated -from rest_framework.renderers import YAMLRenderer -from rest_framework.response import Response -from rest_framework.settings import api_settings -from rest_framework import status - -# AWX -from awx.main.authentication import JobTaskAuthentication -from awx.main.licenses import LicenseReader -from awx.main.base_views import * -from awx.main.models import * -from awx.main.permissions import * -from awx.main.serializers import * -from awx.main.utils import * def handle_error(request, status=404): context = {} @@ -54,1025 +23,3 @@ def handle_404(request): def handle_500(request): return handle_error(request, 500) - -class ApiRootView(APIView): - - permission_classes = (AllowAny,) - view_name = 'REST API' - - def get(self, request, format=None): - ''' list supported API versions ''' - - current = reverse('main:api_v1_root_view', args=[]) - data = dict( - description = 'AWX REST API', - current_version = current, - available_versions = dict( - v1 = current - ) - ) - return Response(data) - -class ApiV1RootView(APIView): - - permission_classes = (AllowAny,) - view_name = 'Version 1' - - def get(self, request, format=None): - ''' list top level resources ''' - - data = SortedDict() - data['authtoken'] = reverse('main:auth_token_view') - data['config'] = reverse('main:api_v1_config_view') - data['me'] = reverse('main:user_me_list') - data['organizations'] = reverse('main:organization_list') - data['users'] = reverse('main:user_list') - data['projects'] = reverse('main:project_list') - data['teams'] = reverse('main:team_list') - data['credentials'] = reverse('main:credential_list') - data['inventory'] = reverse('main:inventory_list') - data['inventory_sources'] = reverse('main:inventory_source_list') - data['groups'] = reverse('main:group_list') - data['hosts'] = reverse('main:host_list') - data['job_templates'] = reverse('main:job_template_list') - data['jobs'] = reverse('main:job_list') - return Response(data) - -class ApiV1ConfigView(APIView): - - permission_classes = (IsAuthenticated,) - view_name = 'Configuration' - - def get(self, request, format=None): - '''Return various sitewide configuration settings.''' - - license_reader = LicenseReader() - license_data = license_reader.from_file() - - data = dict( - time_zone=settings.TIME_ZONE, - license_info=license_data, - version=get_awx_version(), - ansible_version=get_ansible_version(), - ) - - # If LDAP is enabled, user_ldap_fields will return a list of field - # names that are managed by LDAP and should be read-only for users with - # a non-empty ldap_dn attribute. - if getattr(settings, 'AUTH_LDAP_SERVER_URI', None): - user_ldap_fields = ['username', 'password'] - user_ldap_fields.extend(getattr(settings, 'AUTH_LDAP_USER_ATTR_MAP', {}).keys()) - user_ldap_fields.extend(getattr(settings, 'AUTH_LDAP_USER_FLAGS_BY_GROUP', {}).keys()) - data['user_ldap_fields'] = user_ldap_fields - - if request.user.is_superuser or request.user.admin_of_organizations.filter(active=True).count(): - data.update(dict( - project_base_dir = settings.PROJECTS_ROOT, - project_local_paths = Project.get_local_path_choices(), - )) - - return Response(data) - -class AuthTokenView(APIView): - - permission_classes = (AllowAny,) - serializer_class = AuthTokenSerializer - model = AuthToken - - def post(self, request): - serializer = self.serializer_class(data=request.DATA) - if serializer.is_valid(): - request_hash = AuthToken.get_request_hash(self.request) - try: - token = AuthToken.objects.filter(user=serializer.object['user'], - request_hash=request_hash, - expires__gt=now())[0] - token.refresh() - except IndexError: - token = AuthToken.objects.create(user=serializer.object['user'], - request_hash=request_hash) - return Response({'token': token.key, 'expires': token.expires}) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - -class OrganizationList(ListCreateAPIView): - - model = Organization - serializer_class = OrganizationSerializer - -class OrganizationDetail(RetrieveUpdateDestroyAPIView): - - model = Organization - serializer_class = OrganizationSerializer - -class OrganizationInventoriesList(SubListAPIView): - - model = Inventory - serializer_class = InventorySerializer - parent_model = Organization - relationship = 'inventories' - -class OrganizationUsersList(SubListCreateAPIView): - - model = User - serializer_class = UserSerializer - parent_model = Organization - relationship = 'users' - -class OrganizationAdminsList(SubListCreateAPIView): - - model = User - serializer_class = UserSerializer - parent_model = Organization - relationship = 'admins' - -class OrganizationProjectsList(SubListCreateAPIView): - - model = Project - serializer_class = ProjectSerializer - parent_model = Organization - relationship = 'projects' - -class OrganizationTeamsList(SubListCreateAPIView): - - model = Team - serializer_class = TeamSerializer - parent_model = Organization - relationship = 'teams' - parent_key = 'organization' - -class TeamList(ListCreateAPIView): - - model = Team - serializer_class = TeamSerializer - -class TeamDetail(RetrieveUpdateDestroyAPIView): - - model = Team - serializer_class = TeamSerializer - -class TeamUsersList(SubListCreateAPIView): - - model = User - serializer_class = UserSerializer - parent_model = Team - relationship = 'users' - -class TeamPermissionsList(SubListCreateAPIView): - - model = Permission - serializer_class = PermissionSerializer - parent_model = Team - relationship = 'permissions' - parent_key = 'team' - - def get_queryset(self): - # FIXME: Default get_queryset should handle this. - team = Team.objects.get(pk=self.kwargs['pk']) - base = Permission.objects.filter(team = team) - #if Team.can_user_administrate(self.request.user, team, None): - if self.request.user.can_access(Team, 'change', team, None): - return base - elif team.users.filter(pk=self.request.user.pk).count() > 0: - return base - raise PermissionDenied() - -class TeamProjectsList(SubListCreateAPIView): - - model = Project - serializer_class = ProjectSerializer - parent_model = Team - relationship = 'projects' - -class TeamCredentialsList(SubListCreateAPIView): - - model = Credential - serializer_class = CredentialSerializer - parent_model = Team - relationship = 'credentials' - parent_key = 'team' - -class ProjectList(ListCreateAPIView): - - model = Project - serializer_class = ProjectSerializer - - def get(self, request, *args, **kwargs): - # Not optimal, but make sure the project status and last_updated fields - # are up to date here... - projects_qs = Project.objects.filter(active=True) - projects_qs = projects_qs.select_related('current_update', 'last_updated') - for project in projects_qs: - project.set_status_and_last_updated() - return super(ProjectList, self).get(request, *args, **kwargs) - -class ProjectDetail(RetrieveUpdateDestroyAPIView): - - model = Project - serializer_class = ProjectSerializer - -class ProjectPlaybooks(RetrieveAPIView): - - model = Project - serializer_class = ProjectPlaybooksSerializer - -class ProjectOrganizationsList(SubListCreateAPIView): - - model = Organization - serializer_class = OrganizationSerializer - parent_model = Project - relationship = 'organizations' - -class ProjectTeamsList(SubListCreateAPIView): - - model = Team - serializer_class = TeamSerializer - parent_model = Project - relationship = 'teams' - -class ProjectUpdatesList(SubListAPIView): - - model = ProjectUpdate - serializer_class = ProjectUpdateSerializer - parent_model = Project - relationship = 'project_updates' - new_in_13 = True - -class ProjectUpdateView(GenericAPIView): - - model = Project - new_in_13 = True - - def get(self, request, *args, **kwargs): - obj = self.get_object() - data = dict( - can_update=obj.can_update, - ) - if obj.scm_type: - data['passwords_needed_to_update'] = obj.scm_passwords_needed - return Response(data) - - def post(self, request, *args, **kwargs): - obj = self.get_object() - if obj.can_update: - project_update = obj.update(**request.DATA) - if not project_update: - data = dict(passwords_needed_to_update=obj.scm_passwords_needed) - return Response(data, status=status.HTTP_400_BAD_REQUEST) - else: - headers = {'Location': project_update.get_absolute_url()} - return Response(status=status.HTTP_202_ACCEPTED, headers=headers) - else: - return self.http_method_not_allowed(request, *args, **kwargs) - -class ProjectUpdateDetail(RetrieveAPIView): - - model = ProjectUpdate - serializer_class = ProjectUpdateSerializer - new_in_13 = True - -class ProjectUpdateCancel(GenericAPIView): - - model = ProjectUpdate - is_job_cancel = True - new_in_13 = True - - def get(self, request, *args, **kwargs): - obj = self.get_object() - data = dict( - can_cancel=obj.can_cancel, - ) - return Response(data) - - def post(self, request, *args, **kwargs): - obj = self.get_object() - if obj.can_cancel: - result = obj.cancel() - return Response(status=status.HTTP_202_ACCEPTED) - else: - return self.http_method_not_allowed(request, *args, **kwargs) - -class UserList(ListCreateAPIView): - - model = User - serializer_class = UserSerializer - -class UserMeList(ListAPIView): - - model = User - serializer_class = UserSerializer - view_name = 'Me' - - def get_queryset(self): - return self.model.objects.filter(pk=self.request.user.pk) - -class UserTeamsList(SubListAPIView): - - model = Team - serializer_class = TeamSerializer - parent_model = User - relationship = 'teams' - -class UserPermissionsList(SubListCreateAPIView): - - model = Permission - serializer_class = PermissionSerializer - parent_model = User - relationship = 'permissions' - parent_key = 'user' - -class UserProjectsList(SubListAPIView): - - model = Project - serializer_class = ProjectSerializer - parent_model = User - relationship = 'projects' - - def get_queryset(self): - parent = self.get_parent_object() - self.check_parent_access(parent) - qs = self.request.user.get_queryset(self.model) - return qs.filter(teams__in=parent.teams.distinct()) - -class UserCredentialsList(SubListCreateAPIView): - - model = Credential - serializer_class = CredentialSerializer - parent_model = User - relationship = 'credentials' - parent_key = 'user' - -class UserOrganizationsList(SubListAPIView): - - model = Organization - serializer_class = OrganizationSerializer - parent_model = User - relationship = 'organizations' - -class UserAdminOfOrganizationsList(SubListAPIView): - - model = Organization - serializer_class = OrganizationSerializer - parent_model = User - relationship = 'admin_of_organizations' - -class UserDetail(RetrieveUpdateDestroyAPIView): - - model = User - serializer_class = UserSerializer - - def update_filter(self, request, *args, **kwargs): - ''' make sure non-read-only fields that can only be edited by admins, are only edited by admins ''' - obj = User.objects.get(pk=kwargs['pk']) - can_change = request.user.can_access(User, 'change', obj, request.DATA) - can_admin = request.user.can_access(User, 'admin', obj, request.DATA) - if can_change and not can_admin: - admin_only_edit_fields = ('last_name', 'first_name', 'username', - 'is_active', 'is_superuser') - changed = {} - for field in admin_only_edit_fields: - left = getattr(obj, field, None) - right = request.DATA.get(field, None) - if left is not None and right is not None and left != right: - changed[field] = (left, right) - if changed: - raise PermissionDenied('Cannot change %s' % ', '.join(changed.keys())) - -class CredentialList(ListCreateAPIView): - - model = Credential - serializer_class = CredentialSerializer - -class CredentialDetail(RetrieveUpdateDestroyAPIView): - - model = Credential - serializer_class = CredentialSerializer - -class PermissionDetail(RetrieveUpdateDestroyAPIView): - - model = Permission - serializer_class = PermissionSerializer - -class InventoryList(ListCreateAPIView): - - model = Inventory - serializer_class = InventorySerializer - -class InventoryDetail(RetrieveUpdateDestroyAPIView): - - model = Inventory - serializer_class = InventorySerializer - -class HostList(ListCreateAPIView): - - model = Host - serializer_class = HostSerializer - -class HostDetail(RetrieveUpdateDestroyAPIView): - - model = Host - serializer_class = HostSerializer - -class InventoryHostsList(SubListCreateAPIView): - - model = Host - serializer_class = HostSerializer - parent_model = Inventory - relationship = 'hosts' - parent_key = 'inventory' - -class HostGroupsList(SubListCreateAPIView): - ''' the list of groups a host is directly a member of ''' - - model = Group - serializer_class = GroupSerializer - parent_model = Host - relationship = 'groups' - -class HostAllGroupsList(SubListAPIView): - ''' the list of all groups of which the host is directly or indirectly a member ''' - - model = Group - serializer_class = GroupSerializer - parent_model = Host - relationship = 'groups' - - def get_queryset(self): - parent = self.get_parent_object() - self.check_parent_access(parent) - qs = self.request.user.get_queryset(self.model) - sublist_qs = parent.all_groups.distinct() - return qs & sublist_qs - -class GroupList(ListCreateAPIView): - - model = Group - serializer_class = GroupSerializer - -class GroupChildrenList(SubListCreateAPIView): - - model = Group - serializer_class = GroupSerializer - parent_model = Group - relationship = 'children' - - def _unattach(self, request, *args, **kwargs): # FIXME: Disabled for now for UI support. - ''' - Special case for disassociating a child group from the parent. If the - child group has no more parents, then automatically mark it inactive. - ''' - sub_id = request.DATA.get('id', None) - if not sub_id: - data = dict(msg='"id" is required to disassociate') - return Response(data, status=status.HTTP_400_BAD_REQUEST) - - parent = self.get_parent_object() - parent_key = getattr(self, 'parent_key', None) - relationship = getattr(parent, self.relationship) - sub = get_object_or_400(self.model, pk=sub_id) - - if not request.user.can_access(self.parent_model, 'unattach', parent, - sub, self.relationship): - raise PermissionDenied() - - if sub.parents.filter(active=True).exclude(pk=parent.pk).count() == 0: - sub.mark_inactive() - else: - relationship.remove(sub) - - return Response(status=status.HTTP_204_NO_CONTENT) - -class GroupPotentialChildrenList(SubListAPIView): - - model = Group - serializer_class = GroupSerializer - parent_model = Group - new_in_14 = True - - def get_queryset(self): - parent = self.get_parent_object() - self.check_parent_access(parent) - qs = self.request.user.get_queryset(self.model) - qs = qs.filter(inventory__pk=parent.inventory.pk) - except_pks = set([parent.pk]) - except_pks.update(parent.all_parents.values_list('pk', flat=True)) - except_pks.update(parent.all_children.values_list('pk', flat=True)) - return qs.exclude(pk__in=except_pks) - -class GroupHostsList(SubListCreateAPIView): - ''' the list of hosts directly below a group ''' - - model = Host - serializer_class = HostSerializer - parent_model = Group - relationship = 'hosts' - -class GroupAllHostsList(SubListAPIView): - ''' the list of all hosts below a group, even including subgroups ''' - - model = Host - serializer_class = HostSerializer - parent_model = Group - relationship = 'hosts' - - def get_queryset(self): - parent = self.get_parent_object() - self.check_parent_access(parent) - qs = self.request.user.get_queryset(self.model) - sublist_qs = parent.all_hosts.distinct() - return qs & sublist_qs - -class GroupDetail(RetrieveUpdateDestroyAPIView): - - model = Group - serializer_class = GroupSerializer - -class InventoryGroupsList(SubListCreateAPIView): - - model = Group - serializer_class = GroupSerializer - parent_model = Inventory - relationship = 'groups' - parent_key = 'inventory' - -class InventoryRootGroupsList(SubListCreateAPIView): - - model = Group - serializer_class = GroupSerializer - parent_model = Inventory - relationship = 'groups' - parent_key = 'inventory' - - def get_queryset(self): - parent = self.get_parent_object() - self.check_parent_access(parent) - qs = self.request.user.get_queryset(self.model) - return qs & parent.root_groups - -class BaseVariableData(RetrieveUpdateAPIView): - - parser_classes = api_settings.DEFAULT_PARSER_CLASSES + [YAMLParser] - renderer_classes = api_settings.DEFAULT_RENDERER_CLASSES + [YAMLRenderer] - is_variable_data = True # Special flag for permissions check. - -class InventoryVariableData(BaseVariableData): - - model = Inventory - serializer_class = InventoryVariableDataSerializer - -class HostVariableData(BaseVariableData): - - model = Host - serializer_class = HostVariableDataSerializer - -class GroupVariableData(BaseVariableData): - - model = Group - serializer_class = GroupVariableDataSerializer - -class InventoryScriptView(RetrieveAPIView): - - model = Inventory - authentication_classes = [JobTaskAuthentication] + \ - api_settings.DEFAULT_AUTHENTICATION_CLASSES - permission_classes = (JobTaskPermission,) - filter_backends = () - - def retrieve(self, request, *args, **kwargs): - self.object = self.get_object() - hostname = request.QUERY_PARAMS.get('host', '') - hostvars = bool(request.QUERY_PARAMS.get('hostvars', '')) - show_all = bool(request.QUERY_PARAMS.get('all', '')) - if show_all: - hosts_q = dict(active=True) - else: - hosts_q = dict(active=True, enabled=True) - if hostname: - host = get_object_or_404(self.object.hosts, name=hostname, **hosts_q) - data = host.variables_dict - else: - data = SortedDict() - if self.object.variables_dict: - data['all'] = SortedDict() - data['all']['vars'] = self.object.variables_dict - - for group in self.object.groups.filter(active=True): - hosts = group.hosts.filter(**hosts_q) - children = group.children.filter(active=True) - group_info = SortedDict() - group_info['hosts'] = list(hosts.values_list('name', flat=True)) - group_info['children'] = list(children.values_list('name', flat=True)) - group_info['vars'] = group.variables_dict - data[group.name] = group_info - - if hostvars: - data.setdefault('_meta', SortedDict()) - data['_meta'].setdefault('hostvars', SortedDict()) - for host in self.object.hosts.filter(**hosts_q): - data['_meta']['hostvars'][host.name] = host.variables_dict - - # workaround for Ansible inventory bug (github #3687), localhost - # must be explicitly listed in the all group for dynamic inventory - # scripts to pick it up. - localhost_names = ('localhost', '127.0.0.1', '::1') - localhosts_qs = self.object.hosts.filter(name__in=localhost_names, - **hosts_q) - localhosts = list(localhosts_qs.values_list('name', flat=True)) - if localhosts: - data.setdefault('all', SortedDict()) - data['all']['hosts'] = localhosts - - return Response(data) - -class InventoryTreeView(RetrieveAPIView): - - model = Inventory - filter_backends = () - new_in_13 = True - - def retrieve(self, request, *args, **kwargs): - inventory = self.get_object() - groups_qs = inventory.root_groups.filter(active=True) - data = GroupTreeSerializer(groups_qs, many=True).data - return Response(data) - - def get_description_context(self): - d = super(InventoryTreeView, self).get_description_context() - d.update({ - 'serializer_fields': GroupTreeSerializer().metadata(), - }) - return d - -class InventoryInventorySourcesList(SubListAPIView): - - model = InventorySource - serializer_class = InventorySourceSerializer - parent_model = Inventory - relationship = None # Not defined since using get_queryset(). - view_name = 'Inventory Source List' - new_in_14 = True - - def get_queryset(self): - parent = self.get_parent_object() - self.check_parent_access(parent) - qs = self.request.user.get_queryset(self.model) - return qs.filter(Q(inventory__pk=parent.pk) | - Q(group__inventory__pk=parent.pk)) - -class InventorySourceList(ListAPIView): - - model = InventorySource - serializer_class = InventorySourceSerializer - new_in_14 = True - -class InventorySourceDetail(RetrieveUpdateAPIView): - - model = InventorySource - serializer_class = InventorySourceSerializer - new_in_14 = True - -class InventorySourceUpdatesList(SubListAPIView): - - model = InventoryUpdate - serializer_class = InventoryUpdateSerializer - parent_model = InventorySource - relationship = 'inventory_updates' - new_in_14 = True - -class InventorySourceUpdateView(GenericAPIView): - - model = InventorySource - new_in_14 = True - - def get(self, request, *args, **kwargs): - obj = self.get_object() - data = dict( - can_update=obj.can_update, - ) - return Response(data) - - def post(self, request, *args, **kwargs): - obj = self.get_object() - if obj.can_update: - inventory_update = obj.update(**request.DATA) - if not inventory_update: - return Response({}, status=status.HTTP_400_BAD_REQUEST) - else: - headers = {'Location': inventory_update.get_absolute_url()} - return Response(status=status.HTTP_202_ACCEPTED, headers=headers) - else: - return self.http_method_not_allowed(request, *args, **kwargs) - -class InventoryUpdateDetail(RetrieveAPIView): - - model = InventoryUpdate - serializer_class = InventoryUpdateSerializer - new_in_14 = True - -class InventoryUpdateCancel(GenericAPIView): - - model = InventoryUpdate - is_job_cancel = True - new_in_14 = True - - def get(self, request, *args, **kwargs): - obj = self.get_object() - data = dict( - can_cancel=obj.can_cancel, - ) - return Response(data) - - def post(self, request, *args, **kwargs): - obj = self.get_object() - if obj.can_cancel: - result = obj.cancel() - return Response(status=status.HTTP_202_ACCEPTED) - else: - return self.http_method_not_allowed(request, *args, **kwargs) - -class JobTemplateList(ListCreateAPIView): - - model = JobTemplate - serializer_class = JobTemplateSerializer - -class JobTemplateDetail(RetrieveUpdateDestroyAPIView): - - model = JobTemplate - serializer_class = JobTemplateSerializer - -class JobTemplateCallback(GenericAPIView): - - model = JobTemplate - permission_classes = (JobTemplateCallbackPermission,) - - def find_matching_hosts(self): - ''' - Find the host(s) in the job template's inventory that match the remote - host for the current request. - ''' - # Find the list of remote host names/IPs to check. - remote_hosts = set() - for header in settings.REMOTE_HOST_HEADERS: - value = self.request.META.get(header, '').strip() - if value: - remote_hosts.add(value) - # Add the reverse lookup of IP addresses. - for rh in list(remote_hosts): - try: - result = socket.gethostbyaddr(rh) - except socket.herror: - continue - remote_hosts.add(result[0]) - remote_hosts.update(result[1]) - # Filter out any .arpa results. - for rh in list(remote_hosts): - if rh.endswith('.arpa'): - remote_hosts.remove(rh) - if not remote_hosts: - return set() - # Find the host objects to search for a match. - obj = self.get_object() - qs = obj.inventory.hosts.filter(active=True) - # First try for an exact match on the name. - try: - return set([qs.get(name__in=remote_hosts)]) - except (Host.DoesNotExist, Host.MultipleObjectsReturned): - pass - # Next, try matching based on name or ansible_ssh_host variable. - matches = set() - for host in qs: - ansible_ssh_host = host.variables_dict.get('ansible_ssh_host', '') - if ansible_ssh_host in remote_hosts: - matches.add(host) - # FIXME: Not entirely sure if this statement will ever be needed? - if host.name != ansible_ssh_host and host.name in remote_hosts: - matches.add(host) - if len(matches) == 1: - return matches - # Try to resolve forward addresses for each host to find matches. - for host in qs: - hostnames = set([host.name]) - ansible_ssh_host = host.variables_dict.get('ansible_ssh_host', '') - if ansible_ssh_host: - hostnames.add(ansible_ssh_host) - for hostname in hostnames: - try: - result = socket.getaddrinfo(hostname, None) - possible_ips = set(x[4][0] for x in result) - possible_ips.discard(hostname) - if possible_ips and possible_ips & remote_hosts: - matches.add(host) - except socket.gaierror: - pass - # Return all matches found. - return matches - - def get(self, request, *args, **kwargs): - job_template = self.get_object() - matching_hosts = self.find_matching_hosts() - data = dict( - host_config_key=job_template.host_config_key, - matching_hosts=[x.name for x in matching_hosts], - ) - if settings.DEBUG: - d = dict([(k,v) for k,v in request.META.items() - if k.startswith('HTTP_') or k.startswith('REMOTE_')]) - data['request_meta'] = d - return Response(data) - - def post(self, request, *args, **kwargs): - job_template = self.get_object() - # Permission class should have already validated host_config_key. - matching_hosts = self.find_matching_hosts() - if not matching_hosts: - data = dict(msg='No matching host could be found!') - # FIXME: Log! - return Response(data, status=status.HTTP_400_BAD_REQUEST) - elif len(matching_hosts) > 1: - data = dict(msg='Multiple hosts matched the request!') - # FIXME: Log! - return Response(data, status=status.HTTP_400_BAD_REQUEST) - else: - host = list(matching_hosts)[0] - if not job_template.can_start_without_user_input(): - data = dict(msg='Cannot start automatically, user input required!') - # FIXME: Log! - return Response(data, status=status.HTTP_400_BAD_REQUEST) - limit = ':'.join(filter(None, [job_template.limit, host.name])) - job = job_template.create_job(limit=limit, launch_type='callback') - result = job.start() - if not result: - data = dict(msg='Error starting job!') - return Response(data, status=status.HTTP_400_BAD_REQUEST) - else: - return Response(status=status.HTTP_202_ACCEPTED) - -class JobTemplateJobsList(SubListCreateAPIView): - - model = Job - serializer_class = JobSerializer - parent_model = JobTemplate - relationship = 'jobs' - parent_key = 'job_template' - -class JobList(ListCreateAPIView): - - model = Job - serializer_class = JobSerializer - -class JobDetail(RetrieveUpdateDestroyAPIView): - - model = Job - serializer_class = JobSerializer - - def update(self, request, *args, **kwargs): - obj = self.get_object() - # Only allow changes (PUT/PATCH) when job status is "new". - if obj.status != 'new': - return self.http_method_not_allowed(request, *args, **kwargs) - return super(JobDetail, self).update(request, *args, **kwargs) - -class JobStart(GenericAPIView): - - model = Job - is_job_start = True - - def get(self, request, *args, **kwargs): - obj = self.get_object() - data = dict( - can_start=obj.can_start, - ) - if obj.can_start: - data['passwords_needed_to_start'] = obj.passwords_needed_to_start - return Response(data) - - def post(self, request, *args, **kwargs): - obj = self.get_object() - if obj.can_start: - result = obj.start(**request.DATA) - if not result: - data = dict(passwords_needed_to_start=obj.passwords_needed_to_start) - return Response(data, status=status.HTTP_400_BAD_REQUEST) - else: - return Response(status=status.HTTP_202_ACCEPTED) - else: - return self.http_method_not_allowed(request, *args, **kwargs) - -class JobCancel(GenericAPIView): - - model = Job - is_job_cancel = True - - def get(self, request, *args, **kwargs): - obj = self.get_object() - data = dict( - can_cancel=obj.can_cancel, - ) - return Response(data) - - def post(self, request, *args, **kwargs): - obj = self.get_object() - if obj.can_cancel: - result = obj.cancel() - return Response(status=status.HTTP_202_ACCEPTED) - else: - return self.http_method_not_allowed(request, *args, **kwargs) - -class BaseJobHostSummariesList(SubListAPIView): - - model = JobHostSummary - serializer_class = JobHostSummarySerializer - parent_model = None # Subclasses must define this attribute. - relationship = 'job_host_summaries' - view_name = 'Job Host Summaries List' - -class HostJobHostSummariesList(BaseJobHostSummariesList): - - parent_model = Host - -class GroupJobHostSummariesList(BaseJobHostSummariesList): - - parent_model = Group - -class JobJobHostSummariesList(BaseJobHostSummariesList): - - parent_model = Job - -class JobHostSummaryDetail(RetrieveAPIView): - - model = JobHostSummary - serializer_class = JobHostSummarySerializer - -class JobEventList(ListAPIView): - - model = JobEvent - serializer_class = JobEventSerializer - -class JobEventDetail(RetrieveAPIView): - - model = JobEvent - serializer_class = JobEventSerializer - -class JobEventChildrenList(SubListAPIView): - - model = JobEvent - serializer_class = JobEventSerializer - parent_model = JobEvent - relationship = 'children' - view_name = 'Job Event Children List' - -class JobEventHostsList(SubListAPIView): - - model = Host - serializer_class = HostSerializer - parent_model = JobEvent - relationship = 'hosts' - view_name = 'Job Event Hosts List' - -class BaseJobEventsList(SubListAPIView): - - model = JobEvent - serializer_class = JobEventSerializer - parent_model = None # Subclasses must define this attribute. - relationship = 'job_events' - view_name = 'Job Events List' - -class HostJobEventsList(BaseJobEventsList): - - parent_model = Host - -class GroupJobEventsList(BaseJobEventsList): - - parent_model = Group - -class JobJobEventsList(BaseJobEventsList): - - parent_model = Job - authentication_classes = [JobTaskAuthentication] + \ - api_settings.DEFAULT_AUTHENTICATION_CLASSES - permission_classes = (JobTaskPermission,) - - # Post allowed for job event callback only. - def post(self, request, *args, **kwargs): - parent_obj = get_object_or_404(self.parent_model, pk=self.kwargs['pk']) - data = request.DATA.copy() - data['job'] = parent_obj.pk - serializer = self.get_serializer(data=data) - if serializer.is_valid(): - self.pre_save(serializer.object) - self.object = serializer.save(force_insert=True) - self.post_save(self.object, created=True) - headers = {'Location': serializer.data['url']} - return Response(serializer.data, status=status.HTTP_201_CREATED, - headers=headers) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - -# Create view functions for all of the class-based views to simplify inclusion -# in URL patterns and reverse URL lookups, converting CamelCase names to -# lowercase_with_underscore (e.g. MyView.as_view() becomes my_view). -this_module = sys.modules[__name__] -for attr, value in locals().items(): - if isinstance(value, type) and issubclass(value, APIView): - name = camelcase_to_underscore(attr) - view = value.as_view() - setattr(this_module, name, view) diff --git a/awx/middleware/exceptions.py b/awx/middleware/exceptions.py deleted file mode 100644 index 9bfc948513..0000000000 --- a/awx/middleware/exceptions.py +++ /dev/null @@ -1,13 +0,0 @@ -# Copyright (c) 2013 AnsibleWorks, Inc. -# All Rights Reserved. - -import traceback -from django.http import HttpResponse - -class ExceptionMiddleware(object): - - def process_exception(self, request, exception): - if request.path.startswith('/api/'): - # FIXME: For GA, we shouldn't provide this level of detail to the - # end user. - return HttpResponse(traceback.format_exc(exception), content_type="text/plain", status=500) diff --git a/awx/settings/defaults.py b/awx/settings/defaults.py index c4a9f75446..c9bdbe73e1 100644 --- a/awx/settings/defaults.py +++ b/awx/settings/defaults.py @@ -130,28 +130,29 @@ INSTALLED_APPS = ( 'kombu.transport.django', 'taggit', 'awx.main', + 'awx.api', 'awx.ui', ) INTERNAL_IPS = ('127.0.0.1',) REST_FRAMEWORK = { - 'DEFAULT_PAGINATION_SERIALIZER_CLASS': 'awx.main.pagination.PaginationSerializer', + 'DEFAULT_PAGINATION_SERIALIZER_CLASS': 'awx.api.pagination.PaginationSerializer', 'PAGINATE_BY': 25, 'PAGINATE_BY_PARAM': 'page_size', 'DEFAULT_AUTHENTICATION_CLASSES': ( 'rest_framework.authentication.BasicAuthentication', - 'awx.main.authentication.TokenAuthentication', + 'awx.api.authentication.TokenAuthentication', 'rest_framework.authentication.SessionAuthentication', ), 'DEFAULT_PERMISSION_CLASSES': ( - 'awx.main.permissions.ModelAccessPermission', + 'awx.api.permissions.ModelAccessPermission', ), 'DEFAULT_FILTER_BACKENDS': ( - 'awx.main.filters.ActiveOnlyBackend', - 'awx.main.filters.FieldLookupBackend', + 'awx.api.filters.ActiveOnlyBackend', + 'awx.api.filters.FieldLookupBackend', 'rest_framework.filters.SearchFilter', - 'awx.main.filters.OrderByBackend', + 'awx.api.filters.OrderByBackend', ), 'DEFAULT_PARSER_CLASSES': ( 'rest_framework.parsers.JSONParser', @@ -160,10 +161,10 @@ REST_FRAMEWORK = { ), 'DEFAULT_RENDERER_CLASSES': ( 'rest_framework.renderers.JSONRenderer', - 'awx.main.renderers.BrowsableAPIRenderer', + 'awx.api.renderers.BrowsableAPIRenderer', ), - 'VIEW_NAME_FUNCTION': 'awx.main.base_views.get_view_name', - 'VIEW_DESCRIPTION_FUNCTION': 'awx.main.base_views.get_view_description', + 'VIEW_NAME_FUNCTION': 'awx.api.generics.get_view_name', + 'VIEW_DESCRIPTION_FUNCTION': 'awx.api.generics.get_view_description', } AUTHENTICATION_BACKENDS = ( @@ -250,7 +251,7 @@ DEVSERVER_MODULES = ( try: import django_jenkins INSTALLED_APPS += ('django_jenkins',) - PROJECT_APPS = ('awx.main',) + PROJECT_APPS = ('awx.main', 'awx.api',) except ImportError: pass @@ -303,7 +304,7 @@ LOGGING = { '()': 'django.utils.log.RequireDebugFalse', }, 'require_debug_true': { - '()': 'awx.main.compat.RequireDebugTrue', + '()': 'awx.lib.compat.RequireDebugTrue', }, 'require_debug_true_or_test': { '()': 'awx.main.utils.RequireDebugTrueOrTest', @@ -360,11 +361,11 @@ LOGGING = { 'handlers': ['null'], 'propagate': False, }, - 'awx.main.permissions': { + 'awx.main.signals': { 'handlers': ['null'], 'propagate': False, }, - 'awx.main.signals': { + 'awx.api.permissions': { 'handlers': ['null'], 'propagate': False, }, diff --git a/awx/urls.py b/awx/urls.py index 643ff3ebdd..1b8be274d9 100644 --- a/awx/urls.py +++ b/awx/urls.py @@ -10,7 +10,7 @@ handler500 = 'awx.main.views.handle_500' urlpatterns = patterns('', url(r'', include('awx.ui.urls', namespace='ui', app_name='ui')), - url(r'^api/', include('awx.main.urls', namespace='main', app_name='main')), + url(r'^api/', include('awx.api.urls', namespace='api', app_name='api')), ) if 'django.contrib.admin' in settings.INSTALLED_APPS: