Neutron配额管理源码分析




本文基于Mitaka版本源码进行分析。

Neutron跟Nova的配额管理流程有一些不同,但设计思想都是类似的。

相关命令

[root@host-10-0-70-141 neutron]# neutron --help | grep quota
  quota-delete                      Delete defined quotas of a given tenant.
  quota-list                        List quotas of all tenants who have non-default quota values.
  quota-show                        Show quotas of a given tenant.
  quota-update                      Define tenant's quotas not to use defaults.
[root@host-10-0-70-141 neutron]# neutron quota-show    ### 也可以指定--tenant-id $ID来查看给定tenant的配额情况                
+---------------------+-------+
| Field               | Value |
+---------------------+-------+
| floatingip          | 50    |
| network             | 10    |
| port                | 3     |
| rbac_policy         | 10    |
| router              | 10    |
| security_group      | 3     |
| security_group_rule | 100   |
| subnet              | 10    |
| subnetpool          | -1    |
+---------------------+-------+

相关配置项

默认配额是在neutron.conf中配置的,具体配置项如下,没有加入到配置项里面的,用default_quota配置项的值作为默认配额,需注意’track_quota_usage’的默认值为True。

    cfg.IntOpt('default_quota',
               default=-1,
               help=_('Default number of resource allowed per tenant. '
                      'A negative value means unlimited.')),
    cfg.IntOpt('quota_network',
               default=10,
               help=_('Number of networks allowed per tenant. '
                      'A negative value means unlimited.')),
    cfg.IntOpt('quota_subnet',
               default=10,
               help=_('Number of subnets allowed per tenant, '
                      'A negative value means unlimited.')),
    cfg.IntOpt('quota_port',
               default=50,
               help=_('Number of ports allowed per tenant. '
                      'A negative value means unlimited.')),

其他配置项不用管,实际上neutron.conf中[quotas]段不需要进行配置,除非你想对network、subnet、port等资源设置不同的默认配额。

相关数据库表

quotas:保存租户各种资源(network、port等)配额限制信息

quotausages:保存租户各种资源的配额使用量信息

reservations:保存资源预留信息(预留过期时间),一般是操作过程中用到,操作结束后清理

resourcedeltas:同上,保存资源预留信息(预留资源量)

配额使用量查询

Mitaka版本没有提供,据说Pike版本提供了,http://www.cnblogs.com/sammyliu/p/7453548.html,但没有来及的分析源码。

也就是说没有API用来查询配额使用量,在做相关web或者对外API开发的时候就比较郁闷了,只能自己增加接口,直接查询quotausages表,或者直接在相关资源的表如ports中统计使用量。

配额管理流程

以network为例,其他资源管理流程基本完全相同。API请求转发流程可以参考:http://aspirer.wang/?p=690

资源注册

router中注册network的controller过程中会同步注册到配额管理模块,

–> neutron.api.v2.router.APIRouter:

class APIRouter(base_wsgi.Router):

    @classmethod
    def factory(cls, global_config, **local_config):
        return cls(**local_config)

    def __init__(self, **local_config):
        ......
        col_kwargs = dict(collection_actions=COLLECTION_ACTIONS,
                          member_actions=MEMBER_ACTIONS)

        def _map_resource(collection, resource, params, parent=None):
            ......
            mapper_kwargs = dict(controller=controller,
                                 requirements=REQUIREMENTS,
                                 path_prefix=path_prefix,
                                 **col_kwargs)
            return mapper.collection(collection, resource,
                                     **mapper_kwargs)

        mapper.connect('index', '/', controller=Index(RESOURCES))
        for resource in RESOURCES:
            _map_resource(RESOURCES[resource], resource,
                          attributes.RESOURCE_ATTRIBUTE_MAP.get(
                              RESOURCES[resource], dict()))
            resource_registry.register_resource_by_name(resource)   ### 注册network等资源到配额管理模块
        ......

–> neutron.quota.resource_registry.register_resource_by_name –> neutron.quota.resource_registry.ResourceRegistry#register_resource_by_name –> neutron.quota.resource_registry.ResourceRegistry#_create_resource_instance:

    def _create_resource_instance(self, resource_name, plural_name):
        ......
        if (not cfg.CONF.QUOTAS.track_quota_usage or
            resource_name not in self._tracked_resource_mappings):
            LOG.info(_LI("Creating instance of CountableResource for "
                         "resource:%s"), resource_name)
            return resource.CountableResource(
                resource_name, resource._count_resource,
                'quota_%s' % resource_name)
        else:
            LOG.info(_LI("Creating instance of TrackedResource for "
                         "resource:%s"), resource_name)
            return resource.TrackedResource(  ### 一般资源都是TrackedResource类型
                resource_name,
                self._tracked_resource_mappings[resource_name],
                'quota_%s' % resource_name)

