diff --git a/instances/jobs.py b/instances/jobs.py index 8e4d2282..e7f4a6ae 100644 --- a/instances/jobs.py +++ b/instances/jobs.py @@ -8,6 +8,7 @@ import time from django.db import transaction from django.core.exceptions import PermissionDenied from django.shortcuts import get_object_or_404 +from django.urls import reverse from django.template.loader import render_to_string from django.utils.translation import gettext_noop from django.utils.translation import gettext_noop as _ @@ -112,7 +113,7 @@ def _snapshot_free_space(): CLONE_POST_KEY_RE = re.compile(r"^(clone-net-mac-\d+|disk-[a-z0-9]+|meta-[a-z0-9]+)$") -CLONE_FIELDS = ("name", "clone-title", "clone-description") +CLONE_FIELDS = ("name", "clone-title", "clone-description", "clone-vcpu", "clone-memory", "clone-start") def clone_data(user, instance, fields): @@ -126,9 +127,19 @@ def clone_data(user, instance, fields): data["clone-title"] = fields.get("clone-title", "").strip() data["clone-description"] = fields.get("clone-description", "").strip() - # the clone copies the persistent definition - quota = {"cpus": instance.proxy.get_vcpu(config=True), "memory": instance.proxy.get_memory(config=True), - "disk": utils.disk_gib(instance.config_disks)} + # the clone copies the persistent definition, with the size the form gives + charged = {} + for key, source, label in (("vcpu", instance.config_vcpu, _("vCPUs")), + ("memory", instance.config_memory, _("memory (MiB)"))): + value = fields.get(f"clone-{key}") or str(source) + if not value.isdigit() or int(value) < 1: + raise util.OperationError(_("Enter a whole number of %(what)s for the clone") % {"what": label}) + # None: as the VM, nothing to change after the copy + data[key] = int(value) if int(value) != source else None + # the larger: a resize that fails after the copy leaves the clone at the VM's size + charged[key] = max(int(value), source) + data["start"] = fields.get("clone-start") == "true" + quota = {"cpus": charged["vcpu"], "memory": charged["memory"], "disk": utils.disk_gib(instance.config_disks)} quota_msg = utils.check_user_quota(user, 1, quota["cpus"], quota["memory"], quota["disk"]) data["disk_owner_uid"] = int(app_settings.INSTANCE_VOLUME_DEFAULT_OWNER_UID) @@ -188,6 +199,37 @@ def clone_data(user, instance, fields): return data, quota +def _finish_clone(op, clone, warning): + """The clone, defined but not recorded yet (the quota counts the job, at the + size it charged, until then): the size the form gave, and started if + asked. It is made either way: a step that fails is a warning of the job.""" + c = Compute.objects.get(pk=op.intent["compute"]) + proxy = wvmInstance(c.hostname, c.login, c.password, c.type, op.intent["name"], uuid=op.intent["uuid"]) + notes = [warning] if warning else [] + try: + if clone.get("vcpu"): + proxy.resize_cpu(str(clone["vcpu"]), str(clone["vcpu"])) + if clone.get("memory"): + proxy.resize_mem(clone["memory"], clone["memory"]) + if clone.get("start"): + proxy.start() + except libvirtError as err: + notes.append(_("The clone is made, but not as asked: %(error)s") % {"error": err}) + return {"warning": " ".join(notes)} if notes else {} + + +def _clone_made(op, result): + """Records the clone; the job's result, with the clone's page, which the + job panel opens: the page the job ran from is the VM cloned.""" + record_vm(op, owner=True) + return {**result, "open": _clone_page(op)} + + +def _clone_page(op): + clone = Instance.objects.get(compute_id=op.intent["compute"], uuid=op.intent["uuid"]) + return reverse("instances:instance", args=[clone.pk]) + + def vm_claims(compute, name): """A job that adds a VM: other VMs stay usable; the VM list reconcile waits.""" return {claims.compute_key(compute.pk): Claim.SHARED, claims.reconcile_key(compute.pk): Claim.EXCLUSIVE, @@ -494,8 +536,7 @@ class CloneInstance(Kind): ctx.record_intent(live=False) ctx.set_phase(gettext_noop("Copying disks")) proxy.clone_instance(clone, uuid) - record_vm(op, owner=True) - return {} + return _clone_made(op, _finish_clone(op, clone, "")) if status not in (1, 3): raise util.OperationError(CLONE_STATE_REFUSED) targets = proxy.live_clone_targets(clone, **_snapshot_free_space()) @@ -555,8 +596,7 @@ class CloneInstance(Kind): success, errmsg = result if success: proxy.define_clone(clone, uuid, {c["dev"]: c["path"] for c in copies}) - record_vm(op, owner=True) - return {"warning": warning} if warning else {} + return _clone_made(op, _finish_clone(op, clone, warning)) _delete_created(proxy, op) op.refresh_from_db(fields=["cancel_requested"]) if op.cancel_requested: @@ -578,7 +618,10 @@ class CloneInstance(Kind): def reconcile(self, op): if not op.intent.get("live"): disks = [v for k, v in op.intent["clone"].items() if k.startswith("disk-") and v] - return vm_outcome(op, owner=True, volumes=disks) + outcome = vm_outcome(op, owner=True, volumes=disks) + if outcome.state == Operation.SUCCEEDED: + outcome.result = {**outcome.result, "open": _clone_page(op)} + return outcome proxy = _recovery_proxy(op) if proxy is None: created = ", ".join(c["path"] for c in op.intent.get("created", [])) @@ -597,8 +640,7 @@ class CloneInstance(Kind): """The spec's recovery table: a defined clone first, then a volume still being created, since it comes before the copy.""" if _domain(op.intent["compute"], op.intent["uuid"]) is not None: - record_vm(op, owner=True) - return Outcome(Operation.SUCCEEDED) + return Outcome(Operation.SUCCEEDED, result=_clone_made(op, {})) created = op.intent.get("created", []) paths = [c["path"] for c in created] ended = Operation.CANCELLED if op.cancel_requested else Operation.FAILED @@ -637,8 +679,7 @@ class CloneInstance(Kind): _delete_created(proxy, op) return Outcome(ended, error=errmsg or "The copy failed") proxy.define_clone(op.intent["clone"], op.intent["uuid"], {c["dev"]: c["path"] for c in created}) - record_vm(op, owner=True) - return Outcome(Operation.SUCCEEDED) + return Outcome(Operation.SUCCEEDED, result=_clone_made(op, _finish_clone(op, op.intent["clone"], ""))) def log_entry(self, op, state): source = op.resource_label.rpartition("/")[2] diff --git a/instances/models.py b/instances/models.py index aaae924e..94db5012 100644 --- a/instances/models.py +++ b/instances/models.py @@ -187,6 +187,11 @@ class Instance(models.Model): def config_cur_vcpu(self) -> int: return self.proxy.get_cur_vcpu(config=True) + @cached_property + def config_memory(self) -> int: + """Memory (MiB) of the persistent definition, which a clone starts from.""" + return self.proxy.get_memory(config=True) + @cached_property def config_title(self) -> str: """Title and description of the persistent definition, which the diff --git a/instances/templates/instances/settings_tab.html b/instances/templates/instances/settings_tab.html index 4fb425ee..036cd3eb 100644 --- a/instances/templates/instances/settings_tab.html +++ b/instances/templates/instances/settings_tab.html @@ -834,6 +834,24 @@ {% endif %} +
+ {% trans "Size" %} +
+
+ + +
+
+ + +
+
+

