##// END OF EJS Templates
system-info: fix display of text in system inodes.
marcink -
r1155:9821bb6a default
parent child Browse files
Show More
@@ -1,641 +1,640 b''
1 1 import os
2 2 import sys
3 3 import time
4 4 import platform
5 5 import pkg_resources
6 6 import logging
7 7 import string
8 8
9 9
10 10 log = logging.getLogger(__name__)
11 11
12 12
13 13 psutil = None
14 14
15 15 try:
16 16 # cygwin cannot have yet psutil support.
17 17 import psutil as psutil
18 18 except ImportError:
19 19 pass
20 20
21 21
22 22 _NA = 'NOT AVAILABLE'
23 23
24 24 STATE_OK = 'ok'
25 25 STATE_ERR = 'error'
26 26 STATE_WARN = 'warning'
27 27
28 28 STATE_OK_DEFAULT = {'message': '', 'type': STATE_OK}
29 29
30 30
31 31 # HELPERS
32 32 def percentage(part, whole):
33 33 whole = float(whole)
34 34 if whole > 0:
35 return 100 * float(part) / whole
36 return 0
35 return round(100 * float(part) / whole, 1)
36 return 0.0
37 37
38 38
39 39 def get_storage_size(storage_path):
40 40 sizes = []
41 41 for file_ in os.listdir(storage_path):
42 42 storage_file = os.path.join(storage_path, file_)
43 43 if os.path.isfile(storage_file):
44 44 try:
45 45 sizes.append(os.path.getsize(storage_file))
46 46 except OSError:
47 47 log.exception('Failed to get size of storage file %s',
48 48 storage_file)
49 49 pass
50 50
51 51 return sum(sizes)
52 52
53 53
54 54 class SysInfoRes(object):
55 55 def __init__(self, value, state=STATE_OK_DEFAULT, human_value=None):
56 56 self.value = value
57 57 self.state = state
58 58 self.human_value = human_value or value
59 59
60 60 def __json__(self):
61 61 return {
62 62 'value': self.value,
63 63 'state': self.state,
64 64 'human_value': self.human_value,
65 65 }
66 66
67 67 def __str__(self):
68 68 return '<SysInfoRes({})>'.format(self.__json__())
69 69
70 70
71 71 class SysInfo(object):
72 72
73 73 def __init__(self, func_name, **kwargs):
74 74 self.func_name = func_name
75 75 self.value = _NA
76 76 self.state = None
77 77 self.kwargs = kwargs or {}
78 78
79 79 def __call__(self):
80 80 computed = self.compute(**self.kwargs)
81 81 if not isinstance(computed, SysInfoRes):
82 82 raise ValueError(
83 83 'computed value for {} is not instance of '
84 84 '{}, got {} instead'.format(
85 85 self.func_name, SysInfoRes, type(computed)))
86 86 return computed.__json__()
87 87
88 88 def __str__(self):
89 89 return '<SysInfo({})>'.format(self.func_name)
90 90
91 91 def compute(self, **kwargs):
92 92 return self.func_name(**kwargs)
93 93
94 94
95 95 # SysInfo functions
96 96 def python_info():
97 97 value = dict(version=' '.join(platform._sys_version()),
98 98 executable=sys.executable)
99 99 return SysInfoRes(value=value)
100 100
101 101
102 102 def py_modules():
103 103 mods = dict([(p.project_name, p.version)
104 104 for p in pkg_resources.working_set])
105 105 value = sorted(mods.items(), key=lambda k: k[0].lower())
106 106 return SysInfoRes(value=value)
107 107
108 108
109 109 def platform_type():
110 110 from rhodecode.lib.utils import safe_unicode, generate_platform_uuid
111 111
112 112 value = dict(
113 113 name=safe_unicode(platform.platform()),
114 114 uuid=generate_platform_uuid()
115 115 )
116 116 return SysInfoRes(value=value)
117 117
118 118
119 119 def uptime():
120 120 from rhodecode.lib.helpers import age, time_to_datetime
121 121
122 122 value = dict(boot_time=0, uptime=0, text='')
123 123 state = STATE_OK_DEFAULT
124 124 if not psutil:
125 125 return SysInfoRes(value=value, state=state)
126 126
127 127 boot_time = psutil.boot_time()
128 128 value['boot_time'] = boot_time
129 129 value['uptime'] = time.time() - boot_time
130 130
131 131 human_value = value.copy()
132 132 human_value['boot_time'] = time_to_datetime(boot_time)
133 133 human_value['uptime'] = age(time_to_datetime(boot_time), show_suffix=False)
134 134 human_value['text'] = 'Server started {}'.format(
135 135 age(time_to_datetime(boot_time)))
136 136
137 137 return SysInfoRes(value=value, human_value=human_value)
138 138
139 139
140 140 def memory():
141 141 from rhodecode.lib.helpers import format_byte_size_binary
142 142 value = dict(available=0, used=0, used_real=0, cached=0, percent=0,
143 143 percent_used=0, free=0, inactive=0, active=0, shared=0,
144 144 total=0, buffers=0, text='')
145 145
146 146 state = STATE_OK_DEFAULT
147 147 if not psutil:
148 148 return SysInfoRes(value=value, state=state)
149 149
150 150 value.update(dict(psutil.virtual_memory()._asdict()))
151 151 value['used_real'] = value['total'] - value['available']
152 152 value['percent_used'] = psutil._common.usage_percent(
153 153 value['used_real'], value['total'], 1)
154 154
155 155 human_value = value.copy()
156 156 human_value['text'] = '%s/%s, %s%% used' % (
157 157 format_byte_size_binary(value['used_real']),
158 158 format_byte_size_binary(value['total']),
159 159 value['percent_used'],)
160 160
161 161 keys = value.keys()[::]
162 162 keys.pop(keys.index('percent'))
163 163 keys.pop(keys.index('percent_used'))
164 164 keys.pop(keys.index('text'))
165 165 for k in keys:
166 166 human_value[k] = format_byte_size_binary(value[k])
167 167
168 168 if state['type'] == STATE_OK and value['percent_used'] > 90:
169 169 msg = 'Critical: your available RAM memory is very low.'
170 170 state = {'message': msg, 'type': STATE_ERR}
171 171
172 172 elif state['type'] == STATE_OK and value['percent_used'] > 70:
173 173 msg = 'Warning: your available RAM memory is running low.'
174 174 state = {'message': msg, 'type': STATE_WARN}
175 175
176 176 return SysInfoRes(value=value, state=state, human_value=human_value)
177 177
178 178
179 179 def machine_load():
180 180 value = {'1_min': _NA, '5_min': _NA, '15_min': _NA, 'text': ''}
181 181 state = STATE_OK_DEFAULT
182 182 if not psutil:
183 183 return SysInfoRes(value=value, state=state)
184 184
185 185 # load averages
186 186 if hasattr(psutil.os, 'getloadavg'):
187 187 value.update(dict(
188 188 zip(['1_min', '5_min', '15_min'], psutil.os.getloadavg())))
189 189
190 190 human_value = value.copy()
191 191 human_value['text'] = '1min: {}, 5min: {}, 15min: {}'.format(
192 192 value['1_min'], value['5_min'], value['15_min'])
193 193
194 194 if state['type'] == STATE_OK and value['15_min'] > 5:
195 195 msg = 'Warning: your machine load is very high.'
196 196 state = {'message': msg, 'type': STATE_WARN}
197 197
198 198 return SysInfoRes(value=value, state=state, human_value=human_value)
199 199
200 200
201 201 def cpu():
202 202 value = 0
203 203 state = STATE_OK_DEFAULT
204 204
205 205 if not psutil:
206 206 return SysInfoRes(value=value, state=state)
207 207
208 208 value = psutil.cpu_percent(0.5)
209 209 human_value = '{} %'.format(value)
210 210 return SysInfoRes(value=value, state=state, human_value=human_value)
211 211
212 212
213 213 def storage():
214 214 from rhodecode.lib.helpers import format_byte_size_binary
215 215 from rhodecode.model.settings import VcsSettingsModel
216 216 path = VcsSettingsModel().get_repos_location()
217 217
218 218 value = dict(percent=0, used=0, total=0, path=path, text='')
219 219 state = STATE_OK_DEFAULT
220 220 if not psutil:
221 221 return SysInfoRes(value=value, state=state)
222 222
223 223 try:
224 224 value.update(dict(psutil.disk_usage(path)._asdict()))
225 225 except Exception as e:
226 226 log.exception('Failed to fetch disk info')
227 227 state = {'message': str(e), 'type': STATE_ERR}
228 228
229 229 human_value = value.copy()
230 230 human_value['used'] = format_byte_size_binary(value['used'])
231 231 human_value['total'] = format_byte_size_binary(value['total'])
232 232 human_value['text'] = "{}/{}, {}% used".format(
233 233 format_byte_size_binary(value['used']),
234 234 format_byte_size_binary(value['total']),
235 235 value['percent'])
236 236
237 237 if state['type'] == STATE_OK and value['percent'] > 90:
238 238 msg = 'Critical: your disk space is very low.'
239 239 state = {'message': msg, 'type': STATE_ERR}
240 240
241 241 elif state['type'] == STATE_OK and value['percent'] > 70:
242 242 msg = 'Warning: your disk space is running low.'
243 243 state = {'message': msg, 'type': STATE_WARN}
244 244
245 245 return SysInfoRes(value=value, state=state, human_value=human_value)
246 246
247 247
248 248 def storage_inodes():
249 249 from rhodecode.model.settings import VcsSettingsModel
250 250 path = VcsSettingsModel().get_repos_location()
251 251
252 252 value = dict(percent=0, free=0, used=0, total=0, path=path, text='')
253 253 state = STATE_OK_DEFAULT
254 254 if not psutil:
255 255 return SysInfoRes(value=value, state=state)
256 256
257 257 try:
258 258 i_stat = os.statvfs(path)
259 259
260 260 value['used'] = i_stat.f_ffree
261 261 value['free'] = i_stat.f_favail
262 262 value['total'] = i_stat.f_files
263 value['percent'] = percentage(
264 value['used'], value['total'])
263 value['percent'] = percentage(value['used'], value['total'])
265 264 except Exception as e:
266 265 log.exception('Failed to fetch disk inodes info')
267 266 state = {'message': str(e), 'type': STATE_ERR}
268 267
269 268 human_value = value.copy()
270 269 human_value['text'] = "{}/{}, {}% used".format(
271 270 value['used'], value['total'], value['percent'])
272 271
273 272 if state['type'] == STATE_OK and value['percent'] > 90:
274 273 msg = 'Critical: your disk free inodes are very low.'
275 274 state = {'message': msg, 'type': STATE_ERR}
276 275
277 276 elif state['type'] == STATE_OK and value['percent'] > 70:
278 277 msg = 'Warning: your disk free inodes are running low.'
279 278 state = {'message': msg, 'type': STATE_WARN}
280 279
281 return SysInfoRes(value=value, state=state)
280 return SysInfoRes(value=value, state=state, human_value=human_value)
282 281
283 282
284 283 def storage_archives():
285 284 import rhodecode
286 285 from rhodecode.lib.utils import safe_str
287 286 from rhodecode.lib.helpers import format_byte_size_binary
288 287
289 288 msg = 'Enable this by setting ' \
290 289 'archive_cache_dir=/path/to/cache option in the .ini file'
291 290 path = safe_str(rhodecode.CONFIG.get('archive_cache_dir', msg))
292 291
293 292 value = dict(percent=0, used=0, total=0, items=0, path=path, text='')
294 293 state = STATE_OK_DEFAULT
295 294 try:
296 295 items_count = 0
297 296 used = 0
298 297 for root, dirs, files in os.walk(path):
299 298 if root == path:
300 299 items_count = len(files)
301 300
302 301 for f in files:
303 302 try:
304 303 used += os.path.getsize(os.path.join(root, f))
305 304 except OSError:
306 305 pass
307 306 value.update({
308 307 'percent': 100,
309 308 'used': used,
310 309 'total': used,
311 310 'items': items_count
312 311 })
313 312
314 313 except Exception as e:
315 314 log.exception('failed to fetch archive cache storage')
316 315 state = {'message': str(e), 'type': STATE_ERR}
317 316
318 317 human_value = value.copy()
319 318 human_value['used'] = format_byte_size_binary(value['used'])
320 319 human_value['total'] = format_byte_size_binary(value['total'])
321 320 human_value['text'] = "{} ({} items)".format(
322 321 human_value['used'], value['items'])
323 322
324 323 return SysInfoRes(value=value, state=state, human_value=human_value)
325 324
326 325
327 326 def storage_gist():
328 327 from rhodecode.model.gist import GIST_STORE_LOC
329 328 from rhodecode.model.settings import VcsSettingsModel
330 329 from rhodecode.lib.utils import safe_str
331 330 from rhodecode.lib.helpers import format_byte_size_binary
332 331 path = safe_str(os.path.join(
333 332 VcsSettingsModel().get_repos_location(), GIST_STORE_LOC))
334 333
335 334 # gist storage
336 335 value = dict(percent=0, used=0, total=0, items=0, path=path, text='')
337 336 state = STATE_OK_DEFAULT
338 337
339 338 try:
340 339 items_count = 0
341 340 used = 0
342 341 for root, dirs, files in os.walk(path):
343 342 if root == path:
344 343 items_count = len(dirs)
345 344
346 345 for f in files:
347 346 try:
348 347 used += os.path.getsize(os.path.join(root, f))
349 348 except OSError:
350 349 pass
351 350 value.update({
352 351 'percent': 100,
353 352 'used': used,
354 353 'total': used,
355 354 'items': items_count
356 355 })
357 356 except Exception as e:
358 357 log.exception('failed to fetch gist storage items')
359 358 state = {'message': str(e), 'type': STATE_ERR}
360 359
361 360 human_value = value.copy()
362 361 human_value['used'] = format_byte_size_binary(value['used'])
363 362 human_value['total'] = format_byte_size_binary(value['total'])
364 363 human_value['text'] = "{} ({} items)".format(
365 364 human_value['used'], value['items'])
366 365
367 366 return SysInfoRes(value=value, state=state, human_value=human_value)
368 367
369 368
370 369 def storage_temp():
371 370 import tempfile
372 371 from rhodecode.lib.helpers import format_byte_size_binary
373 372
374 373 path = tempfile.gettempdir()
375 374 value = dict(percent=0, used=0, total=0, items=0, path=path, text='')
376 375 state = STATE_OK_DEFAULT
377 376
378 377 if not psutil:
379 378 return SysInfoRes(value=value, state=state)
380 379
381 380 try:
382 381 value.update(dict(psutil.disk_usage(path)._asdict()))
383 382 except Exception as e:
384 383 log.exception('Failed to fetch temp dir info')
385 384 state = {'message': str(e), 'type': STATE_ERR}
386 385
387 386 human_value = value.copy()
388 387 human_value['used'] = format_byte_size_binary(value['used'])
389 388 human_value['total'] = format_byte_size_binary(value['total'])
390 389 human_value['text'] = "{}/{}, {}% used".format(
391 390 format_byte_size_binary(value['used']),
392 391 format_byte_size_binary(value['total']),
393 392 value['percent'])
394 393
395 394 return SysInfoRes(value=value, state=state, human_value=human_value)
396 395
397 396
398 397 def search_info():
399 398 import rhodecode
400 399 from rhodecode.lib.index import searcher_from_config
401 400
402 401 backend = rhodecode.CONFIG.get('search.module', '')
403 402 location = rhodecode.CONFIG.get('search.location', '')
404 403
405 404 try:
406 405 searcher = searcher_from_config(rhodecode.CONFIG)
407 406 searcher = searcher.__class__.__name__
408 407 except Exception:
409 408 searcher = None
410 409
411 410 value = dict(
412 411 backend=backend, searcher=searcher, location=location, text='')
413 412 state = STATE_OK_DEFAULT
414 413
415 414 human_value = value.copy()
416 415 human_value['text'] = "backend:`{}`".format(human_value['backend'])
417 416
418 417 return SysInfoRes(value=value, state=state, human_value=human_value)
419 418
420 419
421 420 def git_info():
422 421 from rhodecode.lib.vcs.backends import git
423 422 state = STATE_OK_DEFAULT
424 423 value = human_value = ''
425 424 try:
426 425 value = git.discover_git_version(raise_on_exc=True)
427 426 human_value = 'version reported from VCSServer: {}'.format(value)
428 427 except Exception as e:
429 428 state = {'message': str(e), 'type': STATE_ERR}
430 429
431 430 return SysInfoRes(value=value, state=state, human_value=human_value)
432 431
433 432
434 433 def hg_info():
435 434 from rhodecode.lib.vcs.backends import hg
436 435 state = STATE_OK_DEFAULT
437 436 value = human_value = ''
438 437 try:
439 438 value = hg.discover_hg_version(raise_on_exc=True)
440 439 human_value = 'version reported from VCSServer: {}'.format(value)
441 440 except Exception as e:
442 441 state = {'message': str(e), 'type': STATE_ERR}
443 442 return SysInfoRes(value=value, state=state, human_value=human_value)
444 443
445 444
446 445 def svn_info():
447 446 from rhodecode.lib.vcs.backends import svn
448 447 state = STATE_OK_DEFAULT
449 448 value = human_value = ''
450 449 try:
451 450 value = svn.discover_svn_version(raise_on_exc=True)
452 451 human_value = 'version reported from VCSServer: {}'.format(value)
453 452 except Exception as e:
454 453 state = {'message': str(e), 'type': STATE_ERR}
455 454 return SysInfoRes(value=value, state=state, human_value=human_value)
456 455
457 456
458 457 def vcs_backends():
459 458 import rhodecode
460 459 value = map(
461 460 string.strip, rhodecode.CONFIG.get('vcs.backends', '').split(','))
462 461 human_value = 'Enabled backends in order: {}'.format(','.join(value))
463 462 return SysInfoRes(value=value, human_value=human_value)
464 463
465 464
466 465 def vcs_server():
467 466 import rhodecode
468 467 from rhodecode.lib.vcs.backends import get_vcsserver_version
469 468
470 469 server_url = rhodecode.CONFIG.get('vcs.server')
471 470 enabled = rhodecode.CONFIG.get('vcs.server.enable')
472 471 protocol = rhodecode.CONFIG.get('vcs.server.protocol')
473 472 state = STATE_OK_DEFAULT
474 473 version = None
475 474
476 475 try:
477 476 version = get_vcsserver_version()
478 477 connection = 'connected'
479 478 except Exception as e:
480 479 connection = 'failed'
481 480 state = {'message': str(e), 'type': STATE_ERR}
482 481
483 482 value = dict(
484 483 url=server_url,
485 484 enabled=enabled,
486 485 protocol=protocol,
487 486 connection=connection,
488 487 version=version,
489 488 text='',
490 489 )
491 490
492 491 human_value = value.copy()
493 492 human_value['text'] = \
494 493 '{url}@ver:{ver} via {mode} mode, connection:{conn}'.format(
495 494 url=server_url, ver=version, mode=protocol, conn=connection)
496 495
497 496 return SysInfoRes(value=value, state=state, human_value=human_value)
498 497
499 498
500 499 def rhodecode_app_info():
501 500 import rhodecode
502 501 edition = rhodecode.CONFIG.get('rhodecode.edition')
503 502
504 503 value = dict(
505 504 rhodecode_version=rhodecode.__version__,
506 505 rhodecode_lib_path=os.path.abspath(rhodecode.__file__),
507 506 text=''
508 507 )
509 508 human_value = value.copy()
510 509 human_value['text'] = 'RhodeCode {edition}, version {ver}'.format(
511 510 edition=edition, ver=value['rhodecode_version']
512 511 )
513 512 return SysInfoRes(value=value, human_value=human_value)
514 513
515 514
516 515 def rhodecode_config():
517 516 import rhodecode
518 517 path = rhodecode.CONFIG.get('__file__')
519 518 rhodecode_ini_safe = rhodecode.CONFIG.copy()
520 519
521 520 blacklist = [
522 521 'rhodecode_license_key',
523 522 'routes.map',
524 523 'pylons.h',
525 524 'pylons.app_globals',
526 525 'pylons.environ_config',
527 526 'sqlalchemy.db1.url',
528 527 'channelstream.secret',
529 528 'beaker.session.secret',
530 529 'rhodecode.encrypted_values.secret',
531 530 'rhodecode_auth_github_consumer_key',
532 531 'rhodecode_auth_github_consumer_secret',
533 532 'rhodecode_auth_google_consumer_key',
534 533 'rhodecode_auth_google_consumer_secret',
535 534 'rhodecode_auth_bitbucket_consumer_secret',
536 535 'rhodecode_auth_bitbucket_consumer_key',
537 536 'rhodecode_auth_twitter_consumer_secret',
538 537 'rhodecode_auth_twitter_consumer_key',
539 538
540 539 'rhodecode_auth_twitter_secret',
541 540 'rhodecode_auth_github_secret',
542 541 'rhodecode_auth_google_secret',
543 542 'rhodecode_auth_bitbucket_secret',
544 543
545 544 'appenlight.api_key',
546 545 ('app_conf', 'sqlalchemy.db1.url')
547 546 ]
548 547 for k in blacklist:
549 548 if isinstance(k, tuple):
550 549 section, key = k
551 550 if section in rhodecode_ini_safe:
552 551 rhodecode_ini_safe[section] = '**OBFUSCATED**'
553 552 else:
554 553 rhodecode_ini_safe.pop(k, None)
555 554
556 555 # TODO: maybe put some CONFIG checks here ?
557 556 return SysInfoRes(value={'config': rhodecode_ini_safe, 'path': path})
558 557
559 558
560 559 def database_info():
561 560 import rhodecode
562 561 from sqlalchemy.engine import url as engine_url
563 562 from rhodecode.model.meta import Base as sql_base, Session
564 563 from rhodecode.model.db import DbMigrateVersion
565 564
566 565 state = STATE_OK_DEFAULT
567 566
568 567 db_migrate = DbMigrateVersion.query().filter(
569 568 DbMigrateVersion.repository_id == 'rhodecode_db_migrations').one()
570 569
571 570 db_url_obj = engine_url.make_url(rhodecode.CONFIG['sqlalchemy.db1.url'])
572 571
573 572 try:
574 573 engine = sql_base.metadata.bind
575 574 db_server_info = engine.dialect._get_server_version_info(
576 575 Session.connection(bind=engine))
577 576 db_version = '.'.join(map(str, db_server_info))
578 577 except Exception:
579 578 log.exception('failed to fetch db version')
580 579 db_version = 'UNKNOWN'
581 580
582 581 db_info = dict(
583 582 migrate_version=db_migrate.version,
584 583 type=db_url_obj.get_backend_name(),
585 584 version=db_version,
586 585 url=repr(db_url_obj)
587 586 )
588 587
589 588 human_value = db_info.copy()
590 589 human_value['url'] = "{} @ migration version: {}".format(
591 590 db_info['url'], db_info['migrate_version'])
592 591 human_value['version'] = "{} {}".format(db_info['type'], db_info['version'])
593 592 return SysInfoRes(value=db_info, state=state, human_value=human_value)
594 593
595 594
596 595 def server_info(environ):
597 596 import rhodecode
598 597 from rhodecode.lib.base import get_server_ip_addr, get_server_port
599 598
600 599 value = {
601 600 'server_ip': '%s:%s' % (
602 601 get_server_ip_addr(environ, log_errors=False),
603 602 get_server_port(environ)
604 603 ),
605 604 'server_id': rhodecode.CONFIG.get('instance_id'),
606 605 }
607 606 return SysInfoRes(value=value)
608 607
609 608
610 609 def get_system_info(environ):
611 610 environ = environ or {}
612 611 return {
613 612 'rhodecode_app': SysInfo(rhodecode_app_info)(),
614 613 'rhodecode_config': SysInfo(rhodecode_config)(),
615 614 'python': SysInfo(python_info)(),
616 615 'py_modules': SysInfo(py_modules)(),
617 616
618 617 'platform': SysInfo(platform_type)(),
619 618 'server': SysInfo(server_info, environ=environ)(),
620 619 'database': SysInfo(database_info)(),
621 620
622 621 'storage': SysInfo(storage)(),
623 622 'storage_inodes': SysInfo(storage_inodes)(),
624 623 'storage_archive': SysInfo(storage_archives)(),
625 624 'storage_gist': SysInfo(storage_gist)(),
626 625 'storage_temp': SysInfo(storage_temp)(),
627 626
628 627 'search': SysInfo(search_info)(),
629 628
630 629 'uptime': SysInfo(uptime)(),
631 630 'load': SysInfo(machine_load)(),
632 631 'cpu': SysInfo(cpu)(),
633 632 'memory': SysInfo(memory)(),
634 633
635 634 'vcs_backends': SysInfo(vcs_backends)(),
636 635 'vcs_server': SysInfo(vcs_server)(),
637 636
638 637 'git': SysInfo(git_info)(),
639 638 'hg': SysInfo(hg_info)(),
640 639 'svn': SysInfo(svn_info)(),
641 640 }
General Comments 0
You need to be logged in to leave comments. Login now