Spec-Zone.ru › Ansible 2.4

Ansible и Python 3

Ansible преследует стратегию наличия одного кода, который работает как на Python-2, так и на Python-3, потому что мы хотим, чтобы Ansible мог управлять широким спектром машин. Авторы Ansible должны ознакомиться с советами в этом документе, чтобы они могли писать код, который будет работать на тех же версиях Python, что и остальная часть Ansible.

Ansible можно разделить на три перекрывающиеся части для целей портирования:

  1. Код со стороны контроллера. Это код, который выполняется на машине, где вы вызываете /usr/bin/ansible
  2. Модули. Это код, который Ansible передает по сети и вызывает на управляемой машине.
  3. Код module_utils. Это код, основное назначение которого используется модулями для выполнения задач. Однако некоторые части кода со стороны контроллера могут использовать общие функции отсюда.

Большая часть знаний о портировании кода будет применима ко всем трем частям, но также есть некоторые особые соображения для некоторых из них. Информация, которая в целом применима ко всем трем местам, находится в разделе со стороны контроллера.

Минимальная версия Python-3.x и Python-2.x

Как со стороны контроллера, так и в коде модулей мы поддерживаем Python-3.5 или выше и Python-2.6 или выше. Python-3.5 был выбран в качестве минимальной версии, потому что это самая ранняя версия Python-3, принятая по умолчанию в качестве Python в дистрибутиве Linux с долгосрочной поддержкой (LTS) (в данном случае Ubuntu-16.04). Предыдущие дистрибутивы LTS Linux поставлялись с версией Python-2, на которую пользователи могут полагаться вместо версии Python-3.

Для Python-2 по умолчанию модули работают на Python-2.6 или выше. Это позволяет пользователям с более старыми дистрибутивами, которые застряли на Python-2.6, управлять своими машинами. Модулям разрешается прекратить поддержку Python-2.6, когда один из их зависимых библиотек требует более новую версию Python. Это не приглашение добавлять ненужные зависимые библиотеки, чтобы заставить ваш модуль быть полезным только с более новой версией Python; вместо этого это признание, что некоторые библиотеки (например, boto3 и docker-py) будут работать только с более новой версией Python.

Примечание

Поддержка модулей Python-2.4:

Поддержка Python-2.4 и Python-2.5 была прекращена в Ansible-2.4. RHEL-5 (и его перестройки, такие как CentOS-5) поддерживались до апреля 2017 года. Ansible-2.3 был выпущен в апреле 2017 года и был последним выпуском Ansible, который поддерживал Python-2.4 со стороны модулей.

Портирование кода контроллера в Python 3

Большинство общих советов по переносу кода для использования как в Python-2, так и в Python-3 относится к переносу кода контроллера. Лучшее место для начала обучения переносу кода — книга Леннарта Регебро «Портирование в Python 3».

Книга описывает несколько стратегий портирования в Python 3. Мы используем стратегию поддержки Python-2 и Python-3 из одного кода.

Стратегия строк контроллера

Предыстория

Одним из важнейших моментов при переносе кода в Python-3 является выбор модели строк. Строки могут быть массивом байтов (как в C), или они могут быть массивом текста. Текст — это то, что мы представляем как буквы, цифры, числа, другие печатные символы и небольшое количество непечатаемых «символов» (управляющие коды).

В Python-2 два типа для этих (str для байтов и unicode для текста) часто используются взаимозаменяемо. При работе только с символами ASCII строки можно объединять, сравнивать и преобразовывать из одного типа в другой автоматически. При введении символов, не являющихся ASCII, Python начинает генерировать исключения из-за того, что не знает, в каком кодировании должны быть символы, не являющиеся ASCII.

Python-3 изменяет это поведение, делая разделение между байтами (bytes) и текстом (str) более строгим. Python будет генерировать исключение при попытке объединения и сравнения двух типов. Программист должен явно преобразовывать из одного типа в другой, чтобы смешивать значения каждого из них.

Это изменение позволяет программисту немедленно увидеть, когда код ненадлежащим образом смешивает типы, а не работает, пока один из пользователей не вызовет исключение, введя не-ASCII входные данные. Однако это обязывает программиста активно определять стратегию работы со строками в своей программе, чтобы не смешивать текстовые и байтовые строки непреднамеренно.

