Repository: SaaShup/netbox-docker-plugin (language: python)

Issue:
delete a running container
| Netbox version | Plugin Version | Agent version |  Docker API Version |
| --- | --- | --- | --- |
|  3.7.1 | 1.6.2 | 20.0 | 1.43 |

## Actual Behavior

Today it is possible to request to delete a running container in the plugin but it raises an error in the agent :

 {"message":"You cannot remove a running container d199e8e066f49321ddea958aa8bfe20285f563e0140090fa80ea491e905de3f9. Stop the container before attempting removal or force remove"}

## Desired Behavior

Possible options are : 
- prevent deletion running container from the plugin
- force stop before deletion in the agent

opinions welcome


Discussion / hints:
I would implement the feature inside the plugin. We should not force a stop in the agent if it's not asked to do so

The agent should only do what it is asked for and not implement some hidden tasks

Fix patch:
diff --git a/netbox_docker_plugin/__init__.py b/netbox_docker_plugin/__init__.py
index 423fdab..113fac6 100644
--- a/netbox_docker_plugin/__init__.py
+++ b/netbox_docker_plugin/__init__.py
@@ -10,12 +10,14 @@ class NetBoxDockerConfig(PluginConfig):
     name = "netbox_docker_plugin"
     verbose_name = " NetBox Docker Plugin"
     description = "Manage Docker"
-    version = "1.7.0"
+    version = "1.8.0"
     base_url = "docker"
-    author= "Vincent Simonin"
-    author_email= "vincent@saashup.com"
+    author= "Vincent Simonin <vincent@saashup.com>, David Delassus <david.jose.delassus@gmail.com>"
+    author_email= "vincent@saashup.com, david.jose.delassus@gmail.com"
 
     def ready(self):
+        from . import signals # pylint: disable=unused-import, import-outside-toplevel
+
         post_migrate.connect(create_webhook)
 
         super().ready()
diff --git a/netbox_docker_plugin/forms/container.py b/netbox_docker_plugin/forms/container.py
index e2e9155..628d520 100644
--- a/netbox_docker_plugin/forms/container.py
+++ b/netbox_docker_plugin/forms/container.py
@@ -14,11 +14,7 @@
 )
 from ..models.volume import Volume
 from ..models.host import Host
-from ..models.container import (
-    Container,
-    ContainerStateChoices,
-    ContainerRestartPolicyChoices,
-)
+from ..models.container import Container, ContainerRestartPolicyChoices
 from ..models.image import Image
 
 
@@ -44,7 +40,6 @@ class Meta:
             "image",
             "name",
             "hostname",
-            "state",
             "restart_policy",
             "tags",
         )
@@ -53,7 +48,6 @@ class Meta:
             "host": "Host",
             "image": "Image",
             "hostname": "Hostname",
-            "state": "State",
             "restart_policy": "Restart Policy",
         }
 
@@ -79,9 +73,6 @@ class ContainerFilterForm(NetBoxModelFilterSetForm):
         required=False,
         label="Image",
     )
-    state = forms.ChoiceField(
-        label="State", choices=ContainerStateChoices, required=False
-    )
     restart_policy = forms.ChoiceField(
         label="Restart Policy", choices=ContainerRestartPolicyChoices, required=False
     )
@@ -91,13 +82,6 @@ class ContainerFilterForm(NetBoxModelFilterSetForm):
 class ContainerImportForm(NetBoxModelImportForm):
     """Container importation form definition class"""
 
