aboutsummaryrefslogtreecommitdiff
path: root/zc
AgeCommit message (Collapse)Author
2023-01-18Add $INCLUDE supportRob Austein
This doesn't support the origin stacking defined in RFC 1034 because: 1. Doing so would require us to maintain a real $INCLUDE stack instead of just chaining iterators; and 2. The expected use case is including automatically-generated snippets in zones that are being maintained with zc, so there's no real need for origin fiddling anyway because whatever automation is generating the snippets can just generate FQDNs if necessary. If really needed, we could fix this, but, YAGNI.
2021-11-14Convert to Python 3Rob Austein
2017-11-07Having a default state for mapping might be nice.Rob Austein
2017-06-24Fix exception handling that we broke when adding pretty logging.Rob Austein
2017-05-21Copyright.Rob Austein
2017-05-21First public version.Rob Austein
t> 2014-05-29 22:31:15 +0000 PyLint' href='/sra/rpki.net/commit/rpki/rpkid.py?id=35c1ca65ac2b5cbd943248d59b3e02c6220b232f'>35c1ca65
06023dcd








b603ea6e



7d72caf4
7d72caf4
35c1ca65
7d72caf4
35c1ca65

0ebd5b3d
7d72caf4



7d72caf4






26c65b2b
d97690e0
ba0ee92b
3f4f7622
937db24e

3f4f7622
d97690e0

e04760e2
d97690e0



7f5e7518

d97690e0

aedaacf9

f8d0d3bb

06023dcd

0eafef08

06023dcd





dad61b8e
06023dcd



dad61b8e
06023dcd
0eafef08
7d72caf4

dad61b8e
13d6be44
7d72caf4
d97690e0

06bedea0




35c1ca65
d97690e0




d97690e0

937db24e
d97690e0

35c1ca65
d97690e0
7f5e7518

ba0ee92b


7f5e7518

6d5fe21b

ba0ee92b
7f5e7518
d97690e0
e04760e2








0352b46e
e04760e2






7935cc24
e04760e2







d97690e0

e04760e2


c82f46c9
e04760e2

e04760e2







35c1ca65
e04760e2

937db24e
e04760e2
edc75cd9









e04760e2



63e820d3
edc75cd9
63e820d3
122d0b33
63e820d3


122d0b33
63e820d3

edc75cd9

63e820d3

63e820d3












e04760e2





edc75cd9


e04760e2

edc75cd9

6d5fe21b
e04760e2
edc75cd9



e04760e2
edc75cd9
e04760e2





edc75cd9


e04760e2
486c91ea
e04760e2



edc75cd9
e04760e2
edc75cd9


e04760e2
0fdf6bbe




edc75cd9


0fdf6bbe
122d0b33








6d5fe21b
122d0b33

















ba0ee92b

122d0b33

ba0ee92b


ba0ee92b

ba0ee92b






7f5e7518
















ba0ee92b



ba0ee92b


ba0ee92b

7f5e7518
















ba0ee92b

e04760e2




122d0b33






ba0ee92b
e04760e2
7f5e7518

e04760e2
aedaacf9

ba0ee92b

ab90c10d
ba0ee92b
c67ce584

ba0ee92b




07a045d1
ba0ee92b

ba0ee92b



7f5e7518

ba0ee92b


7f5e7518

a6998f3a





ba0ee92b



7f5e7518
ba0ee92b

c67ce584
7f5e7518
c67ce584


7f5e7518

c67ce584
7f5e7518
c67ce584
7f5e7518


c67ce584

7f5e7518




0714a72b
7f5e7518
c67ce584
0714a72b
7f5e7518



c67ce584

7f5e7518
c67ce584
7f5e7518
c67ce584
0714a72b
6d5fe21b
0714a72b
ba0ee92b






e04760e2

ba0ee92b
72e42a65
e6047c9f
bdd3b437
e04760e2


86bf27d6
e04760e2



86bf27d6
86bf27d6
e04760e2

e04760e2

07a045d1
e04760e2
7f5e7518


421546d2

7f5e7518
e04760e2

24da995f
937db24e
744ec2cb
d37ca9c3
e6047c9f
744ec2cb
e04760e2
8b9ca3f3
e04760e2



