routine.py 19 KB
Newer Older
Valentin Samir's avatar
Valentin Samir committed
1
#!/usr/bin/env python
2
# -*- coding: utf8 -*-
Valentin Samir's avatar
Valentin Samir committed
3
4
5
6

import os
import sys
import datetime
Valentin Samir's avatar
style    
Valentin Samir committed
7
import subprocess
Valentin Samir's avatar
Valentin Samir committed
8
import argparse
9
10
import pwd

Valentin Samir's avatar
Valentin Samir committed
11
12
from functools import total_ordering

13

Valentin Samir's avatar
Valentin Samir committed
14
BASE = "/etc/bind/keys"
15
16
17
# Interval entre 2 opérations sur les clefs dns.
# Par exemple si vous avec la clef1 d'utilisé,
# clef2 est publié INTERVAL avant la désactivation de clef1.
Valentin Samir's avatar
Valentin Samir committed
18
# clef1 est désactivé quand clef2 est activé,
19
20
# clef2 est supprimé INTRERVAL après sa désactivation.
# INTERVAL DOIT être supérieur aux plus long TTL que les enregistrement DS peuvent avoir.
Valentin Samir's avatar
Valentin Samir committed
21
# INTERVAL DOIT egalement être supérieur a l'intervale de signature de bind (défaut de 22.5 jours)
22
23
# Cela dépent essentiellement de la configuration de la zone parente et vous n'avez pas forcement
# de controle dessus.
Valentin Samir's avatar
Valentin Samir committed
24
INTERVAL = datetime.timedelta(days=23)
25
26
27
28
29
30
31
32
33
34
35
36
# Durée au bout de laquelle une ZSK est remplacé par une nouvelle ZSK.
# La génération des ZSK et leur activation/désactivation/suppression est géré
# automatiquement tant que routine.py -c est appelé au moins une fois par
# jour.
ZSK_VALIDITY = datetime.timedelta(days=30)  # ~1 mois
# Temps au bout duquelle une nouvelle KSK est généré et publié pour la zone
# (et activé après INTERVAL). L'ancienne clef n'est retiré que INTERVAL après que la nouvelle
# clef a été routine.py --ds-seen. Cela demande en général une opération
# manuelle avec le registrar (publier le DS de la nouvelle clef dans la zone parente)
# et routine.py -c affiche un message tant que cela n'a pas été fait.
KSK_VALIDITY = datetime.timedelta(days=366)  # ~1 an

Valentin Samir's avatar
Valentin Samir committed
37
38
39
40
41

def get_zones(zone_names=None):
    l = []
    if zone_names is None:
        for f in os.listdir(BASE):
Valentin Samir's avatar
Valentin Samir committed
42
            if os.path.isdir(os.path.join(BASE, f)) and not f.startswith('.'):
Valentin Samir's avatar
Valentin Samir committed
43
44
45
46
47
48
                l.append(Zone(f))
    else:
        for name in zone_names:
            l.append(Zone(name))
    return l

Valentin Samir's avatar
style    
Valentin Samir committed
49

Valentin Samir's avatar
Valentin Samir committed
50
def settime(path, flag, date):
Valentin Samir's avatar
style    
Valentin Samir committed
51
52
53
54
55
    cmd = [
        "/usr/sbin/dnssec-settime",
        "-i", str(int(INTERVAL.total_seconds())),
        "-%s" % flag, date, path
    ]
56
    p = subprocess.Popen(cmd, stderr=subprocess.PIPE, stdout=subprocess.PIPE)
Valentin Samir's avatar
Valentin Samir committed
57
58
59
60
    err = p.communicate()[1]
    if p.returncode != 0:
        raise ValueError("err %s: %s" % (p.returncode, err))
    if err:
61
        sys.stderr.write("%s\n" % err)
Valentin Samir's avatar
Valentin Samir committed
62

Valentin Samir's avatar
style    
Valentin Samir committed
63

Valentin Samir's avatar
Valentin Samir committed
64
def bind_chown(path):
65
66
67
68
69
70
71
72
73
74
75
76
77
    """
        Gives the files to the bind user and sets the modes in a relevant way.
    """
    try:
        bind_uid = pwd.getpwnam('bind').pw_uid
        os.chown(path, bind_uid, -1)
        for root, dirs, files in os.walk(path):
            for momo in dirs:
                os.chown(os.path.join(root, momo), bind_uid, -1)
            for momo in files:
                os.chown(os.path.join(root, momo), bind_uid, -1)
    except KeyError:
        sys.stderr.write("User bind not found, failing to give keys ownership to bind\n")
