diff --git a/computes/templates/computes/instances.html b/computes/templates/computes/instances.html index e56c32af..15f6b3e5 100644 --- a/computes/templates/computes/instances.html +++ b/computes/templates/computes/instances.html @@ -27,7 +27,7 @@
{% if compute.status is not True %}
- {% trans 'Problem occurred with host' %} {{ compute.name }}: {{ compute.connection_error }} + {% blocktrans with name=compute.name error=compute.connection_error %}Problem occurred with host {{ name }}: {{ error }}{% endblocktrans %}
{% endif %} {% if not instances %} diff --git a/console/refusals.py b/console/refusals.py new file mode 100644 index 00000000..52c3e593 --- /dev/null +++ b/console/refusals.py @@ -0,0 +1,9 @@ +"""socketiod's refusals, which its page shows in the user's language. Here, +as makemessages reads only .py files and socketiod has no extension.""" + +from django.utils.translation import gettext_noop + +TURNED_OFF = gettext_noop("The serial console is turned off on this server") +NOT_ALLOWED = gettext_noop("The console link has expired, or you may no longer open this console: reload the page") +OPEN_ELSEWHERE = gettext_noop("The console of this VM is open in another window") +TOO_MANY = gettext_noop("Too many consoles are open on this server: try again later") diff --git a/console/socketiod b/console/socketiod index cb02db20..3f7df5ae 100755 --- a/console/socketiod +++ b/console/socketiod @@ -30,7 +30,11 @@ import socketio # noqa: E402 from gunicorn.app.base import BaseApplication # noqa: E402 from django import db # noqa: E402 from django.conf import settings # noqa: E402 +from django.http import HttpRequest # noqa: E402 +from django.http.cookie import parse_cookie # noqa: E402 +from django.utils import translation # noqa: E402 +from console import refusals # noqa: E402 from console.tokens import authorize, session_user_id # noqa: E402 CONSOLECALLBACK = os.path.join(ROOT_PATH, "conf", "daemon", "consolecallback") @@ -49,30 +53,34 @@ sessions = {} sessions_lock = threading.Lock() -def refused(reason): - """The refusal the page shows; it is logged too.""" +def refused(environ, reason): + """The refusal the page shows, in its language (LocaleMiddleware's choice: + the language cookie, else the browser's); it is logged too, in English.""" logger.info("serial console refused: %s", reason) - return socketio.exceptions.ConnectionRefusedError(reason) + request = HttpRequest() + request.COOKIES, request.META = parse_cookie(environ.get("HTTP_COOKIE", "")), environ + with translation.override(translation.get_language_from_request(request)): + return socketio.exceptions.ConnectionRefusedError(translation.gettext(reason)) @sio.event def connect(sid, environ, auth): # the page shows the reason if not settings.SERIAL_CONSOLE_ENABLED: - raise refused("The serial console is turned off on this server") + raise refused(environ, refusals.TURNED_OFF) try: instance, user = authorize((auth or {}).get("token"), "pty") except PermissionError: - raise refused("The console link has expired, or you may no longer open this console: reload the page") + raise refused(environ, refusals.NOT_ALLOWED) finally: db.close_old_connections() taken = None with sessions_lock: open_sid = next((s for s, session in sessions.items() if session["vm"] == instance.pk), None) if open_sid is not None and not user.is_superuser: - raise refused("The console of this VM is open in another window") + raise refused(environ, refusals.OPEN_ELSEWHERE) if open_sid is None and len(sessions) >= MAX_SESSIONS: - raise refused("Too many consoles are open on this server: try again later") + raise refused(environ, refusals.TOO_MANY) if open_sid is not None: # a superuser takes over a console left open taken = open_sid, sessions.pop(open_sid) # the page tells its user who took the console; first, as its diff --git a/console/tests/test_socketiod.py b/console/tests/test_socketiod.py index 183426f8..6f5e9b83 100644 --- a/console/tests/test_socketiod.py +++ b/console/tests/test_socketiod.py @@ -116,6 +116,18 @@ class SocketiodTestCase(Base): self.assertIn("Too many", self.refused("over", {"token": self.token(vm=other)})) self.assertEqual(list(self.d.sessions), ["first"]) + @override_settings(SERIAL_CONSOLE_ENABLED=False) + def test_a_refusal_is_in_the_page_s_language_and_logged_in_english(self): + # as LocaleMiddleware: the language cookie first, then the browser's + for environ, shown in (({"HTTP_COOKIE": "django_language=tr", "HTTP_ACCEPT_LANGUAGE": "zh-hans"}, "Seri konsol bu sunucuda kapalı"), + ({"HTTP_ACCEPT_LANGUAGE": "zh-CN,zh;q=0.9"}, "此服务器已关闭串行控制台"), + ({}, "The serial console is turned off on this server")): + with self.subTest(shown), self.assertRaises(self.d.socketio.exceptions.ConnectionRefusedError) as refusal, \ + self.assertLogs("socketiod", level="INFO") as log: + self.d.connect("sid", environ, {"token": self.token()}) + self.assertEqual(refusal.exception.error_args["message"], shown) + self.assertIn("refused: The serial console is turned off on this server", log.output[0]) + @override_settings(SERIAL_CONSOLE_ENABLED=True) def test_input_and_resize_go_to_the_session_and_are_checked(self): self.d.connect("sid-1", {}, {"token": self.token()}) diff --git a/instances/api/viewsets.py b/instances/api/viewsets.py index 1022d0c3..e56845e3 100644 --- a/instances/api/viewsets.py +++ b/instances/api/viewsets.py @@ -5,6 +5,7 @@ from django.contrib.messages import get_messages from django.http import Http404 from django.shortcuts import get_object_or_404 from django.urls import reverse +from django.utils.translation import gettext as _ from instances.jobs import MIGRATE_OPTIONS from instances.models import Flavor, Instance from instances.views import ( @@ -144,7 +145,7 @@ class InstanceViewSet(viewsets.ViewSet): if compute_pk is not None and str(queryset.compute_id) != str(compute_pk): raise Http404() if queryset.is_template: - return Response({"status": "Templates cannot be started"}, status=status.HTTP_409_CONFLICT) + return Response({"status": _("Templates cannot be started.")}, status=status.HTTP_409_CONFLICT) return instance_action_response(poweron, request, pk, "poweron command send") @extend_schema(request=None, responses=ActionStatusSerializer) @@ -162,7 +163,7 @@ class InstanceViewSet(viewsets.ViewSet): if compute_pk is not None and str(queryset.compute_id) != str(compute_pk): raise Http404() if queryset.is_template: - return Response({"status": "Templates cannot be started"}, status=status.HTTP_409_CONFLICT) + return Response({"status": _("Templates cannot be started.")}, status=status.HTTP_409_CONFLICT) return instance_action_response(powercycle, request, pk, "powercycle command send") @extend_schema(request=None, responses=ActionStatusSerializer) diff --git a/instances/views.py b/instances/views.py index cb21f25a..3e3ccc58 100755 --- a/instances/views.py +++ b/instances/views.py @@ -640,7 +640,7 @@ def _busy(request, pk, busy): def _moved(request, pk): if hasattr(request, "accepted_renderer") or from_script(request): - return JsonResponse({"detail": "The VM was moved meanwhile; try again."}, status=409) + return JsonResponse({"detail": _("The VM was moved meanwhile; try again.")}, status=409) messages.error(request, _("The VM was moved meanwhile; try again.")) return get_safe_redirect(request, default=reverse("instances:instance", args=[pk])) diff --git a/locale/tr/LC_MESSAGES/django.mo b/locale/tr/LC_MESSAGES/django.mo index 459b4e79..cf2d984f 100644 Binary files a/locale/tr/LC_MESSAGES/django.mo and b/locale/tr/LC_MESSAGES/django.mo differ diff --git a/locale/tr/LC_MESSAGES/django.po b/locale/tr/LC_MESSAGES/django.po index 4bdf1e10..c6391990 100644 --- a/locale/tr/LC_MESSAGES/django.po +++ b/locale/tr/LC_MESSAGES/django.po @@ -2119,8 +2119,10 @@ msgid "New Instance" msgstr "Yeni sanal makine" #: computes/templates/computes/instances.html:30 -msgid "Problem occurred with host" -msgstr "Sunucu ile ilgili bir problem meydana geldi" +#, python-format +#| msgid "Problem occurred with host" +msgid "Problem occurred with host %(name)s: %(error)s" +msgstr "%(name)s sunucusuyla ilgili bir sorun oluştu: %(error)s" #: computes/templates/computes/instances.html:36 #: computes/templates/computes/list.html:16 @@ -2379,7 +2381,7 @@ msgstr "" "Sunucudaki /etc/default/webvirt-gstfsd içindeki GSTFSD_KEY değerinin 64 " "onaltılık karakterini girin" -#: computes/views.py:202 +#: computes/views.py:205 #, python-format msgid "" "Its VMs keep running on the host and leave the panel; their owners are kept " @@ -2390,6 +2392,28 @@ msgstr "" "Kaldırılan Sanal Makineler sayfasında %(days)s gün tutulur ve sunucu yeniden " "eklenirse geri gelir." +#: console/refusals.py:6 +#| msgid "The device is not on this host" +msgid "The serial console is turned off on this server" +msgstr "Seri konsol bu sunucuda kapalı" + +#: console/refusals.py:7 +msgid "" +"The console link has expired, or you may no longer open this console: reload " +"the page" +msgstr "" +"Konsol bağlantısının süresi doldu ya da bu konsolu artık açamazsınız: sayfayı" +" yeniden yükleyin" + +#: console/refusals.py:8 +#| msgid "The console starts scaled to its window" +msgid "The console of this VM is open in another window" +msgstr "Bu sanal makinenin konsolu başka bir pencerede açık" + +#: console/refusals.py:9 +msgid "Too many consoles are open on this server: try again later" +msgstr "Bu sunucuda çok fazla konsol açık: daha sonra yeniden deneyin" + #: console/templates/console-menu.html:6 msgid "Send key(s)" msgstr "Anahtar(ları) gönder" @@ -2634,6 +2658,11 @@ msgstr "" "'%(type)s' konsol tipi desteklenmiyor. Lütfen sanal makinenin grafik tipini " "VNC olarak değiştirin." +#: instances/api/viewsets.py:148 instances/api/viewsets.py:166 +#: instances/views.py:792 instances/views.py:807 +msgid "Templates cannot be started." +msgstr "Şablon makineler başlatılamaz." + #: instances/forms.py:13 instances/templates/create_instance_w2.html:69 #: instances/templates/instances/settings_tab.html:857 msgid "Memory (MiB)" @@ -2999,7 +3028,6 @@ msgstr "Sanal makine tanımlanıyor" #: instances/jobs.py:906 #, python-format -#| msgid "The panel failed unexpectedly; its log has the details." msgid "" "The panel failed unexpectedly; its log has the details. Remove these volumes " "by hand: %(volumes)s" @@ -5924,7 +5952,7 @@ msgstr "" "Sunucu aygıtları olan bir sanal makine taşınamaz: hedefte bu aygıtlar " "olmayabilir. Önce onları kaldırın." -#: instances/views.py:644 +#: instances/views.py:643 instances/views.py:644 msgid "The VM was moved meanwhile; try again." msgstr "Sanal makine bu arada taşındı; yeniden deneyin." @@ -5958,10 +5986,6 @@ msgstr "" msgid "Invalid disk options: %(options)s" msgstr "Geçersiz disk seçenekleri: %(options)s" -#: instances/views.py:792 instances/views.py:807 -msgid "Templates cannot be started." -msgstr "Şablon makineler başlatılamaz." - #: instances/views.py:999 #, python-format msgid "Set the gstfsd key of %(compute)s first (Computes, edit the host)" @@ -7628,6 +7652,12 @@ msgstr "" msgid "%(phase)s (over its time limit)" msgstr "%(phase)s (süre sınırını aştı)" +#: operations/jobs.py:151 +#| msgid "The form has no valid submission key; reload the page" +msgid "This form was already submitted for another request; reload the page" +msgstr "" +"Bu form başka bir istek için zaten gönderildi; sayfayı yeniden yükleyin" + #: operations/jobs.py:157 msgid "The form has no valid submission key; reload the page" msgstr "Formun geçerli bir gönderim anahtarı yok; sayfayı yeniden yükleyin" @@ -7907,10 +7937,14 @@ msgstr "" "Kurtarma bir saat boyunca işin durumunu okuyamadı; sanal makineyi kontrol " "edin, sonra bu işi çözün" -#: operations/worker.py:474 +#: operations/worker.py:479 msgid "The worker stopped before the job changed anything" msgstr "Worker, iş herhangi bir şeyi değiştirmeden önce durdu" +#: storages/api/viewsets.py:203 storages/api/viewsets.py:224 +msgid "Pool is not active" +msgstr "Havuz etkin değil" + #: storages/forms.py:30 storages/templates/storage.html:102 #: storages/templates/storages.html:43 msgid "Path" @@ -8009,6 +8043,26 @@ msgstr "Kopya oluşturulmadı" msgid "Upload ISO image" msgstr "ISO imajı yükle" +#: storages/jobs.py:118 +#| msgid "Invalid file size." +msgid "Invalid file name" +msgstr "Geçersiz dosya adı" + +#: storages/jobs.py:120 +#| msgid "Only .iso files can be uploaded." +msgid "Only .iso files can be uploaded" +msgstr "Yalnızca .iso dosyaları yüklenebilir" + +#: storages/jobs.py:122 +#| msgid "Invalid file size." +msgid "Invalid file size" +msgstr "Geçersiz dosya boyutu" + +#: storages/jobs.py:124 +#| msgid "Invalid chunk index or total chunks." +msgid "Invalid chunk index or total chunks" +msgstr "Geçersiz parça dizini veya toplam parça sayısı" + #: storages/templates/create_stg_block.html:7 msgid "New pool" msgstr "Yeni havuz" @@ -9834,6 +9888,11 @@ msgstr "XML geçerli değil: %(err)s" msgid "The XML may not declare a DTD" msgstr "XML bir DTD bildiremez" +#: vrtManager/util.py:143 +#, python-format +msgid "MAC address must be of the format AA:BB:CC:DD:EE:FF, was %(mac)s" +msgstr "MAC adresi AA:BB:CC:DD:EE:FF biçiminde olmalı; girilen: %(mac)s" + #: vrtManager/virtsecrets.py:26 msgid "A Ceph key is base64 text, as `ceph auth get-key` prints it" msgstr "" diff --git a/locale/zh/LC_MESSAGES/django.mo b/locale/zh/LC_MESSAGES/django.mo index 17aeed85..7de24a53 100644 Binary files a/locale/zh/LC_MESSAGES/django.mo and b/locale/zh/LC_MESSAGES/django.mo differ diff --git a/locale/zh/LC_MESSAGES/django.po b/locale/zh/LC_MESSAGES/django.po index 07d063bc..cd19c93e 100644 --- a/locale/zh/LC_MESSAGES/django.po +++ b/locale/zh/LC_MESSAGES/django.po @@ -2050,8 +2050,10 @@ msgid "New Instance" msgstr "新建虚拟机" #: computes/templates/computes/instances.html:30 -msgid "Problem occurred with host" -msgstr "主机出现问题" +#, python-format +#| msgid "Problem occurred with host" +msgid "Problem occurred with host %(name)s: %(error)s" +msgstr "主机 %(name)s 出现问题:%(error)s" #: computes/templates/computes/instances.html:36 #: computes/templates/computes/list.html:16 @@ -2302,7 +2304,7 @@ msgid "" "/etc/default/webvirt-gstfsd" msgstr "请输入主机 /etc/default/webvirt-gstfsd 中 GSTFSD_KEY 的 64 个十六进制字符" -#: computes/views.py:202 +#: computes/views.py:205 #, python-format msgid "" "Its VMs keep running on the host and leave the panel; their owners are kept " @@ -2310,6 +2312,26 @@ msgid "" "again." msgstr "其虚拟机继续在主机上运行并离开面板;其所有者在“已移除的虚拟机”页面保留 %(days)s 天,如果再次添加该主机则会恢复。" +#: console/refusals.py:6 +#| msgid "The device is not on this host" +msgid "The serial console is turned off on this server" +msgstr "此服务器已关闭串行控制台" + +#: console/refusals.py:7 +msgid "" +"The console link has expired, or you may no longer open this console: reload " +"the page" +msgstr "控制台链接已过期,或您已无权打开此控制台:请重新加载页面" + +#: console/refusals.py:8 +#| msgid "The console starts scaled to its window" +msgid "The console of this VM is open in another window" +msgstr "此虚拟机的控制台已在另一个窗口中打开" + +#: console/refusals.py:9 +msgid "Too many consoles are open on this server: try again later" +msgstr "此服务器上打开的控制台过多:请稍后再试" + #: console/templates/console-menu.html:6 msgid "Send key(s)" msgstr "发送按键" @@ -2541,6 +2563,11 @@ msgid "" "VNC." msgstr "不支持控制台类型 '%(type)s'。请将虚拟机图形切换为 VNC。" +#: instances/api/viewsets.py:148 instances/api/viewsets.py:166 +#: instances/views.py:792 instances/views.py:807 +msgid "Templates cannot be started." +msgstr "模板无法启动。" + #: instances/forms.py:13 instances/templates/create_instance_w2.html:69 #: instances/templates/instances/settings_tab.html:857 msgid "Memory (MiB)" @@ -2879,7 +2906,6 @@ msgstr "正在定义虚拟机" #: instances/jobs.py:906 #, python-format -#| msgid "The panel failed unexpectedly; its log has the details." msgid "" "The panel failed unexpectedly; its log has the details. Remove these volumes " "by hand: %(volumes)s" @@ -5623,7 +5649,7 @@ msgid "" " Remove them first." msgstr "有主机设备的虚拟机无法迁移:目标主机可能没有这些设备。请先移除它们。" -#: instances/views.py:644 +#: instances/views.py:643 instances/views.py:644 msgid "The VM was moved meanwhile; try again." msgstr "该虚拟机在此期间已被移动;请重试。" @@ -5652,10 +5678,6 @@ msgstr "" msgid "Invalid disk options: %(options)s" msgstr "无效的磁盘选项:%(options)s" -#: instances/views.py:792 instances/views.py:807 -msgid "Templates cannot be started." -msgstr "模板无法启动。" - #: instances/views.py:999 #, python-format msgid "Set the gstfsd key of %(compute)s first (Computes, edit the host)" @@ -7226,6 +7248,11 @@ msgstr "字母、数字、点、短横线和下划线。副本包含该过滤器 msgid "%(phase)s (over its time limit)" msgstr "%(phase)s(已超出时间限制)" +#: operations/jobs.py:151 +#| msgid "The form has no valid submission key; reload the page" +msgid "This form was already submitted for another request; reload the page" +msgstr "此表单已为另一个请求提交过;请重新加载页面" + #: operations/jobs.py:157 msgid "The form has no valid submission key; reload the page" msgstr "表单没有有效的提交密钥;请重新加载页面" @@ -7490,10 +7517,14 @@ msgid "" "resolve this job" msgstr "恢复在一小时内无法读取任务状态;请检查虚拟机,然后处理此任务" -#: operations/worker.py:474 +#: operations/worker.py:479 msgid "The worker stopped before the job changed anything" msgstr "工作进程在任务做出任何更改之前停止" +#: storages/api/viewsets.py:203 storages/api/viewsets.py:224 +msgid "Pool is not active" +msgstr "存储池未激活" + #: storages/forms.py:30 storages/templates/storage.html:102 #: storages/templates/storages.html:43 msgid "Path" @@ -7591,6 +7622,26 @@ msgstr "未创建副本" msgid "Upload ISO image" msgstr "上传 ISO 镜像" +#: storages/jobs.py:118 +#| msgid "Invalid file size." +msgid "Invalid file name" +msgstr "文件名无效" + +#: storages/jobs.py:120 +#| msgid "Only .iso files can be uploaded." +msgid "Only .iso files can be uploaded" +msgstr "只能上传 .iso 文件" + +#: storages/jobs.py:122 +#| msgid "Invalid file size." +msgid "Invalid file size" +msgstr "文件大小无效" + +#: storages/jobs.py:124 +#| msgid "Invalid chunk index or total chunks." +msgid "Invalid chunk index or total chunks" +msgstr "分块索引或分块总数无效" + #: storages/templates/create_stg_block.html:7 msgid "New pool" msgstr "新建存储池" @@ -9274,6 +9325,11 @@ msgstr "XML 无效:%(err)s" msgid "The XML may not declare a DTD" msgstr "XML 不能声明 DTD" +#: vrtManager/util.py:143 +#, python-format +msgid "MAC address must be of the format AA:BB:CC:DD:EE:FF, was %(mac)s" +msgstr "MAC 地址的格式必须为 AA:BB:CC:DD:EE:FF,实际为 %(mac)s" + #: vrtManager/virtsecrets.py:26 msgid "A Ceph key is base64 text, as `ceph auth get-key` prints it" msgstr "Ceph 密钥是 base64 文本,即 `ceph auth get-key` 输出的内容" diff --git a/operations/jobs.py b/operations/jobs.py index 6bc056ce..bbe1dc9b 100644 --- a/operations/jobs.py +++ b/operations/jobs.py @@ -13,7 +13,7 @@ from django.db import transaction from django.db.models import F, Value from django.db.models.functions import Coalesce from django.utils import timezone -from django.utils.translation import gettext_noop +from django.utils.translation import gettext, gettext_noop from logs.audit import write from vrtManager.util import OperationError @@ -148,7 +148,7 @@ def _existing(user, key, kind_name, params): Operation.objects.filter(pk=op.pk).update(submission_key=f"expired:{op.pk.hex}") return None if op.kind != kind_name or op.params != params: - raise SubmissionConflict("This form was already submitted for another request; reload the page") + raise SubmissionConflict(gettext("This form was already submitted for another request; reload the page")) return op diff --git a/storages/api/viewsets.py b/storages/api/viewsets.py index 7f5d3dbd..882abbba 100644 --- a/storages/api/viewsets.py +++ b/storages/api/viewsets.py @@ -5,6 +5,7 @@ from appsettings.settings import app_settings from computes.models import Compute from computes.utils import libvirt_compute_lock from django.shortcuts import get_object_or_404 +from django.utils.translation import gettext as _ from drf_spectacular.utils import extend_schema, inline_serializer from libvirt import libvirtError from logs.audit import audit, failed @@ -199,7 +200,7 @@ class VolumeViewSet(viewsets.ViewSet): return Response({"status": "Volume: " + ret + " is created"}) else: failed(request, compute, subject, action, "pool is not active") - return Response({"status": "Pool is not active"}, status=status.HTTP_409_CONFLICT) + return Response({"status": _("Pool is not active")}, status=status.HTTP_409_CONFLICT) else: return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) @@ -220,4 +221,4 @@ class VolumeViewSet(viewsets.ViewSet): return Response({"status": "Volume: " + pk + " is deleted"}) else: failed(request, compute, subject, action, "pool is not active") - return Response({"status": "Pool is not active"}, status=status.HTTP_409_CONFLICT) + return Response({"status": _("Pool is not active")}, status=status.HTTP_409_CONFLICT) diff --git a/storages/jobs.py b/storages/jobs.py index 6430ac9e..3b930f46 100644 --- a/storages/jobs.py +++ b/storages/jobs.py @@ -5,7 +5,7 @@ import logging import os from django.core.exceptions import PermissionDenied -from django.utils.translation import gettext_noop +from django.utils.translation import gettext, gettext_noop from libvirt import libvirtError from vrtManager import util from vrtManager.storage import wvmStorage @@ -115,13 +115,13 @@ class UploadVolume(Kind): raise PermissionDenied name = params["name"] if not name or name in (".", "..") or "\x00" in name: - raise ValueError("Invalid file name") + raise ValueError(gettext("Invalid file name")) if len(name) <= 4 or not is_iso(name): - raise ValueError("Only .iso files can be uploaded") + raise ValueError(gettext("Only .iso files can be uploaded")) if params["size"] <= 0: - raise ValueError("Invalid file size") + raise ValueError(gettext("Invalid file size")) if params["chunks"] < 1: - raise ValueError("Invalid chunk index or total chunks") + raise ValueError(gettext("Invalid chunk index or total chunks")) def plan(self, user, params): compute, stg = _storage(params) diff --git a/vrtManager/util.py b/vrtManager/util.py index acb26262..3d1b5679 100644 --- a/vrtManager/util.py +++ b/vrtManager/util.py @@ -140,7 +140,7 @@ def validate_macaddr(val): form = re.match("^([0-9a-fA-F]{1,2}:){5}[0-9a-fA-F]{1,2}$", val) if form is None: - raise ValueError("MAC address must be of the format AA:BB:CC:DD:EE:FF, was {}".format(val)) + raise ValueError(gettext("MAC address must be of the format AA:BB:CC:DD:EE:FF, was %(mac)s") % {"mac": val}) def vol_dev_type(vol_bus):