From 8bf198f7ed0d6e162186475ed6fa2b672414790d Mon Sep 17 00:00:00 2001 From: Chris Church Date: Wed, 20 Nov 2013 15:19:44 -0500 Subject: [PATCH] AC-687 Add ec2/rax region choices to inventory source options response, add validation for source_regions. --- awx/api/serializers.py | 7 +++ awx/main/models/inventory.py | 55 ++++++++++++++++++ awx/main/tests/inventory.py | 110 +++++++++++++++++++++++++++++++++++ awx/settings/defaults.py | 27 +++++++++ 4 files changed, 199 insertions(+) diff --git a/awx/api/serializers.py b/awx/api/serializers.py index ed5b2ab1fe..bc470a4788 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -677,6 +677,13 @@ class InventorySourceSerializer(BaseSerializer): # FIXME return attrs + def metadata(self): + metadata = super(InventorySourceSerializer, self).metadata() + field_opts = metadata.get('source_regions', {}) + field_opts['ec2_region_choices'] = self.opts.model.get_ec2_region_choices() + field_opts['rax_region_choices'] = self.opts.model.get_rax_region_choices() + return metadata + class InventoryUpdateSerializer(BaseSerializer): diff --git a/awx/main/models/inventory.py b/awx/main/models/inventory.py index f3cb3bbe2a..294999d063 100644 --- a/awx/main/models/inventory.py +++ b/awx/main/models/inventory.py @@ -561,6 +561,36 @@ class InventorySource(PrimordialModel): editable=False, ) + @classmethod + def get_ec2_region_choices(cls): + ec2_region_names = getattr(settings, 'EC2_REGION_NAMES', {}) + ec2_name_replacements = { + 'us': 'US', + 'ap': 'Asia Pacific', + 'eu': 'Europe', + 'sa': 'South America', + } + import boto.ec2 + regions = [('all', 'All')] + for region in boto.ec2.regions(): + label = ec2_region_names.get(region.name, '') + if not label: + label_parts = [] + for part in region.name.split('-'): + part = ec2_name_replacements.get(part.lower(), part.title()) + label_parts.append(part) + label = ' '.join(label_parts) + regions.append((region.name, label)) + return regions + + @classmethod + def get_rax_region_choices(cls): + # Not possible to get rax regions without first authenticating, so use + # list from settings. + regions = list(getattr(settings, 'RAX_REGION_CHOICES', [])) + regions.insert(0, ('ALL', 'All')) + return regions + def clean_credential(self): if not self.source: return None @@ -576,6 +606,31 @@ class InventorySource(PrimordialModel): raise ValidationError('Credential is required for a cloud source') return cred + def clean_source_regions(self): + regions = self.source_regions + if self.source == 'ec2': + valid_regions = [x[0] for x in self.get_ec2_region_choices()] + region_transform = lambda x: x.strip().lower() + elif self.source == 'rax': + valid_regions = [x[0] for x in self.get_rax_region_choices()] + region_transform = lambda x: x.strip().upper() + else: + return '' + all_region = region_transform('all') + valid_regions = [region_transform(x) for x in valid_regions] + regions = [region_transform(x) for x in regions.split(',') if x.strip()] + if all_region in regions: + return all_region + invalid_regions = [] + for r in regions: + if r not in valid_regions and r not in invalid_regions: + invalid_regions.append(r) + if invalid_regions: + raise ValidationError('Invalid %s region%s: %s' % (self.source, + '' if len(invalid_regions) == 1 else 's', + ', '.join(invalid_regions))) + return ','.join(regions) + def save(self, *args, **kwargs): new_instance = not bool(self.pk) # If update_fields has been specified, add our field names to it, diff --git a/awx/main/tests/inventory.py b/awx/main/tests/inventory.py index 9872a35633..6aef2ab951 100644 --- a/awx/main/tests/inventory.py +++ b/awx/main/tests/inventory.py @@ -1030,6 +1030,116 @@ class InventoryUpdatesTest(BaseTransactionTest): self.assertFalse(re.match(r'^i-[0-9a-f]{8}$', group.name, re.I), group.name) + def test_put_inventory_source_detail_with_regions(self): + creds_url = reverse('api:credential_list') + inv_src_url1 = reverse('api:inventory_source_detail', + args=(self.group.inventory_source.pk,)) + inv_src_url2 = reverse('api:inventory_source_detail', + args=(self.group2.inventory_source.pk,)) + # Create an AWS credential to use for first inventory source. + aws_cred_data = { + 'name': 'AWS key that does not need to have valid info because ' + 'we do not care if the update actually succeeds', + 'kind': 'aws', + 'user': self.super_django_user.pk, + 'username': 'aws access key id goes here', + 'password': 'aws secret access key goes here', + } + with self.current_user(self.super_django_user): + aws_cred_response = self.post(creds_url, aws_cred_data, expect=201) + aws_cred_id = aws_cred_response['id'] + # Create a RAX credential to use for second inventory source. + rax_cred_data = { + 'name': 'RAX cred that does not need to have valid info because ' + 'we do not care if the update actually succeeds', + 'kind': 'rax', + 'user': self.super_django_user.pk, + 'username': 'rax username', + 'password': 'rax api key', + } + with self.current_user(self.super_django_user): + rax_cred_response = self.post(creds_url, rax_cred_data, expect=201) + rax_cred_id = rax_cred_response['id'] + # Verify the options request gives ec2 and rax region choices. + with self.current_user(self.super_django_user): + response = self.options(inv_src_url1, expect=200) + self.assertTrue('ec2_region_choices' in response['actions']['GET']['source_regions']) + self.assertTrue('rax_region_choices' in response['actions']['GET']['source_regions']) + # Updaate the first inventory source to use EC2 with empty regions. + inv_src_data = { + 'source': 'ec2', + 'credential': aws_cred_id, + 'source_regions': '', + } + with self.current_user(self.super_django_user): + response = self.put(inv_src_url1, inv_src_data, expect=200) + self.assertEqual(response['source_regions'], '') + # All region. + inv_src_data['source_regions'] = 'ALL' + with self.current_user(self.super_django_user): + response = self.put(inv_src_url1, inv_src_data, expect=200) + self.assertEqual(response['source_regions'], 'all') + # Invalid region. + inv_src_data['source_regions'] = 'us-north-99' + with self.current_user(self.super_django_user): + response = self.put(inv_src_url1, inv_src_data, expect=400) + # All takes precedence over any other regions. + inv_src_data['source_regions'] = 'us-north-99,,all' + with self.current_user(self.super_django_user): + response = self.put(inv_src_url1, inv_src_data, expect=200) + self.assertEqual(response['source_regions'], 'all') + # Valid region. + inv_src_data['source_regions'] = 'us-west-1' + with self.current_user(self.super_django_user): + response = self.put(inv_src_url1, inv_src_data, expect=200) + self.assertEqual(response['source_regions'], 'us-west-1') + # Invalid region (along with valid one). + inv_src_data['source_regions'] = 'us-west-1, us-north-99' + with self.current_user(self.super_django_user): + response = self.put(inv_src_url1, inv_src_data, expect=400) + # Valid regions. + inv_src_data['source_regions'] = 'us-west-1, us-east-1, ' + with self.current_user(self.super_django_user): + response = self.put(inv_src_url1, inv_src_data, expect=200) + self.assertEqual(response['source_regions'], 'us-west-1,us-east-1') + # Updaate the second inventory source to use RAX with empty regions. + inv_src_data = { + 'source': 'rax', + 'credential': rax_cred_id, + 'source_regions': '', + } + with self.current_user(self.super_django_user): + response = self.put(inv_src_url2, inv_src_data, expect=200) + self.assertEqual(response['source_regions'], '') + # All region. + inv_src_data['source_regions'] = 'all' + with self.current_user(self.super_django_user): + response = self.put(inv_src_url2, inv_src_data, expect=200) + self.assertEqual(response['source_regions'], 'ALL') + # Invalid region. + inv_src_data['source_regions'] = 'RDU' + with self.current_user(self.super_django_user): + response = self.put(inv_src_url2, inv_src_data, expect=400) + # All takes precedence over any other regions. + inv_src_data['source_regions'] = 'RDU,,all' + with self.current_user(self.super_django_user): + response = self.put(inv_src_url2, inv_src_data, expect=200) + self.assertEqual(response['source_regions'], 'ALL') + # Valid region. + inv_src_data['source_regions'] = 'dfw' + with self.current_user(self.super_django_user): + response = self.put(inv_src_url2, inv_src_data, expect=200) + self.assertEqual(response['source_regions'], 'DFW') + # Invalid region (along with valid one). + inv_src_data['source_regions'] = 'dfw, rdu' + with self.current_user(self.super_django_user): + response = self.put(inv_src_url2, inv_src_data, expect=400) + # Valid regions. + inv_src_data['source_regions'] = 'ORD, iad, ' + with self.current_user(self.super_django_user): + response = self.put(inv_src_url2, inv_src_data, expect=200) + self.assertEqual(response['source_regions'], 'ORD,IAD') + def test_post_inventory_source_update(self): creds_url = reverse('api:credential_list') inv_src_url = reverse('api:inventory_source_detail', diff --git a/awx/settings/defaults.py b/awx/settings/defaults.py index 9926a40cdb..bba562d01c 100644 --- a/awx/settings/defaults.py +++ b/awx/settings/defaults.py @@ -292,6 +292,33 @@ ANSIBLE_PARAMIKO_RECORD_HOST_KEYS = False # the celery task. AWX_TASK_ENV = {} +# Not possible to get list of regions without authenticating, so use this list +# instead (based on docs from: +# http://docs.rackspace.com/loadbalancers/api/v1.0/clb-devguide/content/Service_Access_Endpoints-d1e517.html) +RAX_REGION_CHOICES = [ + ('ORD', 'Chicago'), + ('DFW', 'Dallas/Ft. Worth'), + ('IAD', 'Northern Virginia'), + ('LON', 'London'), + ('SYD', 'Sydney'), + ('HKG', 'Hong Kong'), +] + +# AWS does not appear to provide pretty region names via any API, so store the +# list of names here. The available region IDs will be pulled from boto. +# http://docs.aws.amazon.com/general/latest/gr/rande.html#ec2_region +EC2_REGION_NAMES = { + 'us-east-1': 'US East (Northern Virginia)', + 'us-west-2': 'US West (Oregon)', + 'us-west-1': 'US West (Northern California)', + 'eu-west-1': 'EU (Ireland)', + 'ap-southeast-1': 'Asia Pacific (Singapore)', + 'ap-southeast-2': 'Asia Pacific (Sydney)', + 'ap-northeast-1': 'Asia Pacific (Tokyo)', + 'sa-east-1': 'South America (Sao Paulo)', + 'us-gov-west-1': 'US West (GovCloud)', +} + # Internal API URL for use by inventory scripts and callback plugin. if 'devserver' in INSTALLED_APPS: INTERNAL_API_URL = 'http://127.0.0.1:%s' % DEVSERVER_DEFAULT_PORT