From 44a2bd72bd67a76da4853cca6737e41a728d4200 Mon Sep 17 00:00:00 2001 From: Chris Church Date: Mon, 21 Oct 2013 23:06:01 -0400 Subject: [PATCH 1/4] AC-585 Added totals fields to inventory group tree. --- awx/main/serializers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/awx/main/serializers.py b/awx/main/serializers.py index 6c320875d8..2615d7521b 100644 --- a/awx/main/serializers.py +++ b/awx/main/serializers.py @@ -588,7 +588,8 @@ class GroupTreeSerializer(GroupSerializer): class Meta: model = Group fields = BASE_FIELDS + ('inventory', 'variables', 'has_active_failures', - 'hosts_with_active_failures', + 'total_hosts', 'hosts_with_active_failures', + 'total_groups', 'groups_with_active_failures', 'has_inventory_sources', 'children') def get_children(self, obj): From c585227df87fc0ceb2e2b6633ce83a1399f6b032 Mon Sep 17 00:00:00 2001 From: Chris Church Date: Mon, 21 Oct 2013 23:16:01 -0400 Subject: [PATCH 2/4] AC-581 group and inventory fields on inventory source are now readonly. --- awx/main/serializers.py | 1 + 1 file changed, 1 insertion(+) diff --git a/awx/main/serializers.py b/awx/main/serializers.py index 2615d7521b..fae0ad9491 100644 --- a/awx/main/serializers.py +++ b/awx/main/serializers.py @@ -643,6 +643,7 @@ class InventorySourceSerializer(BaseSerializer): 'source_regions', 'source_tags', 'overwrite', 'overwrite_vars', 'update_on_launch', 'update_interval', 'last_update_failed', 'status', 'last_updated') + read_only_fields = ('inventory', 'group') def to_native(self, obj): ret = super(InventorySourceSerializer, self).to_native(obj) From 16d6ec252f11569567d203d5ccbcf4c57d18a67b Mon Sep 17 00:00:00 2001 From: Chris Church Date: Tue, 22 Oct 2013 00:41:49 -0400 Subject: [PATCH 3/4] 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 From a32fc02323cda32021954036994c62cb0d680cf4 Mon Sep 17 00:00:00 2001 From: Chris Church Date: Tue, 22 Oct 2013 14:31:05 -0400 Subject: [PATCH 4/4] Fix NameError in case where job extra vars are specified as key=value. --- awx/main/models/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/main/models/__init__.py b/awx/main/models/__init__.py index 764e01a85b..04c9c022de 100644 --- a/awx/main/models/__init__.py +++ b/awx/main/models/__init__.py @@ -109,7 +109,7 @@ class VarsDictProperty(object): pass if d is None and self.key_value: d = {} - for kv in [x.decode('utf-8') for x in shlex.split(extra_vars, posix=True)]: + for kv in [x.decode('utf-8') for x in shlex.split(v, posix=True)]: if '=' in kv: k, v = kv.split('=', 1) d[k] = v