{% trans "The VM's size to start from; the quota counts the larger of the two." %}

+
+ + +
+
{% trans "Optional details" %} diff --git a/instances/tests/test_clone_job.py b/instances/tests/test_clone_job.py index 723c60d9..7409cc4c 100644 --- a/instances/tests/test_clone_job.py +++ b/instances/tests/test_clone_job.py @@ -43,9 +43,11 @@ class CloneJobTestCase(TransactionTestCase): self.client.force_login(self.admin) self.compute = Compute.objects.create(name="clone-c", hostname="192.0.2.30", login="root", password="", type=1) self.source = Instance.objects.create(compute=self.compute, name="src", uuid="12121212-3434-5656-7878-909090909090") - patcher = patch("instances.models.wvmInstance", return_value=proxy()) - self.proxy = patcher.start().return_value - self.addCleanup(patcher.stop) + self.proxy = proxy() + for target_name in ("instances.models.wvmInstance", "instances.jobs.wvmInstance"): + patcher = patch(target_name, return_value=self.proxy) + patcher.start() + self.addCleanup(patcher.stop) def test_the_page_queues_the_clone_and_the_worker_makes_and_records_it(self): response = self.client.post(reverse("instances:clone", args=[self.source.pk]), @@ -69,6 +71,56 @@ class CloneJobTestCase(TransactionTestCase): clone = Instance.objects.get(uuid=op.intent["uuid"]) self.assertTrue(UserInstance.objects.filter(instance=clone, user=self.admin).exists()) + def clone_with(self, **fields): + self.client.post(reverse("instances:clone", args=[self.source.pk]), + {"name": "copy", "disk-vda": "copy.qcow2", "clone-net-mac-0": "52:54:00:00:00:01", + "submission_key": f"key-{uuid4().hex[:12]}", **fields}) + op = Operation.objects.filter(kind="instance.clone").first() + if op is None: + return None, None + 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() + return op, CloneInstance().run(op, Context(op, SimpleNamespace(identity="wvc-test:1:a"))) + + def test_the_clone_takes_the_size_given_and_starts(self): + # before the clone is recorded: until then the quota counts the job, at the size charged + self.proxy.resize_cpu.side_effect = lambda *a: self.assertFalse(Instance.objects.filter(name="copy").exists()) + op, result = self.clone_with(**{"clone-vcpu": "4", "clone-memory": "2048", "clone-start": "true"}) + self.assertEqual((op.intent["quota"]["cpus"], op.intent["quota"]["memory"]), (4, 2048)) # what the quota counts + self.proxy.resize_cpu.assert_called_once_with("4", "4") + self.proxy.resize_mem.assert_called_once_with(2048, 2048) + self.proxy.start.assert_called_once_with() + # the job panel opens the clone's page + clone = Instance.objects.get(uuid=op.intent["uuid"]) + self.assertEqual(result, {"open": reverse("instances:instance", args=[clone.pk])}) + + def test_a_smaller_clone_is_charged_the_vm_s_size(self): + # a shrink that fails after the copy leaves the clone at the VM's size, which the quota allowed + op, _result = self.clone_with(**{"clone-vcpu": "1", "clone-memory": "256"}) + self.assertEqual((op.intent["quota"]["cpus"], op.intent["quota"]["memory"]), (1, 512)) + self.proxy.resize_mem.assert_called_once_with(256, 256) + + def test_the_vm_s_size_changes_nothing_after_the_copy(self): + _op, result = self.clone_with(**{"clone-vcpu": "1", "clone-memory": "512"}) + self.proxy.resize_cpu.assert_not_called() + self.proxy.resize_mem.assert_not_called() + self.proxy.start.assert_not_called() + self.assertEqual(list(result), ["open"]) + + def test_a_size_that_is_not_a_whole_number_is_refused(self): + for value in ("0", "abc", "-2"): + with self.subTest(value=value): + op, _result = self.clone_with(**{"clone-vcpu": value}) + self.assertIsNone(op) + self.proxy.clone_instance.assert_not_called() + + def test_a_clone_that_cannot_start_is_made_with_a_warning(self): + self.proxy.start.side_effect = libvirt.libvirtError("no memory") + op, result = self.clone_with(**{"clone-start": "true"}) + self.assertTrue(Instance.objects.filter(uuid=op.intent["uuid"]).exists()) + self.assertIn("The clone is made, but not as asked", result["warning"]) + def queued(self): self.client.post(reverse("instances:clone", args=[self.source.pk]), {"name": "copy", "disk-vda": "copy.qcow2", "clone-net-mac-0": "52:54:00:00:00:01", @@ -93,8 +145,10 @@ class CloneJobTestCase(TransactionTestCase): def test_recovery(self, conn): op = Operation(user=self.admin, intent={"compute": self.compute.pk, "uuid": "abababab-0000-1111-2222-333333333333", "name": "copy", "clone": {"name": "copy", "disk-vda": "copy.qcow2"}}) - self.assertEqual(CloneInstance().reconcile(op).state, Operation.SUCCEEDED) # the VM is defined - self.assertTrue(Instance.objects.filter(uuid=op.intent["uuid"]).exists()) + recovered = CloneInstance().reconcile(op) + self.assertEqual(recovered.state, Operation.SUCCEEDED) # the VM is defined + clone = Instance.objects.get(uuid=op.intent["uuid"]) + self.assertEqual(recovered.result["open"], reverse("instances:instance", args=[clone.pk])) missing = libvirt.libvirtError("no domain") missing.err = (libvirt.VIR_ERR_NO_DOMAIN, libvirt.VIR_FROM_QEMU, "no domain") conn.return_value.wvm.lookupByUUIDString.side_effect = missing @@ -248,7 +302,7 @@ class LiveCloneJobTestCase(TransactionTestCase): def test_the_live_run_records_each_step_before_it(self): op = self.op() - self.assertEqual(self.run_job(op), {}) + result = self.run_job(op) self.assertEqual(self.events, [ ("intent", "live", "targets"), ("intent", "planned"), ("create", "vda"), ("intent", "created"), @@ -269,6 +323,7 @@ class LiveCloneJobTestCase(TransactionTestCase): clone = Instance.objects.get(uuid=op.intent["uuid"]) self.assertTrue(UserInstance.objects.filter(instance=clone, user=self.admin).exists()) self.assertEqual(self.deleted(), []) + self.assertEqual(result, {"open": reverse("instances:instance", args=[clone.pk])}) def test_a_paused_vm_or_one_without_a_ready_agent_is_not_frozen(self): for status, agent in ((3, True), (1, False)): @@ -505,13 +560,23 @@ class LiveCloneJobTestCase(TransactionTestCase): Instance.objects.filter(uuid=op.intent["uuid"]).delete() op.delete() + def test_recovery_gives_the_clone_the_size_asked_and_starts_it(self): + clone = {"name": "copy", "disk-vda": "copy-vda.qcow2", "disk_owner_uid": 107, "disk_owner_gid": 107, + "vcpu": 2, "memory": None, "start": True} + outcome, _op = self.recovered(copy_result=[True, ""], clone=clone) + self.assertEqual(outcome.state, Operation.SUCCEEDED) + self.proxy.resize_cpu.assert_called_once_with("2", "2") + self.proxy.resize_mem.assert_not_called() + self.proxy.start.assert_called_once_with() + def test_recovery_of_a_defined_clone_succeeds(self): self.proxy.backup_result.return_value = None # libvirtd restarted after the clone was defined self.proxy.backup_targets.return_value = {"/other/x.qcow2"} # checked after it outcome, op = self.recovered(defined=True) self.assertEqual(outcome.state, Operation.SUCCEEDED) self.assertNotIn("define_clone", self.events) - self.assertTrue(Instance.objects.filter(uuid=op.intent["uuid"]).exists()) + clone = Instance.objects.get(uuid=op.intent["uuid"]) + self.assertEqual(outcome.result["open"], reverse("instances:instance", args=[clone.pk])) def test_recovery_of_a_failed_copy_deletes_what_was_created(self): outcome, _op = self.recovered(copy_result=[False, "No space left on device"]) diff --git a/instances/tests/test_pending_edits.py b/instances/tests/test_pending_edits.py index b4db1111..245e63fa 100644 --- a/instances/tests/test_pending_edits.py +++ b/instances/tests/test_pending_edits.py @@ -183,7 +183,7 @@ class PendingEditsTestCase(TestCase): # the DHCP entry of the clone's name holds one address: only the first NIC guesses it self.assertEqual(len(form.xpath(".//button[contains(@onclick, 'guess_mac_address')]")), 1) # a running or paused VM is copied by a backup, which does not preallocate - self.assertEqual(form.xpath(".//input[@type='checkbox']/@name"), metadata) + self.assertEqual(form.xpath(".//input[@type='checkbox']/@name"), [*metadata, "clone-start"]) def test_a_disk_with_a_pending_source_is_not_deleted(self): # running: vda is A, its definition for the next start uses B diff --git a/locale/tr/LC_MESSAGES/django.mo b/locale/tr/LC_MESSAGES/django.mo index afad99a4..76e970da 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 15c6af53..c2f54f8f 100644 --- a/locale/tr/LC_MESSAGES/django.po +++ b/locale/tr/LC_MESSAGES/django.po @@ -11,7 +11,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-11-01 15:30+0000\n" +"POT-Creation-Date: 2026-11-01 19:14+0000\n" "PO-Revision-Date: 2020-11-04 12:00+0000\n" "Last-Translator: catborise , 2020\n" "Language-Team: Turkish (https://www.transifex.com/catborise/teams/110663/" @@ -710,10 +710,10 @@ msgstr "Üyeler" #: instances/templates/instances/resize_tab.html:114 #: instances/templates/instances/settings_tab.html:416 #: instances/templates/instances/settings_tab.html:647 -#: instances/templates/instances/settings_tab.html:876 -#: instances/templates/instances/settings_tab.html:918 -#: instances/templates/instances/settings_tab.html:978 -#: instances/templates/instances/settings_tab.html:1008 +#: instances/templates/instances/settings_tab.html:894 +#: instances/templates/instances/settings_tab.html:936 +#: instances/templates/instances/settings_tab.html:996 +#: instances/templates/instances/settings_tab.html:1026 #: networks/templates/add_network_qos.html:36 #: networks/templates/network.html:335 networks/templates/network.html:375 #: networks/templates/network.html:402 networks/templates/network.html:441 @@ -877,7 +877,7 @@ msgid "Search removed VMs" msgstr "Kaldırılan sanal makinelerde ara" #: admin/templates/admin/tombstone_list.html:28 -#: instances/templates/instances/settings_tab.html:884 +#: instances/templates/instances/settings_tab.html:902 msgid "Template" msgstr "Şablon" @@ -1727,8 +1727,8 @@ msgid "Stored, leave empty to keep" msgstr "Kayıtlı, korumak için boş bırakın" #: computes/forms.py:28 computes/templates/computes/instances.html:45 -#: instances/templates/instances/settings_tab.html:841 -#: instances/templates/instances/settings_tab.html:873 +#: instances/templates/instances/settings_tab.html:859 +#: instances/templates/instances/settings_tab.html:891 #: instances/templates/instances/snapshots_tab.html:23 #: instances/templates/instances/snapshots_tab.html:67 msgid "Description" @@ -1798,7 +1798,9 @@ msgstr "taşıma adresi" msgid "" "IP address or host name the other hosts send migrating VMs to; empty: the " "address the panel reaches the host at" -msgstr "Diğer sunucuların taşınan sanal makineleri gönderdiği IP adresi veya ana bilgisayar adı; boş: panelin bu sunucuya ulaştığı adres" +msgstr "" +"Diğer sunucuların taşınan sanal makineleri gönderdiği IP adresi veya ana " +"bilgisayar adı; boş: panelin bu sunucuya ulaştığı adres" #: computes/models.py:33 computes/templates/computes/form.html:34 msgid "gstfsd key" @@ -2120,9 +2122,11 @@ msgid "of %(memory)s" msgstr "/ %(memory)s" #: computes/templates/computes/list.html:58 computes/templates/overview.html:58 -#: instances/forms.py:15 instances/templates/allinstances_index_grouped.html:16 +#: instances/forms.py:15 instances/jobs.py:131 +#: instances/templates/allinstances_index_grouped.html:16 #: instances/templates/allinstances_index_nongrouped.html:14 #: instances/templates/create_instance_w2.html:60 +#: instances/templates/instances/settings_tab.html:841 msgid "vCPUs" msgstr "vCPU" @@ -2130,14 +2134,14 @@ msgstr "vCPU" #: console/templates/console-vnc.html:13 #: console/templates/console-xterm.html:16 #: instances/templates/allinstances_index_grouped.html:36 -#: instances/templates/instances/settings_tab.html:995 +#: instances/templates/instances/settings_tab.html:1013 #: networks/templates/network.html:260 msgid "Connected" msgstr "Bağlı" #: computes/templates/computes/list.html:73 computes/templates/overview.html:21 #: instances/templates/allinstances_index_grouped.html:38 -#: instances/templates/instances/settings_tab.html:996 +#: instances/templates/instances/settings_tab.html:1014 msgid "Not connected" msgstr "Bağlı değil" @@ -2321,7 +2325,7 @@ msgstr "" "Sunucudaki /etc/default/webvirt-gstfsd içindeki GSTFSD_KEY değerinin 64 " "onaltılık karakterini girin" -#: computes/views.py:196 +#: computes/views.py:197 #, python-format msgid "" "Its VMs keep running on the host and leave the panel; their owners are kept " @@ -2578,6 +2582,7 @@ msgstr "" "VNC olarak değiştirin." #: instances/forms.py:14 instances/templates/create_instance_w2.html:66 +#: instances/templates/instances/settings_tab.html:845 msgid "Memory (MiB)" msgstr "Bellek (MiB)" @@ -2645,184 +2650,198 @@ 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:149 +#: instances/jobs.py:132 +msgid "memory (MiB)" +msgstr "bellek (MiB)" + +#: instances/jobs.py:135 +#, python-format +msgid "Enter a whole number of %(what)s for the clone" +msgstr "Klon için %(what)s olarak bir tam sayı girin" + +#: instances/jobs.py:159 msgid "Choose the clone's name from the list of free names" msgstr "Klonun adını boş adlar listesinden seçin" -#: instances/jobs.py:176 +#: instances/jobs.py:186 #, python-format msgid "User '%(quota_msg)s' quota reached, cannot create '%(clone_name)s'!" msgstr "" "%(quota_msg)s' Kullanıcı kotası doldu, '%(clone_name)s' in CPU su yeniden " "boyutlandırılamıyor!" -#: instances/jobs.py:179 instances/jobs.py:485 +#: instances/jobs.py:189 instances/jobs.py:512 #, python-format msgid "Instance '%(clone_name)s' already exists!" msgstr "'%(clone_name)s' sanal makinesi zaten mevcut!" -#: instances/jobs.py:182 +#: instances/jobs.py:192 #, 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:184 +#: instances/jobs.py:194 #, 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:187 +#: instances/jobs.py:197 #, 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:246 +#: instances/jobs.py:214 +#, 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:273 msgid "Delete snapshot" msgstr "Anlık görüntüyü sil" -#: instances/jobs.py:269 +#: instances/jobs.py:296 msgid "Merging the snapshot" msgstr "Anlık görüntü birleştiriliyor" -#: instances/jobs.py:291 +#: instances/jobs.py:318 msgid "libvirt is still merging the snapshot" msgstr "libvirt anlık görüntüyü hâlâ birleştiriyor" -#: instances/jobs.py:312 +#: instances/jobs.py:339 msgid "Take snapshot" msgstr "Anlık görüntü al" -#: instances/jobs.py:336 +#: instances/jobs.py:363 msgid "Taking the snapshot" msgstr "Anlık görüntü alınıyor" -#: instances/jobs.py:349 +#: instances/jobs.py:376 msgid "libvirt is still taking the snapshot" msgstr "libvirt anlık görüntüyü hâlâ alıyor" -#: instances/jobs.py:375 +#: instances/jobs.py:402 msgid "Revert snapshot" msgstr "Snapshota dön" -#: instances/jobs.py:404 +#: instances/jobs.py:431 msgid "Reverting the VM" msgstr "Sanal makine geri döndürülüyor" -#: instances/jobs.py:415 +#: instances/jobs.py:442 msgid "libvirt is still reverting the VM" msgstr "libvirt sanal makineyi hâlâ geri döndürüyor" -#: instances/jobs.py:456 +#: instances/jobs.py:483 msgid "Clone VM" msgstr "Sanal makineyi klonla" -#: instances/jobs.py:495 instances/jobs.py:503 instances/jobs.py:746 +#: instances/jobs.py:522 instances/jobs.py:530 instances/jobs.py:773 msgid "Copying disks" msgstr "Diskler kopyalanıyor" -#: instances/jobs.py:629 +#: instances/jobs.py:656 msgid "libvirt is still copying the disks" msgstr "libvirt diskleri hâlâ kopyalıyor" -#: instances/jobs.py:662 instances/templates/create_instance_w1.html:58 +#: instances/jobs.py:689 instances/templates/create_instance_w1.html:58 #: instances/templates/create_instance_w2.html:353 msgid "Create VM" msgstr "Sanal makine oluştur" -#: instances/jobs.py:685 instances/views.py:2442 +#: instances/jobs.py:712 instances/views.py:2465 msgid "A virtual machine with this name already exists" msgstr "Bu isimde bir sanal makine mevcut" -#: instances/jobs.py:687 +#: instances/jobs.py:714 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:690 +#: instances/jobs.py:717 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:692 +#: instances/jobs.py:719 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:694 +#: instances/jobs.py:721 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:696 +#: instances/jobs.py:723 msgid "Invalid cache mode" msgstr "Geçersiz önbellek modu" -#: instances/jobs.py:698 +#: instances/jobs.py:725 #, 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:700 +#: instances/jobs.py:727 #, python-format msgid "A CD-ROM cannot use the %(bus)s bus" msgstr "CD-ROM %(bus)s veri yolunu kullanamaz" -#: instances/jobs.py:703 +#: instances/jobs.py:730 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:706 +#: instances/jobs.py:733 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:712 +#: instances/jobs.py:739 #, python-format msgid "This host offers no firmware %(firmware)s" msgstr "Bu sunucu %(firmware)s firmware'ini sunmuyor" -#: instances/jobs.py:714 +#: instances/jobs.py:741 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:765 +#: instances/jobs.py:792 msgid "Writing the cloud-init seed" msgstr "cloud-init tohumu yazılıyor" -#: instances/jobs.py:772 +#: instances/jobs.py:799 msgid "Defining the VM" msgstr "Sanal makine tanımlanıyor" -#: instances/jobs.py:829 +#: instances/jobs.py:856 #, python-format msgid "The instance is already on %(compute)s" msgstr "Sanal makine zaten %(compute)s üzerinde" -#: instances/jobs.py:831 +#: instances/jobs.py:858 #, python-format msgid "%(compute)s is not reachable" msgstr "%(compute)s sunucusuna ulaşılamıyor" -#: instances/jobs.py:833 +#: instances/jobs.py:860 #, 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:851 +#: instances/jobs.py:878 msgid "Migrate VM" msgstr "Sanal makineyi taşı" -#: instances/jobs.py:895 +#: instances/jobs.py:922 msgid "Migrating the VM" msgstr "Sanal makine taşınıyor" -#: instances/jobs.py:929 +#: instances/jobs.py:956 msgid "libvirt is still migrating the VM" msgstr "libvirt sanal makineyi hâlâ taşıyor" -#: instances/jobs.py:970 +#: instances/jobs.py:997 msgid "Move disk" msgstr "Diski taşı" -#: instances/jobs.py:1001 +#: instances/jobs.py:1028 msgid "Copying the disk" msgstr "Disk kopyalanıyor" @@ -2858,67 +2877,67 @@ msgstr "oluşturuldu" msgid "drbd" msgstr "drbd" -#: instances/models.py:221 instances/templates/create_instance_w2.html:115 +#: instances/models.py:226 instances/templates/create_instance_w2.html:115 #: instances/templates/instances/resize_tab.html:20 #: instances/templates/instances/settings_tab.html:25 #: instances/templates/instances/stats_tab.html:49 msgid "Disk" msgstr "Disk" -#: instances/models.py:223 instances/templates/create_instance_w2.html:309 +#: instances/models.py:228 instances/templates/create_instance_w2.html:309 #: instances/templates/instances/settings_tab.html:165 msgid "CD-ROM" msgstr "CD-ROM" -#: instances/models.py:225 +#: instances/models.py:230 msgid "NIC" msgstr "NIC" -#: instances/models.py:390 +#: instances/models.py:395 msgid "Live" msgstr "Canlı" -#: instances/models.py:391 +#: instances/models.py:396 msgid "Offline" msgstr "Çevrimdışı" -#: instances/models.py:392 +#: instances/models.py:397 msgid "Auto Converge" msgstr "Otomatik yavaşlat" -#: instances/models.py:393 +#: instances/models.py:398 msgid "Compress" msgstr "Sıkıştır" -#: instances/models.py:394 +#: instances/models.py:399 msgid "Unsafe" msgstr "Güvenli olmayan" -#: instances/models.py:406 +#: instances/models.py:411 msgid "No Virtual Machine name has been entered" msgstr "Sanal makine adı girilmedi" -#: instances/models.py:412 +#: instances/models.py:417 msgid "No VCPU has been entered" msgstr "VCPU belirtilmedi" -#: instances/models.py:418 +#: instances/models.py:423 msgid "No RAM size has been entered" msgstr "RAM boyutu belirtilmedi" -#: instances/models.py:422 +#: instances/models.py:427 msgid "No Network pool has been choosen" msgstr "Ağ havuzu seçilmedi" -#: instances/models.py:430 +#: instances/models.py:435 msgid "Please select HDD cache mode" msgstr "Lütfen HDD ön bellek modunu seçin" -#: instances/models.py:442 +#: instances/models.py:447 msgid "Please select a video driver" msgstr "Lütfen bir video sürücüsü seçin" -#: instances/models.py:479 +#: instances/models.py:484 msgid "Can access console without password" msgstr "Parolasız konsol erişimi yapabilir" @@ -3061,7 +3080,7 @@ msgid "Disk source" msgstr "Disk kaynağı" #: instances/templates/add_instance_volume.html:21 -#: instances/templates/create_instance_w2.html:131 instances/views.py:2461 +#: instances/templates/create_instance_w2.html:131 instances/views.py:2484 msgid "New disk" msgstr "Yeni disk" @@ -3093,6 +3112,7 @@ msgid "Letters, digits, dot, dash and underscore." msgstr "Harf, rakam, nokta, tire ve alt çizgi." #: instances/templates/add_instance_volume.html:44 +#: instances/templates/instances/settings_tab.html:838 msgid "Size" msgstr "Boyut" @@ -3399,7 +3419,7 @@ msgid "Firmware" msgstr "Üretici yazılımı" #: instances/templates/create_instance_w2.html:83 -#: instances/templates/instances/settings_tab.html:952 +#: instances/templates/instances/settings_tab.html:970 msgid "TPM 2.0" msgstr "TPM 2.0" @@ -3424,7 +3444,7 @@ msgstr "Özel CPU modelleri" msgid "Size (GiB)" msgstr "Boyut (GiB)" -#: instances/templates/create_instance_w2.html:158 instances/views.py:2462 +#: instances/templates/create_instance_w2.html:158 instances/views.py:2485 msgid "Existing volumes" msgstr "Mevcut birimler" @@ -3459,7 +3479,7 @@ msgstr "Aygıt" msgid "Remove" msgstr "Kaldır" -#: instances/templates/create_instance_w2.html:191 instances/views.py:2463 +#: instances/templates/create_instance_w2.html:191 instances/views.py:2486 msgid "Clone from template" msgstr "Şablondan klonla" @@ -3684,8 +3704,8 @@ msgstr "Konuk ajanı yanıt vermedi" #: instances/templates/instance_actions.html:13 #: instances/templates/instances/settings_tab.html:53 -#: instances/templates/instances/settings_tab.html:851 -#: instances/templates/instances/settings_tab.html:853 +#: instances/templates/instances/settings_tab.html:869 +#: instances/templates/instances/settings_tab.html:871 #: nwfilters/templates/nwfilters.html:57 nwfilters/templates/nwfilters.html:94 #: storages/templates/storage.html:143 storages/templates/storage.html:203 msgid "Clone" @@ -3794,7 +3814,7 @@ msgstr "" msgid "Start the VM to open its console." msgstr "Konsolunu açmak için sanal makineyi başlatın." -#: instances/templates/instances/access_tab.html:41 instances/views.py:1077 +#: instances/templates/instances/access_tab.html:41 instances/views.py:1100 msgid "Reset root password" msgstr "Root parolasını sıfırla" @@ -4408,17 +4428,17 @@ msgstr "" "sağlar." #: instances/templates/instances/resize_tab.html:102 -#: instances/templates/instances/settings_tab.html:896 +#: instances/templates/instances/settings_tab.html:914 msgid "Shut the VM down to change this." msgstr "Bunu değiştirmek için sanal makineyi kapatın." #: instances/templates/instances/resize_tab.html:109 -#: instances/templates/instances/settings_tab.html:1003 +#: instances/templates/instances/settings_tab.html:1021 msgid "Enabled" msgstr "Etkin" #: instances/templates/instances/resize_tab.html:110 -#: instances/templates/instances/settings_tab.html:1004 +#: instances/templates/instances/settings_tab.html:1022 msgid "Disabled" msgstr "Devredışı" @@ -5153,16 +5173,24 @@ msgstr "qcow2 meta verisini önceden ayır" msgid "The VM has no disk to copy." msgstr "Sanal makinenin kopyalanacak diski yok." -#: instances/templates/instances/settings_tab.html:838 +#: instances/templates/instances/settings_tab.html:849 +msgid "The VM's size to start from; the quota counts the larger of the two." +msgstr "Başlangıç olarak sanal makinenin boyutu; kota ikisinden büyük olanı sayar." + +#: instances/templates/instances/settings_tab.html:852 +msgid "Start the clone when it is made" +msgstr "Klon oluşturulunca başlat" + +#: instances/templates/instances/settings_tab.html:856 msgid "Optional details" msgstr "İsteğe bağlı ayrıntılar" -#: instances/templates/instances/settings_tab.html:839 -#: instances/templates/instances/settings_tab.html:871 +#: instances/templates/instances/settings_tab.html:857 +#: instances/templates/instances/settings_tab.html:889 msgid "Title" msgstr "Başlık" -#: instances/templates/instances/settings_tab.html:846 +#: instances/templates/instances/settings_tab.html:864 msgid "" "The clone gets none of the VM's host devices: one VM holds a device of the " "host." @@ -5170,46 +5198,46 @@ msgstr "" "Klon, sanal makinenin sunucu aygıtlarını almaz: bir sunucu aygıtını tek bir " "sanal makine tutar." -#: instances/templates/instances/settings_tab.html:849 +#: instances/templates/instances/settings_tab.html:867 msgid "Cloning runs as a background job." msgstr "Klonlama arka planda bir iş olarak çalışır." -#: instances/templates/instances/settings_tab.html:867 +#: instances/templates/instances/settings_tab.html:885 msgid "VM details" msgstr "Sanal makine ayrıntıları" -#: instances/templates/instances/settings_tab.html:868 +#: instances/templates/instances/settings_tab.html:886 msgid "A title and description to identify the VM." msgstr "Sanal makineyi tanımlamak için bir başlık ve açıklama." -#: instances/templates/instances/settings_tab.html:885 +#: instances/templates/instances/settings_tab.html:903 msgid "" "A template is a VM to clone: it is not counted in quotas and cannot be " "started." msgstr "" "Şablon, klonlanacak bir sanal makinedir: kotalarda sayılmaz ve başlatılamaz." -#: instances/templates/instances/settings_tab.html:892 +#: instances/templates/instances/settings_tab.html:910 msgid "Use as a template" msgstr "Şablon olarak kullan" -#: instances/templates/instances/settings_tab.html:895 +#: instances/templates/instances/settings_tab.html:913 msgid "Only administrators and staff can change this." msgstr "Bunu yalnızca yöneticiler ve personel değiştirebilir." -#: instances/templates/instances/settings_tab.html:905 +#: instances/templates/instances/settings_tab.html:923 msgid "Primary video model" msgstr "Birincil video modeli" -#: instances/templates/instances/settings_tab.html:906 +#: instances/templates/instances/settings_tab.html:924 msgid "The graphics device the guest sees." msgstr "Konuk sistemin gördüğü grafik aygıtı." -#: instances/templates/instances/settings_tab.html:927 +#: instances/templates/instances/settings_tab.html:945 msgid "Random number generator" msgstr "Rastgele sayı üreteci" -#: instances/templates/instances/settings_tab.html:928 +#: instances/templates/instances/settings_tab.html:946 msgid "" "A virtio device that feeds the guest's entropy from the host: a new VM does " "not wait for it at boot. From the next start." @@ -5217,19 +5245,19 @@ msgstr "" "Konuk sistemin entropisini sunucudan besleyen bir virtio aygıtı: yeni bir " "sanal makine açılışta onu beklemez. Sonraki başlatmadan itibaren." -#: instances/templates/instances/settings_tab.html:934 +#: instances/templates/instances/settings_tab.html:952 msgid "Virtio RNG" msgstr "Virtio RNG" -#: instances/templates/instances/settings_tab.html:937 +#: instances/templates/instances/settings_tab.html:955 msgid "The VM has its own RNG device: change it in the XML editor." msgstr "Sanal makinenin kendi RNG aygıtı var: XML düzenleyicide değiştirin." -#: instances/templates/instances/settings_tab.html:945 +#: instances/templates/instances/settings_tab.html:963 msgid "TPM" msgstr "TPM" -#: instances/templates/instances/settings_tab.html:946 +#: instances/templates/instances/settings_tab.html:964 msgid "" "An emulated TPM 2.0, as Windows 11 needs with UEFI Secure Boot. Its state " "stays on the host until the VM is destroyed: a TPM added again has it back. " @@ -5239,19 +5267,19 @@ msgstr "" "2.0. Durumu sanal makine yok edilene kadar sunucuda kalır: yeniden eklenen " "TPM onu geri alır. Bir sonraki başlatmadan itibaren." -#: instances/templates/instances/settings_tab.html:955 +#: instances/templates/instances/settings_tab.html:973 msgid "The VM has its own TPM: change it in the XML editor." msgstr "Sanal makinenin kendi TPM'i var: XML düzenleyicide değiştirin." -#: instances/templates/instances/settings_tab.html:957 +#: instances/templates/instances/settings_tab.html:975 msgid "This host offers no TPM 2.0: install swtpm on it." msgstr "Bu sunucu TPM 2.0 sunmuyor: üzerine swtpm kurun." -#: instances/templates/instances/settings_tab.html:965 +#: instances/templates/instances/settings_tab.html:983 msgid "When the guest crashes" msgstr "Konuk sistem çöktüğünde" -#: instances/templates/instances/settings_tab.html:966 +#: instances/templates/instances/settings_tab.html:984 msgid "" "For a crash the guest reports, not a hang. Stop keeps a VM that crashes " "again and again down for a look. From the next start." @@ -5260,39 +5288,39 @@ msgstr "" "tekrar tekrar çöken bir sanal makineyi incelemek için kapalı tutar. Sonraki " "başlatmadan itibaren." -#: instances/templates/instances/settings_tab.html:973 +#: instances/templates/instances/settings_tab.html:991 msgid "Restart" msgstr "Yeniden başlat" -#: instances/templates/instances/settings_tab.html:974 +#: instances/templates/instances/settings_tab.html:992 #: interfaces/templates/interface.html:16 networks/templates/network.html:16 #: storages/templates/storage.html:18 msgid "Stop" msgstr "Durdur" -#: instances/templates/instances/settings_tab.html:982 +#: instances/templates/instances/settings_tab.html:1000 #, python-format msgid "%(action)s: change it in the XML editor." msgstr "%(action)s: XML düzenleyicide değiştirin." -#: instances/templates/instances/settings_tab.html:990 +#: instances/templates/instances/settings_tab.html:1008 msgid "QEMU guest agent" msgstr "QEMU konuk ajanı" -#: instances/templates/instances/settings_tab.html:991 +#: instances/templates/instances/settings_tab.html:1009 msgid "The channel to the agent in the guest: a running VM gets it at once." msgstr "" "Konuk sistemdeki ajana giden kanal: çalışan bir sanal makine onu hemen alır." -#: instances/templates/instances/settings_tab.html:992 +#: instances/templates/instances/settings_tab.html:1010 msgid "Agent" msgstr "Ajan" -#: instances/templates/instances/settings_tab.html:993 +#: instances/templates/instances/settings_tab.html:1011 msgid "VM not running" msgstr "Sanal makine çalışmıyor" -#: instances/templates/instances/settings_tab.html:994 +#: instances/templates/instances/settings_tab.html:1012 msgid "No channel" msgstr "Kanal yok" @@ -5567,51 +5595,51 @@ msgstr "Disk dosyaları %(host)s üzerinde denetlenemiyor: %(err)s" msgid "None available device name" msgstr "Hiçbiri uygun aygıt adı değil" -#: instances/utils.py:345 +#: instances/utils.py:364 #, python-format msgid "%(hostname)s via %(address)s" msgstr "%(address)s üzerinden %(hostname)s" -#: instances/utils.py:384 +#: instances/utils.py:403 msgid "Invalid MAC address" msgstr "Geçersiz MAC adresi" -#: instances/utils.py:386 +#: instances/utils.py:405 msgid "More MAC addresses than networks" msgstr "Ağlardan daha fazla MAC adresi" -#: instances/views.py:143 +#: instances/views.py:166 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:149 instances/views.py:183 +#: instances/views.py:172 instances/views.py:206 msgid "Too many searches are still running; try again shortly." msgstr "Çok fazla arama hâlâ çalışıyor; birazdan yeniden deneyin." -#: instances/views.py:229 +#: instances/views.py:252 #, 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:235 +#: instances/views.py:258 #, 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:236 +#: instances/views.py:259 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:335 +#: instances/views.py:358 msgid "There is no other host to migrate to." msgstr "Taşınacak başka bir sunucu yok." -#: instances/views.py:337 +#: instances/views.py:360 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:339 +#: instances/views.py:362 msgid "" "A VM with snapshots cannot be migrated: its snapshots stay on this host. " "Delete them first." @@ -5619,7 +5647,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:341 +#: instances/views.py:364 msgid "" "A VM with host devices cannot be migrated: the destination may not have " "them. Remove them first." @@ -5627,11 +5655,11 @@ msgstr "" "Sunucu aygıtları olan bir sanal makine taşınamaz: hedefte bu aygıtlar " "olmayabilir. Önce onları kaldırın." -#: instances/views.py:607 +#: instances/views.py:630 msgid "The VM was moved meanwhile; try again." msgstr "Sanal makine bu arada taşındı; yeniden deneyin." -#: instances/views.py:638 +#: instances/views.py:661 msgid "" "The quota cannot be checked because a host of your virtual machines cannot " "be reached. Nothing was changed." @@ -5639,7 +5667,7 @@ msgstr "" "Sanal makinelerinizin bir sunucusuna erişilemediği için kota denetlenemiyor. " "Hiçbir şey değiştirilmedi." -#: instances/views.py:719 +#: instances/views.py:742 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 " @@ -5652,7 +5680,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:723 +#: instances/views.py:746 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 " @@ -5664,82 +5692,82 @@ 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:741 +#: instances/views.py:764 #, python-format msgid "Invalid disk options: %(options)s" msgstr "Geçersiz disk seçenekleri: %(options)s" -#: instances/views.py:751 instances/views.py:766 +#: instances/views.py:774 instances/views.py:789 msgid "Templates cannot be started." msgstr "Şablon makineler başlatılamaz." -#: instances/views.py:862 +#: instances/views.py:885 #, 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:868 +#: instances/views.py:891 #, 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:877 +#: instances/views.py:900 #, 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:999 +#: instances/views.py:1022 #, 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:1005 instances/views.py:1019 instances/views.py:1028 +#: instances/views.py:1028 instances/views.py:1042 instances/views.py:1051 #, python-format msgid "gstfsd error: %(err)s" msgstr "gstfsd hatası: %(err)s" -#: instances/views.py:1029 +#: instances/views.py:1052 msgid "the reply is not signed with the key" msgstr "yanıt anahtarla imzalanmamış" -#: instances/views.py:1037 +#: instances/views.py:1060 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:1050 +#: instances/views.py:1073 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:1081 +#: instances/views.py:1104 msgid "Enter a password" msgstr "Bir parola girin" -#: instances/views.py:1095 +#: instances/views.py:1118 msgid "Select an SSH public key" msgstr "Bir SSH açık anahtarı seçin" -#: instances/views.py:1117 +#: instances/views.py:1140 #, python-format msgid "Installed new SSH public key %(keyname)s" msgstr "Yeni %(keyname)s SSH açık anahtarı yüklendi" -#: instances/views.py:1137 +#: instances/views.py:1160 msgid "Enter a whole number of vCPUs" msgstr "vCPU sayısı için bir tam sayı girin" -#: instances/views.py:1141 +#: instances/views.py:1164 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:1144 +#: instances/views.py:1167 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:1152 +#: instances/views.py:1175 #, python-format msgid "" "User %(quota_msg)s quota reached, cannot resize CPU of '%(instance_name)s'!" @@ -5747,20 +5775,20 @@ msgstr "" "%(quota_msg)s kullanıcısı kotası doldu, '%(instance_name)s' in CPU su " "yeniden boyutlandırılamıyor!" -#: instances/views.py:1163 +#: instances/views.py:1186 #, 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:1182 +#: instances/views.py:1205 msgid "Enter a whole number for memory" msgstr "Bellek için bir tam sayı girin" -#: instances/views.py:1185 +#: instances/views.py:1208 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:1190 +#: instances/views.py:1213 #, python-format msgid "" "User %(quota_msg)s quota reached, cannot resize memory of " @@ -5769,25 +5797,25 @@ msgstr "" "%(quota_msg)s kullanıcı kotası doldu, '%(instance_name)s' in belleği yeniden " "boyutlandırılamıyor!" -#: instances/views.py:1199 +#: instances/views.py:1222 #, 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:1219 +#: instances/views.py:1242 msgid "Enter disk sizes as whole GiB" msgstr "Disk boyutlarını tam GiB olarak girin" -#: instances/views.py:1223 +#: instances/views.py:1246 #, 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:1231 +#: instances/views.py:1254 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:1241 +#: instances/views.py:1264 #, python-format msgid "" "User %(quota_msg)s quota reached, cannot resize disks of '%(instance_name)s'!" @@ -5795,61 +5823,61 @@ msgstr "" "%(quota_msg)s kullanıcı kotası doldu, '%(instance_name)s' in diski yeniden " "boyutlandırılamıyor!" -#: instances/views.py:1256 +#: instances/views.py:1279 #, 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:1258 +#: instances/views.py:1281 #, python-format msgid "Disk is resized: %(dev)s" msgstr "Disk boyutu değiştirildi: %(dev)s" -#: instances/views.py:1352 +#: instances/views.py:1375 msgid "The VM's machine has no PCIe slots to add" msgstr "Sanal makinenin makine tipinde eklenecek PCIe yuvası yok" -#: instances/views.py:1357 +#: instances/views.py:1380 #, python-format msgid "%(count)s PCIe slots added" msgstr "%(count)s PCIe yuvası eklendi" -#: instances/views.py:1359 +#: instances/views.py:1382 #, 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:1490 +#: instances/views.py:1513 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:1495 +#: instances/views.py:1518 msgid "Volume is changed successfully." msgstr "Birim başarıyla değiştirildi." -#: instances/views.py:1513 +#: instances/views.py:1536 #, 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:1520 +#: instances/views.py:1543 #, 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:1535 +#: instances/views.py:1558 #, 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:1547 +#: instances/views.py:1570 #, python-format msgid "" "The guest has not released disk %(dev)s yet; volume %(vol)s was kept. Delete " @@ -5858,52 +5886,52 @@ 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:1558 +#: instances/views.py:1581 #, 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:1584 +#: instances/views.py:1607 #, 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:1587 +#: instances/views.py:1610 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:1680 +#: instances/views.py:1703 #, python-format msgid "Snapshot was not deleted: %(err)s" msgstr "Anlık görüntü silinmedi: %(err)s" -#: instances/views.py:1795 +#: instances/views.py:1818 msgid "Check at least one device to boot from." msgstr "Önyükleme için en az bir aygıt işaretleyin." -#: instances/views.py:1809 +#: instances/views.py:1832 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:1811 +#: instances/views.py:1834 msgid "Boot order changed successfully." msgstr "Ön yükleme sırası başarıyla değiştirildi." -#: instances/views.py:1831 +#: instances/views.py:1854 msgid "The XML is empty" msgstr "XML boş" -#: instances/views.py:1835 instances/views.py:2452 +#: instances/views.py:1858 instances/views.py:2475 #, python-format msgid "The XML is not well-formed: %(error)s" msgstr "XML düzgün biçimlendirilmemiş: %(error)s" -#: instances/views.py:1837 +#: instances/views.py:1860 #, 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:1841 +#: instances/views.py:1864 msgid "" "The definition has changed since the editor was opened. Reload the page to " "edit the current one." @@ -5911,35 +5939,35 @@ 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:1853 +#: instances/views.py:1876 msgid "The XML definition was saved." msgstr "XML tanımı kaydedildi." -#: instances/views.py:1855 +#: instances/views.py:1878 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:1936 +#: instances/views.py:1959 msgid "Select one network interface to change" msgstr "Değiştirilecek bir ağ arabirimi seçin" -#: instances/views.py:1974 +#: instances/views.py:1997 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:1976 +#: instances/views.py:1999 msgid "Network device changed" msgstr "Ağ aygıtı değiştirildi" -#: instances/views.py:1988 +#: instances/views.py:2011 msgid "No network selected" msgstr "Ağ seçilmedi" -#: instances/views.py:2008 +#: instances/views.py:2031 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 " @@ -5949,21 +5977,21 @@ msgstr "" "aygıtı sonra ekleyin; Diskler sekmesindeki PCIe yuvaları ekle, sonraki " "başlatmadan itibaren ona boş yuvalar verir." -#: instances/views.py:2045 instances/views.py:2051 +#: instances/views.py:2068 instances/views.py:2074 msgid "The VM has it" msgstr "Sanal makinede var" -#: instances/views.py:2082 +#: instances/views.py:2105 #, python-format msgid "The host has no USB device %(device)s" msgstr "Sunucuda %(device)s USB aygıtı yok" -#: instances/views.py:2103 +#: instances/views.py:2126 #, python-format msgid "The host has no PCI device %(device)s" msgstr "Sunucuda %(device)s PCI aygıtı yok" -#: instances/views.py:2121 +#: instances/views.py:2144 #, python-format msgid "" "Removal of PCI device %(address)s requested: the guest has not let it go " @@ -5973,20 +6001,20 @@ msgstr "" "bırakmadı; bırakana ya da sanal makine durana kadar çalışan sanal makinede " "kalır." -#: instances/views.py:2166 instances/views.py:2196 +#: instances/views.py:2189 instances/views.py:2219 msgid "Select a network interface and a QoS direction" msgstr "Bir ağ arabirimi ve bir QoS yönü seçin" -#: instances/views.py:2169 +#: instances/views.py:2192 msgid "QoS rates must be whole numbers" msgstr "QoS hızları tam sayı olmalıdır" -#: instances/views.py:2176 networks/views.py:229 +#: instances/views.py:2199 networks/views.py:229 #, python-format msgid "%(qos_dir)s QoS is set" msgstr "%(qos_dir)s QoS ayarlandı" -#: instances/views.py:2181 +#: instances/views.py:2204 #, python-format msgid "" "%(qos_dir)s QoS is set. A running VM gets it after it is powered off and on." @@ -5994,12 +6022,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:2203 networks/views.py:255 +#: instances/views.py:2226 networks/views.py:255 #, python-format msgid "%(qos_dir)s QoS is deleted" msgstr "%(qos_dir)s QoS silindi" -#: instances/views.py:2208 +#: instances/views.py:2231 #, python-format msgid "" "%(qos_dir)s QoS is deleted. A running VM keeps the old limit until it is " @@ -6008,21 +6036,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:2236 +#: instances/views.py:2259 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:2238 +#: instances/views.py:2261 #, python-format msgid "%(user)s is already an owner" msgstr "%(user)s zaten bir sahip" -#: instances/views.py:2282 +#: instances/views.py:2305 #, python-format msgid "Console settings were not saved: %(errors)s" msgstr "Konsol ayarları kaydedilmedi: %(errors)s" -#: instances/views.py:2304 +#: instances/views.py:2327 msgid "" "Error setting console password. You should check that your instance have an " "graphic device." @@ -6030,67 +6058,67 @@ msgstr "" "Konsol parolası ayarlanırken hata oluştu. Sanal makinenin bir grafik " "aygıtına sahip olduğundan emin olunuz." -#: instances/views.py:2351 +#: instances/views.py:2374 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:2358 +#: instances/views.py:2381 #, python-format msgid "User %(quota_msg)s quota reached, '%(instance_name)s' stays a template!" msgstr "" "Kullanıcının %(quota_msg)s kotası doldu, '%(instance_name)s' şablon olarak " "kalıyor!" -#: instances/views.py:2440 +#: instances/views.py:2463 msgid "The XML has no name" msgstr "XML'de ad yok" -#: instances/views.py:2454 webvirtcloud/middleware.py:75 +#: instances/views.py:2477 webvirtcloud/middleware.py:75 #, python-format msgid "libvirt Error - %(exception)s" msgstr "libvirt Hatası - %(exception)s" -#: instances/views.py:2461 +#: instances/views.py:2484 msgid "An empty disk of the size you set" msgstr "Belirlediğiniz boyutta boş bir disk" -#: instances/views.py:2462 +#: instances/views.py:2485 msgid "Volumes already in a pool" msgstr "Bir havuzda zaten bulunan birimler" -#: instances/views.py:2463 +#: instances/views.py:2486 msgid "A copy of a template volume" msgstr "Bir şablon biriminin kopyası" -#: instances/views.py:2552 +#: instances/views.py:2575 msgid "You haven't defined any storage pools" msgstr "Hiç bir depolama havuzu tanımlamadınız" -#: instances/views.py:2554 +#: instances/views.py:2577 msgid "You haven't defined any network pools" msgstr "Hiç bir ağ havuzu tanımlamadınız" -#: instances/views.py:2617 +#: instances/views.py:2640 msgid "Flavor Created" msgstr "Flavor Oluşturuldu" -#: instances/views.py:2625 +#: instances/views.py:2648 msgid "Create Flavor" msgstr "Kullanıcı Oluştur" -#: instances/views.py:2640 +#: instances/views.py:2663 msgid "Flavor Updated" msgstr "Flavor Güncellendi" -#: instances/views.py:2646 +#: instances/views.py:2669 msgid "Update Flavor" msgstr "Flavor Güncelle" -#: instances/views.py:2658 +#: instances/views.py:2681 msgid "Flavor Deleted" msgstr "Flavor Silindi" -#: instances/views.py:2664 +#: instances/views.py:2687 msgid "Only the size template goes; VMs made from it keep their size." msgstr "" "Yalnızca boyut şablonu silinir; ondan oluşturulan sanal makineler " diff --git a/operations/static/operations/job_panel.js b/operations/static/operations/job_panel.js index 9a13e0b1..3353d33f 100644 --- a/operations/static/operations/job_panel.js +++ b/operations/static/operations/job_panel.js @@ -74,7 +74,8 @@ function jobPanel(element, urls, csrfToken, initialIds) { var note = job.result && job.result.warning ? '
' + esc(job.result.warning) + "
" : ""; var warn = note + (left.length ? '
Files not removed:
" : ""); - var buttons = '' + var buttons = (opens(job) ? 'Open' : "") + + '' + ''; return alertHtml(job.state, header(job.label, job.state, "", buttons) + errorHtml(job) + warn); @@ -114,6 +115,11 @@ function jobPanel(element, urls, csrfToken, initialIds) { holding = busy; } + // the page of what the job made, a path of this site + function opens(job) { + return !!(job.result && /^\/[^\/]/.test(job.result.open || "")); + } + // a job that did what it was asked, with nothing more to read: the page shows its effect function quiet(job) { var result = job.result || {}; @@ -134,7 +140,13 @@ function jobPanel(element, urls, csrfToken, initialIds) { var others = Object.keys(outcomes).length + Object.keys(pending).length + Object.keys(shown).length; // the page in use (poll.js): its outcome waits in the panel, with Refresh if (quiet(job) && !pageInUse() && !others) { - window.location.reload(); // the job is not remembered again: no loop + // a job that made a VM opens its page (result.open); another reloads this one, + // where the job is not remembered again: no loop + if (opens(job)) { + window.location.assign(job.result.open); + } else { + window.location.reload(); + } return; } outcomes[id] = job; diff --git a/operations/tests/js/job_panel.test.js b/operations/tests/js/job_panel.test.js index f56e34fd..86de38b0 100644 --- a/operations/tests/js/job_panel.test.js +++ b/operations/tests/js/job_panel.test.js @@ -19,7 +19,8 @@ function advance(ms) { const realNow = Date.now; Date.now = () => 1700000000000 + now; global.document = { hidden: false, addEventListener() {}, activeElement: null, querySelector: () => null }; -global.window = { backgroundJobRunning: false, location: { reload() { window.reloaded = true; } } }; +global.window = { backgroundJobRunning: false, + location: { reload() { window.reloaded = true; }, assign(url) { window.opened = url; } } }; // requests settle only when the test says so; callbacks run in registration order let open = []; @@ -264,6 +265,26 @@ answer("/j/u", { ...running, id: "u", label: "Upload", state: "succeeded" }); assert.ok(window.reloaded, "a job that succeeds without a warning reloads the page: it shows the job's effect"); assert.ok(!el11.innerHTML.includes('succeeded'), "not shown as well"); +// a job that made a VM opens its page; a warning keeps the outcome, with Open; only a path of this site +open = []; timers = []; window.reloaded = false; window.opened = null; +const elv = { innerHTML: "", addEventListener() {} }; +jobPanel(elv, urls, "tok", ["v"]); +advance(0); +answer("/r", empty); +answer("/j/v", { ...running, id: "v", label: "Clone VM", state: "succeeded", result: { open: "/instances/9/" } }); +assert.ok(window.opened === "/instances/9/" && !window.reloaded, "opens the clone's page"); +for (const [result, shown] of [[{ open: "/instances/9/", warning: "not started" }, true], + [{ open: "//evil.example.com/" }, false], [{ open: "javascript:alert(1)" }, false]]) { + open = []; timers = []; window.reloaded = false; window.opened = null; + const elw = { innerHTML: "", addEventListener() {} }; + jobPanel(elw, urls, "tok", ["w"]); + advance(0); + answer("/r", empty); + answer("/j/w", { ...running, id: "w", state: "succeeded", result }); + assert.strictEqual(window.opened, null, JSON.stringify(result)); + assert.strictEqual(elw.innerHTML.includes('href="/instances/9/">Open'), shown, JSON.stringify(result)); +} + // a dialog open or a field in use: the outcome waits in the panel, with Refresh for (const [label, doc] of [["dialog", { querySelector: (s) => s === ".modal.show" ? {} : null }], ["field", { activeElement: { matches: (s) => s === "input, select, textarea" } }]]) {