Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
slapos
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
0
Merge Requests
0
Analytics
Analytics
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Commits
Issue Boards
Open sidebar
isaak yansane-sisk
slapos
Commits
7e37a311
Commit
7e37a311
authored
May 31, 2011
by
Łukasz Nowak
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Imported from
https://svn.erp5.org/repos/sanef/trunk/utils/slapos.recipe.bef_erp5/
parent
ceaed3af
Changes
3
Hide whitespace changes
Inline
Side-by-side
Showing
3 changed files
with
492 additions
and
0 deletions
+492
-0
setup.py
setup.py
+1
-0
slapos/recipe/bef_erp5/__init__.py
slapos/recipe/bef_erp5/__init__.py
+305
-0
slapos/recipe/bef_erp5/template/my.cnf.in
slapos/recipe/bef_erp5/template/my.cnf.in
+186
-0
No files found.
setup.py
View file @
7e37a311
...
@@ -41,6 +41,7 @@ setup(name=name,
...
@@ -41,6 +41,7 @@ setup(name=name,
zip_safe
=
True
,
zip_safe
=
True
,
entry_points
=
{
entry_points
=
{
'zc.buildout'
:
[
'zc.buildout'
:
[
'bef_erp5 = slapos.recipe.bef_erp5:Recipe'
,
'build = slapos.recipe.build:Script'
,
'build = slapos.recipe.build:Script'
,
'buildcmmi = slapos.recipe.build:Cmmi'
,
'buildcmmi = slapos.recipe.build:Cmmi'
,
'download = slapos.recipe.download:Recipe'
,
'download = slapos.recipe.download:Recipe'
,
...
...
slapos/recipe/bef_erp5/__init__.py
0 → 100644
View file @
7e37a311
##############################################################################
#
# Copyright (c) 2010 Vifib SARL and Contributors. All Rights Reserved.
#
# WARNING: This program as such is intended to be used by professional
# programmers who take the whole responsibility of assessing all potential
# consequences resulting from its eventual inadequacies and bugs
# End users who are looking for a ready-to-use solution with commercial
# guarantees and support are strongly adviced to contract a Free Software
# Service Company
#
# This program is Free Software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 3
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
#
##############################################################################
import
slapos.recipe.erp5
import
os
import
pkg_resources
import
zc.buildout
import
sys
import
netaddr
BEF_TIMEZONE
=
'BST'
def
validLoopBackAddress
(
ip
):
if
netaddr
.
IPAddress
(
ip
).
is_loopback
():
return
True
else
:
return
False
def
validPublicAddress
(
ip
):
return
not
validLoopBackAddress
(
ip
)
class
Recipe
(
slapos
.
recipe
.
erp5
.
Recipe
):
site_id
=
'erp5'
def
getLocalIPv4Address
(
self
):
"""Returns local IPv4 address available on partition"""
# XXX: Lack checking for locality of address
if
self
.
development
:
# XXX: Development superhack.
return
slapos
.
recipe
.
erp5
.
Recipe
.
getLocalIPv4Address
(
self
)
return
self
.
_getIpAddress
(
validLoopBackAddress
)
def
getGlobalIPv6Address
(
self
):
"""Returns global IPv6 address available on partition"""
if
self
.
development
:
# XXX: Development superhack.
return
slapos
.
recipe
.
erp5
.
Recipe
.
getGlobalIPv6Address
(
self
)
# XXX: Lack checking for globality of address
return
self
.
_getIpAddress
(
validPublicAddress
)
def
_getZeoClusterDict
(
self
):
site_path
=
'/erp5/'
return
{
'/'
:
(
self
.
_requestZeoFileStorage
(
'Zeo Server 1'
,
'main'
),
site_path
+
'account_module'
),
site_path
+
'portal_activities'
:
(
self
.
_requestZeoFileStorage
(
'Zeo Server 1'
,
'portal_activities'
),
None
),
site_path
+
'task_report_module'
:
(
self
.
_requestZeoFileStorage
(
'Zeo Server 1'
,
'task_report_module'
),
None
),
site_path
+
'video_request_module'
:
(
self
.
_requestZeoFileStorage
(
'Zeo Server 1'
,
'video_request_module'
),
None
),
site_path
+
'bug_module'
:
(
self
.
_requestZeoFileStorage
(
'Zeo Server 1'
,
'bug_module'
),
None
),
site_path
+
'portal_simulation'
:
(
self
.
_requestZeoFileStorage
(
'Zeo Server 1'
,
'portal_simulation'
),
None
),
site_path
+
'notice_transfer_request_module'
:
(
self
.
_requestZeoFileStorage
(
'Zeo Server 1'
,
'notice_transfer_request_module'
),
None
),
site_path
+
'accounting_module'
:
(
self
.
_requestZeoFileStorage
(
'Zeo Server 1'
,
'accounting_module'
),
None
),
site_path
+
'task_module'
:
(
self
.
_requestZeoFileStorage
(
'Zeo Server 1'
,
'task_module'
),
None
),
site_path
+
'video_module'
:
(
self
.
_requestZeoFileStorage
(
'Zeo Server 2'
,
'video_module'
),
None
),
site_path
+
'event_module'
:
(
self
.
_requestZeoFileStorage
(
'Zeo Server 3'
,
'event_module'
),
None
),
site_path
+
'support_request_module'
:
(
self
.
_requestZeoFileStorage
(
'Zeo Server 3'
,
'support_request_module'
),
None
),
site_path
+
'person_module'
:
(
self
.
_requestZeoFileStorage
(
'Zeo Server 4'
,
'person_module'
),
None
),
site_path
+
'organisation_module'
:
(
self
.
_requestZeoFileStorage
(
'Zeo Server 4'
,
'organisation_module'
),
None
),
site_path
+
'portal_synchronizations'
:
(
self
.
_requestZeoFileStorage
(
'Zeo Server 4'
,
'portal_synchronizations'
),
None
),
site_path
+
'scanned_document_module'
:
(
self
.
_requestZeoFileStorage
(
'Zeo Server 5'
,
'scanned_document_module'
),
None
),
site_path
+
'item_module'
:
(
self
.
_requestZeoFileStorage
(
'Zeo Server 5'
,
'item_module'
),
None
),
site_path
+
'document_module'
:
(
self
.
_requestZeoFileStorage
(
'Zeo Server 5'
,
'document_module'
),
None
),
site_path
+
'image_module'
:
(
self
.
_requestZeoFileStorage
(
'Zeo Server 5'
,
'image_module'
),
None
),
site_path
+
'external_source_module'
:
(
self
.
_requestZeoFileStorage
(
'Zeo Server 5'
,
'external_source_module'
),
None
),
}
def
installProductionMysql
(
self
):
mysql_conf
=
self
.
installMysqlServer
(
self
.
getLocalIPv4Address
(),
45678
,
template_filename
=
pkg_resources
.
resource_filename
(
__name__
,
'template/my.cnf.in'
),
parallel_test_database_amount
=
0
,
mysql_conf
=
dict
(
innodb_buffer_pool_size
=
'10G'
))
self
.
setConnectionDict
(
dict
(
mysql_url
=
'%(mysql_database)s@%(ip)s:%(tcp_port)s %(mysql_user)s %(mysql_password)s'
%
mysql_conf
,
))
return
self
.
path_list
def
installProductionApplication
(
self
):
site_check_path
=
'/%s/getId'
%
self
.
site_id
ca_conf
=
self
.
installCertificateAuthority
()
backend_key
,
backend_certificate
=
self
.
requestCertificate
(
'Login Based Access'
)
memcached_conf
=
self
.
installMemcached
(
ip
=
self
.
getLocalIPv4Address
(),
port
=
11000
)
conversion_server_conf
=
self
.
installConversionServer
(
self
.
getLocalIPv4Address
(),
23000
,
23060
)
user
,
password
=
self
.
installERP5
()
ip
=
self
.
getLocalIPv4Address
()
mount_point_zeo_dict
=
self
.
_getZeoClusterDict
()
zeo_conf
=
self
.
installZeo
(
ip
)
zodb_configuration_list
=
[]
known_tid_storage_identifier_dict
=
{}
for
mount_point
,
(
storage_dict
,
check_path
)
in
mount_point_zeo_dict
.
iteritems
():
known_tid_storage_identifier_dict
[
(((
storage_dict
[
'ip'
],
storage_dict
[
'port'
]),),
storage_dict
[
'storage_name'
])
]
=
(
zeo_conf
[
storage_dict
[
'storage_name'
]][
'path'
],
check_path
or
mount_point
)
zodb_configuration_list
.
append
(
self
.
substituteTemplate
(
self
.
getTemplateFilename
(
'zope-zeo-snippet.conf.in'
),
dict
(
storage_name
=
storage_dict
[
'storage_name'
],
address
=
'%s:%s'
%
(
storage_dict
[
'ip'
],
storage_dict
[
'port'
]),
mount_point
=
mount_point
)))
tidstorage_config
=
dict
(
host
=
self
.
getLocalIPv4Address
(),
port
=
'6001'
)
zodb_configuration_string
=
'
\
n
'
.
join
(
zodb_configuration_list
)
zope_port
=
12000
# One Distribution Node
zope_port
+=
1
self
.
installZope
(
ip
,
zope_port
,
'zope_distribution'
,
with_timerservice
=
True
,
zodb_configuration_string
=
zodb_configuration_string
,
tidstorage_config
=
tidstorage_config
,
zope_environment
=
dict
(
TZ
=
BEF_TIMEZONE
))
# Two Activity Nodes
for
i
in
(
1
,
2
):
zope_port
+=
1
self
.
installZope
(
ip
,
zope_port
,
'zope_activity_%s'
%
i
,
with_timerservice
=
True
,
zodb_configuration_string
=
zodb_configuration_string
,
tidstorage_config
=
tidstorage_config
,
zope_environment
=
dict
(
TZ
=
BEF_TIMEZONE
))
# Eight Working Nodes (Human access)
login_url_list
=
[]
for
i
in
(
1
,
2
,
3
,
4
,
5
,
6
,
7
,
8
):
zope_port
+=
1
login_url_list
.
append
(
self
.
installZope
(
ip
,
zope_port
,
'zope_login_%s'
%
i
,
with_timerservice
=
False
,
zodb_configuration_string
=
zodb_configuration_string
,
tidstorage_config
=
tidstorage_config
,
zope_environment
=
dict
(
TZ
=
BEF_TIMEZONE
)))
login_haproxy
=
self
.
installHaproxy
(
ip
,
15001
,
'login'
,
site_check_path
,
login_url_list
)
# One Web Node
zope_port
+=
1
web_url_list
=
[
self
.
installZope
(
ip
,
zope_port
,
'zope_web'
,
with_timerservice
=
True
,
zodb_configuration_string
=
zodb_configuration_string
,
tidstorage_config
=
tidstorage_config
,
thread_amount
=
10
,
zope_environment
=
dict
(
TZ
=
BEF_TIMEZONE
))]
web_haproxy
=
self
.
installHaproxy
(
ip
,
15002
,
'web'
,
site_check_path
,
web_url_list
)
apache_web
=
self
.
installBackendApache
(
self
.
getGlobalIPv6Address
(),
15001
,
web_haproxy
,
backend_key
,
backend_certificate
,
suffix
=
'_web'
)
# One Admin Node
zope_port
+=
1
admin_url_list
=
[
self
.
installZope
(
ip
,
zope_port
,
'zope_admin'
,
with_timerservice
=
True
,
zodb_configuration_string
=
zodb_configuration_string
,
tidstorage_config
=
tidstorage_config
,
zope_environment
=
dict
(
TZ
=
BEF_TIMEZONE
))]
admin_haproxy
=
self
.
installHaproxy
(
ip
,
15003
,
'admin'
,
site_check_path
,
admin_url_list
)
apache_admin
=
self
.
installBackendApache
(
self
.
getGlobalIPv6Address
(),
15002
,
admin_haproxy
,
backend_key
,
backend_certificate
,
suffix
=
'_admin'
)
self
.
installTidStorage
(
tidstorage_config
[
'host'
],
tidstorage_config
[
'port'
],
known_tid_storage_identifier_dict
,
'http://'
+
login_haproxy
)
apache_login
=
self
.
installBackendApache
(
self
.
getGlobalIPv6Address
(),
15000
,
login_haproxy
,
backend_key
,
backend_certificate
,
access_control_string
=
'%s'
%
self
.
getGlobalIPv6Address
())
frontend_key
,
frontend_certificate
=
self
.
requestCertificate
(
self
.
parameter_dict
[
'frontend_name'
])
login_frontend
=
self
.
installFrontendZopeApache
(
self
.
getGlobalIPv6Address
(),
18000
,
self
.
parameter_dict
[
'frontend_name'
],
self
.
parameter_dict
[
'frontend_path'
],
apache_login
,
self
.
parameter_dict
[
'backend_path'
],
frontend_key
,
frontend_certificate
,
access_control_string
=
self
.
parameter_dict
[
'frontend_acl_string'
])
memcached_conf
=
self
.
installMemcached
(
ip
=
self
.
getLocalIPv4Address
(),
port
=
11000
)
kumo_conf
=
self
.
installKumo
(
self
.
getLocalIPv4Address
())
self
.
setConnectionDict
(
dict
(
site_url
=
'https://%s:%s%s'
%
(
self
.
getGlobalIPv6Address
(),
18000
,
self
.
parameter_dict
[
'frontend_path'
]),
site_web_url
=
apache_web
,
site_admin_url
=
apache_admin
,
site_user
=
user
,
site_password
=
password
,
memcached_url
=
memcached_conf
[
'memcached_url'
],
kumo_url
=
kumo_conf
[
'kumo_address'
],
conversion_server_url
=
'%(conversion_server_ip)s:%(conversion_server_port)s'
%
conversion_server_conf
,
# openssl binary might be removed, as soon as CP environment will be
# fully controlled
openssl_binary
=
self
.
options
[
'openssl_binary'
],
# As soon as there would be Vifib ERP5 configuration and possibility to
# call it over the network this can be removed
certificate_authority_path
=
ca_conf
[
'certificate_authority_path'
],
))
return
self
.
path_list
def
installDevelopmentEnvironment
(
self
):
ca_conf
=
self
.
installCertificateAuthority
()
memcached_conf
=
self
.
installMemcached
(
ip
=
self
.
getLocalIPv4Address
(),
port
=
11000
)
conversion_server_conf
=
self
.
installConversionServer
(
self
.
getLocalIPv4Address
(),
23000
,
23060
)
mysql_conf
=
self
.
installMysqlServer
(
self
.
getLocalIPv4Address
(),
45678
,
template_filename
=
pkg_resources
.
resource_filename
(
__name__
,
'template/my.cnf.in'
),
parallel_test_database_amount
=
0
,
mysql_conf
=
dict
(
innodb_buffer_pool_size
=
'1G'
))
kumo_conf
=
self
.
installKumo
(
self
.
getLocalIPv4Address
())
user
,
password
=
self
.
installERP5
()
self
.
installTestRunner
(
ca_conf
,
mysql_conf
,
conversion_server_conf
,
memcached_conf
,
kumo_conf
)
ip
=
self
.
getLocalIPv4Address
()
zope_port
=
'18080'
zodb_dir
=
os
.
path
.
join
(
self
.
data_root_directory
,
'zodb'
)
self
.
_createDirectory
(
zodb_dir
)
zodb_root_path
=
os
.
path
.
join
(
zodb_dir
,
'root.fs'
)
self
.
installZope
(
ip
,
zope_port
,
'zope_development'
,
zodb_configuration_string
=
self
.
substituteTemplate
(
self
.
getTemplateFilename
(
'zope-zodb-snippet.conf.in'
),
dict
(
zodb_root_path
=
zodb_root_path
)),
thread_amount
=
8
,
with_timerservice
=
True
,
zope_environment
=
dict
(
TZ
=
BEF_TIMEZONE
))
self
.
setConnectionDict
(
dict
(
site_user
=
user
,
site_password
=
password
,
memcached_url
=
memcached_conf
[
'memcached_url'
],
kumo_url
=
kumo_conf
[
'kumo_address'
],
conversion_server_url
=
'%(conversion_server_ip)s:%(conversion_server_port)s'
%
conversion_server_conf
,
# openssl binary might be removed, as soon as CP environment will be
# fully controlled
openssl_binary
=
self
.
options
[
'openssl_binary'
],
# As soon as there would be Vifib ERP5 configuration and possibility to
# call it over the network this can be removed
certificate_authority_path
=
ca_conf
[
'certificate_authority_path'
],
# as installERP5Site is not trusted (yet) and this recipe is production
# ready expose more information
mysql_url
=
'%(mysql_database)s@%(ip)s:%(tcp_port)s %(mysql_user)s %(mysql_password)s'
%
mysql_conf
,
development_zope
=
'http://%s:%s/'
%
(
ip
,
zope_port
)
))
return
self
.
path_list
def
_install
(
self
):
self
.
path_list
=
[]
self
.
requirements
,
self
.
ws
=
self
.
egg
.
working_set
([
__name__
])
# self.cron_d is a directory, where cron jobs can be registered
self
.
cron_d
=
self
.
installCrond
()
self
.
logrotate_d
,
self
.
logrotate_backup
=
self
.
installLogrotate
()
self
.
killpidfromfile
=
zc
.
buildout
.
easy_install
.
scripts
(
[(
'killpidfromfile'
,
'slapos.recipe.erp5.killpidfromfile'
,
'killpidfromfile'
)],
self
.
ws
,
sys
.
executable
,
self
.
bin_directory
)[
0
]
self
.
path_list
.
append
(
self
.
killpidfromfile
)
self
.
linkBinary
()
if
self
.
parameter_dict
.
get
(
'development'
,
'false'
).
lower
()
==
'true'
:
self
.
development
=
True
return
self
.
installDevelopmentEnvironment
()
if
self
.
parameter_dict
.
get
(
'production_mysql'
,
'false'
).
lower
()
==
'true'
:
self
.
development
=
False
return
self
.
installProductionMysql
()
if
self
.
parameter_dict
.
get
(
'production_application'
,
'false'
).
lower
()
==
'true'
:
self
.
development
=
False
return
self
.
installProductionApplication
()
raise
NotImplementedError
(
'Flavour of instance have to be given.'
)
slapos/recipe/bef_erp5/template/my.cnf.in
0 → 100644
View file @
7e37a311
# Example MySQL config file for medium systems.
#
# This is for a system with little memory (32M - 64M) where MySQL plays
# an important part, or systems up to 128M where MySQL is used together with
# other programs (such as a web server)
#
# The following options will be passed to all MySQL clients
[client]
user = root
#password = your_password
port = %(tcp_port)s
socket = %(socket)s
# Here follows entries for some specific programs
# The MySQL server
[mysqld]
#user = mysql # not needed in SlapOS
datadir = %(data_directory)s
port = %(tcp_port)s
socket = %(socket)s
pid-file = %(pid_file)s
skip-locking
key_buffer = 128M
max_allowed_packet = 100M
table_cache = 1024
sort_buffer_size = 100M
net_buffer_length = 8K
read_buffer_size = 256K
read_rnd_buffer_size = 512K
myisam_sort_buffer_size = 8M
collation_server = utf8_unicode_ci
character_set_server = utf8
log-slow-queries=%(slow_query_log)s
long_query_time=1
bind-address = %(ip)s # new in SlapOS
log-error = %(error_log)s # new in SlapOS
# max_connections = 256
max_connections = 512
# Default to using old password format for compatibility with old and
# shorter password hash.
# Reference: http://dev.mysql.com/doc/mysql/en/Password_hashing.html
old_passwords
# Don't listen on a TCP/IP port at all. This can be a security enhancement,
# if all processes that need to connect to mysqld run on the same host.
# All interaction with mysqld must be made via Unix sockets or named pipes.
# Note that using this option without enabling named pipes on Windows
# (via the "enable-named-pipe" option) will render mysqld useless!
#
# skip-networking
# Replication Master Server (default)
# binary logging is required for replication
#log-bin=mysql-bin
# required unique id between 1 and 2^32 - 1
# defaults to 1 if master-host is not set
# but will not function as a master if omitted
server-id = 1
# Replication Slave (comment out master section to use this)
#
# To configure this host as a replication slave, you can choose between
# two methods :
#
# 1) Use the CHANGE MASTER TO command (fully described in our manual) -
# the syntax is:
#
# CHANGE MASTER TO MASTER_HOST=<host>, MASTER_PORT=<port>,
# MASTER_USER=<user>, MASTER_PASSWORD=<password> ;
#
# where you replace <host>, <user>, <password> by quoted strings and
# <port> by the master's port number (%(tcp_port)s by default).
#
# Example:
#
# CHANGE MASTER TO MASTER_HOST='125.564.12.1', MASTER_PORT=%(tcp_port)s,
# MASTER_USER='joe', MASTER_PASSWORD='secret';
#
# OR
#
# 2) Set the variables below. However, in case you choose this method, then
# start replication for the first time (even unsuccessfully, for example
# if you mistyped the password in master-password and the slave fails to
# connect), the slave will create a master.info file, and any later
# change in this file to the variables' values below will be ignored and
# overridden by the content of the master.info file, unless you shutdown
# the slave server, delete master.info and restart the slaver server.
# For that reason, you may want to leave the lines below untouched
# (commented) and instead use CHANGE MASTER TO (see above)
#
# required unique id between 2 and 2^32 - 1
# (and different from the master)
# defaults to 2 if master-host is set
# but will not function as a slave if omitted
#server-id = 2
#
# The replication master for this slave - required
#master-host = <hostname>
#
# The username the slave will use for authentication when connecting
# to the master - required
#master-user = <username>
#
# The password the slave will authenticate with when connecting to
# the master - required
#master-password = <password>
#
# The port the master is listening on.
# optional - defaults to %(tcp_port)s
#master-port = <port>
#
# binary logging - not required for slaves, but recommended
#log-bin=mysql-bin
# Point the following paths to different dedicated disks
#tmpdir = /tmp/
#log-update = /path-to-dedicated-directory/hostname
# Uncomment the following if you are using BDB tables
#bdb_cache_size = 4M
#bdb_max_lock = 10000
# Uncomment the following if you are using InnoDB tables
#innodb_data_home_dir = /var/lib/mysql/
#innodb_data_file_path = ibdata1:10M:autoextend
#innodb_log_group_home_dir = /var/lib/mysql/
#innodb_log_arch_dir = /var/lib/mysql/
# You can set .._buffer_pool_size up to 50 - 80 %%
# of RAM but beware of setting memory usage too high
innodb_buffer_pool_size = %(innodb_buffer_pool_size)s
innodb_additional_mem_pool_size = 40M
# Set .._log_file_size to 25 %% of buffer pool size
#innodb_log_file_size = 5M
#innodb_log_buffer_size = 8M
#innodb_flush_log_at_trx_commit = 1
#innodb_lock_wait_timeout = 50
innodb_locks_unsafe_for_binlog=1
#bind-address=192.168.100.1
## Options for mysqld process:
#ndbcluster # run NDB engine
#ndb-connectstring=192.168.0.10 # location of MGM node
## Options for ndbd process:
#[mysql_cluster]
#ndb-connectstring=192.168.0.10 # location of MGM node
#
# Use utf8 as the default character set.
default-character-set=utf8
# Ignore client information and use the default server character set.
skip-character-set-client-handshake
[mysqldump]
quick
max_allowed_packet = 16M
[mysql]
no-auto-rehash
# Remove the next comment character if you are not familiar with SQL
#safe-updates
default-character-set = utf8
[isamchk]
key_buffer = 20M
sort_buffer_size = 20M
read_buffer = 2M
write_buffer = 2M
[myisamchk]
key_buffer = 20M
sort_buffer_size = 20M
read_buffer = 2M
write_buffer = 2M
[mysqlhotcopy]
interactive-timeout
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment