Merge branch 'devel-2.1'
[ganeti-local] / daemons / ganeti-noded
1 #!/usr/bin/python
2 #
3
4 # Copyright (C) 2006, 2007 Google Inc.
5 #
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 2 of the License, or
9 # (at your option) any later version.
10 #
11 # This program is distributed in the hope that it will be useful, but
12 # WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14 # General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
19 # 02110-1301, USA.
20
21
22 """Ganeti node daemon"""
23
24 # pylint: disable-msg=C0103,W0142
25
26 # C0103: Functions in this module need to have a given name structure,
27 # and the name of the daemon doesn't match
28
29 # W0142: Used * or ** magic, since we do use it extensively in this
30 # module
31
32 import os
33 import sys
34 import logging
35 import signal
36
37 from optparse import OptionParser
38
39 from ganeti import backend
40 from ganeti import constants
41 from ganeti import objects
42 from ganeti import errors
43 from ganeti import jstore
44 from ganeti import daemon
45 from ganeti import http
46 from ganeti import utils
47 from ganeti import storage
48 from ganeti import serializer
49
50 import ganeti.http.server # pylint: disable-msg=W0611
51
52
53 queue_lock = None
54
55
56 def _PrepareQueueLock():
57   """Try to prepare the queue lock.
58
59   @return: None for success, otherwise an exception object
60
61   """
62   global queue_lock # pylint: disable-msg=W0603
63
64   if queue_lock is not None:
65     return None
66
67   # Prepare job queue
68   try:
69     queue_lock = jstore.InitAndVerifyQueue(must_lock=False)
70     return None
71   except EnvironmentError, err:
72     return err
73
74
75 def _RequireJobQueueLock(fn):
76   """Decorator for job queue manipulating functions.
77
78   """
79   QUEUE_LOCK_TIMEOUT = 10
80
81   def wrapper(*args, **kwargs):
82     # Locking in exclusive, blocking mode because there could be several
83     # children running at the same time. Waiting up to 10 seconds.
84     if _PrepareQueueLock() is not None:
85       raise errors.JobQueueError("Job queue failed initialization,"
86                                  " cannot update jobs")
87     queue_lock.Exclusive(blocking=True, timeout=QUEUE_LOCK_TIMEOUT)
88     try:
89       return fn(*args, **kwargs)
90     finally:
91       queue_lock.Unlock()
92
93   return wrapper
94
95
96 class NodeHttpServer(http.server.HttpServer):
97   """The server implementation.
98
99   This class holds all methods exposed over the RPC interface.
100
101   """
102   # too many public methods, and unused args - all methods get params
103   # due to the API
104   # pylint: disable-msg=R0904,W0613
105   def __init__(self, *args, **kwargs):
106     http.server.HttpServer.__init__(self, *args, **kwargs)
107     self.noded_pid = os.getpid()
108
109   def HandleRequest(self, req):
110     """Handle a request.
111
112     """
113     if req.request_method.upper() != http.HTTP_PUT:
114       raise http.HttpBadRequest()
115
116     path = req.request_path
117     if path.startswith("/"):
118       path = path[1:]
119
120     method = getattr(self, "perspective_%s" % path, None)
121     if method is None:
122       raise http.HttpNotFound()
123
124     try:
125       result = (True, method(serializer.LoadJson(req.request_body)))
126
127     except backend.RPCFail, err:
128       # our custom failure exception; str(err) works fine if the
129       # exception was constructed with a single argument, and in
130       # this case, err.message == err.args[0] == str(err)
131       result = (False, str(err))
132     except errors.QuitGanetiException, err:
133       # Tell parent to quit
134       logging.info("Shutting down the node daemon, arguments: %s",
135                    str(err.args))
136       os.kill(self.noded_pid, signal.SIGTERM)
137       # And return the error's arguments, which must be already in
138       # correct tuple format
139       result = err.args
140     except Exception, err:
141       logging.exception("Error in RPC call")
142       result = (False, "Error while executing backend function: %s" % str(err))
143
144     return serializer.DumpJson(result, indent=False)
145
146   # the new block devices  --------------------------
147
148   @staticmethod
149   def perspective_blockdev_create(params):
150     """Create a block device.
151
152     """
153     bdev_s, size, owner, on_primary, info = params
154     bdev = objects.Disk.FromDict(bdev_s)
155     if bdev is None:
156       raise ValueError("can't unserialize data!")
157     return backend.BlockdevCreate(bdev, size, owner, on_primary, info)
158
159   @staticmethod
160   def perspective_blockdev_remove(params):
161     """Remove a block device.
162
163     """
164     bdev_s = params[0]
165     bdev = objects.Disk.FromDict(bdev_s)
166     return backend.BlockdevRemove(bdev)
167
168   @staticmethod
169   def perspective_blockdev_rename(params):
170     """Remove a block device.
171
172     """
173     devlist = [(objects.Disk.FromDict(ds), uid) for ds, uid in params]
174     return backend.BlockdevRename(devlist)
175
176   @staticmethod
177   def perspective_blockdev_assemble(params):
178     """Assemble a block device.
179
180     """
181     bdev_s, owner, on_primary = params
182     bdev = objects.Disk.FromDict(bdev_s)
183     if bdev is None:
184       raise ValueError("can't unserialize data!")
185     return backend.BlockdevAssemble(bdev, owner, on_primary)
186
187   @staticmethod
188   def perspective_blockdev_shutdown(params):
189     """Shutdown a block device.
190
191     """
192     bdev_s = params[0]
193     bdev = objects.Disk.FromDict(bdev_s)
194     if bdev is None:
195       raise ValueError("can't unserialize data!")
196     return backend.BlockdevShutdown(bdev)
197
198   @staticmethod
199   def perspective_blockdev_addchildren(params):
200     """Add a child to a mirror device.
201
202     Note: this is only valid for mirror devices. It's the caller's duty
203     to send a correct disk, otherwise we raise an error.
204
205     """
206     bdev_s, ndev_s = params
207     bdev = objects.Disk.FromDict(bdev_s)
208     ndevs = [objects.Disk.FromDict(disk_s) for disk_s in ndev_s]
209     if bdev is None or ndevs.count(None) > 0:
210       raise ValueError("can't unserialize data!")
211     return backend.BlockdevAddchildren(bdev, ndevs)
212
213   @staticmethod
214   def perspective_blockdev_removechildren(params):
215     """Remove a child from a mirror device.
216
217     This is only valid for mirror devices, of course. It's the callers
218     duty to send a correct disk, otherwise we raise an error.
219
220     """
221     bdev_s, ndev_s = params
222     bdev = objects.Disk.FromDict(bdev_s)
223     ndevs = [objects.Disk.FromDict(disk_s) for disk_s in ndev_s]
224     if bdev is None or ndevs.count(None) > 0:
225       raise ValueError("can't unserialize data!")
226     return backend.BlockdevRemovechildren(bdev, ndevs)
227
228   @staticmethod
229   def perspective_blockdev_getmirrorstatus(params):
230     """Return the mirror status for a list of disks.
231
232     """
233     disks = [objects.Disk.FromDict(dsk_s)
234              for dsk_s in params]
235     return [status.ToDict()
236             for status in backend.BlockdevGetmirrorstatus(disks)]
237
238   @staticmethod
239   def perspective_blockdev_find(params):
240     """Expose the FindBlockDevice functionality for a disk.
241
242     This will try to find but not activate a disk.
243
244     """
245     disk = objects.Disk.FromDict(params[0])
246
247     result = backend.BlockdevFind(disk)
248     if result is None:
249       return None
250
251     return result.ToDict()
252
253   @staticmethod
254   def perspective_blockdev_snapshot(params):
255     """Create a snapshot device.
256
257     Note that this is only valid for LVM disks, if we get passed
258     something else we raise an exception. The snapshot device can be
259     remove by calling the generic block device remove call.
260
261     """
262     cfbd = objects.Disk.FromDict(params[0])
263     return backend.BlockdevSnapshot(cfbd)
264
265   @staticmethod
266   def perspective_blockdev_grow(params):
267     """Grow a stack of devices.
268
269     """
270     cfbd = objects.Disk.FromDict(params[0])
271     amount = params[1]
272     return backend.BlockdevGrow(cfbd, amount)
273
274   @staticmethod
275   def perspective_blockdev_close(params):
276     """Closes the given block devices.
277
278     """
279     disks = [objects.Disk.FromDict(cf) for cf in params[1]]
280     return backend.BlockdevClose(params[0], disks)
281
282   @staticmethod
283   def perspective_blockdev_getsize(params):
284     """Compute the sizes of the given block devices.
285
286     """
287     disks = [objects.Disk.FromDict(cf) for cf in params[0]]
288     return backend.BlockdevGetsize(disks)
289
290   @staticmethod
291   def perspective_blockdev_export(params):
292     """Compute the sizes of the given block devices.
293
294     """
295     disk = objects.Disk.FromDict(params[0])
296     dest_node, dest_path, cluster_name = params[1:]
297     return backend.BlockdevExport(disk, dest_node, dest_path, cluster_name)
298
299   # blockdev/drbd specific methods ----------
300
301   @staticmethod
302   def perspective_drbd_disconnect_net(params):
303     """Disconnects the network connection of drbd disks.
304
305     Note that this is only valid for drbd disks, so the members of the
306     disk list must all be drbd devices.
307
308     """
309     nodes_ip, disks = params
310     disks = [objects.Disk.FromDict(cf) for cf in disks]
311     return backend.DrbdDisconnectNet(nodes_ip, disks)
312
313   @staticmethod
314   def perspective_drbd_attach_net(params):
315     """Attaches the network connection of drbd disks.
316
317     Note that this is only valid for drbd disks, so the members of the
318     disk list must all be drbd devices.
319
320     """
321     nodes_ip, disks, instance_name, multimaster = params
322     disks = [objects.Disk.FromDict(cf) for cf in disks]
323     return backend.DrbdAttachNet(nodes_ip, disks,
324                                      instance_name, multimaster)
325
326   @staticmethod
327   def perspective_drbd_wait_sync(params):
328     """Wait until DRBD disks are synched.
329
330     Note that this is only valid for drbd disks, so the members of the
331     disk list must all be drbd devices.
332
333     """
334     nodes_ip, disks = params
335     disks = [objects.Disk.FromDict(cf) for cf in disks]
336     return backend.DrbdWaitSync(nodes_ip, disks)
337
338   # export/import  --------------------------
339
340   @staticmethod
341   def perspective_snapshot_export(params):
342     """Export a given snapshot.
343
344     """
345     disk = objects.Disk.FromDict(params[0])
346     dest_node = params[1]
347     instance = objects.Instance.FromDict(params[2])
348     cluster_name = params[3]
349     dev_idx = params[4]
350     return backend.ExportSnapshot(disk, dest_node, instance,
351                                   cluster_name, dev_idx)
352
353   @staticmethod
354   def perspective_finalize_export(params):
355     """Expose the finalize export functionality.
356
357     """
358     instance = objects.Instance.FromDict(params[0])
359     snap_disks = [objects.Disk.FromDict(str_data)
360                   for str_data in params[1]]
361     return backend.FinalizeExport(instance, snap_disks)
362
363   @staticmethod
364   def perspective_export_info(params):
365     """Query information about an existing export on this node.
366
367     The given path may not contain an export, in which case we return
368     None.
369
370     """
371     path = params[0]
372     return backend.ExportInfo(path)
373
374   @staticmethod
375   def perspective_export_list(params):
376     """List the available exports on this node.
377
378     Note that as opposed to export_info, which may query data about an
379     export in any path, this only queries the standard Ganeti path
380     (constants.EXPORT_DIR).
381
382     """
383     return backend.ListExports()
384
385   @staticmethod
386   def perspective_export_remove(params):
387     """Remove an export.
388
389     """
390     export = params[0]
391     return backend.RemoveExport(export)
392
393   # volume  --------------------------
394
395   @staticmethod
396   def perspective_lv_list(params):
397     """Query the list of logical volumes in a given volume group.
398
399     """
400     vgname = params[0]
401     return backend.GetVolumeList(vgname)
402
403   @staticmethod
404   def perspective_vg_list(params):
405     """Query the list of volume groups.
406
407     """
408     return backend.ListVolumeGroups()
409
410   # Storage --------------------------
411
412   @staticmethod
413   def perspective_storage_list(params):
414     """Get list of storage units.
415
416     """
417     (su_name, su_args, name, fields) = params
418     return storage.GetStorage(su_name, *su_args).List(name, fields)
419
420   @staticmethod
421   def perspective_storage_modify(params):
422     """Modify a storage unit.
423
424     """
425     (su_name, su_args, name, changes) = params
426     return storage.GetStorage(su_name, *su_args).Modify(name, changes)
427
428   @staticmethod
429   def perspective_storage_execute(params):
430     """Execute an operation on a storage unit.
431
432     """
433     (su_name, su_args, name, op) = params
434     return storage.GetStorage(su_name, *su_args).Execute(name, op)
435
436   # bridge  --------------------------
437
438   @staticmethod
439   def perspective_bridges_exist(params):
440     """Check if all bridges given exist on this node.
441
442     """
443     bridges_list = params[0]
444     return backend.BridgesExist(bridges_list)
445
446   # instance  --------------------------
447
448   @staticmethod
449   def perspective_instance_os_add(params):
450     """Install an OS on a given instance.
451
452     """
453     inst_s = params[0]
454     inst = objects.Instance.FromDict(inst_s)
455     reinstall = params[1]
456     return backend.InstanceOsAdd(inst, reinstall)
457
458   @staticmethod
459   def perspective_instance_run_rename(params):
460     """Runs the OS rename script for an instance.
461
462     """
463     inst_s, old_name = params
464     inst = objects.Instance.FromDict(inst_s)
465     return backend.RunRenameInstance(inst, old_name)
466
467   @staticmethod
468   def perspective_instance_os_import(params):
469     """Run the import function of an OS onto a given instance.
470
471     """
472     inst_s, src_node, src_images, cluster_name = params
473     inst = objects.Instance.FromDict(inst_s)
474     return backend.ImportOSIntoInstance(inst, src_node, src_images,
475                                         cluster_name)
476
477   @staticmethod
478   def perspective_instance_shutdown(params):
479     """Shutdown an instance.
480
481     """
482     instance = objects.Instance.FromDict(params[0])
483     timeout = params[1]
484     return backend.InstanceShutdown(instance, timeout)
485
486   @staticmethod
487   def perspective_instance_start(params):
488     """Start an instance.
489
490     """
491     instance = objects.Instance.FromDict(params[0])
492     return backend.StartInstance(instance)
493
494   @staticmethod
495   def perspective_migration_info(params):
496     """Gather information about an instance to be migrated.
497
498     """
499     instance = objects.Instance.FromDict(params[0])
500     return backend.MigrationInfo(instance)
501
502   @staticmethod
503   def perspective_accept_instance(params):
504     """Prepare the node to accept an instance.
505
506     """
507     instance, info, target = params
508     instance = objects.Instance.FromDict(instance)
509     return backend.AcceptInstance(instance, info, target)
510
511   @staticmethod
512   def perspective_finalize_migration(params):
513     """Finalize the instance migration.
514
515     """
516     instance, info, success = params
517     instance = objects.Instance.FromDict(instance)
518     return backend.FinalizeMigration(instance, info, success)
519
520   @staticmethod
521   def perspective_instance_migrate(params):
522     """Migrates an instance.
523
524     """
525     instance, target, live = params
526     instance = objects.Instance.FromDict(instance)
527     return backend.MigrateInstance(instance, target, live)
528
529   @staticmethod
530   def perspective_instance_reboot(params):
531     """Reboot an instance.
532
533     """
534     instance = objects.Instance.FromDict(params[0])
535     reboot_type = params[1]
536     shutdown_timeout = params[2]
537     return backend.InstanceReboot(instance, reboot_type, shutdown_timeout)
538
539   @staticmethod
540   def perspective_instance_info(params):
541     """Query instance information.
542
543     """
544     return backend.GetInstanceInfo(params[0], params[1])
545
546   @staticmethod
547   def perspective_instance_migratable(params):
548     """Query whether the specified instance can be migrated.
549
550     """
551     instance = objects.Instance.FromDict(params[0])
552     return backend.GetInstanceMigratable(instance)
553
554   @staticmethod
555   def perspective_all_instances_info(params):
556     """Query information about all instances.
557
558     """
559     return backend.GetAllInstancesInfo(params[0])
560
561   @staticmethod
562   def perspective_instance_list(params):
563     """Query the list of running instances.
564
565     """
566     return backend.GetInstanceList(params[0])
567
568   # node --------------------------
569
570   @staticmethod
571   def perspective_node_tcp_ping(params):
572     """Do a TcpPing on the remote node.
573
574     """
575     return utils.TcpPing(params[1], params[2], timeout=params[3],
576                          live_port_needed=params[4], source=params[0])
577
578   @staticmethod
579   def perspective_node_has_ip_address(params):
580     """Checks if a node has the given ip address.
581
582     """
583     return utils.OwnIpAddress(params[0])
584
585   @staticmethod
586   def perspective_node_info(params):
587     """Query node information.
588
589     """
590     vgname, hypervisor_type = params
591     return backend.GetNodeInfo(vgname, hypervisor_type)
592
593   @staticmethod
594   def perspective_node_add(params):
595     """Complete the registration of this node in the cluster.
596
597     """
598     return backend.AddNode(params[0], params[1], params[2],
599                            params[3], params[4], params[5])
600
601   @staticmethod
602   def perspective_node_verify(params):
603     """Run a verify sequence on this node.
604
605     """
606     return backend.VerifyNode(params[0], params[1])
607
608   @staticmethod
609   def perspective_node_start_master(params):
610     """Promote this node to master status.
611
612     """
613     return backend.StartMaster(params[0], params[1])
614
615   @staticmethod
616   def perspective_node_stop_master(params):
617     """Demote this node from master status.
618
619     """
620     return backend.StopMaster(params[0])
621
622   @staticmethod
623   def perspective_node_leave_cluster(params):
624     """Cleanup after leaving a cluster.
625
626     """
627     return backend.LeaveCluster(params[0])
628
629   @staticmethod
630   def perspective_node_volumes(params):
631     """Query the list of all logical volume groups.
632
633     """
634     return backend.NodeVolumes()
635
636   @staticmethod
637   def perspective_node_demote_from_mc(params):
638     """Demote a node from the master candidate role.
639
640     """
641     return backend.DemoteFromMC()
642
643
644   @staticmethod
645   def perspective_node_powercycle(params):
646     """Tries to powercycle the nod.
647
648     """
649     hypervisor_type = params[0]
650     return backend.PowercycleNode(hypervisor_type)
651
652
653   # cluster --------------------------
654
655   @staticmethod
656   def perspective_version(params):
657     """Query version information.
658
659     """
660     return constants.PROTOCOL_VERSION
661
662   @staticmethod
663   def perspective_upload_file(params):
664     """Upload a file.
665
666     Note that the backend implementation imposes strict rules on which
667     files are accepted.
668
669     """
670     return backend.UploadFile(*params)
671
672   @staticmethod
673   def perspective_master_info(params):
674     """Query master information.
675
676     """
677     return backend.GetMasterInfo()
678
679   @staticmethod
680   def perspective_write_ssconf_files(params):
681     """Write ssconf files.
682
683     """
684     (values,) = params
685     return backend.WriteSsconfFiles(values)
686
687   # os -----------------------
688
689   @staticmethod
690   def perspective_os_diagnose(params):
691     """Query detailed information about existing OSes.
692
693     """
694     return backend.DiagnoseOS()
695
696   @staticmethod
697   def perspective_os_get(params):
698     """Query information about a given OS.
699
700     """
701     name = params[0]
702     os_obj = backend.OSFromDisk(name)
703     return os_obj.ToDict()
704
705   # hooks -----------------------
706
707   @staticmethod
708   def perspective_hooks_runner(params):
709     """Run hook scripts.
710
711     """
712     hpath, phase, env = params
713     hr = backend.HooksRunner()
714     return hr.RunHooks(hpath, phase, env)
715
716   # iallocator -----------------
717
718   @staticmethod
719   def perspective_iallocator_runner(params):
720     """Run an iallocator script.
721
722     """
723     name, idata = params
724     iar = backend.IAllocatorRunner()
725     return iar.Run(name, idata)
726
727   # test -----------------------
728
729   @staticmethod
730   def perspective_test_delay(params):
731     """Run test delay.
732
733     """
734     duration = params[0]
735     status, rval = utils.TestDelay(duration)
736     if not status:
737       raise backend.RPCFail(rval)
738     return rval
739
740   # file storage ---------------
741
742   @staticmethod
743   def perspective_file_storage_dir_create(params):
744     """Create the file storage directory.
745
746     """
747     file_storage_dir = params[0]
748     return backend.CreateFileStorageDir(file_storage_dir)
749
750   @staticmethod
751   def perspective_file_storage_dir_remove(params):
752     """Remove the file storage directory.
753
754     """
755     file_storage_dir = params[0]
756     return backend.RemoveFileStorageDir(file_storage_dir)
757
758   @staticmethod
759   def perspective_file_storage_dir_rename(params):
760     """Rename the file storage directory.
761
762     """
763     old_file_storage_dir = params[0]
764     new_file_storage_dir = params[1]
765     return backend.RenameFileStorageDir(old_file_storage_dir,
766                                         new_file_storage_dir)
767
768   # jobs ------------------------
769
770   @staticmethod
771   @_RequireJobQueueLock
772   def perspective_jobqueue_update(params):
773     """Update job queue.
774
775     """
776     (file_name, content) = params
777     return backend.JobQueueUpdate(file_name, content)
778
779   @staticmethod
780   @_RequireJobQueueLock
781   def perspective_jobqueue_purge(params):
782     """Purge job queue.
783
784     """
785     return backend.JobQueuePurge()
786
787   @staticmethod
788   @_RequireJobQueueLock
789   def perspective_jobqueue_rename(params):
790     """Rename a job queue file.
791
792     """
793     # TODO: What if a file fails to rename?
794     return [backend.JobQueueRename(old, new) for old, new in params]
795
796   @staticmethod
797   def perspective_jobqueue_set_drain(params):
798     """Set/unset the queue drain flag.
799
800     """
801     drain_flag = params[0]
802     return backend.JobQueueSetDrainFlag(drain_flag)
803
804
805   # hypervisor ---------------
806
807   @staticmethod
808   def perspective_hypervisor_validate_params(params):
809     """Validate the hypervisor parameters.
810
811     """
812     (hvname, hvparams) = params
813     return backend.ValidateHVParams(hvname, hvparams)
814
815
816 def CheckNoded(_, args):
817   """Initial checks whether to run or exit with a failure.
818
819   """
820   if args: # noded doesn't take any arguments
821     print >> sys.stderr, ("Usage: %s [-f] [-d] [-p port] [-b ADDRESS]" %
822                           sys.argv[0])
823     sys.exit(constants.EXIT_FAILURE)
824
825
826 def ExecNoded(options, _):
827   """Main node daemon function, executed with the PID file held.
828
829   """
830   # Read SSL certificate
831   if options.ssl:
832     ssl_params = http.HttpSslParams(ssl_key_path=options.ssl_key,
833                                     ssl_cert_path=options.ssl_cert)
834   else:
835     ssl_params = None
836
837   err = _PrepareQueueLock()
838   if err is not None:
839     # this might be some kind of file-system/permission error; while
840     # this breaks the job queue functionality, we shouldn't prevent
841     # startup of the whole node daemon because of this
842     logging.critical("Can't init/verify the queue, proceeding anyway: %s", err)
843
844   mainloop = daemon.Mainloop()
845   server = NodeHttpServer(mainloop, options.bind_address, options.port,
846                           ssl_params=ssl_params, ssl_verify_peer=True)
847   server.Start()
848   try:
849     mainloop.Run()
850   finally:
851     server.Stop()
852
853
854 def main():
855   """Main function for the node daemon.
856
857   """
858   parser = OptionParser(description="Ganeti node daemon",
859                         usage="%prog [-f] [-d] [-p port] [-b ADDRESS]",
860                         version="%%prog (ganeti) %s" %
861                         constants.RELEASE_VERSION)
862   dirs = [(val, constants.RUN_DIRS_MODE) for val in constants.SUB_RUN_DIRS]
863   dirs.append((constants.LOG_OS_DIR, 0750))
864   dirs.append((constants.LOCK_DIR, 1777))
865   daemon.GenericMain(constants.NODED, parser, dirs, CheckNoded, ExecNoded,
866                      default_ssl_cert=constants.SSL_CERT_FILE,
867                      default_ssl_key=constants.SSL_CERT_FILE)
868
869
870 if __name__ == '__main__':
871   main()