其中_tracked_resource_mappings属性的初始化流程是:

–> neutron.plugins.ml2.plugin.Ml2Plugin#__init__:

    @resource_registry.tracked_resources(
        network=models_v2.Network,
        port=models_v2.Port,
        subnet=models_v2.Subnet,
        subnetpool=models_v2.SubnetPool,
        security_group=securitygroups_db.SecurityGroup,
        security_group_rule=securitygroups_db.SecurityGroupRule)
    def __init__(self):

–> neutron.quota.resource_registry.tracked_resources:

class tracked_resources(object):
    ......
    def __call__(self, f):

        @six.wraps(f)
        def wrapper(*args, **kwargs):
            registry = ResourceRegistry.get_instance()
            for resource_name in self._tracked_resources:
                registry.set_tracked_resource(
                    resource_name,
                    self._tracked_resources[resource_name],
                    self._override)
            return f(*args, **kwargs)

        return wrapper

–> neutron.quota.resource_registry.ResourceRegistry#set_tracked_resource:

    def set_tracked_resource(self, resource_name, model_class, override=False):
        # Do not do anything if tracking is disabled by config
        if not cfg.CONF.QUOTAS.track_quota_usage:
            return

        current_model_class = self._tracked_resource_mappings.setdefault(
            resource_name, model_class)

        # Check whether setdefault also set the entry in the dict
        if current_model_class != model_class:
            LOG.debug("A model class is already defined for %(resource)s: "
                      "%(current_model_class)s. Override:%(override)s",
                      {'resource': resource_name,
                       'current_model_class': current_model_class,
                       'override': override})
            if override:
                self._tracked_resource_mappings[resource_name] = model_class
        LOG.debug("Tracking information for resource: %s configured",
                  resource_name)

数据库表内容变更回调注册

–> neutron.api.v2.router.APIRouter#__init__:

        mapper.connect('index', '/', controller=Index(RESOURCES))
        for resource in RESOURCES:
            _map_resource(RESOURCES[resource], resource,
                          attributes.RESOURCE_ATTRIBUTE_MAP.get(
                              RESOURCES[resource], dict()))
            resource_registry.register_resource_by_name(resource)  ### 注册network、subnet等资源

–> neutron.quota.resource_registry.register_resource_by_name –> neutron.quota.resource_registry.ResourceRegistry#register_resource_by_name –> neutron.quota.resource_registry.ResourceRegistry#register_resource –> neutron.quota.resource.TrackedResource#register_events:

    def register_events(self):  ### 注册回调
        event.listen(self._model_class, 'after_insert', self._db_event_handler)
        event.listen(self._model_class, 'after_delete', self._db_event_handler)

–> neutron.quota.resource.TrackedResource#_db_event_handler:

    def _db_event_handler(self, mapper, _conn, target):
        try:
            tenant_id = target['tenant_id']
        except AttributeError:
            with excutils.save_and_reraise_exception():
                LOG.error(_LE("Model class %s does not have a tenant_id "
                              "attribute"), target)
        self._dirty_tenants.add(tenant_id)    ### network或其他资源表插入或删除数据的时候就标记为dirty tenant

 

创建

neutron.api.v2.base.Controller#create –> neutron.api.v2.base.Controller#_create:

        # Quota enforcement
        reservations = []
        try:
            for (tenant, delta) in request_deltas.items():
                reservation = quota.QUOTAS.make_reservation(
                    request.context,
                    tenant,
                    {self._resource: delta},
                    self._plugin)   ### 创建配额预留资源记录
                reservations.append(reservation)
        except exceptions.QuotaResourceUnknown as e:
                # We don't want to quota this resource
                LOG.debug(e)