Unicode-сэндвич

В коде со стороны контроллера мы используем стратегию, известную как Unicode-сэндвич (названная так по аналогии с текстовым типом unicode Python-2). Для Unicode-сэндвича мы знаем, что на границе нашего кода и внешнего мира (например, файл и сетевой ввод-вывод, переменные среды и некоторые вызовы библиотек) мы получим байты. Нам нужно преобразовать эти байты в текст и использовать его во внутренних частях нашего кода. Когда нам нужно отправить эти строки обратно во внешний мир, мы сначала преобразуем текст обратно в байты. Чтобы визуализировать это, представьте «сэндвич», состоящий из верхнего и нижнего слоя байтов, слоя преобразования между ними и всех типов текста в центре.

Общие границы

Это частичный список мест, где нам нужно преобразовывать байты в текст и наоборот. Он не является исчерпывающим, но дает вам представление о том, где следует искать проблемы.

Чтение и запись в файлы

В Python-2 чтение из файла возвращает байты. В Python-3 оно может возвращать текст. Чтобы создать код, который портативен для обоих, мы не используем возможность Python-3 возвращать текст, а вместо этого явно выполняем преобразование сами. Например:

from ansible.module_utils._text import to_text

with open('filename-with-utf8-data.txt', 'rb') as my_file:
    b_data = my_file.read()
    try:
        data = to_text(b_data, errors='surrogate_or_strict')
    except UnicodeError:
        # Handle the exception gracefully -- usually by displaying a good
        # user-centric error message that can be traced back to this piece
        # of code.
        pass

Примечание

Большая часть Ansible предполагает, что весь закодированный текст — UTF-8. В какой-то момент, если возникнет спрос на другие кодировки, мы можем это изменить, но пока можно считать, что байты являются UTF-8.

Запись в файлы — это обратный процесс:

from ansible.module_utils._text import to_bytes

with open('filename.txt', 'wb') as my_file:
    my_file.write(to_bytes(some_text_string))

Обратите внимание, что нам не нужно ловить UnicodeError здесь, так как мы преобразуем в UTF-8, и все текстовые строки в Python могут быть преобразованы обратно в UTF-8.

Взаимодействие с файловой системой

Работа с именами файлов часто подразумевает возврат к байтам, потому что в системах типа Unix имена файлов являются байтами. В Python-2, если мы передадим текстовую строку этим функциям, текстовая строка будет преобразована в байтовую строку внутри функции, и произойдёт отслеживание ошибок, если присутствуют символы, не являющиеся ASCII. В Python-3 отслеживание ошибок произойдёт только в том случае, если текстовую строку нельзя декодировать в текущем языке, но всё же следует явно выполнять преобразования, чтобы код работал в обеих версиях:

import os.path

from ansible.module_utils._text import to_bytes

filename = u'/var/tmp/くらとみ.txt'
f = open(to_bytes(filename), 'wb')
mtime = os.path.getmtime(to_bytes(filename))
b_filename = os.path.expandvars(to_bytes(filename))
if os.path.exists(to_bytes(filename)):
    pass

Когда вы манипулируете именем файла только как строкой без обращения к файловой системе (или библиотеке C, которая обращается к файловой системе), вы можете часто обойтись без преобразования в байты:

import os.path

os.path.join(u'/var/tmp/café', u'くらとみ')
os.path.split(u'/var/tmp/café/くらとみ')

С другой стороны, если код должен обрабатывать имя файла и также обращаться к файловой системе, может быть удобнее сразу преобразовывать в байты и манипулировать ими.

Предупреждение

Убедитесь, что все переменные, передаваемые в функцию, имеют один и тот же тип. Если вы работаете с чем-то вроде os.path.join(), которое принимает несколько строк и использует их в комбинации, вам нужно убедиться, что все типы одинаковы (либо все байты, либо весь текст). Смешивание байтов и текста приведёт к отслеживанию ошибок.

Взаимодействие с другими программами