13a65b46
8b9ca3f3
3a1da201
e04760e2
26c65b2b
f8d0d3bb


13a65b46
26c65b2b
35c1ca65
26c65b2b
f8d0d3bb

35c1ca65
f8d0d3bb






f8d0d3bb
13a65b46
f8d0d3bb




96dce206
f8d0d3bb




13a65b46
f8d0d3bb


e04760e2




f8d0d3bb
8b9ca3f3
937db24e
8b9ca3f3
26c65b2b
26c65b2b
35c1ca65
26c65b2b



db573437
7f5e7518
35c1ca65
7f5e7518
db573437

7f5e7518
26c65b2b
8b9ca3f3
f8d0d3bb
8b9ca3f3
f8d0d3bb
35c1ca65
f8d0d3bb
8b9ca3f3
f8d0d3bb

35c1ca65
f8d0d3bb
8b9ca3f3
f8d0d3bb
8b9ca3f3
f8d0d3bb
e04760e2
f8d0d3bb
35c1ca65
e04760e2
26c65b2b

e04760e2







744ec2cb


e04760e2
744ec2cb
e04760e2
937db24e
744ec2cb
e04760e2
e04760e2
7f5e7518
e04760e2
7f5e7518
e04760e2
7f5e7518





370b80a6
7f5e7518
e04760e2
7f5e7518

56229734


e04760e2





a11d65c5

ed6e675c
a11d65c5

ed6e675c


a11d65c5

5b9ca78b
3f4f7622


e04760e2


3f4f7622

ed6e675c
e04760e2
afb06330
5b9ca78b

6d5fe21b
a11d65c5
6d5fe21b
5b9ca78b
6d5fe21b
5b9ca78b
6d5fe21b
ed6e675c

6d5fe21b
ed6e675c
3f4f7622

6d5fe21b

ed6e675c
e04760e2
3f4f7622


ed6e675c
e04760e2


e04760e2
e252a23c
35c1ca65
7f5e7518
e252a23c

56229734
e252a23c
c4f104ae



633b5b6c

ed6e675c
633b5b6c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
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
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
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
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
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
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
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
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
      
 


                                                                                    
 


                                                                        
 








                                                                      



               
         
         
           
             

               
 



                        






                      
                       
 
                                                                       
 

                                    
 

                   
                         



                     

                                                                            

                

                                   

                            

                                                           

                                                                                 





                                                                          
                                   



                               
                                
 
                                                                                

                               
                           
                                                   
 

                     




                                     
                                                              




                 

                                                     
                              

                    
                                                                            
 

                                       


                  

                                          

                                                               
 
                                              
 








                                                                              
                                                          






                                                                                          
 







                                                                                                

                     


                                                           
                                                                                            

                                                         







                                                                                      
                                                              

                               
                                                                         
 









                                                                          



                                   
        
                                                
 
                                                                                    


                        
                                                      

                                                                                                

                                                                                             

                                                                                     












                                     





                                                                                     


                                                                      

                    

                                           
                                                                                  
                                              



                                                                                      
 
                                         





                                                                    


                                                                                       
 
                                                                                            



                                                      
                                            
                                        


                                                                          
 




                                                                               


                                                                                                  
 








                                    
                                                               

















                                                                         

                                                                                               

                                              


                                                        

       






                                          
















                                                                                                                               



                                                         


                                                                                

                                          
















                                                                                                                  

 




                                                






                                                                     
                                                                
 

                                                 
        

                                                          

                                                                             
                                                                                 
 

                                                                                




                                                                       
                                                                   

                 



                                                                                              

                                                            


                                                                                    

                                                      





                                                                                                        



                                                                                                
                                                                 

                                                                     
 
               


                                                      

                                                                                   
                                         
                                                            
                                                       


                                                                                

                                     




                                                               
                                                           
                                                             
 
                                             



                                                               

                                                          
                                          
                        
                                                                      
 
                 
                                            
 






                                                

                                            
 
                        
                                                                        
                                                                                


                                                                                
                                             



                            
                    
                           

        

                                                  
                                                                                                    
                                                


                                                                                                                  

                                                                                                         
                                            

                                            
                                                                             
                            
                              
                        
                                                                        
                                                       
 
                                      



                                                                  
 
                                              
                                                                  
 
                           


                                                                       
 
                                   
                                                   
                                  

                 
                                                                 






                                                                     
       
 




                                                
                                               




                                                                    
 


                                 




                            
                            
 
                                     
 
               
                              
                                                         



                                                         
        
                                                    
                     
                                                                                  

                      
                                               
                                          
 
                                                              
 
                                                                 
                                                                                                 
                              
 

                                   
                                                          
                               
 
                                 
                                             
                     
 
                                
                                                                                            
 

                      







                                                                   


                          
                              
                                                 
         
                                                        
                     
 
 
                                
     
                                                    
 





                                                                    
 
                
 

                            


                  





                          

                                                             
 

                                                                             


                                                                              

                                                                               
 


                                                                      


                                         

                                                                                        
 
                                         
                                                                       

                                    
                                    
                              
                         
                         
                     
         
                                                                  

                       
                                                                                                 
         

                                                                                

                                 
 
                           


                                  
 


                          
                              
                            
                                                              
                                                                                               

                  
          
                                                      



                                                                

                  
                                                                                                                                 
                        
# $Id$
#
# Copyright (C) 2013--2014  Dragon Research Labs ("DRL")
# Portions copyright (C) 2009--2012  Internet Systems Consortium ("ISC")
# Portions copyright (C) 2007--2008  American Registry for Internet Numbers ("ARIN")
#
# Permission to use, copy, modify, and distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notices and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND DRL, ISC, AND ARIN DISCLAIM ALL
# WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS.  IN NO EVENT SHALL DRL,
# ISC, OR ARIN BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR
# CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
# OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

