comparison libervia/server/tasks.py @ 1146:76d75423ef53

server: tasks manager first draft: A new task manager will check /tasks directory of website to scripts to execute before launching the site. This allows to generate docs, scripts, or do anything else useful. Generated files are put in in sat local dir, in cache, and are accessible from the website using the new "build_dir" variable.
author Goffi <goffi@goffi.org>
date Fri, 25 Jan 2019 08:58:41 +0100
parents
children 02afab1b15c5
comparison
equal deleted inserted replaced
1145:29eb15062416 1146:76d75423ef53
1 #!/usr/bin/python
2 # -*- coding: utf-8 -*-
3
4 # Libervia: a Salut à Toi frontend
5 # Copyright (C) 2011-2019 Jérôme Poisson <goffi@goffi.org>
6
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU Affero General Public License as published by
9 # the Free Software Foundation, either version 3 of the License, or
10 # (at your option) any later version.
11
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU Affero General Public License for more details.
16
17 # You should have received a copy of the GNU Affero General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
19 import os
20 import os.path
21 from twisted.internet import defer
22 from twisted.python.procutils import which
23 from sat.core import exceptions
24 from sat.core.i18n import _
25 from libervia.server.constants import Const as C
26 from collections import OrderedDict
27 from sat.core.log import getLogger
28 from sat.tools import config
29 from sat.tools.common import async_process
30
31 log = getLogger(__name__)
32
33
34 class TasksManager(object):
35 """Handle tasks of a Libervia site"""
36 FILE_EXTS = {u'py'}
37
38 def __init__(self, host, site_resource):
39 """
40 @param site_resource(LiberviaRootResource): root resource of the site to manage
41 """
42 self.host = host
43 self.resource = site_resource
44 self.tasks_dir = os.path.join(self.resource.site_path, C.TASKS_DIR)
45 self.tasks = OrderedDict()
46 self.parseTasks()
47 self._build_path = None
48 self._current_task = None
49
50 @property
51 def site_path(self):
52 return self.resource.site_path
53
54 @property
55 def config_section(self):
56 return self.resource.site_name.lower().strip()
57
58 @property
59 def build_path(self):
60 """path where generated files will be build for this site"""
61 if self._build_path is None:
62 self._build_path = self.host.getBuildPath(self.site_name)
63 return self._build_path
64
65 def getConfig(self, key, default=None, value_type=None):
66 """Retrieve configuration associated to this site
67
68 Section is automatically set to site name
69 @param key(unicode): key to use
70 @param default: value to use if not found (see [config.getConfig])
71 @param value_type(unicode, None): filter to use on value
72 Note that filters are already automatically used when the key finish
73 by a well known suffix ("_path", "_list", "_dict", or "_json")
74 None to use no filter, else can be:
75 - "path": a path is expected, will be normalized and expanded
76
77 """
78 value = config.getConfig(self.host.main_conf, self.config_section, key,
79 default=default)
80 if value_type is not None:
81 if value_type == u'path':
82 v_filter = lambda v: os.path.abspath(os.path.expanduser(v))
83 else:
84 raise ValueError(u"unknown value type {value_type}".format(
85 value_type = value_type))
86 if isinstance(value, list):
87 value = [v_filter(v) for v in value]
88 elif isinstance(value, dict):
89 value = {k:v_filter(v) for k,v in value.items()}
90 elif value is not None:
91 value = v_filter(v)
92 return value
93
94 @property
95 def site_name(self):
96 return self.resource.site_name
97
98 @property
99 def task_data(self):
100 return self.tasks[self._current_task][u'data']
101
102 def validateData(self, data):
103 """Check values in data"""
104
105 for var, default, allowed in ((u"ON_ERROR", u"stop", (u"continue", u"stop")),
106 (u"LOG_OUTPUT", True, bool)):
107 value = data.setdefault(var, default)
108 if isinstance(allowed, type):
109 if not isinstance(value, allowed):
110 raise ValueError(
111 _(u"Unexpected value for {var}, {allowed} is expected.")
112 .format(var = var, allowed = allowed))
113 else:
114 if not value in allowed:
115 raise ValueError(_(u"Unexpected value for {var}: {value}").format(
116 var = var, value = value))
117
118 for var, default, allowed in [[u"ON_ERROR", u"stop", (u"continue", u"stop")]]:
119 value = data.setdefault(var, default)
120 if not value in allowed:
121 raise ValueError(_(u"Unexpected value for {var}: {value}").format(
122 var = var, value = value))
123
124 def parseTasks(self):
125 if not os.path.isdir(self.tasks_dir):
126 log.debug(_(u"{name} has no task to launch.").format(
127 name = self.resource.site_name or u"default site"))
128 return
129 filenames = os.listdir(self.tasks_dir)
130 filenames.sort()
131 for filename in filenames:
132 filepath = os.path.join(self.tasks_dir, filename)
133 if not filename.startswith(u'task_') or not os.path.isfile(filepath):
134 continue
135 task_name, ext = os.path.splitext(filename)
136 task_name = task_name[5:].lower().strip()
137 if not task_name:
138 continue
139 if ext[1:] not in self.FILE_EXTS:
140 continue
141 if task_name in self.tasks:
142 raise exceptions.ConflictError(
143 u"A task with the name [{name}] already exists".format(
144 name=task_name))
145 task_data = {u"__name__": "{site_name}.task.{name}".format(
146 site_name=self.site_name, name=task_name)}
147 self.tasks[task_name] = {
148 u'path': filepath,
149 u'data': task_data,
150 }
151 execfile(filepath, task_data)
152 self.validateData(task_data)
153
154 @defer.inlineCallbacks
155 def runTasks(self):
156 """Run all the tasks found"""
157 old_path = os.getcwd()
158 for task_name, task_value in self.tasks.iteritems():
159 self._current_task = task_name
160 log.info(_(u'== running task "{task_name}" for {site_name} =='.format(
161 task_name=task_name, site_name=self.site_name)))
162 data = task_value[u'data']
163 os.chdir(self.site_path)
164 try:
165 yield data['start'](self)
166 except Exception as e:
167 on_error = data[u'ON_ERROR']
168 if on_error == u'stop':
169 raise e
170 elif on_error == u'continue':
171 log.warning(_(u'Task "{task_name}" failed for {site_name}: {reason}')
172 .format(task_name = task_name, site_name = self.site_name, reason = e))
173 else:
174 raise exceptions.InternalError(u"we should never reach this point")
175 self._current_task = None
176 os.chdir(old_path)
177
178 def findCommand(self, name, *args):
179 """Find full path of a shell command
180
181 @param name(unicode): name of the command to find
182 @param *args(unicode): extra names the command may have
183 @return (unicode): full path of the command
184 @raise exceptions.NotFound: can't find this command
185 """
186 names = (name,) + args
187 for n in names:
188 try:
189 cmd_path = which(n)[0].encode('utf-8')
190 except IndexError:
191 pass
192 return cmd_path
193 raise exceptions.NotFound(_(
194 u"Can't find {name} command, did you install it?").format(name=name))
195
196 def runCommand(self, command, *args, **kwargs):
197 kwargs['verbose'] = self.task_data[u"LOG_OUTPUT"]
198 return async_process.CommandProtocol.run(command, *args, **kwargs)