-    state = forms.ChoiceField(
-        label="State",
-        choices=ContainerStateChoices,
-        required=False,
-        help_text="Container State. Can be `created`, `restarting`, "
-        + "`running`, `paused`, `exited` or `dead`.",
-    )
     restart_policy = forms.ChoiceField(
         label="Restart Policy",
         choices=ContainerRestartPolicyChoices,
@@ -114,7 +98,6 @@ class Meta:
             "name",
             "host",
             "image",
-            "state",
             "hostname",
         )
         labels = {
@@ -127,11 +110,6 @@ class Meta:
 class ContainerBulkEditForm(NetBoxModelBulkEditForm):
     """Container bulk edit form definition class"""
 
-    state = forms.ChoiceField(
-        label="State",
-        choices=ContainerStateChoices,
-        required=True,
-    )
     restart_policy = forms.ChoiceField(
         label="Restart Policy",
         choices=ContainerRestartPolicyChoices,
@@ -142,7 +120,7 @@ class ContainerBulkEditForm(NetBoxModelBulkEditForm):
     )
 
     model = Container
-    fieldsets = (("General", ("state", "restart_policy", "hostname")),)
+    fieldsets = (("General", ("restart_policy", "hostname")),)
 
 
 class ContainerOperationForm(NetBoxModelForm):
diff --git a/netbox_docker_plugin/migrations/0029_alter_container_state.py b/netbox_docker_plugin/migrations/0029_alter_container_state.py
new file mode 100644
index 0000000..dce0552
--- /dev/null
+++ b/netbox_docker_plugin/migrations/0029_alter_container_state.py
@@ -0,0 +1,20 @@
+# pylint: disable=C0103
+"""Migration file"""
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+    """Migration file"""
+
+    dependencies = [
+        ("netbox_docker_plugin", "0028_mount_and_bind_read_only"),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name="container",
+            name="state",
+            field=models.CharField(default="none", max_length=32),
+        ),
+    ]
diff --git a/netbox_docker_plugin/models/container.py b/netbox_docker_plugin/models/container.py
index 88860ba..307874b 100644
--- a/netbox_docker_plugin/models/container.py
+++ b/netbox_docker_plugin/models/container.py
@@ -16,7 +16,7 @@
 from utilities.querysets import RestrictedQuerySet
 from netbox.models import NetBoxModel
 from .image import Image
-from .host import Host
+from .host import Host, HostStateChoices
 from .network import Network
 from .volume import Volume
 
@@ -26,7 +26,7 @@ class ContainerStateChoices(ChoiceSet):
 
     key = "Container.state"
 
-    DEFAULT_VALUE = "created"
+    DEFAULT_VALUE = "none"
 
     CHOICES = [
         ("created", "Created", "dark"),
@@ -133,6 +133,39 @@ class Container(NetBoxModel):
         default=ContainerRestartPolicyChoices.DEFAULT_VALUE,
     )
 
+    @property
+    def can_create(self) -> bool:
+        """ Check if the container can be created """
+        return self.state == "none"
+
+    @property
+    def can_start(self) -> bool:
+        """ Check if the container can be started """
+        return self.state in ["created", "exited", "dead"]
+
+    @property
+    def can_stop(self) -> bool:
+        """ Check if the container can be stopped """
+        return self.state in ["running"]
+
+    @property
+    def can_restart(self) -> bool:
+        """ Check if the container can be restarted """
+        return self.state in ["running"]
+
+    @property
+    def can_recreate(self) -> bool:
+        """ Check if the container can be recreated """
+        return self.state in ["created", "running", "exited", "dead"]
+
+    @property
+    def can_delete(self) -> bool:
+        """ Check if the container can be deleted """
+        return (
+            self.host.state == HostStateChoices.STATE_DELETED
+            or self.state in ["created", "paused", "exited", "dead"]
+        )
+
     class Meta:
         """Image Model Meta Class"""
 
diff --git a/netbox_docker_plugin/signals.py b/netbox_docker_plugin/signals.py
new file mode 100644
index 0000000..86b1e4c
--- /dev/null
+++ b/netbox_docker_plugin/signals.py
@@ -0,0 +1,33 @@
+""" Hooks on Django model signals. """
+
+from django.db.models.signals import pre_delete, pre_save
+from django.dispatch import receiver
+from utilities.exceptions import AbortRequest
+
+from .models import Container
+
+
+@receiver(pre_delete, sender=Container)
+def prevent_deleting_running_containers(instance, **_kwargs):
+    """ Abort deletion if container is running. """
+
+    if not instance.can_delete:
+        raise AbortRequest(
+            f"The container {instance.name} is running, please stop it before deletion"
+        )
+
+
+@receiver(pre_save, sender=Container)
+def restrict_container_actions(instance, **_kwargs):
+    """ Check if the container's state is correct for the given action """
+
+    if any([
+        instance.operation == "create" and not instance.can_create,
+        instance.operation == "start" and not instance.can_start,
+        instance.operation == "restart" and not instance.can_restart,
+        instance.operation == "stop" and not instance.can_stop,
+        instance.operation == "recreate" and not instance.can_recreate,
+    ]):
+        raise AbortRequest(
+            f"Cannot {instance.operation} container {instance.name} in state {instance.state}"
+        )
diff --git a/netbox_docker_plugin/templates/netbox_docker_plugin/container.html b/netbox_docker_plugin/templates/netbox_docker_plugin/container.html
index f6fbea6..424425f 100644
--- a/netbox_docker_plugin/templates/netbox_docker_plugin/container.html
+++ b/netbox_docker_plugin/templates/netbox_docker_plugin/container.html
@@ -6,7 +6,13 @@
 <form action="{% url 'plugins:netbox_docker_plugin:container_operation' pk=object.id operation='start' %}" method="post">
   {% csrf_token %}
   <input type="hidden" name="operation" value="start">
