一、简要回顾
对于请求:
地址:/dashboard/admin/instances/
方式:POST
参数:
instances_filter_q:
action:instances__soft_reboot__89a8849b-a3cd-4ce0-9158-c3dd69e8508e
URL绑定为:
openstack_dashboard/dashboards/admin/instances/urls.py
二、目录结构
接下来主要分析
openstack_dashboard/dashboards/admin/instances/views.py
views.AdminIndexView.as_view()
从Django的generic view说起....
generic view中的as_view()可以返回一个Django的view函数,该view函数会构造出类实例,将as_view()中传入的参数设置为该实例的属性,然后调用dispatch函数,dispatch函数通常会将request请求转给类中的post或get函数。
generic view的主要使用方法是用子类重写其中的属性或方法。
详细情况可以参考Django官方文档:
https://docs.djangoproject.com/en/1.7/topics/class-based-views/
【小杰鱼说】对Django框架的深入了解对于理解Horizon十分必要,as_view函数最终达到的效果还是将处理逻辑放入post函数或get函数中,这点和其他网络框架类似。
openstack_dashboard/dashboards/admin/instances/views.py
分析AdminIndexView.as_view(),其会调用该类的post函数
class AdminIndexView(tables.DataTableView):
table_class = project_tables.AdminInstancesTable
template_name = ‘admin/instances/index.html‘
由于AdminIndexViewà DataTableViewàMultiTableView,如下图所示。追踪到MultiTableView.post,该post函数会调用该类的get函数。
三、此处插入对DataTable、DataTableView、Action三者的简要概括:
DataTableView簇:
_data={
表名:data(通过get_data函数获得)
...
}
_tables={
表名:table实例
}
table=table实例
说明:本例中data为一个包含instance的list
可以通过table_class绑定具体的DataTable
通过get_data函数获取data,该函数通常调用openstack_dashboard/api模块获取数据
handle_table函数负责将data和table挂钩
综上,DataTableView正如其名字一样,拥有table和data,负责处理data的获取,Table的创建,以及二者的绑定等
DataTable簇:
规定table的column和action
可以处理和table绑定的data
take_action函数负责处理action
Action簇:
利用action函数进行处理
继续分析get函数...
horizon/tables/views.py
MultiTableView类:
def get(self, request, *args, **kwargs):
handled = self.construct_tables()
if handled:
return handled
"""此处暂且不管 """
context = self.get_context_data(**kwargs)
return self.render_to_response(context)
def construct_tables(self):
"""根据类中的table_class属性绑定的DataTable类,创建或返回DataTable对象,此处为AdminInstancesTable对象 """
tables = self.get_tables().values()
# Early out before data is loaded
for table in tables:
"""预先处理,此处暂且不管 """
preempted = table.maybe_preempt()
if preempted:
return preempted
# Load data into each table and check for action handlers
for table in tables:
handled = self.handle_table(table)
if handled:
return handled
# If we didn‘t already return a response, returning None continues
# with the view as normal.
return None
四、此处插入AdminInstanceTable的创建过程,解释如下:
其中使用了metaclass对DataTable及其子类进行修改
先观察AdminInstancesTable类和DataTableOptions类:
class AdminInstancesTable(tables.DataTable):
...
class Meta:
name = "instances"
verbose_name = _("Instances")
status_columns = ["status", "task"]
table_actions = (project_tables.TerminateInstance,
AdminInstanceFilterAction)
row_class = AdminUpdateRow
row_actions = (project_tables.ConfirmResize,
project_tables.RevertResize,
AdminEditInstance,
project_tables.ConsoleLink,
project_tables.LogLink,
project_tables.CreateSnapshot,
project_tables.TogglePause,
project_tables.ToggleSuspend,
MigrateInstance,
project_tables.SoftRebootInstance,
project_tables.RebootInstance,
project_tables.TerminateInstance)
class DataTableOptions(object):
def __init__(self, options):
self.name = getattr(options, ‘name‘, self.__class__.__name__)
verbose_name = getattr(options, ‘verbose_name‘, None) \
or self.name.title()
self.verbose_name = verbose_name
self.columns = getattr(options, ‘columns‘, None)
self.status_columns = getattr(options, ‘status_columns‘, [])
self.table_actions = getattr(options, ‘table_actions‘, [])
self.row_actions = getattr(options, ‘row_actions‘, [])
self.row_class = getattr(options, ‘row_class‘, Row)
self.column_class = getattr(options, ‘column_class‘, Column)
self.pagination_param = getattr(options, ‘pagination_param‘, ‘marker‘)
...
接着分析metaclass对类的修改...
class DataTable(object):
__metaclass__ = DataTableMetaclass
class DataTableMetaclass(type):
def __new__(mcs, name, bases, attrs):
# Process options from Meta
class_name = name
"""将类中的Meta转变为DataTableOptions,添加为类的_meta属性"""
attrs["_meta"] = opts = DataTableOptions(attrs.get("Meta", None))
# Gather columns; this prevents the column from being an attribute
# on the DataTable class and avoids naming conflicts.
"""将类中的column属性聚集作为新的列属性,阻止其作为类属性"""
columns = []
for attr_name, obj in attrs.items():
if issubclass(type(obj), (opts.column_class, Column)):
column_instance = attrs.pop(attr_name)
column_instance.name = attr_name
column_instance.classes.append(‘normal_column‘)
columns.append((attr_name, column_instance))
columns.sort(key=lambda x: x[1].creation_counter)
# Iterate in reverse to preserve final order
for base in bases[::-1]:
if hasattr(base, ‘base_columns‘):
columns = base.base_columns.items() + columns
attrs[‘base_columns‘] = SortedDict(columns)
...
"""收集row_action和table_action对象"""
actions = list(set(opts.row_actions) | set(opts.table_actions))
actions.sort(key=attrgetter(‘name‘))
actions_dict = SortedDict([(action.name, action())
for action in actions])
attrs[‘base_actions‘] = actions_dict
if opts._filter_action:
# Replace our filter action with the instantiated version
opts._filter_action = actions_dict[opts._filter_action.name]
# Create our new class!
return type.__new__(mcs, name, bases, attrs)
概况如下图:
【小杰鱼说】使用metaclass对类进行修改,这样极大地增加了程序的可扩展性和灵活性,但同时复杂度也增大。
metaclass的理解可以参考:
http://blog.csdn.net/psh2009/article/details/10330747
http://jianpx.iteye.com/blog/908121
继续分析handle_table函数...
MultiTableMixin类:
def handle_table(self, table):
name = table.name
"""获取数据"""
data = self._get_data_dict()
"""获取与该DataTable相关的数据,并将数据和该DataTable挂钩"""
self._tables[name].data = data[table._meta.name]
"""暂且不管"""
self._tables[name]._meta.has_more_data = self.has_more_data(table)
"""此处为调用AdminInstancesTable.maybe_handle函数"""
handled = self._tables[name].maybe_handle()
return handled
horizon/tables/base.py
DataTable类:
def maybe_handle(self):
"""
Determine whether the request should be handled by any action on this
table after data has been loaded.
"""
request = self.request
"""获取request中的数据,这里为
table_name=’instances’
action_name=’soft_reboot’
obj_id=’89a8849b-a3cd-4ce0-9158-c3dd69e8508e’
"""
table_name, action_name, obj_id = self.check_handler(request)
if table_name == self.name and action_name:
action_names = [action.name for action in
self.base_actions.values() if not action.preempt]
# do not run preemptive actions here
if action_name in action_names:
return self.take_action(action_name, obj_id)
return None
五、此处插入Action簇的关系,如下图所示:
继续分析take_action函数...
horizon/tables/base.py
DataTable类:
"""
action_name=’soft_reboot’
obj_id=’89a8849b-a3cd-4ce0-9158-c3dd69e8508e’
"""
def take_action(self, action_name, obj_id=None, obj_ids=None):
# See if we have a list of ids
obj_ids = obj_ids or self.request.POST.getlist(‘object_ids‘)
"""得到SoftRebootInstance实例"""
action = self.base_actions.get(action_name, None)
if not action or action.method != self.request.method:
# We either didn‘t get an action or we‘re being hacked. Goodbye.
return None
# Meanhile, back in Gotham...
if not action.requires_input or obj_id or obj_ids:
if obj_id:
obj_id = self.sanitize_id(obj_id)
if obj_ids:
obj_ids = [self.sanitize_id(i) for i in obj_ids]
"""SoftRebootInstanceàRebootInstanceàBatchActionàAction,由于BatchAction有handle函数,所以在Action的__init__()中将属性handles_multiple设置为True"""
# Single handling is easy
if not action.handles_multiple:
response = action.single(self, self.request, obj_id)
# Otherwise figure out what to pass along
else:#进入此项
# Preference given to a specific id, since that implies
# the user selected an action for just one row.
if obj_id:
obj_ids = [obj_id]
response = action.multiple(self, self.request, obj_ids)
return response
elif action and action.requires_input and not (obj_id or obj_ids):
messages.info(self.request,
_("Please select a row before taking that action."))
return None
这里使用了一个trick
horizon/tables/actions.py
Action类:
def __init__(...)
...
if not has_multiple and self.handles_multiple:
def multiple(self, data_table, request, object_ids):
return self.handle(data_table, request, object_ids)
"""为该实例动态绑定multiple方法,其实质为调用handle方法"""
self.multiple = new.instancemethod(multiple, self)
所以,接下来分析BatchAction中的handle函数...
horizon/tables/actions.py
BatchAction类:
def handle(self, table, request, obj_ids):
action_success = []
action_failure = []
action_not_allowed = []
for datum_id in obj_ids:
datum = table.get_object_by_id(datum_id)
datum_display = table.get_object_display(datum) or _("N/A")
if not table._filter_action(self, request, datum):
action_not_allowed.append(datum_display)
LOG.info(‘Permission denied to %s: "%s"‘ %
(self._conjugate(past=True).lower(), datum_display))
continue
try:
self.action(request, datum_id)
#Call update to invoke changes if needed
self.update(request, datum)
action_success.append(datum_display)
self.success_ids.append(datum_id)
LOG.info(‘%s: "%s"‘ %
(self._conjugate(past=True), datum_display))
except Exception as ex:
# Handle the exception but silence it since we‘ll display
# an aggregate error message later. Otherwise we‘d get
# multiple error messages displayed to the user.
if getattr(ex, "_safe_message", None):
ignore = False
else:
ignore = True
action_failure.append(datum_display)
exceptions.handle(request, ignore=ignore)
...
return shortcuts.redirect(self.get_success_url(request))
openstack_dashboard/dashboards/project/instances/tables.py
SoftRebootInstance类:
class SoftRebootInstance(RebootInstance):
name = "soft_reboot"
action_present = _("Soft Reboot")
action_past = _("Soft Rebooted")
def action(self, request, obj_id):
api.nova.server_reboot(request, obj_id, soft_reboot=True)
参考文档:
http://docs.openstack.org/developer/horizon/