–> neutron.db.quota.driver.DbQuotaDriver#make_reservation:

    def make_reservation(self, context, tenant_id, resources, deltas, plugin):
        
        requested_resources = deltas.keys()
        with db_api.autonested_transaction(context.session):
            ......
            current_usages = dict(
                (resource, resources[resource].count(
                    context, plugin, tenant_id, resync_usage=False)) for
                resource in requested_resources)
            # Adjust for expired reservations. Apparently it is cheaper than
            # querying every time for active reservations and counting overall
            # quantity of resources reserved
            expired_deltas = quota_api.get_reservations_for_resources(
                context, tenant_id, requested_resources, expired=True)
            # Verify that the request can be accepted with current limits
            resources_over_limit = []
            for resource in requested_resources:
                expired_reservations = expired_deltas.get(resource, 0)
                total_usage = current_usages[resource] - expired_reservations
                res_headroom = current_limits[resource] - total_usage
                ......
                if res_headroom < deltas[resource]:
                    resources_over_limit.append(resource)
                if expired_reservations:
                    self._handle_expired_reservations(context, tenant_id)

            if resources_over_limit:   ### 配额不足的异常就是在这里抛出来的
                raise exceptions.OverQuota(overs=sorted(resources_over_limit))
            # Success, store the reservation
            # TODO(salv-orlando): Make expiration time configurable
            return quota_api.create_reservation(
                context, tenant_id, deltas)

–> neutron.db.quota.api.create_reservation:

def create_reservation(context, tenant_id, deltas, expiration=None):
    # This method is usually called from within another transaction.
    # Consider using begin_nested
    with context.session.begin(subtransactions=True):
        expiration = expiration or (utcnow() + datetime.timedelta(0, 120))
        resv = quota_models.Reservation(tenant_id=tenant_id,   ### 插入reservations表
                                        expiration=expiration)
        context.session.add(resv)
        for (resource, delta) in deltas.items():
            context.session.add(
                quota_models.ResourceDelta(resource=resource, ### 插入resourcedeltas表
                                           amount=delta,
                                           reservation=resv))
    return ReservationInfo(resv['id'],
                           resv['tenant_id'],
                           resv['expiration'],
                           dict((delta.resource, delta.amount)
                                for delta in resv.resource_deltas))

回到开始的地方–> neutron.api.v2.base.Controller#_create:创建network成功后,发送notify消息,并提交配额变更,

        def notify(create_result):
            # Ensure usage trackers for all resources affected by this API
            # operation are marked as dirty
            with request.context.session.begin():
                # Commit the reservation(s)
                for reservation in reservations:
                    quota.QUOTAS.commit_reservation(
                        request.context, reservation.reservation_id)
                resource_registry.set_resources_dirty(request.context)

–> neutron.db.quota.driver.DbQuotaDriver#commit_reservation:

    def commit_reservation(self, context, reservation_id):
        # Do not mark resource usage as dirty. If a reservation is committed,
        # then the relevant resources have been created. Usage data for these
        # resources has therefore already been marked dirty.
        quota_api.remove_reservation(context, reservation_id,
                                     set_dirty=False)  ### 删除资源预留2个表中的记录

–> neutron.quota.resource_registry.set_resources_dirty:

def set_resources_dirty(context):
    ......
    if not cfg.CONF.QUOTAS.track_quota_usage:
        return

    for res in get_all_resources().values():
        with context.session.begin(subtransactions=True):
            if is_tracked(res.name) and res.dirty:   ### res.dirty是下面的property方法
                res.mark_dirty(context)

–> neutron.quota.resource.TrackedResource#dirty&#mark_dirty:

    @property
    def dirty(self):
        return self._dirty_tenants   ### 这个属性已经在数据库network等资源表插入、删除的回调中修改了(上面提到的_db_event_handler方法)

    def mark_dirty(self, context):
        if not self._dirty_tenants:
            return
        with db_api.autonested_transaction(context.session):
            ......
            dirty_tenants_snap = self._dirty_tenants.copy()
            for tenant_id in dirty_tenants_snap:
                quota_api.set_quota_usage_dirty(context, self.name, tenant_id)  ### 根据tenant_id和resource名称修改quotausages表中的dirty字段为1
                LOG.debug(("Persisted dirty status for tenant:%(tenant_id)s "
                           "on resource:%(resource)s"),
                          {'tenant_id': tenant_id, 'resource': self.name})
        self._out_of_sync_tenants |= dirty_tenants_snap
        self._dirty_tenants -= dirty_tenants_snap

从上面的代码可以看出,实际上提交变更只是清理了资源预留表记录,并修改quotausages表中相关resource和tenant记录的dirty字段,并没有修改in_use字段。

删除

