本作品采用知识共享署名 3.0 非移植许可协议授权。 http://creativecommons.org/licenses/by/3.0/legalcode

集中验证逻辑

https://blueprints.launchpad.net/designate/+spec/validation-cleanup

问题描述

目前,验证在 V1 和 V2 API 之间重复出现,并且未来在更多地方需要验证(入站 AXFR、动态 DNS 等)。将这些验证集中到 Designate 对象中,为所有入口点提供一个可重用的中心位置。

提议的变更

集中此逻辑需要完成多个阶段

  1. 实现对象注册表

  2. 实现对象验证

  3. 实现“适配器”层,替换 V2 API 的视图

  4. 将模式从 designate/resources/schemas 迁移到对象

  5. 更新 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 添加三个新方法

  1. 一个 obj_get_schema(cls) 方法

    class DesignateObject(object):
        @classmethod
        def obj_get_schema(cls):
            """Returns the JSON Schema for this Object."""
    
  2. 一个 is_valid(self) 方法

    class DesignateObject(object):
        def is_valid(self):
            """Returns True if the Object is valid."""
    
  3. 一个 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

工作项

工作项目如“提议的更改”部分所述。

依赖项

  • 无已知依赖项