Valentin Samir's avatar
style    
Valentin Samir committed
78

Valentin Samir's avatar
Valentin Samir committed
79
80
81
82
83
def bind_reload():
    cmd = ["/usr/sbin/rndc", "reload"]
    p = subprocess.Popen(cmd)
    p.wait()

Valentin Samir's avatar
style    
Valentin Samir committed
84

Valentin Samir's avatar
Valentin Samir committed
85
86
87
88
89
90
91
92
93
def nsec3(zone, salt="-"):
    cmd = ["rndc", "signing", "-nsec3param", "1", "0", "10", salt, zone]
    sys.stdout.write("Enabling nsec3 for zone %s: " % zone)
    p = subprocess.Popen(cmd, stdout=subprocess.PIPE)
    out = p.communicate()[0]
    sys.stdout.write(out)
    p.wait()


Valentin Samir's avatar
Valentin Samir committed
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
class Zone(object):
    ZSK = None
    KSK = None
    _path = None
    name = None

    def __str__(self):
        return self.name

    def __unicode__(self):
        return self.name.decode("utf-8")

    def __repr__(self):
        return "Zone %s" % self.name

    @classmethod
    def create(cls, name):
        path = os.path.join(BASE, name)
        if os.path.isdir(path):
            raise ValueError("%s existe" % path)
        os.mkdir(path)
        bind_chown(path)
        return cls(name)

    def do_zsk(self):
        for zsk in self.ZSK:
            if zsk.is_activate:
121
122
                zsk.inactive = zsk.activate + ZSK_VALIDITY
                zsk.delete = zsk.inactive + INTERVAL
123
                last_activate_zsk = zsk
124
        now = datetime.datetime.utcnow()
Valentin Samir's avatar
Valentin Samir committed
125
        if zsk.is_activate:
126
127
            zsk.inactive = max(zsk.inactive, now + INTERVAL)
            zsk.delete = zsk.inactive + INTERVAL
Valentin Samir's avatar
Valentin Samir committed
128
129
            zsk.gen_successor()
            bind_reload()
130
131
        else:
            zsk.activate = last_activate_zsk.inactive
Valentin Samir's avatar
Valentin Samir committed
132

133
134
135
136
137
138
    def do_ksk(self):
        ksk = self.KSK[-1]
        if ksk.need_renew:
            now = datetime.datetime.utcnow()
            new_ksk = Key.create("KSK", self.name)
            new_ksk.publish = now
139
140
            # do not activate the new key until ds-seen
            new_ksk.activate = None
141
            bind_reload()
Valentin Samir's avatar
style    
Valentin Samir committed
142
143
144
145
146
147
148
149
        active_ksk = [key for key in self.KSK if key.is_publish and key.delete is None]
        if len(active_ksk) >= 2:
            sys.stderr.write(
                (
                    "New KSK needs DS seen and/or old KSK needs "
                    "inactivate/remove for zone %s\n"
                ) % self.name
            )
150
151
152
153
154
155
156
157
158
159
160
161
162

    def ds_seen(self, keyid):
        old_ksks = []
        for ksk in self.KSK:
            if ksk.keyid == keyid:
                seen_ksk = ksk
                break
            old_ksks.append(ksk)
        else:
            sys.stderr.write("Key not found\n")
            return
        print "Key %s found" % keyid
        now = datetime.datetime.utcnow()
163
164
        if seen_ksk.activate is None:
            seen_ksk.activate = (now + INTERVAL)
165
166
167
        for ksk in old_ksks:
            print " * program key %s removal" % ksk.keyid
            # set inactive in at least INTERVAL
168
169
170
            ksk.inactive = seen_ksk.activate
            # delete INTERVAL after being inactive
            ksk.delete = ksk.inactive + INTERVAL
171
172
173
174
175
176
177
178
179
        bind_reload()

    def remove_deleted(self):
        deleted_path = os.path.join(self._path, "deleted")
        try:
            os.mkdir(deleted_path)
        except OSError as error:
            if error.errno != 17:  # File exists
                raise