neutron.api.v2.base.Controller#delete  –> neutron.api.v2.base.Controller#_delete:

    def delete(self, request, id, **kwargs):
        """Deletes the specified entity."""
        if request.body:
            msg = _('Request body is not supported in DELETE.')
            raise webob.exc.HTTPBadRequest(msg)
        self._notifier.info(request.context,
                            self._resource + '.delete.start',
                            {self._resource + '_id': id})
        return self._delete(request, id, **kwargs)
    @db_api.retry_db_errors
    def _delete(self, request, id, **kwargs):
        action = self._plugin_handlers[self.DELETE]
        ......

        # A delete operation usually alters resource usage, so mark affected
        # usage trackers as dirty
        resource_registry.set_resources_dirty(request.context)   ### 标记为dirty状态,具体流程参考创建流程中相关代码分析
        notifier_method = self._resource + '.delete.end'
        ......

这里也跟创建流程类似,没有修改in_use字段。

修改

update方法中也是调用set_resources_dirty方法,同删除流程。

查询

index方法调用到_items

–> neutron.api.v2.base.Controller#_items:

        if pagination_links:
            collection[self._collection + "_links"] = pagination_links
        # Synchronize usage trackers, if needed
        resource_registry.resync_resource(   ### 同步network表中tenant资源使用量到quotausages表
            request.context, self._resource, request.context.tenant_id)
        return collection

–> neutron.quota.resource_registry.resync_resource:

def resync_resource(context, resource_name, tenant_id):
    if not cfg.CONF.QUOTAS.track_quota_usage:
        return

    if is_tracked(resource_name):
        res = get_resource(resource_name)
        # If the resource is tracked count supports the resync_usage parameter
        res.resync(context, tenant_id)  ### 资源使用量数据同步

上面的res变量是一个neutron.quota.resource.TrackedResource对象,

–> neutron.quota.resource.TrackedResource#resync&_resync&_set_quota_usage:

    def resync(self, context, tenant_id):
        if tenant_id not in self._out_of_sync_tenants:
            return
        LOG.debug(("Synchronizing usage tracker for tenant:%(tenant_id)s on "
                   "resource:%(resource)s"),
                  {'tenant_id': tenant_id, 'resource': self.name})
        in_use = context.session.query(self._model_class).filter_by(
            tenant_id=tenant_id).count()
        # Update quota usage
        return self._resync(context, tenant_id, in_use)
    def _resync(self, context, tenant_id, in_use):
        # Update quota usage
        usage_info = self._set_quota_usage(context, tenant_id, in_use)

        self._dirty_tenants.discard(tenant_id)
        self._out_of_sync_tenants.discard(tenant_id)
        LOG.debug(("Unset dirty status for tenant:%(tenant_id)s on "
                   "resource:%(resource)s"),
                  {'tenant_id': tenant_id, 'resource': self.name})
        return usage_info
    def _set_quota_usage(self, context, tenant_id, in_use):
        return quota_api.set_quota_usage(
            context, self.name, tenant_id, in_use=in_use)

–> neutron.db.quota.api.set_quota_usage:

def set_quota_usage(context, resource, tenant_id,
                    in_use=None, delta=False):
    ......
    with db_api.autonested_transaction(context.session):   ### 修改quotausages表
        query = common_db_api.model_query(context, quota_models.QuotaUsage)
        query = query.filter_by(resource=resource).filter_by(
            tenant_id=tenant_id)
        usage_data = query.first()
        if not usage_data:
            # Must create entry
            usage_data = quota_models.QuotaUsage(
                resource=resource,
                tenant_id=tenant_id)
            context.session.add(usage_data) ### 如果配额使用量信息没有记录到quotausages表则添加进去
        # Perform explicit comparison with None as 0 is a valid value
        if in_use is not None:
            if delta:
                in_use = usage_data.in_use + in_use
            usage_data.in_use = in_use  ### 更新quotausages表中network等资源的in_use字段的值
        # After an explicit update the dirty bit should always be reset
        usage_data.dirty = False  ### 更新quotausages表中network等资源的dirty字段的值
    return QuotaUsageInfo(usage_data.resource,
                          usage_data.tenant_id,
                          usage_data.in_use,
                          usage_data.dirty)

也就是说真正修改quotausages表中in_use字段的操作是index,也就是列出网络等资源,对应的命令时neutron net-list或neutron xxx-list。

show方法没有涉及到quota操作。