-  <button type="submit" class="btn btn-sm btn-success">
+  <button
+    type="submit"
+    class="btn btn-sm btn-success"
+    {% if not object.can_start %}
+    disabled
+    {% endif %}
+  >
     <i class="mdi mdi-play"></i> Start
   </button>
 </form>
@@ -14,7 +20,13 @@
 <form action="{% url 'plugins:netbox_docker_plugin:container_operation' pk=object.id operation='stop' %}" method="post">
   {% csrf_token %}
   <input type="hidden" name="operation" value="stop">
-  <button type="submit" class="btn btn-sm btn-danger">
+  <button
+    type="submit"
+    class="btn btn-sm btn-danger"
+    {% if not object.can_stop %}
+    disabled
+    {% endif %}
+  >
     <i class="mdi mdi-stop"></i> Stop
   </button>
 </form>
@@ -22,7 +34,13 @@
 <form action="{% url 'plugins:netbox_docker_plugin:container_operation' pk=object.id operation='restart' %}" method="post">
   {% csrf_token %}
   <input type="hidden" name="operation" value="restart">
-  <button type="submit" class="btn btn-sm btn-light">
+  <button
+    type="submit"
+    class="btn btn-sm btn-light"
+    {% if not object.can_restart %}
+    disabled
+    {% endif %}
+  >
     <i class="mdi mdi-restart"></i> Restart
   </button>
 </form>
@@ -30,7 +48,13 @@
 <form action="{% url 'plugins:netbox_docker_plugin:container_operation' pk=object.id operation='recreate' %}" method="post">
   {% csrf_token %}
   <input type="hidden" name="operation" value="recreate">
-  <button type="submit" class="btn btn-sm btn-dark">
+  <button
+    type="submit"
+    class="btn btn-sm btn-dark"
+    {% if not object.can_recreate %}
+    disabled
+    {% endif %}
+  >
     <i class="mdi mdi-autorenew"></i> Recreate
   </button>
 </form>
diff --git a/pyproject.toml b/pyproject.toml
index 91d101e..0617b4c 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,9 +4,10 @@ build-backend = "setuptools.build_meta"
 
 [project]
 name = "netbox-docker-plugin"
-version = "1.7.0"
+version = "1.8.0"
 authors = [
   { name="Vincent Simonin", email="vincent@saashup.com" },
+  { name="David Delassus", email="david.jose.delassus@gmail.com" }
 ]
 description = "Manage Docker with Netbox & style."
 readme = "README.md"


