Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[BUG] Python3 incompatibilities #146

Open
ixs opened this issue Apr 2, 2020 · 11 comments
Open

[BUG] Python3 incompatibilities #146

ixs opened this issue Apr 2, 2020 · 11 comments
Labels

Comments

@ixs
Copy link

ixs commented Apr 2, 2020

When running this formula under python3, multiple issues crop up:

  • dictsort is used extensively and has changed functionality between py2 and py3. Python2 would happily sort a dictionary containing both strings and integers. Python3 however will return a TypeError saying that int and str are incompatible types.
    This is a known change in the sorted() function.

  • {{ zone_records | tojson }} seems to break on mixed data as well. I did not dig into the reason yet.

There are two possible ways of resolving the issue:

  1. Document that zone data must be all strings. Especially when configuring PTR RRs. Instead of using an int as key, this needs to be wrapped in quotes to ensure there are only strings. This ill work but has the drawback that e.g. '110' is now seen as smaller than e.g. '40' and the order of entries in the zone files appear incorrectly sorted.

  2. Remove all use of dictsort and instead rely on the python3 behavior that dicts are now returned in deterministic (but not sorted) order.

What is the preferred solution?

For 2. an example patch might look like this:

diff --git a/bind/config.sls b/bind/config.sls
index 0d200e7..d9a62b8 100644
--- a/bind/config.sls
+++ b/bind/config.sls
@@ -223,9 +223,9 @@ bind_rndc_client_config:

 {%- set views = {False: salt['pillar.get']('bind', {})} %}{# process non-view zones in the same loop #}
 {%- do views.update(salt['pillar.get']('bind:configured_views', {})) %}
-{%- for view, view_data in views|dictsort %}
+{%- for view, view_data in views.items() %}
 {%-   set dash_view = '-' + view if view else '' %}
-{%-   for zone, zone_data in view_data.get('configured_zones', {})|dictsort -%}
+{%-   for zone, zone_data in view_data.get('configured_zones', {}).items() -%}
 {%-     if 'file' in zone_data %}
 {%-       set file = zone_data.file %}
 {%-       set zone = file|replace(".txt", "") %}
@@ -260,7 +260,7 @@ zones{{ dash_view }}-{{ zone }}{{ '.include' if serial_auto else '' }}:
         zone: zones{{ dash_view }}-{{ zone }}
         soa: {{ salt['pillar.get']("bind:available_zones:" + zone + ":soa") | json }}
         includes: {{ salt['pillar.get']("bind:available_zones:" + zone + ":includes") | json }}
-        records: {{ zone_records | json }}
+        records: {{ zone_records }}
         include: False
     {% endif %}
     - user: {{ salt['pillar.get']('bind:config:user', map.user) }}
diff --git a/bind/files/zone.jinja b/bind/files/zone.jinja
index 9583197..ac77c62 100644
--- a/bind/files/zone.jinja
+++ b/bind/files/zone.jinja
@@ -28,11 +28,11 @@ $TTL {{ soa['ttl'] }}
 {% if include %}
 $INCLUDE {{ include }}
 {% else %}
-{% for type, rrs in records|dictsort %}
+{% for type, rrs in records.items() %}
 ;
 ; {{ type }} RRs
 ;
-{%- for host, data in rrs|dictsort %}
+{%- for host, data in rrs.items() %}
 {%- if data is mapping %}
 {%- if 'ttl' in data %}
 {{ host }} {{ data['ttl'] }} {{ data['type'] }} {{ data['record'] }}
@ixs ixs added the bug label Apr 2, 2020
@myii
Copy link
Member

myii commented Apr 2, 2020

@ixs Thanks for raising this issue. We've got well-structured automated testing available on this formula and I believe the way to tackle this is to formally define the problem with failing tests. If you're not familiar with InSpec, that's not a problem -- if you can provide me with the inputs (pillar/config) and outputs (rendered files), py2 versus py3, I can put those tests together so that we can be clear in what we're attempting to fix here. I mean something like the feedback during saltstack-formulas/firewalld-formula#40, which led to tests that helped ensure the new implementation was doing exactly what it was supposed to.

If you look at our current matrix, we're actually testing py3 mainly:

https://travis-ci.com/github/saltstack-formulas/bind-formula/builds/154561467

We've still got the pre-salted py2 images available, it should be relatively simple to put this all together.

@ixs
Copy link
Author

ixs commented Apr 2, 2020

The underlying problem is the following:

[andreas@herbert tmp]$ cat test.py
data = {0: 0, 1: 1, 'a': 'a'}

print(data)
print(sorted(data))
[root@herbert tmp]# python2 test.py
{0: 0, 1: 1, 'a': 'a'}
[0, 1, 'a']
[root@herbert tmp]# python3 test.py
{0: 0, 1: 1, 'a': 'a'}
Traceback (most recent call last):
  File "test.py", line 4, in <module>
    print(sorted(data))
TypeError: '<' not supported between instances of 'str' and 'int'

This would happen for example if you define the following data in your pillar (soa etc. omitted for brevity):

bind:
  available_zones:
    0.0.172.in-addr.arpa:
      records:
          PTR:
            1: ns1.example.net.
            $GENERATE 1-253 $: ip127-0-0-$.example.net.

The first record in the yaml file is cast as an integer, the second one as a string.
At this point, dictsort breaks. If the first record key is wrapped in quotes, it is read as a string and dictsort works fine under py3.

@myii
Copy link
Member

myii commented Apr 2, 2020

@ixs Thanks, I'll rustle up a failing test or two and then get back to you.

@ixs
Copy link
Author

ixs commented Apr 3, 2020

@myii thanks, it's been a while since I last worked with inspec and you're probably much quicker in whipping up a test-case. :-)