Взаимодействие с другими программами происходит через операционную систему и библиотеки C и работает с данными, которые определяет ядро UNIX. Эти интерфейсы ориентированы на байты, поэтому Python-интерфейс также ориентирован на байты. В Python-2 и Python-3 байтовые строки должны передаваться в библиотеку subprocess Python, и от неё должны ожидаться байтовые строки.

Одно из основных мест в коде контроллера Ansible, где мы взаимодействуем с другими программами, — это методы подключения плагинов exec_command. Эти методы преобразуют любые текстовые строки, которые они получают в команде (и аргументы к команде) для выполнения в байты и возвращают stdout и stderr как байтовые строки. Функции более высокого уровня (например, плагины действий _low_level_execute_command) преобразуют вывод в текстовые строки.

Советы, приемы и идиомы для принятия

Шаблон для совместимости с будущими версиями

Используйте следующий шаблон кода в верхней части всех модулей со стороны контроллера, чтобы гарантировать одинаковое поведение определённых конструкций в Python-2 и Python-3:

# Make coding more python3-ish
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type

__metaclass__ = type превращает все классы, определённые в файле, в классы нового стиля без явного наследования от object.

Импорты __future__ выполняют следующие действия:

absolute_import:
Заставляет импорты искать модули в sys.path, пропуская каталог, в котором находится модуль, выполняющий импорт. Если код хочет использовать каталог, в котором находится модуль, выполняющий импорт, есть новая нотация с точкой.
division: Делит целые числа всегда возвращает число с плавающей точкой. Если вам нужно найти частное, используйте x // y вместо x / y.
print_function: Превращает print() из ключевого слова в функцию.

См. также

  • PEP 0328: Абсолютные импорты
  • PEP 0238: Деление
  • PEP 3105: Функция print

Префикс байтовых строк с «b_»

Поскольку смешивание типов text и bytes приводит к ошибкам отслеживания, мы хотим четко понимать, какие переменные содержат текст, а какие — байты. Для этого мы добавляем префикс b_ к любой переменной, хранящей байты. Например:

filename = u'/var/tmp/café.txt'
b_filename = to_bytes(filename)
with open(b_filename) as f:
    data = f.read()

Мы не добавляем префикс к строкам текста, так как мы работаем со строками байтов только на границах, поэтому переменных, которым нужны байты, меньше, чем переменных, которым нужен текст.

Связанная библиотека six

Библиотека сторонних разработчиков python-six существует для помощи проектам в создании кода, который работает как на Python-2, так и на Python-3. Ansible включает в module_utils версию этой библиотеки, чтобы другие модули могли использовать её без необходимости установки на удалённой системе. Для её использования импортируйте её так:

from ansible.module_utils import six

Примечание

Ansible также может использовать системную копию six

Ansible будет использовать системную копию six, если системная копия является более поздней версией, чем та, что поставляется с Ansible.

Исключения

Для работы кода на Python-2.6+ и Python-3 используйте новый синтаксис обработки исключений, который использует ключевое слово as:

try:
    a = 2/0
except ValueError as e:
    module.fail_json(msg="Tried to divide by zero: %s" % e)

Не используйте следующий синтаксис, так как он будет работать неправильно на каждой версии Python-3:

try:
    a = 2/0
except ValueError, e:
    module.fail_json(msg="Tried to divide by zero: %s" % e)

Восьмеричные числа

В Python-2.x восьмеричные литералы можно было указать как 0755. В Python-3 восьмеричные числа должны быть указаны как 0o755.

Форматирование строк

Совместимость str.format()

Начиная с Python-2.6, строки получили метод format() для объединения строк. Однако одна часто используемая функция format() была добавлена только в Python-2.7, поэтому вам нужно помнить, что нельзя использовать её в коде Ansible:

# Does not work in Python-2.6!
new_string = "Dear {}, Welcome to {}".format(username, location)

# Use this instead
new_string = "Dear {0}, Welcome to {1}".format(username, location)

Обе приведенные выше строки форматирования сопоставляют позиционные аргументы метода format() в строке. Однако первая версия не работает в Python-2.6. Всегда помните, что нужно вставлять числа в заполнитель, чтобы код был совместим с Python-2.6.

См. также

Документация Python по форматированию строк

