Skip to content

Commit

Permalink
Better MDM target artifact updates & DDM retries
Browse files Browse the repository at this point in the history
  • Loading branch information
np5 committed Feb 9, 2025
1 parent 3e63141 commit a59f7a5
Show file tree
Hide file tree
Showing 18 changed files with 763 additions and 178 deletions.
339 changes: 303 additions & 36 deletions tests/mdm/test_artifacts.py

Large diffs are not rendered by default.

16 changes: 10 additions & 6 deletions tests/mdm/test_declarations_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,21 +33,23 @@ def test_get_artifact_version_server_token_reinstall_major(self):
target,
{"reinstall_on_os_update": str(Artifact.ReinstallOnOSUpdate.MAJOR),
"reinstall_interval": 0},
{"pk": av_pk}
{"pk": av_pk},
0
)
self.assertEqual(server_token, f"{av_pk}.ov-15")

def test_get_artifact_version_server_token_reinstall_minor(self):
def test_get_artifact_version_server_token_reinstall_minor_one_retry_count(self):
target = Mock()
target.comparable_os_version = (15, 2, 1)
av_pk = str(uuid.uuid4())
server_token = get_artifact_version_server_token(
target,
{"reinstall_on_os_update": str(Artifact.ReinstallOnOSUpdate.MINOR),
"reinstall_interval": 0},
{"pk": av_pk}
{"pk": av_pk},
1
)
self.assertEqual(server_token, f"{av_pk}.ov-15.2")
self.assertEqual(server_token, f"{av_pk}.ov-15.2.rc-1")

def test_get_artifact_version_server_token_reinstall_patch(self):
target = Mock()
Expand All @@ -57,7 +59,8 @@ def test_get_artifact_version_server_token_reinstall_patch(self):
target,
{"reinstall_on_os_update": str(Artifact.ReinstallOnOSUpdate.PATCH),
"reinstall_interval": 0},
{"pk": av_pk}
{"pk": av_pk},
0
)
self.assertEqual(server_token, f"{av_pk}.ov-15.2.1")

Expand All @@ -73,6 +76,7 @@ def test_get_artifact_version_server_token_reinstall_interval(self, patched_date
target,
{"reinstall_on_os_update": str(Artifact.ReinstallOnOSUpdate.NO),
"reinstall_interval": 3600 * 24 * 90},
{"pk": av_pk}
{"pk": av_pk},
0
)
self.assertEqual(server_token, f"{av_pk}.ri-1")
348 changes: 251 additions & 97 deletions zentral/contrib/mdm/artifacts.py

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion zentral/contrib/mdm/commands/install_application.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ def command_acknowledged(self):
self.target.update_target_artifact(
self.artifact_version,
TargetArtifact.Status.ACKNOWLEDGED,
allow_reinstall=True,
unique_install_identifier=self.uuid,
)


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ def command_acknowledged(self):
self.target.update_target_artifact(
self.artifact_version,
TargetArtifact.Status.ACKNOWLEDGED,
allow_reinstall=True,
unique_install_identifier=self.uuid,
)


Expand Down
2 changes: 1 addition & 1 deletion zentral/contrib/mdm/commands/install_profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ def command_acknowledged(self):
self.target.update_target_artifact(
self.artifact_version,
TargetArtifact.Status.ACKNOWLEDGED,
allow_reinstall=True
unique_install_identifier=self.uuid,
)

def command_error(self):
Expand Down
2 changes: 1 addition & 1 deletion zentral/contrib/mdm/commands/installed_application_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ def _update_device_artifact(self):
self.target.update_target_artifact(
self.artifact_version,
TargetArtifact.Status.INSTALLED,
allow_reinstall=True,
unique_install_identifier=self.uuid,
)
elif error:
self.target.update_target_artifact(
Expand Down
2 changes: 1 addition & 1 deletion zentral/contrib/mdm/commands/managed_application_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ def _update_device_artifact(self):
self.artifact_version,
ta_status,
extra_info=extra_info,
allow_reinstall=True,
unique_install_identifier=self.uuid,
)
if not found:
logger.warning("Artifact version %s was not found on device %s.",
Expand Down
5 changes: 2 additions & 3 deletions zentral/contrib/mdm/commands/scheduling.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,8 @@ def _install_artifacts(target, enrollment_session, status):
if target.declarative_management:
# device profiles managed using declarative management
included_types = (Artifact.Type.ENTERPRISE_APP, Artifact.Type.STORE_APP)
else:
included_types = (Artifact.Type.ENTERPRISE_APP, Artifact.Type.PROFILE, Artifact.Type.STORE_APP)
artifact_version = target.next_to_install(included_types=included_types)
if artifact_version:
command_class = None
Expand All @@ -193,9 +195,6 @@ def _install_artifacts(target, enrollment_session, status):
):
# the association is already done, we can send the command
command_class = InstallApplication
else:
# should never happen
raise ValueError(f"Cannot install artifact type {artifact_version.artifact.type}")
if command_class:
return command_class.create_for_target(target, artifact_version)

