Skip to content

Commit

Permalink
Add provisioning feature
Browse files Browse the repository at this point in the history
We introduce a new Django management command: `provision`.

For each app, some resources can be created and updated
automatically, based on their configuration in the Zentral config.

We start with the MDM `SCEPConfig` resources.
  • Loading branch information
np5 committed Jun 20, 2024
1 parent 758ae7c commit c0366e8
Show file tree
Hide file tree
Showing 18 changed files with 740 additions and 57 deletions.
16 changes: 16 additions & 0 deletions docker-entrypoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,21 @@ def wait_for_db_migration():
print("DB migration OK")


def wait_for_provisioning():
i = 0
while True:
try:
subprocess.run(['python', 'server/manage.py', 'provision'], check=True)
except subprocess.CalledProcessError:
retry_delay = min(20, (i + 1)) * random.uniform(0.8, 1.2)
warnings.warn(f"Can't do provisioning! Sleep {retry_delay:.1f}s…")
time.sleep(retry_delay)
i += 1
else:
break
print("Provisioning OK")


def django_collectstatic():
subprocess.run(['python', 'server/manage.py', 'collectstatic', '-v0', '--noinput'], check=True)

Expand Down Expand Up @@ -120,6 +135,7 @@ def create_zentral_superuser():
create_zentral_superuser()
else:
wait_for_db(env)
wait_for_provisioning()
if cmd in KNOWN_COMMANDS_TRIGGERING_COLLECTSTATIC:
django_collectstatic()
wd = KNOWN_COMMANDS_CHDIR.get(cmd)
Expand Down
36 changes: 36 additions & 0 deletions server/base/management/commands/provision.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import inspect
import logging
from django.apps import apps
from django.core.management.base import BaseCommand
from django.db import transaction
from zentral.conf import settings
from zentral.utils.apps import ZentralAppConfig
from zentral.utils.provisioning import Provisioner


logger = logging.getLogger("zentral.server.base.management.commands.provision")


class Command(BaseCommand):
help = 'Provision Zentral'

@staticmethod
def add_arguments(parser):
pass

def iter_provisiners(self):
for app_config in apps.app_configs.values():
if not isinstance(app_config, ZentralAppConfig):
continue
if not app_config.provisioning_module:
continue
for _, provisioner_cls in inspect.getmembers(
app_config.provisioning_module,
lambda m: inspect.isclass(m) and m != Provisioner and issubclass(m, Provisioner)
):
yield provisioner_cls(app_config, settings)

def handle(self, *args, **options):
with transaction.atomic():
for provisioner in self.iter_provisiners():
provisioner.apply()
6 changes: 6 additions & 0 deletions server/templates/_created_updated_at.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
<div class="table-responsive">
<table class="table w-auto">
<tbody>
{% if object.provisioning_uid %}
<tr>
<th class="border-0 p-0 pe-3 text-secondary">Provisioning UID</th>
<td class="border-0 p-0 text-secondary">{{ object.provisioning_uid }}</td>
</tr>
{% endif %}
{% if object.version %}
<tr>
<th class="border-0 p-0 pe-3 text-secondary">Version</th>
Expand Down
14 changes: 14 additions & 0 deletions tests/conf/base.json
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,20 @@
"push_csr_signer": {
"backend": "zentral.contrib.mdm.push_csr_signers.ZentralSaaSPushCSRSigner",
"url": "https://www.example.com/api/"
},
"provisioning": {
"scep_configs": {
"test": {
"name": "YoloFomo",
"url": "https://www.example.com/scep/",
"challenge_type": "MICROSOFT_CA",
"microsoft_ca_challenge_kwargs": {
"url": "https://www.example.com/ndes/",
"username": "Yolo",
"password": "Fomo"
}
}
}
}
},
"zentral.contrib.munki": {
Expand Down
20 changes: 19 additions & 1 deletion tests/mdm/test_management_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from django.core.management import call_command
from django.test import TestCase
from django.utils.crypto import get_random_string
from zentral.contrib.mdm.models import Location
from zentral.contrib.mdm.models import Location, SCEPConfig
from zentral.contrib.mdm.dep_client import DEPClientError
from .utils import force_dep_virtual_server

Expand Down Expand Up @@ -204,3 +204,21 @@ def test_sync_dep_devices_list_servers(self, sync_dep_virtual_server_devices):
f"{dvs2.pk} {dvs2}\n"
)
sync_dep_virtual_server_devices.assert_not_called()

# provisioning

def test_scep_config_provisioning(self):
qs = SCEPConfig.objects.all()
self.assertEqual(qs.count(), 0)
call_command('provision')
self.assertEqual(qs.count(), 1)
scep_config = qs.first()
# see tests/conf/base.json
self.assertEqual(scep_config.name, "YoloFomo")
self.assertEqual(scep_config.challenge_type, "MICROSOFT_CA")
self.assertEqual(
scep_config.get_challenge_kwargs(),
{"url": "https://www.example.com/ndes/",
"username": "Yolo",
"password": "Fomo"}
)
Loading

0 comments on commit c0366e8

Please sign in to comment.