Python ruamel.yaml сбрасывает теги с кавычками

1

Я пытаюсь использовать ruamel.yaml для изменения шаблона AWS CloudFormation на лету с помощью python. Я добавил следующий код, чтобы safe_load работал с облачными функциями, такими как !Ref. Однако, когда я выкидываю их, эти значения с помощью Ref (или любых других функций) будут обернуты кавычками. CloudFormation не может идентифицировать это.

См. Пример ниже:

import sys, json, io, boto3
import ruamel.yaml

def funcparse(loader, node):
  node.value = {
      ruamel.yaml.ScalarNode:   loader.construct_scalar,
      ruamel.yaml.SequenceNode: loader.construct_sequence,
      ruamel.yaml.MappingNode:  loader.construct_mapping,
  }[type(node)](node)
  node.tag = node.tag.replace(u'!Ref', 'Ref').replace(u'!', u'Fn::')
  return dict([ (node.tag, node.value) ])

funcnames = [ 'Ref', 'Base64', 'FindInMap', 'GetAtt', 'GetAZs', 'ImportValue',
              'Join', 'Select', 'Split', 'Split', 'Sub', 'And', 'Equals', 'If',
              'Not', 'Or' ]

for func in funcnames:
    ruamel.yaml.SafeLoader.add_constructor(u'!' + func, funcparse)

txt = open("/space/tmp/a.template","r")
base = ruamel.yaml.safe_load(txt)
base["foo"] = {
    "name": "abc",
    "Resources": {
        "RouteTableId" : "!Ref aaa",
        "VpcPeeringConnectionId" : "!Ref bbb",
        "yourname": "dfw"
    }
}

ruamel.yaml.safe_dump(
    base,
    sys.stdout,
    default_flow_style=False
)

Входной файл выглядит следующим образом:

foo:
  bar: !Ref barr
  aa: !Ref bb

Вывод выглядит следующим образом:

foo:
  Resources:
    RouteTableId: '!Ref aaa'
    VpcPeeringConnectionId: '!Ref bbb'
    yourname: dfw
  name: abc

Обратите внимание, что '! Ref VpcRouteTable' обернут одинарными кавычками. Это не будет определено CloudFormation. Есть ли способ настроить самосвал, чтобы выход был следующим:

foo:
  Resources:
    RouteTableId: !Ref aaa
    VpcPeeringConnectionId: !Ref bbb
    yourname: dfw
  name: abc

Другие вещи, которые я пробовал:

  • pyyaml, работает одинаково
  • Используйте Ref :: вместо! Ref, работает одинаково
Теги:
yaml
ruamel.yaml
pyyaml

2 ответа

2
Лучший ответ

По сути вы настраиваете загрузчик, загружаете тегированные (скалярные) объекты, как если бы они были сопоставлениями, с тегом - ключ и значение - скаляр. Но вы не делаете ничего, чтобы отличить dict загруженный от такого сопоставления из других dicts, загруженных из нормальных отображений, и у вас нет какого-либо конкретного кода для представления такого сопоставления, чтобы "вернуть тег".

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

