1146
|
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) |