1import hashlib
2import logging
3import os
4
5import salt.auth
6from salt.utils.versions import StrictVersion as _StrictVersion
7
8__virtualname__ = os.path.abspath(__file__).rsplit(os.sep)[-2] or "rest_tornado"
9
10log = logging.getLogger(__virtualname__)
11
12# we require at least 4.0, as that includes all the Future's stuff we use
13min_tornado_version = "4.0"
14has_tornado = False
15try:
16    import salt.ext.tornado
17
18    if _StrictVersion(salt.ext.tornado.version) >= _StrictVersion(min_tornado_version):
19        has_tornado = True
20    else:
21        log.error("rest_tornado requires at least tornado %s", min_tornado_version)
22except (ImportError, TypeError) as err:
23    has_tornado = False
24    log.error("ImportError! %s", err)
25
26
27def __virtual__():
28    mod_opts = __opts__.get(__virtualname__, {})
29
30    if has_tornado and "port" in mod_opts:
31        return __virtualname__
32
33    return False
34
35
36def get_application(opts):
37    try:
38        from . import saltnado
39    except ImportError as err:
40        log.error("ImportError! %s", err)
41        return None
42
43    mod_opts = opts.get(__virtualname__, {})
44
45    paths = [
46        (r"/", saltnado.SaltAPIHandler),
47        (r"/login", saltnado.SaltAuthHandler),
48        (r"/minions/(.*)", saltnado.MinionSaltAPIHandler),
49        (r"/minions", saltnado.MinionSaltAPIHandler),
50        (r"/jobs/(.*)", saltnado.JobsSaltAPIHandler),
51        (r"/jobs", saltnado.JobsSaltAPIHandler),
52        (r"/run", saltnado.RunSaltAPIHandler),
53        (r"/events", saltnado.EventsSaltAPIHandler),
54        (r"/hook(/.*)?", saltnado.WebhookSaltAPIHandler),
55    ]
56
57    # if you have enabled websockets, add them!
58    if mod_opts.get("websockets", False):
59        from . import saltnado_websockets
60
61        token_pattern = r"([0-9A-Fa-f]{{{0}}})".format(
62            len(getattr(hashlib, opts.get("hash_type", "md5"))().hexdigest())
63        )
64        all_events_pattern = r"/all_events/{}".format(token_pattern)
65        formatted_events_pattern = r"/formatted_events/{}".format(token_pattern)
66        log.debug("All events URL pattern is %s", all_events_pattern)
67        paths += [
68            # Matches /all_events/[0-9A-Fa-f]{n}
69            # Where n is the length of hexdigest
70            # for the current hashing algorithm.
71            # This algorithm is specified in the
72            # salt master config file.
73            (all_events_pattern, saltnado_websockets.AllEventsHandler),
74            (formatted_events_pattern, saltnado_websockets.FormattedEventsHandler),
75        ]
76
77    application = salt.ext.tornado.web.Application(
78        paths, debug=mod_opts.get("debug", False)
79    )
80
81    application.opts = opts
82    application.mod_opts = mod_opts
83    application.auth = salt.auth.LoadAuth(opts)
84    return application
85
86
87def start():
88    """
89    Start the saltnado!
90    """
91    mod_opts = __opts__.get(__virtualname__, {})
92
93    if "num_processes" not in mod_opts:
94        mod_opts["num_processes"] = 1
95
96    if mod_opts["num_processes"] > 1 and mod_opts.get("debug", False) is True:
97        raise Exception(
98            "Tornado's debug implementation is not compatible with multiprocess. "
99            "Either disable debug, or set num_processes to 1."
100        )
101
102    # the kwargs for the HTTPServer
103    kwargs = {}
104    if not mod_opts.get("disable_ssl", False):
105        if "ssl_crt" not in mod_opts:
106            log.error(
107                "Not starting '%s'. Options 'ssl_crt' and "
108                "'ssl_key' are required if SSL is not disabled.",
109                __name__,
110            )
111
112            return None
113        # cert is required, key may be optional
114        # https://docs.python.org/2/library/ssl.html#ssl.wrap_socket
115        ssl_opts = {"certfile": mod_opts["ssl_crt"]}
116        if mod_opts.get("ssl_key", False):
117            ssl_opts.update({"keyfile": mod_opts["ssl_key"]})
118        kwargs["ssl_options"] = ssl_opts
119
120    import salt.ext.tornado.httpserver
121
122    http_server = salt.ext.tornado.httpserver.HTTPServer(
123        get_application(__opts__), **kwargs
124    )
125    try:
126        http_server.bind(
127            mod_opts["port"],
128            address=mod_opts.get("address"),
129            backlog=mod_opts.get("backlog", 128),
130        )
131        http_server.start(mod_opts["num_processes"])
132    except Exception:  # pylint: disable=broad-except
133        log.error(
134            "Rest_tornado unable to bind to port %s", mod_opts["port"], exc_info=True
135        )
136        raise SystemExit(1)
137
138    try:
139        salt.ext.tornado.ioloop.IOLoop.current().start()
140    except KeyboardInterrupt:
141        raise SystemExit(0)
142