本文基于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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
|
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
|
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
|
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
|
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
|
@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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
|
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操作。