本作品采用知识共享署名 3.0 非移植许可协议授权。 http://creativecommons.org/licenses/by/3.0/legalcode
集中验证逻辑¶
https://blueprints.launchpad.net/designate/+spec/validation-cleanup
问题描述¶
目前,验证在 V1 和 V2 API 之间重复出现,并且未来在更多地方需要验证(入站 AXFR、动态 DNS 等)。将这些验证集中到 Designate 对象中,为所有入口点提供一个可重用的中心位置。
提议的变更¶
集中此逻辑需要完成多个阶段
实现对象注册表
实现对象验证
实现“适配器”层,替换 V2 API 的视图
将模式从 designate/resources/schemas 迁移到对象
更新 API 层(V1 和 V2),以使用新的验证和适配器
对象注册表¶
对象注册表允许通过类名查找任何 DesignateObject 类的引用。这将允许一个对象的模式轻松引用其他对象。
注意
对象注册表不会取代检索对象类的标准和现有方法(通过导入)。注册表提供了一种使用类名获取类引用的替代方法。这对于需要在 Python 代码外部引用对象的情况很有用。例如,在 JSON-Schema 中,或在服务之间使用 oslo.messaging 传递的 JSON 消息中。
为了实现注册表,DesignateObjectMetaclass 类将被更新,以跟踪每个对象类在构造时的一个引用。这些引用将存储在附加到 DesignateObject 基类的一个字典中。
注意
DesignateObjectMetaclass 代码在对象类构造时执行,而不是在对象实例创建时执行。这确保了代码只在 Designate 服务启动时执行一次。
注册表查找将通过一个新的 DesignateObject.obj_cls_from_name() 方法执行,该方法将接受一个用于对象名称的字符串参数。
class DesignateObject(object):
@classmethod
def obj_cls_from_name(cls, name):
pass
注册表的示例用法
class RecordSet(DesignateObject):
FIELDS = {
'id': {},
'name': {},
}
RecordSet = DesignateObject.obj_cls_from_name('RecordSet')
my_recordset = RecordSet(id='12345', name='example.org.')
对象验证¶
对象验证规则将继续使用 JSON-Schema,在每个字段级别实现
class ValidatableObject(DesignateObject):
FIELDS = {
'id': {
'required': True,
'schema': {
'type': 'string',
'format': 'uuid'
}
},
'ttl': {
'schema': {
'type': 'integer',
'minimum': 0,
'maximum': 100
}
},
'recursive': {
'schema': {
'$ref': 'obj://ValidatableObject/#',
}
},
'nested': {
'schema': {
'$ref': 'obj://AnotherObject/#',
}
}
}
为了构造最终和完整的模式,并实例化模式验证器,DesignateObjectMetaclass 类将被更新,以调用一个 make_class_validator(cls) 方法,该方法类似于 make_class_properties(cls) 方法实现。
此 make_class_validator 方法将把每个字段的模式片段组装成完整的 JSON Schema,并生成必要的样板代码。 此外,此方法将构建 python-jsonschema Validator 实例并将其作为 cls._obj_validator 附加到对象类。
最后,将向基类 DesignateObject 添加三个新方法
一个 obj_get_schema(cls) 方法
class DesignateObject(object): @classmethod def obj_get_schema(cls): """Returns the JSON Schema for this Object."""
一个 is_valid(self) 方法
class DesignateObject(object): def is_valid(self): """Returns True if the Object is valid."""
一个 validate(self) 方法
class DesignateObject(object): def validate(self): """ Raises an InvalidObject exception if the Object is invalid Attached to the `errors` attribute of exception will be a `ValidationErrorList` object containing the details of the failures. """
验证的示例用法
class RecordSet(DesignateObject):
FIELDS = {
'id': {
'required': True,
'schema': {
'type': 'string',
'format': 'uuid'
}
},
'ttl': {
'schema': {
'type': 'integer',
'minimum': 0,
'maximum': 100
}
}
}
my_recordset = RecordSet(id='12345', ttl=50)
# Returns False, as the 12345 is NOT a UUID.
my_recordset.is_valid()
try:
# Raises an InvalidObject exception, as the 12345 is NOT a UUID.
my_recordset.validate()
except InvalidObject as e:
LOG.warning('Invalid Object, Errors below:')
for error in e.errors:
LOG.warning('Error at path %s, Message: %s', e.absolute_path,
e.message)
对象适配器¶
对象适配器将取代当前的 V2 API 视图,从而提供一种结构化的方式,将对象转换为 V1 或 V2 API 格式。 这包括标准输出中字段的重命名、呈现的 JSON Schema,以及在 ValidationError 消息中,并支持隐藏在匹配的 API 版本中不应可见的字段。
注意
以下是一个 WIP 模拟 - 预计会有变化!
对象适配器的示例用法
# Standard Object Definition
class Domain(DesignateObject):
FIELDS = {
'id': {
'required': True,
'schema': {
'type': 'string',
'format': 'uuid'
}
},
'name': {
'schema': {
'type': 'string',
'pattern': 'domainname'
}
},
'ttl': {
'schema': {
'type': 'integer',
'minimum': 0,
'maximum': 100
}
},
'version': {
'schema': {
'type': 'integer',
'minimum': 0,
'maximum': 100
}
}
}
# Define the V2 API Adaptor for the Domain Object above
class DomainAdaptorV2(DesignateObjectAdaptorV2):
obj_cls = Domain
obj_list_cls = DomainList
# Any fields NOT specificed will not be returned by the API.
FIELDS = {
'id': {
# No V2 Specific Customization Needed
}
'ttl': {
# Let's rename "ttl" to "default_ttl" in V2
'name': 'default_ttl'
}
}
# Use the Adaptor in the API
class ZonesController(rest.RestController):
_adaptor = DomainAdaptorV2()
@pecan.expose(template='json:', content_type='application/json')
@utils.validate_uuid('zone_id')
def get_one(self, zone_id):
"""Get Zone"""
# Real life would Fetch a zone from designate-central
domain = Domain(id='2b9e1b86-d4f1-42d2-88ff-b888f2dd068a'
name='example.com.',
ttl=50)
return self._adaptor.render(domain, single=True)
@pecan.expose(template='json:', content_type='application/json')
def post_all(self):
"""Create Zone"""
request = pecan.request
response = pecan.response
context = request.environ['context']
# The Adaptor class will parse the incoming JSON into an
# approperiate Object instance, trigger validation, and raise
# an exception if there are any failures. The
# `FaultWrapperMiddleware` will catch and render this exception.
domain = self._adaptor.parse(request.body_dict, single=True)
# Create the Domain
domain = self.central_api.create_domain(context, domain)
# Prepare the response headers and status
response.status_int = 201
response.headers['Location'] = '<url for new zone>'
# Send the response
return self._adaptor.render(domain)
其他变更¶
Designate 的任何其他更改,按正在更改的子系统细分
实现¶
负责人¶
- 主要负责人
kiall
里程碑¶
- 完成目标里程碑
Kilo-1
工作项¶
工作项目如“提议的更改”部分所述。
依赖项¶
无已知依赖项