@myii
Copy link
Member

myii commented Apr 3, 2020

@ixs First bit of feedback.


This was enough to trigger the error: myii@485880a.

https://travis-ci.org/github/myii/bind-formula/builds/670517157:

All of the tracebacks in the failing job returned back to this line, which isn't highlighting the dictsort problem itself just yet:

records: {{ zone_records | json }}    <======================

Added another tiny commit to get through to the dictsort error: myii@f2a923a.

https://travis-ci.org/github/myii/bind-formula/builds/670520245:

The failing job contains the traceback returning back to the dictsort:

              Traceback (most recent call last):
                File "/usr/lib/python3/dist-packages/salt/utils/templates.py", line 394, in render_jinja_tmpl
                  output = template.render(**decoded_context)
                File "/usr/lib/python3/dist-packages/jinja2/environment.py", line 1008, in render
                  return self.environment.handle_exception(exc_info, True)
                File "/usr/lib/python3/dist-packages/jinja2/environment.py", line 780, in handle_exception
                  reraise(exc_type, exc_value, tb)
                File "/usr/lib/python3/dist-packages/jinja2/_compat.py", line 37, in reraise
                  raise value.with_traceback(tb)
                File "<template>", line 35, in top-level template code
                File "/usr/lib/python3/dist-packages/jinja2/filters.py", line 222, in do_dictsort
                  return sorted(value.items(), key=sort_func)
              TypeError: unorderable types: str() < int()
              
              ; line 35
              
              ---
              [...]
              {% else %}
              {% for type, rrs in records|dictsort %}
              ;
              ; {{ type }} RRs
              ;
              {%- for host, data in rrs|dictsort %}    <======================

There are two possible ways of resolving the issue:

  1. Document that zone data must be all strings. Especially when configuring PTR RRs. Instead of using an int as key, this needs to be wrapped in quotes to ensure there are only strings. This ill work but has the drawback that e.g. '110' is now seen as smaller than e.g. '40' and the order of entries in the zone files appear incorrectly sorted.

  2. Remove all use of dictsort and instead rely on the python3 behavior that dicts are now returned in deterministic (but not sorted) order.

If the sorting is of great significance, then there's always the option of using lists but that's heavy-handed and a lot of hassle for everyone in terms of having to modify their pillars. If necessary, I can add another test to check for the ordering as well.

Otherwise, at this stage, number 2 seems like the better option, since that avoids errors and pillar modification.

CC: @daks, your feedback would be useful here.

@ixs
Copy link
Author

ixs commented Apr 4, 2020

@myii yeah, the | json error I spotted too but it never was clear to me why it is breaking. But I also did not dig deep enough.

I agree that option 2, removing dictsort is the best approach. Since python3.6 (IIRC) dicts are preserving the order of insertion of keys and are not random anymore.
That is actually a nice feature which means that dictsort is not really that important anymore IMHO.

@myii
Copy link
Member

myii commented May 18, 2020

saltstack-formulas/users-formula#219 (comment)

Relevant here as well, that .items()|sort results in the same error as |dictsort.

@remichristiaan
Copy link

remichristiaan commented Aug 7, 2020

I ran into the following on Py 3.6.9. (salt 3001) using a pillar with 2 views defined:

Data failed to compile:
----------
    Rendering SLS 'base:bind.config' failed: Jinja error: '<' not supported between instances of 'str' and 'bool'
Traceback (most recent call last):
  File "/usr/lib/python3/dist-packages/salt/utils/templates.py", line 400, in render_jinja_tmpl
    output = template.render(**decoded_context)
  File "/usr/lib/python3/dist-packages/jinja2/asyncsupport.py", line 76, in render
    return original_render(self, *args, **kwargs)
  File "/usr/lib/python3/dist-packages/jinja2/environment.py", line 1008, in render
    return self.environment.handle_exception(exc_info, True)
  File "/usr/lib/python3/dist-packages/jinja2/environment.py", line 780, in handle_exception
    reraise(exc_type, exc_value, tb)
  File "/usr/lib/python3/dist-packages/jinja2/_compat.py", line 37, in reraise
    raise value.with_traceback(tb)
  File "<template>", line 206, in top-level template code
  File "/usr/lib/python3/dist-packages/jinja2/filters.py", line 242, in do_dictsort
    return sorted(value.items(), key=sort_func, reverse=reverse)
