diff --git a/README.md b/README.md index e9bfa054..1cee0540 100644 --- a/README.md +++ b/README.md @@ -772,7 +772,7 @@ What an update from an older version changes for operators and users: - The panel backs itself up every day ([Backups](#backups)); in Docker into a new volume, `webvirtcloud-backups`. - In Docker, the TLS client certificates of TLS computes are kept in a new volume, `webvirtcloud-pki` (`/var/www/.pki`), and backed up; put them there once ([Adding a Compute Node](#adding-a-compute-node), step 2). - The SQLite database must be on a local file system (WAL). Outside compose, stop the Docker container with `docker stop -t 95` so the worker can finish running jobs. -- Long operations run as jobs in a worker process (`manage.py run_worker`): creating, cloning and migrating VMs, cloning volumes, taking and reverting snapshots and deleting external snapshots. The page returns at once and the job panel and the Jobs page follow them; a migration can be cancelled. The API's VM create and migrate answer 202 with the job and need a new `submission_key` per request. The create form no longer takes a console password; set it on the VM page. +- Long operations run as jobs in a worker process (`manage.py run_worker`): creating, cloning, migrating and deleting VMs, cloning volumes, taking and reverting snapshots and deleting external snapshots. The page returns at once and the job panel and the Jobs page follow them; a migration can be cancelled. The API's VM create, migrate and delete answer 202 with the job and need a new `submission_key` per request. The create form no longer takes a console password; set it on the VM page. - An ISO upload is tracked as a job that can be cancelled. The browser sends the file, so stay on the page until it ends; an upload left idle for an hour is ended and its partial file deleted. - A clone of a running VM (libvirt 11.10 or later, local file disks, no TPM) locks the VM until the copy ends and can be cancelled; cancelling a clone of a shut-off VM has no effect. A job that ends uncertain keeps its VM locked until a superuser resolves it on its job page. - The job worker runs at most half of its concurrency (Settings, at least one) of one host's jobs at a time, so a host that does not answer cannot take every slot. diff --git a/instances/api/serializers.py b/instances/api/serializers.py index 33dca89e..e5b72a47 100644 --- a/instances/api/serializers.py +++ b/instances/api/serializers.py @@ -70,6 +70,12 @@ class DestroyOptionsSerializer(serializers.Serializer): delete_nvram = serializers.BooleanField(default=False) +class DestroySerializer(DestroyOptionsSerializer): + """The API's delete: a job, which a new submission key per request names.""" + + submission_key = serializers.CharField(write_only=True) + + class CreateInstanceSerializer(serializers.ModelSerializer): # BIOS, UEFI or UEFI Secure Boot, as the host offers; left out: the machine's default firmware = serializers.CharField(required=False) diff --git a/instances/api/viewsets.py b/instances/api/viewsets.py index 2b5a6cd5..1022d0c3 100644 --- a/instances/api/viewsets.py +++ b/instances/api/viewsets.py @@ -7,7 +7,6 @@ from django.shortcuts import get_object_or_404 from django.urls import reverse from instances.jobs import MIGRATE_OPTIONS from instances.models import Flavor, Instance -from instances.views import destroy as instance_destroy from instances.views import ( force_off, get_instance, @@ -28,7 +27,7 @@ from webvirtcloud.serializers import ActionStatusSerializer from .serializers import ( CreateInstanceSerializer, - DestroyOptionsSerializer, + DestroySerializer, FlavorSerializer, InstanceDetailsSerializer, InstanceSerializer, @@ -40,8 +39,8 @@ def instance_action_response(action, request, pk, message): response = action(request, pk) if response.status_code >= 400: return response - # the page's view reports a partial result (a disk that was kept) as a - # message for the page it redirects to: the API answers with them + # the page's view reports a partial result as a message for the page it + # redirects to: the API answers with them return Response({"status": message, "messages": [str(m) for m in get_messages(request)]}) @@ -124,12 +123,19 @@ class InstanceViewSet(viewsets.ViewSet): return Response(serializer.data) - @extend_schema(request=DestroyOptionsSerializer, responses=ActionStatusSerializer) + @extend_schema(request=DestroySerializer, responses={202: OperationSerializer, 400: dict}) def destroy(self, request, pk=None, compute_pk=None): queryset = get_instance(request.user, pk, perm_type="delete") if compute_pk is not None and str(queryset.compute_id) != str(compute_pk): raise Http404() - return instance_action_response(instance_destroy, request, pk, "Instance is destroyed") + serializer = DestroySerializer(data=request.data) + if not serializer.is_valid(): + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + data = dict(serializer.validated_data) + key = data.pop("submission_key") + op = jobs.submit("instance.delete", request.user, {"instance": queryset.pk, **data}, key) + return Response(OperationSerializer(op, context={"request": request}).data, status=status.HTTP_202_ACCEPTED, + headers={"Location": reverse("operation-detail", args=[op.pk])}) @extend_schema(request=None, responses=ActionStatusSerializer) @action(detail=True, methods=["post"]) diff --git a/instances/jobs.py b/instances/jobs.py index 04d61708..43536877 100644 --- a/instances/jobs.py +++ b/instances/jobs.py @@ -14,7 +14,9 @@ from django.template.loader import render_to_string from django.utils.translation import gettext_noop from django.utils.translation import gettext_noop as _ from libvirt import (VIR_CONNECT_LIST_STORAGE_POOLS_ACTIVE, VIR_DOMAIN_JOB_NONE, VIR_DOMAIN_SNAPSHOT_LIST_EXTERNAL, - VIR_DOMAIN_SHUTOFF, VIR_ERR_NO_DOMAIN, VIR_ERR_NO_STORAGE_VOL, libvirtError) + VIR_DOMAIN_SHUTOFF, VIR_DOMAIN_UNDEFINE_KEEP_NVRAM, VIR_DOMAIN_UNDEFINE_MANAGED_SAVE, + VIR_DOMAIN_UNDEFINE_NVRAM, VIR_DOMAIN_UNDEFINE_SNAPSHOTS_METADATA, VIR_ERR_NO_DOMAIN, + VIR_ERR_NO_STORAGE_VOL, libvirtError) from lxml import etree from vrtManager import util from vrtManager.connection import wvmConnect @@ -27,6 +29,7 @@ from appsettings.settings import app_settings from computes.models import Compute from instances import utils from instances.models import Instance +from logs.audit import write from operations import claims from operations.utils import safe_text, say from operations.jobs import Cancelled, Kind, Outcome, Plan, register @@ -1215,3 +1218,108 @@ class MoveDisk(Kind): def log_entry(self, op, state): return _log(op, state, f"move disk {op.params['dev']} to pool {op.params['pool']}") + + +DISK_KEPT_BELOW = _("Disk %(path)s was kept: a disk file that stays needs it") +DISK_NOT_DELETED = _("The VM is removed, but disk %(path)s was not deleted: %(err)s") +DISK_SHARED = _("Disk %(path)s is used by another VM and was kept") + + +def _deletable(user, pk): + """The delete view's rule: the VM's delete right.""" + from instances.views import get_instance + + if user is None or not user.is_active: + raise PermissionDenied + return get_instance(user, pk, perm_type="delete") + + +def _removed(op, recovering=False): + """What follows the undefine: the VM's record, then the disk files the job + chose before it stopped the VM; the job's result. Each disk has its own + Logs row; a file that stays keeps the files below it, which it needs. In + recovery a file already gone was deleted before the worker stopped.""" + Instance.objects.filter(compute_id=op.intent["compute"], uuid=op.intent["uuid"]).delete() + compute = Compute.objects.get(pk=op.intent["compute"]) + conn = wvmConnect(compute.hostname, compute.login, compute.password, compute.type) + notes, kept = [], set() + for path in op.intent["to_delete"]: + if path in kept: + notes += say(DISK_KEPT_BELOW, path=path) + continue + try: + conn.get_volume_by_path(path).delete(0) + except util.ConnectionFailed: + raise # unreachable: recovery waits and retries + except libvirtError as err: + if recovering and err.get_error_code() == VIR_ERR_NO_STORAGE_VOL: + continue + notes += say(DISK_NOT_DELETED, path=path, err=err) + write(op.username, compute.name, op.intent["name"], f"VM {op.intent['name']}: delete volume {path}: " + f"{safe_text(err)}", False, uuid=op.intent["uuid"]) + try: + while (path := conn.backing_path(path)) and path not in kept: + kept.add(path) + except libvirtError: + kept.update(op.intent["to_delete"]) # the files it needs are unknown: none of the rest goes + else: + write(op.username, compute.name, op.intent["name"], f"VM {op.intent['name']}: delete volume {path}", + True, uuid=op.intent["uuid"]) + for path in op.intent["shared"]: + notes += say(DISK_SHARED, path=path) + return {"open": reverse("instances:index"), **({"warning": notes} if notes else {})} + + +@register +class DeleteInstance(Kind): + """Undefine the VM, then remove its record and the disk files chosen + before it was stopped. Recovery finishes a delete libvirt has done.""" + + name = "instance.delete" + label = gettext_noop("Delete VM") + + def normalize(self, raw): + return {"instance": int(raw["instance"]), "delete_disk": bool(raw.get("delete_disk")), + "delete_nvram": bool(raw.get("delete_nvram"))} + + def validate(self, user, params): + _deletable(user, params["instance"]) + + def plan(self, user, params): + instance = _instance(params["instance"]) + # the whole compute: a file to delete must not be attached by another VM meanwhile + return Plan(claims={claims.compute_key(instance.compute_id): Claim.EXCLUSIVE, + claims.instance_key(instance.uuid): Claim.EXCLUSIVE}, + resource=claims.instance_key(instance.uuid), label=f"{instance.compute.name}/{instance.name}", + intent={"compute": instance.compute_id, "uuid": instance.uuid, "name": instance.name}) + + def precheck(self, op, ctx): + if _deletable(op.user, op.params["instance"]).compute_id != op.intent["compute"]: + raise util.OperationError(MOVED) + + def run(self, op, ctx): + instance = _instance(op.params["instance"]) + proxy = _proxy(instance) + to_delete, shared = [], [] + if op.params["delete_disk"]: + # known before the VM is stopped: a host that cannot be checked stops nothing + to_delete, shared = proxy.split_disk_paths_by_use( + lambda own: utils.paths_used_on_other_computes(instance.compute, own)) + ctx.record_intent(to_delete=to_delete, shared=shared) + ctx.set_phase(gettext_noop("Deleting the VM")) + # paused and other active states stop too, or the VM runs on as a transient domain + if proxy.instance.isActive(): + proxy.force_shutdown() + # snapshot metadata goes with the domain; internal snapshot data stays in kept disk images + flags = VIR_DOMAIN_UNDEFINE_MANAGED_SAVE | VIR_DOMAIN_UNDEFINE_SNAPSHOTS_METADATA + flags |= VIR_DOMAIN_UNDEFINE_NVRAM if op.params["delete_nvram"] else VIR_DOMAIN_UNDEFINE_KEEP_NVRAM + proxy.delete(flags) + return _removed(op) + + def reconcile(self, op): + if _domain(op.intent["compute"], op.intent["uuid"]) is not None: + return Outcome(Operation.FAILED, error=say(_("The VM was not deleted; it may have been shut off"))) + return Outcome(Operation.SUCCEEDED, result=_removed(op, recovering=True)) + + def log_entry(self, op, state): + return _log(op, state, "delete") diff --git a/instances/templates/instances/destroy_instance_form.html b/instances/templates/instances/destroy_instance_form.html index e495faef..252ae1ad 100644 --- a/instances/templates/instances/destroy_instance_form.html +++ b/instances/templates/instances/destroy_instance_form.html @@ -1,5 +1,6 @@ {% extends 'base.html' %} {% load i18n %} +{% load operations %} {% block title %}{% blocktrans with name=instance.name %}Destroy {{ name }}{% endblocktrans %}{% endblock title %} {% block page_heading %} @@ -8,7 +9,7 @@ {% block content %}

{% trans "The VM's definition, saved state and snapshot records are removed. A running VM is forced off first; what the guest has not written to disk is lost." %}

-
{% csrf_token %} +{% csrf_token %}{% submission_key %}
{% trans "What should happen to the files?" %}
diff --git a/instances/tests/test_destroy.py b/instances/tests/test_destroy.py index 80f4f3df..54adb4aa 100644 --- a/instances/tests/test_destroy.py +++ b/instances/tests/test_destroy.py @@ -1,36 +1,67 @@ from contextlib import contextmanager +from types import SimpleNamespace from unittest.mock import MagicMock, patch from computes.models import Compute from django.contrib.auth import get_user_model from django.contrib.messages import get_messages -from django.test import TestCase +from django.test import TestCase, TransactionTestCase from django.urls import reverse +from instances.jobs import DeleteInstance from instances.models import Instance from instances.utils import paths_used_on_other_computes -from libvirt import VIR_DOMAIN_UNDEFINE_KEEP_NVRAM, VIR_DOMAIN_UNDEFINE_NVRAM, libvirtError -from operations.utils import public_error_text +from libvirt import VIR_DOMAIN_UNDEFINE_KEEP_NVRAM, VIR_DOMAIN_UNDEFINE_NVRAM, VIR_ERR_NO_STORAGE_VOL, libvirtError +from logs.models import Logs +from operations.jobs import Context +from operations.models import Claim, Operation, acquire +from operations.utils import public_error_text, words from vrtManager.util import ConnectionFailed, OperationError +UUID = "11111111-2222-3333-4444-555555555555" -class DestroyOrderTestCase(TestCase): + +class DestroyOrderTestCase(TransactionTestCase): def setUp(self): - admin = get_user_model().objects.create_superuser("destroy_admin", "d@example.com", "pw") - self.client.force_login(admin) + self.admin = get_user_model().objects.create_superuser("destroy_admin", "d@example.com", "pw") + self.client.force_login(self.admin) self.client.raise_request_exception = False compute = Compute.objects.create(name="destroy-compute", hostname="127.0.0.1", login="", password="", type=4) - self.instance = Instance.objects.create( - compute=compute, name="destroy-vm", uuid="11111111-2222-3333-4444-555555555555" - ) + self.instance = Instance.objects.create(compute=compute, name="destroy-vm", uuid=UUID) + self.proxy = MagicMock() + self.proxy.instance.isActive.return_value = False + self.proxy.backing_path.return_value = None + self.proxy.split_disk_paths_by_use.return_value = (["/pool/own.qcow2"], ["/pool/shared.qcow2"]) + # the job's VM and its compute's connection are the same host here + for target in ("instances.jobs.wvmInstance", "instances.jobs.wvmConnect", "instances.models.wvmInstance"): + patch(target, return_value=self.proxy).start() + self.addCleanup(patch.stopall) - def destroy(self, undefine_error=None, data=None): - with patch("instances.models.wvmInstance") as wvm: - proxy = wvm.return_value - proxy.instance.isActive.return_value = False - proxy.split_disk_paths_by_use.return_value = (["/pool/own.qcow2"], ["/pool/shared.qcow2"]) - proxy.delete.side_effect = undefine_error - self.client.post(reverse("instances:destroy", args=[self.instance.id]), data or {"delete_disk": "1"}) - return proxy + def queue(self, **data): + self.client.post(reverse("instances:destroy", args=[self.instance.id]), + {"submission_key": "key-destroy01", **data}) + return Operation.objects.get() + + def run_job(self, op): + """The job as the worker runs it; its result.""" + acquire("wvc-test:1:a", "wvc-test") + Operation.objects.filter(pk=op.pk).update(state=Operation.RUNNING, worker="wvc-test:1:a") + op.refresh_from_db() + ctx = Context(op, SimpleNamespace(identity="wvc-test:1:a")) + DeleteInstance().precheck(op, ctx) + return DeleteInstance().run(op, ctx) + + def test_the_page_queues_the_delete_and_changes_nothing(self): + response = self.client.post(reverse("instances:destroy", args=[self.instance.id]), + {"submission_key": "key-destroy01", "delete_disk": "true"}) + self.assertRedirects(response, reverse("instances:instance", args=[self.instance.id]), + fetch_redirect_response=False) + op = Operation.objects.get() + self.assertEqual((op.kind, op.params), ("instance.delete", { + "instance": self.instance.pk, "delete_disk": True, "delete_nvram": False})) + # the whole compute: a file to delete must not be attached meanwhile + self.assertEqual(set(Claim.objects.values_list("resource", "mode")), { + (f"compute:{self.instance.compute_id}", "exclusive"), (f"instance:{UUID}", "exclusive")}) + self.assertEqual(self.proxy.mock_calls, []) def test_the_nvram_goes_only_when_asked(self): for data, wanted, unwanted in ( @@ -38,47 +69,35 @@ class DestroyOrderTestCase(TestCase): ({}, VIR_DOMAIN_UNDEFINE_KEEP_NVRAM, VIR_DOMAIN_UNDEFINE_NVRAM), ): with self.subTest(data=data): - Instance.objects.get_or_create( - compute=self.instance.compute, name="destroy-vm", uuid="11111111-2222-3333-4444-555555555555" - ) - self.instance = Instance.objects.get(uuid="11111111-2222-3333-4444-555555555555") - flags = self.destroy(data=data).delete.call_args[0][0] + self.instance = Instance.objects.get_or_create(compute=self.instance.compute, name="destroy-vm", + uuid=UUID)[0] + Operation.objects.all().delete() + self.run_job(self.queue(**data)) + flags = self.proxy.delete.call_args[0][0] self.assertTrue(flags & wanted) self.assertFalse(flags & unwanted) - def test_the_api_takes_its_options_as_booleans(self): - url = f"/api/v1/computes/{self.instance.compute_id}/instances/{self.instance.id}/" - for body, content_type, deleted in ( - ({"delete_disk": True}, "application/json", True), - ("delete_disk=false", "application/x-www-form-urlencoded", False), - ): - with self.subTest(body=body): - Instance.objects.get_or_create( - compute=self.instance.compute, name="destroy-vm", uuid="11111111-2222-3333-4444-555555555555" - ) - self.instance = Instance.objects.get(uuid="11111111-2222-3333-4444-555555555555") - with patch("instances.models.wvmInstance") as wvm: - proxy = wvm.return_value - proxy.instance.isActive.return_value = False - proxy.split_disk_paths_by_use.return_value = (["/pool/own.qcow2"], []) - url = f"/api/v1/computes/{self.instance.compute_id}/instances/{self.instance.id}/" - response = self.client.delete(url, body, content_type=content_type) - self.assertEqual(response.status_code, 200) - self.assertEqual(proxy.get_volume_by_path.called, deleted) + def test_the_files_are_checked_before_the_vm_is_stopped_and_deleted_after_the_undefine(self): + self.proxy.instance.isActive.return_value = True + op = self.queue(delete_disk="true") + result = self.run_job(op) + calls = [name for name, *_ in self.proxy.mock_calls + if name in ("split_disk_paths_by_use", "force_shutdown", "delete", "get_volume_by_path")] + self.assertEqual(calls, ["split_disk_paths_by_use", "force_shutdown", "delete", "get_volume_by_path"]) + self.proxy.get_volume_by_path.assert_called_once_with("/pool/own.qcow2") + self.assertFalse(Instance.objects.filter(pk=self.instance.pk).exists()) + self.assertEqual(Operation.objects.get().intent["to_delete"], ["/pool/own.qcow2"]) # recorded first + self.assertEqual(result["open"], reverse("instances:index")) + self.assertEqual(words(result["warning"]), "Disk /pool/shared.qcow2 is used by another VM and was kept") + self.assertEqual(list(Logs.objects.exclude(message="Login").values_list("message", "success")), + [("VM destroy-vm: delete volume /pool/own.qcow2", True)]) - def test_the_api_tells_which_disks_were_not_deleted(self): - with patch("instances.models.wvmInstance") as wvm: - proxy = wvm.return_value - proxy.instance.isActive.return_value = False - proxy.split_disk_paths_by_use.return_value = (["/pool/own.qcow2"], ["/pool/shared.qcow2"]) - proxy.backing_path.return_value = None - proxy.get_volume_by_path.return_value.delete.side_effect = libvirtError("Permission denied") - url = f"/api/v1/computes/{self.instance.compute_id}/instances/{self.instance.id}/" - response = self.client.delete(url, {"delete_disk": True}, content_type="application/json") - self.assertEqual(response.status_code, 200) - self.assertEqual(response.json(), {"status": "Instance is destroyed", "messages": [ - "The VM is removed, but disk /pool/own.qcow2 was not deleted: Permission denied", - "Disk /pool/shared.qcow2 is used by another VM and was kept"]}) + def test_a_failed_undefine_deletes_no_disk(self): + self.proxy.delete.side_effect = libvirtError("refused") + with self.assertRaises(libvirtError): + self.run_job(self.queue(delete_disk="true")) + self.proxy.get_volume_by_path.assert_not_called() + self.assertTrue(Instance.objects.filter(pk=self.instance.pk).exists()) def test_a_disk_file_that_stays_keeps_the_files_it_needs(self): # the overlay is not deleted: the file below it would leave it broken @@ -90,64 +109,85 @@ class DestroyOrderTestCase(TestCase): vol.delete.side_effect = libvirtError("Permission denied") return vol - with patch("instances.models.wvmInstance") as wvm: - proxy = wvm.return_value - proxy.instance.isActive.return_value = False - proxy.split_disk_paths_by_use.return_value = (["/pool/vm.snap1", "/pool/vm.qcow2", "/pool/vm-data.qcow2"], []) - proxy.get_volume_by_path.side_effect = volume - proxy.backing_path.side_effect = {"/pool/vm.snap1": "/pool/vm.qcow2"}.get - response = self.client.post(reverse("instances:destroy", args=[self.instance.id]), {"delete_disk": "true"}) + self.proxy.split_disk_paths_by_use.return_value = ( + ["/pool/vm.snap1", "/pool/vm.qcow2", "/pool/vm-data.qcow2"], []) + self.proxy.get_volume_by_path.side_effect = volume + self.proxy.backing_path.side_effect = {"/pool/vm.snap1": "/pool/vm.qcow2"}.get + result = self.run_job(self.queue(delete_disk="true")) self.assertNotIn("/pool/vm.qcow2", volumes) volumes["/pool/vm-data.qcow2"].delete.assert_called_once_with(0) - self.assertEqual([str(m) for m in get_messages(response.wsgi_request)][-1], + # each disk has its own row, and the job's end the VM's (log_entry) + self.assertEqual(list(Logs.objects.exclude(message="Login").values_list("message", "success", "instance_uuid")), [ + ("VM destroy-vm: delete volume /pool/vm.snap1: Permission denied", False, UUID), + ("VM destroy-vm: delete volume /pool/vm-data.qcow2", True, UUID)]) + self.assertEqual(DeleteInstance().log_entry(Operation.objects.get(), Operation.SUCCEEDED), + ("destroy-compute", "destroy-vm", UUID, "VM destroy-vm: delete")) + self.assertEqual(words(result["warning"]), + "The VM is removed, but disk /pool/vm.snap1 was not deleted: Permission denied. " "Disk /pool/vm.qcow2 was kept: a disk file that stays needs it") def test_when_the_files_a_kept_one_needs_are_unknown_none_of_the_rest_goes(self): - with patch("instances.models.wvmInstance") as wvm: - proxy = wvm.return_value - proxy.instance.isActive.return_value = False - proxy.split_disk_paths_by_use.return_value = (["/pool/a.qcow2", "/pool/b.qcow2"], []) - proxy.get_volume_by_path.return_value.delete.side_effect = libvirtError("Permission denied") - proxy.backing_path.side_effect = libvirtError("pool unreachable") - response = self.client.post(reverse("instances:destroy", args=[self.instance.id]), {"delete_disk": "true"}) - self.assertEqual(response.status_code, 302) - proxy.get_volume_by_path.assert_called_once_with("/pool/a.qcow2") - - def test_the_files_are_checked_before_the_vm_is_stopped(self): - with patch("instances.models.wvmInstance") as wvm: - proxy = wvm.return_value - proxy.instance.isActive.return_value = True - proxy.split_disk_paths_by_use.return_value = (["/pool/own.qcow2"], []) - self.client.post(reverse("instances:destroy", args=[self.instance.id]), {"delete_disk": "true"}) - calls = [name for name, *_ in proxy.mock_calls if name in ("split_disk_paths_by_use", "force_shutdown", "delete")] - self.assertEqual(calls, ["split_disk_paths_by_use", "force_shutdown", "delete"]) + self.proxy.split_disk_paths_by_use.return_value = (["/pool/a.qcow2", "/pool/b.qcow2"], []) + self.proxy.get_volume_by_path.return_value.delete.side_effect = libvirtError("Permission denied") + self.proxy.backing_path.side_effect = libvirtError("pool unreachable") + self.run_job(self.queue(delete_disk="true")) + self.proxy.get_volume_by_path.assert_called_once_with("/pool/a.qcow2") def test_a_host_that_cannot_be_checked_stops_nothing(self): Compute.objects.create(name="other-host", hostname="192.0.2.50", login="u", password="p", type=1) + def connect(host, *args): if host == "192.0.2.50": raise libvirtError("unreachable") return MagicMock() - with patch("instances.models.wvmInstance") as wvm, patch("instances.utils.wvmConnect", side_effect=connect): - proxy = wvm.return_value - proxy.instance.isActive.return_value = True - proxy.split_disk_paths_by_use.side_effect = lambda elsewhere: elsewhere(["/pool/own.qcow2"]) - response = self.client.post(reverse("instances:destroy", args=[self.instance.id]), {"delete_disk": "true"}) - proxy.force_shutdown.assert_not_called() - proxy.delete.assert_not_called() + self.proxy.instance.isActive.return_value = True + self.proxy.split_disk_paths_by_use.side_effect = lambda elsewhere: elsewhere(["/pool/own.qcow2"]) + op = self.queue(delete_disk="true") + with patch("instances.utils.wvmConnect", side_effect=connect), \ + self.assertRaisesMessage(ConnectionFailed, "The disk files cannot be checked on other-host"): + self.run_job(op) + self.proxy.force_shutdown.assert_not_called() + self.proxy.delete.assert_not_called() + self.assertFalse(Operation.objects.get().effects_started) # the job fails without recovery self.assertTrue(Instance.objects.filter(pk=self.instance.pk).exists()) - self.assertIn("The disk files cannot be checked on other-host", str(list(get_messages(response.wsgi_request))[-1])) - def test_disks_are_deleted_after_the_undefine(self): - proxy = self.destroy() - proxy.get_volume_by_path.assert_called_once_with("/pool/own.qcow2") + def test_recovery_finishes_a_delete_libvirt_has_done(self): + op = self.queue(delete_disk="true") + Operation.objects.filter(pk=op.pk).update(intent={**op.intent, "to_delete": ["/pool/gone.qcow2", "/pool/own.qcow2"], + "shared": []}) + op.refresh_from_db() + gone = libvirtError("no volume") + gone.err = (VIR_ERR_NO_STORAGE_VOL, 0, "no volume") + self.proxy.get_volume_by_path.side_effect = lambda path: (_ for _ in ()).throw(gone) \ + if path == "/pool/gone.qcow2" else MagicMock() + with patch("instances.jobs._domain", return_value=None): + outcome = DeleteInstance().reconcile(op) + self.assertEqual((outcome.state, outcome.result), (Operation.SUCCEEDED, {"open": reverse("instances:index")})) self.assertFalse(Instance.objects.filter(pk=self.instance.pk).exists()) + self.assertEqual(list(Logs.objects.exclude(message="Login").values_list("message", flat=True)), + ["VM destroy-vm: delete volume /pool/own.qcow2"]) - def test_failed_undefine_deletes_no_disk(self): - proxy = self.destroy(undefine_error=libvirtError("refused")) - proxy.get_volume_by_path.assert_not_called() + def test_recovery_of_a_vm_still_on_its_host_fails(self): + op = self.queue(delete_disk="true") + with patch("instances.jobs._domain", return_value=MagicMock()): + self.assertEqual(DeleteInstance().reconcile(op).state, Operation.FAILED) self.assertTrue(Instance.objects.filter(pk=self.instance.pk).exists()) + self.proxy.get_volume_by_path.assert_not_called() + + def test_the_api_queues_the_delete_with_its_options_as_booleans(self): + url = f"/api/v1/computes/{self.instance.compute_id}/instances/{self.instance.id}/" + for n, (body, content_type, deleted) in enumerate(( + ({"delete_disk": True, "submission_key": "key-api00001"}, "application/json", True), + ("delete_disk=false&submission_key=key-api00002", "application/x-www-form-urlencoded", False), + )): + with self.subTest(body=body): + Operation.objects.all().delete() + response = self.client.delete(url, body, content_type=content_type) + self.assertEqual(response.status_code, 202) + self.assertEqual(Operation.objects.get().params["delete_disk"], deleted) + self.assertEqual(self.client.delete(url, {"delete_disk": True}, content_type="application/json").status_code, + 400) # no submission key def test_destroy_and_delete_vol_hold_the_compute_exclusively(self): calls = [] @@ -158,10 +198,8 @@ class DestroyOrderTestCase(TestCase): yield with patch("instances.views.libvirt_compute_lock", record): - with patch("instances.models.wvmInstance"): - self.client.post(reverse("instances:delete_vol", args=[self.instance.id]), {"dev": "vda"}) - self.destroy() - self.assertEqual(calls, [False, False]) + self.client.post(reverse("instances:delete_vol", args=[self.instance.id]), {"dev": "vda"}) + self.assertEqual(calls, [False]) def test_a_volume_the_vms_snapshots_need_is_kept_after_the_detach(self): disk = {"dev": "vdb", "path": "/pool/data.qcow2", "storage": "default", "image": "data.qcow2"} diff --git a/instances/tests/test_live_dataloss.py b/instances/tests/test_live_dataloss.py index 32c43757..088a2bbf 100644 --- a/instances/tests/test_live_dataloss.py +++ b/instances/tests/test_live_dataloss.py @@ -8,6 +8,8 @@ TEST_LIBVIRT_HOST set (see instances/tests/livetest.py). import os import unittest +from types import SimpleNamespace +from uuid import uuid4 import libvirt from computes.models import Compute @@ -18,7 +20,10 @@ from django.urls import reverse from vrtManager.connection import connection_manager from . import livetest +from instances.jobs import DeleteInstance from instances.models import Instance +from operations.jobs import Context +from operations.models import Operation, acquire from instances.utils import refr P = livetest.PREFIX @@ -75,6 +80,19 @@ class LiveDataLossTestCase(TestCase): HTTP_REFERER=reverse("instances:instance", args=[instance.id]), ) + def destroy(self, instance, data=None): + """The delete page's request, then its job as the worker runs it.""" + response = self.post("destroy", instance, {"submission_key": f"key-{uuid4().hex[:12]}", **(data or {})}) + op = Operation.objects.get(kind="instance.delete", state=Operation.QUEUED) + acquire("live-test:1:a", "live-test") + Operation.objects.filter(pk=op.pk).update(state=Operation.RUNNING, worker="live-test:1:a") + op.refresh_from_db() + ctx = Context(op, SimpleNamespace(identity="live-test:1:a")) + DeleteInstance().precheck(op, ctx) + DeleteInstance().run(op, ctx) + Operation.objects.filter(pk=op.pk).update(state=Operation.SUCCEEDED) + return response + def disk_sources(self, dom, flags=0): from lxml import etree @@ -156,7 +174,7 @@ class LiveDataLossTestCase(TestCase): _, inst = self.vm("r14-a", [shared]) self.vm("r14-b", [shared]) - response = self.post("destroy", inst, {"delete_disk": "1"}) + response = self.destroy(inst, {"delete_disk": "1"}) self.assertEqual(response.status_code, 302) self.assertTrue(livetest.volume_exists(self.conn, shared), "destroy deleted another VM's disk") @@ -175,7 +193,7 @@ class LiveDataLossTestCase(TestCase): if running: dom_b.create() - response = self.post("destroy", inst, {"delete_disk": "1"}) + response = self.destroy(inst, {"delete_disk": "1"}) self.assertEqual(response.status_code, 302) self.assertTrue(livetest.volume_exists(self.conn, base), "deleted the backing file of another VM's disk") @@ -192,7 +210,7 @@ class LiveDataLossTestCase(TestCase): dom.create() dom.managedSave(0) - self.post("destroy", inst, {"delete_disk": "1"}) + self.destroy(inst, {"delete_disk": "1"}) uuid = dom.UUIDString() self.assertFalse(self.domain_exists(uuid), "VM is still defined") @@ -204,7 +222,7 @@ class LiveDataLossTestCase(TestCase): dom.create() dom.suspend() - self.post("destroy", inst) + self.destroy(inst) self.assertFalse(self.domain_exists(dom.UUIDString()), "paused VM is still running") @@ -231,7 +249,7 @@ class LiveDataLossTestCase(TestCase): dom.destroy() self.assertIn(f"{P}r14-nvram_VARS.fd", self.nvram_files()) - self.post("destroy", inst, data) + self.destroy(inst, data) self.assertFalse(self.domain_exists(dom.UUIDString()), "VM is still defined") return f"{P}r14-nvram_VARS.fd" in self.nvram_files() @@ -267,7 +285,7 @@ class LiveDataLossTestCase(TestCase): dom, inst = self.vm("r14-chain", [overlay]) dom.snapshotCreateXML("snap", 0) - response = self.post("destroy", inst, {"delete_disk": "1"}) + response = self.destroy(inst, {"delete_disk": "1"}) self.assertEqual(response.status_code, 302) self.assertFalse(self.domain_exists(dom.UUIDString()), "VM is still defined") @@ -295,7 +313,7 @@ class LiveDataLossTestCase(TestCase): made = {name for name in pool.listVolumes() if name.startswith(P + "r14-ext.")} - {P + "r14-ext.qcow2"} self.assertEqual(len(made), 2, made) # one overlay per snapshot - response = self.post("destroy", inst, {"delete_disk": "1"}) + response = self.destroy(inst, {"delete_disk": "1"}) self.assertEqual(response.status_code, 302) self.assertFalse(self.domain_exists(dom.UUIDString()), "VM is still defined") @@ -313,7 +331,7 @@ class LiveDataLossTestCase(TestCase): inst.proxy.create_external_snapshot("b") inst.proxy.revert_external_snapshot("a") # b's overlay is off the disk's chain now - response = self.post("destroy", inst, {"delete_disk": "1"}) + response = self.destroy(inst, {"delete_disk": "1"}) self.assertEqual(response.status_code, 302) pool = self.conn.storagePoolLookupByName(livetest.POOL) diff --git a/instances/tests/test_live_jobs.py b/instances/tests/test_live_jobs.py index ae143da3..138a9485 100644 --- a/instances/tests/test_live_jobs.py +++ b/instances/tests/test_live_jobs.py @@ -20,7 +20,7 @@ from vrtManager.instance import wvmInstance from appsettings.settings import get_settings from computes.models import Compute -from instances.jobs import DeleteExternalSnapshot +from instances.jobs import DeleteExternalSnapshot, DeleteInstance from instances.models import Instance from operations import jobs from operations.models import Operation @@ -111,6 +111,23 @@ class LiveJobsTestCase(TransactionTestCase): self.assertEqual(op.state, Operation.SUCCEEDED, op.error) return op + def test_the_worker_deletes_a_running_vm_with_its_disk_and_recovery_agrees(self): + disk = livetest.create_volume(self.conn, P + "job-delete") + dom = livetest.define_vm(self.conn, P + "job-delete", [disk]) + dom.create() + instance = Instance.objects.create(compute=self.compute, name=dom.name(), uuid=dom.UUIDString()) + op = self.run_job(jobs.submit("instance.delete", self.admin, {"instance": instance.pk, "delete_disk": True}, + "key-delete01")) + self.assertEqual(op.result, {"open": reverse("instances:index")}) + self.assertFalse(Instance.objects.filter(pk=instance.pk).exists()) + self.assertFalse(livetest.volume_exists(self.conn, disk)) + with self.assertRaises(libvirt.libvirtError): + self.conn.lookupByUUIDString(op.intent["uuid"]) + # a worker that stopped after the undefine: recovery finds the work done + Operation.objects.filter(pk=op.pk).update(state=Operation.RUNNING) + op.refresh_from_db() + self.assertEqual(DeleteInstance().reconcile(op).state, Operation.SUCCEEDED) + def test_snapshot_create_and_revert_run_as_jobs(self): running, _vm = self.vm("smoke-ext", "running") stopped, _vm = self.vm("smoke-int") diff --git a/instances/tests/test_moved_while_waiting.py b/instances/tests/test_moved_while_waiting.py index 64c70fb0..a54fdb57 100644 --- a/instances/tests/test_moved_while_waiting.py +++ b/instances/tests/test_moved_while_waiting.py @@ -12,7 +12,10 @@ from django.test import TestCase from django.urls import reverse from computes.models import Compute +from instances.jobs import DeleteInstance from instances.models import Instance +from operations.models import Operation +from vrtManager.util import OperationError class MovedWhileWaitingTestCase(TestCase): @@ -54,18 +57,11 @@ class MovedWhileWaitingTestCase(TestCase): self.assertTrue(touched) def test_destroy_of_a_vm_moved_while_it_waited_is_not_run(self): - # destroy holds the whole compute; the migration moved the VM before - # releasing it - @contextmanager - def compute_lock_after_a_migration(compute, *, shared=False): - Instance.objects.filter(pk=self.vm.pk).update(compute=self.target) - yield - - with patch("instances.views.libvirt_compute_lock", compute_lock_after_a_migration), patch( - "instances.models.wvmInstance" - ) as wvm, patch.object(Compute, "status", new_callable=PropertyMock, return_value=True): - response = self.client.post(reverse("instances:destroy", args=[self.vm.id]), {"delete_disk": "on"}) - self.assertFalse(wvm.return_value.delete.called) - self.assertTrue(Instance.objects.filter(pk=self.vm.pk).exists()) - messages = [str(m) for m in response.wsgi_request._messages] - self.assertIn("The VM was moved meanwhile; try again.", messages) + # the delete job holds the whole compute; a migration moved the VM before it ran + with patch("instances.models.wvmInstance"): + self.client.post(reverse("instances:destroy", args=[self.vm.id]), + {"delete_disk": "on", "submission_key": "key-moved001"}) + op = Operation.objects.get(kind="instance.delete") + Instance.objects.filter(pk=self.vm.pk).update(compute=self.target) + with self.assertRaisesMessage(OperationError, "The VM moved to another compute"): + DeleteInstance().precheck(op, None) diff --git a/instances/tests/test_security_mutations.py b/instances/tests/test_security_mutations.py index 8b8ee757..d88f9d9a 100644 --- a/instances/tests/test_security_mutations.py +++ b/instances/tests/test_security_mutations.py @@ -9,6 +9,7 @@ from django.urls import reverse from accounts.models import UserInstance from computes.models import Compute from instances.models import Instance +from operations.models import Operation from operations import claims from instances.views import get_instance @@ -257,11 +258,11 @@ class InstanceSecurityMutationsTestCase(TestCase): # 3. Superuser can destroy VM self.client.force_login(self.superuser) - with patch("instances.models.wvmInstance") as mock_wvm_cls: - mock_wvm_cls.return_value.get_status.return_value = 5 # shutoff - res = self.client.post(reverse("instances:destroy", args=[self.instance.id]), {}) + with patch("instances.models.wvmInstance"): + res = self.client.post(reverse("instances:destroy", args=[self.instance.id]), + {"submission_key": "key-destroy01"}) self.assertEqual(res.status_code, 302) - self.assertFalse(Instance.objects.filter(id=self.instance.id).exists()) + self.assertTrue(Operation.objects.filter(kind="instance.delete").exists()) # queued def test_migration_deduplicate_instances_logic(self): import importlib diff --git a/instances/tests/test_template_rules.py b/instances/tests/test_template_rules.py index 8b589a0d..fb352086 100644 --- a/instances/tests/test_template_rules.py +++ b/instances/tests/test_template_rules.py @@ -12,6 +12,7 @@ from rest_framework.test import APIClient from accounts.models import UserInstance from computes.models import Compute from instances.models import Instance +from operations.models import Operation class TemplateRulesTestCase(TestCase): @@ -46,10 +47,9 @@ class TemplateRulesTestCase(TestCase): self.assertFalse(wvm.return_value.delete.called) self.client.force_login(self.staff) - with patch("instances.models.wvmInstance") as wvm: - wvm.return_value.instance.isActive.return_value = False - self.client.post(reverse("instances:destroy", args=[self.template.id])) - self.assertTrue(wvm.return_value.delete.called) + with patch("instances.models.wvmInstance"): + self.client.post(reverse("instances:destroy", args=[self.template.id]), {"submission_key": "key-tpldel01"}) + self.assertTrue(Operation.objects.filter(kind="instance.delete").exists()) # queued def test_the_destroy_confirmation_page_offers_the_form_to_who_may_delete(self): url = reverse("instances:destroy", args=[self.template.id]) diff --git a/instances/tests/test_vm_audit_rows.py b/instances/tests/test_vm_audit_rows.py index 6491d147..173ffd9b 100644 --- a/instances/tests/test_vm_audit_rows.py +++ b/instances/tests/test_vm_audit_rows.py @@ -143,17 +143,6 @@ class VMRowTests(TestCase): self.assertEqual(self.rows(), [("VM vm: set console password", True)]) self.proxy.set_console_listener_addr.assert_not_called() - def test_a_deleted_vm_and_each_of_its_disks_have_their_own_row(self): - self.proxy.split_disk_paths_by_use.return_value = (["/pool/a.qcow2", "/pool/b.qcow2"], []) - volume = self.proxy.get_volume_by_path.return_value - volume.delete.side_effect = [libvirtError("Permission denied"), None] - self.client.post(reverse("instances:destroy", args=[self.vm.pk]), {"delete_disk": "1"}) - self.assertFalse(Instance.objects.filter(pk=self.vm.pk).exists()) - self.assertEqual(self.rows(), [("VM vm: delete", True), - ("VM vm: delete volume /pool/a.qcow2: Permission denied", False), - ("VM vm: delete volume /pool/b.qcow2", True)]) - self.assertEqual(set(Logs.objects.values_list("instance_uuid", flat=True)), {UUID}) - def test_a_disk_delete_logs_the_detach_and_the_volume(self): disk = {"dev": "vdb", "path": "/pool/data.qcow2", "storage": "default", "image": "data.qcow2"} self.proxy.get_disk_devices.return_value = [disk] diff --git a/instances/views.py b/instances/views.py index 3e0d8af5..9a6b1bc7 100755 --- a/instances/views.py +++ b/instances/views.py @@ -42,10 +42,6 @@ from django.views.decorators.debug import sensitive_post_parameters from libvirt import (VIR_DOMAIN_PAUSED, VIR_DOMAIN_RUNNING, VIR_DOMAIN_SHUTOFF, - VIR_DOMAIN_UNDEFINE_KEEP_NVRAM, - VIR_DOMAIN_UNDEFINE_MANAGED_SAVE, - VIR_DOMAIN_UNDEFINE_SNAPSHOTS_METADATA, - VIR_DOMAIN_UNDEFINE_NVRAM, VIR_DOMAIN_XML_INACTIVE, VIR_ERR_NO_DOMAIN, VIR_ERR_NO_SUPPORT, @@ -861,62 +857,21 @@ def force_off(request, pk): ) -@serialize_compute_mutation def destroy(request, pk): if request.method in ["POST", "DELETE"]: - instance = get_instance(request.user, pk, perm_type="delete") - # an API request is a DRF request whose body is in .data; refused - # options stop nothing - options = DestroyOptionsSerializer(data=getattr(request, "data", request.POST)) + get_instance(request.user, pk, perm_type="delete") + options = DestroyOptionsSerializer(data=request.POST) options.is_valid(raise_exception=True) - with libvirt_instance_lock(instance): - with audit_vm(request, instance, "delete"): - proxy = instance.proxy - # the files to delete are known before the VM is stopped: a - # host that cannot be checked refuses the destruction - to_delete, shared = [], [] - if options.validated_data["delete_disk"]: - to_delete, shared = proxy.split_disk_paths_by_use( - lambda own: utils.paths_used_on_other_computes(instance.compute, own)) - - # Paused and other active states must stop too, or the VM keeps - # running as a transient domain after the undefine. - if proxy.instance.isActive(): - proxy.force_shutdown() - - # Undefine before deleting any disk: if it fails, nothing is lost. - # Snapshot metadata goes with the domain; internal snapshot data - # stays in the disk images that are kept. - flags = VIR_DOMAIN_UNDEFINE_MANAGED_SAVE | VIR_DOMAIN_UNDEFINE_SNAPSHOTS_METADATA - if options.validated_data["delete_nvram"]: - flags |= VIR_DOMAIN_UNDEFINE_NVRAM - else: - flags |= VIR_DOMAIN_UNDEFINE_KEEP_NVRAM - proxy.delete(flags) - instance.delete() - - # each disk has its own row: the VM is gone even if one is kept; a - # file that stays keeps the files below it, which it needs - kept = set() - for path in to_delete: - if path in kept: - messages.warning(request, _("Disk %(path)s was kept: a disk file that stays needs it") % {"path": path}) - continue - try: - with audit_vm(request, instance, f"delete volume {path}"): - proxy.get_volume_by_path(path).delete(0) - except libvirtError as err: - messages.error(request, _("The VM is removed, but disk %(path)s was not deleted: %(err)s") % { - "path": path, "err": error_text(request, err)}) - try: - while (path := proxy.backing_path(path)) and path not in kept: - kept.add(path) - except libvirtError: - # the files it needs are unknown: none of the rest goes - kept.update(to_delete) - for path in shared: - messages.warning(request, _("Disk %(path)s is used by another VM and was kept") % {"path": path}) - return redirect(reverse("instances:index")) + try: + op = jobs.submit("instance.delete", request.user, {"instance": pk, **options.validated_data}, + request.POST.get("submission_key", "")) + except claims.Busy as busy: + return _busy(request, pk, busy) + except (jobs.SubmissionConflict, libvirtError) as err: + messages.error(request, error_text(request, err)) + return redirect(reverse("instances:destroy", args=[pk])) + jobs.remember_job(request, op) + return redirect(reverse("instances:instance", args=[pk])) instance = get_instance(request.user, pk, perm_type="delete") try: diff --git a/locale/tr/LC_MESSAGES/django.mo b/locale/tr/LC_MESSAGES/django.mo index ef6aa6fd..4bf1a925 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 b9b6285b..820a44d1 100644 --- a/locale/tr/LC_MESSAGES/django.po +++ b/locale/tr/LC_MESSAGES/django.po @@ -284,7 +284,7 @@ msgstr "Burada oturumunuz açık kalır; diğer oturumlarınız kapanır." #: instances/templates/add_instance_volume.html:108 #: instances/templates/create_instance_w1.html:40 #: instances/templates/create_instance_w2.html:355 -#: instances/templates/instances/destroy_instance_form.html:51 +#: instances/templates/instances/destroy_instance_form.html:52 #: instances/templates/instances/edit_instance_volume.html:111 #: instances/templates/instances/move_disk.html:27 #: instances/templates/instances/settings_tab.html:411 @@ -915,7 +915,7 @@ msgid "IP" msgstr "IP" #: admin/templates/admin/logs.html:56 admin/views.py:286 instances/states.py:19 -#: instances/views.py:419 instances/views.py:421 logs/views.py:17 +#: instances/views.py:415 instances/views.py:417 logs/views.py:17 msgid "Unknown" msgstr "Bilinmeyen" @@ -1714,7 +1714,7 @@ msgid "New VM defaults" msgstr "Yeni sanal makine varsayılanları" #: appsettings/views.py:40 instances/templates/instance.html:64 -#: instances/templates/instances/destroy_instance_form.html:32 +#: instances/templates/instances/destroy_instance_form.html:33 #: instances/templates/instances/resize_tab.html:20 #: instances/templates/instances/settings_tab.html:197 msgid "Disks" @@ -2704,57 +2704,57 @@ msgstr "" msgid "The name of the virtual machine must not contain any special characters" msgstr "Sanal makine adı özel karakterler içeremez" -#: instances/jobs.py:69 +#: instances/jobs.py:72 msgid "The VM moved to another compute after the job was queued" msgstr "İş kuyruğa alındıktan sonra sanal makine başka bir sunucuya taşındı" -#: instances/jobs.py:137 +#: instances/jobs.py:140 msgid "Enter a whole number of vCPUs for the clone" msgstr "Klon için vCPU sayısını tam sayı olarak girin" -#: instances/jobs.py:139 +#: instances/jobs.py:142 msgid "Enter a whole number of memory (MiB) for the clone" msgstr "Klon için belleği (MiB) tam sayı olarak girin" -#: instances/jobs.py:166 +#: instances/jobs.py:169 msgid "Choose the clone's name from the list of free names" msgstr "Klonun adını boş adlar listesinden seçin" -#: instances/jobs.py:194 +#: instances/jobs.py:197 #, python-format msgid "%(quota)s: cannot create '%(clone_name)s'." msgstr "%(quota)s: '%(clone_name)s' oluşturulamıyor." -#: instances/jobs.py:197 instances/jobs.py:549 +#: instances/jobs.py:200 instances/jobs.py:552 #, python-format msgid "Instance '%(clone_name)s' already exists!" msgstr "'%(clone_name)s' sanal makinesi zaten mevcut!" -#: instances/jobs.py:199 +#: instances/jobs.py:202 #, python-format msgid "Instance name '%(clone_name)s' contains invalid characters!" msgstr "Sanal makine adı '%(clone_name)s' geçersiz karakterler içermektedir!" -#: instances/jobs.py:202 +#: instances/jobs.py:205 #, python-format msgid "Instance MAC '%(clone_mac)s' invalid format!" msgstr "Sanal makine MAC adresi '%(clone_mac)s' geçersiz biçemde belirtilmiş!" -#: instances/jobs.py:204 +#: instances/jobs.py:207 #, python-format msgid "Disk name '%(disk_name)s' contains invalid characters!" msgstr "Disk adı '%(disk_name)s' geçersiz karakterler içeriyor!" -#: instances/jobs.py:224 +#: instances/jobs.py:227 #, python-format msgid "The clone is made, but not as asked: %(error)s" msgstr "Klon oluşturuldu, ama istendiği gibi değil: %(error)s" -#: instances/jobs.py:280 +#: instances/jobs.py:283 msgid "The VM was not defined and no volume of it was left." msgstr "Sanal makine tanımlanmadı ve ondan geriye birim kalmadı." -#: instances/jobs.py:282 +#: instances/jobs.py:285 #, python-format msgid "" "The VM was not defined; check these volumes, then resolve this job: " @@ -2763,28 +2763,28 @@ msgstr "" "Sanal makine tanımlanmadı; şu birimleri kontrol edin, sonra bu işi çözün: " "%(volumes)s" -#: instances/jobs.py:303 +#: instances/jobs.py:306 msgid "Delete snapshot" msgstr "Anlık görüntüyü sil" -#: instances/jobs.py:313 vrtManager/instance.py:2299 +#: instances/jobs.py:316 vrtManager/instance.py:2287 #, python-format msgid "The VM has no external snapshot named %(name)s" msgstr "Sanal makinede %(name)s adında harici anlık görüntü yok" -#: instances/jobs.py:326 +#: instances/jobs.py:329 msgid "Merging the snapshot" msgstr "Anlık görüntü birleştiriliyor" -#: instances/jobs.py:345 +#: instances/jobs.py:348 msgid "The VM is no longer on its compute" msgstr "Sanal makine artık kendi sunucusunda değil" -#: instances/jobs.py:348 +#: instances/jobs.py:351 msgid "libvirt is still merging the snapshot" msgstr "libvirt anlık görüntüyü hâlâ birleştiriyor" -#: instances/jobs.py:356 +#: instances/jobs.py:359 msgid "" "The snapshot is still listed and no merge runs. It cannot be told whether the" " disks changed; check the VM before deleting the snapshot again or reverting " @@ -2794,44 +2794,44 @@ msgstr "" "değişmediği bilinemiyor; anlık görüntüyü yeniden silmeden ya da ona dönmeden " "önce sanal makineyi kontrol edin." -#: instances/jobs.py:363 +#: instances/jobs.py:366 msgid "The VM is no longer on the compute the job ran on" msgstr "Sanal makine artık işin çalıştığı sunucuda değil" -#: instances/jobs.py:369 +#: instances/jobs.py:372 msgid "Take snapshot" msgstr "Anlık görüntü al" -#: instances/jobs.py:381 instances/jobs.py:390 vrtManager/instance.py:1895 +#: instances/jobs.py:384 instances/jobs.py:393 vrtManager/instance.py:1883 #, python-format msgid "A snapshot named %(name)s already exists" msgstr "%(name)s adında bir anlık görüntü zaten var" -#: instances/jobs.py:396 +#: instances/jobs.py:399 msgid "Taking the snapshot" msgstr "Anlık görüntü alınıyor" -#: instances/jobs.py:409 +#: instances/jobs.py:412 msgid "libvirt is still taking the snapshot" msgstr "libvirt anlık görüntüyü hâlâ alıyor" -#: instances/jobs.py:412 +#: instances/jobs.py:415 msgid "The snapshot was not taken" msgstr "Anlık görüntü alınmadı" -#: instances/jobs.py:435 +#: instances/jobs.py:438 msgid "Revert snapshot" msgstr "Snapshota dön" -#: instances/jobs.py:466 +#: instances/jobs.py:469 msgid "Reverting the VM" msgstr "Sanal makine geri döndürülüyor" -#: instances/jobs.py:477 +#: instances/jobs.py:480 msgid "libvirt is still reverting the VM" msgstr "libvirt sanal makineyi hâlâ geri döndürüyor" -#: instances/jobs.py:481 +#: instances/jobs.py:484 msgid "" "It cannot be told whether the VM was reverted; check it, then revert again or" " resolve this job." @@ -2839,12 +2839,12 @@ msgstr "" "Sanal makinenin geri döndürülüp döndürülmediği bilinemiyor; kontrol edin, " "sonra yeniden geri döndürün ya da bu işi çözün." -#: instances/jobs.py:498 +#: instances/jobs.py:501 msgid "The VM must be running, paused or shut off to be cloned" msgstr "" "Klonlanmak için sanal makine çalışıyor, duraklatılmış ya da kapalı olmalı" -#: instances/jobs.py:499 +#: instances/jobs.py:502 #, python-format msgid "" "The guest's file systems could not be thawed (%(error)s); thaw them with: " @@ -2853,19 +2853,19 @@ msgstr "" "Konuğun dosya sistemleri çözülemedi (%(error)s); şununla çözün: virsh " "domfsthaw %(vm)s" -#: instances/jobs.py:518 +#: instances/jobs.py:521 msgid "Clone VM" msgstr "Sanal makineyi klonla" -#: instances/jobs.py:559 instances/jobs.py:566 instances/jobs.py:841 +#: instances/jobs.py:562 instances/jobs.py:569 instances/jobs.py:844 msgid "Copying disks" msgstr "Diskler kopyalanıyor" -#: instances/jobs.py:618 +#: instances/jobs.py:621 msgid "libvirt has no result of the copy" msgstr "libvirt'te kopyanın sonucu yok" -#: instances/jobs.py:629 +#: instances/jobs.py:632 #, python-format msgid "" "The copy failed: %(error)s. The guest's file systems could not be thawed " @@ -2874,11 +2874,11 @@ msgstr "" "Kopyalama başarısız oldu: %(error)s. Konuğun dosya sistemleri çözülemedi " "(%(thaw)s); şununla çözün: virsh domfsthaw %(vm)s" -#: instances/jobs.py:632 instances/jobs.py:716 +#: instances/jobs.py:635 instances/jobs.py:719 msgid "The copy failed" msgstr "Kopyalama başarısız oldu" -#: instances/jobs.py:657 +#: instances/jobs.py:660 #, python-format msgid "" "The VM is no longer on the compute the job ran on; check these volumes, then " @@ -2887,7 +2887,7 @@ msgstr "" "Sanal makine artık işin çalıştığı sunucuda değil; şu birimleri kontrol edin, " "sonra bu işi çözün: %(volumes)s" -#: instances/jobs.py:692 +#: instances/jobs.py:695 #, python-format msgid "" "A volume of the clone was being created; check these volumes, then resolve " @@ -2896,15 +2896,15 @@ msgstr "" "Klonun bir birimi oluşturuluyordu; şu birimleri kontrol edin, sonra bu işi " "çözün: %(volumes)s" -#: instances/jobs.py:696 +#: instances/jobs.py:699 msgid "The job stopped before the copy started" msgstr "İş, kopyalama başlamadan durdu" -#: instances/jobs.py:703 +#: instances/jobs.py:706 msgid "libvirt is still copying the disks" msgstr "libvirt diskleri hâlâ kopyalıyor" -#: instances/jobs.py:706 +#: instances/jobs.py:709 #, python-format msgid "" "Another backup runs on the VM; check these volumes, then resolve this job: " @@ -2913,7 +2913,7 @@ msgstr "" "Sanal makinede başka bir yedekleme çalışıyor; şu birimleri kontrol edin, " "sonra bu işi çözün: %(volumes)s" -#: instances/jobs.py:711 +#: instances/jobs.py:714 #, python-format msgid "" "libvirt has no result of the copy; check these volumes, then resolve this " @@ -2922,7 +2922,7 @@ msgstr "" "libvirt'te kopyanın sonucu yok; şu birimleri kontrol edin, sonra bu işi " "çözün: %(volumes)s" -#: instances/jobs.py:723 +#: instances/jobs.py:726 #, python-format msgid "" "The disks were copied, but the clone could not be defined: %(error)s; check " @@ -2931,156 +2931,184 @@ msgstr "" "Diskler kopyalandı, ancak klon tanımlanamadı: %(error)s; şu birimleri kontrol" " edin, sonra bu işi çözün: %(volumes)s" -#: instances/jobs.py:746 instances/templates/create_instance_w1.html:60 +#: instances/jobs.py:749 instances/templates/create_instance_w1.html:60 #: instances/templates/create_instance_w2.html:356 msgid "Create VM" msgstr "Sanal makine oluştur" -#: instances/jobs.py:770 instances/jobs.py:815 instances/views.py:2504 +#: instances/jobs.py:773 instances/jobs.py:818 instances/views.py:2459 msgid "A virtual machine with this name already exists" msgstr "Bu isimde bir sanal makine mevcut" -#: instances/jobs.py:772 instances/jobs.py:812 +#: instances/jobs.py:775 instances/jobs.py:815 msgid "There is an instance with same name. Remove it and try again!" msgstr "Aynı isimde başka bir sanal makine var. Onu silip yeniden deneyin!" -#: instances/jobs.py:775 +#: instances/jobs.py:778 msgid "First you need to create or select an image" msgstr "Öncelikle bir imaj oluşturun ya da mevcut bir imajı seçin" -#: instances/jobs.py:777 +#: instances/jobs.py:780 msgid "Choose one disk source: a new disk, existing volumes or a template" msgstr "" "Tek bir disk kaynağı seçin: yeni bir disk, mevcut birimler ya da bir şablon" -#: instances/jobs.py:779 +#: instances/jobs.py:782 msgid "Image has already exist. Please check volumes or change instance name" msgstr "" "İmaj zaten mevcut. Lütfen ya makine adını değişin ya da disk alanlarını " "kontrol edin" -#: instances/jobs.py:781 +#: instances/jobs.py:784 msgid "Invalid cache mode" msgstr "Geçersiz önbellek modu" -#: instances/jobs.py:783 +#: instances/jobs.py:786 #, python-format msgid "This host offers no CPU mode or model %(mode)s" msgstr "Bu sunucu %(mode)s CPU modunu veya modelini sunmuyor" -#: instances/jobs.py:785 +#: instances/jobs.py:788 #, python-format msgid "A CD-ROM cannot use the %(bus)s bus" msgstr "CD-ROM %(bus)s veri yolunu kullanamaz" -#: instances/jobs.py:788 +#: instances/jobs.py:791 msgid "The cloud-init seed is read from a CD-ROM: choose its bus" msgstr "cloud-init tohumu bir CD-ROM'dan okunur: veri yolunu seçin" -#: instances/jobs.py:791 +#: instances/jobs.py:794 msgid "The cloud-init seed needs a directory storage pool for the disk" msgstr "cloud-init tohumu, disk için bir dizin depolama havuzu gerektirir" -#: instances/jobs.py:797 +#: instances/jobs.py:800 #, python-format msgid "This host offers no firmware %(firmware)s" msgstr "Bu sunucu %(firmware)s firmware'ini sunmuyor" -#: instances/jobs.py:799 vrtManager/instance.py:3023 +#: instances/jobs.py:802 vrtManager/instance.py:3011 msgid "This host offers no TPM 2.0: install swtpm on it" msgstr "Bu sunucu TPM 2.0 sunmuyor: üzerine swtpm kurun" -#: instances/jobs.py:860 +#: instances/jobs.py:863 msgid "Writing the cloud-init seed" msgstr "cloud-init tohumu yazılıyor" -#: instances/jobs.py:867 +#: instances/jobs.py:870 msgid "Defining the VM" msgstr "Sanal makine tanımlanıyor" -#: instances/jobs.py:900 +#: instances/jobs.py:903 #, python-format msgid "%(error)s. Remove these volumes by hand: %(volumes)s" msgstr "%(error)s. Şu birimleri elle silin: %(volumes)s" -#: instances/jobs.py:925 +#: instances/jobs.py:928 #, python-format msgid "The instance is already on %(compute)s" msgstr "Sanal makine zaten %(compute)s üzerinde" -#: instances/jobs.py:927 +#: instances/jobs.py:930 #, python-format msgid "%(compute)s is not reachable" msgstr "%(compute)s sunucusuna ulaşılamıyor" -#: instances/jobs.py:929 +#: instances/jobs.py:932 #, python-format msgid "%(compute)s is the VM's own host under another name" msgstr "%(compute)s, sanal makinenin kendi sunucusunun başka bir adı" -#: instances/jobs.py:963 +#: instances/jobs.py:966 #, python-format msgid "" "The VM migrated, but autostart could not be set on the destination: %(error)s" msgstr "" "Sanal makine taşındı, ancak hedefte otomatik başlatma ayarlanamadı: %(error)s" -#: instances/jobs.py:969 +#: instances/jobs.py:972 #, python-format msgid "The VM migrated, but its NVRAM file stays on the source: %(error)s" msgstr "Sanal makine taşındı, ancak NVRAM dosyası kaynakta kaldı: %(error)s" -#: instances/jobs.py:976 +#: instances/jobs.py:979 msgid "Migrate VM" msgstr "Sanal makineyi taşı" -#: instances/jobs.py:1023 +#: instances/jobs.py:1026 msgid "Migrating the VM" msgstr "Sanal makine taşınıyor" -#: instances/jobs.py:1070 +#: instances/jobs.py:1073 msgid "libvirt is still migrating the VM" msgstr "libvirt sanal makineyi hâlâ taşıyor" -#: instances/jobs.py:1079 +#: instances/jobs.py:1084 msgid "The VM stayed on the source host" msgstr "Sanal makine kaynak sunucuda kaldı" -#: instances/jobs.py:1081 +#: instances/jobs.py:1086 msgid "" "The VM is on both computes or on neither; check them, then resolve this job." msgstr "" "Sanal makine ya iki sunucuda da var ya da hiçbirinde yok; kontrol edin, sonra" " bu işi çözün." -#: instances/jobs.py:1129 +#: instances/jobs.py:1134 msgid "Move disk" msgstr "Diski taşı" -#: instances/jobs.py:1136 +#: instances/jobs.py:1141 msgid "Choose a disk of the VM" msgstr "Sanal makinenin bir diskini seçin" -#: instances/jobs.py:1160 +#: instances/jobs.py:1165 msgid "Copying the disk" msgstr "Disk kopyalanıyor" -#: instances/jobs.py:1203 +#: instances/jobs.py:1208 #, python-format msgid "Another block job runs on %(dev)s; check the VM" msgstr "" "%(dev)s üzerinde başka bir blok işi çalışıyor; sanal makineyi kontrol edin" -#: instances/jobs.py:1209 +#: instances/jobs.py:1214 #, python-format msgid "The disk stayed in %(path)s; remove %(dest)s if it is there" msgstr "Disk %(path)s içinde kaldı; varsa %(dest)s dosyasını silin" -#: instances/jobs.py:1211 +#: instances/jobs.py:1216 #, python-format msgid "The definitions of %(dev)s name %(paths)s; check the VM" msgstr "%(dev)s tanımları %(paths)s gösteriyor; sanal makineyi kontrol edin" +#: instances/jobs.py:1223 +#, python-format +msgid "Disk %(path)s was kept: a disk file that stays needs it" +msgstr "%(path)s diski korundu: kalan bir disk dosyası ona ihtiyaç duyuyor" + +#: instances/jobs.py:1224 +#, python-format +msgid "The VM is removed, but disk %(path)s was not deleted: %(err)s" +msgstr "Sanal makine kaldırıldı, ancak %(path)s diski silinmedi: %(err)s" + +#: instances/jobs.py:1225 +#, python-format +msgid "Disk %(path)s is used by another VM and was kept" +msgstr "" +"%(path)s diski başka bir sanal makine tarafından kullanılıyor ve korundu" + +#: instances/jobs.py:1279 +msgid "Delete VM" +msgstr "Sanal makineyi sil" + +#: instances/jobs.py:1309 +msgid "Deleting the VM" +msgstr "Sanal makine siliniyor" + +#: instances/jobs.py:1321 +msgid "The VM was not deleted; it may have been shut off" +msgstr "Sanal makine silinmedi; kapatılmış olabilir" + #: instances/models.py:18 msgid "label" msgstr "etiket" @@ -3308,7 +3336,7 @@ msgid "Disk source" msgstr "Disk kaynağı" #: instances/templates/add_instance_volume.html:21 -#: instances/templates/create_instance_w2.html:134 instances/views.py:2523 +#: instances/templates/create_instance_w2.html:134 instances/views.py:2478 msgid "New disk" msgstr "Yeni disk" @@ -3680,7 +3708,7 @@ msgstr "Özel CPU modelleri" msgid "Size (GiB)" msgstr "Boyut (GiB)" -#: instances/templates/create_instance_w2.html:161 instances/views.py:2524 +#: instances/templates/create_instance_w2.html:161 instances/views.py:2479 msgid "Existing volumes" msgstr "Mevcut birimler" @@ -3716,7 +3744,7 @@ msgstr "Aygıt" msgid "Remove" msgstr "Kaldır" -#: instances/templates/create_instance_w2.html:194 instances/views.py:2525 +#: instances/templates/create_instance_w2.html:194 instances/views.py:2480 msgid "Clone from template" msgstr "Şablondan klonla" @@ -4060,18 +4088,18 @@ msgstr "%(kind)s %(name)s yukarı taşı" msgid "Move %(kind)s %(name)s down" msgstr "%(kind)s %(name)s aşağı taşı" -#: instances/templates/instances/destroy_instance_form.html:3 -#: instances/templates/instances/destroy_instance_form.html:6 +#: instances/templates/instances/destroy_instance_form.html:4 +#: instances/templates/instances/destroy_instance_form.html:7 #, python-format msgid "Destroy %(name)s" msgstr "%(name)s sanal makinesini sil" -#: instances/templates/instances/destroy_instance_form.html:6 +#: instances/templates/instances/destroy_instance_form.html:7 #, python-format msgid "on %(host)s" msgstr "%(host)s üzerinde" -#: instances/templates/instances/destroy_instance_form.html:10 +#: instances/templates/instances/destroy_instance_form.html:11 msgid "" "The VM's definition, saved state and snapshot records are removed. A running " "VM is forced off first; what the guest has not written to disk is lost." @@ -4080,15 +4108,15 @@ msgstr "" " Çalışan bir sanal makine önce zorla kapatılır; konuk sistemin diske " "yazmadıkları kaybolur." -#: instances/templates/instances/destroy_instance_form.html:13 +#: instances/templates/instances/destroy_instance_form.html:14 msgid "What should happen to the files?" msgstr "Dosyalara ne olsun?" -#: instances/templates/instances/destroy_instance_form.html:18 +#: instances/templates/instances/destroy_instance_form.html:19 msgid "Delete disk files" msgstr "Disk dosyalarını sil" -#: instances/templates/instances/destroy_instance_form.html:19 +#: instances/templates/instances/destroy_instance_form.html:20 msgid "" "With them go this VM's external snapshot files, snapshot memory files and " "cloud-init seed." @@ -4096,11 +4124,11 @@ msgstr "" "Onlarla birlikte bu sanal makinenin harici anlık görüntü dosyaları, anlık " "görüntü bellek dosyaları ve cloud-init tohumu da silinir." -#: instances/templates/instances/destroy_instance_form.html:25 +#: instances/templates/instances/destroy_instance_form.html:26 msgid "Keep the disk files" msgstr "Disk dosyalarını koru" -#: instances/templates/instances/destroy_instance_form.html:26 +#: instances/templates/instances/destroy_instance_form.html:27 msgid "" "They stay with their internal snapshot data; the snapshot records still go " "with the VM." @@ -4108,11 +4136,11 @@ msgstr "" "Dosyalar iç anlık görüntü verileriyle birlikte kalır; anlık görüntü kayıtları" " yine de sanal makineyle birlikte silinir." -#: instances/templates/instances/destroy_instance_form.html:35 +#: instances/templates/instances/destroy_instance_form.html:36 msgid "no file" msgstr "dosya yok" -#: instances/templates/instances/destroy_instance_form.html:39 +#: instances/templates/instances/destroy_instance_form.html:40 msgid "" "A file another VM uses on any of the panel's hosts, directly or under its own" " disk, is kept, and so is the template a disk was made from. If a host cannot" @@ -4122,23 +4150,23 @@ msgstr "" "kendi diskinin altında kullandığı dosyalar korunur; bir diskin oluşturulduğu " "şablon da korunur. Bir sunucu denetlenemezse sanal makine silinmez." -#: instances/templates/instances/destroy_instance_form.html:43 +#: instances/templates/instances/destroy_instance_form.html:44 msgid "Delete the NVRAM file (firmware settings). Untick to keep it." msgstr "" "NVRAM dosyasını (ürün yazılımı ayarları) sil. Korumak için işareti kaldırın." -#: instances/templates/instances/destroy_instance_form.html:47 +#: instances/templates/instances/destroy_instance_form.html:48 msgid "" "Its TPM state is deleted with it, and the keys in it, such as BitLocker's." msgstr "" "TPM durumu da onunla birlikte silinir, içindeki anahtarlar da, örneğin " "BitLocker'ınkiler." -#: instances/templates/instances/destroy_instance_form.html:50 +#: instances/templates/instances/destroy_instance_form.html:51 msgid "This cannot be undone." msgstr "Bu işlem geri alınamaz." -#: instances/templates/instances/destroy_instance_form.html:52 +#: instances/templates/instances/destroy_instance_form.html:53 msgid "Destroy VM" msgstr "Sanal makineyi sil" @@ -5879,43 +5907,43 @@ msgstr "Geçersiz MAC adresi" msgid "More MAC addresses than networks" msgstr "Ağlardan daha fazla MAC adresi" -#: instances/views.py:173 +#: instances/views.py:169 msgid "Enter an IP address, a network such as 192.0.2.0/24, or a MAC address." msgstr "Bir IP adresi, 192.0.2.0/24 gibi bir ağ ya da bir MAC adresi girin." -#: instances/views.py:179 instances/views.py:213 +#: instances/views.py:175 instances/views.py:209 msgid "Too many searches are still running; try again shortly." msgstr "Çok fazla arama hâlâ çalışıyor; birazdan yeniden deneyin." -#: instances/views.py:261 +#: instances/views.py:257 #, python-format msgid "The host %(host)s of '%(name)s' cannot be reached: %(error)s" msgstr "'%(name)s' sanal makinesinin sunucusu %(host)s erişilemiyor: %(error)s" -#: instances/views.py:267 +#: instances/views.py:263 #, python-format msgid "The host of '%(name)s' cannot be reached right now." msgstr "'%(name)s' sanal makinesinin sunucusuna şu anda erişilemiyor." -#: instances/views.py:268 +#: instances/views.py:264 msgid " Jobs on it can be followed under Jobs in the menu." msgstr " Üzerindeki işler menüdeki İşler bölümünden izlenebilir." -#: instances/views.py:276 +#: instances/views.py:272 #, python-format msgid "'%(name)s' is no longer on its host." msgstr "'%(name)s' artık sunucusunda yok." -#: instances/views.py:374 +#: instances/views.py:370 msgid "There is no other host to migrate to." msgstr "Taşınacak başka bir sunucu yok." -#: instances/views.py:376 +#: instances/views.py:372 msgid "A VM migrates when it is running, paused or shut off." msgstr "" "Sanal makine çalışırken, duraklatılmışken ya da kapalıyken taşınabilir." -#: instances/views.py:378 +#: instances/views.py:374 msgid "" "A VM with snapshots cannot be migrated: its snapshots stay on this host. " "Delete them first." @@ -5923,7 +5951,7 @@ msgstr "" "Anlık görüntüsü olan bir sanal makine taşınamaz: anlık görüntüleri bu " "sunucuda kalır. Önce onları silin." -#: instances/views.py:380 +#: instances/views.py:376 msgid "" "A VM with host devices cannot be migrated: the destination may not have them." " Remove them first." @@ -5931,11 +5959,11 @@ 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:640 msgid "The VM was moved meanwhile; try again." msgstr "Sanal makine bu arada taşındı; yeniden deneyin." -#: instances/views.py:760 +#: instances/views.py:756 msgid "" "The running VM has no free PCI slot for another disk on this bus: choose the " "scsi bus if the VM has a SCSI controller, or shut the VM down and add the " @@ -5948,7 +5976,7 @@ msgstr "" "yuvaları ekle, sonraki başlatmadan itibaren ona boş yuvalar verir. Yeni bir " "birim havuzunda kalır, mevcut birim olarak eklenebilir." -#: instances/views.py:764 +#: instances/views.py:760 msgid "" "The running VM has no free PCI slot for the SCSI controller the disk needs: " "shut the VM down and add the disk then; Add PCIe slots on the Disks tab gives" @@ -5960,191 +5988,175 @@ msgstr "" "PCIe yuvaları ekle, sonraki başlatmadan itibaren ona boş yuvalar verir. Yeni " "bir birim havuzunda kalır, mevcut birim olarak eklenebilir." -#: instances/views.py:782 +#: instances/views.py:778 #, python-format msgid "Invalid disk options: %(options)s" msgstr "Geçersiz disk seçenekleri: %(options)s" -#: instances/views.py:792 instances/views.py:807 +#: instances/views.py:788 instances/views.py:803 msgid "Templates cannot be started." msgstr "Şablon makineler başlatılamaz." -#: instances/views.py:903 -#, python-format -msgid "Disk %(path)s was kept: a disk file that stays needs it" -msgstr "%(path)s diski korundu: kalan bir disk dosyası ona ihtiyaç duyuyor" - -#: instances/views.py:909 -#, python-format -msgid "The VM is removed, but disk %(path)s was not deleted: %(err)s" -msgstr "Sanal makine kaldırıldı, ancak %(path)s diski silinmedi: %(err)s" - -#: instances/views.py:918 -#, python-format -msgid "Disk %(path)s is used by another VM and was kept" -msgstr "" -"%(path)s diski başka bir sanal makine tarafından kullanılıyor ve korundu" - -#: instances/views.py:1040 +#: instances/views.py:995 #, python-format msgid "Set the gstfsd key of %(compute)s first (Computes, edit the host)" msgstr "" "Önce %(compute)s sunucusunun gstfsd anahtarını ayarlayın (Sunucular, sunucuyu" " düzenle)" -#: instances/views.py:1047 instances/views.py:1065 instances/views.py:1074 +#: instances/views.py:1002 instances/views.py:1020 instances/views.py:1029 #, python-format msgid "gstfsd error: %(err)s" msgstr "gstfsd hatası: %(err)s" -#: instances/views.py:1075 +#: instances/views.py:1030 msgid "the reply is not signed with the key" msgstr "yanıt anahtarla imzalanmamış" -#: instances/views.py:1083 +#: instances/views.py:1038 msgid "Shut the VM down, or start its QEMU guest agent, and try again" msgstr "" "Sanal makineyi kapatın ya da QEMU konuk ajanını başlatın ve yeniden deneyin" -#: instances/views.py:1096 +#: instances/views.py:1051 msgid "The change may still be made when the agent answers again." msgstr "Ajan yeniden yanıt verdiğinde değişiklik yine de yapılabilir." -#: instances/views.py:1124 +#: instances/views.py:1079 msgid "Reset root password" msgstr "Root parolasını sıfırla" -#: instances/views.py:1128 +#: instances/views.py:1083 msgid "Enter a password" msgstr "Bir parola girin" -#: instances/views.py:1142 +#: instances/views.py:1097 msgid "Select an SSH public key" msgstr "Bir SSH açık anahtarı seçin" -#: instances/views.py:1164 +#: instances/views.py:1119 #, python-format msgid "Installed new SSH public key %(keyname)s" msgstr "Yeni %(keyname)s SSH açık anahtarı yüklendi" -#: instances/views.py:1184 +#: instances/views.py:1139 msgid "Enter a whole number of vCPUs" msgstr "vCPU sayısı için bir tam sayı girin" -#: instances/views.py:1188 +#: instances/views.py:1143 msgid "Shut off the VM to change its vCPUs" msgstr "vCPU'larını değiştirmek için sanal makineyi kapatın" -#: instances/views.py:1191 +#: instances/views.py:1146 msgid "The current vCPUs cannot be more than the maximum" msgstr "Geçerli vCPU sayısı en fazla değerden büyük olamaz" -#: instances/views.py:1198 +#: instances/views.py:1153 #, python-format msgid "%(quota)s: cannot resize the CPU of '%(instance_name)s'." msgstr "%(quota)s: '%(instance_name)s' CPU'su yeniden boyutlandırılamıyor." -#: instances/views.py:1206 +#: instances/views.py:1161 #, python-format msgid "vCPUs set to %(cur)s (maximum %(max)s)" msgstr "vCPU sayısı %(cur)s olarak ayarlandı (en fazla %(max)s)" -#: instances/views.py:1225 +#: instances/views.py:1180 msgid "Enter a whole number for memory" msgstr "Bellek için bir tam sayı girin" -#: instances/views.py:1228 +#: instances/views.py:1183 msgid "The current memory cannot be more than the maximum" msgstr "Geçerli bellek en fazla değerden büyük olamaz" -#: instances/views.py:1232 +#: instances/views.py:1187 #, python-format msgid "%(quota)s: cannot resize the memory of '%(instance_name)s'." msgstr "%(quota)s: '%(instance_name)s' belleği yeniden boyutlandırılamıyor." -#: instances/views.py:1238 +#: instances/views.py:1193 #, python-format msgid "Memory set to %(cur)s MiB (maximum %(max)s MiB)" msgstr "Bellek %(cur)s MiB olarak ayarlandı (en fazla %(max)s MiB)" -#: instances/views.py:1258 +#: instances/views.py:1213 msgid "Enter disk sizes as whole GiB" msgstr "Disk boyutlarını tam GiB olarak girin" -#: instances/views.py:1262 +#: instances/views.py:1217 #, python-format msgid "%(dev)s is %(size)s: a disk cannot shrink" msgstr "%(dev)s boyutu %(size)s: disk küçültülemez" -#: instances/views.py:1270 +#: instances/views.py:1225 msgid "No disks were selected or valid for resizing." msgstr "Boyutlandırma için seçilmiş ya da geçerli disk yok." -#: instances/views.py:1279 +#: instances/views.py:1234 #, python-format msgid "%(quota)s: cannot resize the disks of '%(instance_name)s'." msgstr "%(quota)s: '%(instance_name)s' diskleri yeniden boyutlandırılamıyor." -#: instances/views.py:1291 +#: instances/views.py:1246 #, python-format msgid "The storage pool of %(dev)s cannot resize its volumes" msgstr "%(dev)s diskinin depolama havuzu birimlerini boyutlandıramaz" -#: instances/views.py:1293 +#: instances/views.py:1248 #, python-format msgid "Disk is resized: %(dev)s" msgstr "Disk boyutu değiştirildi: %(dev)s" -#: instances/views.py:1387 +#: instances/views.py:1342 msgid "The VM's machine has no PCIe slots to add" msgstr "Sanal makinenin makine tipinde eklenecek PCIe yuvası yok" -#: instances/views.py:1392 +#: instances/views.py:1347 #, python-format msgid "%(count)s PCIe slots added" msgstr "%(count)s PCIe yuvası eklendi" -#: instances/views.py:1394 +#: instances/views.py:1349 #, python-format msgid "%(count)s PCIe slots added: the VM gets them at its next start" msgstr "" "%(count)s PCIe yuvası eklendi: sanal makine bunları sonraki başlatmasında " "alır" -#: instances/views.py:1440 +#: instances/views.py:1395 #, python-format msgid "%(volume)s is already a disk of this VM" msgstr "%(volume)s zaten bu sanal makinenin bir diski" -#: instances/views.py:1530 +#: instances/views.py:1485 msgid "Volume changes are applied. But it will be activated after shutdown" msgstr "" "Birim değişiklikleri uygulandı. Ama yeniden başlatıldıktan sonra uygulanacak" -#: instances/views.py:1535 +#: instances/views.py:1490 msgid "Volume is changed successfully." msgstr "Birim başarıyla değiştirildi." -#: instances/views.py:1553 +#: instances/views.py:1508 #, python-format msgid "Disk %(dev)s is not a storage volume of this instance" msgstr "%(dev)s diski bu sanal makinenin bir depolama birimi değil" -#: instances/views.py:1560 +#: instances/views.py:1515 #, python-format msgid "Disk %(dev)s has a pending change; delete it once the VM is shut down" msgstr "" "%(dev)s diskinin bekleyen bir değişikliği var; sanal makine kapatıldığında " "silin" -#: instances/views.py:1575 +#: instances/views.py:1530 #, python-format msgid "Volume %(vol)s is used by another VM; detach it instead" msgstr "" "%(vol)s birimi başka bir sanal makine tarafından kullanılıyor; onun yerine " "ayırın" -#: instances/views.py:1587 +#: instances/views.py:1542 #, python-format msgid "" "The guest has not released disk %(dev)s yet; volume %(vol)s was kept. Delete " @@ -6153,62 +6165,62 @@ msgstr "" "Konuk sistem %(dev)s diskini henüz bırakmadı; %(vol)s birimi korundu. Sanal " "makine kapatıldığında depolama havuzundan silin." -#: instances/views.py:1598 +#: instances/views.py:1553 #, python-format msgid "Disk %(dev)s was detached, but its volume was kept: %(err)s" msgstr "%(dev)s diski ayrıldı, ancak birimi korundu: %(err)s" -#: instances/views.py:1624 +#: instances/views.py:1579 #, python-format msgid "This VM cannot take a CD-ROM on the %(bus)s bus now" msgstr "Bu sanal makineye şu anda %(bus)s veri yolunda CD-ROM eklenemez" -#: instances/views.py:1627 +#: instances/views.py:1582 msgid "A USB CD-ROM needs an ISO image: libvirt has no empty USB drive" msgstr "USB CD-ROM bir ISO imajı ister: libvirt boş USB sürücü desteklemiyor" -#: instances/views.py:1632 +#: instances/views.py:1587 #, python-format msgid "%(image)s is already in a CD-ROM of this VM" msgstr "%(image)s bu sanal makinenin bir CD-ROM'unda zaten takılı" -#: instances/views.py:1713 +#: instances/views.py:1668 #, python-format msgid "Pool %(pool)s was not read again: %(err)s" msgstr "%(pool)s havuzu yeniden okunamadı: %(err)s" -#: instances/views.py:1734 +#: instances/views.py:1689 #, python-format msgid "Snapshot was not deleted: %(err)s" msgstr "Anlık görüntü silinmedi: %(err)s" -#: instances/views.py:1849 +#: instances/views.py:1804 msgid "Add at least one device to the boot order." msgstr "Ön yükleme sırasına en az bir aygıt ekleyin." -#: instances/views.py:1863 +#: instances/views.py:1818 msgid "Boot order changed. It applies at the next start." msgstr "Ön yükleme sırası değiştirildi. Sonraki başlatmada uygulanır." -#: instances/views.py:1865 +#: instances/views.py:1820 msgid "Boot order changed successfully." msgstr "Ön yükleme sırası başarıyla değiştirildi." -#: instances/views.py:1885 +#: instances/views.py:1840 msgid "The XML is empty" msgstr "XML boş" -#: instances/views.py:1889 instances/views.py:2514 +#: instances/views.py:1844 instances/views.py:2469 #, python-format msgid "The XML is not well-formed: %(error)s" msgstr "XML düzgün biçimlendirilmemiş: %(error)s" -#: instances/views.py:1891 +#: instances/views.py:1846 #, python-format msgid "The XML must keep the VM's UUID %(uuid)s." msgstr "XML, sanal makinenin %(uuid)s UUID'sini korumalıdır." -#: instances/views.py:1895 +#: instances/views.py:1850 msgid "" "The definition has changed since the editor was opened. Reload the page to " "edit the current one." @@ -6216,40 +6228,40 @@ msgstr "" "Düzenleyici açıldığından beri tanım değişti. Geçerli tanımı düzenlemek için " "sayfayı yeniden yükleyin." -#: instances/views.py:1907 +#: instances/views.py:1862 msgid "The XML definition was saved." msgstr "XML tanımı kaydedildi." -#: instances/views.py:1909 +#: instances/views.py:1864 msgid "The XML definition was saved; the VM uses it from its next start." msgstr "" "XML tanımı kaydedildi; sanal makine onu sonraki başlatmasından itibaren " "kullanır." -#: instances/views.py:1990 +#: instances/views.py:1945 msgid "Select one network interface to change" msgstr "Değiştirilecek bir ağ arabirimi seçin" -#: instances/views.py:2028 +#: instances/views.py:1983 msgid "Network Device Config is changed. Please shutdown instance to activate." msgstr "" "Ağ aygıtı yapılandırması değiştirildi. Aktifleştirmek için lütfen sanal " "makineyi kapatın." -#: instances/views.py:2030 +#: instances/views.py:1985 msgid "Network device changed" msgstr "Ağ aygıtı değiştirildi" -#: instances/views.py:2042 +#: instances/views.py:1997 msgid "No network selected" msgstr "Ağ seçilmedi" -#: instances/views.py:2047 +#: instances/views.py:2002 #, python-format msgid "The VM already has a network device with MAC %(mac)s" msgstr "Sanal makinede %(mac)s MAC adresli bir ağ aygıtı zaten var" -#: instances/views.py:2066 +#: instances/views.py:2021 msgid "" "The running VM has no free PCI slot for another network device: shut it down " "and add the device then; Add PCIe slots on the Disks tab gives it free ones " @@ -6259,21 +6271,21 @@ msgstr "" "aygıtı sonra ekleyin; Diskler sekmesindeki PCIe yuvaları ekle, sonraki " "başlatmadan itibaren ona boş yuvalar verir." -#: instances/views.py:2107 instances/views.py:2113 +#: instances/views.py:2062 instances/views.py:2068 msgid "The VM has it" msgstr "Sanal makinede var" -#: instances/views.py:2144 +#: instances/views.py:2099 #, python-format msgid "The host has no USB device %(device)s" msgstr "Sunucuda %(device)s USB aygıtı yok" -#: instances/views.py:2165 +#: instances/views.py:2120 #, python-format msgid "The host has no PCI device %(device)s" msgstr "Sunucuda %(device)s PCI aygıtı yok" -#: instances/views.py:2183 +#: instances/views.py:2138 #, python-format msgid "" "Removal of PCI device %(address)s requested: the guest has not let it go yet;" @@ -6283,20 +6295,20 @@ msgstr "" "bırakmadı; bırakana ya da sanal makine durana kadar çalışan sanal makinede " "kalır." -#: instances/views.py:2228 instances/views.py:2258 +#: instances/views.py:2183 instances/views.py:2213 msgid "Select a network interface and a QoS direction" msgstr "Bir ağ arabirimi ve bir QoS yönü seçin" -#: instances/views.py:2231 vrtManager/network.py:459 +#: instances/views.py:2186 vrtManager/network.py:459 msgid "QoS rates must be whole numbers" msgstr "QoS hızları tam sayı olmalıdır" -#: instances/views.py:2238 networks/views.py:229 +#: instances/views.py:2193 networks/views.py:229 #, python-format msgid "%(qos_dir)s QoS is set" msgstr "%(qos_dir)s QoS ayarlandı" -#: instances/views.py:2243 +#: instances/views.py:2198 #, python-format msgid "" "%(qos_dir)s QoS is set. A running VM gets it after it is powered off and on." @@ -6304,12 +6316,12 @@ msgstr "" "%(qos_dir)s QoS ayarlandı. Çalışan bir sanal makine, kapatılıp açıldıktan " "sonra bunu alır." -#: instances/views.py:2265 networks/views.py:255 +#: instances/views.py:2220 networks/views.py:255 #, python-format msgid "%(qos_dir)s QoS is deleted" msgstr "%(qos_dir)s QoS silindi" -#: instances/views.py:2270 +#: instances/views.py:2225 #, python-format msgid "" "%(qos_dir)s QoS is deleted. A running VM keeps the old limit until it is " @@ -6318,21 +6330,21 @@ msgstr "" "%(qos_dir)s QoS silindi. Çalışan bir sanal makine, kapatılıp açılana kadar " "eski sınırı korur." -#: instances/views.py:2298 +#: instances/views.py:2253 msgid "Only one owner is allowed and the one already added" msgstr "Sadece bir sahibe izin verilmiştir ve bir sahip zaten eklenmiş" -#: instances/views.py:2300 +#: instances/views.py:2255 #, python-format msgid "%(user)s is already an owner" msgstr "%(user)s zaten bir sahip" -#: instances/views.py:2345 +#: instances/views.py:2300 #, python-format msgid "Console settings were not saved: %(errors)s" msgstr "Konsol ayarları kaydedilmedi: %(errors)s" -#: instances/views.py:2367 +#: instances/views.py:2322 msgid "" "Error setting console password. You should check that your instance have an " "graphic device." @@ -6340,65 +6352,65 @@ msgstr "" "Konsol parolası ayarlanırken hata oluştu. Sanal makinenin bir grafik aygıtına" " sahip olduğundan emin olunuz." -#: instances/views.py:2414 +#: instances/views.py:2369 msgid "Shut the VM down to change whether it is a template." msgstr "Şablon olup olmadığını değiştirmek için sanal makineyi kapatın." -#: instances/views.py:2420 +#: instances/views.py:2375 #, python-format msgid "%(quota)s: '%(instance_name)s' stays a template." msgstr "%(quota)s: '%(instance_name)s' şablon olarak kalıyor." -#: instances/views.py:2502 +#: instances/views.py:2457 msgid "The XML has no name" msgstr "XML'de ad yok" -#: instances/views.py:2516 webvirtcloud/middleware.py:126 +#: instances/views.py:2471 webvirtcloud/middleware.py:126 #, python-format msgid "libvirt Error - %(exception)s" msgstr "libvirt Hatası - %(exception)s" -#: instances/views.py:2523 +#: instances/views.py:2478 msgid "An empty disk of the size you set" msgstr "Belirlediğiniz boyutta boş bir disk" -#: instances/views.py:2524 +#: instances/views.py:2479 msgid "Volumes already in a pool" msgstr "Bir havuzda zaten bulunan birimler" -#: instances/views.py:2525 +#: instances/views.py:2480 msgid "A copy of a template volume" msgstr "Bir şablon biriminin kopyası" -#: instances/views.py:2614 +#: instances/views.py:2569 msgid "You haven't defined any storage pools" msgstr "Hiç bir depolama havuzu tanımlamadınız" -#: instances/views.py:2616 +#: instances/views.py:2571 msgid "You haven't defined any network pools" msgstr "Hiç bir ağ havuzu tanımlamadınız" -#: instances/views.py:2679 +#: instances/views.py:2634 msgid "Flavor Created" msgstr "Flavor Oluşturuldu" -#: instances/views.py:2687 +#: instances/views.py:2642 msgid "Create Flavor" msgstr "Kullanıcı Oluştur" -#: instances/views.py:2702 +#: instances/views.py:2657 msgid "Flavor Updated" msgstr "Flavor Güncellendi" -#: instances/views.py:2708 +#: instances/views.py:2663 msgid "Update Flavor" msgstr "Flavor Güncelle" -#: instances/views.py:2720 +#: instances/views.py:2675 msgid "Flavor Deleted" msgstr "Flavor Silindi" -#: instances/views.py:2726 +#: instances/views.py:2681 msgid "Only the size template goes; VMs made from it keep their size." msgstr "" "Yalnızca boyut şablonu silinir; ondan oluşturulan sanal makineler boyutlarını" @@ -7954,7 +7966,7 @@ msgstr "" "Kurtarma bir saat boyunca işin durumunu okuyamadı; sanal makineyi kontrol " "edin, sonra bu işi çözün" -#: operations/worker.py:464 +#: operations/worker.py:473 msgid "The worker stopped before the job changed anything" msgstr "Worker, iş herhangi bir şeyi değiştirmeden önce durdu" @@ -9139,56 +9151,56 @@ msgid "Please shutdown the instance then try to enable vCPU hotplug" msgstr "" "vCPU hotplug özelliğini etkinleştirmek için önce sanal makineyi kapatın" -#: vrtManager/instance.py:1854 +#: vrtManager/instance.py:1842 msgid "" "The VM has external snapshots; internal snapshots cannot be mixed with them" msgstr "" "Sanal makinenin harici anlık görüntüleri var; dahili anlık görüntüler " "bunlarla karıştırılamaz" -#: vrtManager/instance.py:1886 +#: vrtManager/instance.py:1874 msgid "A snapshot with memory is not quiesced; it keeps the whole guest state" msgstr "" "Bellekli bir anlık görüntüde dosya sistemleri dondurulmaz; tüm konuk durumunu" " saklar" -#: vrtManager/instance.py:1888 +#: vrtManager/instance.py:1876 msgid "Only a running VM's file systems can be frozen" msgstr "Yalnızca çalışan bir sanal makinenin dosya sistemleri dondurulabilir" -#: vrtManager/instance.py:1890 +#: vrtManager/instance.py:1878 msgid "The QEMU guest agent is not connected; it freezes the file systems" msgstr "QEMU konuk aracısı bağlı değil; dosya sistemlerini o dondurur" -#: vrtManager/instance.py:1893 +#: vrtManager/instance.py:1881 msgid "" "The VM has internal snapshots; external snapshots cannot be mixed with them" msgstr "" "Sanal makinenin dahili anlık görüntüleri var; harici anlık görüntüler " "bunlarla karıştırılamaz" -#: vrtManager/instance.py:1898 +#: vrtManager/instance.py:1886 msgid "Newer snapshots exist after the last revert; delete them first" msgstr "" "Son geri dönüşten sonra daha yeni anlık görüntüler var; önce onları silin" -#: vrtManager/instance.py:1901 +#: vrtManager/instance.py:1889 msgid "No snapshot is current; revert to a snapshot first" msgstr "Geçerli anlık görüntü yok; önce bir anlık görüntüye geri dönün" -#: vrtManager/instance.py:1907 +#: vrtManager/instance.py:1895 #, python-format msgid "Eject the media from %(dev)s first" msgstr "Önce %(dev)s içindeki ortamı çıkarın" -#: vrtManager/instance.py:1909 +#: vrtManager/instance.py:1897 #, python-format msgid "Disk %(dev)s is not a file; external snapshots need file disks" msgstr "" "Disk %(dev)s bir dosya değil; harici anlık görüntüler dosya diskleri " "gerektirir" -#: vrtManager/instance.py:1917 +#: vrtManager/instance.py:1905 #, python-format msgid "" "The storage pools could not be refreshed, so their free space is unknown: " @@ -9196,21 +9208,21 @@ msgid "" msgstr "" "Depolama havuzları yenilenemedi, bu yüzden boş alanları bilinmiyor: %(err)s" -#: vrtManager/instance.py:1938 +#: vrtManager/instance.py:1926 msgid "Memory is saved only from a running VM" msgstr "Bellek yalnızca çalışan bir sanal makineden kaydedilir" -#: vrtManager/instance.py:1940 +#: vrtManager/instance.py:1928 msgid "The VM has no disk to keep its memory next to" msgstr "Sanal makinenin, belleğinin yanına kaydedileceği bir diski yok" -#: vrtManager/instance.py:1944 +#: vrtManager/instance.py:1932 #, python-format msgid "%(disk)s is not in a storage pool; memory is saved only into one" msgstr "" "%(disk)s bir depolama havuzunda değil; bellek yalnızca bir havuza kaydedilir" -#: vrtManager/instance.py:1975 +#: vrtManager/instance.py:1963 #, python-format msgid "" "Storage pool %(pool)s has %(free).1f GiB free; the snapshot needs %(size).1f " @@ -9221,7 +9233,7 @@ msgstr "" "%(size).1f GiB gerektiriyor (sanal makinenin oradaki disklerinin " "%%%(percent)g kadarı, en az %(minimum)g GiB, ve kaydediliyorsa bellek)" -#: vrtManager/instance.py:1993 +#: vrtManager/instance.py:1981 msgid "" "This snapshot was taken by an older version on a UEFI VM and cannot be " "reverted; delete it instead" @@ -9229,7 +9241,7 @@ msgstr "" "Bu anlık görüntü UEFI bir sanal makinede eski bir sürümle alındı ve geri " "dönülemez; bunun yerine silin" -#: vrtManager/instance.py:2023 +#: vrtManager/instance.py:2011 #, python-format msgid "" "The revert failed twice; try again in a moment (%(error)s). The VM is " @@ -9238,7 +9250,7 @@ msgstr "" "Geri dönüş iki kez başarısız oldu; biraz sonra tekrar deneyin (%(error)s). " "Sanal makine çalışıyor." -#: vrtManager/instance.py:2025 +#: vrtManager/instance.py:2013 #, python-format msgid "" "The revert failed twice; try again in a moment (%(error)s). The VM is shut " @@ -9247,13 +9259,13 @@ msgstr "" "Geri dönüş iki kez başarısız oldu; biraz sonra tekrar deneyin (%(error)s). " "Sanal makine kapalı." -#: vrtManager/instance.py:2188 +#: vrtManager/instance.py:2176 msgid "A disk moves when its VM is defined and running, paused or shut off" msgstr "" "Bir disk, sanal makinesi tanımlıyken ve çalışırken, duraklatılmışken veya " "kapalıyken taşınır" -#: vrtManager/instance.py:2190 +#: vrtManager/instance.py:2178 msgid "" "A disk of a VM with snapshots, a saved state or a running block job does not " "move" @@ -9261,7 +9273,7 @@ msgstr "" "Anlık görüntüleri, kaydedilmiş durumu veya çalışan bir blok işi olan sanal " "makinenin diski taşınmaz" -#: vrtManager/instance.py:2198 +#: vrtManager/instance.py:2186 #, python-format msgid "" "%(dev)s is not a disk file of the VM, the same in its running and next " @@ -9270,132 +9282,132 @@ msgstr "" "%(dev)s, sanal makinenin çalışan ve sonraki başlatma tanımlarında aynı olan " "bir disk dosyası değil" -#: vrtManager/instance.py:2203 +#: vrtManager/instance.py:2191 #, python-format msgid "%(dev)s does not move: only writable raw and qcow2 disks do" msgstr "%(dev)s taşınmaz: yalnızca yazılabilir raw ve qcow2 diskler taşınır" -#: vrtManager/instance.py:2210 +#: vrtManager/instance.py:2198 #, python-format msgid "%(dev)s does not move: no storage pool has %(path)s" msgstr "%(dev)s taşınmaz: hiçbir depolama havuzunda %(path)s yok" -#: vrtManager/instance.py:2212 +#: vrtManager/instance.py:2200 #, python-format msgid "%(dev)s does not move: it has a backing file" msgstr "%(dev)s taşınmaz: temel (backing) dosyası var" -#: vrtManager/instance.py:2215 +#: vrtManager/instance.py:2203 #, python-format msgid "%(pool_name)s is not an active pool of disk files of this host" msgstr "%(pool_name)s bu sunucunun etkin bir disk dosyası havuzu değil" -#: vrtManager/instance.py:2218 +#: vrtManager/instance.py:2206 #, python-format msgid "%(dev)s is in pool %(pool_name)s already" msgstr "%(dev)s zaten %(pool_name)s havuzunda" -#: vrtManager/instance.py:2222 +#: vrtManager/instance.py:2210 #, python-format msgid "Pool %(pool_name)s already has a file %(name)s" msgstr "%(pool_name)s havuzunda zaten %(name)s dosyası var" -#: vrtManager/instance.py:2246 +#: vrtManager/instance.py:2234 #, python-format msgid "The copy of %(dev)s ended before it was complete" msgstr "%(dev)s kopyası tamamlanmadan sona erdi" -#: vrtManager/instance.py:2291 +#: vrtManager/instance.py:2279 #, python-format msgid "%(error)s; remove %(dest)s if it is there: %(cleanup_error)s" msgstr "%(error)s; varsa %(dest)s dosyasını kaldırın: %(cleanup_error)s" -#: vrtManager/instance.py:2293 +#: vrtManager/instance.py:2281 #, python-format msgid "%(error)s; the VM may use %(dest)s now: check its disk %(dev)s" msgstr "" "%(error)s; sanal makine şu anda %(dest)s dosyasını kullanıyor olabilir: " "%(dev)s diskini kontrol edin" -#: vrtManager/instance.py:2300 +#: vrtManager/instance.py:2288 #, python-format msgid "The VM has no internal snapshot named %(name)s" msgstr "Sanal makinenin %(name)s adlı dahili anlık görüntüsü yok" -#: vrtManager/instance.py:2309 +#: vrtManager/instance.py:2297 msgid "The VM has a saved state; start it or remove the saved state first" msgstr "" "Sanal makinenin kaydedilmiş durumu var; önce başlatın veya kaydedilmiş durumu" " kaldırın" -#: vrtManager/instance.py:2311 +#: vrtManager/instance.py:2299 msgid "A snapshot job is still running on this VM; try again when it ends" msgstr "" "Bu sanal makinede hâlâ bir anlık görüntü işi çalışıyor; bittiğinde tekrar " "deneyin" -#: vrtManager/instance.py:2321 +#: vrtManager/instance.py:2309 msgid "Shut the VM down to revert to an internal snapshot" msgstr "Dahili bir anlık görüntüye geri dönmek için sanal makineyi kapatın" -#: vrtManager/instance.py:2415 +#: vrtManager/instance.py:2403 msgid "Cannot clone a VM whose NVRAM is not a file" msgstr "NVRAM'i dosya olmayan bir sanal makine klonlanamaz" -#: vrtManager/instance.py:2483 +#: vrtManager/instance.py:2471 #, python-format msgid "Clone failed (%(err)s); remove these copies by hand: %(path)s" msgstr "" "Klonlama başarısız oldu (%(err)s); bu kopyaları elle kaldırın: %(path)s" -#: vrtManager/instance.py:2493 +#: vrtManager/instance.py:2481 msgid "Shut the VM down before cloning it" msgstr "Klonlamadan önce sanal makineyi kapatın" -#: vrtManager/instance.py:2547 +#: vrtManager/instance.py:2535 #, python-format msgid "Clone failed (%(err)s); remove these copies by hand: %(leftovers)s" msgstr "" "Klonlama başarısız oldu (%(err)s); bu kopyaları elle kaldırın: %(leftovers)s" -#: vrtManager/instance.py:2558 +#: vrtManager/instance.py:2546 msgid "Live clone needs libvirt 11.10 or later; shut the VM down to clone it" msgstr "" "Canlı klonlama libvirt 11.10 veya sonrasını gerektirir; klonlamak için sanal " "makineyi kapatın" -#: vrtManager/instance.py:2561 +#: vrtManager/instance.py:2549 msgid "A VM with a TPM is cloned only shut down" msgstr "TPM'i olan bir sanal makine yalnızca kapalıyken klonlanır" -#: vrtManager/instance.py:2564 +#: vrtManager/instance.py:2552 #, python-format msgid "Disk %(dev)s is not a local file; shut the VM down to clone it" msgstr "" "Disk %(dev)s yerel bir dosya değil; klonlamak için sanal makineyi kapatın" -#: vrtManager/instance.py:2566 +#: vrtManager/instance.py:2554 #, python-format msgid "Disk %(dev)s is shareable; shut the VM down to clone it" msgstr "Disk %(dev)s paylaşılabilir; klonlamak için sanal makineyi kapatın" -#: vrtManager/instance.py:2571 +#: vrtManager/instance.py:2559 #, python-format msgid "Disk %(dev)s changes at the next start; shut the VM down to clone it" msgstr "" "Disk %(dev)s sonraki başlatmada değişiyor; klonlamak için sanal makineyi " "kapatın" -#: vrtManager/instance.py:2573 +#: vrtManager/instance.py:2561 msgid "Another job is running on the VM; clone it when that ends" msgstr "Sanal makinede başka bir iş çalışıyor; o bittiğinde klonlayın" -#: vrtManager/instance.py:2586 +#: vrtManager/instance.py:2574 #, python-format msgid "Volume %(name)s already exists in storage pool %(pool)s" msgstr "%(name)s birimi %(pool)s depolama havuzunda zaten var" -#: vrtManager/instance.py:2605 +#: vrtManager/instance.py:2593 #, python-format msgid "" "Storage pool %(pool)s has %(free).1f GiB free; the clone needs %(size).1f GiB" @@ -9405,20 +9417,20 @@ msgstr "" "gerektiriyor (kopyalanan diskler, artı bunların %%%(percent)g kadarı, en az " "%(minimum)g GiB)" -#: vrtManager/instance.py:2744 +#: vrtManager/instance.py:2732 msgid "A network interface that is a host device is removed in the XML editor" msgstr "Sunucu aygıtı olan bir ağ arabirimi XML düzenleyicide kaldırılır" -#: vrtManager/instance.py:2760 +#: vrtManager/instance.py:2748 #, python-format msgid "No network interface with MAC %(old_mac)s" msgstr "MAC adresi %(old_mac)s olan ağ arabirimi yok" -#: vrtManager/instance.py:2763 +#: vrtManager/instance.py:2751 msgid "A network interface that is a host device is changed in the XML editor" msgstr "Sunucu aygıtı olan bir ağ arabirimi XML düzenleyicide değiştirilir" -#: vrtManager/instance.py:2854 +#: vrtManager/instance.py:2842 msgid "" "A VM changes its PCI devices while it runs, or is shut off without a saved " "state" @@ -9426,14 +9438,14 @@ msgstr "" "Sanal makine PCI aygıtlarını çalışırken veya kaydedilmiş durumu olmadan " "kapalıyken değiştirir" -#: vrtManager/instance.py:2866 +#: vrtManager/instance.py:2854 #, python-format msgid "%(err)s; whether the VM has PCI device %(address)s now is not known" msgstr "" "%(err)s; sanal makinede şu anda %(address)s PCI aygıtının olup olmadığı " "bilinmiyor" -#: vrtManager/instance.py:2868 +#: vrtManager/instance.py:2856 #, python-format msgid "" "%(err)s; PCI device %(address)s: the VM is not running, its next start has it" @@ -9441,7 +9453,7 @@ msgstr "" "%(err)s; PCI aygıtı %(address)s: sanal makine çalışmıyor, sonraki başlatmada " "var" -#: vrtManager/instance.py:2870 +#: vrtManager/instance.py:2858 #, python-format msgid "" "%(err)s; PCI device %(address)s: the VM is not running, its next start does " @@ -9450,7 +9462,7 @@ msgstr "" "%(err)s; PCI aygıtı %(address)s: sanal makine çalışmıyor, sonraki başlatmada " "yok" -#: vrtManager/instance.py:2871 +#: vrtManager/instance.py:2859 #, python-format msgid "" "%(err)s; PCI device %(address)s: the running VM has it, its next start has it" @@ -9458,7 +9470,7 @@ msgstr "" "%(err)s; PCI aygıtı %(address)s: çalışan sanal makinede var, sonraki " "başlatmada var" -#: vrtManager/instance.py:2873 +#: vrtManager/instance.py:2861 #, python-format msgid "" "%(err)s; PCI device %(address)s: the running VM has it, its next start does " @@ -9467,7 +9479,7 @@ msgstr "" "%(err)s; PCI aygıtı %(address)s: çalışan sanal makinede var, sonraki " "başlatmada yok" -#: vrtManager/instance.py:2875 +#: vrtManager/instance.py:2863 #, python-format msgid "" "%(err)s; PCI device %(address)s: the running VM does not have it, its next " @@ -9476,7 +9488,7 @@ msgstr "" "%(err)s; PCI aygıtı %(address)s: çalışan sanal makinede yok, sonraki " "başlatmada var" -#: vrtManager/instance.py:2877 +#: vrtManager/instance.py:2865 #, python-format msgid "" "%(err)s; PCI device %(address)s: the running VM does not have it, its next " @@ -9485,29 +9497,29 @@ msgstr "" "%(err)s; PCI aygıtı %(address)s: çalışan sanal makinede yok, sonraki " "başlatmada yok" -#: vrtManager/instance.py:2911 +#: vrtManager/instance.py:2899 #, python-format msgid "The VM has no PCI device %(address)s" msgstr "Sanal makinede %(address)s PCI aygıtı yok" -#: vrtManager/instance.py:2929 +#: vrtManager/instance.py:2917 msgid "The VM has no USB controller" msgstr "Sanal makinede USB denetleyicisi yok" -#: vrtManager/instance.py:2948 +#: vrtManager/instance.py:2936 msgid "The VM has no such USB device" msgstr "Sanal makinede böyle bir USB aygıtı yok" -#: vrtManager/instance.py:2989 +#: vrtManager/instance.py:2977 msgid "The VM has its own RNG device: change it in the XML editor" msgstr "" "Sanal makinenin kendi RNG aygıtı var: bunu XML düzenleyicide değiştirin" -#: vrtManager/instance.py:3016 +#: vrtManager/instance.py:3004 msgid "The VM has its own TPM: change it in the XML editor" msgstr "Sanal makinenin kendi TPM'i var: bunu XML düzenleyicide değiştirin" -#: vrtManager/instance.py:3035 +#: vrtManager/instance.py:3023 #, python-format msgid "Unknown crash action %(action)s" msgstr "Bilinmeyen çökme eylemi %(action)s" diff --git a/locale/zh/LC_MESSAGES/django.mo b/locale/zh/LC_MESSAGES/django.mo index e12ef6f0..f761599c 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 923ce01b..937295e8 100644 --- a/locale/zh/LC_MESSAGES/django.po +++ b/locale/zh/LC_MESSAGES/django.po @@ -281,7 +281,7 @@ msgstr "您在此处保持登录;您的其他会话将结束。" #: instances/templates/add_instance_volume.html:108 #: instances/templates/create_instance_w1.html:40 #: instances/templates/create_instance_w2.html:355 -#: instances/templates/instances/destroy_instance_form.html:51 +#: instances/templates/instances/destroy_instance_form.html:52 #: instances/templates/instances/edit_instance_volume.html:111 #: instances/templates/instances/move_disk.html:27 #: instances/templates/instances/settings_tab.html:411 @@ -895,7 +895,7 @@ msgid "IP" msgstr "IP" #: admin/templates/admin/logs.html:56 admin/views.py:286 instances/states.py:19 -#: instances/views.py:419 instances/views.py:421 logs/views.py:17 +#: instances/views.py:415 instances/views.py:417 logs/views.py:17 msgid "Unknown" msgstr "未知" @@ -1660,7 +1660,7 @@ msgid "New VM defaults" msgstr "新建虚拟机默认值" #: appsettings/views.py:40 instances/templates/instance.html:64 -#: instances/templates/instances/destroy_instance_form.html:32 +#: instances/templates/instances/destroy_instance_form.html:33 #: instances/templates/instances/resize_tab.html:20 #: instances/templates/instances/settings_tab.html:197 msgid "Disks" @@ -2609,354 +2609,381 @@ msgstr "控制台监听的不只是 localhost;只有管理员可以移除其 msgid "The name of the virtual machine must not contain any special characters" msgstr "虚拟机名称不能包含任何特殊字符" -#: instances/jobs.py:69 +#: instances/jobs.py:72 msgid "The VM moved to another compute after the job was queued" msgstr "任务排队后,虚拟机已移到另一个计算节点" -#: instances/jobs.py:137 +#: instances/jobs.py:140 msgid "Enter a whole number of vCPUs for the clone" msgstr "请为克隆输入整数个 vCPU" -#: instances/jobs.py:139 +#: instances/jobs.py:142 msgid "Enter a whole number of memory (MiB) for the clone" msgstr "请为克隆输入整数的内存(MiB)" -#: instances/jobs.py:166 +#: instances/jobs.py:169 msgid "Choose the clone's name from the list of free names" msgstr "请从可用名称列表中选择克隆的名称" -#: instances/jobs.py:194 +#: instances/jobs.py:197 #, python-format msgid "%(quota)s: cannot create '%(clone_name)s'." msgstr "%(quota)s:无法创建“%(clone_name)s”。" -#: instances/jobs.py:197 instances/jobs.py:549 +#: instances/jobs.py:200 instances/jobs.py:552 #, python-format msgid "Instance '%(clone_name)s' already exists!" msgstr "虚拟机 '%(clone_name)s' 已存在!" -#: instances/jobs.py:199 +#: instances/jobs.py:202 #, python-format msgid "Instance name '%(clone_name)s' contains invalid characters!" msgstr "虚拟机名称 '%(clone_name)s' 包含无效字符!" -#: instances/jobs.py:202 +#: instances/jobs.py:205 #, python-format msgid "Instance MAC '%(clone_mac)s' invalid format!" msgstr "虚拟机 MAC '%(clone_mac)s' 格式无效!" -#: instances/jobs.py:204 +#: instances/jobs.py:207 #, python-format msgid "Disk name '%(disk_name)s' contains invalid characters!" msgstr "磁盘名称 '%(disk_name)s' 包含无效字符!" -#: instances/jobs.py:224 +#: instances/jobs.py:227 #, python-format msgid "The clone is made, but not as asked: %(error)s" msgstr "克隆已创建,但与要求不符:%(error)s" -#: instances/jobs.py:280 +#: instances/jobs.py:283 msgid "The VM was not defined and no volume of it was left." msgstr "虚拟机未定义,也没有留下它的卷。" -#: instances/jobs.py:282 +#: instances/jobs.py:285 #, python-format msgid "" "The VM was not defined; check these volumes, then resolve this job: " "%(volumes)s" msgstr "虚拟机未定义;请检查这些卷,然后处理此任务:%(volumes)s" -#: instances/jobs.py:303 +#: instances/jobs.py:306 msgid "Delete snapshot" msgstr "删除快照" -#: instances/jobs.py:313 vrtManager/instance.py:2299 +#: instances/jobs.py:316 vrtManager/instance.py:2287 #, python-format msgid "The VM has no external snapshot named %(name)s" msgstr "虚拟机没有名为 %(name)s 的外部快照" -#: instances/jobs.py:326 +#: instances/jobs.py:329 msgid "Merging the snapshot" msgstr "正在合并快照" -#: instances/jobs.py:345 +#: instances/jobs.py:348 msgid "The VM is no longer on its compute" msgstr "虚拟机已不在其计算节点上" -#: instances/jobs.py:348 +#: instances/jobs.py:351 msgid "libvirt is still merging the snapshot" msgstr "libvirt 仍在合并快照" -#: instances/jobs.py:356 +#: instances/jobs.py:359 msgid "" "The snapshot is still listed and no merge runs. It cannot be told whether the" " disks changed; check the VM before deleting the snapshot again or reverting " "to it." msgstr "快照仍在列表中,且没有正在运行的合并。无法判断磁盘是否已更改;再次删除该快照或恢复到它之前,请检查虚拟机。" -#: instances/jobs.py:363 +#: instances/jobs.py:366 msgid "The VM is no longer on the compute the job ran on" msgstr "虚拟机已不在运行该任务的计算节点上" -#: instances/jobs.py:369 +#: instances/jobs.py:372 msgid "Take snapshot" msgstr "创建快照" -#: instances/jobs.py:381 instances/jobs.py:390 vrtManager/instance.py:1895 +#: instances/jobs.py:384 instances/jobs.py:393 vrtManager/instance.py:1883 #, python-format msgid "A snapshot named %(name)s already exists" msgstr "名为 %(name)s 的快照已存在" -#: instances/jobs.py:396 +#: instances/jobs.py:399 msgid "Taking the snapshot" msgstr "正在创建快照" -#: instances/jobs.py:409 +#: instances/jobs.py:412 msgid "libvirt is still taking the snapshot" msgstr "libvirt 仍在创建快照" -#: instances/jobs.py:412 +#: instances/jobs.py:415 msgid "The snapshot was not taken" msgstr "未创建快照" -#: instances/jobs.py:435 +#: instances/jobs.py:438 msgid "Revert snapshot" msgstr "恢复快照" -#: instances/jobs.py:466 +#: instances/jobs.py:469 msgid "Reverting the VM" msgstr "正在恢复虚拟机" -#: instances/jobs.py:477 +#: instances/jobs.py:480 msgid "libvirt is still reverting the VM" msgstr "libvirt 仍在恢复虚拟机" -#: instances/jobs.py:481 +#: instances/jobs.py:484 msgid "" "It cannot be told whether the VM was reverted; check it, then revert again or" " resolve this job." msgstr "无法判断虚拟机是否已恢复;请检查后再次恢复,或处理此任务。" -#: instances/jobs.py:498 +#: instances/jobs.py:501 msgid "The VM must be running, paused or shut off to be cloned" msgstr "虚拟机必须处于运行、暂停或关机状态才能克隆" -#: instances/jobs.py:499 +#: instances/jobs.py:502 #, python-format msgid "" "The guest's file systems could not be thawed (%(error)s); thaw them with: " "virsh domfsthaw %(vm)s" msgstr "无法解冻客户机的文件系统(%(error)s);请用以下命令解冻:virsh domfsthaw %(vm)s" -#: instances/jobs.py:518 +#: instances/jobs.py:521 msgid "Clone VM" msgstr "克隆虚拟机" -#: instances/jobs.py:559 instances/jobs.py:566 instances/jobs.py:841 +#: instances/jobs.py:562 instances/jobs.py:569 instances/jobs.py:844 msgid "Copying disks" msgstr "正在复制磁盘" -#: instances/jobs.py:618 +#: instances/jobs.py:621 msgid "libvirt has no result of the copy" msgstr "libvirt 没有复制的结果" -#: instances/jobs.py:629 +#: instances/jobs.py:632 #, python-format msgid "" "The copy failed: %(error)s. The guest's file systems could not be thawed " "(%(thaw)s); thaw them with: virsh domfsthaw %(vm)s" msgstr "复制失败:%(error)s。无法解冻客户机的文件系统(%(thaw)s);请用以下命令解冻:virsh domfsthaw %(vm)s" -#: instances/jobs.py:632 instances/jobs.py:716 +#: instances/jobs.py:635 instances/jobs.py:719 msgid "The copy failed" msgstr "复制失败" -#: instances/jobs.py:657 +#: instances/jobs.py:660 #, python-format msgid "" "The VM is no longer on the compute the job ran on; check these volumes, then " "resolve this job: %(volumes)s" msgstr "虚拟机已不在运行该任务的计算节点上;请检查这些卷,然后处理此任务:%(volumes)s" -#: instances/jobs.py:692 +#: instances/jobs.py:695 #, python-format msgid "" "A volume of the clone was being created; check these volumes, then resolve " "this job: %(volumes)s" msgstr "克隆的一个卷正在创建中;请检查这些卷,然后处理此任务:%(volumes)s" -#: instances/jobs.py:696 +#: instances/jobs.py:699 msgid "The job stopped before the copy started" msgstr "任务在复制开始前停止" -#: instances/jobs.py:703 +#: instances/jobs.py:706 msgid "libvirt is still copying the disks" msgstr "libvirt 仍在复制磁盘" -#: instances/jobs.py:706 +#: instances/jobs.py:709 #, python-format msgid "" "Another backup runs on the VM; check these volumes, then resolve this job: " "%(volumes)s" msgstr "虚拟机上正在运行另一个备份;请检查这些卷,然后处理此任务:%(volumes)s" -#: instances/jobs.py:711 +#: instances/jobs.py:714 #, python-format msgid "" "libvirt has no result of the copy; check these volumes, then resolve this " "job: %(volumes)s" msgstr "libvirt 没有复制的结果;请检查这些卷,然后处理此任务:%(volumes)s" -#: instances/jobs.py:723 +#: instances/jobs.py:726 #, python-format msgid "" "The disks were copied, but the clone could not be defined: %(error)s; check " "these volumes, then resolve this job: %(volumes)s" msgstr "磁盘已复制,但无法定义克隆:%(error)s;请检查这些卷,然后处理此任务:%(volumes)s" -#: instances/jobs.py:746 instances/templates/create_instance_w1.html:60 +#: instances/jobs.py:749 instances/templates/create_instance_w1.html:60 #: instances/templates/create_instance_w2.html:356 msgid "Create VM" msgstr "创建虚拟机" -#: instances/jobs.py:770 instances/jobs.py:815 instances/views.py:2504 +#: instances/jobs.py:773 instances/jobs.py:818 instances/views.py:2459 msgid "A virtual machine with this name already exists" msgstr "同名虚拟机已存在" -#: instances/jobs.py:772 instances/jobs.py:812 +#: instances/jobs.py:775 instances/jobs.py:815 msgid "There is an instance with same name. Remove it and try again!" msgstr "已存在同名虚拟机。请删除后重试!" -#: instances/jobs.py:775 +#: instances/jobs.py:778 msgid "First you need to create or select an image" msgstr "请先创建或选择一个镜像" -#: instances/jobs.py:777 +#: instances/jobs.py:780 msgid "Choose one disk source: a new disk, existing volumes or a template" msgstr "请选择一种磁盘来源:新磁盘、现有卷或模板" -#: instances/jobs.py:779 +#: instances/jobs.py:782 msgid "Image has already exist. Please check volumes or change instance name" msgstr "镜像已存在。请检查卷或更改虚拟机名称" -#: instances/jobs.py:781 +#: instances/jobs.py:784 msgid "Invalid cache mode" msgstr "无效的缓存模式" -#: instances/jobs.py:783 +#: instances/jobs.py:786 #, python-format msgid "This host offers no CPU mode or model %(mode)s" msgstr "此主机不提供 CPU 模式或型号 %(mode)s" -#: instances/jobs.py:785 +#: instances/jobs.py:788 #, python-format msgid "A CD-ROM cannot use the %(bus)s bus" msgstr "CD-ROM 不能使用 %(bus)s 总线" -#: instances/jobs.py:788 +#: instances/jobs.py:791 msgid "The cloud-init seed is read from a CD-ROM: choose its bus" msgstr "cloud-init 种子从 CD-ROM 读取:请选择其总线" -#: instances/jobs.py:791 +#: instances/jobs.py:794 msgid "The cloud-init seed needs a directory storage pool for the disk" msgstr "cloud-init 种子需要为磁盘使用目录类型的存储池" -#: instances/jobs.py:797 +#: instances/jobs.py:800 #, python-format msgid "This host offers no firmware %(firmware)s" msgstr "此主机不提供固件 %(firmware)s" -#: instances/jobs.py:799 vrtManager/instance.py:3023 +#: instances/jobs.py:802 vrtManager/instance.py:3011 msgid "This host offers no TPM 2.0: install swtpm on it" msgstr "此主机不提供 TPM 2.0:请在主机上安装 swtpm" -#: instances/jobs.py:860 +#: instances/jobs.py:863 msgid "Writing the cloud-init seed" msgstr "正在写入 cloud-init 种子" -#: instances/jobs.py:867 +#: instances/jobs.py:870 msgid "Defining the VM" msgstr "正在定义虚拟机" -#: instances/jobs.py:900 +#: instances/jobs.py:903 #, python-format msgid "%(error)s. Remove these volumes by hand: %(volumes)s" msgstr "%(error)s。请手动删除这些卷:%(volumes)s" -#: instances/jobs.py:925 +#: instances/jobs.py:928 #, python-format msgid "The instance is already on %(compute)s" msgstr "虚拟机已在 %(compute)s 上" -#: instances/jobs.py:927 +#: instances/jobs.py:930 #, python-format msgid "%(compute)s is not reachable" msgstr "无法连接 %(compute)s" -#: instances/jobs.py:929 +#: instances/jobs.py:932 #, python-format msgid "%(compute)s is the VM's own host under another name" msgstr "%(compute)s 是该虚拟机所在主机的另一个名称" -#: instances/jobs.py:963 +#: instances/jobs.py:966 #, python-format msgid "" "The VM migrated, but autostart could not be set on the destination: %(error)s" msgstr "虚拟机已迁移,但无法在目标主机上设置自动启动:%(error)s" -#: instances/jobs.py:969 +#: instances/jobs.py:972 #, python-format msgid "The VM migrated, but its NVRAM file stays on the source: %(error)s" msgstr "虚拟机已迁移,但其 NVRAM 文件仍留在源主机上:%(error)s" -#: instances/jobs.py:976 +#: instances/jobs.py:979 msgid "Migrate VM" msgstr "迁移虚拟机" -#: instances/jobs.py:1023 +#: instances/jobs.py:1026 msgid "Migrating the VM" msgstr "正在迁移虚拟机" -#: instances/jobs.py:1070 +#: instances/jobs.py:1073 msgid "libvirt is still migrating the VM" msgstr "libvirt 仍在迁移虚拟机" -#: instances/jobs.py:1079 +#: instances/jobs.py:1084 msgid "The VM stayed on the source host" msgstr "虚拟机留在了源主机上" -#: instances/jobs.py:1081 +#: instances/jobs.py:1086 msgid "" "The VM is on both computes or on neither; check them, then resolve this job." msgstr "虚拟机同时位于两个计算节点上,或都不在;请检查后处理此任务。" -#: instances/jobs.py:1129 +#: instances/jobs.py:1134 msgid "Move disk" msgstr "移动磁盘" -#: instances/jobs.py:1136 +#: instances/jobs.py:1141 msgid "Choose a disk of the VM" msgstr "请选择虚拟机的一个磁盘" -#: instances/jobs.py:1160 +#: instances/jobs.py:1165 msgid "Copying the disk" msgstr "正在复制磁盘" -#: instances/jobs.py:1203 +#: instances/jobs.py:1208 #, python-format msgid "Another block job runs on %(dev)s; check the VM" msgstr "%(dev)s 上正在运行另一个块任务;请检查虚拟机" -#: instances/jobs.py:1209 +#: instances/jobs.py:1214 #, python-format msgid "The disk stayed in %(path)s; remove %(dest)s if it is there" msgstr "磁盘仍在 %(path)s;如果 %(dest)s 存在,请将其删除" -#: instances/jobs.py:1211 +#: instances/jobs.py:1216 #, python-format msgid "The definitions of %(dev)s name %(paths)s; check the VM" msgstr "%(dev)s 的定义指向 %(paths)s;请检查虚拟机" +#: instances/jobs.py:1223 +#, python-format +msgid "Disk %(path)s was kept: a disk file that stays needs it" +msgstr "磁盘 %(path)s 已保留:某个保留的磁盘文件依赖它" + +#: instances/jobs.py:1224 +#, python-format +msgid "The VM is removed, but disk %(path)s was not deleted: %(err)s" +msgstr "虚拟机已删除,但磁盘 %(path)s 未被删除:%(err)s" + +#: instances/jobs.py:1225 +#, python-format +msgid "Disk %(path)s is used by another VM and was kept" +msgstr "磁盘 %(path)s 正被另一台虚拟机使用,已保留" + +#: instances/jobs.py:1279 +msgid "Delete VM" +msgstr "删除虚拟机" + +#: instances/jobs.py:1309 +msgid "Deleting the VM" +msgstr "正在删除虚拟机" + +#: instances/jobs.py:1321 +msgid "The VM was not deleted; it may have been shut off" +msgstr "虚拟机未被删除;它可能已被关闭" + #: instances/models.py:18 msgid "label" msgstr "标签" @@ -3180,7 +3207,7 @@ msgid "Disk source" msgstr "磁盘来源" #: instances/templates/add_instance_volume.html:21 -#: instances/templates/create_instance_w2.html:134 instances/views.py:2523 +#: instances/templates/create_instance_w2.html:134 instances/views.py:2478 msgid "New disk" msgstr "新磁盘" @@ -3544,7 +3571,7 @@ msgstr "自定义 CPU 型号" msgid "Size (GiB)" msgstr "大小(GiB)" -#: instances/templates/create_instance_w2.html:161 instances/views.py:2524 +#: instances/templates/create_instance_w2.html:161 instances/views.py:2479 msgid "Existing volumes" msgstr "已有卷" @@ -3580,7 +3607,7 @@ msgstr "设备" msgid "Remove" msgstr "移除" -#: instances/templates/create_instance_w2.html:194 instances/views.py:2525 +#: instances/templates/create_instance_w2.html:194 instances/views.py:2480 msgid "Clone from template" msgstr "从模板克隆" @@ -3913,72 +3940,72 @@ msgstr "将 %(kind)s %(name)s 上移" msgid "Move %(kind)s %(name)s down" msgstr "将 %(kind)s %(name)s 下移" -#: instances/templates/instances/destroy_instance_form.html:3 -#: instances/templates/instances/destroy_instance_form.html:6 +#: instances/templates/instances/destroy_instance_form.html:4 +#: instances/templates/instances/destroy_instance_form.html:7 #, python-format msgid "Destroy %(name)s" msgstr "销毁 %(name)s" -#: instances/templates/instances/destroy_instance_form.html:6 +#: instances/templates/instances/destroy_instance_form.html:7 #, python-format msgid "on %(host)s" msgstr "位于 %(host)s" -#: instances/templates/instances/destroy_instance_form.html:10 +#: instances/templates/instances/destroy_instance_form.html:11 msgid "" "The VM's definition, saved state and snapshot records are removed. A running " "VM is forced off first; what the guest has not written to disk is lost." msgstr "虚拟机的定义、已保存状态和快照记录将被删除。运行中的虚拟机会先被强制关闭;客户机尚未写入磁盘的数据将丢失。" -#: instances/templates/instances/destroy_instance_form.html:13 +#: instances/templates/instances/destroy_instance_form.html:14 msgid "What should happen to the files?" msgstr "这些文件如何处理?" -#: instances/templates/instances/destroy_instance_form.html:18 +#: instances/templates/instances/destroy_instance_form.html:19 msgid "Delete disk files" msgstr "删除磁盘文件" -#: instances/templates/instances/destroy_instance_form.html:19 +#: instances/templates/instances/destroy_instance_form.html:20 msgid "" "With them go this VM's external snapshot files, snapshot memory files and " "cloud-init seed." msgstr "同时删除此虚拟机的外部快照文件、快照内存文件和 cloud-init 种子。" -#: instances/templates/instances/destroy_instance_form.html:25 +#: instances/templates/instances/destroy_instance_form.html:26 msgid "Keep the disk files" msgstr "保留磁盘文件" -#: instances/templates/instances/destroy_instance_form.html:26 +#: instances/templates/instances/destroy_instance_form.html:27 msgid "" "They stay with their internal snapshot data; the snapshot records still go " "with the VM." msgstr "它们连同内部快照数据一起保留;快照记录仍随虚拟机删除。" -#: instances/templates/instances/destroy_instance_form.html:35 +#: instances/templates/instances/destroy_instance_form.html:36 msgid "no file" msgstr "无文件" -#: instances/templates/instances/destroy_instance_form.html:39 +#: instances/templates/instances/destroy_instance_form.html:40 msgid "" "A file another VM uses on any of the panel's hosts, directly or under its own" " disk, is kept, and so is the template a disk was made from. If a host cannot" " be checked, the VM is not destroyed." msgstr "面板任一主机上其他虚拟机直接使用或作为其磁盘底层使用的文件会被保留,磁盘所基于的模板也会保留。如果某台主机无法检查,则不会销毁该虚拟机。" -#: instances/templates/instances/destroy_instance_form.html:43 +#: instances/templates/instances/destroy_instance_form.html:44 msgid "Delete the NVRAM file (firmware settings). Untick to keep it." msgstr "删除 NVRAM 文件(固件设置)。取消勾选则保留。" -#: instances/templates/instances/destroy_instance_form.html:47 +#: instances/templates/instances/destroy_instance_form.html:48 msgid "" "Its TPM state is deleted with it, and the keys in it, such as BitLocker's." msgstr "其中的 TPM 状态及其中的密钥(例如 BitLocker 的密钥)也会一并删除。" -#: instances/templates/instances/destroy_instance_form.html:50 +#: instances/templates/instances/destroy_instance_form.html:51 msgid "This cannot be undone." msgstr "此操作无法撤销。" -#: instances/templates/instances/destroy_instance_form.html:52 +#: instances/templates/instances/destroy_instance_form.html:53 msgid "Destroy VM" msgstr "销毁虚拟机" @@ -5586,58 +5613,58 @@ msgstr "无效的 MAC 地址" msgid "More MAC addresses than networks" msgstr "MAC 地址数量多于网络数量" -#: instances/views.py:173 +#: instances/views.py:169 msgid "Enter an IP address, a network such as 192.0.2.0/24, or a MAC address." msgstr "请输入 IP 地址、网络(如 192.0.2.0/24)或 MAC 地址。" -#: instances/views.py:179 instances/views.py:213 +#: instances/views.py:175 instances/views.py:209 msgid "Too many searches are still running; try again shortly." msgstr "仍在运行的搜索过多;请稍后再试。" -#: instances/views.py:261 +#: instances/views.py:257 #, python-format msgid "The host %(host)s of '%(name)s' cannot be reached: %(error)s" msgstr "无法连接“%(name)s”所在的主机 %(host)s:%(error)s" -#: instances/views.py:267 +#: instances/views.py:263 #, python-format msgid "The host of '%(name)s' cannot be reached right now." msgstr "目前无法连接“%(name)s”所在的主机。" -#: instances/views.py:268 +#: instances/views.py:264 msgid " Jobs on it can be followed under Jobs in the menu." msgstr " 其上的任务可在菜单的“任务”中跟踪。" -#: instances/views.py:276 +#: instances/views.py:272 #, python-format msgid "'%(name)s' is no longer on its host." msgstr "“%(name)s”已不在其主机上。" -#: instances/views.py:374 +#: instances/views.py:370 msgid "There is no other host to migrate to." msgstr "没有其他可迁移到的主机。" -#: instances/views.py:376 +#: instances/views.py:372 msgid "A VM migrates when it is running, paused or shut off." msgstr "虚拟机在运行、暂停或关机状态下均可迁移。" -#: instances/views.py:378 +#: instances/views.py:374 msgid "" "A VM with snapshots cannot be migrated: its snapshots stay on this host. " "Delete them first." msgstr "有快照的虚拟机无法迁移:其快照保留在本主机上。请先删除快照。" -#: instances/views.py:380 +#: instances/views.py:376 msgid "" "A VM with host devices cannot be migrated: the destination may not have them." " Remove them first." msgstr "有主机设备的虚拟机无法迁移:目标主机可能没有这些设备。请先移除它们。" -#: instances/views.py:644 +#: instances/views.py:640 msgid "The VM was moved meanwhile; try again." msgstr "该虚拟机在此期间已被移动;请重试。" -#: instances/views.py:760 +#: instances/views.py:756 msgid "" "The running VM has no free PCI slot for another disk on this bus: choose the " "scsi bus if the VM has a SCSI controller, or shut the VM down and add the " @@ -5647,7 +5674,7 @@ msgstr "" "运行中的虚拟机在此总线上没有可用 PCI 插槽来添加磁盘:如果虚拟机有 SCSI 控制器,请选择 scsi " "总线;或关闭虚拟机后再添加磁盘。“磁盘”选项卡上的“添加 PCIe 插槽”可使其从下次启动起获得可用插槽。新卷会保留在其存储池中,可作为现有卷挂载。" -#: instances/views.py:764 +#: instances/views.py:760 msgid "" "The running VM has no free PCI slot for the SCSI controller the disk needs: " "shut the VM down and add the disk then; Add PCIe slots on the Disks tab gives" @@ -5657,277 +5684,262 @@ msgstr "" "运行中的虚拟机没有可用 PCI 插槽来添加磁盘所需的 SCSI 控制器:请关闭虚拟机后再添加磁盘。“磁盘”选项卡上的“添加 PCIe " "插槽”可使其从下次启动起获得可用插槽。新卷会保留在其存储池中,可作为现有卷挂载。" -#: instances/views.py:782 +#: instances/views.py:778 #, python-format msgid "Invalid disk options: %(options)s" msgstr "无效的磁盘选项:%(options)s" -#: instances/views.py:792 instances/views.py:807 +#: instances/views.py:788 instances/views.py:803 msgid "Templates cannot be started." msgstr "模板无法启动。" -#: instances/views.py:903 -#, python-format -msgid "Disk %(path)s was kept: a disk file that stays needs it" -msgstr "磁盘 %(path)s 已保留:某个保留的磁盘文件依赖它" - -#: instances/views.py:909 -#, python-format -msgid "The VM is removed, but disk %(path)s was not deleted: %(err)s" -msgstr "虚拟机已删除,但磁盘 %(path)s 未被删除:%(err)s" - -#: instances/views.py:918 -#, python-format -msgid "Disk %(path)s is used by another VM and was kept" -msgstr "磁盘 %(path)s 正被另一台虚拟机使用,已保留" - -#: instances/views.py:1040 +#: instances/views.py:995 #, python-format msgid "Set the gstfsd key of %(compute)s first (Computes, edit the host)" msgstr "请先设置 %(compute)s 的 gstfsd 密钥(计算节点,编辑该主机)" -#: instances/views.py:1047 instances/views.py:1065 instances/views.py:1074 +#: instances/views.py:1002 instances/views.py:1020 instances/views.py:1029 #, python-format msgid "gstfsd error: %(err)s" msgstr "gstfsd 错误:%(err)s" -#: instances/views.py:1075 +#: instances/views.py:1030 msgid "the reply is not signed with the key" msgstr "回复未使用该密钥签名" -#: instances/views.py:1083 +#: instances/views.py:1038 msgid "Shut the VM down, or start its QEMU guest agent, and try again" msgstr "请关闭虚拟机,或启动其 QEMU 客户机代理,然后重试" -#: instances/views.py:1096 +#: instances/views.py:1051 msgid "The change may still be made when the agent answers again." msgstr "当代理再次响应时,此更改仍可能生效。" -#: instances/views.py:1124 +#: instances/views.py:1079 msgid "Reset root password" msgstr "重置 root 密码" -#: instances/views.py:1128 +#: instances/views.py:1083 msgid "Enter a password" msgstr "请输入密码" -#: instances/views.py:1142 +#: instances/views.py:1097 msgid "Select an SSH public key" msgstr "请选择 SSH 公钥" -#: instances/views.py:1164 +#: instances/views.py:1119 #, python-format msgid "Installed new SSH public key %(keyname)s" msgstr "已安装新的 SSH 公钥 %(keyname)s" -#: instances/views.py:1184 +#: instances/views.py:1139 msgid "Enter a whole number of vCPUs" msgstr "请输入整数个 vCPU" -#: instances/views.py:1188 +#: instances/views.py:1143 msgid "Shut off the VM to change its vCPUs" msgstr "请关闭虚拟机以更改其 vCPU" -#: instances/views.py:1191 +#: instances/views.py:1146 msgid "The current vCPUs cannot be more than the maximum" msgstr "当前 vCPU 数不能超过最大值" -#: instances/views.py:1198 +#: instances/views.py:1153 #, python-format msgid "%(quota)s: cannot resize the CPU of '%(instance_name)s'." msgstr "%(quota)s:无法调整“%(instance_name)s”的 CPU。" -#: instances/views.py:1206 +#: instances/views.py:1161 #, python-format msgid "vCPUs set to %(cur)s (maximum %(max)s)" msgstr "vCPU 已设为 %(cur)s(最大 %(max)s)" -#: instances/views.py:1225 +#: instances/views.py:1180 msgid "Enter a whole number for memory" msgstr "请输入整数内存值" -#: instances/views.py:1228 +#: instances/views.py:1183 msgid "The current memory cannot be more than the maximum" msgstr "当前内存不能超过最大值" -#: instances/views.py:1232 +#: instances/views.py:1187 #, python-format msgid "%(quota)s: cannot resize the memory of '%(instance_name)s'." msgstr "%(quota)s:无法调整“%(instance_name)s”的内存。" -#: instances/views.py:1238 +#: instances/views.py:1193 #, python-format msgid "Memory set to %(cur)s MiB (maximum %(max)s MiB)" msgstr "内存已设为 %(cur)s MiB(最大 %(max)s MiB)" -#: instances/views.py:1258 +#: instances/views.py:1213 msgid "Enter disk sizes as whole GiB" msgstr "请以整数 GiB 输入磁盘大小" -#: instances/views.py:1262 +#: instances/views.py:1217 #, python-format msgid "%(dev)s is %(size)s: a disk cannot shrink" msgstr "%(dev)s 为 %(size)s:磁盘不能缩小" -#: instances/views.py:1270 +#: instances/views.py:1225 msgid "No disks were selected or valid for resizing." msgstr "未选择磁盘或没有可调整大小的有效磁盘。" -#: instances/views.py:1279 +#: instances/views.py:1234 #, python-format msgid "%(quota)s: cannot resize the disks of '%(instance_name)s'." msgstr "%(quota)s:无法调整“%(instance_name)s”的磁盘。" -#: instances/views.py:1291 +#: instances/views.py:1246 #, python-format msgid "The storage pool of %(dev)s cannot resize its volumes" msgstr "%(dev)s 所在的存储池无法调整其卷的大小" -#: instances/views.py:1293 +#: instances/views.py:1248 #, python-format msgid "Disk is resized: %(dev)s" msgstr "磁盘已调整大小:%(dev)s" -#: instances/views.py:1387 +#: instances/views.py:1342 msgid "The VM's machine has no PCIe slots to add" msgstr "该虚拟机的机器类型没有可添加的 PCIe 插槽" -#: instances/views.py:1392 +#: instances/views.py:1347 #, python-format msgid "%(count)s PCIe slots added" msgstr "已添加 %(count)s 个 PCIe 插槽" -#: instances/views.py:1394 +#: instances/views.py:1349 #, python-format msgid "%(count)s PCIe slots added: the VM gets them at its next start" msgstr "已添加 %(count)s 个 PCIe 插槽:虚拟机在下次启动时获得它们" -#: instances/views.py:1440 +#: instances/views.py:1395 #, python-format msgid "%(volume)s is already a disk of this VM" msgstr "%(volume)s 已是此虚拟机的磁盘" -#: instances/views.py:1530 +#: instances/views.py:1485 msgid "Volume changes are applied. But it will be activated after shutdown" msgstr "卷更改已应用,但将在关机后生效" -#: instances/views.py:1535 +#: instances/views.py:1490 msgid "Volume is changed successfully." msgstr "卷已成功更改。" -#: instances/views.py:1553 +#: instances/views.py:1508 #, python-format msgid "Disk %(dev)s is not a storage volume of this instance" msgstr "磁盘 %(dev)s 不是此虚拟机的存储卷" -#: instances/views.py:1560 +#: instances/views.py:1515 #, python-format msgid "Disk %(dev)s has a pending change; delete it once the VM is shut down" msgstr "磁盘 %(dev)s 有待生效的更改;请在虚拟机关机后删除它" -#: instances/views.py:1575 +#: instances/views.py:1530 #, python-format msgid "Volume %(vol)s is used by another VM; detach it instead" msgstr "卷 %(vol)s 正被另一台虚拟机使用;请改为分离它" -#: instances/views.py:1587 +#: instances/views.py:1542 #, python-format msgid "" "The guest has not released disk %(dev)s yet; volume %(vol)s was kept. Delete " "it from the storage pool once the VM is shut down." msgstr "客户机尚未释放磁盘 %(dev)s;卷 %(vol)s 已保留。请在虚拟机关机后从存储池中删除它。" -#: instances/views.py:1598 +#: instances/views.py:1553 #, python-format msgid "Disk %(dev)s was detached, but its volume was kept: %(err)s" msgstr "磁盘 %(dev)s 已分离,但其卷已保留:%(err)s" -#: instances/views.py:1624 +#: instances/views.py:1579 #, python-format msgid "This VM cannot take a CD-ROM on the %(bus)s bus now" msgstr "该虚拟机现在无法在 %(bus)s 总线上添加 CD-ROM" -#: instances/views.py:1627 +#: instances/views.py:1582 msgid "A USB CD-ROM needs an ISO image: libvirt has no empty USB drive" msgstr "USB CD-ROM 需要 ISO 镜像:libvirt 不支持空的 USB 驱动器" -#: instances/views.py:1632 +#: instances/views.py:1587 #, python-format msgid "%(image)s is already in a CD-ROM of this VM" msgstr "%(image)s 已在此虚拟机的某个光驱中" -#: instances/views.py:1713 +#: instances/views.py:1668 #, python-format msgid "Pool %(pool)s was not read again: %(err)s" msgstr "未重新读取存储池 %(pool)s:%(err)s" -#: instances/views.py:1734 +#: instances/views.py:1689 #, python-format msgid "Snapshot was not deleted: %(err)s" msgstr "快照未删除:%(err)s" -#: instances/views.py:1849 +#: instances/views.py:1804 msgid "Add at least one device to the boot order." msgstr "请至少向启动顺序中添加一个设备。" -#: instances/views.py:1863 +#: instances/views.py:1818 msgid "Boot order changed. It applies at the next start." msgstr "启动顺序已更改,将在下次启动时生效。" -#: instances/views.py:1865 +#: instances/views.py:1820 msgid "Boot order changed successfully." msgstr "启动顺序已成功更改。" -#: instances/views.py:1885 +#: instances/views.py:1840 msgid "The XML is empty" msgstr "XML 为空" -#: instances/views.py:1889 instances/views.py:2514 +#: instances/views.py:1844 instances/views.py:2469 #, python-format msgid "The XML is not well-formed: %(error)s" msgstr "XML 格式不正确:%(error)s" -#: instances/views.py:1891 +#: instances/views.py:1846 #, python-format msgid "The XML must keep the VM's UUID %(uuid)s." msgstr "XML 必须保留虚拟机的 UUID %(uuid)s。" -#: instances/views.py:1895 +#: instances/views.py:1850 msgid "" "The definition has changed since the editor was opened. Reload the page to " "edit the current one." msgstr "自打开编辑器以来定义已被更改。请重新加载页面以编辑当前定义。" -#: instances/views.py:1907 +#: instances/views.py:1862 msgid "The XML definition was saved." msgstr "XML 定义已保存。" -#: instances/views.py:1909 +#: instances/views.py:1864 msgid "The XML definition was saved; the VM uses it from its next start." msgstr "XML 定义已保存;虚拟机从下次启动起使用它。" -#: instances/views.py:1990 +#: instances/views.py:1945 msgid "Select one network interface to change" msgstr "请选择一个要更改的网络接口" -#: instances/views.py:2028 +#: instances/views.py:1983 msgid "Network Device Config is changed. Please shutdown instance to activate." msgstr "网络设备配置已更改。请关闭虚拟机以使其生效。" -#: instances/views.py:2030 +#: instances/views.py:1985 msgid "Network device changed" msgstr "网络设备已更改" -#: instances/views.py:2042 +#: instances/views.py:1997 msgid "No network selected" msgstr "未选择网络" -#: instances/views.py:2047 +#: instances/views.py:2002 #, python-format msgid "The VM already has a network device with MAC %(mac)s" msgstr "虚拟机已有 MAC 地址为 %(mac)s 的网络设备" -#: instances/views.py:2066 +#: instances/views.py:2021 msgid "" "The running VM has no free PCI slot for another network device: shut it down " "and add the device then; Add PCIe slots on the Disks tab gives it free ones " @@ -5935,137 +5947,137 @@ msgid "" msgstr "" "运行中的虚拟机没有可用 PCI 插槽来添加网络设备:请关机后再添加设备。“磁盘”选项卡上的“添加 PCIe 插槽”可使其从下次启动起获得可用插槽。" -#: instances/views.py:2107 instances/views.py:2113 +#: instances/views.py:2062 instances/views.py:2068 msgid "The VM has it" msgstr "虚拟机已有此设备" -#: instances/views.py:2144 +#: instances/views.py:2099 #, python-format msgid "The host has no USB device %(device)s" msgstr "主机上没有 USB 设备 %(device)s" -#: instances/views.py:2165 +#: instances/views.py:2120 #, python-format msgid "The host has no PCI device %(device)s" msgstr "主机上没有 PCI 设备 %(device)s" -#: instances/views.py:2183 +#: instances/views.py:2138 #, python-format msgid "" "Removal of PCI device %(address)s requested: the guest has not let it go yet;" " it stays in the running VM until it does or the VM stops." msgstr "已请求移除 PCI 设备 %(address)s:客户机尚未释放它;在客户机释放或虚拟机停止之前,它仍保留在运行中的虚拟机中。" -#: instances/views.py:2228 instances/views.py:2258 +#: instances/views.py:2183 instances/views.py:2213 msgid "Select a network interface and a QoS direction" msgstr "请选择网络接口和 QoS 方向" -#: instances/views.py:2231 vrtManager/network.py:459 +#: instances/views.py:2186 vrtManager/network.py:459 msgid "QoS rates must be whole numbers" msgstr "QoS 速率必须为整数" -#: instances/views.py:2238 networks/views.py:229 +#: instances/views.py:2193 networks/views.py:229 #, python-format msgid "%(qos_dir)s QoS is set" msgstr "%(qos_dir)s QoS 已设置" -#: instances/views.py:2243 +#: instances/views.py:2198 #, python-format msgid "" "%(qos_dir)s QoS is set. A running VM gets it after it is powered off and on." msgstr "%(qos_dir)s QoS 已设置。运行中的虚拟机需关机再开机后才能生效。" -#: instances/views.py:2265 networks/views.py:255 +#: instances/views.py:2220 networks/views.py:255 #, python-format msgid "%(qos_dir)s QoS is deleted" msgstr "%(qos_dir)s QoS 已删除" -#: instances/views.py:2270 +#: instances/views.py:2225 #, python-format msgid "" "%(qos_dir)s QoS is deleted. A running VM keeps the old limit until it is " "powered off and on." msgstr "%(qos_dir)s QoS 已删除。运行中的虚拟机在关机再开机之前保留旧的限制。" -#: instances/views.py:2298 +#: instances/views.py:2253 msgid "Only one owner is allowed and the one already added" msgstr "只允许一个所有者,且已添加" -#: instances/views.py:2300 +#: instances/views.py:2255 #, python-format msgid "%(user)s is already an owner" msgstr "%(user)s 已是所有者" -#: instances/views.py:2345 +#: instances/views.py:2300 #, python-format msgid "Console settings were not saved: %(errors)s" msgstr "控制台设置未保存:%(errors)s" -#: instances/views.py:2367 +#: instances/views.py:2322 msgid "" "Error setting console password. You should check that your instance have an " "graphic device." msgstr "设置控制台密码出错。请检查虚拟机是否有图形设备。" -#: instances/views.py:2414 +#: instances/views.py:2369 msgid "Shut the VM down to change whether it is a template." msgstr "请关闭虚拟机以更改其是否为模板。" -#: instances/views.py:2420 +#: instances/views.py:2375 #, python-format msgid "%(quota)s: '%(instance_name)s' stays a template." msgstr "%(quota)s:“%(instance_name)s”仍为模板。" -#: instances/views.py:2502 +#: instances/views.py:2457 msgid "The XML has no name" msgstr "XML 中没有名称" -#: instances/views.py:2516 webvirtcloud/middleware.py:126 +#: instances/views.py:2471 webvirtcloud/middleware.py:126 #, python-format msgid "libvirt Error - %(exception)s" msgstr "libvirt 错误 - %(exception)s" -#: instances/views.py:2523 +#: instances/views.py:2478 msgid "An empty disk of the size you set" msgstr "按设定大小创建的空磁盘" -#: instances/views.py:2524 +#: instances/views.py:2479 msgid "Volumes already in a pool" msgstr "存储池中已有的卷" -#: instances/views.py:2525 +#: instances/views.py:2480 msgid "A copy of a template volume" msgstr "模板卷的副本" -#: instances/views.py:2614 +#: instances/views.py:2569 msgid "You haven't defined any storage pools" msgstr "您尚未定义任何存储池" -#: instances/views.py:2616 +#: instances/views.py:2571 msgid "You haven't defined any network pools" msgstr "您尚未定义任何网络池" -#: instances/views.py:2679 +#: instances/views.py:2634 msgid "Flavor Created" msgstr "规格已创建" -#: instances/views.py:2687 +#: instances/views.py:2642 msgid "Create Flavor" msgstr "创建规格" -#: instances/views.py:2702 +#: instances/views.py:2657 msgid "Flavor Updated" msgstr "规格已更新" -#: instances/views.py:2708 +#: instances/views.py:2663 msgid "Update Flavor" msgstr "更新规格" -#: instances/views.py:2720 +#: instances/views.py:2675 msgid "Flavor Deleted" msgstr "规格已删除" -#: instances/views.py:2726 +#: instances/views.py:2681 msgid "Only the size template goes; VMs made from it keep their size." msgstr "仅删除此规格;由它创建的虚拟机保持其大小。" @@ -7540,7 +7552,7 @@ msgid "" "resolve this job" msgstr "恢复在一小时内无法读取任务状态;请检查虚拟机,然后处理此任务" -#: operations/worker.py:464 +#: operations/worker.py:473 msgid "The worker stopped before the job changed anything" msgstr "工作进程在任务做出任何更改之前停止" @@ -8665,67 +8677,67 @@ msgstr "磁盘 %(target_dev)s 不在虚拟机的定义中" msgid "Please shutdown the instance then try to enable vCPU hotplug" msgstr "请先关闭虚拟机,再启用 vCPU 热插拔" -#: vrtManager/instance.py:1854 +#: vrtManager/instance.py:1842 msgid "" "The VM has external snapshots; internal snapshots cannot be mixed with them" msgstr "虚拟机有外部快照;内部快照不能与其混用" -#: vrtManager/instance.py:1886 +#: vrtManager/instance.py:1874 msgid "A snapshot with memory is not quiesced; it keeps the whole guest state" msgstr "含内存的快照不会静默文件系统;它保存整个客户机状态" -#: vrtManager/instance.py:1888 +#: vrtManager/instance.py:1876 msgid "Only a running VM's file systems can be frozen" msgstr "只有运行中的虚拟机的文件系统可以冻结" -#: vrtManager/instance.py:1890 +#: vrtManager/instance.py:1878 msgid "The QEMU guest agent is not connected; it freezes the file systems" msgstr "QEMU 客户机代理未连接;文件系统由它冻结" -#: vrtManager/instance.py:1893 +#: vrtManager/instance.py:1881 msgid "" "The VM has internal snapshots; external snapshots cannot be mixed with them" msgstr "虚拟机有内部快照;外部快照不能与其混用" -#: vrtManager/instance.py:1898 +#: vrtManager/instance.py:1886 msgid "Newer snapshots exist after the last revert; delete them first" msgstr "上次还原后存在更新的快照;请先删除它们" -#: vrtManager/instance.py:1901 +#: vrtManager/instance.py:1889 msgid "No snapshot is current; revert to a snapshot first" msgstr "没有当前快照;请先还原到某个快照" -#: vrtManager/instance.py:1907 +#: vrtManager/instance.py:1895 #, python-format msgid "Eject the media from %(dev)s first" msgstr "请先弹出 %(dev)s 中的介质" -#: vrtManager/instance.py:1909 +#: vrtManager/instance.py:1897 #, python-format msgid "Disk %(dev)s is not a file; external snapshots need file disks" msgstr "磁盘 %(dev)s 不是文件;外部快照需要文件磁盘" -#: vrtManager/instance.py:1917 +#: vrtManager/instance.py:1905 #, python-format msgid "" "The storage pools could not be refreshed, so their free space is unknown: " "%(err)s" msgstr "无法刷新存储池,其可用空间未知:%(err)s" -#: vrtManager/instance.py:1938 +#: vrtManager/instance.py:1926 msgid "Memory is saved only from a running VM" msgstr "只能保存运行中的虚拟机的内存" -#: vrtManager/instance.py:1940 +#: vrtManager/instance.py:1928 msgid "The VM has no disk to keep its memory next to" msgstr "虚拟机没有可存放其内存的磁盘" -#: vrtManager/instance.py:1944 +#: vrtManager/instance.py:1932 #, python-format msgid "%(disk)s is not in a storage pool; memory is saved only into one" msgstr "%(disk)s 不在存储池中;内存只能保存到存储池中" -#: vrtManager/instance.py:1975 +#: vrtManager/instance.py:1963 #, python-format msgid "" "Storage pool %(pool)s has %(free).1f GiB free; the snapshot needs %(size).1f " @@ -8735,156 +8747,156 @@ msgstr "" "存储池 %(pool)s 可用 %(free).1f GiB;快照需要 %(size).1f GiB(其中虚拟机磁盘的 %(percent)g%%,至少 " "%(minimum)g GiB,若保存内存则另加内存)" -#: vrtManager/instance.py:1993 +#: vrtManager/instance.py:1981 msgid "" "This snapshot was taken by an older version on a UEFI VM and cannot be " "reverted; delete it instead" msgstr "此快照由旧版本在 UEFI 虚拟机上创建,无法还原;请将其删除" -#: vrtManager/instance.py:2023 +#: vrtManager/instance.py:2011 #, python-format msgid "" "The revert failed twice; try again in a moment (%(error)s). The VM is " "running." msgstr "还原两次失败;请稍后重试(%(error)s)。虚拟机正在运行。" -#: vrtManager/instance.py:2025 +#: vrtManager/instance.py:2013 #, python-format msgid "" "The revert failed twice; try again in a moment (%(error)s). The VM is shut " "off." msgstr "还原两次失败;请稍后重试(%(error)s)。虚拟机已关机。" -#: vrtManager/instance.py:2188 +#: vrtManager/instance.py:2176 msgid "A disk moves when its VM is defined and running, paused or shut off" msgstr "仅当虚拟机已定义且处于运行、暂停或关机状态时才能移动磁盘" -#: vrtManager/instance.py:2190 +#: vrtManager/instance.py:2178 msgid "" "A disk of a VM with snapshots, a saved state or a running block job does not " "move" msgstr "有快照、保存的状态或正在运行块任务的虚拟机的磁盘不能移动" -#: vrtManager/instance.py:2198 +#: vrtManager/instance.py:2186 #, python-format msgid "" "%(dev)s is not a disk file of the VM, the same in its running and next " "start's definition" msgstr "%(dev)s 不是该虚拟机在运行中的定义和下次启动定义中相同的磁盘文件" -#: vrtManager/instance.py:2203 +#: vrtManager/instance.py:2191 #, python-format msgid "%(dev)s does not move: only writable raw and qcow2 disks do" msgstr "%(dev)s 不能移动:只能移动可写的 raw 和 qcow2 磁盘" -#: vrtManager/instance.py:2210 +#: vrtManager/instance.py:2198 #, python-format msgid "%(dev)s does not move: no storage pool has %(path)s" msgstr "%(dev)s 不能移动:没有存储池包含 %(path)s" -#: vrtManager/instance.py:2212 +#: vrtManager/instance.py:2200 #, python-format msgid "%(dev)s does not move: it has a backing file" msgstr "%(dev)s 不能移动:它有后备文件" -#: vrtManager/instance.py:2215 +#: vrtManager/instance.py:2203 #, python-format msgid "%(pool_name)s is not an active pool of disk files of this host" msgstr "%(pool_name)s 不是此主机上活动的磁盘文件存储池" -#: vrtManager/instance.py:2218 +#: vrtManager/instance.py:2206 #, python-format msgid "%(dev)s is in pool %(pool_name)s already" msgstr "%(dev)s 已在存储池 %(pool_name)s 中" -#: vrtManager/instance.py:2222 +#: vrtManager/instance.py:2210 #, python-format msgid "Pool %(pool_name)s already has a file %(name)s" msgstr "存储池 %(pool_name)s 中已有文件 %(name)s" -#: vrtManager/instance.py:2246 +#: vrtManager/instance.py:2234 #, python-format msgid "The copy of %(dev)s ended before it was complete" msgstr "%(dev)s 的复制在完成前结束" -#: vrtManager/instance.py:2291 +#: vrtManager/instance.py:2279 #, python-format msgid "%(error)s; remove %(dest)s if it is there: %(cleanup_error)s" msgstr "%(error)s;如果 %(dest)s 存在请将其删除:%(cleanup_error)s" -#: vrtManager/instance.py:2293 +#: vrtManager/instance.py:2281 #, python-format msgid "%(error)s; the VM may use %(dest)s now: check its disk %(dev)s" msgstr "%(error)s;虚拟机现在可能在使用 %(dest)s:请检查其磁盘 %(dev)s" -#: vrtManager/instance.py:2300 +#: vrtManager/instance.py:2288 #, python-format msgid "The VM has no internal snapshot named %(name)s" msgstr "虚拟机没有名为 %(name)s 的内部快照" -#: vrtManager/instance.py:2309 +#: vrtManager/instance.py:2297 msgid "The VM has a saved state; start it or remove the saved state first" msgstr "虚拟机有保存的状态;请先启动它或删除保存的状态" -#: vrtManager/instance.py:2311 +#: vrtManager/instance.py:2299 msgid "A snapshot job is still running on this VM; try again when it ends" msgstr "此虚拟机上仍有快照任务在运行;请在其结束后重试" -#: vrtManager/instance.py:2321 +#: vrtManager/instance.py:2309 msgid "Shut the VM down to revert to an internal snapshot" msgstr "请关闭虚拟机后再还原到内部快照" -#: vrtManager/instance.py:2415 +#: vrtManager/instance.py:2403 msgid "Cannot clone a VM whose NVRAM is not a file" msgstr "NVRAM 不是文件的虚拟机无法克隆" -#: vrtManager/instance.py:2483 +#: vrtManager/instance.py:2471 #, python-format msgid "Clone failed (%(err)s); remove these copies by hand: %(path)s" msgstr "克隆失败(%(err)s);请手动删除这些副本:%(path)s" -#: vrtManager/instance.py:2493 +#: vrtManager/instance.py:2481 msgid "Shut the VM down before cloning it" msgstr "请先关闭虚拟机再克隆" -#: vrtManager/instance.py:2547 +#: vrtManager/instance.py:2535 #, python-format msgid "Clone failed (%(err)s); remove these copies by hand: %(leftovers)s" msgstr "克隆失败(%(err)s);请手动删除这些副本:%(leftovers)s" -#: vrtManager/instance.py:2558 +#: vrtManager/instance.py:2546 msgid "Live clone needs libvirt 11.10 or later; shut the VM down to clone it" msgstr "在线克隆需要 libvirt 11.10 或更高版本;请关闭虚拟机后再克隆" -#: vrtManager/instance.py:2561 +#: vrtManager/instance.py:2549 msgid "A VM with a TPM is cloned only shut down" msgstr "带 TPM 的虚拟机只能在关机时克隆" -#: vrtManager/instance.py:2564 +#: vrtManager/instance.py:2552 #, python-format msgid "Disk %(dev)s is not a local file; shut the VM down to clone it" msgstr "磁盘 %(dev)s 不是本地文件;请关闭虚拟机后再克隆" -#: vrtManager/instance.py:2566 +#: vrtManager/instance.py:2554 #, python-format msgid "Disk %(dev)s is shareable; shut the VM down to clone it" msgstr "磁盘 %(dev)s 是可共享的;请关闭虚拟机后再克隆" -#: vrtManager/instance.py:2571 +#: vrtManager/instance.py:2559 #, python-format msgid "Disk %(dev)s changes at the next start; shut the VM down to clone it" msgstr "磁盘 %(dev)s 在下次启动时会改变;请关闭虚拟机后再克隆" -#: vrtManager/instance.py:2573 +#: vrtManager/instance.py:2561 msgid "Another job is running on the VM; clone it when that ends" msgstr "虚拟机上有另一个任务正在运行;请在其结束后再克隆" -#: vrtManager/instance.py:2586 +#: vrtManager/instance.py:2574 #, python-format msgid "Volume %(name)s already exists in storage pool %(pool)s" msgstr "卷 %(name)s 已存在于存储池 %(pool)s 中" -#: vrtManager/instance.py:2605 +#: vrtManager/instance.py:2593 #, python-format msgid "" "Storage pool %(pool)s has %(free).1f GiB free; the clone needs %(size).1f GiB" @@ -8893,92 +8905,92 @@ msgstr "" "存储池 %(pool)s 可用 %(free).1f GiB;克隆需要 %(size).1f GiB(复制的磁盘,再加其 %(percent)g%%,至少" " %(minimum)g GiB)" -#: vrtManager/instance.py:2744 +#: vrtManager/instance.py:2732 msgid "A network interface that is a host device is removed in the XML editor" msgstr "作为主机设备的网络接口需在 XML 编辑器中删除" -#: vrtManager/instance.py:2760 +#: vrtManager/instance.py:2748 #, python-format msgid "No network interface with MAC %(old_mac)s" msgstr "没有 MAC 为 %(old_mac)s 的网络接口" -#: vrtManager/instance.py:2763 +#: vrtManager/instance.py:2751 msgid "A network interface that is a host device is changed in the XML editor" msgstr "作为主机设备的网络接口需在 XML 编辑器中修改" -#: vrtManager/instance.py:2854 +#: vrtManager/instance.py:2842 msgid "" "A VM changes its PCI devices while it runs, or is shut off without a saved " "state" msgstr "虚拟机只能在运行时或无保存的状态的关机时更改其 PCI 设备" -#: vrtManager/instance.py:2866 +#: vrtManager/instance.py:2854 #, python-format msgid "%(err)s; whether the VM has PCI device %(address)s now is not known" msgstr "%(err)s;虚拟机现在是否有 PCI 设备 %(address)s 未知" -#: vrtManager/instance.py:2868 +#: vrtManager/instance.py:2856 #, python-format msgid "" "%(err)s; PCI device %(address)s: the VM is not running, its next start has it" msgstr "%(err)s;PCI 设备 %(address)s:虚拟机未运行,下次启动时有它" -#: vrtManager/instance.py:2870 +#: vrtManager/instance.py:2858 #, python-format msgid "" "%(err)s; PCI device %(address)s: the VM is not running, its next start does " "not have it" msgstr "%(err)s;PCI 设备 %(address)s:虚拟机未运行,下次启动时没有它" -#: vrtManager/instance.py:2871 +#: vrtManager/instance.py:2859 #, python-format msgid "" "%(err)s; PCI device %(address)s: the running VM has it, its next start has it" msgstr "%(err)s;PCI 设备 %(address)s:运行中的虚拟机有它,下次启动时有它" -#: vrtManager/instance.py:2873 +#: vrtManager/instance.py:2861 #, python-format msgid "" "%(err)s; PCI device %(address)s: the running VM has it, its next start does " "not have it" msgstr "%(err)s;PCI 设备 %(address)s:运行中的虚拟机有它,下次启动时没有它" -#: vrtManager/instance.py:2875 +#: vrtManager/instance.py:2863 #, python-format msgid "" "%(err)s; PCI device %(address)s: the running VM does not have it, its next " "start has it" msgstr "%(err)s;PCI 设备 %(address)s:运行中的虚拟机没有它,下次启动时有它" -#: vrtManager/instance.py:2877 +#: vrtManager/instance.py:2865 #, python-format msgid "" "%(err)s; PCI device %(address)s: the running VM does not have it, its next " "start does not have it" msgstr "%(err)s;PCI 设备 %(address)s:运行中的虚拟机没有它,下次启动时没有它" -#: vrtManager/instance.py:2911 +#: vrtManager/instance.py:2899 #, python-format msgid "The VM has no PCI device %(address)s" msgstr "虚拟机没有 PCI 设备 %(address)s" -#: vrtManager/instance.py:2929 +#: vrtManager/instance.py:2917 msgid "The VM has no USB controller" msgstr "虚拟机没有 USB 控制器" -#: vrtManager/instance.py:2948 +#: vrtManager/instance.py:2936 msgid "The VM has no such USB device" msgstr "虚拟机没有此 USB 设备" -#: vrtManager/instance.py:2989 +#: vrtManager/instance.py:2977 msgid "The VM has its own RNG device: change it in the XML editor" msgstr "虚拟机有自己的 RNG 设备:请在 XML 编辑器中修改" -#: vrtManager/instance.py:3016 +#: vrtManager/instance.py:3004 msgid "The VM has its own TPM: change it in the XML editor" msgstr "虚拟机有自己的 TPM:请在 XML 编辑器中修改" -#: vrtManager/instance.py:3035 +#: vrtManager/instance.py:3023 #, python-format msgid "Unknown crash action %(action)s" msgstr "未知的崩溃操作 %(action)s" diff --git a/vrtManager/connection.py b/vrtManager/connection.py index 398f3a42..91b14684 100644 --- a/vrtManager/connection.py +++ b/vrtManager/connection.py @@ -976,6 +976,18 @@ class wvmConnect(object): def get_volume_by_path(self, path): return self.wvm.storageVolLookupByPath(path) + def backing_path(self, path): + """The backing file of a volume; None if it has none or is in no pool. + Any other error is raised: a chain cut short would let a file a kept + one needs count as deletable.""" + try: + vol = self.get_volume_by_path(path) + except libvirtError as err: + if err.get_error_code() != libvirt.VIR_ERR_NO_STORAGE_VOL: + raise + return None + return util.get_xml_path(vol.XMLDesc(0), "/volume/backingStore/path") + def remove_nvram(self, path, destination): """Deletes the NVRAM file of a VM that migrated from this host to destination (a wvmConnect). Only a file in libvirt's per-host NVRAM diff --git a/vrtManager/instance.py b/vrtManager/instance.py index 278bcf87..e2ac1f4f 100644 --- a/vrtManager/instance.py +++ b/vrtManager/instance.py @@ -1833,18 +1833,6 @@ class wvmInstance(wvmConnect): grew = True return files[len(tops):] + memory - def backing_path(self, path): - """The backing file of a volume; None if it has none or is in no pool. - Any other error is raised: a chain cut short would let a file a kept - one needs count as deletable.""" - try: - vol = self.get_volume_by_path(path) - except libvirtError as err: - if err.get_error_code() != VIR_ERR_NO_STORAGE_VOL: - raise - return None - return util.get_xml_path(vol.XMLDesc(0), "/volume/backingStore/path") - def _snapshotCreateXML(self, xml, flag): self.instance.snapshotCreateXML(xml, flag)