diff --git a/.github/workflows/main_ci.yml b/.github/workflows/main_ci.yml index 58bd57d..7a7b64d 100644 --- a/.github/workflows/main_ci.yml +++ b/.github/workflows/main_ci.yml @@ -3,7 +3,7 @@ name: main branch CI on: push: branches: - - "main" + - "main_v3" jobs: testing: @@ -12,7 +12,7 @@ jobs: max-parallel: 4 matrix: python-version: ["3.11"] - netbox-version: ["v4.1.0"] + netbox-version: ["v4.1.11"] services: redis: image: redis @@ -55,6 +55,11 @@ jobs: run: | ln -s $(pwd)/../netbox-docker-plugin/netbox_configuration/configuration_testing.py netbox/netbox/configuration.py python -m pip install --upgrade pip + # Les runners embarquent les en-tetes PostgreSQL 18, contre lesquels + # psycopg-c ne compile pas (macro HAVE_LONG_INT_64 supprimee). La roue + # binaire embarque sa propre libpq et evite toute compilation. + sed -i 's/^psycopg\[c,pool\]/psycopg[binary,pool]/' requirements.txt + grep -q '^psycopg\[binary,pool\]' requirements.txt pip install -r requirements.txt -U - name: Run Pylint run: | diff --git a/.github/workflows/pr_ci.yml b/.github/workflows/pr_ci.yml index c558685..660718c 100644 --- a/.github/workflows/pr_ci.yml +++ b/.github/workflows/pr_ci.yml @@ -3,7 +3,7 @@ name: Pull Request CI on: pull_request: branches: - - "main" + - "main_v3" jobs: testing: @@ -58,6 +58,11 @@ jobs: run: | ln -s $(pwd)/../netbox-docker-plugin/netbox_configuration/configuration_testing.py netbox/netbox/configuration.py python -m pip install --upgrade pip + # Les runners embarquent les en-tetes PostgreSQL 18, contre lesquels + # psycopg-c ne compile pas (macro HAVE_LONG_INT_64 supprimee). La roue + # binaire embarque sa propre libpq et evite toute compilation. + sed -i 's/^psycopg\[c,pool\]/psycopg[binary,pool]/' requirements.txt + grep -q '^psycopg\[binary,pool\]' requirements.txt pip install -r requirements.txt -U - name: Run Pylint run: | diff --git a/netbox_docker_plugin/__init__.py b/netbox_docker_plugin/__init__.py index 41576a6..3e2d5f3 100644 --- a/netbox_docker_plugin/__init__.py +++ b/netbox_docker_plugin/__init__.py @@ -11,7 +11,7 @@ class NetBoxDockerConfig(PluginConfig): name = "netbox_docker_plugin" verbose_name = " NetBox Docker Plugin" description = "Manage Docker" - version = "3.3.0" + version = "3.4.0" base_url = "docker" min_version = "4.1.0" author = "Vincent Simonin , David Delassus " diff --git a/netbox_docker_plugin/forms/container.py b/netbox_docker_plugin/forms/container.py index c0d0ab8..306c3c4 100644 --- a/netbox_docker_plugin/forms/container.py +++ b/netbox_docker_plugin/forms/container.py @@ -19,7 +19,6 @@ Container, ContainerRestartPolicyChoices, ContainerCapAddChoices, - ContainerLogDriverChoices, ) from ..models.image import Image @@ -121,9 +120,7 @@ class ContainerFilterForm(NetBoxModelFilterSetForm): restart_policy = forms.ChoiceField( label="Restart Policy", choices=ContainerRestartPolicyChoices, required=False ) - log_driver = forms.ChoiceField( - label="Logging driver", choices=ContainerLogDriverChoices, required=False - ) + log_driver = forms.CharField(label="Logging driver", required=False) tag = TagFilterField(model) @@ -138,11 +135,11 @@ class ContainerImportForm(NetBoxModelImportForm): + "`always`, `unless-stopped`.", ) - log_driver = forms.ChoiceField( + log_driver = forms.CharField( label="Logging driver", - choices=ContainerLogDriverChoices, required=False, - help_text="Logging driver. Can be `json-log`, `syslog`.", + help_text="Logging driver, as reported by Docker. For example " + + "`json-file`, `syslog`, `journald`, `local`.", ) class Meta: @@ -170,10 +167,9 @@ class ContainerBulkEditForm(NetBoxModelBulkEditForm): choices=ContainerRestartPolicyChoices, required=True, ) - log_driver = forms.ChoiceField( + log_driver = forms.CharField( label="Logging driver", - choices=ContainerLogDriverChoices, - required=True, + required=False, ) hostname = forms.CharField( label="Hostname", max_length=256, min_length=1, required=False diff --git a/netbox_docker_plugin/migrations/0036_alter_container_log_driver.py b/netbox_docker_plugin/migrations/0036_alter_container_log_driver.py new file mode 100644 index 0000000..0ffd31f --- /dev/null +++ b/netbox_docker_plugin/migrations/0036_alter_container_log_driver.py @@ -0,0 +1,32 @@ +# pylint: disable=C0103 +"""Migration file""" + +from django.db import migrations, models + + +class Migration(migrations.Migration): + """Make Container.log_driver a free-form value. + + The agent reports HostConfig.LogConfig.Type verbatim from the Docker + daemon (json-file, journald, local, ...), which the previous two-value + choice set rejected. The legacy `json-log` value was never a valid Docker + driver name and is normalised to `json-file`. + """ + + dependencies = [ + ("netbox_docker_plugin", "0035_container_log_driver_logdriveroption"), + ] + + operations = [ + migrations.RunSQL( + sql="UPDATE netbox_docker_plugin_container " + "SET log_driver='json-file' WHERE log_driver='json-log'", + reverse_sql="UPDATE netbox_docker_plugin_container " + "SET log_driver='json-log' WHERE log_driver='json-file'", + ), + migrations.AlterField( + model_name="container", + name="log_driver", + field=models.CharField(max_length=32, null=True, blank=True), + ), + ] diff --git a/netbox_docker_plugin/migrations/0037_alter_env_value.py b/netbox_docker_plugin/migrations/0037_alter_env_value.py new file mode 100644 index 0000000..0171fd7 --- /dev/null +++ b/netbox_docker_plugin/migrations/0037_alter_env_value.py @@ -0,0 +1,32 @@ +# pylint: disable=C0103 +"""Migration file""" + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + """Raise Env.value to 32768 characters. + + The agent imports environment variables verbatim from the Docker daemon; + inline configuration and certificate material routinely exceed the previous + 4096 character ceiling. + """ + + dependencies = [ + ("netbox_docker_plugin", "0036_alter_container_log_driver"), + ] + + operations = [ + migrations.AlterField( + model_name="env", + name="value", + field=models.CharField( + blank=True, + max_length=32768, + validators=[ + django.core.validators.MaxLengthValidator(limit_value=32768) + ], + ), + ), + ] diff --git a/netbox_docker_plugin/migrations/0038_container_cmd.py b/netbox_docker_plugin/migrations/0038_container_cmd.py new file mode 100644 index 0000000..35ec8fd --- /dev/null +++ b/netbox_docker_plugin/migrations/0038_container_cmd.py @@ -0,0 +1,26 @@ +# pylint: disable=C0103 +"""Migration file""" + +import django.contrib.postgres.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + """Migration file""" + + dependencies = [ + ("netbox_docker_plugin", "0037_alter_env_value"), + ] + + operations = [ + migrations.AddField( + model_name="container", + name="cmd", + field=django.contrib.postgres.fields.ArrayField( + base_field=models.CharField(blank=True, max_length=1024, null=True), + blank=True, + null=True, + size=None, + ), + ), + ] diff --git a/netbox_docker_plugin/migrations/1039_alter_port_public_port.py b/netbox_docker_plugin/migrations/1039_alter_port_public_port.py new file mode 100644 index 0000000..cca1bbd --- /dev/null +++ b/netbox_docker_plugin/migrations/1039_alter_port_public_port.py @@ -0,0 +1,30 @@ +# pylint: disable=C0103 +"""Migration file""" + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + """Allow Port.public_port to be -1. + + The agent uses -1 to mean "exposed but not published"; the previous floor + of 0 rejected every container with an unpublished exposed port. + """ + + dependencies = [ + ("netbox_docker_plugin", "0038_container_cmd"), + ] + + operations = [ + migrations.AlterField( + model_name="port", + name="public_port", + field=models.IntegerField( + validators=[ + django.core.validators.MinValueValidator(limit_value=-1), + django.core.validators.MaxValueValidator(limit_value=65535), + ] + ), + ), + ] diff --git a/netbox_docker_plugin/models/container.py b/netbox_docker_plugin/models/container.py index c1619ea..bb83bf9 100644 --- a/netbox_docker_plugin/models/container.py +++ b/netbox_docker_plugin/models/container.py @@ -73,19 +73,6 @@ class ContainerRestartPolicyChoices(ChoiceSet): ] -class ContainerLogDriverChoices(ChoiceSet): - """Container log driver choices definition class""" - - key = "Container.log_driver" - - DEFAULT_VALUE = "json-log" - - CHOICES = [ - ("json-log", "json-log", "blue"), - ("syslog", "syslog", "blue"), - ] - - class PortTypeChoices(ChoiceSet): """Port type choices definition class""" @@ -105,7 +92,27 @@ class ContainerCapAddChoices(ChoiceSet): key = "Container.cap_add" CHOICES = [ + ("AUDIT_WRITE", "AUDIT_WRITE"), + ("CHOWN", "CHOWN"), + ("DAC_OVERRIDE", "DAC_OVERRIDE"), + ("DAC_READ_SEARCH", "DAC_READ_SEARCH"), + ("FOWNER", "FOWNER"), + ("FSETID", "FSETID"), + ("KILL", "KILL"), + ("MKNOD", "MKNOD"), ("NET_ADMIN", "NET_ADMIN"), + ("NET_BIND_SERVICE", "NET_BIND_SERVICE"), + ("NET_RAW", "NET_RAW"), + ("SETFCAP", "SETFCAP"), + ("SETGID", "SETGID"), + ("SETPCAP", "SETPCAP"), + ("SETUID", "SETUID"), + ("SYS_ADMIN", "SYS_ADMIN"), + ("SYS_CHROOT", "SYS_CHROOT"), + ("SYS_NICE", "SYS_NICE"), + ("SYS_PTRACE", "SYS_PTRACE"), + ("SYS_RESOURCE", "SYS_RESOURCE"), + ("ALL", "ALL"), ] @@ -168,8 +175,13 @@ class Container(NetBoxModel): ) log_driver = models.CharField( max_length=32, - choices=ContainerLogDriverChoices, - default=ContainerLogDriverChoices.DEFAULT_VALUE, + null=True, + blank=True, + ) + cmd = ArrayField( + models.CharField(max_length=1024, blank=True, null=True), + null=True, + blank=True, ) @property @@ -261,7 +273,7 @@ class Port(models.Model): ) public_port = models.IntegerField( validators=[ - MinValueValidator(limit_value=0), + MinValueValidator(limit_value=-1), MaxValueValidator(limit_value=65535), ], ) @@ -345,9 +357,9 @@ class Env(models.Model): ) value = models.CharField( blank=True, - max_length=4096, + max_length=32768, validators=[ - MaxLengthValidator(limit_value=4096), + MaxLengthValidator(limit_value=32768), ], ) diff --git a/netbox_docker_plugin/templates/netbox_docker_plugin/container.html b/netbox_docker_plugin/templates/netbox_docker_plugin/container.html index 24bad1b..a73390f 100644 --- a/netbox_docker_plugin/templates/netbox_docker_plugin/container.html +++ b/netbox_docker_plugin/templates/netbox_docker_plugin/container.html @@ -51,7 +51,7 @@
CONTAINER
Logging driver - {{ object.get_log_driver_display }} + {{ object.log_driver }} diff --git a/netbox_docker_plugin/tests/container/test_agent_compatibility.py b/netbox_docker_plugin/tests/container/test_agent_compatibility.py new file mode 100644 index 0000000..f7efaac --- /dev/null +++ b/netbox_docker_plugin/tests/container/test_agent_compatibility.py @@ -0,0 +1,96 @@ +"""Agent compatibility regression tests. + +Each test pins one payload shape that netbox-docker-agent >= 1.25 sends back to +NetBox and that plugin 3.3.0 rejected. See the migrations 0036 to 0038. +""" + +from django.test import TestCase +from netbox_docker_plugin.models.container import Container, Env, Port +from netbox_docker_plugin.models.host import Host +from netbox_docker_plugin.models.image import Image +from netbox_docker_plugin.models.registry import Registry + + +class AgentCompatibilityTestCase(TestCase): + """Agent Compatibility Test Case Class""" + + objects = {} + + @classmethod + def setUpTestData(cls) -> None: + 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["image1"] = Image.objects.create( + host=cls.objects["host1"], + name="image1", + registry=cls.objects["registry1"], + ) + + def _container(self, name, **kwargs): + """Create a container on the shared fixture host""" + + container = Container.objects.create( + host=self.objects["host1"], + image=self.objects["image1"], + name=name, + operation="none", + state="created", + **kwargs, + ) + container.full_clean() + return container + + def test_docker_log_drivers_are_accepted(self): + """The agent reports LogConfig.Type verbatim from the Docker daemon""" + + for index, driver in enumerate( + ["json-file", "syslog", "journald", "local", "none"] + ): + container = self._container(f"container-log-{index}", log_driver=driver) + self.assertEqual(container.log_driver, driver) + + def test_log_driver_may_be_empty(self): + """A container created before any refresh has no log driver yet""" + + container = self._container("container-log-null") + self.assertIsNone(container.log_driver) + + def test_unpublished_port_is_accepted(self): + """The agent uses -1 for exposed but unpublished ports""" + + container = self._container("container-port") + port = Port.objects.create( + container=container, + private_port=8080, + public_port=-1, + type="tcp", + ) + port.full_clean() + self.assertEqual(port.public_port, -1) + + def test_docker_capabilities_are_accepted(self): + """The agent reports the container capabilities with CAP_ stripped""" + + container = self._container( + "container-caps", + cap_add=["CHOWN", "SETUID", "SYS_ADMIN", "MKNOD"], + ) + self.assertEqual(len(container.cap_add), 4) + + def test_long_environment_value_is_accepted(self): + """Inline configuration and certificates exceed the old 4096 ceiling""" + + container = self._container("container-env") + env = Env.objects.create( + container=container, + var_name="BIG_VALUE", + value="x" * 20000, + ) + env.full_clean() + self.assertEqual(len(env.value), 20000) diff --git a/netbox_docker_plugin/tests/container/test_container_views.py b/netbox_docker_plugin/tests/container/test_container_views.py index 37cf81a..c77ba54 100644 --- a/netbox_docker_plugin/tests/container/test_container_views.py +++ b/netbox_docker_plugin/tests/container/test_container_views.py @@ -85,5 +85,6 @@ def setUpTestData(cls): cls.csv_update_data = ( "id,name,host,image,hostname,restart_policy,log_driver", f"{container1.pk},container1,{host1.pk},{image1.pk},,on-failure,syslog", - f"{container2.pk},container2,{host1.pk},{image1.pk},container2,unless-stopped,json-log", + f"{container2.pk},container2,{host1.pk},{image1.pk}," + "container2,unless-stopped,json-file", ) diff --git a/pyproject.toml b/pyproject.toml index 883d9fa..49161b6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "netbox-docker-plugin" -version = "3.3.0" +version = "3.4.0" authors = [ { name="Vincent Simonin", email="vincent@saashup.com" }, { name="David Delassus", email="david.jose.delassus@gmail.com" }