# Copyright (c) 2015 Ansible, Inc. (formerly AnsibleWorks, Inc.) # All Rights Reserved. # Python import getpass import json import os import re import time import subprocess import tempfile import urlparse # Django from django.conf import settings from django.contrib.auth.models import User from django.core.urlresolvers import reverse from django.test.utils import override_settings from django.utils.timezone import now # AWX from awx.main.models import * # noqa from awx.main.tests.base import BaseTransactionTest from awx.main.tests.tasks import TEST_SSH_KEY_DATA, TEST_SSH_KEY_DATA_LOCKED, TEST_SSH_KEY_DATA_UNLOCK from awx.main.utils import decrypt_field, update_scm_url TEST_PLAYBOOK = '''- hosts: mygroup gather_facts: false tasks: - name: woohoo command: test 1 = 1 ''' class ProjectsTest(BaseTransactionTest): # tests for users, projects, and teams def collection(self): return reverse('api:project_list') def setUp(self): super(ProjectsTest, self).setUp() self.setup_instances() self.setup_users() self.organizations = self.make_organizations(self.super_django_user, 10) self.projects = self.make_projects(self.normal_django_user, 10, TEST_PLAYBOOK) # add projects to organizations in a more or less arbitrary way for project in self.projects[0:2]: self.organizations[0].projects.add(project) for project in self.projects[3:8]: self.organizations[1].projects.add(project) for project in self.projects[9:10]: self.organizations[2].projects.add(project) self.organizations[0].projects.add(self.projects[-1]) self.organizations[9].projects.add(self.projects[-2]) # get the URL for various organization records self.a_detail_url = "%s%s" % (self.collection(), self.organizations[0].pk) self.b_detail_url = "%s%s" % (self.collection(), self.organizations[1].pk) self.c_detail_url = "%s%s" % (self.collection(), self.organizations[2].pk) # configuration: # admin_user is an admin and regular user in all organizations # other_user is all organizations # normal_user is a user in organization 0, and an admin of organization 1 for x in self.organizations: # NOTE: superuser does not have to be explicitly added to admin group # x.admins.add(self.super_django_user) x.users.add(self.super_django_user) self.organizations[0].users.add(self.normal_django_user) self.organizations[1].admins.add(self.normal_django_user) self.team1 = Team.objects.create( name = 'team1', organization = self.organizations[0] ) self.team2 = Team.objects.create( name = 'team2', organization = self.organizations[0] ) # create some teams in the first org self.team1.projects.add(self.projects[0]) self.team2.projects.add(self.projects[1]) self.team2.projects.add(self.projects[2]) self.team2.projects.add(self.projects[3]) self.team2.projects.add(self.projects[4]) self.team2.projects.add(self.projects[5]) self.team1.save() self.team2.save() self.team1.users.add(self.normal_django_user) self.team2.users.add(self.other_django_user) def test_playbooks(self): def write_test_file(project, name, content): full_path = os.path.join(project.get_project_path(), name) if not os.path.exists(os.path.dirname(full_path)): os.makedirs(os.path.dirname(full_path)) f = file(full_path, 'wb') f.write(content) f.close() # Invalid local_path project = self.projects[0] project.local_path = 'path_does_not_exist' project.save() self.assertFalse(project.get_project_path()) self.assertEqual(len(project.playbooks), 0) # Simple playbook project = self.projects[1] self.assertEqual(len(project.playbooks), 1) write_test_file(project, 'foo.yml', TEST_PLAYBOOK) self.assertEqual(len(project.playbooks), 2) # Other files project = self.projects[2] self.assertEqual(len(project.playbooks), 1) write_test_file(project, 'foo.txt', 'not a playbook') self.assertEqual(len(project.playbooks), 1) # Empty playbook project = self.projects[3] self.assertEqual(len(project.playbooks), 1) write_test_file(project, 'blah.yml', '') self.assertEqual(len(project.playbooks), 1) # Invalid YAML (now allowed to show) project = self.projects[4] self.assertEqual(len(project.playbooks), 1) write_test_file(project, 'blah.yml', TEST_PLAYBOOK + '----') self.assertEqual(len(project.playbooks), 2) # No hosts or includes project = self.projects[5] self.assertEqual(len(project.playbooks), 1) playbook_content = TEST_PLAYBOOK.replace('hosts', 'hoists') write_test_file(project, 'blah.yml', playbook_content) self.assertEqual(len(project.playbooks), 1) # Playbook in roles folder project = self.projects[6] self.assertEqual(len(project.playbooks), 1) write_test_file(project, 'roles/blah.yml', TEST_PLAYBOOK) self.assertEqual(len(project.playbooks), 1) # Playbook in tasks folder project = self.projects[7] self.assertEqual(len(project.playbooks), 1) write_test_file(project, 'tasks/blah.yml', TEST_PLAYBOOK) self.assertEqual(len(project.playbooks), 1) def test_api_config(self): # superuser can read all config data. 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) self.assertTrue('project_local_paths' in response) self.assertEqual(set(response['project_local_paths']), set(Project.get_local_path_choices())) # return local paths are only the ones not used by any active project. qs = Project.objects.filter(active=True) used_paths = qs.values_list('local_path', flat=True) self.assertFalse(set(response['project_local_paths']) & set(used_paths)) for project in self.projects: local_path = project.local_path response = self.get(url, expect=200, auth=self.get_super_credentials()) self.assertTrue(local_path not in response['project_local_paths']) project.mark_inactive() response = self.get(url, expect=200, auth=self.get_super_credentials()) self.assertTrue(local_path in response['project_local_paths']) # org admin can read config and will get project fields. response = self.get(url, expect=200, auth=self.get_normal_credentials()) self.assertTrue('project_base_dir' in response) self.assertTrue('project_local_paths' in response) # regular user can read configuration, but won't have project fields. response = self.get(url, expect=200, auth=self.get_nobody_credentials()) self.assertFalse('project_base_dir' in response) self.assertFalse('project_local_paths' in response) # anonymous/invalid user can't access config. self.get(url, expect=401) self.get(url, expect=401, auth=self.get_invalid_credentials()) def test_dashboard(self): url = reverse('api:dashboard_view') # superuser can read dashboard. self.get(url, expect=200, auth=self.get_super_credentials()) # org admin can read dashboard. self.get(url, expect=200, auth=self.get_normal_credentials()) # regular user can read dashboard. self.get(url, expect=200, auth=self.get_nobody_credentials()) # anonymous/invalid user can't access dashboard. self.get(url, expect=401) self.get(url, expect=401, auth=self.get_invalid_credentials()) # FIXME: Test that data on dashboard is filtered based on RBAC. def test_mainline(self): # ===================================================================== # PROJECTS - LISTING # can get projects list projects = reverse('api:project_list') # invalid auth self.get(projects, expect=401) self.get(projects, expect=401, auth=self.get_invalid_credentials()) # super user results = self.get(projects, expect=200, auth=self.get_super_credentials()) self.assertEquals(results['count'], 10) # org admin results = self.get(projects, expect=200, auth=self.get_normal_credentials()) self.assertEquals(results['count'], 10) # user on a team results = self.get(projects, expect=200, auth=self.get_other_credentials()) self.assertEquals(results['count'], 5) # user not on any teams results = self.get(projects, expect=200, auth=self.get_nobody_credentials()) self.assertEquals(results['count'], 0) # can add projects (super user) project_dir = tempfile.mkdtemp(dir=settings.PROJECTS_ROOT) self._temp_paths.append(project_dir) project_data = { 'name': 'My Test Project', 'description': 'Does amazing things', 'local_path': os.path.basename(project_dir), 'scm_type': None, } # Adding a project with scm_type=None should work, but scm_type will be # changed to an empty string. response = self.post(projects, project_data, expect=201, auth=self.get_super_credentials()) self.assertEqual(response['scm_type'], u'') # can edit project using same local path. 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, auth=self.get_super_credentials()) # cannot update using local_path from another project. project_data['local_path'] = self.projects[2].local_path response = self.put(project_detail, project_data, expect=400, auth=self.get_super_credentials()) # cannot update using a path that doesn't exist. project_data['local_path'] = 'my_secret_invisible_project_path' response = self.put(project_detail, project_data, expect=400, auth=self.get_super_credentials()) # ===================================================================== # PROJECTS - ACCESS 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()) self.get(project, expect=403, auth=self.get_nobody_credentials()) # can create a schedule for a project (doesn't matter if the project # has SCM for this test). project_schedules = reverse('api:project_schedules_list', args=(self.projects[3].pk,)) with self.current_user(self.super_django_user): response = self.get(project_schedules, expect=200) self.assertEqual(response['count'], 0) dtstart = now().replace(microsecond=0).strftime('%Y%m%dT%H%M%SZ') data = { 'name': 'test schedule', 'rrule': 'DTSTART:%s RRULE:FREQ=DAILY;INTERVAL=1' % dtstart, } response = self.post(project_schedules, data, expect=201) schedule_url = response['url'] response = self.get(schedule_url, expect=200) # can delete projects, schedule should be "deleted" along with project. self.delete(project, expect=204, auth=self.get_normal_credentials()) self.get(project, expect=404, auth=self.get_normal_credentials()) with self.current_user(self.super_django_user): self.get(schedule_url, expect=404) # can list playbooks for projects proj_playbooks = reverse('api:project_playbooks', args=(self.projects[2].pk,)) got = self.get(proj_playbooks, expect=200, auth=self.get_super_credentials()) got_new = [] for g in got: got_new.append(g.encode('utf-8')) self.assertEqual(got_new, self.projects[2].playbooks) # can list member organizations for projects 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('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()) got = self.get(proj_orgs, expect=200, auth=self.get_super_credentials()) self.assertEquals(got['count'], 2) # ===================================================================== # TEAMS 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()) self.assertEquals(got['count'], 2) # FIXME: for other accounts, also check filtering # can get teams got = self.get(team1, expect=200, auth=self.get_super_credentials()) 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')) self.team1.save() got = self.get(team1, expect=200, auth=self.get_other_credentials()) got = self.get(team1, expect=403, auth=self.get_nobody_credentials()) new_team = dict(name='newTeam', description='blarg', organization=self.organizations[0].pk) new_team2 = dict(name='newTeam2', description='blarg', organization=self.organizations[0].pk) new_team3 = dict(name='newTeam3', description='bad wolf', organization=self.organizations[0].pk) # can add teams posted1 = self.post(all_teams, data=new_team, expect=201, auth=self.get_super_credentials()) self.post(all_teams, data=new_team, expect=400, auth=self.get_super_credentials()) # normal user is not an admin of organizations[0], but is for [1]. posted3 = self.post(all_teams, data=new_team2, expect=403, auth=self.get_normal_credentials()) new_team2['organization'] = self.organizations[1].pk posted3 = self.post(all_teams, data=new_team2, expect=201, auth=self.get_normal_credentials()) self.post(all_teams, data=new_team2, expect=400, auth=self.get_normal_credentials()) self.post(all_teams, data=new_team3, expect=403, auth=self.get_other_credentials()) posted1['url'] url3 = posted3['url'] posted1['url'] new_team = Team.objects.create(name='newTeam4', organization=self.organizations[1]) url = reverse('api:team_detail', args=(new_team.pk,)) # can delete teams self.delete(url, expect=401) self.delete(url, expect=403, auth=self.get_nobody_credentials()) self.delete(url, expect=403, auth=self.get_other_credentials()) self.delete(url, expect=204, auth=self.get_normal_credentials()) self.delete(url3, expect=204, auth=self.get_super_credentials()) # ===================================================================== # ORGANIZATION TEAMS # can list organization teams (filtered by user) -- this is an org admin function org_teams = reverse('api:organization_teams_list', args=(self.organizations[1].pk,)) self.get(org_teams, expect=401) data2 = self.get(org_teams, expect=403, auth=self.get_nobody_credentials()) self.get(org_teams, expect=403, auth=self.get_other_credentials()) self.get(org_teams, expect=200, auth=self.get_normal_credentials()) self.get(org_teams, expect=200, auth=self.get_super_credentials()) # can add teams to organizations new_team1 = dict(name='super new team A') # also tests that sub posts overwrite the related field: new_team2 = dict(name='super new team B', organization=34567) new_team3 = dict(name='super new team C') self.post(org_teams, new_team1, expect=401) self.post(org_teams, new_team1, expect=403, auth=self.get_nobody_credentials()) self.post(org_teams, new_team1, expect=403, auth=self.get_other_credentials()) data2 = self.post(org_teams, new_team2, expect=201, auth=self.get_normal_credentials()) self.post(org_teams, new_team3, expect=201, auth=self.get_super_credentials()) # can remove teams from organizations data2['disassociate'] = 1 url = data2['url'] self.post(org_teams, data2, expect=204, auth=self.get_normal_credentials()) got = self.get(url, expect=404, auth=self.get_normal_credentials()) # ===================================================================== # TEAM PROJECTS team = Team.objects.filter(active=True, organization__pk=self.organizations[1].pk)[0] team_projects = reverse('api:team_projects_list', args=(team.pk,)) p1 = self.projects[0] team.projects.add(p1) team.save() got = self.get(team_projects, expect=200, auth=self.get_super_credentials()) # FIXME: project postablility tests somewhat incomplete. # add tests to show we can create new projects on the subresource and so on. self.assertEquals(got['count'], 1) # ===================================================================== # TEAMS USER MEMBERSHIP team = Team.objects.filter(active=True, organization__pk=self.organizations[1].pk)[0] team_users = reverse('api:team_users_list', args=(team.pk,)) for x in team.users.all(): team.users.remove(x) team.save() # can list uses on teams self.get(team_users, expect=401) self.get(team_users, expect=401, auth=self.get_invalid_credentials()) self.get(team_users, expect=403, auth=self.get_nobody_credentials()) self.get(team_users, expect=403, auth=self.get_other_credentials()) self.get(team_users, expect=200, auth=self.get_normal_credentials()) self.get(team_users, expect=200, auth=self.get_super_credentials()) # can add users to teams (but only users I can see - as an org admin, can now see all users) 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=dict(x, is_superuser=False), expect=204, auth=self.get_normal_credentials()) # The normal admin user can't create a super user vicariously through the team/project self.post(team_users, data=dict(username='attempted_superuser_create', password='thepassword', is_superuser=True), expect=403, auth=self.get_normal_credentials()) # ... but a superuser can self.post(team_users, data=dict(username='attempted_superuser_create', password='thepassword', is_superuser=True), expect=201, auth=self.get_super_credentials()) self.assertEqual(Team.objects.get(pk=team.pk).users.count(), 5) # can remove users from teams for x in all_users['results']: y = dict(id=x['id'], disassociate=1) self.post(team_users, data=y, expect=403, auth=self.get_nobody_credentials()) self.post(team_users, data=y, expect=204, auth=self.get_normal_credentials()) self.assertEquals(Team.objects.get(pk=team.pk).users.count(), 1) # Leaving just the super user we created # ===================================================================== # USER TEAMS # from a user, can see what teams they are on (related resource) other = User.objects.get(username = 'other') 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()) other.organizations.add(Organization.objects.get(pk=self.organizations[1].pk)) # Normal user can only see some teams that other user is a part of, # since normal user is not an admin of that organization. my_teams1 = self.get(url, expect=200, auth=self.get_normal_credentials()) self.assertEqual(my_teams1['count'], 1) # Other user should be able to see all his own teams. my_teams2 = self.get(url, expect=200, auth=self.get_other_credentials()) self.assertEqual(my_teams2['count'], 2) # ===================================================================== # USER PROJECTS 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 got = self.get(url, expect=200, auth=self.get_other_credentials()) self.assertEquals(got['count'], 5) got = self.get(url, expect=403, auth=self.get_nobody_credentials()) got = self.get(url, expect=401, auth=self.get_invalid_credentials()) got = self.get(url, expect=401) got = self.get(url, expect=200, auth=self.get_super_credentials()) # ===================================================================== # CREDENTIALS 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', project = Project.objects.order_by('pk')[0].pk, default_username = 'foo', ssh_key_data = TEST_SSH_KEY_DATA_LOCKED, ssh_key_unlock = TEST_SSH_KEY_DATA_UNLOCK, ssh_password = 'narf', sudo_password = 'troz' ) # can add credentials to a user (if user or org admin or super user) self.post(other_creds, data=new_credentials, expect=401) self.post(other_creds, data=new_credentials, expect=401, auth=self.get_invalid_credentials()) new_credentials['team'] = team.pk result = self.post(other_creds, data=new_credentials, expect=201, auth=self.get_super_credentials()) cred_user = result['id'] self.assertEqual(result['team'], None) del new_credentials['team'] new_credentials['name'] = 'credential2' self.post(other_creds, data=new_credentials, expect=201, auth=self.get_normal_credentials()) new_credentials['name'] = 'credential3' result = self.post(other_creds, data=new_credentials, expect=201, auth=self.get_other_credentials()) new_credentials['name'] = 'credential4' self.post(other_creds, data=new_credentials, expect=403, auth=self.get_nobody_credentials()) # can add credentials to a team new_credentials['name'] = 'credential' new_credentials['user'] = other.pk self.post(team_creds, data=new_credentials, expect=401) self.post(team_creds, data=new_credentials, expect=401, auth=self.get_invalid_credentials()) result = self.post(team_creds, data=new_credentials, expect=201, auth=self.get_super_credentials()) self.assertEqual(result['user'], None) del new_credentials['user'] new_credentials['name'] = 'credential2' result = self.post(team_creds, data=new_credentials, expect=201, auth=self.get_normal_credentials()) new_credentials['name'] = 'credential3' self.post(team_creds, data=new_credentials, expect=403, auth=self.get_other_credentials()) self.post(team_creds, data=new_credentials, expect=403, auth=self.get_nobody_credentials()) cred_team = result['id'] # can list credentials on a user self.get(other_creds, expect=401) self.get(other_creds, expect=401, auth=self.get_invalid_credentials()) self.get(other_creds, expect=200, auth=self.get_super_credentials()) self.get(other_creds, expect=200, auth=self.get_normal_credentials()) self.get(other_creds, expect=200, auth=self.get_other_credentials()) self.get(other_creds, expect=403, auth=self.get_nobody_credentials()) # can list credentials on a team self.get(team_creds, expect=401) self.get(team_creds, expect=401, auth=self.get_invalid_credentials()) self.get(team_creds, expect=200, auth=self.get_super_credentials()) self.get(team_creds, expect=200, auth=self.get_normal_credentials()) self.get(team_creds, expect=403, auth=self.get_other_credentials()) self.get(team_creds, expect=403, auth=self.get_nobody_credentials()) # Check /api/v1/credentials (GET) url = reverse('api:credential_list') with self.current_user(self.super_django_user): self.options(url) self.head(url) response = self.get(url) qs = Credential.objects.all() self.check_pagination_and_size(response, qs.count()) self.check_list_ids(response, qs) # POST should now work for all users. with self.current_user(self.super_django_user): data = dict(name='xyz', user=self.super_django_user.pk) self.post(url, data, expect=201) # Repeating the same POST should violate a unique constraint. with self.current_user(self.super_django_user): data = dict(name='xyz', user=self.super_django_user.pk) self.post(url, data, expect=400) # Test with null where we expect a string value. with self.current_user(self.super_django_user): data = dict(name='zyx', user=self.super_django_user.pk, kind='ssh', become_username=None) self.post(url, data, expect=400) # Test with encrypted ssh key and no unlock password. with self.current_user(self.super_django_user): data = dict(name='wxy', user=self.super_django_user.pk, kind='ssh', ssh_key_data=TEST_SSH_KEY_DATA_LOCKED) self.post(url, data, expect=400) data['ssh_key_unlock'] = TEST_SSH_KEY_DATA_UNLOCK self.post(url, data, expect=201) # Test with invalid ssh key data. with self.current_user(self.super_django_user): bad_key_data = TEST_SSH_KEY_DATA.replace('PRIVATE', 'PUBLIC') data = dict(name='wyx', user=self.super_django_user.pk, kind='ssh', ssh_key_data=bad_key_data) self.post(url, data, expect=400) data['ssh_key_data'] = TEST_SSH_KEY_DATA.replace('-', '=') self.post(url, data, expect=400) data['ssh_key_data'] = '\n'.join(TEST_SSH_KEY_DATA.splitlines()[1:-1]) self.post(url, data, expect=400) data['ssh_key_data'] = TEST_SSH_KEY_DATA.replace('--B', '---B') self.post(url, data, expect=400) data['ssh_key_data'] = TEST_SSH_KEY_DATA self.post(url, data, expect=201) # Test post as organization admin where team is part of org, but user # creating credential is not a member of the team. UI may pass user # as an empty string instead of None. normal_org = self.normal_django_user.admin_of_organizations.all()[0] org_team = normal_org.teams.create(name='new empty team') with self.current_user(self.normal_django_user): data = { 'name': 'my team cred', 'team': org_team.pk, 'user': '', } self.post(url, data, expect=201) # FIXME: Check list as other users. # can edit a credential cred_user = Credential.objects.get(pk=cred_user) cred_team = Credential.objects.get(pk=cred_team) 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('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()) self.put(edit_creds1, data=d_cred_user, expect=200, auth=self.get_super_credentials()) self.put(edit_creds1, data=d_cred_user, expect=200, auth=self.get_normal_credentials()) # We now allow credential to be reassigned (with the right permissions). cred_put_u = self.put(edit_creds1, data=d_cred_user2, expect=200, auth=self.get_normal_credentials()) self.put(edit_creds1, data=d_cred_user, expect=403, auth=self.get_other_credentials()) self.put(edit_creds2, data=d_cred_team, expect=401) self.put(edit_creds2, data=d_cred_team, expect=401, auth=self.get_invalid_credentials()) self.put(edit_creds2, data=d_cred_team, expect=200, auth=self.get_super_credentials()) cred_put_t = self.put(edit_creds2, data=d_cred_team, expect=200, auth=self.get_normal_credentials()) self.put(edit_creds2, data=d_cred_team, expect=403, auth=self.get_other_credentials()) # Reassign credential between team and user. with self.current_user(self.super_django_user): self.post(team_creds, data=dict(id=cred_user.pk), expect=204) response = self.get(edit_creds1) self.assertEqual(response['team'], team.pk) self.assertEqual(response['user'], None) self.post(other_creds, data=dict(id=cred_user.pk), expect=204) response = self.get(edit_creds1) self.assertEqual(response['team'], None) self.assertEqual(response['user'], other.pk) self.post(other_creds, data=dict(id=cred_team.pk), expect=204) response = self.get(edit_creds2) self.assertEqual(response['team'], None) self.assertEqual(response['user'], other.pk) self.post(team_creds, data=dict(id=cred_team.pk), expect=204) response = self.get(edit_creds2) self.assertEqual(response['team'], team.pk) self.assertEqual(response['user'], None) cred_put_t['disassociate'] = 1 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('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 #data = self.delete(url, expect=204, auth=self.get_other_credentials()) data = self.delete(url, expect=404, auth=self.get_other_credentials()) # ===================================================================== # PERMISSIONS user = self.other_django_user team = Team.objects.order_by('pk')[0] organization = Organization.objects.order_by('pk')[0] inventory = Inventory.objects.create( name = 'test inventory', organization = organization, created_by = self.super_django_user ) project = Project.objects.order_by('pk')[0] # can add permissions to a user user_permission = dict( name='user can deploy a certain project to a certain inventory', # user=user.pk, # no need to specify, this will be automatically filled in inventory=inventory.pk, project=project.pk, permission_type=PERM_INVENTORY_DEPLOY ) team_permission = dict( name='team can deploy a certain project to a certain inventory', # team=team.pk, # no need to specify, this will be automatically filled in inventory=inventory.pk, project=project.pk, permission_type=PERM_INVENTORY_DEPLOY ) 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'] user_perm_detail = 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('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()) # cannot set admin/read/write permissions when a project is involved. user_permission.pop('team') user_permission['name'] = 'user permission 3' user_permission['permission_type'] = PERM_INVENTORY_ADMIN self.post(url, user_permission, expect=400, auth=self.get_super_credentials()) # project is required for a deployment permission user_permission['name'] = 'user permission 4' user_permission['permission_type'] = PERM_INVENTORY_DEPLOY user_permission.pop('project') self.post(url, user_permission, expect=400, auth=self.get_super_credentials()) # can add permissions on a team 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('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('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('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()) # can edit a permission -- reducing the permission level team_permission['permission_type'] = PERM_INVENTORY_CHECK self.put(url2, team_permission, expect=200, auth=self.get_super_credentials()) self.put(url2, team_permission, expect=403, auth=self.get_other_credentials()) # can remove permissions # do need to disassociate, just delete it self.delete(url2, expect=403, auth=self.get_other_credentials()) self.delete(url2, expect=204, auth=self.get_super_credentials()) self.delete(user_perm_detail, expect=204, auth=self.get_super_credentials()) self.delete(url2, expect=404, auth=self.get_other_credentials()) # User is still a team member self.get(reverse('api:project_detail', args=(project.pk,)), expect=200, auth=self.get_other_credentials()) team.users.remove(self.other_django_user) # User is no longer a team member and has no permissions self.get(reverse('api:project_detail', args=(project.pk,)), expect=403, auth=self.get_other_credentials()) @override_settings(CELERY_ALWAYS_EAGER=True, CELERY_EAGER_PROPAGATES_EXCEPTIONS=True, ANSIBLE_TRANSPORT='local', PROJECT_UPDATE_IDLE_TIMEOUT=60, PROJECT_UPDATE_VVV=True) class ProjectUpdatesTest(BaseTransactionTest): def setUp(self): super(ProjectUpdatesTest, self).setUp() self.setup_instances() self.start_queue() self.setup_users() def tearDown(self): super(ProjectUpdatesTest, self).tearDown() self.terminate_queue() def create_project(self, **kwargs): cred_fields = ['scm_username', 'scm_password', 'scm_key_data', 'scm_key_unlock'] if set(cred_fields) & set(kwargs.keys()): kw = { 'kind': 'scm', 'user': self.super_django_user, } for field in cred_fields: if field not in kwargs: continue if field.startswith('scm_key_'): kw[field.replace('scm_key_', 'ssh_key_')] = kwargs.pop(field) else: kw[field.replace('scm_', '')] = kwargs.pop(field) credential = Credential.objects.create(**kw) kwargs['credential'] = credential project = Project.objects.create(**kwargs) project_path = project.get_project_path(check_if_exists=False) self._temp_paths.append(project_path) return project def test_update_scm_url(self): # Handle all of the URL formats supported by the SCM systems: urls_to_test = [ # (scm type, original url, new url, new url with username, new url with username and password) # git: https://www.kernel.org/pub/software/scm/git/docs/git-clone.html#URLS # - ssh://[user@]host.xz[:port]/path/to/repo.git/ ('git', 'ssh://host.xz/path/to/repo.git/', None, 'ssh://testuser@host.xz/path/to/repo.git/', 'ssh://testuser:testpass@host.xz/path/to/repo.git/'), ('git', 'ssh://host.xz:1022/path/to/repo.git', None, 'ssh://testuser@host.xz:1022/path/to/repo.git', 'ssh://testuser:testpass@host.xz:1022/path/to/repo.git'), ('git', 'ssh://user@host.xz/path/to/repo.git/', None, 'ssh://testuser@host.xz/path/to/repo.git/', 'ssh://testuser:testpass@host.xz/path/to/repo.git/'), ('git', 'ssh://user@host.xz:1022/path/to/repo.git', None, 'ssh://testuser@host.xz:1022/path/to/repo.git', 'ssh://testuser:testpass@host.xz:1022/path/to/repo.git'), ('git', 'ssh://user:pass@host.xz/path/to/repo.git/', None, 'ssh://testuser:pass@host.xz/path/to/repo.git/', 'ssh://testuser:testpass@host.xz/path/to/repo.git/'), ('git', 'ssh://user:pass@host.xz:1022/path/to/repo.git', None, 'ssh://testuser:pass@host.xz:1022/path/to/repo.git', 'ssh://testuser:testpass@host.xz:1022/path/to/repo.git'), # - git://host.xz[:port]/path/to/repo.git/ (doesn't really support authentication) ('git', 'git://host.xz/path/to/repo.git/', None, 'git://testuser@host.xz/path/to/repo.git/', 'git://testuser:testpass@host.xz/path/to/repo.git/'), ('git', 'git://host.xz:9418/path/to/repo.git', None, 'git://testuser@host.xz:9418/path/to/repo.git', 'git://testuser:testpass@host.xz:9418/path/to/repo.git'), ('git', 'git://user@host.xz/path/to/repo.git/', None, 'git://testuser@host.xz/path/to/repo.git/', 'git://testuser:testpass@host.xz/path/to/repo.git/'), ('git', 'git://user@host.xz:9418/path/to/repo.git', None, 'git://testuser@host.xz:9418/path/to/repo.git', 'git://testuser:testpass@host.xz:9418/path/to/repo.git'), # - http[s]://host.xz[:port]/path/to/repo.git/ ('git', 'http://host.xz/path/to/repo.git/', None, 'http://testuser@host.xz/path/to/repo.git/', 'http://testuser:testpass@host.xz/path/to/repo.git/'), ('git', 'http://host.xz:8080/path/to/repo.git', None, 'http://testuser@host.xz:8080/path/to/repo.git', 'http://testuser:testpass@host.xz:8080/path/to/repo.git'), ('git', 'http://user@host.xz/path/to/repo.git/', None, 'http://testuser@host.xz/path/to/repo.git/', 'http://testuser:testpass@host.xz/path/to/repo.git/'), ('git', 'http://user@host.xz:8080/path/to/repo.git', None, 'http://testuser@host.xz:8080/path/to/repo.git', 'http://testuser:testpass@host.xz:8080/path/to/repo.git'), ('git', 'http://user:pass@host.xz/path/to/repo.git/', None, 'http://testuser:pass@host.xz/path/to/repo.git/', 'http://testuser:testpass@host.xz/path/to/repo.git/'), ('git', 'http://user:pass@host.xz:8080/path/to/repo.git', None, 'http://testuser:pass@host.xz:8080/path/to/repo.git', 'http://testuser:testpass@host.xz:8080/path/to/repo.git'), ('git', 'https://host.xz/path/to/repo.git/', None, 'https://testuser@host.xz/path/to/repo.git/', 'https://testuser:testpass@host.xz/path/to/repo.git/'), ('git', 'https://host.xz:8443/path/to/repo.git', None, 'https://testuser@host.xz:8443/path/to/repo.git', 'https://testuser:testpass@host.xz:8443/path/to/repo.git'), ('git', 'https://user@host.xz/path/to/repo.git/', None, 'https://testuser@host.xz/path/to/repo.git/', 'https://testuser:testpass@host.xz/path/to/repo.git/'), ('git', 'https://user@host.xz:8443/path/to/repo.git', None, 'https://testuser@host.xz:8443/path/to/repo.git', 'https://testuser:testpass@host.xz:8443/path/to/repo.git'), ('git', 'https://user:pass@host.xz/path/to/repo.git/', None, 'https://testuser:pass@host.xz/path/to/repo.git/', 'https://testuser:testpass@host.xz/path/to/repo.git/'), ('git', 'https://user:pass@host.xz:8443/path/to/repo.git', None, 'https://testuser:pass@host.xz:8443/path/to/repo.git', 'https://testuser:testpass@host.xz:8443/path/to/repo.git'), # - ftp[s]://host.xz[:port]/path/to/repo.git/ ('git', 'ftp://host.xz/path/to/repo.git/', None, 'ftp://testuser@host.xz/path/to/repo.git/', 'ftp://testuser:testpass@host.xz/path/to/repo.git/'), ('git', 'ftp://host.xz:8021/path/to/repo.git', None, 'ftp://testuser@host.xz:8021/path/to/repo.git', 'ftp://testuser:testpass@host.xz:8021/path/to/repo.git'), ('git', 'ftp://user@host.xz/path/to/repo.git/', None, 'ftp://testuser@host.xz/path/to/repo.git/', 'ftp://testuser:testpass@host.xz/path/to/repo.git/'), ('git', 'ftp://user@host.xz:8021/path/to/repo.git', None, 'ftp://testuser@host.xz:8021/path/to/repo.git', 'ftp://testuser:testpass@host.xz:8021/path/to/repo.git'), ('git', 'ftp://user:pass@host.xz/path/to/repo.git/', None, 'ftp://testuser:pass@host.xz/path/to/repo.git/', 'ftp://testuser:testpass@host.xz/path/to/repo.git/'), ('git', 'ftp://user:pass@host.xz:8021/path/to/repo.git', None, 'ftp://testuser:pass@host.xz:8021/path/to/repo.git', 'ftp://testuser:testpass@host.xz:8021/path/to/repo.git'), ('git', 'ftps://host.xz/path/to/repo.git/', None, 'ftps://testuser@host.xz/path/to/repo.git/', 'ftps://testuser:testpass@host.xz/path/to/repo.git/'), ('git', 'ftps://host.xz:8990/path/to/repo.git', None, 'ftps://testuser@host.xz:8990/path/to/repo.git', 'ftps://testuser:testpass@host.xz:8990/path/to/repo.git'), ('git', 'ftps://user@host.xz/path/to/repo.git/', None, 'ftps://testuser@host.xz/path/to/repo.git/', 'ftps://testuser:testpass@host.xz/path/to/repo.git/'), ('git', 'ftps://user@host.xz:8990/path/to/repo.git', None, 'ftps://testuser@host.xz:8990/path/to/repo.git', 'ftps://testuser:testpass@host.xz:8990/path/to/repo.git'), ('git', 'ftps://user:pass@host.xz/path/to/repo.git/', None, 'ftps://testuser:pass@host.xz/path/to/repo.git/', 'ftps://testuser:testpass@host.xz/path/to/repo.git/'), ('git', 'ftps://user:pass@host.xz:8990/path/to/repo.git', None, 'ftps://testuser:pass@host.xz:8990/path/to/repo.git', 'ftps://testuser:testpass@host.xz:8990/path/to/repo.git'), # - rsync://host.xz/path/to/repo.git/ ('git', 'rsync://host.xz/path/to/repo.git/', ValueError, ValueError, ValueError), # - [user@]host.xz:path/to/repo.git/ (SCP style) ('git', 'host.xz:path/to/repo.git/', 'git+ssh://host.xz/path/to/repo.git/', 'git+ssh://testuser@host.xz/path/to/repo.git/', 'git+ssh://testuser:testpass@host.xz/path/to/repo.git/'), ('git', 'user@host.xz:path/to/repo.git/', 'git+ssh://user@host.xz/path/to/repo.git/', 'git+ssh://testuser@host.xz/path/to/repo.git/', 'git+ssh://testuser:testpass@host.xz/path/to/repo.git/'), ('git', 'user:pass@host.xz:path/to/repo.git/', 'git+ssh://user:pass@host.xz/path/to/repo.git/', 'git+ssh://testuser:pass@host.xz/path/to/repo.git/', 'git+ssh://testuser:testpass@host.xz/path/to/repo.git/'), ('git', 'host.xz:~/path/to/repo.git/', 'git+ssh://host.xz/~/path/to/repo.git/', 'git+ssh://testuser@host.xz/~/path/to/repo.git/', 'git+ssh://testuser:testpass@host.xz/~/path/to/repo.git/'), ('git', 'user@host.xz:~/path/to/repo.git/', 'git+ssh://user@host.xz/~/path/to/repo.git/', 'git+ssh://testuser@host.xz/~/path/to/repo.git/', 'git+ssh://testuser:testpass@host.xz/~/path/to/repo.git/'), ('git', 'user:pass@host.xz:~/path/to/repo.git/', 'git+ssh://user:pass@host.xz/~/path/to/repo.git/', 'git+ssh://testuser:pass@host.xz/~/path/to/repo.git/', 'git+ssh://testuser:testpass@host.xz/~/path/to/repo.git/'), ('git', 'host.xz:/path/to/repo.git/', 'git+ssh://host.xz//path/to/repo.git/', 'git+ssh://testuser@host.xz//path/to/repo.git/', 'git+ssh://testuser:testpass@host.xz//path/to/repo.git/'), ('git', 'user@host.xz:/path/to/repo.git/', 'git+ssh://user@host.xz//path/to/repo.git/', 'git+ssh://testuser@host.xz//path/to/repo.git/', 'git+ssh://testuser:testpass@host.xz//path/to/repo.git/'), ('git', 'user:pass@host.xz:/path/to/repo.git/', 'git+ssh://user:pass@host.xz//path/to/repo.git/', 'git+ssh://testuser:pass@host.xz//path/to/repo.git/', 'git+ssh://testuser:testpass@host.xz//path/to/repo.git/'), # - /path/to/repo.git/ (local file) ('git', '/path/to/repo.git', ValueError, ValueError, ValueError), ('git', 'path/to/repo.git', ValueError, ValueError, ValueError), # - file:///path/to/repo.git/ ('git', 'file:///path/to/repo.git', ValueError, ValueError, ValueError), ('git', 'file://localhost/path/to/repo.git', ValueError, ValueError, ValueError), # Invalid SSH URLs: ('git', 'ssh:github.com:ansible/ansible-examples.git', ValueError, ValueError, ValueError), ('git', 'ssh://github.com:ansible/ansible-examples.git', ValueError, ValueError, ValueError), # Special case for github URLs: ('git', 'git@github.com:ansible/ansible-examples.git', 'git+ssh://git@github.com/ansible/ansible-examples.git', ValueError, ValueError), ('git', 'bob@github.com:ansible/ansible-examples.git', ValueError, ValueError, ValueError), # Special case for bitbucket URLs: ('git', 'ssh://git@bitbucket.org/foo/bar.git', None, ValueError, ValueError), ('git', 'ssh://git@altssh.bitbucket.org:443/foo/bar.git', None, ValueError, ValueError), ('git', 'ssh://hg@bitbucket.org/foo/bar.git', ValueError, ValueError, ValueError), ('git', 'ssh://hg@altssh.bitbucket.org:443/foo/bar.git', ValueError, ValueError, ValueError), # hg: http://www.selenic.com/mercurial/hg.1.html#url-paths # - local/filesystem/path[#revision] ('hg', '/path/to/repo', ValueError, ValueError, ValueError), ('hg', 'path/to/repo/', ValueError, ValueError, ValueError), ('hg', '/path/to/repo#rev', ValueError, ValueError, ValueError), ('hg', 'path/to/repo/#rev', ValueError, ValueError, ValueError), # - file://local/filesystem/path[#revision] ('hg', 'file:///path/to/repo', ValueError, ValueError, ValueError), ('hg', 'file://localhost/path/to/repo/', ValueError, ValueError, ValueError), ('hg', 'file:///path/to/repo#rev', ValueError, ValueError, ValueError), ('hg', 'file://localhost/path/to/repo/#rev', ValueError, ValueError, ValueError), # - http://[user[:pass]@]host[:port]/[path][#revision] ('hg', 'http://host.xz/path/to/repo/', None, 'http://testuser@host.xz/path/to/repo/', 'http://testuser:testpass@host.xz/path/to/repo/'), ('hg', 'http://host.xz:8080/path/to/repo', None, 'http://testuser@host.xz:8080/path/to/repo', 'http://testuser:testpass@host.xz:8080/path/to/repo'), ('hg', 'http://user@host.xz/path/to/repo/', None, 'http://testuser@host.xz/path/to/repo/', 'http://testuser:testpass@host.xz/path/to/repo/'), ('hg', 'http://user@host.xz:8080/path/to/repo', None, 'http://testuser@host.xz:8080/path/to/repo', 'http://testuser:testpass@host.xz:8080/path/to/repo'), ('hg', 'http://user:pass@host.xz/path/to/repo/', None, 'http://testuser:pass@host.xz/path/to/repo/', 'http://testuser:testpass@host.xz/path/to/repo/'), ('hg', 'http://user:pass@host.xz:8080/path/to/repo', None, 'http://testuser:pass@host.xz:8080/path/to/repo', 'http://testuser:testpass@host.xz:8080/path/to/repo'), ('hg', 'http://host.xz/path/to/repo/#rev', None, 'http://testuser@host.xz/path/to/repo/#rev', 'http://testuser:testpass@host.xz/path/to/repo/#rev'), ('hg', 'http://host.xz:8080/path/to/repo#rev', None, 'http://testuser@host.xz:8080/path/to/repo#rev', 'http://testuser:testpass@host.xz:8080/path/to/repo#rev'), ('hg', 'http://user@host.xz/path/to/repo/#rev', None, 'http://testuser@host.xz/path/to/repo/#rev', 'http://testuser:testpass@host.xz/path/to/repo/#rev'), ('hg', 'http://user@host.xz:8080/path/to/repo#rev', None, 'http://testuser@host.xz:8080/path/to/repo#rev', 'http://testuser:testpass@host.xz:8080/path/to/repo#rev'), ('hg', 'http://user:pass@host.xz/path/to/repo/#rev', None, 'http://testuser:pass@host.xz/path/to/repo/#rev', 'http://testuser:testpass@host.xz/path/to/repo/#rev'), ('hg', 'http://user:pass@host.xz:8080/path/to/repo#rev', None, 'http://testuser:pass@host.xz:8080/path/to/repo#rev', 'http://testuser:testpass@host.xz:8080/path/to/repo#rev'), # - https://[user[:pass]@]host[:port]/[path][#revision] ('hg', 'https://host.xz/path/to/repo/', None, 'https://testuser@host.xz/path/to/repo/', 'https://testuser:testpass@host.xz/path/to/repo/'), ('hg', 'https://host.xz:8443/path/to/repo', None, 'https://testuser@host.xz:8443/path/to/repo', 'https://testuser:testpass@host.xz:8443/path/to/repo'), ('hg', 'https://user@host.xz/path/to/repo/', None, 'https://testuser@host.xz/path/to/repo/', 'https://testuser:testpass@host.xz/path/to/repo/'), ('hg', 'https://user@host.xz:8443/path/to/repo', None, 'https://testuser@host.xz:8443/path/to/repo', 'https://testuser:testpass@host.xz:8443/path/to/repo'), ('hg', 'https://user:pass@host.xz/path/to/repo/', None, 'https://testuser:pass@host.xz/path/to/repo/', 'https://testuser:testpass@host.xz/path/to/repo/'), ('hg', 'https://user:pass@host.xz:8443/path/to/repo', None, 'https://testuser:pass@host.xz:8443/path/to/repo', 'https://testuser:testpass@host.xz:8443/path/to/repo'), ('hg', 'https://host.xz/path/to/repo/#rev', None, 'https://testuser@host.xz/path/to/repo/#rev', 'https://testuser:testpass@host.xz/path/to/repo/#rev'), ('hg', 'https://host.xz:8443/path/to/repo#rev', None, 'https://testuser@host.xz:8443/path/to/repo#rev', 'https://testuser:testpass@host.xz:8443/path/to/repo#rev'), ('hg', 'https://user@host.xz/path/to/repo/#rev', None, 'https://testuser@host.xz/path/to/repo/#rev', 'https://testuser:testpass@host.xz/path/to/repo/#rev'), ('hg', 'https://user@host.xz:8443/path/to/repo#rev', None, 'https://testuser@host.xz:8443/path/to/repo#rev', 'https://testuser:testpass@host.xz:8443/path/to/repo#rev'), ('hg', 'https://user:pass@host.xz/path/to/repo/#rev', None, 'https://testuser:pass@host.xz/path/to/repo/#rev', 'https://testuser:testpass@host.xz/path/to/repo/#rev'), ('hg', 'https://user:pass@host.xz:8443/path/to/repo#rev', None, 'https://testuser:pass@host.xz:8443/path/to/repo#rev', 'https://testuser:testpass@host.xz:8443/path/to/repo#rev'), # - ssh://[user@]host[:port]/[path][#revision] # Password is always stripped out for hg when using SSH. ('hg', 'ssh://host.xz/path/to/repo/', None, 'ssh://testuser@host.xz/path/to/repo/', 'ssh://testuser@host.xz/path/to/repo/'), ('hg', 'ssh://host.xz:1022/path/to/repo', None, 'ssh://testuser@host.xz:1022/path/to/repo', 'ssh://testuser@host.xz:1022/path/to/repo'), ('hg', 'ssh://user@host.xz/path/to/repo/', None, 'ssh://testuser@host.xz/path/to/repo/', 'ssh://testuser@host.xz/path/to/repo/'), ('hg', 'ssh://user@host.xz:1022/path/to/repo', None, 'ssh://testuser@host.xz:1022/path/to/repo', 'ssh://testuser@host.xz:1022/path/to/repo'), ('hg', 'ssh://user:pass@host.xz/path/to/repo/', 'ssh://user@host.xz/path/to/repo/', 'ssh://testuser@host.xz/path/to/repo/', 'ssh://testuser@host.xz/path/to/repo/'), ('hg', 'ssh://user:pass@host.xz:1022/path/to/repo', 'ssh://user@host.xz:1022/path/to/repo', 'ssh://testuser@host.xz:1022/path/to/repo', 'ssh://testuser@host.xz:1022/path/to/repo'), ('hg', 'ssh://host.xz/path/to/repo/#rev', None, 'ssh://testuser@host.xz/path/to/repo/#rev', 'ssh://testuser@host.xz/path/to/repo/#rev'), ('hg', 'ssh://host.xz:1022/path/to/repo#rev', None, 'ssh://testuser@host.xz:1022/path/to/repo#rev', 'ssh://testuser@host.xz:1022/path/to/repo#rev'), ('hg', 'ssh://user@host.xz/path/to/repo/#rev', None, 'ssh://testuser@host.xz/path/to/repo/#rev', 'ssh://testuser@host.xz/path/to/repo/#rev'), ('hg', 'ssh://user@host.xz:1022/path/to/repo#rev', None, 'ssh://testuser@host.xz:1022/path/to/repo#rev', 'ssh://testuser@host.xz:1022/path/to/repo#rev'), ('hg', 'ssh://user:pass@host.xz/path/to/repo/#rev', 'ssh://user@host.xz/path/to/repo/#rev', 'ssh://testuser@host.xz/path/to/repo/#rev', 'ssh://testuser@host.xz/path/to/repo/#rev'), ('hg', 'ssh://user:pass@host.xz:1022/path/to/repo#rev', 'ssh://user@host.xz:1022/path/to/repo#rev', 'ssh://testuser@host.xz:1022/path/to/repo#rev', 'ssh://testuser@host.xz:1022/path/to/repo#rev'), # Special case for bitbucket URLs: ('hg', 'ssh://hg@bitbucket.org/foo/bar', None, ValueError, ValueError), ('hg', 'ssh://hg@altssh.bitbucket.org:443/foo/bar', None, ValueError, ValueError), ('hg', 'ssh://bob@bitbucket.org/foo/bar', ValueError, ValueError, ValueError), ('hg', 'ssh://bob@altssh.bitbucket.org:443/foo/bar', ValueError, ValueError, ValueError), # svn: http://svnbook.red-bean.com/en/1.7/svn-book.html#svn.advanced.reposurls # - file:/// Direct repository access (on local disk) ('svn', 'file:///path/to/repo', ValueError, ValueError, ValueError), ('svn', 'file://localhost/path/to/repo/', ValueError, ValueError, ValueError), # - http:// Access via WebDAV protocol to Subversion-aware Apache server ('svn', 'http://host.xz/path/to/repo/', None, 'http://testuser@host.xz/path/to/repo/', 'http://testuser:testpass@host.xz/path/to/repo/'), ('svn', 'http://host.xz:8080/path/to/repo', None, 'http://testuser@host.xz:8080/path/to/repo', 'http://testuser:testpass@host.xz:8080/path/to/repo'), ('svn', 'http://user@host.xz/path/to/repo/', None, 'http://testuser@host.xz/path/to/repo/', 'http://testuser:testpass@host.xz/path/to/repo/'), ('svn', 'http://user@host.xz:8080/path/to/repo', None, 'http://testuser@host.xz:8080/path/to/repo', 'http://testuser:testpass@host.xz:8080/path/to/repo'), ('svn', 'http://user:pass@host.xz/path/to/repo/', None, 'http://testuser:pass@host.xz/path/to/repo/', 'http://testuser:testpass@host.xz/path/to/repo/'), ('svn', 'http://user:pass@host.xz:8080/path/to/repo', None, 'http://testuser:pass@host.xz:8080/path/to/repo', 'http://testuser:testpass@host.xz:8080/path/to/repo'), # - https:// Same as http://, but with SSL encryption ('svn', 'https://host.xz/path/to/repo/', None, 'https://testuser@host.xz/path/to/repo/', 'https://testuser:testpass@host.xz/path/to/repo/'), ('svn', 'https://host.xz:8080/path/to/repo', None, 'https://testuser@host.xz:8080/path/to/repo', 'https://testuser:testpass@host.xz:8080/path/to/repo'), ('svn', 'https://user@host.xz/path/to/repo/', None, 'https://testuser@host.xz/path/to/repo/', 'https://testuser:testpass@host.xz/path/to/repo/'), ('svn', 'https://user@host.xz:8080/path/to/repo', None, 'https://testuser@host.xz:8080/path/to/repo', 'https://testuser:testpass@host.xz:8080/path/to/repo'), ('svn', 'https://user:pass@host.xz/path/to/repo/', None, 'https://testuser:pass@host.xz/path/to/repo/', 'https://testuser:testpass@host.xz/path/to/repo/'), ('svn', 'https://user:pass@host.xz:8080/path/to/repo', None, 'https://testuser:pass@host.xz:8080/path/to/repo', 'https://testuser:testpass@host.xz:8080/path/to/repo'), # - svn:// Access via custom protocol to an svnserve server ('svn', 'svn://host.xz/path/to/repo/', None, 'svn://testuser@host.xz/path/to/repo/', 'svn://testuser:testpass@host.xz/path/to/repo/'), ('svn', 'svn://host.xz:3690/path/to/repo', None, 'svn://testuser@host.xz:3690/path/to/repo', 'svn://testuser:testpass@host.xz:3690/path/to/repo'), ('svn', 'svn://user@host.xz/path/to/repo/', None, 'svn://testuser@host.xz/path/to/repo/', 'svn://testuser:testpass@host.xz/path/to/repo/'), ('svn', 'svn://user@host.xz:3690/path/to/repo', None, 'svn://testuser@host.xz:3690/path/to/repo', 'svn://testuser:testpass@host.xz:3690/path/to/repo'), ('svn', 'svn://user:pass@host.xz/path/to/repo/', None, 'svn://testuser:pass@host.xz/path/to/repo/', 'svn://testuser:testpass@host.xz/path/to/repo/'), ('svn', 'svn://user:pass@host.xz:3690/path/to/repo', None, 'svn://testuser:pass@host.xz:3690/path/to/repo', 'svn://testuser:testpass@host.xz:3690/path/to/repo'), # - svn+ssh:// Same as svn://, but through an SSH tunnel ('svn', 'svn+ssh://host.xz/path/to/repo/', None, 'svn+ssh://testuser@host.xz/path/to/repo/', 'svn+ssh://testuser:testpass@host.xz/path/to/repo/'), ('svn', 'svn+ssh://host.xz:1022/path/to/repo', None, 'svn+ssh://testuser@host.xz:1022/path/to/repo', 'svn+ssh://testuser:testpass@host.xz:1022/path/to/repo'), ('svn', 'svn+ssh://user@host.xz/path/to/repo/', None, 'svn+ssh://testuser@host.xz/path/to/repo/', 'svn+ssh://testuser:testpass@host.xz/path/to/repo/'), ('svn', 'svn+ssh://user@host.xz:1022/path/to/repo', None, 'svn+ssh://testuser@host.xz:1022/path/to/repo', 'svn+ssh://testuser:testpass@host.xz:1022/path/to/repo'), ('svn', 'svn+ssh://user:pass@host.xz/path/to/repo/', None, 'svn+ssh://testuser:pass@host.xz/path/to/repo/', 'svn+ssh://testuser:testpass@host.xz/path/to/repo/'), ('svn', 'svn+ssh://user:pass@host.xz:1022/path/to/repo', None, 'svn+ssh://testuser:pass@host.xz:1022/path/to/repo', 'svn+ssh://testuser:testpass@host.xz:1022/path/to/repo'), ] # Some invalid URLs. for scm_type in ('git', 'svn', 'hg'): urls_to_test.append((scm_type, 'host', ValueError, ValueError, ValueError)) urls_to_test.append((scm_type, '/path', ValueError, ValueError, ValueError)) urls_to_test.append((scm_type, 'mailto:joe@example.com', ValueError, ValueError, ValueError)) urls_to_test.append((scm_type, 'telnet://host.xz/path/to/repo', ValueError, ValueError, ValueError)) def is_exception(e): return bool(isinstance(e, Exception) or (isinstance(e, type) and issubclass(e, Exception))) for url_opts in urls_to_test: scm_type, url, new_url, new_url_u, new_url_up = url_opts new_url = new_url or url new_url_u = new_url_u or url new_url_up = new_url_up or url # Check existing URL as-is. if is_exception(new_url): self.assertRaises(new_url, update_scm_url, scm_type, url) else: updated_url = update_scm_url(scm_type, url) self.assertEqual(new_url, updated_url) if updated_url.startswith('git+ssh://'): new_url2 = new_url.replace('git+ssh://', '', 1).replace('/', ':', 1) updated_url2 = update_scm_url(scm_type, url, scp_format=True) self.assertEqual(new_url2, updated_url2) # Check URL with username replaced. if is_exception(new_url_u): self.assertRaises(new_url_u, update_scm_url, scm_type, url, username='testuser') else: updated_url = update_scm_url(scm_type, url, username='testuser') self.assertEqual(new_url_u, updated_url) if updated_url.startswith('git+ssh://'): new_url2 = new_url_u.replace('git+ssh://', '', 1).replace('/', ':', 1) updated_url2 = update_scm_url(scm_type, url, username='testuser', scp_format=True) self.assertEqual(new_url2, updated_url2) # Check URL with username and password replaced. if is_exception(new_url_up): self.assertRaises(new_url_up, update_scm_url, scm_type, url, username='testuser', password='testpass') else: updated_url = update_scm_url(scm_type, url, username='testuser', password='testpass') self.assertEqual(new_url_up, updated_url) if updated_url.startswith('git+ssh://'): new_url2 = new_url_up.replace('git+ssh://', '', 1).replace('/', ':', 1) updated_url2 = update_scm_url(scm_type, url, username='testuser', password='testpass', scp_format=True) self.assertEqual(new_url2, updated_url2) def is_public_key_in_authorized_keys(self): auth_keys = set() auth_keys_path = os.path.expanduser('~/.ssh/authorized_keys') if os.path.exists(auth_keys_path): for line in file(auth_keys_path, 'r'): if line.strip(): key = tuple(line.strip().split()[:2]) auth_keys.add(key) pub_keys = set() rsa_key_path = os.path.expanduser('~/.ssh/id_rsa.pub') if os.path.exists(rsa_key_path): for line in file(rsa_key_path, 'r'): if line.strip(): key = tuple(line.strip().split()[:2]) pub_keys.add(key) dsa_key_path = os.path.expanduser('~/.ssh/id_dsa.pub') if os.path.exists(dsa_key_path): for line in file(dsa_key_path, 'r'): if line.strip(): key = tuple(line.strip().split()[:2]) pub_keys.add(key) return bool(auth_keys & pub_keys) def check_project_update(self, project, should_fail=False, **kwargs): pu = kwargs.pop('project_update', None) should_error = kwargs.pop('should_error', False) if not pu: pu = project.update(**kwargs) self.assertTrue(pu) pu = ProjectUpdate.objects.get(pk=pu.pk) if should_error: self.assertEqual(pu.status, 'error', pu.result_stdout + pu.result_traceback) elif should_fail: self.assertEqual(pu.status, 'failed', pu.result_stdout + pu.result_traceback) elif should_fail is False: self.assertEqual(pu.status, 'successful', pu.result_stdout + pu.result_traceback) else: #print pu.result_stdout pass # If should_fail is None, we don't care. # Get the SCM URL from the job args, if it starts with a '/' we aren't # handling the URL correctly. if not should_error: scm_url_in_args_re = re.compile(r'\\(?:\\\\)??"scm_url\\(?:\\\\)??": \\(?:\\\\)??"(.*?)\\(?:\\\\)??"') match = scm_url_in_args_re.search(pu.job_args) self.assertTrue(match, pu.job_args) scm_url_in_args = match.groups()[0] self.assertFalse(scm_url_in_args.startswith('/'), scm_url_in_args) #return pu # Make sure scm_password doesn't show up anywhere in args or output # from project update. if project.credential: scm_password = kwargs.get('scm_password', decrypt_field(project.credential, 'password')) if scm_password: self.assertFalse(scm_password in pu.job_args, pu.job_args) self.assertFalse(scm_password in json.dumps(pu.job_env), json.dumps(pu.job_env)) # FIXME: Not filtering password from stdout since saving it # directly to a file. #self.assertFalse(scm_password in pu.result_stdout, # pu.result_stdout) self.assertFalse(scm_password in pu.result_traceback, pu.result_traceback) # Make sure scm_key_unlock doesn't show up anywhere in args or output # from project update. if project.credential: scm_key_unlock = kwargs.get('scm_key_unlock', decrypt_field(project.credential, 'ssh_key_unlock')) if scm_key_unlock: self.assertFalse(scm_key_unlock in pu.job_args, pu.job_args) self.assertFalse(scm_key_unlock in json.dumps(pu.job_env), json.dumps(pu.job_env)) self.assertFalse(scm_key_unlock in pu.result_stdout, pu.result_stdout) self.assertFalse(scm_key_unlock in pu.result_traceback, pu.result_traceback) project = Project.objects.get(pk=project.pk) self.assertEqual(project.last_update, pu) self.assertEqual(project.last_update_failed, pu.failed) return pu def change_file_in_project(self, project): project_path = project.get_project_path() self.assertTrue(project_path) for root, dirs, files in os.walk(project_path): for f in files: if f.startswith('.') or f == 'yadayada.txt': continue path_parts = os.path.relpath(root, project_path).split(os.sep) if any([x.startswith('.') and x != '.' for x in path_parts]): continue path = os.path.join(root, f) before = file(path, 'rb').read() #print 'changed', path file(path, 'wb').write('CHANGED FILE') after = file(path, 'rb').read() return path, before, after self.fail('no file found to change!') def check_project_scm(self, project): project = Project.objects.get(pk=project.pk) project_path = project.get_project_path(check_if_exists=False) # If project could be auto-updated on creation, the project dir should # already exist, otherwise run an initial checkout. if project.scm_type: self.assertTrue(project.last_update) self.check_project_update(project, project_update=project.last_update) self.assertTrue(os.path.exists(project_path)) else: self.assertFalse(os.path.exists(project_path)) self.check_project_update(project) self.assertTrue(os.path.exists(project_path)) # TODO: Removed pending resolution of: https://github.com/ansible/ansible/issues/6582 # # Stick a new untracked file in the project. untracked_path = os.path.join(project_path, 'yadayada.txt') self.assertFalse(os.path.exists(untracked_path)) file(untracked_path, 'wb').write('yabba dabba doo') self.assertTrue(os.path.exists(untracked_path)) # # Update to existing checkout (should leave untracked file alone). # self.check_project_update(project) # self.assertTrue(os.path.exists(untracked_path)) # # Change file then update (with scm_clean=False). Modified file should # # not be changed. # self.assertFalse(project.scm_clean) # modified_path, before, after = self.change_file_in_project(project) # # Mercurial still returns successful if a modified file is present. # should_fail = bool(project.scm_type != 'hg') # self.check_project_update(project, should_fail=should_fail) # content = file(modified_path, 'rb').read() # self.assertEqual(content, after) # self.assertTrue(os.path.exists(untracked_path)) # # Set scm_clean=True then try to update again. Modified file should # # have been replaced with the original. Untracked file should still be # # present. # project.scm_clean = True # project.save() # self.check_project_update(project) # content = file(modified_path, 'rb').read() # self.assertEqual(content, before) # self.assertTrue(os.path.exists(untracked_path)) # # If scm_type or scm_url changes, scm_delete_on_next_update should be # # set, causing project directory (including untracked file) to be # # completely blown away, but only for the next update.. # self.assertFalse(project.scm_delete_on_update) # self.assertFalse(project.scm_delete_on_next_update) # scm_type = project.scm_type # project.scm_type = '' # project.save() # self.assertTrue(project.scm_delete_on_next_update) # project.scm_type = scm_type # project.save() # self.check_project_update(project) # self.assertFalse(os.path.exists(untracked_path)) # # Check that the flag is cleared after the update, and that an # # untracked file isn't blown away. # project = Project.objects.get(pk=project.pk) # self.assertFalse(project.scm_delete_on_next_update) # file(untracked_path, 'wb').write('yabba dabba doo') # self.assertTrue(os.path.exists(untracked_path)) # self.check_project_update(project) # self.assertTrue(os.path.exists(untracked_path)) # Set scm_delete_on_update=True then update again. Project directory # (including untracked file) should be completely blown away. self.assertFalse(project.scm_delete_on_update) project.scm_delete_on_update = True project.save() self.check_project_update(project) self.assertFalse(os.path.exists(untracked_path)) # Change username/password for private projects and verify the update # fails (but doesn't cause the task to hang). scm_url_parts = urlparse.urlsplit(project.scm_url) # FIXME: Implement these tests again with new credentials! if 0 and project.scm_username and project.scm_password: scm_username = project.scm_username should_still_fail = not (getpass.getuser() == scm_username and scm_url_parts.hostname == 'localhost' and 'ssh' in scm_url_parts.scheme and self.is_public_key_in_authorized_keys()) # Clear username only. project = Project.objects.get(pk=project.pk) project.scm_username = '' project.save() self.check_project_update(project, should_fail=should_still_fail) # Try invalid username. project = Project.objects.get(pk=project.pk) project.scm_username = 'not a\\ valid\' user" name' project.save() self.check_project_update(project, should_fail=True) # Clear username and password. project = Project.objects.get(pk=project.pk) project.scm_username = '' project.scm_password = '' project.save() self.check_project_update(project, should_fail=should_still_fail) # Set username, but no password. project = Project.objects.get(pk=project.pk) project.scm_username = scm_username project.save() self.check_project_update(project, should_fail=should_still_fail) # Set username, with invalid password. project = Project.objects.get(pk=project.pk) project.scm_password = 'not a\\ valid\' "password' project.save() if project.scm_type == 'svn': self.check_project_update(project, should_fail=True) # should_still_fail) else: self.check_project_update(project, should_fail=should_still_fail) # Test that we can delete project updates. for pu in project.project_updates.filter(active=True): pu_url = reverse('api:project_update_detail', args=(pu.pk,)) with self.current_user(self.super_django_user): self.delete(pu_url, expect=204) def test_create_project_with_scm(self): scm_url = getattr(settings, 'TEST_GIT_PUBLIC_HTTPS', 'https://github.com/ansible/ansible.github.com.git') if not all([scm_url]): self.skipTest('no public git repo defined for https!') projects_url = reverse('api:project_list') credentials_url = reverse('api:credential_list') # Test basic project creation without a credential. project_data = { 'name': 'my public git project over https', 'scm_type': 'git', 'scm_url': scm_url, } with self.current_user(self.super_django_user): self.post(projects_url, project_data, expect=201) # Test with an invalid URL. project_data = { 'name': 'my local git project', 'scm_type': 'git', 'scm_url': 'file:///path/to/repo.git', } with self.current_user(self.super_django_user): self.post(projects_url, project_data, expect=400) # Test creation with a credential. credential_data = { 'name': 'my scm credential', 'kind': 'scm', 'user': self.super_django_user.pk, 'username': 'testuser', 'password': 'testpass', } with self.current_user(self.super_django_user): response = self.post(credentials_url, credential_data, expect=201) credential_id = response['id'] project_data = { 'name': 'my git project over https with credential', 'scm_type': 'git', 'scm_url': scm_url, 'credential': credential_id, } with self.current_user(self.super_django_user): self.post(projects_url, project_data, expect=201) # Test creation with an invalid credential type. ssh_credential_data = { 'name': 'my ssh credential', 'kind': 'ssh', 'user': self.super_django_user.pk, 'username': 'testuser', 'password': 'testpass', } with self.current_user(self.super_django_user): response = self.post(credentials_url, ssh_credential_data, expect=201) ssh_credential_id = response['id'] project_data = { 'name': 'my git project with invalid credential type', 'scm_type': 'git', 'scm_url': scm_url, 'credential': ssh_credential_id, } with self.current_user(self.super_django_user): self.post(projects_url, project_data, expect=400) # Test special case for github/bitbucket URLs. project_data = { 'name': 'my github project over ssh', 'scm_type': 'git', 'scm_url': 'ssh://git@github.com/ansible/ansible.github.com.git', 'credential': credential_id, } with self.current_user(self.super_django_user): self.post(projects_url, project_data, expect=201) def test_public_git_project_over_https(self): scm_url = getattr(settings, 'TEST_GIT_PUBLIC_HTTPS', 'https://github.com/ansible/ansible.github.com.git') if not all([scm_url]): self.skipTest('no public git repo defined for https!') project = self.create_project( name='my public git project over https', scm_type='git', scm_url=scm_url, ) self.check_project_scm(project) # Test passing username/password for public project. Though they're not # needed, the update should still work. scm_username = getattr(settings, 'TEST_GIT_USERNAME', '') scm_password = getattr(settings, 'TEST_GIT_PASSWORD', '') if scm_username or scm_password: project2 = self.create_project( name='my other public git project over https', scm_type='git', scm_url=scm_url, scm_username=scm_username, scm_password=scm_password, ) self.check_project_update(project2) def test_private_git_project_over_https(self): scm_url = getattr(settings, 'TEST_GIT_PRIVATE_HTTPS', '') scm_username = getattr(settings, 'TEST_GIT_USERNAME', '') scm_password = getattr(settings, 'TEST_GIT_PASSWORD', '') if not all([scm_url, scm_username, scm_password]): self.skipTest('no private git repo defined for https!') project = self.create_project( name='my private git project over https', scm_type='git', scm_url=scm_url, scm_username=scm_username, scm_password=scm_password, ) self.check_project_scm(project) def test_private_git_project_over_ssh(self): scm_url = getattr(settings, 'TEST_GIT_PRIVATE_SSH', '') scm_key_data = getattr(settings, 'TEST_GIT_KEY_DATA', '') scm_username = getattr(settings, 'TEST_GIT_USERNAME', '') scm_password = 'blahblahblah' # getattr(settings, 'TEST_GIT_PASSWORD', '') if not all([scm_url, scm_key_data, scm_username, scm_password]): self.skipTest('no private git repo defined for ssh!') project = self.create_project( name='my private git project over ssh', scm_type='git', scm_url=scm_url, scm_key_data=scm_key_data, ) self.check_project_scm(project) # Test project using SSH username/password instead of key. Should fail # because of bad password, but never hang. project2 = self.create_project( name='my other private git project over ssh', scm_type='git', scm_url=scm_url, scm_username=scm_username, scm_password=scm_password, ) bool('github.com' in scm_url and scm_username != 'git') self.check_project_update(project2, should_fail=None) # , should_error=should_error) def test_scm_key_unlock_on_project_update(self): scm_url = 'git@github.com:ansible/ansible.github.com.git' project = self.create_project( name='my git project over ssh with encrypted key', scm_type='git', scm_url=scm_url, scm_key_data=TEST_SSH_KEY_DATA_LOCKED, scm_key_unlock=TEST_SSH_KEY_DATA_UNLOCK, ) 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']) with self.current_user(self.super_django_user): response = self.post(url, {}, expect=202) project_update = project.project_updates.order_by('-pk')[0] self.check_project_update(project, should_fail=None, project_update=project_update) # Verify that we responded to ssh-agent prompt. self.assertTrue('Identity added' in project_update.result_stdout, project_update.result_stdout) # Try again with a bad unlock password. project = self.create_project( name='my git project over ssh with encrypted key and bad pass', scm_type='git', scm_url=scm_url, scm_key_data=TEST_SSH_KEY_DATA_LOCKED, scm_key_unlock='not the right password', ) with self.current_user(self.super_django_user): response = self.get(url, expect=200) self.assertTrue(response['can_update']) with self.current_user(self.super_django_user): response = self.post(url, {}, expect=202) project_update = project.project_updates.order_by('-pk')[0] self.check_project_update(project, should_fail=None, project_update=project_update) # Verify response to ssh-agent prompt, did not accept password. self.assertTrue('Bad passphrase' in project_update.result_stdout, project_update.result_stdout) self.assertFalse('Identity added' in project_update.result_stdout, project_update.result_stdout) def create_local_git_repo(self): repo_dir = tempfile.mkdtemp() self._temp_paths.append(repo_dir) handle, playbook_path = tempfile.mkstemp(suffix='.yml', dir=repo_dir) test_playbook_file = os.fdopen(handle, 'w') test_playbook_file.write(TEST_PLAYBOOK) test_playbook_file.close() subprocess.check_call(['git', 'init', '.'], cwd=repo_dir, stdout=subprocess.PIPE, stderr=subprocess.PIPE) subprocess.check_call(['git', 'add', os.path.basename(playbook_path)], cwd=repo_dir, stdout=subprocess.PIPE, stderr=subprocess.PIPE) subprocess.check_call(['git', 'commit', '-m', 'blah'], cwd=repo_dir, stdout=subprocess.PIPE, stderr=subprocess.PIPE) return repo_dir def _test_git_project_from_local_path(self): repo_dir = self.create_local_git_repo() project = self.create_project( name='my git project from local path', scm_type='git', scm_url=repo_dir, ) self.check_project_scm(project) def test_git_project_via_ssh_loopback(self): scm_username = getattr(settings, 'TEST_SSH_LOOPBACK_USERNAME', '') scm_password = getattr(settings, 'TEST_SSH_LOOPBACK_PASSWORD', '') if not all([scm_username, scm_password]): self.skipTest('no ssh loopback username/password defined!') repo_dir = self.create_local_git_repo() scm_url = 'ssh://localhost%s' % repo_dir project = self.create_project( name='my git project via ssh loopback', scm_type='git', scm_url=scm_url, scm_username=scm_username, scm_password=scm_password, ) self.check_project_scm(project) def test_public_hg_project_over_https(self): scm_url = getattr(settings, 'TEST_HG_PUBLIC_HTTPS', 'https://bitbucket.org/cchurch/django-hotrunner') if not all([scm_url]): self.skipTest('no public hg repo defined for https!') project = self.create_project( name='my public hg project over https', scm_type='hg', scm_url=scm_url, ) self.check_project_scm(project) # Test passing username/password for public project. Though they're not # needed, the update should still work. scm_username = getattr(settings, 'TEST_HG_USERNAME', '') scm_password = getattr(settings, 'TEST_HG_PASSWORD', '') if scm_username or scm_password: project2 = self.create_project( name='my other public hg project over https', scm_type='hg', scm_url=scm_url, scm_username=scm_username, scm_password=scm_password, ) self.check_project_update(project2) def test_private_hg_project_over_https(self): scm_url = getattr(settings, 'TEST_HG_PRIVATE_HTTPS', '') scm_username = getattr(settings, 'TEST_HG_USERNAME', '') scm_password = getattr(settings, 'TEST_HG_PASSWORD', '') if not all([scm_url, scm_username, scm_password]): self.skipTest('no private hg repo defined for https!') project = self.create_project( name='my private hg project over https', scm_type='hg', scm_url=scm_url, scm_username=scm_username, scm_password=scm_password, ) self.check_project_scm(project) def test_private_hg_project_over_ssh(self): scm_url = getattr(settings, 'TEST_HG_PRIVATE_SSH', '') scm_key_data = getattr(settings, 'TEST_HG_KEY_DATA', '') if not all([scm_url, scm_key_data]): self.skipTest('no private hg repo defined for ssh!') project = self.create_project( name='my private hg project over ssh', scm_type='hg', scm_url=scm_url, scm_key_data=scm_key_data, ) self.check_project_scm(project) # hg doesn't support password for ssh:// urls. def create_local_hg_repo(self): repo_dir = tempfile.mkdtemp() self._temp_paths.append(repo_dir) handle, playbook_path = tempfile.mkstemp(suffix='.yml', dir=repo_dir) test_playbook_file = os.fdopen(handle, 'w') test_playbook_file.write(TEST_PLAYBOOK) test_playbook_file.close() subprocess.check_call(['hg', 'init', '.'], cwd=repo_dir, stdout=subprocess.PIPE, stderr=subprocess.PIPE) subprocess.check_call(['hg', 'add', os.path.basename(playbook_path)], cwd=repo_dir, stdout=subprocess.PIPE, stderr=subprocess.PIPE) subprocess.check_call(['hg', 'commit', '-m', 'blah'], cwd=repo_dir, stdout=subprocess.PIPE, stderr=subprocess.PIPE) return repo_dir def _test_hg_project_from_local_path(self): repo_dir = self.create_local_hg_repo() project = self.create_project( name='my hg project from local path', scm_type='hg', scm_url=repo_dir, ) self.check_project_scm(project) def _test_hg_project_via_ssh_loopback(self): # hg doesn't support password for ssh:// urls. scm_username = getattr(settings, 'TEST_SSH_LOOPBACK_USERNAME', '') if not all([scm_username]): self.skipTest('no ssh loopback username defined!') if not self.is_public_key_in_authorized_keys(): self.skipTest('ssh loopback for hg requires public key in authorized keys') repo_dir = self.create_local_hg_repo() scm_url = 'ssh://localhost/%s' % repo_dir project = self.create_project( name='my hg project via ssh loopback', scm_type='hg', scm_url=scm_url, scm_username=scm_username, ) self.check_project_scm(project) def test_public_svn_project_over_https(self): scm_url = getattr(settings, 'TEST_SVN_PUBLIC_HTTPS', 'https://github.com/ansible/ansible.github.com') if not all([scm_url]): self.skipTest('no public svn repo defined for https!') project = self.create_project( name='my public svn project over https', scm_type='svn', scm_url=scm_url, ) self.check_project_scm(project) def test_private_svn_project_over_https(self): scm_url = getattr(settings, 'TEST_SVN_PRIVATE_HTTPS', '') scm_username = getattr(settings, 'TEST_SVN_USERNAME', '') scm_password = getattr(settings, 'TEST_SVN_PASSWORD', '') if not all([scm_url, scm_username, scm_password]): self.skipTest('no private svn repo defined for https!') project = self.create_project( name='my private svn project over https', scm_type='svn', scm_url=scm_url, scm_username=scm_username, scm_password=scm_password, ) self.check_project_scm(project) def create_local_svn_repo(self): repo_dir = tempfile.mkdtemp() self._temp_paths.append(repo_dir) subprocess.check_call(['svnadmin', 'create', '.'], cwd=repo_dir, stdout=subprocess.PIPE, stderr=subprocess.PIPE) handle, playbook_path = tempfile.mkstemp(suffix='.yml', dir=repo_dir) test_playbook_file = os.fdopen(handle, 'w') test_playbook_file.write(TEST_PLAYBOOK) test_playbook_file.close() subprocess.check_call(['svn', 'import', '-m', 'blah', os.path.basename(playbook_path), 'file://%s/%s' % (repo_dir, os.path.basename(playbook_path))], cwd=repo_dir, stdout=subprocess.PIPE, stderr=subprocess.PIPE) return repo_dir def _test_svn_project_from_local_path(self): repo_dir = self.create_local_svn_repo() scm_url = 'file://%s' % repo_dir project = self.create_project( name='my svn project from local path', scm_type='svn', scm_url=scm_url, ) self.check_project_scm(project) def test_svn_project_via_ssh_loopback(self): scm_username = getattr(settings, 'TEST_SSH_LOOPBACK_USERNAME', '') scm_password = getattr(settings, 'TEST_SSH_LOOPBACK_PASSWORD', '') if not all([scm_username, scm_password]): self.skipTest('no ssh loopback username/password defined!') repo_dir = self.create_local_svn_repo() scm_url = 'svn+ssh://localhost%s' % repo_dir project = self.create_project( name='my svn project via ssh loopback', scm_type='svn', scm_url=scm_url, scm_username=scm_username, scm_password=scm_password, ) self.check_project_scm(project) def create_test_job_template(self, **kwargs): opts = { 'name': 'test-job-template %s' % str(now()), 'inventory': self.inventory, 'project': self.project, 'credential': self.credential, 'job_type': 'run', } try: opts['playbook'] = self.project.playbooks[0] except (AttributeError, IndexError): pass opts.update(kwargs) self.job_template = JobTemplate.objects.create(**opts) return self.job_template def create_test_job(self, **kwargs): job_template = kwargs.pop('job_template', None) if job_template: self.job = job_template.create_job(**kwargs) else: opts = { 'name': 'test-job %s' % str(now()), 'inventory': self.inventory, 'project': self.project, 'credential': self.credential, 'job_type': 'run', } try: opts['playbook'] = self.project.playbooks[0] except (AttributeError, IndexError): pass opts.update(kwargs) self.job = Job.objects.create(**opts) return self.job # TODO: We need to test this another way due to concurrency conflicts # Will add some tests for the task runner system def _test_update_on_launch(self): scm_url = getattr(settings, 'TEST_GIT_PUBLIC_HTTPS', 'https://github.com/ansible/ansible.github.com.git') if not all([scm_url]): self.skipTest('no public git repo defined for https!') self.organization = self.make_organizations(self.super_django_user, 1)[0] self.inventory = Inventory.objects.create(name='test-inventory', description='description for test-inventory', organization=self.organization) self.host = self.inventory.hosts.create(name='host.example.com', inventory=self.inventory) self.group = self.inventory.groups.create(name='test-group', inventory=self.inventory) self.group.hosts.add(self.host) self.credential = Credential.objects.create(name='test-creds', user=self.super_django_user) self.project = self.create_project( name='my public git project over https', scm_type='git', scm_url=scm_url, scm_update_on_launch=True) # First update triggered by saving a new project with SCM. self.assertEqual(self.project.project_updates.count(), 1) self.check_project_update(self.project) self.assertEqual(self.project.project_updates.count(), 2) job_template = self.create_test_job_template() job = self.create_test_job(job_template=job_template) self.assertEqual(job.status, 'new') self.assertFalse(job.passwords_needed_to_start) self.assertTrue(job.signal_start()) time.sleep(10) # Need some time to wait for the dependency to run job = Job.objects.get(pk=job.pk) self.assertTrue(job.status in ('successful', 'failed')) self.assertEqual(self.project.project_updates.count(), 3) def _test_update_on_launch_with_project_passwords(self): scm_url = getattr(settings, 'TEST_GIT_PRIVATE_HTTPS', '') scm_username = getattr(settings, 'TEST_GIT_USERNAME', '') scm_password = getattr(settings, 'TEST_GIT_PASSWORD', '') if not all([scm_url, scm_username, scm_password]): self.skipTest('no private git repo defined for https!') self.organization = self.make_organizations(self.super_django_user, 1)[0] self.inventory = Inventory.objects.create(name='test-inventory', description='description for test-inventory', organization=self.organization) self.host = self.inventory.hosts.create(name='host.example.com', inventory=self.inventory) self.group = self.inventory.groups.create(name='test-group', inventory=self.inventory) self.group.hosts.add(self.host) self.credential = Credential.objects.create(name='test-creds', user=self.super_django_user) self.project = self.create_project( name='my private git project over https', scm_type='git', scm_url=scm_url, scm_username=scm_username, scm_password=scm_password, scm_update_on_launch=True, ) self.check_project_update(self.project) self.assertEqual(self.project.project_updates.count(), 2) job_template = self.create_test_job_template() job = self.create_test_job(job_template=job_template) self.assertEqual(job.status, 'new') self.assertFalse(job.passwords_needed_to_start) self.assertTrue(job.start()) self.assertTrue(job.status in ('pending', 'waiting'), job.status) job = Job.objects.get(pk=job.pk) self.assertTrue(job.status in ('successful', 'failed'), job.result_stdout + job.result_traceback) self.assertEqual(self.project.project_updates.count(), 3) # Try again but set a bad project password - the job should flag an # error because the project update failed. cred = self.project.credential cred.password = 'bad scm password' cred.save() job = self.create_test_job(job_template=job_template) self.assertEqual(job.status, 'new') self.assertFalse(job.passwords_needed_to_start) self.assertTrue(job.start()) self.assertTrue(job.status in ('pending', 'waiting'), job.status) job = Job.objects.get(pk=job.pk) # FIXME: Not quite sure why the project update still returns successful # in this case? #self.assertEqual(job.status, 'error', # '\n'.join([job.result_stdout, job.result_traceback])) self.assertEqual(self.project.project_updates.count(), 4)