Test patch:
diff --git a/netbox_docker_plugin/tests/container/test_container_actions.py b/netbox_docker_plugin/tests/container/test_container_actions.py
new file mode 100644
index 0000000..246b050
--- /dev/null
+++ b/netbox_docker_plugin/tests/container/test_container_actions.py
@@ -0,0 +1,171 @@
+""" Container action restrictions test suite """
+
+from django.db import transaction
+from django.test import TestCase
+from utilities.exceptions import AbortRequest
+from netbox_docker_plugin.models.container import Container
+from netbox_docker_plugin.models.host import Host, HostStateChoices
+from netbox_docker_plugin.models.image import Image
+from netbox_docker_plugin.models.registry import Registry
+
+
+class ContainerActionTestCase(TestCase):
+    """ Test the various scenarii of container actions """
+
+    objects = {}
+
+    def test_allowed_operations(self):
+        """ Test that only specific operations are allowed according to a state """
+
+        cases = [
+            ("create", ["none"]),
+            ("start", ["created", "exited", "dead"]),
+            ("stop", ["running"]),
+            ("restart", ["running"]),
+            ("recreate", ["created", "exited", "dead", "running"]),
+            ("noop", ["created", "restarting", "running", "paused", "exited", "dead", "none"]),
+        ]
+
+        for operation, states in cases:
+            for state in states:
+                with self.subTest(operation=operation, state=state):
+                    with transaction.atomic():
+                        name = f"container-{operation}-{state}"
+                        obj = Container.objects.create(
+                            host=self.objects["host1"],
+                            image=self.objects["image2"],
+                            name=name,
+                            operation="none",
+                            state=state,
+                        )
+
+                        obj.operation = operation
+                        obj.save()
+
+                    self.assertEqual(
+                        Container.objects.get(name=name).operation,
+                        operation,
+                    )
+
+    def test_forbidden_operations(self):
+        """ Test that forbidden operations raise an AbortRequest """
+
+        cases = [
+            ("create", ["created", "restarting", "running", "paused", "exited", "dead"]),
+            ("start", ["running", "restarting", "paused", "none"]),
+            ("stop", ["created", "restarting", "paused", "exited", "dead", "none"]),
+            ("restart", ["created", "restarting", "paused", "exited", "dead", "none"]),
+            ("recreate", ["restarting", "paused", "none"]),
+        ]
+
+        for operation, states in cases:
+            for state in states:
+                with self.subTest(operation=operation, state=state):
+                    with transaction.atomic():
+                        with self.assertRaises(
+                            AbortRequest,
+                            msg=f"AbortRequest not raised for {state}",
+                        ):
+                            name = f"container-{operation}-{state}"
+                            obj = Container.objects.create(
+                                host=self.objects["host1"],
+                                image=self.objects["image2"],
+                                name=name,
+                                operation="none",
+                                state=state,
+                            )
+
+                            obj.operation = operation
+                            obj.save()
+
+    def test_delete_non_running_container(self):
+        """ Test that a non running container can be deleted """
+
+        for state in ["created", "paused", "exited", "dead"]:
+            with self.subTest(operation="delete", state=state):
+                with transaction.atomic():
+                    name = f"container-delete-{state}"
+                    obj = Container.objects.create(
+                        host=self.objects["host1"],
+                        image=self.objects["image2"],
+                        name=name,
+                        operation="none",
+                        state=state,
+                    )
+
+                    obj.delete()
+
+                self.assertFalse(Container.objects.filter(name=name).exists())
+
+    def test_delete_running_container(self):
+        """ Test that a running container cannot be deleted """
+
+        for state in ["running", "restarting", "none"]:
+            with self.subTest(operation="delete", state=state):
+                with transaction.atomic():
+                    with self.assertRaises(
+                        AbortRequest,
+                        msg=f"AbortRequest not raised for {state}",
+                    ):
+                        name = f"container-delete-{state}"
+                        obj = Container.objects.create(
+                            host=self.objects["host1"],
+                            image=self.objects["image2"],
+                            name=name,
+                            operation="none",
+                            state=state,
+                        )
+
+                        obj.delete()
+
+    def test_delete_container_on_deleted_host(self):
+        """ Test that a container on a deleted host can be deleted """
+
+        for state in ["created", "running", "restarted", "paused", "exited", "dead", "none"]:
+            with self.subTest(operation="delete", state=state):
+                with transaction.atomic():
+                    name = f"container-delete-{state}"
+                    obj = Container.objects.create(
+                        host=self.objects["host2"],
+                        image=self.objects["image2"],
+                        name=name,
+                        operation="none",
+                        state=state,
+                    )
+
+                    obj.delete()
+
+                self.assertFalse(Container.objects.filter(name=name).exists())
+
+    @classmethod
+    def setUpTestData(cls):
+        cls.objects["host1"] = Host.objects.create(
+            endpoint="http://localhost:8080",
+            name="host1",
+        )
+        cls.objects["registry1"] = Registry.objects.create(
+            host=cls.objects["host1"],
+            name="registry1",
+            serveraddress="http://localhost:8080",
+        )
+        cls.objects["image2"] = Image.objects.create(
+            host=cls.objects["host1"],
+            registry=cls.objects["registry1"],
+            name="image2",
+        )
+
+        cls.objects["host2"] = Host.objects.create(
+            endpoint="http://localhost:8080",
+            name="host2",
+            state=HostStateChoices.STATE_DELETED,
+        )
+        cls.objects["registry2"] = Registry.objects.create(
+            host=cls.objects["host2"],
+            name="registry2",
+            serveraddress="http://localhost:8080",
+        )
+        cls.objects["image2"] = Image.objects.create(
+            host=cls.objects["host2"],
+            registry=cls.objects["registry2"],
+            name="image2",
+        )
diff --git a/netbox_docker_plugin/tests/container/test_container_api.py b/netbox_docker_plugin/tests/container/test_container_api.py
index 39b2b99..209325a 100644
--- a/netbox_docker_plugin/tests/container/test_container_api.py
+++ b/netbox_docker_plugin/tests/container/test_container_api.py
@@ -1,3 +1,4 @@
+# pylint: disable=R0801
 """Container Test Case"""
 
 from utilities.testing import APIViewTestCases
