1"""
2Interaction with the Supervisor daemon
3======================================
4
5.. code-block:: yaml
6
7    wsgi_server:
8      supervisord.running:
9        - require:
10          - pkg: supervisor
11        - watch:
12          - file: /etc/nginx/sites-enabled/wsgi_server.conf
13"""
14
15import logging
16
17log = logging.getLogger(__name__)
18
19
20def _check_error(result, success_message):
21    ret = {}
22
23    if "ERROR" in result:
24        if any(
25            substring in result
26            for substring in [
27                "already started",
28                "not running",
29                "process group already active",
30            ]
31        ):
32            ret["comment"] = success_message
33        else:
34            ret["comment"] = result
35            ret["result"] = False
36    else:
37        ret["comment"] = success_message
38
39    return ret
40
41
42def _is_stopped_state(state):
43    if state in ("STOPPED", "STOPPING", "EXITED", "FATAL", "BACKOFF"):
44        return True
45    if state in ("STARTING", "RUNNING"):
46        return False
47    return False
48
49
50def running(
51    name, restart=False, update=False, user=None, conf_file=None, bin_env=None, **kwargs
52):
53    """
54    Ensure the named service is running.
55
56    name
57        Service name as defined in the supervisor configuration file
58
59    restart
60        Whether to force a restart
61
62    update
63        Whether to update the supervisor configuration.
64
65    user
66        Name of the user to run the supervisorctl command
67
68        .. versionadded:: 0.17.0
69
70    conf_file
71        path to supervisorctl config file
72
73    bin_env
74        path to supervisorctl bin or path to virtualenv with supervisor
75        installed
76
77    """
78    if name.endswith(":*"):
79        name = name[:-1]
80
81    ret = {"name": name, "result": True, "comment": "", "changes": {}}
82
83    if "supervisord.status" not in __salt__:
84        ret["result"] = False
85        ret[
86            "comment"
87        ] = "Supervisord module not activated. Do you need to install supervisord?"
88        return ret
89
90    all_processes = __salt__["supervisord.status"](
91        user=user, conf_file=conf_file, bin_env=bin_env
92    )
93
94    # parse process groups
95    process_groups = set()
96    for proc in all_processes:
97        if ":" in proc:
98            process_groups.add(proc[: proc.index(":") + 1])
99    process_groups = sorted(process_groups)
100
101    matches = {}
102    if name in all_processes:
103        matches[name] = all_processes[name]["state"].lower() == "running"
104    elif name in process_groups:
105        for process in (x for x in all_processes if x.startswith(name)):
106            matches[process] = all_processes[process]["state"].lower() == "running"
107    to_add = not bool(matches)
108
109    if __opts__["test"]:
110        if not to_add:
111            # Process/group already present, check if any need to be started
112            to_start = [x for x, y in matches.items() if y is False]
113            if to_start:
114                ret["result"] = None
115                if name.endswith(":"):
116                    # Process group
117                    if len(to_start) == len(matches):
118                        ret[
119                            "comment"
120                        ] = "All services in group '{}' will be started".format(name)
121                    else:
122                        ret[
123                            "comment"
124                        ] = "The following services will be started: {}".format(
125                            " ".join(to_start)
126                        )
127                else:
128                    # Single program
129                    ret["comment"] = "Service {} will be started".format(name)
130            else:
131                if name.endswith(":"):
132                    # Process group
133                    ret[
134                        "comment"
135                    ] = "All services in group '{}' are already running".format(name)
136                else:
137                    ret["comment"] = "Service {} is already running".format(name)
138        else:
139            ret["result"] = None
140            # Process/group needs to be added
141            if name.endswith(":"):
142                _type = "Group '{}'".format(name)
143            else:
144                _type = "Service {}".format(name)
145            ret["comment"] = "{} will be added and started".format(_type)
146        return ret
147
148    changes = []
149    just_updated = False
150
151    if update:
152        # If the state explicitly asks to update, we don't care if the process
153        # is being added or not, since it'll take care of this for us,
154        # so give this condition priority in order
155        #
156        # That is, unless `to_add` somehow manages to contain processes
157        # we don't want running, in which case adding them may be a mistake
158        comment = "Updating supervisor"
159        result = __salt__["supervisord.update"](
160            user=user, conf_file=conf_file, bin_env=bin_env
161        )
162        ret.update(_check_error(result, comment))
163        log.debug(comment)
164
165        if "{}: updated".format(name) in result:
166            just_updated = True
167    elif to_add:
168        # Not sure if this condition is precise enough.
169        comment = "Adding service: {}".format(name)
170        __salt__["supervisord.reread"](user=user, conf_file=conf_file, bin_env=bin_env)
171        # Causes supervisorctl to throw `ERROR: process group already active`
172        # if process group exists. At this moment, I'm not sure how to handle
173        # this outside of grepping out the expected string in `_check_error`.
174        result = __salt__["supervisord.add"](
175            name, user=user, conf_file=conf_file, bin_env=bin_env
176        )
177
178        ret.update(_check_error(result, comment))
179        changes.append(comment)
180        log.debug(comment)
181
182    is_stopped = None
183
184    process_type = None
185    if name in process_groups:
186        process_type = "group"
187
188        # check if any processes in this group are stopped
189        is_stopped = False
190        for proc in all_processes:
191            if proc.startswith(name) and _is_stopped_state(
192                all_processes[proc]["state"]
193            ):
194                is_stopped = True
195                break
196
197    elif name in all_processes:
198        process_type = "service"
199
200        if _is_stopped_state(all_processes[name]["state"]):
201            is_stopped = True
202        else:
203            is_stopped = False
204
205    if is_stopped is False:
206        if restart and not just_updated:
207            comment = "Restarting{}: {}".format(
208                process_type is not None and " {}".format(process_type) or "", name
209            )
210            log.debug(comment)
211            result = __salt__["supervisord.restart"](
212                name, user=user, conf_file=conf_file, bin_env=bin_env
213            )
214            ret.update(_check_error(result, comment))
215            changes.append(comment)
216        elif just_updated:
217            comment = "Not starting updated{}: {}".format(
218                process_type is not None and " {}".format(process_type) or "", name
219            )
220            result = comment
221            ret.update({"comment": comment})
222        else:
223            comment = "Not starting already running{}: {}".format(
224                process_type is not None and " {}".format(process_type) or "", name
225            )
226            result = comment
227            ret.update({"comment": comment})
228
229    elif not just_updated:
230        comment = "Starting{}: {}".format(
231            process_type is not None and " {}".format(process_type) or "", name
232        )
233        changes.append(comment)
234        log.debug(comment)
235        result = __salt__["supervisord.start"](
236            name, user=user, conf_file=conf_file, bin_env=bin_env
237        )
238
239        ret.update(_check_error(result, comment))
240        log.debug(str(result))
241
242    if ret["result"] and changes:
243        ret["changes"][name] = " ".join(changes)
244    return ret
245
246
247def dead(name, user=None, conf_file=None, bin_env=None, **kwargs):
248    """
249    Ensure the named service is dead (not running).
250
251    name
252        Service name as defined in the supervisor configuration file
253
254    user
255        Name of the user to run the supervisorctl command
256
257        .. versionadded:: 0.17.0
258
259    conf_file
260        path to supervisorctl config file
261
262    bin_env
263        path to supervisorctl bin or path to virtualenv with supervisor
264        installed
265
266    """
267    ret = {"name": name, "result": True, "comment": "", "changes": {}}
268
269    if __opts__["test"]:
270        ret["result"] = None
271        ret["comment"] = "Service {} is set to be stopped".format(name)
272    else:
273        comment = "Stopping service: {}".format(name)
274        log.debug(comment)
275
276        all_processes = __salt__["supervisord.status"](
277            user=user, conf_file=conf_file, bin_env=bin_env
278        )
279
280        # parse process groups
281        process_groups = []
282        for proc in all_processes:
283            if ":" in proc:
284                process_groups.append(proc[: proc.index(":") + 1])
285        process_groups = list(set(process_groups))
286
287        is_stopped = None
288
289        if name in process_groups:
290            # check if any processes in this group are stopped
291            is_stopped = False
292            for proc in all_processes:
293                if proc.startswith(name) and _is_stopped_state(
294                    all_processes[proc]["state"]
295                ):
296                    is_stopped = True
297                    break
298
299        elif name in all_processes:
300            if _is_stopped_state(all_processes[name]["state"]):
301                is_stopped = True
302            else:
303                is_stopped = False
304        else:
305            # process name doesn't exist
306            ret["comment"] = "Service {} doesn't exist".format(name)
307            return ret
308
309        if is_stopped is True:
310            ret["comment"] = "Service {} is not running".format(name)
311        else:
312            result = {
313                name: __salt__["supervisord.stop"](
314                    name, user=user, conf_file=conf_file, bin_env=bin_env
315                )
316            }
317            ret.update(_check_error(result, comment))
318            ret["changes"][name] = comment
319            log.debug(str(result))
320    return ret
321
322
323def mod_watch(
324    name, restart=True, update=False, user=None, conf_file=None, bin_env=None, **kwargs
325):
326    """
327    The supervisord watcher, called to invoke the watch command.
328    Always restart on watch
329
330    .. note::
331        This state exists to support special handling of the ``watch``
332        :ref:`requisite <requisites>`. It should not be called directly.
333
334        Parameters for this function should be set by the state being triggered.
335    """
336    return running(
337        name,
338        restart=restart,
339        update=update,
340        user=user,
341        conf_file=conf_file,
342        bin_env=bin_env,
343    )
344