180
        now = datetime.datetime.utcnow()
181
        for key in self.ZSK + self.KSK:
182
            if key.delete and (key.delete + INTERVAL) <= now:
183
184
185
186
187
                for path in [key._path, key._path_private]:
                    basename = os.path.basename(path)
                    new_path = os.path.join(deleted_path, basename)
                    os.rename(path, new_path)

Valentin Samir's avatar
Valentin Samir committed
188
189
190
191
192
    def ds(self):
        for ksk in self.KSK:
            cmd = ["/usr/sbin/dnssec-dsfromkey", ksk._path]
            p = subprocess.Popen(cmd)
            p.wait()
Valentin Samir's avatar
style    
Valentin Samir committed
193

Valentin Samir's avatar
Valentin Samir committed
194
195
196
197
198
199
200
201
202
203
204
205
206
207
    def key(self):
        for ksk in self.KSK:
            print ksk

    def __init__(self, name):
        path = os.path.join(BASE, name)
        if not os.path.isdir(path):
            raise ValueError("%s n'est pas un dossier" % path)
        self.name = name
        self._path = path
        self.ZSK = []
        self.KSK = []
        for file in os.listdir(path):
            file_path = os.path.join(path, file)
208
            if os.path.isfile(file_path) and file_path.endswith(".private"):
Valentin Samir's avatar
style    
Valentin Samir committed
209
210
211
212
213
214
215
216
217
218
                try:
                    key = Key(file_path)
                    if key.type == "ZSK":
                        self.ZSK.append(key)
                    elif key.type == "KSK":
                        self.KSK.append(key)
                    else:
                        raise RuntimeError("impossible")
                except ValueError as error:
                    sys.stderr.write("%s\n" % error)
Valentin Samir's avatar
Valentin Samir committed
219
220
221
222
223
224
225
        self.ZSK.sort()
        self.KSK.sort()
        if not self.ZSK:
            self.ZSK.append(Key.create("ZSK", name))
        if not self.KSK:
            self.KSK.append(Key.create("KSK", name))

Valentin Samir's avatar
style    
Valentin Samir committed
226

Valentin Samir's avatar
Valentin Samir committed
227
228
229
230
231
232
233
234
235
236
237
@total_ordering
class Key(object):
    _created = None
    _publish = None
    _activate = None
    _inactive = None
    _delete = None
    _data = None
    _path = None
    type = None
    keyid = None
238
239
    flag = None
    zone_name = None
Valentin Samir's avatar
Valentin Samir committed
240
241
242
243
244
245
246

    def __str__(self):
        return self._data

    def __repr__(self):
        r = os.path.basename(self._path)
        return r
Valentin Samir's avatar
style    
Valentin Samir committed
247

Valentin Samir's avatar
Valentin Samir committed
248
249
250
    def _date_from_key(self, date):
        if date is not None:
            return datetime.datetime.strptime(date, "%Y%m%d%H%M%S")
Valentin Samir's avatar
style    
Valentin Samir committed
251

Valentin Samir's avatar
Valentin Samir committed
252
    def _date_to_key(self, date):
253
254
255
256
        if date is None:
            return 'none'
        else:
            return datetime.datetime.strftime(date, "%Y%m%d%H%M%S")
Valentin Samir's avatar
Valentin Samir committed
257

258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
    def _date_check(self, value, needed_date, value_name, needed_date_name):
        if value is not None:
            if needed_date is None or value < needed_date:
                raise RuntimeError(
                    "Cannot set %s date before %s date on key %s on zone %s" % (
                        value_name,
                        needed_date_name,
                        self.keyid,
                        self.zone_name
                     )
                )

    def _date_check2(self, value, needed_date, value_name, needed_date_name):
        msg = "Cannot set %s date after %s date on key %s on zone %s" % (
                  value_name,
                  needed_date_name,
                  self.keyid,
                  self.zone_name
              )
        if value is None and needed_date is not None:
            raise RuntimeError(msg)
        elif value is not None and needed_date is not None:
            if value > needed_date:
                raise RuntimeError(msg)

Valentin Samir's avatar
Valentin Samir committed
283
    @classmethod
284
285
286
    def create(cls, typ, name, options=None):
        if options is None:
            options = []