То, что обфускает все это, заключается в том, что ваш пример перезаписывает загруженные данные, назначая base["foo"] поэтому единственное, что вы можете извлечь из safe_load, и весь ваш код до этого, заключается в том, что он не генерирует исключение, Т.е. если вы не укажете строки, начинающиеся с base["foo"] = { ваш результат будет выглядеть так:

foo:
  aa:
    Ref: bb
  bar:
    Ref: barr

И в том, что Ref: bb не отличается от обычного сбрасываемого dict. Если вы хотите изучить этот маршрут, то вы должны сделать подкласс TagDict(dict), и есть funcparse вернуть этот подкласс, а также добавить representer для этого подкласса, который воссоздает тег из ключа, а затем выводит значение. После того, как это будет работать (с обратной связью равно), вы можете сделать:

     "RouteTableId" : TagDict('Ref', 'aaa')

Если вы это сделаете, вы должны, кроме удаления неиспользуемых библиотек, также изменить свой код, чтобы закрыть файл-указатель txt в коде, поскольку это может привести к проблемам. Вы можете сделать это элегантно, используя инструкцию with:

with open("/space/tmp/a.template","r") as txt:
    base = ruamel.yaml.safe_load(txt)

(Я также оставил бы "r" (или поставил перед ним пробел) и заменил txt более подходящим именем переменной, указывающим, что это указатель файла (ввода)).

У вас также есть запись 'Split' дважды в ваших funcnames, что является излишним.


Более общее решение может быть достигнуто с помощью multi-constructor который соответствует любому тегу и имеет три основных типа для покрытия скаляров, отображений и последовательностей.

import sys
import ruamel.yaml

yaml_str = """\
foo:
  scalar: !Ref barr
  mapping: !Select
    a: !Ref 1
    b: !Base64 A413
  sequence: !Split
  - !Ref baz
  - !Split Multi word scalar
"""

class Generic:
    def __init__(self, tag, value, style=None):
        self._value = value
        self._tag = tag
        self._style = style


class GenericScalar(Generic):
    @classmethod
    def to_yaml(self, representer, node):
        return representer.represent_scalar(node._tag, node._value)

    @staticmethod
    def construct(constructor, node):
        return constructor.construct_scalar(node)


class GenericMapping(Generic):
    @classmethod
    def to_yaml(self, representer, node):
        return representer.represent_mapping(node._tag, node._value)

    @staticmethod
    def construct(constructor, node):
        return constructor.construct_mapping(node, deep=True)


class GenericSequence(Generic):
    @classmethod
    def to_yaml(self, representer, node):
        return representer.represent_sequence(node._tag, node._value)

    @staticmethod
    def construct(constructor, node):
        return constructor.construct_sequence(node, deep=True)


def default_constructor(constructor, tag_suffix, node):
    generic = {
        ruamel.yaml.ScalarNode: GenericScalar,
        ruamel.yaml.MappingNode: GenericMapping,
        ruamel.yaml.SequenceNode: GenericSequence,
    }.get(type(node))
    if generic is None:
        raise NotImplementedError('Node: ' + str(type(node)))
    style = getattr(node, 'style', None)
    instance = generic.__new__(generic)
    yield instance
    state = generic.construct(constructor, node)
    instance.__init__(tag_suffix, state, style=style)


ruamel.yaml.add_multi_constructor('', default_constructor, Loader=ruamel.yaml.SafeLoader)


yaml = ruamel.yaml.YAML(typ='safe', pure=True)
yaml.default_flow_style = False
yaml.register_class(GenericScalar)
yaml.register_class(GenericMapping)
yaml.register_class(GenericSequence)

base = yaml.load(yaml_str)
base['bar'] = {
    'name': 'abc',
    'Resources': {
        'RouteTableId' : GenericScalar('!Ref', 'aaa'),
        'VpcPeeringConnectionId' : GenericScalar('!Ref', 'bbb'),
        'yourname': 'dfw',
        's' : GenericSequence('!Split', ['a', GenericScalar('!Not', 'b'), 'c']),
    }
}
yaml.dump(base, sys.stdout)

который выводит:

bar:
  Resources:
    RouteTableId: !Ref aaa
    VpcPeeringConnectionId: !Ref bbb
    s: !Split
    - a
    - !Not b
    - c
    yourname: dfw
  name: abc
foo:
  mapping: !Select
    a: !Ref 1
    b: !Base64 A413
  scalar: !Ref barr
  sequence: !Split
  - !Ref baz
  - !Split Multi word scalar

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

  • тег, который вы указываете, действительно действителен
  • значение, связанное с тегом, имеет правильный тип для этого имени тега (скаляр, отображение, последовательность)
  • если вы хотите, чтобы GenericMapping вел себя больше как dict, тогда вы, вероятно, захотите, чтобы он был подклассом dict (а не Generic) и предоставил соответствующий __init__ (idem для GenericSequence/list)

Когда назначение изменено на нечто более близкое к вашему:

base["foo"] = {
    "name": "abc",
    "Resources": {
        "RouteTableId" : GenericScalar('!Ref', 'aaa'),
        "VpcPeeringConnectionId" : GenericScalar('!Ref', 'bbb'),
        "yourname": "dfw"
    }
}

выход:

foo:
  Resources:
    RouteTableId: !Ref aaa
    VpcPeeringConnectionId: !Ref bbb
    yourname: dfw
  name: abc

который именно то, что вы хотите.

0

Помимо подробного ответа Anthon выше, для конкретного вопроса с точки зрения шаблона CloudFormation, я нашел еще одно очень быстрое и приятное решение.

Все еще используя фрагмент конструктора для загрузки YAML.

def funcparse(loader, node):
  node.value = {
      ruamel.yaml.ScalarNode:   loader.construct_scalar,
      ruamel.yaml.SequenceNode: loader.construct_sequence,
      ruamel.yaml.MappingNode:  loader.construct_mapping,
  }[type(node)](node)
  node.tag = node.tag.replace(u'!Ref', 'Ref').replace(u'!', u'Fn::')
  return dict([ (node.tag, node.value) ])

funcnames = [ 'Ref', 'Base64', 'FindInMap', 'GetAtt', 'GetAZs', 'ImportValue',
              'Join', 'Select', 'Split', 'Split', 'Sub', 'And', 'Equals', 'If',
              'Not', 'Or' ]

for func in funcnames:
    ruamel.yaml.SafeLoader.add_constructor(u'!' + func, funcparse)

Когда мы манипулируем данными, а не делаем

base["foo"] = {
    "name": "abc",
    "Resources": {
        "RouteTableId" : "!Ref aaa",
        "VpcPeeringConnectionId" : "!Ref bbb",
        "yourname": "dfw"
    }
}

который обернет значение !Ref aaa с кавычками, мы можем просто сделать:

base["foo"] = {
    "name": "abc",
    "Resources": {
        "RouteTableId" : {
            "Ref" : "aaa"
        },
        "VpcPeeringConnectionId" : {
            "Ref" : "bbb
         },
        "yourname": "dfw"
    }
}

Аналогично, для других функций CloudFormation, таких как GetAtt, мы должны использовать их длинную форму Fn::GetAtt и использовать их в качестве ключа объекта JSON. Проблема решена легко.

Ещё вопросы

Сообщество Overcoder
Наверх
Меню