TypeError: '<' not supported between instances of 'str' and 'bool'

; line 206
{%- do views.update(salt['pillar.get']('bind:configured_views', {})) %}
{%- for view, view_data in views|dictsort %}    <======================
{%-   set dash_view = '-' + view if view else '' %}
{%-   for zone, zone_data in view_data.get('configured_zones', {})|dictsort -%}
{%-     if 'file' in zone_data %}
{%-       set file = zone_data.file %}
{%-       set zone = file|replace(".txt", "") %}
[...]

Removing |dictsort from the {%- for view, view_data in views|dictsort %} line results in:

Rendering SLS 'base:bind.config' failed: Jinja error: 'bool' object is not iterable
Traceback (most recent call last):
  File "/usr/lib/python3/dist-packages/salt/utils/templates.py", line 400, in render_jinja_tmpl
    output = template.render(**decoded_context)
  File "/usr/lib/python3/dist-packages/jinja2/asyncsupport.py", line 76, in render
    return original_render(self, *args, **kwargs)
  File "/usr/lib/python3/dist-packages/jinja2/environment.py", line 1008, in render
    return self.environment.handle_exception(exc_info, True)
  File "/usr/lib/python3/dist-packages/jinja2/environment.py", line 780, in handle_exception
    reraise(exc_type, exc_value, tb)
  File "/usr/lib/python3/dist-packages/jinja2/_compat.py", line 37, in reraise
    raise value.with_traceback(tb)
  File "<template>", line 206, in top-level template code
TypeError: 'bool' object is not iterable

; line 206

Changing line 204 to the following resolves the 'bool' issue with |dictsort set):

{%- set views = {'': salt['pillar.get']('bind', {})} %}{# process non-view zones in the same loop #}

If you also remove dictsort (line 206), the following error is given:

    Rendering SLS 'base:bind.config' failed: Jinja error: not enough values to unpack (expected 2, got 0)
Traceback (most recent call last):
  File "/usr/lib/python3/dist-packages/salt/utils/templates.py", line 400, in render_jinja_tmpl
    output = template.render(**decoded_context)
  File "/usr/lib/python3/dist-packages/jinja2/asyncsupport.py", line 76, in render
    return original_render(self, *args, **kwargs)
  File "/usr/lib/python3/dist-packages/jinja2/environment.py", line 1008, in render
    return self.environment.handle_exception(exc_info, True)
  File "/usr/lib/python3/dist-packages/jinja2/environment.py", line 780, in handle_exception
    reraise(exc_type, exc_value, tb)
  File "/usr/lib/python3/dist-packages/jinja2/_compat.py", line 37, in reraise
    raise value.with_traceback(tb)
  File "<template>", line 206, in top-level template code
ValueError: not enough values to unpack (expected 2, got 0)

; line 206

---
[...]
{%- endif %}
{% endif %}

{%- set views = {'': salt['pillar.get']('bind', {})} %}{# process non-view zones in the same loop #}
{%- do views.update(salt['pillar.get']('bind:configured_views', {})) %}
{%- for view, view_data in views %}    <======================
{%-   set dash_view = '-' + view if view else '' %}
{%-   for zone, zone_data in view_data.get('configured_zones', {})|dictsort -%}
{%-     if 'file' in zone_data %}
{%-       set file = zone_data.file %}
{%-       set zone = file|replace(".txt", "") %}
[...]

@bcrisp4
Copy link

bcrisp4 commented Mar 2, 2021

With a quick "find and replace" I've replaced all uses of dictsort with .items(). bcrisp4@bebf12f

This seems to have resolved this issue for me.

If removing dictsort is the preferred solution, I can look at putting together a PR for this. Is there anything else that needs to be done to resolve this issue?

Edit:

I missed also removing the json filter in the config state per the originally proposed patch

@daks
Copy link
Member

daks commented Mar 4, 2021

CC: @daks, your feedback would be useful here.

@myii just saw now that you pinged me on this issue! Do you still need me to look at something? (FYI we don't use this formula, just an old version with custom patches)

@myii
Copy link
Member

myii commented Mar 4, 2021

@myii just saw now that you pinged me on this issue! Do you still need me to look at something? (FYI we don't use this formula, just an old version with custom patches)

@daks That was probably because you provided #129 so I must have thought you used the formula back then. No worries, the general situation is understood, we just need to finalise how we resolve this.

With a quick "find and replace" I've replaced all uses of dictsort with .items(). bcrisp4@bebf12f

This seems to have resolved this issue for me.

If removing dictsort is the preferred solution, I can look at putting together a PR for this. Is there anything else that needs to be done to resolve this issue?

@bcrisp4 Thanks for the offer, I think that's the right way forward. @ixs Are you good with that?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

5 participants