Valentin Samir's avatar
Valentin Samir committed
287
        path = os.path.join(BASE, name)
288
        cmd = ["/usr/sbin/dnssec-keygen", "-a", "RSASHA256"]
Valentin Samir's avatar
Valentin Samir committed
289
        if typ == "KSK":
290
            cmd.extend(["-b", "2048", "-f", "KSK"])
Valentin Samir's avatar
Valentin Samir committed
291
        elif typ == "ZSK":
292
            cmd.extend(["-b", "1024"])
293
294
        else:
            raise ValueError("typ must be KSK or ZSK")
295
296
        cmd.extend(options)
        cmd.extend(["-K", path,  name])
Valentin Samir's avatar
Valentin Samir committed
297
298
299
300
301
302
        p = subprocess.Popen(cmd, stdout=subprocess.PIPE)
        p.wait()
        if p.returncode != 0:
            raise ValueError("La creation de la clef a echoue")
        keyname = p.communicate()[0].strip()
        bind_chown(path)
303
        return cls(os.path.join(path, "%s.private" % keyname))
Valentin Samir's avatar
style    
Valentin Samir committed
304

Valentin Samir's avatar
Valentin Samir committed
305
    def gen_successor(self):
Valentin Samir's avatar
style    
Valentin Samir committed
306
307
308
309
        cmd = [
            "/usr/sbin/dnssec-keygen", "-i", str(int(INTERVAL.total_seconds())),
            "-S", self._path, "-K", os.path.dirname(self._path)
        ]
Valentin Samir's avatar
Valentin Samir committed
310
311
312
313
314
315
316
317
318
319
        p = subprocess.Popen(cmd, stderr=subprocess.PIPE)
        err = p.communicate()[1]
        if p.returncode != 0:
            raise ValueError("err %s: %s" % (p.returncode, err))
        if err:
            print err
        bind_chown(os.path.dirname(self._path))

    @property
    def created(self):
320
321
        if self._created is not None:
            return self._date_from_key(self._created)
Valentin Samir's avatar
Valentin Samir committed
322
323
324

    @property
    def publish(self):
325
326
        if self._publish is not None:
            return self._date_from_key(self._publish)
Valentin Samir's avatar
Valentin Samir committed
327
328
    @publish.setter
    def publish(self, value):
329
330
        self._date_check(value, self.created, "publish", "created")
        self._date_check2(value, self.activate, "publish", "activate")
Valentin Samir's avatar
Valentin Samir committed
331
332
333
334
        date = self._date_to_key(value)
        if date != self._publish:
            settime(self._path, 'P', date)
            self._publish = date
Valentin Samir's avatar
style    
Valentin Samir committed
335
            with open(self._path, 'r') as f:
Valentin Samir's avatar
Valentin Samir committed
336
337
338
339
                self._data = f.read()

    @property
    def activate(self):
340
341
        if self._activate is not None:
            return self._date_from_key(self._activate)
Valentin Samir's avatar
Valentin Samir committed
342
343
    @activate.setter
    def activate(self, value):
344
345
        self._date_check(value, self.publish, "active", "publish")
        self._date_check2(value, self.inactive, "activate", "inactive")
Valentin Samir's avatar
Valentin Samir committed
346
347
348
349
        date = self._date_to_key(value)
        if date != self._activate:
            settime(self._path, 'A', date)
            self._activate = date
Valentin Samir's avatar
style    
Valentin Samir committed
350
            with open(self._path, 'r') as f:
Valentin Samir's avatar
Valentin Samir committed
351
352
353
354
                self._data = f.read()

    @property
    def inactive(self):
355
356
        if self._inactive is not None:
            return self._date_from_key(self._inactive)
Valentin Samir's avatar
Valentin Samir committed
357
358
    @inactive.setter
    def inactive(self, value):
359
360
        self._date_check(value, self.activate, "inactive", "activate")
        self._date_check2(value, self.delete, "inactive", "delete")
Valentin Samir's avatar
Valentin Samir committed
361
362
363
364
        date = self._date_to_key(value)
        if date != self._inactive:
            settime(self._path, 'I', date)
            self._inactive = date
Valentin Samir's avatar
style    
Valentin Samir committed
365
            with open(self._path, 'r') as f:
Valentin Samir's avatar
Valentin Samir committed
366
367
368
369
                self._data = f.read()

    @property
    def delete(self):