@@ -64,9 +65,27 @@ def setUpTestData(cls) -> None:
         volume2 = Volume.objects.create(host=host1, name="volume2")
         volume3 = Volume.objects.create(host=host2, name="volume3")
 
-        Container.objects.create(host=host1, image=image1, name="container1")
-        Container.objects.create(host=host1, image=image1, name="container2")
-        Container.objects.create(host=host2, image=image2, name="container3")
+        Container.objects.create(
+            host=host1,
+            image=image1,
+            name="container1",
+            operation="none",
+            state="created",
+        )
+        Container.objects.create(
+            host=host1,
+            image=image1,
+            name="container2",
+            operation="none",
+            state="created",
+        )
+        Container.objects.create(
+            host=host2,
+            image=image2,
+            name="container3",
+            operation="none",
+            state="created",
+        )
 
         cls.create_data = [
             {
diff --git a/netbox_docker_plugin/tests/container/test_container_operation_view.py b/netbox_docker_plugin/tests/container/test_container_operation_view.py
index 53c9d4b..65ae5e5 100644
--- a/netbox_docker_plugin/tests/container/test_container_operation_view.py
+++ b/netbox_docker_plugin/tests/container/test_container_operation_view.py
@@ -1,3 +1,4 @@
+# pylint: disable=R0801
 """Container operation Views Test Case"""
 
 from django.urls import reverse
@@ -22,7 +23,7 @@ class ContainerViewsTestCase(BaseModelViewTestCase, ModelViewTestCase):
 
     def test_operation_start_object_without_permission(self):
         """Test operation start object without permission"""
-        instance = self._get_queryset().first()
+        instance = self._get_queryset().exclude(state="running").first()
 
         # Try GET without permission
         with disable_warnings("django.request"):
@@ -42,7 +43,7 @@ def test_operation_start_object_without_permission(self):
     @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
     def test_operation_start_object_with_permission(self):
         """Test operation start object with permission""" """  """
-        instance = self._get_queryset().first()
+        instance = self._get_queryset().exclude(state="running").first()
 
         # Assign model-level permission
         obj_perm = ObjectPermission(name="Test permission", actions=["change"])
@@ -78,7 +79,7 @@ def test_operation_start_object_with_permission(self):
 
     def test_operation_stop_object_without_permission(self):
         """Test operation stop object without permission"""
-        instance = self._get_queryset().first()
+        instance = self._get_queryset().filter(state="running").first()
 
         # Try GET without permission
         with disable_warnings("django.request"):
@@ -98,7 +99,7 @@ def test_operation_stop_object_without_permission(self):
     @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
     def test_operation_stop_object_with_permission(self):
         """Test operation stop object with permission""" """  """
-        instance = self._get_queryset().first()
+        instance = self._get_queryset().filter(state="running").first()
 
         # Assign model-level permission
         obj_perm = ObjectPermission(name="Test permission", actions=["change"])
@@ -134,7 +135,7 @@ def test_operation_stop_object_with_permission(self):
 
     def test_operation_restart_object_without_permission(self):
         """Test operation restart object without permission"""
-        instance = self._get_queryset().first()
+        instance = self._get_queryset().filter(state="running").first()
 
         # Try GET without permission
         with disable_warnings("django.request"):
@@ -154,7 +155,7 @@ def test_operation_restart_object_without_permission(self):
     @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
     def test_operation_restart_object_with_permission(self):
         """Test operation restart object with permission""" """  """
-        instance = self._get_queryset().first()
+        instance = self._get_queryset().filter(state="running").first()
 
         # Assign model-level permission
         obj_perm = ObjectPermission(name="Test permission", actions=["change"])
@@ -259,7 +260,31 @@ def setUpTestData(cls):
         image1 = Image.objects.create(host=host1, name="image1", registry=registry1)
         image2 = Image.objects.create(host=host2, name="image2", registry=registry2)
 
-        Container.objects.create(host=host1, image=image1, name="container1")
-        Container.objects.create(host=host1, image=image1, name="container2")
-        Container.objects.create(host=host2, image=image2, name="container3")
-        Container.objects.create(host=host2, image=image2, name="container4")
+        Container.objects.create(
+            host=host1,
+            image=image1,
+            name="container1",
+            operation="none",
+            state="created",
+        )
+        Container.objects.create(
+            host=host1,
+            image=image1,
+            name="container2",
+            operation="none",
+            state="created",
+        )
+        Container.objects.create(
+            host=host2,
+            image=image2,
+            name="container3",
+            operation="none",
+            state="created",
+        )
+        Container.objects.create(
+            host=host2,
+            image=image2,
+            name="container4",
+            operation="none",
+            state="running",
+        )
diff --git a/netbox_docker_plugin/tests/container/test_container_validation.py b/netbox_docker_plugin/tests/container/test_container_validation.py
index ca5ece5..450ff12 100644
--- a/netbox_docker_plugin/tests/container/test_container_validation.py
+++ b/netbox_docker_plugin/tests/container/test_container_validation.py
@@ -23,6 +23,8 @@ def test_that_container_is_created(self):
             host=self.objects["host1"],
             image=self.objects["image1"],
             name="container1",
+            operation="none",
+            state="created",
         )
 
         self.assertTrue(isinstance(container.id, int))
@@ -31,6 +33,8 @@ def test_that_container_is_created(self):
             host=self.objects["host2"],
             image=self.objects["image2"],
             name="container2",
+            operation="none",
+            state="created",
         )
 
         self.assertTrue(isinstance(container.id, int))
