From 16d6ec252f11569567d203d5ccbcf4c57d18a67b Mon Sep 17 00:00:00 2001 From: Chris Church Date: Tue, 22 Oct 2013 00:41:49 -0400 Subject: [PATCH] AC-403 Add API support for host:port format, port is automatically split into host variables as ansible_ssh_port. --- awx/main/serializers.py | 58 ++++++++++++++++++++++++++++--------- awx/main/tests/inventory.py | 33 ++++++++++++++------- 2 files changed, 67 insertions(+), 24 deletions(-) diff --git a/awx/main/serializers.py b/awx/main/serializers.py index fae0ad9491..d2bd05dea7 100644 --- a/awx/main/serializers.py +++ b/awx/main/serializers.py @@ -523,9 +523,9 @@ class HostSerializer(BaseSerializerWithVariables): d['groups'] = [{'id': g.id, 'name': g.name} for g in obj.groups.all()] return d - def _validate_name(self, attrs, source): - name = unicode(attrs.get(source, '')) + def _get_host_port_from_name(self, name): # Allow hostname (except IPv6 for now) to specify the port # inline. + port = None if name.count(':') == 1: name, port = name.split(':') try: @@ -533,20 +533,52 @@ class HostSerializer(BaseSerializerWithVariables): if port < 1 or port > 65535: raise ValueError except ValueError: - raise serializers.ValidationError('Invalid port specification') - for family in (socket.AF_INET, socket.AF_INET6): - try: - socket.inet_pton(family, name) - return attrs - except socket.error: - pass + raise serializers.ValidationError('Invalid port specification: %s' % str(port)) + return name, port + + def validate_name(self, attrs, source): + name = unicode(attrs.get(source, '')) + # Validate here only, update in main validate method. + host, port = self._get_host_port_from_name(name) + #for family in (socket.AF_INET, socket.AF_INET6): + # try: + # socket.inet_pton(family, name) + # return attrs + # except socket.error: + # pass # Hostname should match the following regular expression and have at # last one letter in the name (to catch invalid IPv4 addresses from # above). - valid_host_re = r'^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$' - if re.match(valid_host_re, name) and re.match(r'^.*?[a-zA-Z].*?$', name): - return attrs - raise serializers.ValidationError('Invalid host name or IP') + #valid_host_re = r'^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$' + #if re.match(valid_host_re, name) and re.match(r'^.*?[a-zA-Z].*?$', name): + # return attrs + #raise serializers.ValidationError('Invalid host name or IP') + return attrs + + def validate(self, attrs): + name = unicode(attrs.get('name', '')) + host, port = self._get_host_port_from_name(name) + + if port: + attrs['name'] = host + if self.object: + variables = unicode(attrs.get('variables', self.object.variables) or '') + else: + variables = unicode(attrs.get('variables', '')) + try: + vars_dict = json.loads(variables.strip() or '{}') + vars_dict['ansible_ssh_port'] = port + attrs['variables'] = json.dumps(vars_dict) + except (ValueError, TypeError): + try: + vars_dict = yaml.safe_load(variables) + vars_dict['ansible_ssh_port'] = port + attrs['variables'] = yaml.dump(vars_dict) + except (yaml.YAMLError, TypeError): + raise serializers.ValidationError('Must be valid JSON or YAML') + + return attrs + class GroupSerializer(BaseSerializerWithVariables): diff --git a/awx/main/tests/inventory.py b/awx/main/tests/inventory.py index 505f37a49a..7386053956 100644 --- a/awx/main/tests/inventory.py +++ b/awx/main/tests/inventory.py @@ -260,11 +260,17 @@ class InventoryTest(BaseTest): invalid = dict(name='asdf0.example.com') new_host_a = dict(name='asdf0.example.com:1022', inventory=inv.pk) new_host_b = dict(name='asdf1.example.com', inventory=inv.pk) - new_host_c = dict(name='127.1.2.3:2022', inventory=inv.pk) + new_host_c = dict(name='127.1.2.3:2022', inventory=inv.pk, + variables=json.dumps({'who': 'what?'})) new_host_d = dict(name='asdf3.example.com', inventory=inv.pk) new_host_e = dict(name='asdf4.example.com', inventory=inv.pk) host_data0 = self.post(hosts, data=invalid, expect=400, auth=self.get_super_credentials()) host_data0 = self.post(hosts, data=new_host_a, expect=201, auth=self.get_super_credentials()) + + # Port should be split out into host variables. + host_a = Host.objects.get(pk=host_data0['id']) + self.assertEqual(host_a.name, 'asdf0.example.com') + self.assertEqual(host_a.variables_dict, {'ansible_ssh_port': 1022}) # an org admin can add hosts host_data1 = self.post(hosts, data=new_host_e, expect=201, auth=self.get_normal_credentials()) @@ -280,6 +286,11 @@ class InventoryTest(BaseTest): ) host_data3 = self.post(hosts, data=new_host_c, expect=201, auth=self.get_other_credentials()) + # Port should be split out into host variables, other variables kept intact. + host_c = Host.objects.get(pk=host_data3['id']) + self.assertEqual(host_c.name, '127.1.2.3') + self.assertEqual(host_c.variables_dict, {'ansible_ssh_port': 2022, 'who': 'what?'}) + # hostnames must be unique inside an organization host_data4 = self.post(hosts, data=new_host_c, expect=400, auth=self.get_other_credentials()) @@ -680,22 +691,22 @@ class InventoryTest(BaseTest): # Try with invalid hostnames and invalid IPs. hosts = reverse('main:host_list') - invalid_expect = 201 # hostname validation is disabled for now. + invalid_expect = 400 # hostname validation is disabled for now. data = dict(name='', inventory=inv.pk) with self.current_user(self.super_django_user): response = self.post(hosts, data=data, expect=400) - data = dict(name='not a valid host name', inventory=inv.pk) - with self.current_user(self.super_django_user): - response = self.post(hosts, data=data, expect=invalid_expect) + #data = dict(name='not a valid host name', inventory=inv.pk) + #with self.current_user(self.super_django_user): + # response = self.post(hosts, data=data, expect=invalid_expect) data = dict(name='validhost:99999', inventory=inv.pk) with self.current_user(self.super_django_user): response = self.post(hosts, data=data, expect=invalid_expect) - data = dict(name='123.234.345.456', inventory=inv.pk) - with self.current_user(self.super_django_user): - response = self.post(hosts, data=data, expect=invalid_expect) - data = dict(name='2001::1::3F', inventory=inv.pk) - with self.current_user(self.super_django_user): - response = self.post(hosts, data=data, expect=invalid_expect) + #data = dict(name='123.234.345.456', inventory=inv.pk) + #with self.current_user(self.super_django_user): + # response = self.post(hosts, data=data, expect=invalid_expect) + #data = dict(name='2001::1::3F', inventory=inv.pk) + #with self.current_user(self.super_django_user): + # response = self.post(hosts, data=data, expect=invalid_expect) ######################################################### # FIXME: TAGS