Я пытаюсь использовать 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
Другие вещи, которые я пробовал:
По сути вы настраиваете загрузчик, загружаете тегированные (скалярные) объекты, как если бы они были сопоставлениями, с тегом - ключ и значение - скаляр. Но вы не делаете ничего, чтобы отличить 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
который именно то, что вы хотите.
Помимо подробного ответа 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. Проблема решена легко.