instance-zope.cfg.in 18.3 KB
Newer Older
1
{% set use_ipv6 = slapparameter_dict.get('use-ipv6', False) -%}
2
{% set next_port = itertools.count(slapparameter_dict['port-base']).next -%}
3
{% set site_id = slapparameter_dict['site-id'] -%}
4
{% set zodb_dict = slapparameter_dict['zodb-dict'] -%}
5
{% set instance_index_list = range(slapparameter_dict['instance-count']) -%}
6
{% set node_id_base = slapparameter_dict['name'] -%}
7
{% set saucelabs_dict = slapparameter_dict.get('saucelabs-dict', None) -%}
8
{% set node_id_index_format = '-%%0%ii' % (len(str(instance_index_list[-1])), ) -%}
9 10 11 12 13 14 15 16 17 18 19
{% set part_list = [] -%}
{% set publish_list = [] -%}
{% set longrequest_logger_base_path = buildout_directory ~ '/var/log/longrequest_logger_' -%}
{% macro section(name) %}{% do part_list.append(name) %}{{ name }}{% endmacro -%}
{% set bin_directory = parameter_dict['buildout-bin-directory'] -%}
{#
XXX: This template only supports exactly one IPv4 and one IPv6 per
partition. No more (undefined result), no less (IndexError).
-#}
{% set ipv4 = (ipv4_set | list)[0] -%}

20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
{% set hosts_dict = {} -%}
{% set port_dict = {} -%}
{% for alias, url in (
    ('erp5-memcached-volatile', slapparameter_dict['memcached-url']),
    ('erp5-memcached-persistent', slapparameter_dict['kumofs-url']),
    ('erp5-cloudooo', slapparameter_dict['cloudooo-url']),
    ('erp5-smtp', slapparameter_dict['smtp-url']),
  ) -%}
{%   set parsed_url = urlparse.urlparse(url) -%}
{%   do port_dict.__setitem__(alias, parsed_url.port)  -%}
{%   do hosts_dict.__setitem__(alias, parsed_url.hostname)  -%}
{%- endfor %}
{% for i, url in enumerate(slapparameter_dict['mysql-url-list']) -%}
{%   do hosts_dict.__setitem__(
       'erp5-catalog-' ~ i,
       urlparse.urlparse(url).hostname,
     ) -%}
{%- endfor %}
{% do hosts_dict.update(slapparameter_dict['hosts-dict']) -%}

40 41 42 43
[jinja2-template-base]
recipe = slapos.recipe.template:jinja2
mode = 644

44
[run-common]
45
<= userhosts-wrapper-base
46 47 48 49 50 51 52 53 54 55 56 57 58
environment-extra =
environment +=
  TMP=${directory:tmp}
  TMPDIR=${directory:tmp}
  HOME=${buildout:directory}
  PATH=${binary-link:target-directory}:{{ parameter_dict['coreutils'] }}/bin
  TZ={{ slapparameter_dict['timezone'] }}
  MATPLOTLIBRC={{ parameter_dict['matplotlibrc'] }}
  INSTANCE_HOME=${:instance-home}
{% if slapparameter_dict.get('wendelin-core-zblk-fmt') %}
  WENDELIN_CORE_ZBLK_FMT={{ slapparameter_dict['wendelin-core-zblk-fmt'] }}
{% endif %}
  ${:environment-extra}
59

60 61
[test-certificate-authority]
recipe = slapos.cookbook:certificate_authority
62
openssl-binary = ${binary-link:target-directory}/openssl
63
ca-dir = ${directory:test-ca-dir}
64
requests-directory = ${directory:test-ca-requests}
65
wrapper = ${directory:services}/test-ca
66 67 68 69
ca-private = ${directory:test-ca-private}
ca-certs = ${directory:test-ca-certs}
ca-newcerts = ${directory:test-ca-newcerts}
ca-crl = ${directory:test-ca-crl}
70

71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87
{% if saucelabs_dict -%}
[test-zelenium-runner-parameter]
configuration = {{ dumps(saucelabs_dict) }}
user = {{ dumps(slapparameter_dict['inituser-login']) }}
password = {{ dumps(slapparameter_dict['inituser-password']) }}

[{{ section('test-zelenium-runner') }}]
recipe = slapos.recipe.template:jinja2
template = {{ parameter_dict['run-zelenium-template'] }}
rendered = ${directory:bin}/runTestSuite
extensions = jinja2.ext.do
context =
    import json_module json
    key configuration test-zelenium-runner-parameter:configuration
    key user test-zelenium-runner-parameter:user
    key password test-zelenium-runner-parameter:password
{% else -%}
88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144
{% if slapparameter_dict['mysql-test-url-list'] -%}
[{{ section('run-unit-test-userhosts-wrapper') }}]
<= userhosts-wrapper-base
wrapped-command-line = ${runUnitTest:wrapper-path}
wrapper-path = ${buildout:bin-directory}/runUnitTest

[{{ section('run-test-suite-userhosts-wrapper') }}]
<= userhosts-wrapper-base
wrapped-command-line = ${runTestSuite:wrapper-path}
wrapper-path = ${buildout:bin-directory}/runTestSuite

{% set connection_string_list = [] -%}
{% for url in slapparameter_dict['mysql-test-url-list'] -%}
{%   set parsed_url = urlparse.urlparse(url) -%}
{%   do connection_string_list.append(
       '%s@%s:%s %s %s' % (
         parsed_url.path.lstrip('/'),
         parsed_url.hostname,
         parsed_url.port,
         parsed_url.username,
         parsed_url.password,
       ),
     ) -%}
{% endfor -%}
[run-test-common]
< = run-common
environment-extra =
  REAL_INSTANCE_HOME=${:instance-home}
  OPENSSL_BINARY=${test-certificate-authority:openssl-binary}
  TEST_CA_PATH=${test-certificate-authority:ca-dir}
instance-home = ${directory:unit-test-path}
wrapper-path = ${directory:bin}/${:command-name}.real
command-line =
  '{{ parameter_dict['bin-directory'] }}/${:command-name}'
  ${:command-line-extra}
  --conversion_server_url={{ slapparameter_dict['cloudooo-url'] }}
  --conversion_server_retry_count={{ slapparameter_dict.get('cloudooo-retry-count', 2) }}
{#- BBB: We still have test suites that only accept the following 2 options. #}
  --conversion_server_hostname=erp5-cloudooo
  --conversion_server_port={{ port_dict['erp5-cloudooo'] }}
  --volatile_memcached_server_hostname=erp5-memcached-volatile
  --volatile_memcached_server_port={{ port_dict['erp5-memcached-volatile'] }}
  --persistent_memcached_server_hostname=erp5-memcached-persistent
  --persistent_memcached_server_port={{ port_dict['erp5-memcached-persistent'] }}

[{{ section('runUnitTest') }}]
< = run-test-common
command-name = runUnitTest
command-line-extra =
  --erp5_sql_connection_string '{{ connection_string_list[0] }}'
  --extra_sql_connection_string_list '{{ ','.join(connection_string_list[1:]) }}'

[{{ section('runTestSuite') }}]
< = run-test-common
command-name = runTestSuite
command-line-extra =
  --db_list '{{ ','.join(connection_string_list) }}'
145
{%- endif %}
146
{%- endif %}
147

148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165
[directory]
recipe = slapos.cookbook:mkdirectory
bin = ${buildout:directory}/bin
etc = ${buildout:directory}/etc
instance = ${:srv}/erp5shared
instance-constraint = ${:instance}/Constraint
instance-document = ${:instance}/Document
instance-etc = ${:instance}/etc
instance-etc-package-include = ${:instance}/etc/package-include
instance-extensions = ${:instance}/Extensions
instance-import = ${:instance}/import
instance-lib = ${:instance}/lib
instance-products = ${:instance}/Products
instance-propertysheet = ${:instance}/PropertySheet
instance-tests = ${:instance}/tests
log = ${:var}/log
run = ${:var}/run
services = ${:etc}/run
166
service-on-watch = ${:etc}/service
167 168 169 170
srv = ${buildout:directory}/srv
tmp = ${buildout:directory}/tmp
var = ${buildout:directory}/var
promises = ${:etc}/promise
171 172
unit-test-path = ${:srv}/test-instance/unit_test
test-ca-dir = ${:srv}/test-ca
173 174 175 176 177 178 179 180 181 182 183
test-ca-requests = ${:test-ca-dir}/requests
test-ca-private = ${:test-ca-dir}/private
test-ca-certs = ${:test-ca-dir}/certs
test-ca-newcerts = ${:test-ca-dir}/newcerts
test-ca-crl = ${:test-ca-dir}/crl
ca-dir = ${:srv}/ca
ca-requests = ${:ca-dir}/requests
ca-private = ${:ca-dir}/private
ca-certs = ${:ca-dir}/certs
ca-newcerts = ${:ca-dir}/newcerts
ca-crl = ${:ca-dir}/crl
184 185 186 187

[binary-link]
recipe = slapos.cookbook:symbolic.link
target-directory = ${directory:bin}
188
link-binary = {{ dumps(parameter_dict['link-binary']) }}
189 190

[certificate-authority-common]
191
requests-directory = ${directory:ca-requests}
192
ca-dir = ${directory:ca-dir}
193 194 195 196
ca-private = ${directory:ca-private}
ca-certs = ${directory:ca-certs}
ca-newcerts = ${directory:ca-newcerts}
ca-crl = ${directory:ca-crl}
197

198
[{{ section('certificate-authority') }}]
199 200
< = certificate-authority-common
recipe = slapos.cookbook:certificate_authority
201
openssl-binary = ${binary-link:target-directory}/openssl
202 203 204 205 206 207 208 209 210 211 212 213 214 215
wrapper = ${directory:services}/ca

{% if use_ipv6 -%}
{%   set ipv6 = (ipv6_set | list)[0] -%}

[ipv6toipv4-base]
recipe = slapos.cookbook:ipv6toipv4
runner-path = ${directory:services}/${:base-name}
6tunnel-path = {{ parameter_dict['6tunnel'] }}/bin/6tunnel
shell-path = {{ parameter_dict['dash'] }}/bin/dash
ipv4 = {{ ipv4 }}
ipv6 = {{ ipv6 }}
{% endif -%}

216
[hosts-parameter]
217
# Used for both hosts and hostaliases sections.
218
host-dict = {{ dumps(hosts_dict) }}
219 220 221 222 223 224 225 226 227
hostalias-dict = {{ dumps(slapparameter_dict['hostalias-dict']) }}

# Note: there is a subtle difference between hosts and hostaliases files:
# - hosts files start with resolved, followed by alias(es) (only one alias per
#   line in this case)
# - hostaliases start with alias, followed by resolved
# ...so it's not possible to merge these templates (not a big deal anyway).

[hostaliases]
228
< = jinja2-template-base
229 230 231 232 233 234 235
template = inline: {{ '
  {% for alias, aliased in host_dict.items() -%}
  {{ alias }} {{ aliased }}
  {% endfor %}
' }}
rendered = ${directory:etc}/hostaliases
context = key host_dict hosts-parameter:hostalias-dict
236 237

[hosts]
238
< = jinja2-template-base
239 240 241 242 243 244 245 246
template = inline: {{ '
  {% for alias, aliased in host_dict.items() -%}
  {{ aliased }} {{ alias }}
  {% endfor %}
' }}
rendered = ${directory:etc}/hosts
context = key host_dict hosts-parameter:host-dict

247 248 249 250 251 252 253
[userhosts-wrapper-base]
recipe = slapos.cookbook:wrapper
environment =
  HOSTALIASES=${hostaliases:rendered}
  HOSTS=${hosts:rendered}
command-line = '{{ parameter_dict['userhosts'] }}' ${:wrapped-command-line}

254 255 256 257 258 259 260 261 262
{# Hack to deploy SSL certs via instance parameters -#}
{% for zodb in zodb_dict.itervalues() -%}
{%   set storage_dict = zodb.setdefault('storage-dict', {}) -%}
{%   if zodb['type'] == 'neo' and storage_dict.get('ssl', 1) -%}
{%     for k, v in (('_ca', 'ca.crt'),
                    ('_cert', 'neo.crt'),
                    ('_key', 'neo.key')) -%}
{%       if k in storage_dict -%}
[{{ section('neo-ssl-' + k[1:]) }}]
263
< = jinja2-template-base
264 265 266 267 268 269 270 271 272 273 274
rendered = ${directory:etc}/{{v}}
template = inline:{{'{{'}}pem}}
context = key pem :pem
pem = {{dumps(storage_dict.pop(k))}}

{%       endif -%}
{%     endfor -%}
{%   endif -%}
{% endfor -%}
{# endhack -#}

275
[runzope-base]
276 277
<= run-common
instance-home = ${directory:instance}
278
wrapped-command-line = '{{ bin_directory }}/runzope' -C '${:configuration-file}'
279
private-dev-shm = {{ slapparameter_dict['private-dev-shm'] }}
280 281

[{{ section('zcml') }}]
282
recipe = slapos.cookbook:copyfilelist
283
target-directory = ${directory:instance-etc}
284
file-list = {{ parameter_dict['site-zcml'] }}
285 286

[{{ section('zope-inituser') }}]
287
< = jinja2-template-base
288 289 290 291
rendered = ${directory:instance}/inituser
template = inline:{{ slapparameter_dict['inituser-login'] }}:{SHA}{{ hashlib.sha1(slapparameter_dict['inituser-password']).digest().encode('base64').rstrip() }}
mode = 600
once = ${:rendered}_done
292 293 294 295

[zope-conf-parameter-base]
ip = {{ ipv4 }}
site-id = {{ site_id }}
296 297 298 299
{% if site_id -%}
mysql-url = {{ slapparameter_dict['mysql-url-list'][0] }}
inituser = {{ slapparameter_dict['inituser-login'] }}
{%  set mysql = urlparse.urlsplit(slapparameter_dict['mysql-url-list'][0]) -%}
300
{%  set mysql_db = mysql.path.split('/')[1] -%}
301
sql-connection-string = {{ '%s@erp5-catalog-0:%s %s %s' % (
302
    mysql_db, mysql.port, mysql.username, mysql.password) }}
303 304
bt5 = {{ slapparameter_dict['bt5'] }}
bt5-repository-url = {{ slapparameter_dict['bt5-repository-url'] }}
305
id-store-interval = {{ dumps(slapparameter_dict['id-store-interval']) }}
306 307 308 309 310 311 312 313 314
home = ${buildout:directory}
# We only want to change the hostname to 'erp5-cloudooo' if we use the internal
# cloudooo. We plan to remove the ability to have an internal one, so this
# heuristic is enough.
{%  set cloudooo = urlparse.urlsplit(slapparameter_dict['cloudooo-url']) -%}
cloudooo-url = {{ (cloudooo if cloudooo.port == None else
  cloudooo._replace(netloc='erp5-cloudooo:%s' % cloudooo.port)).geturl() }}

{% endif -%}
315
{% set zeo_dict = slapparameter_dict.get('zodb-zeo', {}) -%}
316
{% for name, zodb in zodb_dict.iteritems() -%}
317 318 319 320 321 322 323 324 325 326 327 328 329 330 331
{%   set storage_dict = zodb.setdefault('storage-dict', {}) -%}
{%   if zodb['type'] == 'zeo' -%}
{%     do storage_dict.update(zeo_dict.get(name, ())) -%}
{%   else -%}
{%     if name == slapparameter_dict.get('neo-name') -%}
{%       do storage_dict.update(master_nodes=slapparameter_dict['neo-masters'],
                                name=slapparameter_dict['neo-cluster']) -%}
{%     endif -%}
{{     assert(storage_dict['master_nodes'], name) }}
{%     if storage_dict.pop('ssl', 1) -%}
{%       do storage_dict.update(ca='~/etc/ca.crt',
                                cert='~/etc/neo.crt',
                                key='~/etc/neo.key') -%}
{%     endif -%}
{%   endif -%}
332
{% endfor -%}
333
developer-list = {{ dumps(slapparameter_dict['developer-list']) }}
334 335 336 337
instance = ${directory:instance}
instance-products = ${directory:instance-products}
deadlock-path = /manage_debug_threads
deadlock-debugger-password = {{ dumps(slapparameter_dict['deadlock-debugger-password']) }}
338 339 340 341
{% if slapparameter_dict.get('tidstorage-ip') -%}
tidstorage-ip = {{ dumps(slapparameter_dict['tidstorage-ip']) }}
tidstorage-port = {{ dumps(slapparameter_dict['tidstorage-port']) }}
{% endif -%}
342
{% set thread_amount = slapparameter_dict['thread-amount'] -%}
343
{% set large_file_threshold = slapparameter_dict['large-file-threshold']  -%}
344
thread-amount = {{ thread_amount }}
345 346 347 348 349 350 351 352
{% set webdav = slapparameter_dict['webdav'] -%}
webdav = {{ dumps(webdav) }}
{% if webdav -%}
{%   set timerserver_interval = 0 -%}
{% else -%}
{%   set timerserver_interval = slapparameter_dict['timerserver-interval'] -%}
{%- endif %}
timerserver-interval = {{ dumps(timerserver_interval) }}
353 354

[zope-conf-base]
355
< = jinja2-template-base
356 357 358
template = {{ parameter_dict['zope-conf-template'] }}

{% macro zope(
359
  index,
360 361 362 363
  port,
  longrequest_logger_timeout,
  longrequest_logger_interval
) -%}
364
{% set name = 'zope-' ~ index -%}
365 366 367 368 369 370 371 372 373 374 375
{% set conf_name = name ~ '-conf' -%}
{% set conf_parameter_name = conf_name ~ '-param' -%}
{% set zope_tunnel_section_name = name ~ '-ipv6toipv4' -%}
{% set zope_tunnel_base_name = zope_tunnel_section_name -%}
[{{ conf_parameter_name }}]
< = zope-conf-parameter-base
pid-file = ${directory:run}/{{ name }}.pid
lock-file = ${directory:run}/{{ name }}.lock
port = {{ port }}
event-log = ${directory:log}/{{ name }}-event.log
z2-log = ${directory:log}/{{ name }}-Z2.log
376
node-id = {{ dumps(node_id_base ~ (node_id_index_format % index)) }}
377
{% set log_list = [] -%}
378 379
{% set import_set = set() -%}
{% for db_name, zodb in zodb_dict.iteritems() -%}
380
{%   do zodb.setdefault('pool-size', thread_amount) -%}
381 382
{%   if zodb['type'] == 'neo' -%}
{%     do import_set.add('neo.client') -%}
383 384 385
{%     set log = name ~ '-neo-' ~ db_name ~ '.log' -%}
{%     do log_list.append('${directory:log}/' + log) -%}
{%     do zodb['storage-dict'].update(logfile='~/var/log/'+log) -%}
386 387
{%   endif -%}
{% endfor -%}
388 389
import-list = {{ dumps(list(import_set)) }}
zodb-dict = {{ dumps(zodb_dict) }}
390
large-file-threshold = {{ large_file_threshold }} 
391 392 393 394 395 396 397
{% if longrequest_logger_interval > 0 -%}
longrequest-logger-file = {{ longrequest_logger_base_path ~ name ~ ".log" }}
longrequest-logger-timeout = {{ longrequest_logger_timeout }}
longrequest-logger-interval = {{ longrequest_logger_interval }}
{% else -%}
longrequest-logger-file =
{% endif -%}
398 399 400 401

[{{ conf_name }}]
< = zope-conf-base
rendered = ${directory:etc}/{{ name }}.conf
402
extensions = jinja2.ext.do
403
context =
404
  section parameter_dict {{ conf_parameter_name }}
405
  import os os
406

407
[{{ section(name) }}]
408 409
< = runzope-base
wrapper-path = ${directory:service-on-watch}/{{ name }}
410 411 412 413
configuration-file = {{ '${' ~ conf_name ~ ':rendered}' }}

[{{ section("promise-" ~ name) }}]
recipe = slapos.cookbook:check_port_listening
414 415
hostname = {{ ipv4 }}
port = {{ port }}
416 417
path = ${directory:promises}/{{ name }}

418 419 420 421 422 423 424 425 426
{% set extra_path_list = [] -%}
{% set shell_escaped_extra_path_list = [] -%}
{% for line in parameter_dict['extra-path-list'].splitlines() -%}
{%   set line = line.strip() -%}
{%   do extra_path_list.append(line) -%}
{%   do shell_escaped_extra_path_list.append(line.replace("\x27", "\x27\\\x27\x27")) -%}
{% endfor -%}
[{{ section("promise-" ~ name ~ "-is-running-actual-product") }}]
recipe = slapos.cookbook:wrapper
427
command-line = '{{ parameter_dict['bin-directory'] }}/is-process-older-than-dependency-set' '{{ "${" ~ conf_parameter_name ~ ":pid-file}" }}' {{ " ".join(shell_escaped_extra_path_list) }}
428 429
wrapper-path = ${directory:promises}/{{ name }}-is-running-actual-product

430 431 432 433 434 435
{% if use_ipv6 -%}
[{{ zope_tunnel_section_name }}]
< = ipv6toipv4-base
base-name = {{ zope_tunnel_base_name }}
ipv6-port = {{ port }}
ipv4-port = {{ port }}
436
{%   do publish_list.append(("[" ~ ipv6 ~ "]:" ~ port, thread_amount, webdav)) -%}
437 438 439 440 441 442 443

[{{ section("promise-tunnel-" ~ name) }}]
recipe = slapos.cookbook:check_port_listening
hostname = {{ '${' ~ zope_tunnel_section_name ~ ':ipv6}' }}
port = {{ '${' ~ zope_tunnel_section_name ~ ':ipv6-port}' }}
path = ${directory:promises}/{{ zope_tunnel_base_name }}
{% else -%}
444
{%   do publish_list.append((ipv4 ~ ":" ~ port, thread_amount, webdav)) -%}
445 446 447 448 449
{% endif -%}

[{{ section('logrotate-entry-' ~ name) }}]
< = logrotate-entry-base
name = {{ name }}
450
log = {{ '${' ~ conf_parameter_name ~ ':event-log}' }} {{ '${' ~ conf_parameter_name ~ ':z2-log}' }} {{ '${' ~ conf_parameter_name ~ ':longrequest-logger-file}' }} {{ ' '.join(log_list) }}
451
post = test ! -s {{ '${' ~ conf_parameter_name ~ ':pid-file}' }} || {{ bin_directory }}/slapos-kill --pidfile {{ '${' ~ conf_parameter_name ~ ':pid-file}' }} -s USR2
452 453
{% endmacro -%}

454
{% for i in instance_index_list -%}
455 456
{{   zope(
       i,
457
       next_port(),
458 459 460
       slapparameter_dict['longrequest-logger-timeout'],
       slapparameter_dict['longrequest-logger-interval'],
     ) }}
461 462
{% endfor -%}

463 464 465 466 467 468 469 470 471 472
[{{ section("watch_activities") }}]
<= userhosts-wrapper-base
environment +=
  MYSQL=${binary-link:target-directory}/mysql
wrapped-command-line = {{ parameter_dict['erp5-location'] }}/product/CMFActivity/bin/${:_buildout_section_name_} "-h erp5-catalog-0 -P  {{ mysql.port }}  -u {{ mysql.username }} -p{{ mysql.password}} {{ mysql_db }}" 5 3600
wrapper-path = ${buildout:bin-directory}/${:_buildout_section_name_}

[{{ section("wait_activities") }}]
<= watch_activities

473
[publish]
474 475
recipe = slapos.cookbook:publish.serialised
zope-address-list = {{ dumps(publish_list) }}
476 477 478 479 480 481 482 483
{#
Note: hosts_dist is generated at zope level rather than at erp5 (root partition)
level, as it is easier: we can access urls as python values trivially here.
This has the downside of making each zope partition publish the (hopefuly) same
dict toward erp5 partition, violating the DRY principle and making the intent
hard to guess.
-#}
hosts-dict = {{ dumps(hosts_dict) }}
484
monitor-base-url = ${monitor-publish-parameters:monitor-base-url}
485

486 487
[monitor-instance-parameter]
monitor-httpd-ipv6 = {{ (ipv6_set | list)[0] }}
488
monitor-httpd-port = {{ next_port() }}
489
monitor-title = {{ slapparameter_dict['name'] }}
490
password = {{ slapparameter_dict['monitor-passwd'] }}
491

492
[buildout]
493 494 495
extends =
  {{ logrotate_cfg }}
  {{ parameter_dict['template-monitor'] }}
496 497
parts +=
  {{ part_list | join('\n  ') }}
498
  publish