370
371
        if self._delete:
            return self._date_from_key(self._delete)
Valentin Samir's avatar
Valentin Samir committed
372
373
    @delete.setter
    def delete(self, value):
374
        self._date_check(value, self.inactive, "delete", "inactive")
Valentin Samir's avatar
Valentin Samir committed
375
376
377
378
        date = self._date_to_key(value)
        if date != self._delete:
            settime(self._path, 'D', date)
            self._delete = date
Valentin Samir's avatar
style    
Valentin Samir committed
379
            with open(self._path, 'r') as f:
Valentin Samir's avatar
Valentin Samir committed
380
381
                self._data = f.read()

382
383
384
    @property
    def is_publish(self):
        return self.publish is not None and self.publish <= datetime.datetime.utcnow()
Valentin Samir's avatar
style    
Valentin Samir committed
385

386
387
388
    @property
    def is_activate(self):
        return self.activate is not None and self.activate <= datetime.datetime.utcnow()
Valentin Samir's avatar
style    
Valentin Samir committed
389

390
391
392
    @property
    def is_inactive(self):
        return self.inactive is not None and self.inactive <= datetime.datetime.utcnow()
Valentin Samir's avatar
style    
Valentin Samir committed
393

394
395
396
397
398
399
400
401
402
403
404
405
406
    @property
    def is_delete(self):
        return self.delete is not None and self.delete <= datetime.datetime.utcnow()

    @property
    def need_renew(self):
        if self.type == "KSK":
            return (self.activate + KSK_VALIDITY) <= (datetime.datetime.utcnow() + INTERVAL)
        elif self.type == "ZSK":
            return (self.activate + ZSK_VALIDITY) <= (datetime.datetime.utcnow() + INTERVAL)
        else:
            raise RuntimeError("impossible")

Valentin Samir's avatar
Valentin Samir committed
407
    def __init__(self, path):
408
409
410
411
412
413
414
415
        if not path.endswith(".private"):
            raise ValueError("%s n'est pas une clef valide" % path)
        if not os.path.isfile(path):
            raise ValueError("%s n'existe pas" % path)
        ppath = "%s.key" % path[:-8]
        if not os.path.isfile(ppath):
            raise ValueError("la clef publique (%s) de %s n'existe pas" % (ppath, path))
        with open(ppath, 'r') as f:
Valentin Samir's avatar
Valentin Samir committed
416
            self._data = f.read()
417
418
419
420
421
422
423
424
        with open(path, 'r') as f:
            private_data = f.read()
        for line in self._data.split("\n"):
            if line.startswith(";") or not line:
                continue
            line = line.split(";", 1)[0].strip()
            line = line.split()
            if len(line) < 7:
Valentin Samir's avatar
style    
Valentin Samir committed
425
426
427
                raise ValueError(
                    "La clef publique %s devrait avoir au moins 7 champs: %r" % (ppath, line)
                )
428
            if not line[0].endswith('.'):
Valentin Samir's avatar
style    
Valentin Samir committed
429
430
431
432
433
434
                raise ValueError(
                    (
                        "La clef publique %s devrait commencer par le fqdn "
                        "(finissant par un .) de la zone"
                    ) % ppath
                )
435
436
437
438
            self.zone_name = line[0][:-1]
            try:
                self.flag = int(line[3])
            except ValueError:
Valentin Samir's avatar
style    
Valentin Samir committed
439
440
441
                raise ValueError(
                    "Le flag %s de la clef publique %s devrait être un entier" % (line[3], ppath)
                )
442
        if self.flag == 256:
Valentin Samir's avatar
Valentin Samir committed
443
            self.type = "ZSK"
444
        elif self.flag == 257:
