diff --git a/circle/common/operations.py b/circle/common/operations.py index 443cc87..d063352 100644 --- a/circle/common/operations.py +++ b/circle/common/operations.py @@ -38,15 +38,23 @@ class SubOperationMixin(object): parent, user, kwargs) +class WritableDocMeta(type): + pass + + class Operation(object): """Base class for VM operations. """ + + __metaclass__ = WritableDocMeta + async_queue = 'localhost.man' required_perms = None superuser_required = False do_not_call_in_templates = True abortable = False has_percentage = False + description = None @classmethod def get_activity_code_suffix(cls): @@ -219,10 +227,12 @@ operation_registry_name = '_ops' class OperatedMixin(object): - def __getattr__(self, name): - # NOTE: __getattr__ is only called if the attribute doesn't already - # exist in your __dict__ - return self.get_operation_class(name)(self) + + def __getattribute__(self, name): + ops = getattr(type(self), operation_registry_name, {}) + if name in ops: + return type(self).get_operation_class(name)(self) + return object.__getattribute__(self, name) @classmethod def get_operation_class(cls, name): @@ -261,6 +271,19 @@ class OperatedMixin(object): return None +def generate_op_doc(op, op_id): + doc = """ + Usage: %s(<args>) | %s.async(<args>) + Args: %s + + %s + """ % (op_id, op_id, + ", ".join(getargspec(op._operation)[0]), + op.__doc__) + + op.__doc__ = doc + + def register_operation(op_cls, op_id=None, target_cls=None): """Register the specified operation with the target class. @@ -297,5 +320,9 @@ def register_operation(op_cls, op_id=None, target_cls=None): if not hasattr(target_cls, operation_registry_name): setattr(target_cls, operation_registry_name, dict()) + op_cls.__doc__ = op_cls.description or "It has no documentation. :(" + op = op_cls(None) + generate_op_doc(op, op_id) + setattr(target_cls, op_id, op) getattr(target_cls, operation_registry_name)[op_id] = op_cls return op_cls diff --git a/circle/dashboard/tests/test_views.py b/circle/dashboard/tests/test_views.py index 73e4d67..34d8171 100644 --- a/circle/dashboard/tests/test_views.py +++ b/circle/dashboard/tests/test_views.py @@ -24,6 +24,7 @@ from django.contrib.auth.models import User, Group, Permission from django.contrib.auth import authenticate from common.tests.celery_mock import MockCeleryMixin +from common.operations import operation_registry_name from dashboard.views import VmAddInterfaceView from vm.models import Instance, InstanceTemplate, Lease, Node, Trait from vm.operations import (WakeUpOperation, AddInterfaceOperation, @@ -482,7 +483,7 @@ class VmDetailTest(LoginMixin, MockCeleryMixin, TestCase): patch.object(Instance.WrongStateError, 'send_message') as wro: inst = Instance.objects.get(pk=1) new_wake_up.side_effect = inst.wake_up - inst._wake_up_vm = Mock() + getattr(inst, operation_registry_name)["_wake_up_vm"] = Mock() inst.get_remote_queue_name = Mock(return_value='test') inst.status = 'SUSPENDED' inst.set_level(self.u2, 'owner') diff --git a/circle/vm/operations.py b/circle/vm/operations.py index 2c3632d..d021f9c 100644 --- a/circle/vm/operations.py +++ b/circle/vm/operations.py @@ -30,7 +30,9 @@ from urlparse import urlsplit from django.core.exceptions import PermissionDenied, SuspiciousOperation from django.core.urlresolvers import reverse from django.utils import timezone -from django.utils.translation import ugettext_lazy as _, ugettext_noop +from django.utils.translation import ( + ugettext_noop, ugettext_noop as _, ugettext_lazy +) from django.conf import settings from django.db.models import Q @@ -205,7 +207,7 @@ class RemoteAgentOperation(EnsureAgentMixin, RemoteInstanceOperation): @register_operation class AddInterfaceOperation(InstanceOperation): id = 'add_interface' - name = _("add interface") + name = ugettext_lazy("add interface") description = _("Add a new network interface for the specified VLAN to " "the VM.") required_perms = () @@ -250,7 +252,7 @@ class AddInterfaceOperation(InstanceOperation): class CreateDiskOperation(InstanceOperation): id = 'create_disk' - name = _("create disk") + name = ugettext_lazy("create disk") description = _("Create and attach empty disk to the virtual machine.") required_perms = ('storage.create_empty_disk', ) accept_states = ('STOPPED', 'PENDING', 'RUNNING') @@ -287,7 +289,7 @@ class CreateDiskOperation(InstanceOperation): class ResizeDiskOperation(RemoteInstanceOperation): id = 'resize_disk' - name = _("resize disk") + name = ugettext_lazy("resize disk") description = _("Resize the virtual disk image. " "Size must be greater value than the actual size.") required_perms = ('storage.resize_disk', ) @@ -317,7 +319,7 @@ class ResizeDiskOperation(RemoteInstanceOperation): @register_operation class DownloadDiskOperation(InstanceOperation): id = 'download_disk' - name = _("download disk") + name = ugettext_lazy("download disk") description = _("Download and attach disk image (ISO file) for the " "virtual machine. Most operating systems do not detect a " "new optical drive, so you may have to reboot the " @@ -354,7 +356,7 @@ class DownloadDiskOperation(InstanceOperation): @register_operation class DeployOperation(InstanceOperation): id = 'deploy' - name = _("deploy") + name = ugettext_lazy("deploy") description = _("Deploy and start the virtual machine (including storage " "and network configuration).") required_perms = () @@ -417,7 +419,7 @@ class DeployOperation(InstanceOperation): @register_operation class DeployVmOperation(SubOperationMixin, RemoteInstanceOperation): id = "_deploy_vm" - name = _("deploy vm") + name = ugettext_lazy("deploy vm") description = _("Deploy virtual machine.") remote_queue = ("vm", "slow") task = vm_tasks.deploy @@ -434,7 +436,7 @@ class DeployOperation(InstanceOperation): @register_operation class DeployDisksOperation(SubOperationMixin, InstanceOperation): id = "_deploy_disks" - name = _("deploy disks") + name = ugettext_lazy("deploy disks") description = _("Deploy all associated disks.") def _operation(self): @@ -452,7 +454,7 @@ class DeployOperation(InstanceOperation): @register_operation class ResumeVmOperation(SubOperationMixin, RemoteInstanceOperation): id = "_resume_vm" - name = _("boot virtual machine") + name = ugettext_lazy("boot virtual machine") remote_queue = ("vm", "slow") task = vm_tasks.resume @@ -460,7 +462,7 @@ class DeployOperation(InstanceOperation): @register_operation class DestroyOperation(InstanceOperation): id = 'destroy' - name = _("destroy") + name = ugettext_lazy("destroy") description = _("Permanently destroy virtual machine, its network " "settings and disks.") required_perms = () @@ -503,7 +505,7 @@ class DestroyOperation(InstanceOperation): @register_operation class DeleteVmOperation(SubOperationMixin, RemoteInstanceOperation): id = "_delete_vm" - name = _("destroy virtual machine") + name = ugettext_lazy("destroy virtual machine") task = vm_tasks.destroy # if e.libvirtError and "Domain not found" in str(e): @@ -511,7 +513,7 @@ class DestroyOperation(InstanceOperation): class DeleteMemDumpOperation(RemoteOperationMixin, SubOperationMixin, InstanceOperation): id = "_delete_mem_dump" - name = _("removing memory dump") + name = ugettext_lazy("removing memory dump") task = storage_tasks.delete_dump def _get_remote_queue(self): @@ -525,7 +527,7 @@ class DestroyOperation(InstanceOperation): @register_operation class MigrateOperation(RemoteInstanceOperation): id = 'migrate' - name = _("migrate") + name = ugettext_lazy("migrate") description = _("Move a running virtual machine to an other worker node " "keeping its full state.") required_perms = () @@ -585,7 +587,7 @@ class MigrateOperation(RemoteInstanceOperation): @register_operation class RebootOperation(RemoteInstanceOperation): id = 'reboot' - name = _("reboot") + name = ugettext_lazy("reboot") description = _("Warm reboot virtual machine by sending Ctrl+Alt+Del " "signal to its console.") required_perms = () @@ -602,7 +604,7 @@ class RebootOperation(RemoteInstanceOperation): @register_operation class RemoveInterfaceOperation(InstanceOperation): id = 'remove_interface' - name = _("remove interface") + name = ugettext_lazy("remove interface") description = _("Remove the specified network interface and erase IP " "address allocations, related firewall rules and " "hostnames.") @@ -626,7 +628,7 @@ class RemoveInterfaceOperation(InstanceOperation): @register_operation class RemovePortOperation(InstanceOperation): id = 'remove_port' - name = _("close port") + name = ugettext_lazy("close port") description = _("Close the specified port.") concurrency_check = False acl_level = "operator" @@ -645,7 +647,7 @@ class RemovePortOperation(InstanceOperation): @register_operation class AddPortOperation(InstanceOperation): id = 'add_port' - name = _("open port") + name = ugettext_lazy("open port") description = _("Open the specified port.") concurrency_check = False acl_level = "operator" @@ -663,7 +665,7 @@ class AddPortOperation(InstanceOperation): @register_operation class RemoveDiskOperation(InstanceOperation): id = 'remove_disk' - name = _("remove disk") + name = ugettext_lazy("remove disk") description = _("Remove the specified disk from the virtual machine, and " "destroy the data.") required_perms = () @@ -687,7 +689,7 @@ class RemoveDiskOperation(InstanceOperation): @register_operation class ResetOperation(RemoteInstanceOperation): id = 'reset' - name = _("reset") + name = ugettext_lazy("reset") description = _("Cold reboot virtual machine (power cycle).") required_perms = () accept_states = ('RUNNING', ) @@ -703,7 +705,7 @@ class ResetOperation(RemoteInstanceOperation): @register_operation class SaveAsTemplateOperation(InstanceOperation): id = 'save_as_template' - name = _("save as template") + name = ugettext_lazy("save as template") description = _("Save virtual machine as a template so they can be shared " "with users and groups. Anyone who has access to a " "template (and to the networks it uses) will be able to " @@ -815,7 +817,7 @@ class SaveAsTemplateOperation(InstanceOperation): class ShutdownOperation(AbortableRemoteOperationMixin, RemoteInstanceOperation): id = 'shutdown' - name = _("shutdown") + name = ugettext_lazy("shutdown") description = _("Try to halt virtual machine by a standard ACPI signal, " "allowing the operating system to keep a consistent " "state. The operation will fail if the machine does not " @@ -847,7 +849,7 @@ class ShutdownOperation(AbortableRemoteOperationMixin, @register_operation class ShutOffOperation(InstanceOperation): id = 'shut_off' - name = _("shut off") + name = ugettext_lazy("shut off") description = _("Forcibly halt a virtual machine without notifying the " "operating system. This operation will even work in cases " "when shutdown does not, but the operating system and the " @@ -873,7 +875,7 @@ class ShutOffOperation(InstanceOperation): @register_operation class SleepOperation(InstanceOperation): id = 'sleep' - name = _("sleep") + name = ugettext_lazy("sleep") description = _("Suspend virtual machine. This means the machine is " "stopped and its memory is saved to disk, so if the " "machine is waked up, all the applications will keep " @@ -908,7 +910,7 @@ class SleepOperation(InstanceOperation): @register_operation class SuspendVmOperation(SubOperationMixin, RemoteInstanceOperation): id = "_suspend_vm" - name = _("suspend virtual machine") + name = ugettext_lazy("suspend virtual machine") task = vm_tasks.sleep remote_queue = ("vm", "slow") remote_timeout = 1000 @@ -922,7 +924,7 @@ class SleepOperation(InstanceOperation): @register_operation class WakeUpOperation(InstanceOperation): id = 'wake_up' - name = _("wake up") + name = ugettext_lazy("wake up") description = _("Wake up sleeping (suspended) virtual machine. This will " "load the saved memory of the system and start the " "virtual machine from this state.") @@ -962,7 +964,7 @@ class WakeUpOperation(InstanceOperation): @register_operation class WakeUpVmOperation(SubOperationMixin, RemoteInstanceOperation): id = "_wake_up_vm" - name = _("resume virtual machine") + name = ugettext_lazy("resume virtual machine") task = vm_tasks.wake_up remote_queue = ("vm", "slow") remote_timeout = 1000 @@ -976,7 +978,7 @@ class WakeUpOperation(InstanceOperation): @register_operation class RenewOperation(InstanceOperation): id = 'renew' - name = _("renew") + name = ugettext_lazy("renew") description = _("Virtual machines are suspended and destroyed after they " "expire. This operation renews expiration times according " "to the lease type. If the machine is close to the " @@ -1032,7 +1034,7 @@ class RenewOperation(InstanceOperation): @register_operation class ChangeStateOperation(InstanceOperation): id = 'emergency_change_state' - name = _("emergency state change") + name = ugettext_lazy("emergency state change") description = _("Change the virtual machine state to NOSTATE. This " "should only be used if manual intervention was needed in " "the virtualization layer, and the machine has to be " @@ -1061,7 +1063,7 @@ class ChangeStateOperation(InstanceOperation): @register_operation class RedeployOperation(InstanceOperation): id = 'redeploy' - name = _("redeploy") + name = ugettext_lazy("redeploy") description = _("Change the virtual machine state to NOSTATE " "and redeploy the VM. This operation allows starting " "machines formerly running on a failed node.") @@ -1125,7 +1127,7 @@ class NodeOperation(Operation): @register_operation class ResetNodeOperation(NodeOperation): id = 'reset' - name = _("reset") + name = ugettext_lazy("reset") description = _("Disable missing node and redeploy all instances " "on other ones.") required_perms = () @@ -1154,7 +1156,7 @@ class ResetNodeOperation(NodeOperation): @register_operation class FlushOperation(NodeOperation): id = 'flush' - name = _("flush") + name = ugettext_lazy("flush") description = _("Passivate node and move all instances to other ones.") required_perms = () async_queue = "localhost.man.slow" @@ -1174,7 +1176,7 @@ class FlushOperation(NodeOperation): @register_operation class ActivateOperation(NodeOperation): id = 'activate' - name = _("activate") + name = ugettext_lazy("activate") description = _("Make node active, i.e. scheduler is allowed to deploy " "virtual machines to it.") required_perms = () @@ -1195,7 +1197,7 @@ class ActivateOperation(NodeOperation): @register_operation class PassivateOperation(NodeOperation): id = 'passivate' - name = _("passivate") + name = ugettext_lazy("passivate") description = _("Make node passive, i.e. scheduler is denied to deploy " "virtual machines to it, but remaining instances and " "the ones manually migrated will continue running.") @@ -1217,7 +1219,7 @@ class PassivateOperation(NodeOperation): @register_operation class DisableOperation(NodeOperation): id = 'disable' - name = _("disable") + name = ugettext_lazy("disable") description = _("Disable node.") required_perms = () online_required = False @@ -1241,7 +1243,7 @@ class DisableOperation(NodeOperation): @register_operation class UpdateNodeOperation(NodeOperation): id = 'update_node' - name = _("update node") + name = ugettext_lazy("update node") description = _("Upgrade or install node software (vmdriver, agentdriver, " "monitor-client) with Salt.") required_perms = () @@ -1314,7 +1316,7 @@ class UpdateNodeOperation(NodeOperation): @register_operation class ScreenshotOperation(RemoteInstanceOperation): id = 'screenshot' - name = _("screenshot") + name = ugettext_lazy("screenshot") description = _("Get a screenshot about the virtual machine's console. A " "key will be pressed on the keyboard to stop " "screensaver.") @@ -1327,7 +1329,7 @@ class ScreenshotOperation(RemoteInstanceOperation): @register_operation class RecoverOperation(InstanceOperation): id = 'recover' - name = _("recover") + name = ugettext_lazy("recover") description = _("Try to recover virtual machine disks from destroyed " "state. Network resources (allocations) are already lost, " "so you will have to manually add interfaces afterwards.") @@ -1368,7 +1370,7 @@ class RecoverOperation(InstanceOperation): @register_operation class ResourcesOperation(InstanceOperation): id = 'resources_change' - name = _("resources change") + name = ugettext_lazy("resources change") description = _("Change resources of a stopped virtual machine.") acl_level = "owner" required_perms = ('vm.change_resources', ) @@ -1405,7 +1407,7 @@ class ResourcesOperation(InstanceOperation): @register_operation class PasswordResetOperation(RemoteAgentOperation): id = 'password_reset' - name = _("password reset") + name = ugettext_lazy("password reset") description = _("Generate and set a new login password on the virtual " "machine. This operation requires the agent running. " "Resetting the password is not warranted to allow you " @@ -1430,7 +1432,7 @@ class PasswordResetOperation(RemoteAgentOperation): @register_operation class InstallKeysOperation(RemoteAgentOperation): id = 'install_keys' - name = _("install SSH keys") + name = ugettext_lazy("install SSH keys") description = _("Copy your public keys to the virtual machines. " "Only works on UNIX-like operating systems.") acl_level = "user" @@ -1447,7 +1449,7 @@ class InstallKeysOperation(RemoteAgentOperation): @register_operation class RemoveKeysOperation(RemoteAgentOperation): id = 'remove_keys' - name = _("remove SSH keys") + name = ugettext_lazy("remove SSH keys") acl_level = "user" task = agent_tasks.del_keys required_perms = () @@ -1460,7 +1462,7 @@ class RemoveKeysOperation(RemoteAgentOperation): @register_operation class AgentStartedOperation(InstanceOperation): id = 'agent_started' - name = _("agent") + name = ugettext_lazy("agent") acl_level = "owner" required_perms = () concurrency_check = False @@ -1537,13 +1539,13 @@ class AgentStartedOperation(InstanceOperation): @register_operation class CleanupOperation(SubOperationMixin, RemoteAgentOperation): id = '_cleanup' - name = _("cleanup") + name = ugettext_lazy("cleanup") task = agent_tasks.cleanup @register_operation class SetTimeOperation(SubOperationMixin, RemoteAgentOperation): id = '_set_time' - name = _("set time") + name = ugettext_lazy("set time") task = agent_tasks.set_time def _get_remote_args(self, **kwargs): @@ -1554,7 +1556,7 @@ class AgentStartedOperation(InstanceOperation): @register_operation class SetHostnameOperation(SubOperationMixin, RemoteAgentOperation): id = '_set_hostname' - name = _("set hostname") + name = ugettext_lazy("set hostname") task = agent_tasks.set_hostname def _get_remote_args(self, **kwargs): @@ -1565,13 +1567,13 @@ class AgentStartedOperation(InstanceOperation): @register_operation class RestartNetworkingOperation(SubOperationMixin, RemoteAgentOperation): id = '_restart_networking' - name = _("restart networking") + name = ugettext_lazy("restart networking") task = agent_tasks.restart_networking @register_operation class ChangeIpOperation(SubOperationMixin, RemoteAgentOperation): id = '_change_ip' - name = _("change ip") + name = ugettext_lazy("change ip") task = agent_tasks.change_ip def _get_remote_args(self, **kwargs): @@ -1586,7 +1588,7 @@ class AgentStartedOperation(InstanceOperation): @register_operation class UpdateAgentOperation(RemoteAgentOperation): id = 'update_agent' - name = _("update agent") + name = ugettext_lazy("update agent") acl_level = "owner" required_perms = () @@ -1675,7 +1677,7 @@ class UpdateAgentOperation(RemoteAgentOperation): @register_operation class MountStoreOperation(EnsureAgentMixin, InstanceOperation): id = 'mount_store' - name = _("mount store") + name = ugettext_lazy("mount store") description = _( "This operation attaches your personal file store. Other users who " "have access to this machine can see these files as well." @@ -1711,7 +1713,7 @@ class AbstractDiskOperation(SubOperationMixin, RemoteInstanceOperation): @register_operation class AttachDisk(AbstractDiskOperation): id = "_attach_disk" - name = _("attach disk") + name = ugettext_lazy("attach disk") task = vm_tasks.attach_disk @@ -1732,7 +1734,7 @@ class DetachMixin(object): @register_operation class DetachDisk(DetachMixin, AbstractDiskOperation): id = "_detach_disk" - name = _("detach disk") + name = ugettext_lazy("detach disk") task = vm_tasks.detach_disk @@ -1747,12 +1749,12 @@ class AbstractNetworkOperation(SubOperationMixin, RemoteInstanceOperation): @register_operation class AttachNetwork(AbstractNetworkOperation): id = "_attach_network" - name = _("attach network") + name = ugettext_lazy("attach network") task = vm_tasks.attach_network @register_operation class DetachNetwork(DetachMixin, AbstractNetworkOperation): id = "_detach_network" - name = _("detach network") + name = ugettext_lazy("detach network") task = vm_tasks.detach_network