# coding=utf-8 import sys import time from django.core.management.base import BaseCommand, CommandError from django.db import transaction from logging import StreamHandler from filewave.log import log from generic.models.clients import GenericDesktopClient from ios.apn import APNPriority, send_apn_to_devices from ios.command_queue import CommandQueueCache from ios.models import MDMUser class Command(BaseCommand): def add_arguments(self, parser): parser.add_argument( '--serial', dest='serial', help='Device serial.', ) parser.add_argument( '--serial-file', dest='serial_file', help='Text file containing serials, one per line.', ) parser.add_argument( '--all', dest='all_devices', action='store_true', help='Explicitly target every macOS device. Required as a ' 'confirmation if neither --serial nor --serial-file is given.', ) parser.add_argument( '--dry-run', dest='dry_run', action='store_true', help='List matching devices only. Queue nothing, send no APNs.', ) def handle(self, *args, **options): # print to stdout usual log messages log.handlers.append(StreamHandler(stream=sys.stdout)) serial = options['serial'] serial_file = options['serial_file'] all_devices = options['all_devices'] dry_run = options['dry_run'] if serial and serial_file: raise CommandError('Pass only one of --serial or --serial-file, not both.') if not serial and not serial_file and not all_devices: raise CommandError('Pass --serial, --serial-file, or --all to confirm.') qs = GenericDesktopClient.objects.filter(operating_system__type='OSX') if serial: qs = qs.filter(serial_number=serial) elif serial_file: with open(serial_file, 'r') as f: serials = [line.strip() for line in f if line.strip()] if not serials: raise CommandError('%s contained no serial numbers.' % serial_file) qs = qs.filter(serial_number__in=serials) pks = set(qs.values_list('pk', flat=True)) if not pks: self.stdout.write('No matching device.') self.stdout.write('Done.') return if dry_run: for serial_number in qs.values_list('serial_number', flat=True).order_by('serial_number'): self.stdout.write('[dry-run] Would target device %s.' % serial_number) self.stdout.write('[dry-run] %d matching device(s). Nothing queued, no APNs sent.' % len(pks)) self.stdout.write('Done.') return # Only the command-queue writes happen inside the transaction. This # part is DB-only (no network calls, no sleeps), so wrapping it # atomically is safe and cheap: either every device gets its command # queued, or none do. mdm_users = [] with transaction.atomic(): users = MDMUser.get_users_and_command_queue( device_pks=pks, exclude_users=True, #locking=LockingStrategy.FOR_UPDATE_OF_COMMAND_QUEUE, ) if users: with CommandQueueCache() as cq_cache: for user in users: mdm_client = getattr(user, 'mdm_client', None) if not mdm_client or not getattr(mdm_client, 'client', None): self.stderr.write( 'Skipping MDMUser %s: no linked MDM client.' % user.pk ) continue cq = cq_cache.get_for_user(user) self.stdout.write( 'Add InstallApplication command to device %s.' % mdm_client.client.serial_number ) cq.create_install_fwcld_command() mdm_users.append(user) if not mdm_users: self.stdout.write('No InstallApplication commands queued.') self.stdout.write('Done.') return self.stdout.write('Queued %d InstallApplication command(s).' % len(mdm_users)) # APNs are sent after the queuing transaction has committed and # outside of any transaction. A failed or slow push here can never # undo commands that were already queued successfully above. sent = 0 failed = [] for user in mdm_users: try: send_apn_to_devices(priority=APNPriority.PRIORITY_0, mdm_users=[user.pk]) sent += 1 self.stdout.write('Sent APN to %s.' % user.pk) except Exception as exc: failed.append(user.pk) self.stderr.write('Failed to send APN to %s: %s' % (user.pk, exc)) time.sleep(1) self.stdout.write('Sent %d APN(s), %d failed.' % (sent, len(failed))) if failed: self.stdout.write( 'Failed MDMUser pks (command is already queued for these; ' 'retry the APN or wait for the next natural check-in): %s' % ', '.join(str(pk) for pk in failed) ) self.stdout.write('Done.')