Horizon 深度解析:OpenStack Web 控制台架构与定制开发
定位与职责
Horizon 是 OpenStack 的 Web 管理控制台,基于 Django 框架构建:
- 提供图形化界面管理所有 OpenStack 资源
- 通过 OpenStack Python SDK 调用各组件 API
- 支持插件化扩展(自定义 Panel/Dashboard)
- 多租户视图(普通用户 vs 管理员视图)
架构总览
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| 浏览器 │ HTTP ▼ Apache/Nginx(静态资源) │ WSGI ▼ Django Application(horizon) ├── openstack_dashboard/ ← 主应用 │ ├── dashboards/ │ │ ├── project/ ← 项目视图(普通用户) │ │ ├── admin/ ← 管理员视图 │ │ └── identity/ ← 身份管理视图 │ └── api/ ← 封装 OpenStack API 调用 │ └── horizon/ ← 框架核心(Panel/Dashboard 基类) ├── base.py ← Dashboard/Panel 基类 ├── tables.py ← 数据表格框架 ├── forms.py ← 表单框架 └── workflows.py ← 多步骤向导框架
|
Panel 插件体系
Horizon 的核心设计是插件化,每个功能模块是一个 Panel:
1 2 3 4 5 6 7 8 9 10 11 12 13
|
class Dashboard(Registry, HorizonComponent): """顶级导航项(如 Project、Admin、Identity)""" name = "Project" slug = "project" panels = ('compute', 'network', 'storage', ...)
class Panel(HorizonComponent): """二级导航项(如 Instances、Networks)""" name = "Instances" slug = "instances" urls = 'openstack_dashboard.dashboards.project.instances.urls'
|
注册自定义 Panel
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| from django.utils.translation import gettext_lazy as _ import horizon
class MyCustomPanel(horizon.Panel): name = _("My Custom Panel") slug = "my_custom"
class MyDashboard(horizon.Dashboard): name = _("My Dashboard") slug = "my_dashboard" panels = ('my_custom',) default_panel = 'my_custom'
horizon.register(MyDashboard)
|
数据表格框架
Horizon 的 DataTable 是最常用的 UI 组件:
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
|
from horizon import tables
class TerminateInstance(tables.BatchAction): name = "terminate" action_type = "danger"
def action(self, request, obj_id): api.nova.server_delete(request, obj_id)
class InstancesTable(tables.DataTable): name = tables.Column("name", verbose_name=_("Instance Name"), link="horizon:project:instances:detail") status = tables.Column("status", verbose_name=_("Status"), status=True, status_choices=STATUS_CHOICES) ip = tables.Column("ip", verbose_name=_("IP Address")) flavor = tables.Column("flavor_name", verbose_name=_("Flavor"))
class Meta: name = "instances" verbose_name = _("Instances") table_actions = (LaunchLink, TerminateInstance, InstancesFilterAction) row_actions = (StartInstance, StopInstance, RebootInstance, TerminateInstance, ConsoleLink)
|
API 封装层
Horizon 通过 openstack_dashboard/api/ 封装所有 OpenStack API 调用:
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
|
def server_list(request, search_opts=None, all_tenants=False): """获取虚拟机列表""" c = novaclient(request) if all_tenants: search_opts['all_tenants'] = True return c.servers.list(True, search_opts)
def server_create(request, name, image, flavor, key_name, ...): """创建虚拟机""" return novaclient(request).servers.create( name, image, flavor, key_name=key_name, security_groups=security_groups, nics=nics, ... )
def novaclient(request): """获取 Nova 客户端(带 Token)""" return nova_client.Client( NOVA_API_VERSION, session=keystone.get_session(request), endpoint_override=base.url_for(request, 'compute'), )
|
多步骤向导(Workflow)
创建虚拟机这类复杂操作使用 Workflow 框架:
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
|
class SetInstanceDetails(workflows.Step): action_class = SetInstanceDetailsAction contributes = ("source_type", "source_id", "flavor_id", "count", "name")
class SetNetwork(workflows.Step): action_class = SetNetworkAction contributes = ("network_id",)
class SetSecurityGroups(workflows.Step): action_class = SetSecurityGroupsAction contributes = ("security_group_ids",)
class LaunchInstance(workflows.Workflow): slug = "launch_instance" name = _("Launch Instance") steps = (SetInstanceDetails, SetNetwork, SetSecurityGroups, SetKeypair, SetAdvanced)
def handle(self, request, context): api.nova.server_create( request, context['name'], context['source_id'], context['flavor_id'], ... )
|
定制开发实践
1. 添加自定义列
1 2 3 4 5 6 7
| class InstancesTable(tables.DataTable): custom_tag = tables.Column( lambda obj: obj.metadata.get('custom_tag', '-'), verbose_name=_("Custom Tag") )
|
2. 自定义 Action
1 2 3 4 5 6 7 8 9 10 11
| class MigrateInstance(tables.LinkAction): name = "migrate" verbose_name = _("Live Migrate") url = "horizon:admin:instances:live_migrate" classes = ("ajax-modal",) icon = "exchange"
def allowed(self, request, instance): return (request.user.is_superuser and instance.status == "ACTIVE")
|
3. 自定义 Panel 完整示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| import horizon
class MyPanel(horizon.Panel): name = "GPU 管理" slug = "gpu_management" permissions = ('openstack.roles.admin',)
from horizon import tables from . import tables as gpu_tables
class IndexView(tables.DataTableView): table_class = gpu_tables.GPUTable template_name = 'admin/gpu_management/index.html' page_title = "GPU 资源管理"
def get_data(self): return api.get_gpu_list(self.request)
|
性能优化
Horizon 默认性能较差,生产环境需要优化:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
|
CACHES = { 'default': { 'BACKEND': 'django.core.cache.backends.memcached.PyMemcacheCache', 'LOCATION': 'memcached:11211', } }
COMPRESS_ENABLED = True COMPRESS_OFFLINE = True
OPENSTACK_NEUTRON_NETWORK = { 'enable_router': True, 'enable_quotas': True, 'enable_ipv6': False, }
SESSION_ENGINE = 'django.contrib.sessions.backends.cache'
|
生产部署
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| server { listen 80; server_name horizon.example.com;
location /static { alias /var/lib/openstack-dashboard/static; expires 30d; }
location / { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }
|
1 2 3 4 5 6 7 8 9 10 11
| python manage.py collectstatic --noinput
python manage.py compress --force
gunicorn openstack_dashboard.wsgi:application \ --workers 4 \ --bind 127.0.0.1:8080 \ --timeout 120
|