##// END OF EJS Templates
http: added service backend for testing communication, and extracting vcsserver version...
marcink -
r102:da6339df default
parent child Browse files
Show More
@@ -1,359 +1,376 b''
1 # RhodeCode VCSServer provides access to different vcs backends via network.
1 # RhodeCode VCSServer provides access to different vcs backends via network.
2 # Copyright (C) 2014-2016 RodeCode GmbH
2 # Copyright (C) 2014-2016 RodeCode GmbH
3 #
3 #
4 # This program is free software; you can redistribute it and/or modify
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
7 # (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
17
18 import base64
18 import base64
19 import locale
19 import locale
20 import logging
20 import logging
21 import uuid
21 import uuid
22 import wsgiref.util
22 import wsgiref.util
23 from itertools import chain
23 from itertools import chain
24
24
25 import msgpack
25 import msgpack
26 from beaker.cache import CacheManager
26 from beaker.cache import CacheManager
27 from beaker.util import parse_cache_config_options
27 from beaker.util import parse_cache_config_options
28 from pyramid.config import Configurator
28 from pyramid.config import Configurator
29 from pyramid.wsgi import wsgiapp
29 from pyramid.wsgi import wsgiapp
30
30
31 from vcsserver import remote_wsgi, scm_app, settings, hgpatches
31 from vcsserver import remote_wsgi, scm_app, settings, hgpatches
32 from vcsserver.echo_stub import remote_wsgi as remote_wsgi_stub
32 from vcsserver.echo_stub import remote_wsgi as remote_wsgi_stub
33 from vcsserver.echo_stub.echo_app import EchoApp
33 from vcsserver.echo_stub.echo_app import EchoApp
34 from vcsserver.exceptions import HTTPRepoLocked
34 from vcsserver.exceptions import HTTPRepoLocked
35 from vcsserver.server import VcsServer
35 from vcsserver.server import VcsServer
36
36
37 try:
37 try:
38 from vcsserver.git import GitFactory, GitRemote
38 from vcsserver.git import GitFactory, GitRemote
39 except ImportError:
39 except ImportError:
40 GitFactory = None
40 GitFactory = None
41 GitRemote = None
41 GitRemote = None
42 try:
42 try:
43 from vcsserver.hg import MercurialFactory, HgRemote
43 from vcsserver.hg import MercurialFactory, HgRemote
44 except ImportError:
44 except ImportError:
45 MercurialFactory = None
45 MercurialFactory = None
46 HgRemote = None
46 HgRemote = None
47 try:
47 try:
48 from vcsserver.svn import SubversionFactory, SvnRemote
48 from vcsserver.svn import SubversionFactory, SvnRemote
49 except ImportError:
49 except ImportError:
50 SubversionFactory = None
50 SubversionFactory = None
51 SvnRemote = None
51 SvnRemote = None
52
52
53 log = logging.getLogger(__name__)
53 log = logging.getLogger(__name__)
54
54
55
55
56 class VCS(object):
56 class VCS(object):
57 def __init__(self, locale=None, cache_config=None):
57 def __init__(self, locale=None, cache_config=None):
58 self.locale = locale
58 self.locale = locale
59 self.cache_config = cache_config
59 self.cache_config = cache_config
60 self._configure_locale()
60 self._configure_locale()
61 self._initialize_cache()
61 self._initialize_cache()
62
62
63 if GitFactory and GitRemote:
63 if GitFactory and GitRemote:
64 git_repo_cache = self.cache.get_cache_region(
64 git_repo_cache = self.cache.get_cache_region(
65 'git', region='repo_object')
65 'git', region='repo_object')
66 git_factory = GitFactory(git_repo_cache)
66 git_factory = GitFactory(git_repo_cache)
67 self._git_remote = GitRemote(git_factory)
67 self._git_remote = GitRemote(git_factory)
68 else:
68 else:
69 log.info("Git client import failed")
69 log.info("Git client import failed")
70
70
71 if MercurialFactory and HgRemote:
71 if MercurialFactory and HgRemote:
72 hg_repo_cache = self.cache.get_cache_region(
72 hg_repo_cache = self.cache.get_cache_region(
73 'hg', region='repo_object')
73 'hg', region='repo_object')
74 hg_factory = MercurialFactory(hg_repo_cache)
74 hg_factory = MercurialFactory(hg_repo_cache)
75 self._hg_remote = HgRemote(hg_factory)
75 self._hg_remote = HgRemote(hg_factory)
76 else:
76 else:
77 log.info("Mercurial client import failed")
77 log.info("Mercurial client import failed")
78
78
79 if SubversionFactory and SvnRemote:
79 if SubversionFactory and SvnRemote:
80 svn_repo_cache = self.cache.get_cache_region(
80 svn_repo_cache = self.cache.get_cache_region(
81 'svn', region='repo_object')
81 'svn', region='repo_object')
82 svn_factory = SubversionFactory(svn_repo_cache)
82 svn_factory = SubversionFactory(svn_repo_cache)
83 self._svn_remote = SvnRemote(svn_factory, hg_factory=hg_factory)
83 self._svn_remote = SvnRemote(svn_factory, hg_factory=hg_factory)
84 else:
84 else:
85 log.info("Subversion client import failed")
85 log.info("Subversion client import failed")
86
86
87 self._vcsserver = VcsServer()
87 self._vcsserver = VcsServer()
88
88
89 def _initialize_cache(self):
89 def _initialize_cache(self):
90 cache_config = parse_cache_config_options(self.cache_config)
90 cache_config = parse_cache_config_options(self.cache_config)
91 log.info('Initializing beaker cache: %s' % cache_config)
91 log.info('Initializing beaker cache: %s' % cache_config)
92 self.cache = CacheManager(**cache_config)
92 self.cache = CacheManager(**cache_config)
93
93
94 def _configure_locale(self):
94 def _configure_locale(self):
95 if self.locale:
95 if self.locale:
96 log.info('Settings locale: `LC_ALL` to %s' % self.locale)
96 log.info('Settings locale: `LC_ALL` to %s' % self.locale)
97 else:
97 else:
98 log.info(
98 log.info(
99 'Configuring locale subsystem based on environment variables')
99 'Configuring locale subsystem based on environment variables')
100 try:
100 try:
101 # If self.locale is the empty string, then the locale
101 # If self.locale is the empty string, then the locale
102 # module will use the environment variables. See the
102 # module will use the environment variables. See the
103 # documentation of the package `locale`.
103 # documentation of the package `locale`.
104 locale.setlocale(locale.LC_ALL, self.locale)
104 locale.setlocale(locale.LC_ALL, self.locale)
105
105
106 language_code, encoding = locale.getlocale()
106 language_code, encoding = locale.getlocale()
107 log.info(
107 log.info(
108 'Locale set to language code "%s" with encoding "%s".',
108 'Locale set to language code "%s" with encoding "%s".',
109 language_code, encoding)
109 language_code, encoding)
110 except locale.Error:
110 except locale.Error:
111 log.exception(
111 log.exception(
112 'Cannot set locale, not configuring the locale system')
112 'Cannot set locale, not configuring the locale system')
113
113
114
114
115 class WsgiProxy(object):
115 class WsgiProxy(object):
116 def __init__(self, wsgi):
116 def __init__(self, wsgi):
117 self.wsgi = wsgi
117 self.wsgi = wsgi
118
118
119 def __call__(self, environ, start_response):
119 def __call__(self, environ, start_response):
120 input_data = environ['wsgi.input'].read()
120 input_data = environ['wsgi.input'].read()
121 input_data = msgpack.unpackb(input_data)
121 input_data = msgpack.unpackb(input_data)
122
122
123 error = None
123 error = None
124 try:
124 try:
125 data, status, headers = self.wsgi.handle(
125 data, status, headers = self.wsgi.handle(
126 input_data['environment'], input_data['input_data'],
126 input_data['environment'], input_data['input_data'],
127 *input_data['args'], **input_data['kwargs'])
127 *input_data['args'], **input_data['kwargs'])
128 except Exception as e:
128 except Exception as e:
129 data, status, headers = [], None, None
129 data, status, headers = [], None, None
130 error = {
130 error = {
131 'message': str(e),
131 'message': str(e),
132 '_vcs_kind': getattr(e, '_vcs_kind', None)
132 '_vcs_kind': getattr(e, '_vcs_kind', None)
133 }
133 }
134
134
135 start_response(200, {})
135 start_response(200, {})
136 return self._iterator(error, status, headers, data)
136 return self._iterator(error, status, headers, data)
137
137
138 def _iterator(self, error, status, headers, data):
138 def _iterator(self, error, status, headers, data):
139 initial_data = [
139 initial_data = [
140 error,
140 error,
141 status,
141 status,
142 headers,
142 headers,
143 ]
143 ]
144
144
145 for d in chain(initial_data, data):
145 for d in chain(initial_data, data):
146 yield msgpack.packb(d)
146 yield msgpack.packb(d)
147
147
148
148
149 class HTTPApplication(object):
149 class HTTPApplication(object):
150 ALLOWED_EXCEPTIONS = ('KeyError', 'URLError')
150 ALLOWED_EXCEPTIONS = ('KeyError', 'URLError')
151
151
152 remote_wsgi = remote_wsgi
152 remote_wsgi = remote_wsgi
153 _use_echo_app = False
153 _use_echo_app = False
154
154
155 def __init__(self, settings=None):
155 def __init__(self, settings=None):
156 self.config = Configurator(settings=settings)
156 self.config = Configurator(settings=settings)
157 locale = settings.get('', 'en_US.UTF-8')
157 locale = settings.get('', 'en_US.UTF-8')
158 vcs = VCS(locale=locale, cache_config=settings)
158 vcs = VCS(locale=locale, cache_config=settings)
159 self._remotes = {
159 self._remotes = {
160 'hg': vcs._hg_remote,
160 'hg': vcs._hg_remote,
161 'git': vcs._git_remote,
161 'git': vcs._git_remote,
162 'svn': vcs._svn_remote,
162 'svn': vcs._svn_remote,
163 'server': vcs._vcsserver,
163 'server': vcs._vcsserver,
164 }
164 }
165 if settings.get('dev.use_echo_app', 'false').lower() == 'true':
165 if settings.get('dev.use_echo_app', 'false').lower() == 'true':
166 self._use_echo_app = True
166 self._use_echo_app = True
167 log.warning("Using EchoApp for VCS operations.")
167 log.warning("Using EchoApp for VCS operations.")
168 self.remote_wsgi = remote_wsgi_stub
168 self.remote_wsgi = remote_wsgi_stub
169 self._configure_settings(settings)
169 self._configure_settings(settings)
170 self._configure()
170 self._configure()
171
171
172 def _configure_settings(self, app_settings):
172 def _configure_settings(self, app_settings):
173 """
173 """
174 Configure the settings module.
174 Configure the settings module.
175 """
175 """
176 git_path = app_settings.get('git_path', None)
176 git_path = app_settings.get('git_path', None)
177 if git_path:
177 if git_path:
178 settings.GIT_EXECUTABLE = git_path
178 settings.GIT_EXECUTABLE = git_path
179
179
180 def _configure(self):
180 def _configure(self):
181 self.config.add_renderer(
181 self.config.add_renderer(
182 name='msgpack',
182 name='msgpack',
183 factory=self._msgpack_renderer_factory)
183 factory=self._msgpack_renderer_factory)
184
184
185 self.config.add_route('service', '/_service')
185 self.config.add_route('status', '/status')
186 self.config.add_route('status', '/status')
186 self.config.add_route('hg_proxy', '/proxy/hg')
187 self.config.add_route('hg_proxy', '/proxy/hg')
187 self.config.add_route('git_proxy', '/proxy/git')
188 self.config.add_route('git_proxy', '/proxy/git')
188 self.config.add_route('vcs', '/{backend}')
189 self.config.add_route('vcs', '/{backend}')
189 self.config.add_route('stream_git', '/stream/git/*repo_name')
190 self.config.add_route('stream_git', '/stream/git/*repo_name')
190 self.config.add_route('stream_hg', '/stream/hg/*repo_name')
191 self.config.add_route('stream_hg', '/stream/hg/*repo_name')
191
192
192 self.config.add_view(
193 self.config.add_view(
193 self.status_view, route_name='status', renderer='json')
194 self.status_view, route_name='status', renderer='json')
195 self.config.add_view(
196 self.service_view, route_name='service', renderer='msgpack')
197
194 self.config.add_view(self.hg_proxy(), route_name='hg_proxy')
198 self.config.add_view(self.hg_proxy(), route_name='hg_proxy')
195 self.config.add_view(self.git_proxy(), route_name='git_proxy')
199 self.config.add_view(self.git_proxy(), route_name='git_proxy')
196 self.config.add_view(
200 self.config.add_view(
197 self.vcs_view, route_name='vcs', renderer='msgpack')
201 self.vcs_view, route_name='vcs', renderer='msgpack')
198
202
199 self.config.add_view(self.hg_stream(), route_name='stream_hg')
203 self.config.add_view(self.hg_stream(), route_name='stream_hg')
200 self.config.add_view(self.git_stream(), route_name='stream_git')
204 self.config.add_view(self.git_stream(), route_name='stream_git')
201 self.config.add_view(
205 self.config.add_view(
202 self.handle_vcs_exception, context=Exception,
206 self.handle_vcs_exception, context=Exception,
203 custom_predicates=[self.is_vcs_exception])
207 custom_predicates=[self.is_vcs_exception])
204
208
205 def wsgi_app(self):
209 def wsgi_app(self):
206 return self.config.make_wsgi_app()
210 return self.config.make_wsgi_app()
207
211
208 def vcs_view(self, request):
212 def vcs_view(self, request):
209 remote = self._remotes[request.matchdict['backend']]
213 remote = self._remotes[request.matchdict['backend']]
210 payload = msgpack.unpackb(request.body, use_list=True)
214 payload = msgpack.unpackb(request.body, use_list=True)
211 method = payload.get('method')
215 method = payload.get('method')
212 params = payload.get('params')
216 params = payload.get('params')
213 wire = params.get('wire')
217 wire = params.get('wire')
214 args = params.get('args')
218 args = params.get('args')
215 kwargs = params.get('kwargs')
219 kwargs = params.get('kwargs')
216 if wire:
220 if wire:
217 try:
221 try:
218 wire['context'] = uuid.UUID(wire['context'])
222 wire['context'] = uuid.UUID(wire['context'])
219 except KeyError:
223 except KeyError:
220 pass
224 pass
221 args.insert(0, wire)
225 args.insert(0, wire)
222
226
223 try:
227 try:
224 resp = getattr(remote, method)(*args, **kwargs)
228 resp = getattr(remote, method)(*args, **kwargs)
225 except Exception as e:
229 except Exception as e:
226 type_ = e.__class__.__name__
230 type_ = e.__class__.__name__
227 if type_ not in self.ALLOWED_EXCEPTIONS:
231 if type_ not in self.ALLOWED_EXCEPTIONS:
228 type_ = None
232 type_ = None
229
233
230 resp = {
234 resp = {
231 'id': payload.get('id'),
235 'id': payload.get('id'),
232 'error': {
236 'error': {
233 'message': e.message,
237 'message': e.message,
234 'type': type_
238 'type': type_
235 }
239 }
236 }
240 }
237 try:
241 try:
238 resp['error']['_vcs_kind'] = e._vcs_kind
242 resp['error']['_vcs_kind'] = e._vcs_kind
239 except AttributeError:
243 except AttributeError:
240 pass
244 pass
241 else:
245 else:
242 resp = {
246 resp = {
243 'id': payload.get('id'),
247 'id': payload.get('id'),
244 'result': resp
248 'result': resp
245 }
249 }
246
250
247 return resp
251 return resp
248
252
249 def status_view(self, request):
253 def status_view(self, request):
250 return {'status': 'OK'}
254 return {'status': 'OK'}
251
255
256 def service_view(self, request):
257 import vcsserver
258 payload = msgpack.unpackb(request.body, use_list=True)
259 resp = {
260 'id': payload.get('id'),
261 'result': dict(
262 version=vcsserver.__version__,
263 config={},
264 payload=payload,
265 )
266 }
267 return resp
268
252 def _msgpack_renderer_factory(self, info):
269 def _msgpack_renderer_factory(self, info):
253 def _render(value, system):
270 def _render(value, system):
254 value = msgpack.packb(value)
271 value = msgpack.packb(value)
255 request = system.get('request')
272 request = system.get('request')
256 if request is not None:
273 if request is not None:
257 response = request.response
274 response = request.response
258 ct = response.content_type
275 ct = response.content_type
259 if ct == response.default_content_type:
276 if ct == response.default_content_type:
260 response.content_type = 'application/x-msgpack'
277 response.content_type = 'application/x-msgpack'
261 return value
278 return value
262 return _render
279 return _render
263
280
264 def hg_proxy(self):
281 def hg_proxy(self):
265 @wsgiapp
282 @wsgiapp
266 def _hg_proxy(environ, start_response):
283 def _hg_proxy(environ, start_response):
267 app = WsgiProxy(self.remote_wsgi.HgRemoteWsgi())
284 app = WsgiProxy(self.remote_wsgi.HgRemoteWsgi())
268 return app(environ, start_response)
285 return app(environ, start_response)
269 return _hg_proxy
286 return _hg_proxy
270
287
271 def git_proxy(self):
288 def git_proxy(self):
272 @wsgiapp
289 @wsgiapp
273 def _git_proxy(environ, start_response):
290 def _git_proxy(environ, start_response):
274 app = WsgiProxy(self.remote_wsgi.GitRemoteWsgi())
291 app = WsgiProxy(self.remote_wsgi.GitRemoteWsgi())
275 return app(environ, start_response)
292 return app(environ, start_response)
276 return _git_proxy
293 return _git_proxy
277
294
278 def hg_stream(self):
295 def hg_stream(self):
279 if self._use_echo_app:
296 if self._use_echo_app:
280 @wsgiapp
297 @wsgiapp
281 def _hg_stream(environ, start_response):
298 def _hg_stream(environ, start_response):
282 app = EchoApp('fake_path', 'fake_name', None)
299 app = EchoApp('fake_path', 'fake_name', None)
283 return app(environ, start_response)
300 return app(environ, start_response)
284 return _hg_stream
301 return _hg_stream
285 else:
302 else:
286 @wsgiapp
303 @wsgiapp
287 def _hg_stream(environ, start_response):
304 def _hg_stream(environ, start_response):
288 repo_path = environ['HTTP_X_RC_REPO_PATH']
305 repo_path = environ['HTTP_X_RC_REPO_PATH']
289 repo_name = environ['HTTP_X_RC_REPO_NAME']
306 repo_name = environ['HTTP_X_RC_REPO_NAME']
290 packed_config = base64.b64decode(
307 packed_config = base64.b64decode(
291 environ['HTTP_X_RC_REPO_CONFIG'])
308 environ['HTTP_X_RC_REPO_CONFIG'])
292 config = msgpack.unpackb(packed_config)
309 config = msgpack.unpackb(packed_config)
293 app = scm_app.create_hg_wsgi_app(
310 app = scm_app.create_hg_wsgi_app(
294 repo_path, repo_name, config)
311 repo_path, repo_name, config)
295
312
296 # Consitent path information for hgweb
313 # Consitent path information for hgweb
297 environ['PATH_INFO'] = environ['HTTP_X_RC_PATH_INFO']
314 environ['PATH_INFO'] = environ['HTTP_X_RC_PATH_INFO']
298 environ['REPO_NAME'] = repo_name
315 environ['REPO_NAME'] = repo_name
299 return app(environ, ResponseFilter(start_response))
316 return app(environ, ResponseFilter(start_response))
300 return _hg_stream
317 return _hg_stream
301
318
302 def git_stream(self):
319 def git_stream(self):
303 if self._use_echo_app:
320 if self._use_echo_app:
304 @wsgiapp
321 @wsgiapp
305 def _git_stream(environ, start_response):
322 def _git_stream(environ, start_response):
306 app = EchoApp('fake_path', 'fake_name', None)
323 app = EchoApp('fake_path', 'fake_name', None)
307 return app(environ, start_response)
324 return app(environ, start_response)
308 return _git_stream
325 return _git_stream
309 else:
326 else:
310 @wsgiapp
327 @wsgiapp
311 def _git_stream(environ, start_response):
328 def _git_stream(environ, start_response):
312 repo_path = environ['HTTP_X_RC_REPO_PATH']
329 repo_path = environ['HTTP_X_RC_REPO_PATH']
313 repo_name = environ['HTTP_X_RC_REPO_NAME']
330 repo_name = environ['HTTP_X_RC_REPO_NAME']
314 packed_config = base64.b64decode(
331 packed_config = base64.b64decode(
315 environ['HTTP_X_RC_REPO_CONFIG'])
332 environ['HTTP_X_RC_REPO_CONFIG'])
316 config = msgpack.unpackb(packed_config)
333 config = msgpack.unpackb(packed_config)
317
334
318 environ['PATH_INFO'] = environ['HTTP_X_RC_PATH_INFO']
335 environ['PATH_INFO'] = environ['HTTP_X_RC_PATH_INFO']
319 app = scm_app.create_git_wsgi_app(
336 app = scm_app.create_git_wsgi_app(
320 repo_path, repo_name, config)
337 repo_path, repo_name, config)
321 return app(environ, start_response)
338 return app(environ, start_response)
322 return _git_stream
339 return _git_stream
323
340
324 def is_vcs_exception(self, context, request):
341 def is_vcs_exception(self, context, request):
325 """
342 """
326 View predicate that returns true if the context object is a VCS
343 View predicate that returns true if the context object is a VCS
327 exception.
344 exception.
328 """
345 """
329 return hasattr(context, '_vcs_kind')
346 return hasattr(context, '_vcs_kind')
330
347
331 def handle_vcs_exception(self, exception, request):
348 def handle_vcs_exception(self, exception, request):
332 if exception._vcs_kind == 'repo_locked':
349 if exception._vcs_kind == 'repo_locked':
333 # Get custom repo-locked status code if present.
350 # Get custom repo-locked status code if present.
334 status_code = request.headers.get('X-RC-Locked-Status-Code')
351 status_code = request.headers.get('X-RC-Locked-Status-Code')
335 return HTTPRepoLocked(
352 return HTTPRepoLocked(
336 title=exception.message, status_code=status_code)
353 title=exception.message, status_code=status_code)
337
354
338 # Re-raise exception if we can not handle it.
355 # Re-raise exception if we can not handle it.
339 raise exception
356 raise exception
340
357
341
358
342 class ResponseFilter(object):
359 class ResponseFilter(object):
343
360
344 def __init__(self, start_response):
361 def __init__(self, start_response):
345 self._start_response = start_response
362 self._start_response = start_response
346
363
347 def __call__(self, status, response_headers, exc_info=None):
364 def __call__(self, status, response_headers, exc_info=None):
348 headers = tuple(
365 headers = tuple(
349 (h, v) for h, v in response_headers
366 (h, v) for h, v in response_headers
350 if not wsgiref.util.is_hop_by_hop(h))
367 if not wsgiref.util.is_hop_by_hop(h))
351 return self._start_response(status, headers, exc_info)
368 return self._start_response(status, headers, exc_info)
352
369
353
370
354 def main(global_config, **settings):
371 def main(global_config, **settings):
355 if MercurialFactory:
372 if MercurialFactory:
356 hgpatches.patch_largefiles_capabilities()
373 hgpatches.patch_largefiles_capabilities()
357 hgpatches.patch_subrepo_type_mapping()
374 hgpatches.patch_subrepo_type_mapping()
358 app = HTTPApplication(settings=settings)
375 app = HTTPApplication(settings=settings)
359 return app.wsgi_app()
376 return app.wsgi_app()
General Comments 0
You need to be logged in to leave comments. Login now