mirror of
https://github.com/ansible/awx.git
synced 2026-05-12 20:07:37 -02:30
Merge pull request #406 from AlanCoding/variables_debt
Consolidation of variables parsing throughout codebase
This commit is contained in:
@@ -11,9 +11,6 @@ import urllib
|
|||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from dateutil import rrule
|
from dateutil import rrule
|
||||||
|
|
||||||
# PyYAML
|
|
||||||
import yaml
|
|
||||||
|
|
||||||
# Django
|
# Django
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth import authenticate
|
from django.contrib.auth import authenticate
|
||||||
@@ -28,7 +25,7 @@ from django.utils.timezone import now
|
|||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
|
|
||||||
# Django REST Framework
|
# Django REST Framework
|
||||||
from rest_framework.exceptions import ValidationError, PermissionDenied
|
from rest_framework.exceptions import ValidationError, PermissionDenied, ParseError
|
||||||
from rest_framework import fields
|
from rest_framework import fields
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from rest_framework import validators
|
from rest_framework import validators
|
||||||
@@ -1312,6 +1309,9 @@ class HostSerializer(BaseSerializerWithVariables):
|
|||||||
raise serializers.ValidationError({"detail": _("Cannot create Host for Smart Inventory")})
|
raise serializers.ValidationError({"detail": _("Cannot create Host for Smart Inventory")})
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
def validate_variables(self, value):
|
||||||
|
return vars_validate_or_raise(value)
|
||||||
|
|
||||||
def validate(self, attrs):
|
def validate(self, attrs):
|
||||||
name = force_text(attrs.get('name', self.instance and self.instance.name or ''))
|
name = force_text(attrs.get('name', self.instance and self.instance.name or ''))
|
||||||
host, port = self._get_host_port_from_name(name)
|
host, port = self._get_host_port_from_name(name)
|
||||||
@@ -1319,19 +1319,9 @@ class HostSerializer(BaseSerializerWithVariables):
|
|||||||
if port:
|
if port:
|
||||||
attrs['name'] = host
|
attrs['name'] = host
|
||||||
variables = force_text(attrs.get('variables', self.instance and self.instance.variables or ''))
|
variables = force_text(attrs.get('variables', self.instance and self.instance.variables or ''))
|
||||||
try:
|
vars_dict = parse_yaml_or_json(variables)
|
||||||
vars_dict = json.loads(variables.strip() or '{}')
|
vars_dict['ansible_ssh_port'] = port
|
||||||
vars_dict['ansible_ssh_port'] = port
|
attrs['variables'] = json.dumps(vars_dict)
|
||||||
attrs['variables'] = json.dumps(vars_dict)
|
|
||||||
except (ValueError, TypeError):
|
|
||||||
try:
|
|
||||||
vars_dict = yaml.safe_load(variables)
|
|
||||||
if vars_dict is None:
|
|
||||||
vars_dict = {}
|
|
||||||
vars_dict['ansible_ssh_port'] = port
|
|
||||||
attrs['variables'] = yaml.dump(vars_dict)
|
|
||||||
except (yaml.YAMLError, TypeError):
|
|
||||||
raise serializers.ValidationError({'variables': _('Must be valid JSON or YAML.')})
|
|
||||||
|
|
||||||
return super(HostSerializer, self).validate(attrs)
|
return super(HostSerializer, self).validate(attrs)
|
||||||
|
|
||||||
@@ -3326,18 +3316,11 @@ class JobLaunchSerializer(BaseSerializer):
|
|||||||
|
|
||||||
extra_vars = attrs.get('extra_vars', {})
|
extra_vars = attrs.get('extra_vars', {})
|
||||||
|
|
||||||
if isinstance(extra_vars, basestring):
|
try:
|
||||||
try:
|
extra_vars = parse_yaml_or_json(extra_vars, silent_failure=False)
|
||||||
extra_vars = json.loads(extra_vars)
|
except ParseError as e:
|
||||||
except (ValueError, TypeError):
|
# Catch known user variable formatting errors
|
||||||
try:
|
errors['extra_vars'] = str(e)
|
||||||
extra_vars = yaml.safe_load(extra_vars)
|
|
||||||
assert isinstance(extra_vars, dict)
|
|
||||||
except (yaml.YAMLError, TypeError, AttributeError, AssertionError):
|
|
||||||
errors['extra_vars'] = _('Must be valid JSON or YAML.')
|
|
||||||
|
|
||||||
if not isinstance(extra_vars, dict):
|
|
||||||
extra_vars = {}
|
|
||||||
|
|
||||||
if self.get_survey_enabled(obj):
|
if self.get_survey_enabled(obj):
|
||||||
validation_errors = obj.survey_variable_validation(extra_vars)
|
validation_errors = obj.survey_variable_validation(extra_vars)
|
||||||
@@ -3411,18 +3394,11 @@ class WorkflowJobLaunchSerializer(BaseSerializer):
|
|||||||
|
|
||||||
extra_vars = attrs.get('extra_vars', {})
|
extra_vars = attrs.get('extra_vars', {})
|
||||||
|
|
||||||
if isinstance(extra_vars, basestring):
|
try:
|
||||||
try:
|
extra_vars = parse_yaml_or_json(extra_vars, silent_failure=False)
|
||||||
extra_vars = json.loads(extra_vars)
|
except ParseError as e:
|
||||||
except (ValueError, TypeError):
|
# Catch known user variable formatting errors
|
||||||
try:
|
errors['extra_vars'] = str(e)
|
||||||
extra_vars = yaml.safe_load(extra_vars)
|
|
||||||
assert isinstance(extra_vars, dict)
|
|
||||||
except (yaml.YAMLError, TypeError, AttributeError, AssertionError):
|
|
||||||
errors['extra_vars'] = _('Must be valid JSON or YAML.')
|
|
||||||
|
|
||||||
if not isinstance(extra_vars, dict):
|
|
||||||
extra_vars = {}
|
|
||||||
|
|
||||||
if self.get_survey_enabled(obj):
|
if self.get_survey_enabled(obj):
|
||||||
validation_errors = obj.survey_variable_validation(extra_vars)
|
validation_errors = obj.survey_variable_validation(extra_vars)
|
||||||
|
|||||||
@@ -1,13 +1,6 @@
|
|||||||
# Copyright (c) 2015 Ansible, Inc.
|
# Copyright (c) 2015 Ansible, Inc.
|
||||||
# All Rights Reserved.
|
# All Rights Reserved.
|
||||||
|
|
||||||
# Python
|
|
||||||
import json
|
|
||||||
import shlex
|
|
||||||
|
|
||||||
# PyYAML
|
|
||||||
import yaml
|
|
||||||
|
|
||||||
# Django
|
# Django
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.core.exceptions import ValidationError, ObjectDoesNotExist
|
from django.core.exceptions import ValidationError, ObjectDoesNotExist
|
||||||
@@ -21,7 +14,7 @@ from taggit.managers import TaggableManager
|
|||||||
from crum import get_current_user
|
from crum import get_current_user
|
||||||
|
|
||||||
# AWX
|
# AWX
|
||||||
from awx.main.utils import encrypt_field
|
from awx.main.utils import encrypt_field, parse_yaml_or_json
|
||||||
|
|
||||||
__all__ = ['prevent_search', 'VarsDictProperty', 'BaseModel', 'CreatedModifiedModel',
|
__all__ = ['prevent_search', 'VarsDictProperty', 'BaseModel', 'CreatedModifiedModel',
|
||||||
'PasswordFieldsModel', 'PrimordialModel', 'CommonModel',
|
'PasswordFieldsModel', 'PrimordialModel', 'CommonModel',
|
||||||
@@ -80,26 +73,7 @@ class VarsDictProperty(object):
|
|||||||
if hasattr(v, 'items'):
|
if hasattr(v, 'items'):
|
||||||
return v
|
return v
|
||||||
v = v.encode('utf-8')
|
v = v.encode('utf-8')
|
||||||
d = None
|
return parse_yaml_or_json(v)
|
||||||
try:
|
|
||||||
d = json.loads(v.strip() or '{}')
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
if d is None:
|
|
||||||
try:
|
|
||||||
d = yaml.safe_load(v)
|
|
||||||
# This can happen if the whole file is commented out
|
|
||||||
if d is None:
|
|
||||||
d = {}
|
|
||||||
except yaml.YAMLError:
|
|
||||||
pass
|
|
||||||
if d is None and self.key_value:
|
|
||||||
d = {}
|
|
||||||
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
|
|
||||||
return d if hasattr(d, 'items') else {}
|
|
||||||
|
|
||||||
def __set__(self, obj, value):
|
def __set__(self, obj, value):
|
||||||
raise AttributeError('readonly property')
|
raise AttributeError('readonly property')
|
||||||
|
|||||||
@@ -199,7 +199,7 @@ def test_job_reject_invalid_prompted_extra_vars(runtime_data, job_template_promp
|
|||||||
dict(extra_vars='{"unbalanced brackets":'), admin_user, expect=400)
|
dict(extra_vars='{"unbalanced brackets":'), admin_user, expect=400)
|
||||||
|
|
||||||
assert 'extra_vars' in response.data
|
assert 'extra_vars' in response.data
|
||||||
assert 'valid JSON or YAML' in str(response.data['extra_vars'][0])
|
assert 'Cannot parse as' in str(response.data['extra_vars'][0])
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
|
|||||||
@@ -5,9 +5,13 @@
|
|||||||
import os
|
import os
|
||||||
import pytest
|
import pytest
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
import json
|
||||||
|
import yaml
|
||||||
|
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
|
|
||||||
|
from rest_framework.exceptions import ParseError
|
||||||
|
|
||||||
from awx.main.utils import common
|
from awx.main.utils import common
|
||||||
|
|
||||||
from awx.main.models import (
|
from awx.main.models import (
|
||||||
@@ -38,6 +42,42 @@ def test_parse_yaml_or_json(input_, output):
|
|||||||
assert common.parse_yaml_or_json(input_) == output
|
assert common.parse_yaml_or_json(input_) == output
|
||||||
|
|
||||||
|
|
||||||
|
class TestParserExceptions:
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def json_error(data):
|
||||||
|
try:
|
||||||
|
json.loads(data)
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
return str(e)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def yaml_error(data):
|
||||||
|
try:
|
||||||
|
yaml.load(data)
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
return str(e)
|
||||||
|
|
||||||
|
def test_invalid_JSON_and_YAML(self):
|
||||||
|
data = "{key:val"
|
||||||
|
with pytest.raises(ParseError) as exc:
|
||||||
|
common.parse_yaml_or_json(data, silent_failure=False)
|
||||||
|
message = str(exc.value)
|
||||||
|
assert "Cannot parse as" in message
|
||||||
|
assert self.json_error(data) in message
|
||||||
|
assert self.yaml_error(data) in message
|
||||||
|
|
||||||
|
def test_invalid_vars_type(self):
|
||||||
|
data = "[1, 2, 3]"
|
||||||
|
with pytest.raises(ParseError) as exc:
|
||||||
|
common.parse_yaml_or_json(data, silent_failure=False)
|
||||||
|
message = str(exc.value)
|
||||||
|
assert "Cannot parse as" in message
|
||||||
|
assert "Input type `list` is not a dictionary" in message
|
||||||
|
|
||||||
|
|
||||||
def test_set_environ():
|
def test_set_environ():
|
||||||
key = str(uuid4())
|
key = str(uuid4())
|
||||||
old_environ = os.environ.copy()
|
old_environ = os.environ.copy()
|
||||||
|
|||||||
@@ -582,21 +582,48 @@ def cache_list_capabilities(page, prefetch_list, model, user):
|
|||||||
obj.capabilities_cache[display_method] = True
|
obj.capabilities_cache[display_method] = True
|
||||||
|
|
||||||
|
|
||||||
def parse_yaml_or_json(vars_str):
|
def validate_vars_type(vars_obj):
|
||||||
|
if not isinstance(vars_obj, dict):
|
||||||
|
vars_type = type(vars_obj)
|
||||||
|
if hasattr(vars_type, '__name__'):
|
||||||
|
data_type = vars_type.__name__
|
||||||
|
else:
|
||||||
|
data_type = str(vars_type)
|
||||||
|
raise AssertionError(
|
||||||
|
_('Input type `{data_type}` is not a dictionary').format(
|
||||||
|
data_type=data_type)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_yaml_or_json(vars_str, silent_failure=True):
|
||||||
'''
|
'''
|
||||||
Attempt to parse a string with variables, and if attempt fails,
|
Attempt to parse a string of variables.
|
||||||
return an empty dictionary.
|
First, with JSON parser, if that fails, then with PyYAML.
|
||||||
|
If both attempts fail, return an empty dictionary if `silent_failure`
|
||||||
|
is True, re-raise combination error if `silent_failure` if False.
|
||||||
'''
|
'''
|
||||||
if isinstance(vars_str, dict):
|
if isinstance(vars_str, dict):
|
||||||
return vars_str
|
return vars_str
|
||||||
|
elif isinstance(vars_str, basestring) and vars_str == '""':
|
||||||
|
return {}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
vars_dict = json.loads(vars_str)
|
vars_dict = json.loads(vars_str)
|
||||||
except (ValueError, TypeError):
|
validate_vars_type(vars_dict)
|
||||||
|
except (ValueError, TypeError, AssertionError) as json_err:
|
||||||
try:
|
try:
|
||||||
vars_dict = yaml.safe_load(vars_str)
|
vars_dict = yaml.safe_load(vars_str)
|
||||||
assert isinstance(vars_dict, dict)
|
# Can be None if '---'
|
||||||
except (yaml.YAMLError, TypeError, AttributeError, AssertionError):
|
if vars_dict is None:
|
||||||
vars_dict = {}
|
return {}
|
||||||
|
validate_vars_type(vars_dict)
|
||||||
|
except (yaml.YAMLError, TypeError, AttributeError, AssertionError) as yaml_err:
|
||||||
|
if silent_failure:
|
||||||
|
return {}
|
||||||
|
raise ParseError(_(
|
||||||
|
'Cannot parse as JSON (error: {json_error}) or '
|
||||||
|
'YAML (error: {yaml_error}).').format(
|
||||||
|
json_error=str(json_err), yaml_error=str(yaml_err)))
|
||||||
return vars_dict
|
return vars_dict
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,6 @@
|
|||||||
# Python
|
# Python
|
||||||
import base64
|
import base64
|
||||||
import re
|
import re
|
||||||
import yaml
|
|
||||||
import json
|
|
||||||
|
|
||||||
# Django
|
# Django
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
@@ -13,6 +11,10 @@ from django.core.exceptions import ValidationError
|
|||||||
|
|
||||||
# REST framework
|
# REST framework
|
||||||
from rest_framework.serializers import ValidationError as RestValidationError
|
from rest_framework.serializers import ValidationError as RestValidationError
|
||||||
|
from rest_framework.exceptions import ParseError
|
||||||
|
|
||||||
|
# AWX
|
||||||
|
from awx.main.utils import parse_yaml_or_json
|
||||||
|
|
||||||
|
|
||||||
def validate_pem(data, min_keys=0, max_keys=None, min_certs=0, max_certs=None):
|
def validate_pem(data, min_keys=0, max_keys=None, min_certs=0, max_certs=None):
|
||||||
@@ -179,21 +181,9 @@ def vars_validate_or_raise(vars_str):
|
|||||||
job templates, inventories, or hosts are either an acceptable
|
job templates, inventories, or hosts are either an acceptable
|
||||||
blank string, or are valid JSON or YAML dict
|
blank string, or are valid JSON or YAML dict
|
||||||
"""
|
"""
|
||||||
if isinstance(vars_str, dict) or (isinstance(vars_str, basestring) and vars_str == '""'):
|
try:
|
||||||
|
parse_yaml_or_json(vars_str, silent_failure=False)
|
||||||
return vars_str
|
return vars_str
|
||||||
|
except ParseError as e:
|
||||||
try:
|
raise RestValidationError(str(e))
|
||||||
r = json.loads((vars_str or '').strip() or '{}')
|
|
||||||
if isinstance(r, dict):
|
|
||||||
return vars_str
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
try:
|
|
||||||
r = yaml.safe_load(vars_str)
|
|
||||||
# Can be None if '---'
|
|
||||||
if isinstance(r, dict) or r is None:
|
|
||||||
return vars_str
|
|
||||||
except yaml.YAMLError:
|
|
||||||
pass
|
|
||||||
raise RestValidationError(_('Must be valid JSON or YAML.'))
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user