Использование форматирования с процентом со строками байтов

В Python-3.x строки байтов не имеют метода format(). Однако они поддерживают более старое форматирование с использованием процентов.

b_command_line = b'ansible-playbook --become-user %s -K %s' % (user, playbook_file)

Примечание

Форматирование с процентом добавлено в Python-3.5

Форматирование строк байтов с процентами было добавлено обратно в Python 3 в версии 3.5. Это не проблема для нас, так как наша минимальная версия Python — 3.5. Однако, если вы тестируете код Ansible с Python-3.4 или более ранней версией, вы обнаружите, что форматирование строк байтов здесь не будет работать. Обновите Python до версии 3.5 для тестирования.

См. также

Документация Python по форматированию с процентами

Перенос модулей на Python 3

Модули Ansible немного сложнее переносить, чем обычный код из других проектов. Для тестирования модуля Ansible требуется много мокирования, поэтому сложнее проверить, что ваш перенос исправил все ошибки, или убедиться, что последующие изменения не повлияли на поддержку Python-3.

Стратегия строк модуля

В Ansible большое количество модулей. Большинство из них поддерживаются сообществом Ansible, а не централизованной командой. Чтобы облегчить им жизнь, было принято решение не нарушать обратную совместимость, требуя, чтобы все строки внутри модулей были строками текста, и преобразования между текстом и байтами на границах; вместо этого мы используем стратегию родных строк на данный момент.

Родные строки относятся к типу, который Python использует, когда вы указываете строковый литерал без префиксов:

"This is a native string"

В Python-2 они являются строками байтов. В Python-3 это строковые значения. Модуль_utils, поставляемый с Ansible, пытается принять родные строки в качестве входных данных для своих функций и выдать родные строки в качестве выходных данных. Модули должны быть написаны так, чтобы ожидать байты в Python-2 и текстовые значения в Python-3.

Советы, хитрости и идиомы для использования

Синтаксис исключений, совместимый с Python-2.4

До Ansible-2.4 модулям необходимо было быть совместимыми с Python-2.4. Python-2.4 не понимал нового синтаксиса обработки исключений, поэтому нам пришлось написать функцию совместимости, которая могла работать как с Python-2, так и с Python-3. Вы можете всё ещё увидеть это в некоторых модулях:

from ansible.module_utils.pycompat24 import get_exception

try:
    a = 2/0
except ValueError:
    e = get_exception()
    module.fail_json(msg="Tried to divide by zero: %s" % e)

Если изменения не будут перенесены назад в Ansible-2.3, вам больше не нужно использовать это в новом коде.

Обходной путь для восьмеричных чисел в Python 2.4

До Ansible-2.4 модулям необходимо было быть совместимыми с Python-2.4. Python-2.4 не понимал нового синтаксиса для восьмеричных литералов, поэтому мы использовали следующий обходной путь для указания восьмеричных значений:

# Can't use 0755 on Python-3 and can't use 0o755 on Python-2.4
EXECUTABLE_PERMS = int('0755', 8)

Если изменения не будут перенесены назад в Ansible-2.3, вам больше не нужно использовать это в новом коде.

Перенос кода module_utils на Python 3

Код module_utils в основном похож на код модулей. Однако некоторые его части также используются контроллером. Из-за этого он должен быть совместим с предположениями контроллера. Это особенно заметно в стратегии строк.

Стратегия строк module_utils

module_utils обязательно должен использовать стратегию родных строк. Функции в module_utils получают либо строковые значения, либо строки байтов и могут выводить тот же тип, что и получили, или родную строку для версии Python, на которой они выполняются, в зависимости от того, что имеет больше смысла для этой функции. Функции, которые возвращают строки, обязательно должны документировать, возвращают ли они текст, байты или родные строки. Поэтому функции module_utils часто являются очень защищенными, преобразуя потенциальные текстовые или байтовые значения в начале функции и преобразуя их в тип родной строки в конце.

© 2012–2018 Michael DeHaan
© 2018–2019 Red Hat, Inc.
Licensed under the GNU General Public License version 3.
https://docs.ansible.com/ansible/2.4/dev_guide/developing_python3.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API