"""
RPKI CA engine.
"""

import os
import re
import time
import random
import logging
import argparse

import rpki.resource_set
import rpki.up_down
import rpki.left_right
import rpki.x509
import rpki.http
import rpki.config
import rpki.exceptions
import rpki.relaxng
import rpki.log
import rpki.async
import rpki.daemonize
import rpki.rpkid_tasks

from lxml.etree import Element, SubElement, tostring as ElementToString

logger = logging.getLogger(__name__)


class main(object):
  """
  Main program for rpkid.
  """

  def __init__(self):

    os.environ.update(TZ = "UTC",
                      DJANGO_SETTINGS_MODULE = "rpki.django_settings.rpkid")
    time.tzset()

    self.irdbd_cms_timestamp = None
    self.irbe_cms_timestamp = None
    self.task_current = None
    self.task_queue = []

    parser = argparse.ArgumentParser(description = __doc__)
    parser.add_argument("-c", "--config",
                        help = "override default location of configuration file")
    parser.add_argument("-f", "--foreground", action = "store_true",
                        help = "do not daemonize")
    parser.add_argument("--pidfile",
                        help = "override default location of pid file")
    parser.add_argument("--profile",
                        help = "enable profiling, saving data to PROFILE")
    rpki.log.argparse_setup(parser)
    args = parser.parse_args()

    self.profile = args.profile

    rpki.log.init("rpkid", args)

    self.cfg = rpki.config.parser(set_filename = args.config, section = "rpkid")
    self.cfg.set_global_flags()

    if not args.foreground:
      rpki.daemonize.daemon(pidfile = args.pidfile)

    if self.profile:
      import cProfile
      prof = cProfile.Profile()
      try:
        prof.runcall(self.main)
      finally:
        prof.dump_stats(self.profile)
        logger.info("Dumped profile data to %s", self.profile)
    else:
      self.main()

  def main(self):

    startup_msg = self.cfg.get("startup-message", "")
    if startup_msg:
      logger.info(startup_msg)

    if self.profile:
      logger.info("Running in profile mode with output to %s", self.profile)

    logger.debug("Initializing Django")

    import django
    django.setup()

    logger.debug("Initializing rpkidb...")

    global rpki                         # pylint: disable=W0602
    import rpki.rpkidb                  # pylint: disable=W0621

    logger.debug("Initializing rpkidb...done")

    self.bpki_ta    = rpki.x509.X509(Auto_update = self.cfg.get("bpki-ta"))
    self.irdb_cert  = rpki.x509.X509(Auto_update = self.cfg.get("irdb-cert"))
    self.irbe_cert  = rpki.x509.X509(Auto_update = self.cfg.get("irbe-cert"))
    self.rpkid_cert = rpki.x509.X509(Auto_update = self.cfg.get("rpkid-cert"))
    self.rpkid_key  = rpki.x509.RSA( Auto_update = self.cfg.get("rpkid-key"))

    self.irdb_url   = self.cfg.get("irdb-url")

    self.http_server_host = self.cfg.get("server-host", "")
    self.http_server_port = self.cfg.getint("server-port")

    self.publication_kludge_base = self.cfg.get("publication-kludge-base", "publication/")

    self.use_internal_cron = self.cfg.getboolean("use-internal-cron", True)

    self.initial_delay = random.randint(self.cfg.getint("initial-delay-min", 10),
                                        self.cfg.getint("initial-delay-max", 120))

    # Should be much longer in production
    self.cron_period = rpki.sundial.timedelta(seconds = self.cfg.getint("cron-period", 120))
    self.cron_keepalive = rpki.sundial.timedelta(seconds = self.cfg.getint("cron-keepalive", 0))
    if not self.cron_keepalive:
      self.cron_keepalive = self.cron_period * 4
    self.cron_timeout = None

    self.start_cron()

    rpki.http.server(
      host     = self.http_server_host,
      port     = self.http_server_port,
      handlers = (("/left-right", self.left_right_handler),
                  ("/up-down/",   self.up_down_handler, rpki.up_down.allowed_content_types),
                  ("/cronjob",    self.cronjob_handler)))

  def start_cron(self):
    """
    Start clock for rpkid's internal cron process.
    """

    if self.use_internal_cron:
      self.cron_timer = rpki.async.timer(handler = self.cron)
      when = rpki.sundial.now() + rpki.sundial.timedelta(seconds = self.initial_delay)
      logger.debug("Scheduling initial cron pass at %s", when)
      self.cron_timer.set(when)
    else:
      logger.debug("Not using internal clock, start_cron() call ignored")

  @staticmethod
  def _compose_left_right_query():
    """
    Compose top level element of a left-right query to irdbd.
    """

    return Element(rpki.left_right.tag_msg, nsmap = rpki.left_right.nsmap,
                   type = "query", version = rpki.left_right.version)

  def irdb_query(self, q_msg, callback, errback):
    """
    Perform an IRDB callback query.
    """

    try:
      q_tags = set(q_pdu.tag for q_pdu in q_msg)

      q_der = rpki.left_right.cms_msg().wrap(q_msg, self.rpkid_key, self.rpkid_cert)

      def unwrap(r_der):
        try:
          r_cms = rpki.left_right.cms_msg(DER = r_der)
          r_msg = r_cms.unwrap((self.bpki_ta, self.irdb_cert))
          self.irdbd_cms_timestamp = r_cms.check_replay(self.irdbd_cms_timestamp, self.irdb_url)
          #rpki.left_right.check_response(r_msg)
          if r_msg.get("type") != "reply" or not all(r_pdu.tag in q_tags for r_pdu in r_msg):
            raise rpki.exceptions.BadIRDBReply(
              "Unexpected response to IRDB query: %s" % r_cms.pretty_print_content())
          callback(r_msg)
        except Exception, e:
          errback(e)

      rpki.http.client(
        url          = self.irdb_url,
        msg          = q_der,
        callback     = unwrap,
        errback      = errback)

    except Exception, e:
      errback(e)


  def irdb_query_child_resources(self, self_handle, child_handle, callback, errback):
    """
    Ask IRDB about a child's resources.
    """

    q_msg = self._compose_left_right_query()
    SubElement(q_msg, rpki.left_right.tag_list_resources,
               self_handle = self_handle, child_handle = child_handle)

    def done(r_msg):
      if len(r_msg) != 1:
        raise rpki.exceptions.BadIRDBReply(
          "Expected exactly one PDU from IRDB: %s" % r_msg.pretty_print_content())
      callback(rpki.resource_set.resource_bag(
        asn         = rpki.resource_set.resource_set_as(r_msg[0].get("asn")),
        v4          = rpki.resource_set.resource_set_ipv4(r_msg[0].get("ipv4")),
        v6          = rpki.resource_set.resource_set_ipv6(r_msg[0].get("ipv6")),
        valid_until = rpki.sundial.datetime.fromXMLtime(r_msg[0].get("valid_until"))))

    self.irdb_query(q_msg, done, errback)

  def irdb_query_roa_requests(self, self_handle, callback, errback):
    """
    Ask IRDB about self's ROA requests.
    """

    q_msg = self._compose_left_right_query()
    SubElement(q_msg, rpki.left_right.tag_list_roa_requests, self_handle = self_handle)
    self.irdb_query(q_msg, callback, errback)

  def irdb_query_ghostbuster_requests(self, self_handle, parent_handles, callback, errback):
    """
    Ask IRDB about self's ghostbuster record requests.
    """

    q_msg = self._compose_left_right_query()
    for parent_handle in parent_handles:
      SubElement(q_msg, rpki.left_right.tag_list_ghostbuster_requests,
                 self_handle = self_handle, parent_handle = parent_handle)
    self.irdb_query(q_msg, callback, errback)

  def irdb_query_ee_certificate_requests(self, self_handle, callback, errback):
    """
    Ask IRDB about self's EE certificate requests.
    """

    q_msg = self._compose_left_right_query()
    SubElement(q_msg, rpki.left_right.tag_list_ee_certificate_requests, self_handle = self_handle)
    self.irdb_query(q_msg, callback, errback)

  @property
  def left_right_models(self):
    """
    Map element tag to rpkidb model.
    """

    try:
      return self._left_right_models
    except AttributeError:
      import rpki.rpkidb.models         # pylint: disable=W0621
      self._left_right_models = {
        rpki.left_right.tag_self        : rpki.rpkidb.models.Self,
        rpki.left_right.tag_bsc         : rpki.rpkidb.models.BSC,
        rpki.left_right.tag_parent      : rpki.rpkidb.models.Parent,
        rpki.left_right.tag_child       : rpki.rpkidb.models.Child,
        rpki.left_right.tag_repository  : rpki.rpkidb.models.Repository }
      return self._left_right_models

  @property
  def left_right_trivial_handlers(self):
    """
    Map element tag to bound handler methods for trivial PDU types.
    """

    try:
      return self._left_right_trivial_handlers
    except AttributeError:
      self._left_right_trivial_handlers = {
        rpki.left_right.tag_list_published_objects      : self.handle_list_published_objects,
        rpki.left_right.tag_list_received_resources     : self.handle_list_received_resources }
      return self._left_right_trivial_handlers

  def handle_list_published_objects(self, q_pdu, r_msg):
    """
    <list_published_objects/> server.
    """

    self_handle = q_pdu.get("self_handle")
    msg_tag     = q_pdu.get("tag")

    kw = dict(self_handle = self_handle)
    if msg_tag is not None:
      kw.update(tag = msg_tag)

    for ca_detail in rpki.rpkidb.models.CADetail.objects.filter(ca__parent__self__self_handle = self_handle, state = "active"):
      SubElement(r_msg, rpki.left_right.tag_list_published_objects,
                 uri = ca_detail.crl_uri, **kw).text = ca_detail.latest_crl.get_Base64()
      SubElement(r_msg, rpki.left_right.tag_list_published_objects,
                 uri = ca_detail.manifest_uri, **kw).text = ca_detail.latest_manifest.get_Base64()
      for c in ca_detail.child_certs.all():
        SubElement(r_msg, rpki.left_right.tag_list_published_objects,
                   uri = c.uri, child_handle = c.child.child_handle, **kw).text = c.cert.get_Base64()
      for r in ca_detail.roas.filter(roa__isnull = False):
        SubElement(r_msg, rpki.left_right.tag_list_published_objects,
                   uri = r.uri, **kw).text = r.roa.get_Base64()
      for g in ca_detail.ghostbusters.all():
        SubElement(r_msg, rpki.left_right.tag_list_published_objects,
                   uri = g.uri, **kw).text = g.ghostbuster.get_Base64()
      for c in ca_detail.ee_certificates.all():
        SubElement(r_msg, rpki.left_right.tag_list_published_objects,
                   uri = c.uri, **kw).text = c.cert.get_Base64()

  def handle_list_received_resources(self, q_pdu, r_msg):
    """
    <list_received_resources/> server.
    """

    logger.debug(".handle_list_received_resources() %s", ElementToString(q_pdu))
    self_handle = q_pdu.get("self_handle")
    msg_tag     = q_pdu.get("tag")
    for ca_detail in rpki.rpkidb.models.CADetail.objects.filter(ca__parent__self__self_handle = self_handle,
                                                                state = "active", latest_ca_cert__isnull = False):
      cert      = ca_detail.latest_ca_cert
      resources = cert.get_3779resources()
      r_pdu = SubElement(r_msg, rpki.left_right.tag_list_received_resources,
                         self_handle        = self_handle,
                         parent_handle      = ca_detail.ca.parent.parent_handle,
                         uri                = ca_detail.ca_cert_uri,
                         notBefore          = str(cert.getNotBefore()),
                         notAfter           = str(cert.getNotAfter()),
                         sia_uri            = cert.get_sia_directory_uri(),
                         aia_uri            = cert.get_aia_uri(),
                         asn                = str(resources.asn),
                         ipv4               = str(resources.v4),
                         ipv6               = str(resources.v6))
      if msg_tag is not None:
        r_pdu.set("tag", msg_tag)


  def left_right_handler(self, query, path, cb):
    """
    Process one left-right PDU.
    """

    # This handles five persistent classes (self, bsc, parent, child,
    # repository) and two simple queries (list_published_objects and
    # list_received_resources).  The former probably need to dispatch
    # via methods to the corresponding model classes; the latter
    # probably just become calls to ordinary methods of this
    # (rpki.rpkid.main) class.
    #
    # Need to clone logic from rpki.pubd.main.control_handler().

    logger.debug("Entering left_right_handler()")

    try:
      q_cms = rpki.left_right.cms_msg(DER = query)
      q_msg = q_cms.unwrap((self.bpki_ta, self.irbe_cert))
      r_msg = Element(rpki.left_right.tag_msg, nsmap = rpki.left_right.nsmap,
                      type = "reply", version = rpki.left_right.version)
      self.irbe_cms_timestamp = q_cms.check_replay(self.irbe_cms_timestamp, path)

      assert q_msg.tag.startswith(rpki.left_right.xmlns)
      assert all(q_pdu.tag.startswith(rpki.left_right.xmlns) for q_pdu in q_msg)

      if q_msg.get("version") != rpki.left_right.version:
        raise rpki.exceptions.BadQuery("Unrecognized protocol version")

      if q_msg.get("type") != "query":
        raise rpki.exceptions.BadQuery("Message type is not query")

      def done():
        cb(200, body = rpki.left_right.cms_msg().wrap(r_msg, self.rpkid_key, self.rpkid_cert))

      def loop(iterator, q_pdu):

        logger.debug("left_right_handler():loop(%r)", q_pdu)

        def fail(e):
          if not isinstance(e, rpki.exceptions.NotFound):
            logger.exception("Unhandled exception serving left-right PDU %r", q_pdu)
          error_self_handle = q_pdu.get("self_handle")
          error_tag         = q_pdu.get("tag")
          r_pdu = SubElement(r_msg, rpki.left_right.tag_report_error, error_code = e.__class__.__name__)
          r_pdu.text = str(e)
          if error_tag is not None:
            r_pdu.set("tag", error_tag)
          if error_self_handle is not None:
            r_pdu.set("self_handle", error_self_handle)
          cb(200, body = rpki.left_right.cms_msg().wrap(r_msg, self.rpkid_key, self.rpkid_cert))

        try:
          if q_pdu.tag in self.left_right_trivial_handlers:
            logger.debug("left_right_handler(): trivial handler")
            self.left_right_trivial_handlers[q_pdu.tag](q_pdu, r_msg)
            iterator()

          else:
            action = q_pdu.get("action")
            model  = self.left_right_models[q_pdu.tag]

            logger.debug("left_right_handler(): action %s model %r", action, model)

            if action in ("get", "list"):
              logger.debug("left_right_handler(): get/list")
              for obj in model.objects.xml_list(q_pdu):
                logger.debug("left_right_handler(): get/list: encoding %r", obj)
                obj.xml_template.encode(obj, q_pdu, r_msg)
              iterator()

            elif action == "destroy":
              def destroy_cb():
                obj.delete()
                obj.xml_template.acknowledge(obj, q_pdu, r_msg)
                iterator()
              logger.debug("left_right_handler(): destroy")
              obj = model.objects.xml_get_for_delete(q_pdu)
              obj.xml_pre_delete_hook(self, destroy_cb, fail)

            elif action in ("create", "set"):
              def create_set_cb():
                obj.xml_template.acknowledge(obj, q_pdu, r_msg)
                iterator()
              logger.debug("left_right_handler(): create/set")
              obj = model.objects.xml_get_or_create(q_pdu)
              obj.xml_template.decode(obj, q_pdu)
              obj.xml_pre_save_hook(q_pdu)
              obj.save()
              obj.xml_post_save_hook(self, q_pdu, create_set_cb, fail)

            else:
              raise rpki.exceptions.BadQuery

        except (rpki.async.ExitNow, SystemExit):
          raise
        except Exception, e:
          fail(e)

      rpki.async.iterator(q_msg, loop, done)

    except (rpki.async.ExitNow, SystemExit):
      raise

    except Exception, e:
      logger.exception("Unhandled exception serving left-right request")
      cb(500, reason = "Unhandled exception %s: %s" % (e.__class__.__name__, e))

  up_down_url_regexp = re.compile("/up-down/([-A-Z0-9_]+)/([-A-Z0-9_]+)$", re.I)

  def up_down_handler(self, q_der, path, cb):
    """
    Process one up-down PDU.
    """

    def done(r_der):
      cb(200, body = r_der)

    try:
      match = self.up_down_url_regexp.search(path)
      if match is None:
        raise rpki.exceptions.BadContactURL("Bad URL path received in up_down_handler(): %s" % path)
      self_handle, child_handle = match.groups()
      try:
        child = rpki.rpkidb.models.Child.objects.get(self__self_handle = self_handle, child_handle = child_handle)
      except rpki.rpkidb.models.Child.DoesNotExist:
        raise rpki.exceptions.ChildNotFound("Could not find child %s of self %s in up_down_handler()" % (
          child_handle, self_handle))
      child.serve_up_down(self, q_der, done)
    except (rpki.async.ExitNow, SystemExit):
      raise
    except (rpki.exceptions.ChildNotFound, rpki.exceptions.BadContactURL), e:
      logger.warning(str(e))
      cb(400, reason = str(e))
    except Exception, e:
      logger.exception("Unhandled exception processing up-down request")
      cb(400, reason = "Could not process PDU: %s" % e)

  def checkpoint(self, force = False):
    """
    Record that we were still alive when we got here, by resetting
    keepalive timer.
    """

    if force or self.cron_timeout is not None:
      self.cron_timeout = rpki.sundial.now() + self.cron_keepalive

  def task_add(self, task):
    """
    Add a task to the scheduler task queue, unless it's already queued.
    """

    if task not in self.task_queue:
      logger.debug("Adding %r to task queue", task)
      self.task_queue.append(task)
      return True
    else:
      logger.debug("Task %r was already in the task queue", task)
      return False

  def task_next(self):
    """
    Pull next task from the task queue and put it the deferred event
    queue (we don't want to run it directly, as that could eventually
    blow out our call stack).
    """

    try:
      self.task_current = self.task_queue.pop(0)
    except IndexError:
      self.task_current = None
    else:
      rpki.async.event_defer(self.task_current)

  def task_run(self):
    """
    Run first task on the task queue, unless one is running already.
    """

    if self.task_current is None:
      self.task_next()

  def cron(self, cb = None):
    """
    Periodic tasks.
    """

    now = rpki.sundial.now()

    logger.debug("Starting cron run")

    def done():
      self.cron_timeout = None
      logger.info("Finished cron run started at %s", now)
      if cb is not None:
        cb()

    completion = rpki.rpkid_tasks.CompletionHandler(done)
    try:
      selves = rpki.rpkidb.models.Self.objects.all()
    except Exception:
      logger.exception("Error pulling selves from SQL, maybe SQL server is down?")
    else:
      for s in selves:
        s.schedule_cron_tasks(self, completion)
    nothing_queued = completion.count == 0

    assert self.use_internal_cron or self.cron_timeout is None

    if self.cron_timeout is not None and self.cron_timeout < now:
      logger.warning("cron keepalive threshold %s has expired, breaking lock", self.cron_timeout)
      self.cron_timeout = None

    if self.use_internal_cron:
      when = now + self.cron_period
      logger.debug("Scheduling next cron run at %s", when)
      self.cron_timer.set(when)

    if self.cron_timeout is None:
      self.checkpoint(self.use_internal_cron)
      self.task_run()

    elif self.use_internal_cron:
      logger.warning("cron already running, keepalive will expire at %s", self.cron_timeout)

    if nothing_queued:
      done()

  def cronjob_handler(self, query, path, cb):
    """
    External trigger for periodic tasks.  This is somewhat obsolete
    now that we have internal timers, but the test framework still
    uses it.
    """

    def done():
      cb(200, body = "OK")

    if self.use_internal_cron:
      cb(500, reason = "Running cron internally")
    else:
      logger.debug("Starting externally triggered cron")
      self.cron(done)