Expand Down
21 changes: 12 additions & 9 deletions zentral/contrib/mdm/declarations/data_asset.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,22 +36,25 @@ def build_data_asset(enrollment_session, target, declaration_identifier):
artifact_pk = artifact_pk_from_identifier_and_model(declaration_identifier, DataAsset)
except ValueError:
raise DeclarationError('Invalid DataAsset Identifier')
data_asset_artifact_version = data_asset_artifact = None
for artifact, artifact_version in target.all_installed_or_to_install_serialized((Artifact.Type.DATA_ASSET,)):
da_artifact, da_artifact_version, da_retry_count = (None, None, 0)
for artifact, artifact_version, retry_count in target.all_installed_or_to_install_serialized(
(Artifact.Type.DATA_ASSET,)
):
if artifact["pk"] == artifact_pk:
data_asset_artifact = artifact
data_asset_artifact_version = artifact_version
da_artifact = artifact
da_artifact_version = artifact_version
da_retry_count = retry_count
break
if not data_asset_artifact_version:
if not da_artifact_version:
raise DeclarationError(f'Could not find DataAsset artifact {artifact_pk}')
try:
data_asset = DataAsset.objects.get(artifact_version__pk=data_asset_artifact_version["pk"])
data_asset = DataAsset.objects.get(artifact_version__pk=da_artifact_version["pk"])
except DataAsset.DoesNotExist:
raise DeclarationError(f'DataAsset for artifact version {data_asset_artifact_version["pk"]} does not exist')
raise DeclarationError(f'DataAsset for artifact version {da_artifact_version["pk"]} does not exist')
return {
"Type": "com.apple.asset.data",
"Identifier": get_artifact_identifier(data_asset_artifact),
"ServerToken": get_artifact_version_server_token(target, data_asset_artifact, data_asset_artifact_version),
"Identifier": get_artifact_identifier(da_artifact),
"ServerToken": get_artifact_version_server_token(target, da_artifact, da_artifact_version, da_retry_count),
"Payload": {
"Reference": {
"DataURL": "https://{}{}".format(
Expand Down
19 changes: 10 additions & 9 deletions zentral/contrib/mdm/declarations/declaration.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ def build_declaration(enrollment_session, target, declaration_identifier):
artifact_pk = artifact_pk_from_identifier_and_model(declaration_identifier, Declaration)
except ValueError:
raise DeclarationError("Invalid Declaration Identifier")
declaration_artifact_version = declaration_artifact = None
for artifact, artifact_version in target.all_installed_or_to_install_serialized(
d_artifact, d_artifact_version, d_retry_count = (None, None, 0)
for artifact, artifact_version, retry_count in target.all_installed_or_to_install_serialized(
included_types=tuple(
t for t in Artifact.Type if t.is_raw_declaration
),
Expand All @@ -29,16 +29,17 @@ def build_declaration(enrollment_session, target, declaration_identifier):
)
):
if artifact["pk"] == artifact_pk:
declaration_artifact = artifact
declaration_artifact_version = artifact_version
d_artifact = artifact
d_artifact_version = artifact_version
d_retry_count = retry_count
break
if not declaration_artifact_version:
if not d_artifact_version:
raise DeclarationError(f'Could not find Declaration artifact {artifact_pk}')
try:
declaration = (Declaration.objects.prefetch_related("declarationref_set__artifact")
.get(artifact_version__pk=declaration_artifact_version["pk"]))
.get(artifact_version__pk=d_artifact_version["pk"]))
except Declaration.DoesNotExist:
raise DeclarationError(f'Declaration for artifact version {declaration_artifact_version["pk"]} does not exist')
raise DeclarationError(f'Declaration for artifact version {d_artifact_version["pk"]} does not exist')
# prepare payload
payload = declaration.payload
# substitute references to other declarations
Expand All @@ -54,7 +55,7 @@ def build_declaration(enrollment_session, target, declaration_identifier):
payload = substitute_variables(payload, enrollment_session, target.enrolled_user)
return {
"Type": declaration.type,
"Identifier": get_artifact_identifier(declaration_artifact),
"ServerToken": get_artifact_version_server_token(target, declaration_artifact, declaration_artifact_version),
"Identifier": get_artifact_identifier(d_artifact),
"ServerToken": get_artifact_version_server_token(target, d_artifact, d_artifact_version, d_retry_count),
"Payload": payload,
}
17 changes: 10 additions & 7 deletions zentral/contrib/mdm/declarations/legacy_profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,18 +36,21 @@ def build_legacy_profile(enrollment_session, target, declaration_identifier):
artifact_pk = artifact_pk_from_identifier_and_model(declaration_identifier, Profile)
except ValueError:
raise DeclarationError('Invalid Profile Identifier')
profile_artifact_version = profile_artifact = None
for artifact, artifact_version in target.all_installed_or_to_install_serialized((Artifact.Type.PROFILE,)):
p_artifact, p_artifact_version, p_retry_count = (None, None, 0)
for artifact, artifact_version, retry_count in target.all_installed_or_to_install_serialized(
(Artifact.Type.PROFILE,)
):
if artifact["pk"] == artifact_pk:
profile_artifact = artifact
profile_artifact_version = artifact_version
p_artifact = artifact
p_artifact_version = artifact_version
p_retry_count = retry_count
break
if not profile_artifact_version:
if not p_artifact_version:
raise DeclarationError(f'Could not find Profile artifact {artifact_pk}')
return {
"Type": "com.apple.configuration.legacy",
"Identifier": get_artifact_identifier(profile_artifact),
"ServerToken": get_artifact_version_server_token(target, profile_artifact, profile_artifact_version),
"Identifier": get_artifact_identifier(p_artifact),
"ServerToken": get_artifact_version_server_token(target, p_artifact, p_artifact_version, p_retry_count),
"Payload": {
"ProfileURL": "https://{}{}".format(
settings["api"]["fqdn"],
Expand Down
16 changes: 11 additions & 5 deletions zentral/contrib/mdm/declarations/status_report.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ def get_target_artifact_info(item):
reasons = item.get("reasons")
if reasons:
extra_info["reasons"] = reasons
return artifact_version_pk, (status, extra_info, server_token)
return artifact_version_pk, status, extra_info, server_token


def get_status_report_target_artifacts_info(status_report):
Expand All @@ -32,14 +32,20 @@ def get_status_report_target_artifacts_info(status_report):
except KeyError:
logger.error("Status report without declarations section")
return
target_artifacts_info = {}
target_artifacts_info = []
for section in ("activations", "assets", "configurations", "management"):
for item in declarations.get(section, []):
try:
parse_artifact_identifier(item["identifier"])
_, artifact_pk = parse_artifact_identifier(item["identifier"])
except ValueError:
pass
else:
artifact_version_pk, info = get_target_artifact_info(item)
target_artifacts_info[artifact_version_pk] = info
artifact_version_pk, status, extra_info, server_token = get_target_artifact_info(item)
target_artifacts_info.append((
artifact_pk,
artifact_version_pk,
status,
extra_info,
server_token,
))
return target_artifacts_info
10 changes: 9 additions & 1 deletion zentral/contrib/mdm/declarations/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@
logger = logging.getLogger("zentral.contrib.mdm.declarations.utils")


MAX_DECLARATION_RETRIES = 3


# declaration identifiers


Expand Down Expand Up @@ -70,8 +73,9 @@ def artifact_pk_from_identifier_and_model(identifier, model):
raise ValueError("Invalid artifact identifier model")


def get_artifact_version_server_token(target, artifact, artifact_version):
def get_artifact_version_server_token(target, artifact, artifact_version, retry_count):
elements = [artifact_version["pk"]]
# reinstall on OS updates
reinstall_on_os_update = Artifact.ReinstallOnOSUpdate(artifact["reinstall_on_os_update"])
if reinstall_on_os_update != Artifact.ReinstallOnOSUpdate.NO:
slice_length = None
Expand All @@ -83,10 +87,14 @@ def get_artifact_version_server_token(target, artifact, artifact_version):
slice_length = 3
if slice_length:
elements.append("ov-{}".format(".".join(str(i) for i in target.comparable_os_version[:slice_length])))
# reinstall interval
reinstall_interval = artifact["reinstall_interval"]
if reinstall_interval:
install_num = int((datetime.utcnow() - target.target.created_at) / timedelta(seconds=reinstall_interval))
elements.append(f"ri-{install_num}")
# retry count
if retry_count:
elements.append(f"rc-{retry_count}")
return ".".join(elements)


Expand Down
1 change: 1 addition & 0 deletions zentral/contrib/mdm/events/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from .apps_books import * # NOQA
from .artifacts import * # NOQA
from .filevault import * # NOQA
from .management import * # NOQA
from .mdm import * # NOQA
Expand Down
50 changes: 50 additions & 0 deletions zentral/contrib/mdm/events/artifacts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import logging
import uuid
from zentral.core.events import register_event_type
from zentral.core.events.base import BaseEvent, EventMetadata


logger = logging.getLogger('zentral.contrib.mdm.events.artifacts')


# Target Artifact


class TargetArtifactUpdateEvent(BaseEvent):
event_type = "target_artifact_update"
tags = ["mdm"]

def get_linked_objects_keys(self):
keys = {}
# artifact, artifact version
try:
av = self.payload["target_artifact"]["artifact_version"]
av_pk = av["pk"]
a_pk = av["artifact"]["pk"]
except KeyError:
logging.warning("Missing event information")
else:
keys["mdm_artifact"] = [(a_pk,)]
keys["mdm_artifactversion"] = [(av_pk,)]
# enrolled user
try:
eu_pk = self.payload["enrolled_user"]["pk"]
except KeyError:
pass
else:
keys["mdm_enrolleduser"] = [(eu_pk,)]
return keys


register_event_type(TargetArtifactUpdateEvent)


def post_target_artifact_update_events(target, payloads):
event_uuid = uuid.uuid4()
for index, payload in enumerate(payloads):
event_metadata = EventMetadata(
uuid=event_uuid, index=index,
machine_serial_number=target.serial_number,
)
event = TargetArtifactUpdateEvent(event_metadata, payload)
event.post()
Loading

0 comments on commit a59f7a5

Please sign in to comment.