Valentin Samir's avatar
Valentin Samir committed
445
446
            self.type = "KSK"
        else:
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
            raise ValueError("%s n'est pas une clef valide: flag %s inconnu" % (ppath, self.flag))
        self._path = ppath
        self._path_private = path
        keyid = path.split('.')[-2].split('+')[-1]
        try:
            self.keyid = int(keyid)
        except ValueError:
            raise ValueError("Le keyid %s de la clef %s devrait être un entier" % (keyid, path))
        for line in private_data.split("\n"):
            if line.startswith("Created:"):
                self._created = line[8:].strip()
                self._date_from_key(self._created)
            elif line.startswith("Publish:"):
                self._publish = line[8:].strip()
                self._date_from_key(self._publish)
            elif line.startswith("Activate:"):
                self._activate = line[9:].strip()
                self._date_from_key(self._activate)
            elif line.startswith("Inactive:"):
                self._inactive = line[9:].strip()
                self._date_from_key(self._inactive)
            elif line.startswith("Delete:"):
                self._delete = line[7:].strip()
                self._date_from_key(self._delete)
        if self.created is None:
            raise ValueError("La clef %s doit au moins avoir le champs Created de définit" % path)
Valentin Samir's avatar
Valentin Samir committed
473
474

    def __lt__(self, y):
Valentin Samir's avatar
style    
Valentin Samir committed
475
476
477
478
479
480
481
482
        if not isinstance(y, Key):
            raise ValueError("can only compare two Keys")
        if self.activate is not None and y.activate is not None:
            return self.activate < y.activate
        elif self.publish is not None and y.publish is not None:
            return self.publish < y.publish
        else:
            return self.created < y.created
Valentin Samir's avatar
Valentin Samir committed
483
484
485
486
487
488

    def __eq__(self, y):
        return isinstance(y, Key) and y._path == self._path

if __name__ == '__main__':
    try:
Valentin Samir's avatar
Valentin Samir committed
489
490
        parser = argparse.ArgumentParser()
        parser.add_argument('zone', nargs='*', help='zone name')
Valentin Samir's avatar
style    
Valentin Samir committed
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
        parser.add_argument(
            '--make', '-m',
            action='store_true',
            help='Create keys for each supplied zone'
        )
        parser.add_argument(
            '--cron', '-c',
            action='store_true',
            help='Perform maintenance for each supplied zone or for all zones if no zone supplied'
        )
        parser.add_argument(
            '-ds',
            action='store_true',
            help='Show DS for each supplied zone or for all zones if no zone supplied'
        )
        parser.add_argument(
            '-key',
            action='store_true',
            help='Show DNSKEY for each zone supplied zone or for all zones if no zone supplied'
        )
        parser.add_argument(
            '--ds-seen',
            metavar='KEYID',
            type=int,
            help=(
                'To call with the ID of a new KSK published in the parent zone. '
                'Programs old KSK removal'
            )
        )
        parser.add_argument(
            '--nsec3',
            action='store_true',
            help='Enable NSEC3 for the zones, using a random salt'
        )
Valentin Samir's avatar
Valentin Samir committed
525
526
527
        args = parser.parse_args()
        zones = args.zone
        if args.make:
Valentin Samir's avatar
Valentin Samir committed
528
529
530
            for zone in zones:
                Zone.create(zone)
        zones = get_zones(zones if zones else None)
Valentin Samir's avatar
Valentin Samir committed
531
532
533
        if args.nsec3:
            for zone in zones:
                nsec3(zone.name, os.urandom(24).encode("hex"))
534
535
536
537
538
539
        if args.ds_seen:
            if len(zones) != 1:
                sys.stderr.write("Please specify exactly ONE zone name\n")
                sys.exit(1)
            for zone in zones:
                zone.ds_seen(args.ds_seen)
Valentin Samir's avatar
Valentin Samir committed
540
        if args.cron:
Valentin Samir's avatar
Valentin Samir committed
541
542
            for zone in zones:
                zone.do_zsk()
543
544
                zone.do_ksk()
                zone.remove_deleted()
Valentin Samir's avatar
Valentin Samir committed
545
        if args.ds:
Valentin Samir's avatar
Valentin Samir committed
546
547
            for zone in zones:
                zone.ds()
Valentin Samir's avatar
Valentin Samir committed
548
        if args.key:
Valentin Samir's avatar
Valentin Samir committed
549
550
            for zone in zones:
                zone.key()
Valentin Samir's avatar
Valentin Samir committed
551
        if not any([args.make, args.cron, args.ds, args.key, args.ds_seen, args.nsec3]):
Valentin Samir's avatar
Valentin Samir committed
552
            parser.print_help()
Valentin Samir's avatar
Valentin Samir committed
553
554
555
    except ValueError as error:
        sys.stderr.write("%s\n" % error)
        sys.exit(1)