class publication_queue(object):
  """
  Utility to simplify publication from within rpkid.

  General idea here is to accumulate a collection of objects to be
  published, in one or more repositories, each potentially with its
  own completion callback.  Eventually we want to publish everything
  we've accumulated, at which point we need to iterate over the
  collection and do repository.call_pubd() for each repository.
  """

  replace = True

  def __init__(self, rpkid):
    self.rpkid = rpkid
    self.clear()

  def clear(self):
    self.repositories = {}
    self.msgs = {}
    self.handlers = {}
    if self.replace:
      self.uris = {}

  def queue(self, uri, repository, handler = None,
            old_obj = None, new_obj = None, old_hash = None):

    assert old_obj is not None or new_obj is not None or old_hash is not None
    assert old_obj is None or old_hash is None
    assert old_obj is None or isinstance(old_obj, rpki.x509.uri_dispatch(uri))
    assert new_obj is None or isinstance(new_obj, rpki.x509.uri_dispatch(uri))

    logger.debug("Queuing publication action: uri %s, old %r, new %r, hash %s",
                 uri, old_obj, new_obj, old_hash)

    # id(repository) may need to change to repository.peer_contact_uri
    # once we convert from our custom SQL cache to Django ORM.

    rid = id(repository)
    if rid not in self.repositories:
      self.repositories[rid] = repository
      self.msgs[rid] = Element(rpki.publication.tag_msg, nsmap = rpki.publication.nsmap,
                               type = "query", version = rpki.publication.version)

    if self.replace and uri in self.uris:
      logger.debug("Removing publication duplicate %r", self.uris[uri])
      old_pdu = self.uris.pop(uri)
      self.msgs[rid].remove(old_pdu)
      pdu_hash = old_pdu.get("hash")
    elif old_hash is not None:
      pdu_hash = old_hash
    elif old_obj is None:
      pdu_hash = None
    else:
      pdu_hash = rpki.x509.sha256(old_obj.get_DER()).encode("hex")

    if new_obj is None:
      pdu = SubElement(self.msgs[rid], rpki.publication.tag_withdraw, uri = uri, hash = pdu_hash)
    else:
      pdu = SubElement(self.msgs[rid], rpki.publication.tag_publish,  uri = uri)
      pdu.text = new_obj.get_Base64()
      if pdu_hash is not None:
        pdu.set("hash", pdu_hash)

    if handler is not None:
      tag = str(id(pdu))
      self.handlers[tag] = handler
      pdu.set("tag", tag)

    if self.replace:
      self.uris[uri] = pdu

  def call_pubd(self, cb, eb):
    def loop(iterator, rid):
      logger.debug("Calling pubd[%r]", self.repositories[rid])
      self.repositories[rid].call_pubd(self.rpkid, iterator, eb, self.msgs[rid], self.handlers)
    def done():
      self.clear()
      cb()
    rpki.async.iterator(self.repositories, loop, done)

  @property
  def size(self):
    return sum(len(self.msgs[rid]) for rid in self.repositories)

  def empty(self):
    assert (not self.msgs) == (self.size == 0), "Assertion failure: not self.msgs: %r, self.size %r" % (not self.msgs, self.size)
    return not self.msgs