@@ -43,6 +47,8 @@ def test_that_container_image_must_be_on_the_same_host(self):
                 host=self.objects["host1"],
                 image=self.objects["image2"],
                 name="container2",
+                operation="none",
+                state="created",
             )
             container.clean()
 
@@ -63,6 +69,8 @@ def test_that_volume_mounted_must_be_on_the_same_container_host(self):
                 host=self.objects["host1"],
                 image=self.objects["image1"],
                 name="container3",
+                operation="none",
+                state="created",
             )
 
             mount = Mount(
@@ -88,6 +96,8 @@ def test_that_network_setting_must_be_on_the_same_container_host(self):
                 host=self.objects["host1"],
                 image=self.objects["image1"],
                 name="container3",
+                operation="none",
+                state="created",
             )
 
             network_setting = NetworkSetting(
diff --git a/netbox_docker_plugin/tests/container/test_container_views.py b/netbox_docker_plugin/tests/container/test_container_views.py
index 50457f3..4c201b0 100644
--- a/netbox_docker_plugin/tests/container/test_container_views.py
+++ b/netbox_docker_plugin/tests/container/test_container_views.py
@@ -1,3 +1,4 @@
+# pylint: disable=R0801
 """Container Views Test Case"""
 
 from utilities.testing import ViewTestCases
@@ -31,19 +32,39 @@ def setUpTestData(cls):
         image2 = Image.objects.create(host=host2, name="image2", registry=registry2)
 
         container1 = Container.objects.create(
-            host=host1, image=image1, name="container1", restart_policy="always"
+            host=host1,
+            image=image1,
+            name="container1",
+            restart_policy="always",
+            operation="none",
+            state="created",
         )
         container2 = Container.objects.create(
-            host=host1, image=image1, name="container2"
+            host=host1,
+            image=image1,
+            name="container2",
+            operation="none",
+            state="created",
+        )
+        Container.objects.create(
+            host=host2,
+            image=image2,
+            name="container3",
+            operation="none",
+            state="created",
+        )
+        Container.objects.create(
+            host=host2,
+            image=image2,
+            name="container4",
+            operation="none",
+            state="created",
         )
-        Container.objects.create(host=host2, image=image2, name="container3")
-        Container.objects.create(host=host2, image=image2, name="container4")
 
         cls.form_data = {
             "name": "container5",
             "host": host1.pk,
             "image": image1.pk,
-            "state": "created",
             "restart_policy": "unless-stopped",
         }
 
@@ -54,12 +75,12 @@ def setUpTestData(cls):
         )
 
         cls.bulk_edit_data = {
-            "state": "running",
+            "hostname": "h",
             "restart_policy": "always",
         }
 
         cls.csv_update_data = (
-            "id,name,host,image,state,hostname,restart_policy",
-            f"{container1.pk},container1,{host1.pk},{image1.pk},paused,,on-failure",
-            f"{container2.pk},container2,{host1.pk},{image1.pk},running,container2,unless-stopped",
+            "id,name,host,image,hostname,restart_policy",
+            f"{container1.pk},container1,{host1.pk},{image1.pk},,on-failure",
+            f"{container2.pk},container2,{host1.pk},{image1.pk},container2,unless-stopped",
         )