Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
S
slapos-mynij-dev
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
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Analytics
Analytics
CI / CD
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Mynij
slapos-mynij-dev
Commits
bc357712
Commit
bc357712
authored
Apr 03, 2015
by
Alain Takoudjou
Browse files
Options
Browse Files
Download
Plain Diff
Merge branch 're6st-master' into master
parents
23d31649
484c0931
Changes
14
Show whitespace changes
Inline
Side-by-side
Showing
14 changed files
with
1189 additions
and
2 deletions
+1189
-2
component/re6stnet/buildout.cfg
component/re6stnet/buildout.cfg
+3
-2
setup.py
setup.py
+1
-0
slapos/recipe/re6stnet/__init__.py
slapos/recipe/re6stnet/__init__.py
+220
-0
slapos/recipe/re6stnet/re6stnet.py
slapos/recipe/re6stnet/re6stnet.py
+183
-0
slapos/test/recipe/test_re6stnet.py
slapos/test/recipe/test_re6stnet.py
+184
-0
software/re6stnet/apache.conf.in
software/re6stnet/apache.conf.in
+57
-0
software/re6stnet/instance-logrotate-base.cfg.in
software/re6stnet/instance-logrotate-base.cfg.in
+50
-0
software/re6stnet/instance-re6stnet-input-schema.json
software/re6stnet/instance-re6stnet-input-schema.json
+17
-0
software/re6stnet/instance-re6stnet-output-schema.json
software/re6stnet/instance-re6stnet-output-schema.json
+11
-0
software/re6stnet/instance-re6stnet.cfg.in
software/re6stnet/instance-re6stnet.cfg.in
+214
-0
software/re6stnet/instance.cfg.in
software/re6stnet/instance.cfg.in
+50
-0
software/re6stnet/re6st-registry.conf.in
software/re6stnet/re6st-registry.conf.in
+11
-0
software/re6stnet/software.cfg
software/re6stnet/software.cfg
+167
-0
software/re6stnet/software.cfg.json
software/re6stnet/software.cfg.json
+21
-0
No files found.
component/re6stnet/buildout.cfg
View file @
bc357712
...
@@ -28,8 +28,9 @@ stop-on-error = true
...
@@ -28,8 +28,9 @@ stop-on-error = true
dir = ${re6stnet-repository:location}
dir = ${re6stnet-repository:location}
command =
command =
rm -f "${:dir}/re6stconf.py" && ln -s re6st-conf "${:dir}/re6stconf.py"
rm -f "${:dir}/re6stconf.py" && ln -s re6st-conf "${:dir}/re6stconf.py"
rm -f "${:dir}/re6stregister.py" && ln -s re6st-conf "${:dir}/re6stregister.py"
rm -f "${:dir}/re6stregistry.py" && ln -s re6st-registry "${:dir}/re6stregistry.py"
rm -f "${:dir}/re6stnet.py" && ln -s re6st-conf "${:dir}/re6stnet.py"
rm -f "${:dir}/re6stnet.py" && ln -s re6stnet "${:dir}/re6stnet.py"
sed -i 's#("git",)#("${git:location}/bin/git",)#' ${:dir}/re6st/version.py
update-command = ${:command}
update-command = ${:command}
...
...
setup.py
View file @
bc357712
...
@@ -174,6 +174,7 @@ setup(name=name,
...
@@ -174,6 +174,7 @@ setup(name=name,
'request.serialised = slapos.recipe.request:Serialised'
,
'request.serialised = slapos.recipe.request:Serialised'
,
'request.edge = slapos.recipe.request:RequestEdge'
,
'request.edge = slapos.recipe.request:RequestEdge'
,
'requestoptional = slapos.recipe.request:RequestOptional'
,
'requestoptional = slapos.recipe.request:RequestOptional'
,
're6stnet.registry = slapos.recipe.re6stnet:Recipe'
,
'reverseproxy.nginx = slapos.recipe.reverse_proxy_nginx:Recipe'
,
'reverseproxy.nginx = slapos.recipe.reverse_proxy_nginx:Recipe'
,
'seleniumrunner = slapos.recipe.seleniumrunner:Recipe'
,
'seleniumrunner = slapos.recipe.seleniumrunner:Recipe'
,
'sheepdogtestbed = slapos.recipe.sheepdogtestbed:SheepDogTestBed'
,
'sheepdogtestbed = slapos.recipe.sheepdogtestbed:SheepDogTestBed'
,
...
...
slapos/recipe/re6stnet/__init__.py
0 → 100644
View file @
bc357712
##############################################################################
#
# 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
subprocess
from
slapos.recipe.librecipe
import
GenericBaseRecipe
import
socket
import
struct
import
os
import
string
,
random
import
json
import
traceback
from
slapos
import
slap
class
Recipe
(
GenericBaseRecipe
):
def
__init__
(
self
,
buildout
,
name
,
options
):
"""Default initialisation"""
self
.
slap
=
slap
.
slap
()
# SLAP related information
slap_connection
=
buildout
[
'slap-connection'
]
self
.
computer_id
=
slap_connection
[
'computer-id'
]
self
.
computer_partition_id
=
slap_connection
[
'partition-id'
]
self
.
server_url
=
slap_connection
[
'server-url'
]
self
.
software_release_url
=
slap_connection
[
'software-release-url'
]
self
.
key_file
=
slap_connection
.
get
(
'key-file'
)
self
.
cert_file
=
slap_connection
.
get
(
'cert-file'
)
return
GenericBaseRecipe
.
__init__
(
self
,
buildout
,
name
,
options
)
def
getSerialFromIpv6
(
self
,
ipv6
):
prefix
=
ipv6
.
split
(
'/'
)[
0
].
lower
()
hi
,
lo
=
struct
.
unpack
(
'!QQ'
,
socket
.
inet_pton
(
socket
.
AF_INET6
,
prefix
))
ipv6_int
=
(
hi
<<
64
)
|
lo
serial
=
'0x1%x'
%
ipv6_int
# delete non significant part
for
part
in
prefix
.
split
(
':'
)[::
-
1
]:
if
part
:
for
i
in
[
'0'
]
*
(
4
-
len
(
part
)):
part
=
i
+
part
serial
=
serial
.
split
(
part
)[
0
]
+
part
break
return
serial
def
generateCertificate
(
self
):
key_file
=
self
.
options
[
'key-file'
].
strip
()
cert_file
=
self
.
options
[
'cert-file'
].
strip
()
if
not
os
.
path
.
exists
(
key_file
):
serial
=
self
.
getSerialFromIpv6
(
self
.
options
[
'ipv6-prefix'
].
strip
())
key_command
=
[
self
.
options
[
'openssl-bin'
],
'genrsa'
,
'-out'
,
'%s'
%
key_file
,
self
.
options
[
'key-size'
]]
#'-config', openssl_configuration
cert_command
=
[
self
.
options
[
'openssl-bin'
],
'req'
,
'-nodes'
,
'-new'
,
'-x509'
,
'-batch'
,
'-key'
,
'%s'
%
key_file
,
'-set_serial'
,
'%s'
%
serial
,
'-days'
,
'3650'
,
'-out'
,
'%s'
%
cert_file
]
subprocess
.
check_call
(
key_command
)
subprocess
.
check_call
(
cert_command
)
def
generateSlaveTokenList
(
self
,
slave_instance_list
,
token_file
):
to_remove_dict
=
{}
to_add_dict
=
{}
token_dict
=
self
.
loadJsonFile
(
token_file
)
reference_list
=
[
slave_instance
.
get
(
'slave_reference'
)
for
slave_instance
in
slave_instance_list
]
for
reference
in
reference_list
:
if
not
reference
in
token_dict
:
# we generate new token
number
=
reference
.
split
(
'-'
)[
1
]
new_token
=
number
+
''
.
join
(
random
.
sample
(
string
.
ascii_lowercase
,
15
))
token_dict
[
reference
]
=
new_token
to_add_dict
[
reference
]
=
new_token
for
reference
in
token_dict
.
keys
():
if
not
reference
in
reference_list
:
# This slave instance is destroyed ?
to_remove_dict
[
reference
]
=
token_dict
.
pop
(
reference
)
return
token_dict
,
to_add_dict
,
to_remove_dict
def
loadJsonFile
(
self
,
path
):
if
os
.
path
.
exists
(
path
):
with
open
(
path
,
'r'
)
as
f
:
content
=
f
.
read
()
return
json
.
loads
(
content
)
else
:
return
{}
def
writeFile
(
self
,
path
,
data
):
with
open
(
path
,
'w'
)
as
f
:
f
.
write
(
data
)
return
path
def
readFile
(
self
,
path
):
if
os
.
path
.
exists
(
path
):
with
open
(
path
,
'r'
)
as
f
:
content
=
f
.
read
()
return
content
return
''
def
install
(
self
):
path_list
=
[]
token_save_path
=
os
.
path
.
join
(
self
.
options
[
'conf-dir'
],
'token.json'
)
token_list_path
=
self
.
options
[
'token-dir'
]
self
.
generateCertificate
()
wrapper
=
self
.
createWrapper
(
name
=
self
.
options
[
'wrapper'
],
command
=
self
.
options
[
'command'
],
parameters
=
[
'@%s'
%
self
.
options
[
'config-file'
]])
path_list
.
append
(
wrapper
)
slave_list
=
json
.
loads
(
self
.
options
[
'slave-instance-list'
])
registry_url
=
'http://%s:%s/'
%
(
self
.
options
[
'ipv4'
],
self
.
options
[
'port'
])
token_dict
,
add_token_dict
,
rm_token_dict
=
self
.
generateSlaveTokenList
(
slave_list
,
token_save_path
)
# write request add token
for
reference
in
add_token_dict
:
path
=
os
.
path
.
join
(
token_list_path
,
'%s.add'
%
reference
)
if
not
os
.
path
.
exists
(
path
):
self
.
createFile
(
path
,
add_token_dict
[
reference
])
# write request remove token
for
reference
in
rm_token_dict
:
path
=
os
.
path
.
join
(
token_list_path
,
'%s.remove'
%
reference
)
if
not
os
.
path
.
exists
(
path
):
self
.
createFile
(
path
,
rm_token_dict
[
reference
])
# remove request add file if exists
add_path
=
os
.
path
.
join
(
token_list_path
,
'%s.add'
%
reference
)
if
os
.
path
.
exists
(
add_path
):
os
.
unlink
(
add_path
)
self
.
createFile
(
token_save_path
,
json
.
dumps
(
token_dict
))
service_dict
=
dict
(
token_base_path
=
token_list_path
,
token_json
=
token_save_path
,
db
=
self
.
options
[
'db-path'
],
partition_id
=
self
.
computer_partition_id
,
computer_id
=
self
.
computer_id
,
registry_url
=
registry_url
)
service_dict
[
'server_url'
]
=
self
.
server_url
service_dict
[
'cert_file'
]
=
self
.
cert_file
service_dict
[
'key_file'
]
=
self
.
key_file
request_add
=
self
.
createPythonScript
(
self
.
options
[
'manager-wrapper'
].
strip
(),
'%s.re6stnet.manage'
%
__name__
,
service_dict
)
path_list
.
append
(
request_add
)
request_drop
=
self
.
createPythonScript
(
self
.
options
[
'drop-service-wrapper'
].
strip
(),
'%s.re6stnet.requestRemoveToken'
%
__name__
,
service_dict
)
path_list
.
append
(
request_drop
)
request_check
=
self
.
createPythonScript
(
self
.
options
[
'check-service-wrapper'
].
strip
(),
'%s.re6stnet.checkService'
%
__name__
,
service_dict
)
path_list
.
append
(
request_check
)
# Send connection parameters of slave instances
if
token_dict
:
self
.
slap
.
initializeConnection
(
self
.
server_url
,
self
.
key_file
,
self
.
cert_file
)
computer_partition
=
self
.
slap
.
registerComputerPartition
(
self
.
computer_id
,
self
.
computer_partition_id
)
for
slave_reference
,
token
in
token_dict
.
iteritems
():
try
:
status_file
=
os
.
path
.
join
(
token_list_path
,
'%s.status'
%
slave_reference
)
status
=
self
.
readFile
(
status_file
)
or
'New token requested'
msg
=
status
if
status
==
'TOKEN_ADDED'
:
msg
=
'Token is ready for use'
elif
status
==
'TOKEN_USED'
:
msg
=
'Token not available, it has been used to generate re6stnet certificate.'
computer_partition
.
setConnectionDict
(
{
'token'
:
token
,
'1_info'
:
msg
},
slave_reference
)
except
:
self
.
logger
.
fatal
(
"Error while sending slave %s informations: %s"
,
slave_reference
,
traceback
.
format_exc
())
return
path_list
slapos/recipe/re6stnet/re6stnet.py
0 → 100644
View file @
bc357712
# -*- coding: utf-8 -*-
import
logging
import
json
import
os
import
time
import
sqlite3
import
slapos
from
re6st
import
registry
log
=
logging
.
getLogger
(
'SLAPOS-RE6STNET'
)
logging
.
basicConfig
(
level
=
logging
.
DEBUG
)
def
loadJsonFile
(
path
):
if
os
.
path
.
exists
(
path
):
with
open
(
path
,
'r'
)
as
f
:
content
=
f
.
read
()
return
json
.
loads
(
content
)
else
:
return
{}
def
writeFile
(
path
,
data
):
with
open
(
path
,
'w'
)
as
f
:
f
.
write
(
data
)
def
readFile
(
path
):
if
os
.
path
.
exists
(
path
):
with
open
(
path
,
'r'
)
as
f
:
content
=
f
.
read
()
return
content
return
''
def
getDb
(
db_path
):
db
=
sqlite3
.
connect
(
db_path
,
isolation_level
=
None
,
check_same_thread
=
False
)
db
.
text_factory
=
str
return
db
.
cursor
()
def
bang
(
args
):
computer_guid
=
args
[
'computer_id'
]
partition_id
=
args
[
'partition_id'
]
slap
=
slapos
.
slap
.
slap
()
# Redeploy instance to update published information
slap
.
initializeConnection
(
args
[
'server_url'
],
args
[
'key_file'
],
args
[
'cert_file'
])
partition
=
slap
.
registerComputerPartition
(
computer_guid
=
computer_guid
,
partition_id
=
partition_id
)
partition
.
bang
(
message
=
'Published parameters changed!'
)
log
.
info
(
"Bang with message 'parameters changed'..."
)
def
requestAddToken
(
args
,
can_bang
=
True
):
time
.
sleep
(
3
)
registry_url
=
args
[
'registry_url'
]
base_token_path
=
args
[
'token_base_path'
]
path_list
=
[
x
for
x
in
os
.
listdir
(
base_token_path
)
if
x
.
endswith
(
'.add'
)]
if
not
path_list
:
log
.
info
(
"No new token to add. Exiting..."
)
return
client
=
registry
.
RegistryClient
(
registry_url
)
call_bang
=
False
for
reference_key
in
path_list
:
request_file
=
os
.
path
.
join
(
base_token_path
,
reference_key
)
token
=
readFile
(
request_file
)
if
token
:
reference
=
reference_key
.
split
(
'.'
)[
0
]
email
=
'%s@slapos'
%
reference
.
lower
()
try
:
result
=
client
.
requestAddToken
(
token
,
email
)
except
Exception
,
e
:
log
.
debug
(
'Request add token fail for %s...
\
n
%s'
%
(
request_file
,
str
(
e
)))
continue
if
result
and
result
==
token
:
# update information
log
.
info
(
"New token added for slave instance %s. Updating file status..."
%
reference
)
writeFile
(
os
.
path
.
join
(
base_token_path
,
'%s.status'
%
reference
),
'TOKEN_ADDED'
)
os
.
unlink
(
request_file
)
call_bang
=
True
else
:
log
.
debug
(
'Bad token. Request add token fail for %s...'
%
request_file
)
if
can_bang
and
call_bang
:
bang
(
args
)
def
requestRemoveToken
(
args
):
base_token_path
=
args
[
'token_base_path'
]
path_list
=
[
x
for
x
in
os
.
listdir
(
base_token_path
)
if
x
.
endswith
(
'.remove'
)]
if
not
path_list
:
log
.
info
(
"No token to delete. Exiting..."
)
return
client
=
registry
.
RegistryClient
(
args
[
'registry_url'
])
for
reference_key
in
path_list
:
request_file
=
os
.
path
.
join
(
base_token_path
,
reference_key
)
token
=
readFile
(
request_file
)
if
token
:
reference
=
reference_key
.
split
(
'.'
)[
0
]
try
:
result
=
client
.
requestDeleteToken
(
token
)
except
Exception
,
e
:
log
.
debug
(
'Request delete token fail for %s...
\
n
%s'
%
(
request_file
,
str
(
e
)))
continue
if
result
==
'True'
:
# update information
log
.
info
(
"Token deleted for slave instance %s. Clean up file status..."
%
reference
)
os
.
unlink
(
request_file
)
status_file
=
os
.
path
.
join
(
base_token_path
,
'%s.status'
%
reference
)
if
os
.
path
.
exists
(
status_file
):
os
.
unlink
(
status_file
)
else
:
log
.
debug
(
'Request delete token fail for %s...'
%
request_file
)
else
:
log
.
debug
(
'Bad token. Request add token fail for %s...'
%
request_file
)
def
checkService
(
args
,
can_bang
=
True
):
base_token_path
=
args
[
'token_base_path'
]
token_dict
=
loadJsonFile
(
args
[
'token_json'
])
if
not
token_dict
:
return
db
=
getDb
(
args
[
'db'
])
call_bang
=
False
computer_guid
=
args
[
'computer_id'
]
partition_id
=
args
[
'partition_id'
]
slap
=
slapos
.
slap
.
slap
()
# Check token status
for
slave_reference
,
token
in
token_dict
.
iteritems
():
status_file
=
os
.
path
.
join
(
base_token_path
,
'%s.status'
%
slave_reference
)
if
not
os
.
path
.
exists
(
status_file
):
# This token is not added yet!
continue
msg
=
readFile
(
status_file
)
if
msg
==
'TOKEN_USED'
:
continue
# Check if token is not in the database
status
=
False
try
:
token_found
,
=
db
.
execute
(
"SELECT token FROM token WHERE token = ?"
,
(
token
,)).
next
()
if
token_found
==
token
:
status
=
True
except
StopIteration
:
pass
if
not
status
:
# Token is used to register client
call_bang
=
True
try
:
time
.
sleep
(
1
)
writeFile
(
status_file
,
'TOKEN_USED'
)
log
.
info
(
"Token status of %s updated to 'used'."
%
slave_reference
)
except
IOError
,
e
:
# XXX- this file should always exists
log
.
debug
(
'Error when writing in file %s. Clould not update status of %s...'
%
(
status_file
,
slave_reference
))
if
call_bang
and
can_bang
:
bang
(
args
)
def
manage
(
args
):
# Request Add new tokens
requestAddToken
(
args
)
# Request delete removed token
requestRemoveToken
(
args
)
# check status of all token
checkService
(
args
)
slapos/test/recipe/test_re6stnet.py
0 → 100644
View file @
bc357712
import
os
,
time
import
shutil
import
sys
import
tempfile
import
unittest
from
slapos.slap.slap
import
NotFoundError
from
slapos.recipe
import
re6stnet
class
Re6stnetTest
(
unittest
.
TestCase
):
def
setUp
(
self
):
self
.
ssl_dir
=
tempfile
.
mkdtemp
()
self
.
conf_dir
=
tempfile
.
mkdtemp
()
self
.
base_dir
=
tempfile
.
mkdtemp
()
self
.
token_dir
=
tempfile
.
mkdtemp
()
self
.
dir_list
=
[
self
.
ssl_dir
,
self
.
conf_dir
,
self
.
base_dir
,
self
.
token_dir
]
config_file
=
os
.
path
.
join
(
self
.
base_dir
,
'config'
)
with
open
(
config_file
,
'w'
)
as
f
:
f
.
write
(
'port 9201'
)
self
.
options
=
options
=
{
'openssl-bin'
:
'openssl'
,
'key-file'
:
os
.
path
.
join
(
self
.
ssl_dir
,
'cert.key'
),
'cert-file'
:
os
.
path
.
join
(
self
.
ssl_dir
,
'cert.crt'
),
'key-size'
:
'2048'
,
'conf-dir'
:
self
.
conf_dir
,
'token-dir'
:
self
.
token_dir
,
'wrapper'
:
os
.
path
.
join
(
self
.
base_dir
,
'wrapper'
),
'config-file'
:
config_file
,
'ipv4'
:
'127.0.0.1'
,
'port'
:
'9201'
,
'db-path'
:
'/path/to/db'
,
'command'
:
'/path/to/command'
,
'manager-wrapper'
:
os
.
path
.
join
(
self
.
base_dir
,
'manager_wrapper'
),
'drop-service-wrapper'
:
os
.
path
.
join
(
self
.
base_dir
,
'drop_wrapper'
),
'check-service-wrapper'
:
os
.
path
.
join
(
self
.
base_dir
,
'check_wrapper'
),
'slave-instance-list'
:
'{}'
}
def
tearDown
(
self
):
for
path
in
self
.
dir_list
:
if
os
.
path
.
exists
(
path
):
shutil
.
rmtree
(
path
)
def
new_recipe
(
self
):
buildout
=
{
'buildout'
:
{
'bin-directory'
:
''
,
'find-links'
:
''
,
'allow-hosts'
:
''
,
'develop-eggs-directory'
:
''
,
'eggs-directory'
:
''
,
'python'
:
'testpython'
,
},
'testpython'
:
{
'executable'
:
sys
.
executable
,
},
'slap-connection'
:
{
'computer-id'
:
''
,
'partition-id'
:
''
,
'server-url'
:
''
,
'software-release-url'
:
''
,
}
}
options
=
self
.
options
return
re6stnet
.
Recipe
(
buildout
=
buildout
,
name
=
're6stnet'
,
options
=
options
)
def
test_generateCertificates
(
self
):
self
.
options
[
'ipv6-prefix'
]
=
'2001:db8:24::/48'
self
.
options
[
'key-size'
]
=
'2048'
recipe
=
self
.
new_recipe
()
recipe
.
generateCertificate
()
self
.
assertTrue
(
os
.
path
.
exists
(
self
.
options
[
'key-file'
]))
self
.
assertTrue
(
os
.
path
.
exists
(
self
.
options
[
'cert-file'
]))
last_time
=
time
.
ctime
(
os
.
stat
(
self
.
options
[
'key-file'
])[
7
])
recipe
.
generateCertificate
()
self
.
assertTrue
(
os
.
path
.
exists
(
self
.
options
[
'key-file'
]))
this_time
=
time
.
ctime
(
os
.
stat
(
self
.
options
[
'key-file'
])[
7
])
self
.
assertEqual
(
last_time
,
this_time
)
def
test_generateCertificates_other_ipv6
(
self
):
self
.
options
[
'ipv6-prefix'
]
=
'be28:db8:fe6a:d85:4fe:54a:ae:aea/64'
recipe
=
self
.
new_recipe
()
recipe
.
generateCertificate
()
self
.
assertTrue
(
os
.
path
.
exists
(
self
.
options
[
'key-file'
]))
self
.
assertTrue
(
os
.
path
.
exists
(
self
.
options
[
'cert-file'
]))
def
test_install
(
self
):
recipe
=
self
.
new_recipe
()
recipe
.
options
.
update
({
'ipv6-prefix'
:
'2001:db8:24::/48'
,
'slave-instance-list'
:
'''[
{"slave_reference":"SOFTINST-58770"},
{"slave_reference":"SOFTINST-58778"}
]
'''
})
try
:
recipe
.
install
()
except
NotFoundError
:
# Recipe will raise not found error when trying to publish slave informations
pass
self
.
assertItemsEqual
(
os
.
listdir
(
self
.
ssl_dir
),
[
'cert.key'
,
'cert.crt'
])
token_file
=
os
.
path
.
join
(
self
.
options
[
'conf-dir'
],
'token.json'
)
self
.
assertTrue
(
os
.
path
.
exists
(
token_file
))
# token file must contain 2 elements
token_content
=
recipe
.
readFile
(
token_file
)
self
.
assertIn
(
'SOFTINST-58770'
,
token_content
)
self
.
assertIn
(
'SOFTINST-58778'
,
token_content
)
token_dict
=
recipe
.
loadJsonFile
(
token_file
)
self
.
assertEqual
(
len
(
token_dict
),
2
)
self
.
assertTrue
(
token_dict
.
has_key
(
'SOFTINST-58770'
))
self
.
assertTrue
(
token_dict
.
has_key
(
'SOFTINST-58778'
))
self
.
assertItemsEqual
(
os
.
listdir
(
self
.
token_dir
),
[
'SOFTINST-58770.add'
,
'SOFTINST-58778.add'
])
first_add
=
recipe
.
readFile
(
os
.
path
.
join
(
self
.
token_dir
,
'SOFTINST-58770.add'
))
self
.
assertEqual
(
token_dict
[
'SOFTINST-58770'
],
first_add
)
second_add
=
recipe
.
readFile
(
os
.
path
.
join
(
self
.
token_dir
,
'SOFTINST-58778.add'
))
self
.
assertEqual
(
token_dict
[
'SOFTINST-58778'
],
second_add
)
# Remove one element
recipe
.
options
.
update
({
"slave-instance-list"
:
"""[{"slave_reference":"SOFTINST-58770"}]"""
})
try
:
recipe
.
install
()
except
NotFoundError
:
# Recipe will raise not found error when trying to publish slave informations
pass
token_dict
=
recipe
.
loadJsonFile
(
token_file
)
self
.
assertEqual
(
len
(
token_dict
),
1
)
self
.
assertEqual
(
token_dict
[
'SOFTINST-58770'
],
first_add
)
self
.
assertItemsEqual
(
os
.
listdir
(
self
.
token_dir
),
[
'SOFTINST-58770.add'
,
'SOFTINST-58778.remove'
])
second_remove
=
recipe
.
readFile
(
os
.
path
.
join
(
self
.
token_dir
,
'SOFTINST-58778.remove'
))
self
.
assertEqual
(
second_add
,
second_remove
)
def
test_install_empty_slave
(
self
):
recipe
=
self
.
new_recipe
()
recipe
.
options
.
update
({
'ipv6-prefix'
:
'2001:db8:24::/48'
})
recipe
.
install
()
self
.
assertItemsEqual
(
os
.
listdir
(
self
.
ssl_dir
),
[
'cert.key'
,
'cert.crt'
])
token_file
=
os
.
path
.
join
(
self
.
options
[
'conf-dir'
],
'token.json'
)
self
.
assertTrue
(
os
.
path
.
exists
(
token_file
))
token_content
=
recipe
.
readFile
(
token_file
)
self
.
assertEqual
(
token_content
,
'{}'
)
self
.
assertItemsEqual
(
os
.
listdir
(
self
.
options
[
'token-dir'
]),
[])
software/re6stnet/apache.conf.in
0 → 100644
View file @
bc357712
LoadModule unixd_module modules/mod_unixd.so
LoadModule access_compat_module modules/mod_access_compat.so
LoadModule authz_core_module modules/mod_authz_core.so
LoadModule authz_host_module modules/mod_authz_host.so
LoadModule log_config_module modules/mod_log_config.so
LoadModule setenvif_module modules/mod_setenvif.so
LoadModule version_module modules/mod_version.so
LoadModule proxy_module modules/mod_proxy.so
LoadModule proxy_http_module modules/mod_proxy_http.so
LoadModule socache_shmcb_module modules/mod_socache_shmcb.so
LoadModule ssl_module modules/mod_ssl.so
LoadModule mime_module modules/mod_mime.so
#LoadModule dav_module modules/mod_dav.so
#LoadModule dav_fs_module modules/mod_dav_fs.so
LoadModule negotiation_module modules/mod_negotiation.so
LoadModule rewrite_module modules/mod_rewrite.so
LoadModule headers_module modules/mod_headers.so
PidFile "{{ pid_file }}"
ServerAdmin admin@
TypesConfig conf/mime.types
AddType application/x-compress .Z
AddType application/x-gzip .gz .tgz
ServerTokens Prod
ServerSignature Off
TraceEnable Off
ErrorLog "{{ error_log }}"
# Default apache log format with request time in microsecond at the end
LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\" %D" combined
CustomLog "{{ access_log }}" combined
{% if uri_scheme == 'https' -%}
# SSL Configuration
SSLCertificateFile {{ certificate }}
SSLCertificateKeyFile {{ key }}
SSLRandomSeed startup builtin
SSLRandomSeed connect builtin
SSLProtocol ALL -SSLv2
{% endif -%}
<Directory />
Options FollowSymLinks
AllowOverride None
Allow from all
</Directory>
Listen {{ ipv6 }}:{{ apache_port }}
<VirtualHost *:{{ apache_port }}>
{% if uri_scheme == 'https' -%}
SSLEngine On
SSLProxyEngine On
{% endif -%}
ProxyPass / {{ uri_scheme }}://{{ re6st_ipv4 }}:{{ re6st_port }}/
</VirtualHost>
\ No newline at end of file
software/re6stnet/instance-logrotate-base.cfg.in
0 → 100644
View file @
bc357712
[buildout]
parts =
cron-entry-logrotate
[cron]
recipe = slapos.cookbook:cron
cron-entries = ${logrotate-directory:cron-entries}
dcrond-binary = {{ dcron_location }}/sbin/crond
crontabs = ${logrotate-directory:crontabs}
cronstamps = ${logrotate-directory:cronstamps}
catcher = ${cron-simplelogger:wrapper}
binary = ${logrotate-directory:services}/crond
[cron-simplelogger]
recipe = slapos.cookbook:simplelogger
wrapper = ${logrotate-directory:bin}/cron_simplelogger
log = ${logrotate-directory:log}/cron.log
[logrotate]
recipe = slapos.cookbook:logrotate
logrotate-entries = ${logrotate-directory:logrotate-entries}
backup = ${logrotate-directory:logrotate-backup}
logrotate-binary = {{ logrotate_location }}/usr/sbin/logrotate
gzip-binary = {{ gzip_location }}/bin/gzip
gunzip-binary = {{ gzip_location }}/bin/gunzip
wrapper = ${logrotate-directory:bin}/logrotate
conf = ${logrotate-directory:etc}/logrotate.conf
state-file = ${logrotate-directory:srv}/logrotate.status
[cron-entry-logrotate]
recipe = slapos.cookbook:cron.d
cron-entries = ${cron:cron-entries}
name = logrotate
frequency = 0 0 * * *
command = ${logrotate:wrapper}
[logrotate-directory]
recipe = slapos.cookbook:mkdirectory
cron-entries = ${:etc}/cron.d
cronstamps = ${:etc}/cronstamps
crontabs = ${:etc}/crontabs
logrotate-backup = ${:backup}/logrotate
logrotate-entries = ${:etc}/logrotate.d
bin = ${buildout:directory}/bin
srv = ${buildout:directory}/srv
backup = ${:srv}/backup
etc = ${buildout:directory}/etc
services = ${:etc}/service
log = ${buildout:directory}/var/log
software/re6stnet/instance-re6stnet-input-schema.json
0 → 100644
View file @
bc357712
{
"$schema"
:
"http://json-schema.org/draft-04/schema#"
,
"properties"
:
{
"ipv6-prefix"
:
{
"title"
:
"Ipv6 prefix to use to setup the new re6st network"
,
"description"
:
"Prefix ipv6 used by re6st to setup network. It is something like 2001:db8:42::/48"
,
"type"
:
"string"
},
"key-size"
:
{
"title"
:
"Number of bit to use for certificate generation"
,
"description"
:
"Specify the size of certificate generated by re6st. by default, generate 2048-bit key length"
,
"type"
:
"integer"
,
"minimum"
:
1024
,
"default"
:
2048
}
}
}
\ No newline at end of file
software/re6stnet/instance-re6stnet-output-schema.json
0 → 100644
View file @
bc357712
{
"$schema"
:
"http://json-schema.org/draft-04/schema#"
,
"description"
:
"Values returned by Re6st Master instanciation"
,
"properties"
:
{
"re6stry-url"
:
{
"description"
:
"ipv6 url to access your re6st registry service"
,
"type"
:
"string"
}
},
"type"
:
"object"
}
\ No newline at end of file
software/re6stnet/instance-re6stnet.cfg.in
0 → 100644
View file @
bc357712
{% set python_bin = parameter_dict['python-executable'] -%}
{% set re6st_registry = parameter_dict['re6st-registry'] -%}
{% set publish_dict = {} -%}
{% set part_list = [] -%}
{% set ipv6 = (ipv6_set | list)[0] -%}
{% set ipv4 = (ipv4_set | list)[0] -%}
{% set uri_scheme = slapparameter_dict.get('uri-scheme', 'http') -%}
{% macro section(name) %}{% do part_list.append(name) %}{{ name }}{% endmacro -%}
[directory]
recipe = slapos.cookbook:mkdirectory
bin = ${buildout:directory}/bin
etc = ${buildout:directory}/etc
srv = ${buildout:directory}/srv
var = ${buildout:directory}/var
log = ${:var}/log
services = ${:etc}/service
script = ${:etc}/run
promises = ${:etc}/promise
run = ${:var}/run
ca-dir = ${:etc}/ssl
requests = ${:ca-dir}/requests
private = ${:ca-dir}/private
certs = ${:ca-dir}/certs
newcerts = ${:ca-dir}/newcerts
crl = ${:ca-dir}/crl
re6st = ${:srv}/res6stnet
[re6stnet-dirs]
recipe = slapos.cookbook:mkdirectory
registry = ${directory:re6st}/registry
log = ${directory:log}/re6stnet
conf = ${directory:etc}/re6stnet
ssl = ${:conf}/ssl
token = ${:conf}/token
[certificate-authority]
recipe = slapos.cookbook:certificate_authority
openssl-binary = {{ openssl_bin }}/openssl
ca-dir = ${directory:ca-dir}
requests-directory = ${directory:requests}
wrapper = ${directory:services}/certificate_authority
ca-private = ${directory:private}
ca-certs = ${directory:certs}
ca-newcerts = ${directory:newcerts}
ca-crl = ${directory:crl}
[apache-conf]
recipe = slapos.recipe.template:jinja2
template = {{ parameter_dict['template-apache-conf'] }}
rendered = ${directory:etc}/apache.conf
ipv6 = {{ ipv6 }}
port = 9026
error-log = ${directory:log}/apache-error.log
access-log = ${directory:log}/apache-access.log
pid-file = ${directory:run}/apache.pid
context =
key apache_port :port
key re6st_ipv4 re6st-registry:ipv4
key re6st_port re6st-registry:port
key access_log :access-log
key error_log :error-log
key pid_file :pid-file
raw certificate ${directory:certs}/apache.crt
raw key ${directory:private}/apache.key
raw ipv6 {{ ipv6 }}
raw uri_scheme {{ uri_scheme }}
{% set apache_wrapper = '${directory:services}/httpd' -%}
{% if uri_scheme == 'https' -%}
{% set apache_wrapper = '${directory:bin}/httpd_raw' -%}
{% endif -%}
[apache-httpd]
recipe = slapos.cookbook:wrapper
wrapper-path = {{ apache_wrapper }}
command-line = "{{ parameter_dict['apache-location'] }}/bin/httpd" -f "${apache-conf:rendered}" -DFOREGROUND
{% if uri_scheme == 'https' %}
[{{ section('apache-ca') }}]
<= certificate-authority
recipe = slapos.cookbook:certificate_authority.request
executable = ${apache-httpd:wrapper-path}
wrapper = ${directory:services}/httpd
key-file = ${certificate-authority:ca-private}/apache.key
cert-file = ${certificate-authority:ca-certs}/apache.crt
{% endif %}
[logrotate-apache]
< = logrotate-entry-base
name = apache
log = ${apache-conf:error-log} ${apache-conf:access-log}
post = {{ parameter_dict['bin-directory'] }}/slapos-kill --pidfile ${apache-conf:pid-file} -s USR1
[logrotate-entry-base]
recipe = slapos.cookbook:logrotate.d
logrotate-entries = ${logrotate:logrotate-entries}
backup = ${logrotate:backup}
[re6st-registry-conf-dict]
port = 9201
ipv4 = {{ ipv4 }}
ipv6 = {{ ipv6 }}
db = ${re6stnet-dirs:registry}/registry.db
ca = ${re6stnet-dirs:ssl}/re6stnet.crt
key = ${re6stnet-dirs:ssl}/re6stnet.key
mailhost = 127.0.0.1
prefix-length = 16
anonymous-prefix-length = 32
logfile = ${re6stnet-dirs:log}/registry.log
verbose = 2
[re6st-registry-conf]
recipe = slapos.recipe.template:jinja2
template = {{ parameter_dict['template-re6st-registry-conf'] }}
rendered = ${directory:etc}/re6st-registry.conf
context = section parameter_dict re6st-registry-conf-dict
[re6st-registry]
recipe = slapos.cookbook:re6stnet.registry
port = ${re6st-registry-conf-dict:port}
ipv4 = ${re6st-registry-conf-dict:ipv4}
command = {{ re6st_registry }}
config-file = ${re6st-registry-conf:rendered}
db-path = ${re6st-registry-conf-dict:db}
wrapper = ${directory:services}/re6st-registry
manager-wrapper = ${directory:bin}/re6stManageToken
check-service-wrapper = ${directory:bin}/re6stCheckService
drop-service-wrapper = ${directory:bin}/re6stManageDeleteToken
key-file = ${re6st-registry-conf-dict:key}
cert-file = ${re6st-registry-conf-dict:ca}
openssl-bin = {{ openssl_bin }}/openssl
python-bin = {{ python_bin }}
ipv6-prefix = {{ slapparameter_dict.get('ipv6-prefix', '2001:db8:24::/48') }}
key-size = {{ slapparameter_dict.get('key-size', 2048) }}
conf-dir = ${re6stnet-dirs:conf}
token-dir = ${re6stnet-dirs:token}
slave-instance-list = ${slap-parameter:slave_instance_list}
environment =
PATH={{ openssl_bin }}
[re6stnet-manage]
recipe = slapos.cookbook:wrapper
wrapper-path = ${directory:script}/re6st-token-manager
command-line = "{{ python_bin }}" ${re6st-registry:manager-wrapper}
[cron-entry-re6st-check]
recipe = slapos.cookbook:cron.d
cron-entries = ${cron:cron-entries}
name = re6stnet-check-token
frequency = 0 */1 * * *
command = {{ python_bin }} ${re6st-registry:check-service-wrapper}
[cron-entry-re6st-drop]
recipe = slapos.cookbook:cron.d
cron-entries = ${cron:cron-entries}
name = re6stnet-drop-token
frequency = */30 * * * *
command = {{ python_bin }} ${re6st-registry:drop-service-wrapper}
[logrotate-entry-re6stnet]
< = logrotate-entry-base
name = re6stnet
log = ${re6st-registry-conf-dict:logfile}
[re6st-registry-promise]
recipe = slapos.cookbook:check_port_listening
path = ${directory:promises}/re6st-registry
hostname = ${re6st-registry:ipv4}
port = ${re6st-registry:port}
[apache-registry-promise]
recipe = slapos.cookbook:check_port_listening
path = ${directory:promises}/apache-re6st-registry
hostname = ${apache-conf:ipv6}
port = ${apache-conf:port}
{% do publish_dict.__setitem__('re6stry-url', uri_scheme ~ '://[${apache-conf:ipv6}]:${apache-conf:port}') -%}
[publish]
recipe = slapos.cookbook:publish
{% for name, value in publish_dict.items() -%}
{{ name }} = {{ value }}
{% endfor -%}
[buildout]
extends =
{{ logrotate_cfg }}
parts =
certificate-authority
logrotate-apache
logrotate-entry-re6stnet
re6stnet-manage
cron-entry-logrotate
cron-entry-re6st-check
cron-entry-re6st-drop
apache-httpd
publish
re6st-registry-promise
apache-registry-promise
# Complete parts with sections
{{ part_list | join('\n ') }}
eggs-directory = {{ eggs_directory }}
develop-eggs-directory = {{ develop_eggs_directory }}
offline = true
[slap-parameter]
slave_instance_list = {}
software/re6stnet/instance.cfg.in
0 → 100644
View file @
bc357712
[buildout]
parts = switch-softwaretype
eggs-directory = {{ eggs_directory }}
develop-eggs-directory = {{ develop_eggs_directory }}
[slap-configuration]
recipe = slapos.cookbook:slapconfiguration.serialised
computer = ${slap-connection:computer-id}
partition = ${slap-connection:partition-id}
url = ${slap-connection:server-url}
key = ${slap-connection:key-file}
cert = ${slap-connection:cert-file}
[jinja2-template-base]
recipe = slapos.recipe.template:jinja2
rendered = ${buildout:parts-directory}/${:_buildout_section_name_}/${:filename}
extra-context =
context =
key develop_eggs_directory buildout:develop-eggs-directory
key eggs_directory buildout:eggs-directory
key ipv6_set slap-configuration:ipv6
key ipv4_set slap-configuration:ipv4
key slapparameter_dict slap-configuration:configuration
key computer_id slap-configuration:computer
raw logrotate_cfg {{ template_logrotate_base }}
raw dash_binary {{ dash_location }}/bin/dash
raw openssl_bin {{ openssl_location}}/bin
${:extra-context}
[dynamic-template-re6stnet-parameters]
bin-directory = {{ bin_directory }}
python-executable = {{ python_with_eggs }}
re6st-registry = {{ re6stnet_registry }}
template-apache-conf = {{ template_apache_conf }}
apache-location = {{ apache_location }}
template-re6st-registry-conf = {{ template_re6st_registry_conf }}
[dynamic-template-re6stnet]
< = jinja2-template-base
template = {{ template_re6stnet }}
filename = instance-re6stnet.cfg
extensions = jinja2.ext.do
extra-context =
section parameter_dict dynamic-template-re6stnet-parameters
[switch-softwaretype]
recipe = slapos.cookbook:softwaretype
default = ${dynamic-template-re6stnet:rendered}
registry = ${:default}
software/re6stnet/re6st-registry.conf.in
0 → 100644
View file @
bc357712
port {{ parameter_dict['port'] }}
4 {{ parameter_dict['ipv4'] }}
6 {{ parameter_dict['ipv6'] }}
db {{ parameter_dict['db'] }}
ca {{ parameter_dict['ca'] }}
key {{ parameter_dict['key'] }}
mailhost {{ parameter_dict['mailhost'] }}
prefix-length {{ parameter_dict['prefix-length'] }}
anonymous-prefix-length {{ parameter_dict['anonymous-prefix-length'] }}
logfile {{ parameter_dict['logfile'] }}
verbose {{ parameter_dict['verbose'] }}
\ No newline at end of file
software/re6stnet/software.cfg
0 → 100644
View file @
bc357712
[buildout]
extends =
../../component/re6stnet/buildout.cfg
../../component/dash/buildout.cfg
../../component/git/buildout.cfg
../../component/dcron/buildout.cfg
../../component/gzip/buildout.cfg
../../component/openssl/buildout.cfg
../../component/logrotate/buildout.cfg
../../component/apache/buildout.cfg
../../stack/slapos.cfg
develop =
${:parts-directory}/re6stnet-repository
${:parts-directory}/slapos.cookbook-repository
parts =
slapos-cookbook
eggs
dash
babeld
re6stnet-develop
re6stnet
template
slapos.cookbook-repository
check-recipe
[eggs]
recipe = zc.recipe.egg
eggs =
${lxml-python:egg}
slapos.toolbox
scripts =
slapos-kill
[extra-eggs]
recipe = zc.recipe.egg
interpreter = pythonwitheggs
eggs =
${lxml-python:egg}
${python-cffi:egg}
${python-cryptography:egg}
pyOpenSSL
miniupnpc
re6stnet
[re6stnet-repository]
repository = http://git.erp5.org/repos/re6stnet.git
branch = re6st-slapos
[slapos.cookbook-repository]
recipe = slapos.recipe.build:gitclone
repository = http://git.erp5.org/repos/slapos.git
branch = re6st-master
git-executable = ${git:location}/bin/git
[download-base]
recipe = slapos.recipe.build:download
url = ${:_profile_base_location_}/${:filename}
mode = 644
[template-jinja2-base]
recipe = slapos.recipe.template:jinja2
template = ${:_profile_base_location_}/${:filename}.in
rendered = ${buildout:directory}/${:filename}
# XXX: extra-context is needed because we cannot append to a key of an extended
# section.
extra-context =
context =
key bin_directory buildout:bin-directory
key develop_eggs_directory buildout:develop-eggs-directory
key eggs_directory buildout:eggs-directory
${:extra-context}
[template]
< = template-jinja2-base
filename = template.cfg
template = ${:_profile_base_location_}/instance.cfg.in
md5sum = 0929cf851c4883bcb5c69fc2f918eaeb
extra-context =
key apache_location apache:location
key dash_location dash:location
key logrotate_location logrotate:location
key openssl_location openssl:location
key template_apache_conf template-apache-conf:target
key template_re6stnet template-re6stnet:target
key template_re6st_registry_conf template-re6st-registry-conf:target
key template_logrotate_base template-logrotate-base:rendered
raw python_with_eggs ${buildout:directory}/bin/${extra-eggs:interpreter}
raw re6stnet_registry ${buildout:directory}/bin/re6st-registry
[template-re6stnet]
< = download-base
filename = instance-re6stnet.cfg.in
md5sum = e088fb05ea6e1ceff8a5ac00fd28bd75
[template-logrotate-base]
< = template-jinja2-base
filename = instance-logrotate-base.cfg
md5sum = f28fbd310944f321ccb34b2a34c82005
extra-context =
key dcron_location dcron:location
key gzip_location gzip:location
key logrotate_location logrotate:location
[template-apache-conf]
< = download-base
filename = apache.conf.in
md5sum = c220229ee37866c8cc404d602edd389d
[template-re6st-registry-conf]
< = download-base
filename = re6st-registry.conf.in
md5sum = ae910e8e154be6575bb19f6eae686a87
[check-recipe]
recipe = plone.recipe.command
stop-on-error = true
update-command = ${:command}
command =
grep parts ${buildout:develop-eggs-directory}/re6stnet.egg-link
grep parts ${buildout:develop-eggs-directory}/slapos.cookbook.egg-link
[versions]
apache-libcloud = 0.17.0
ecdsa = 0.13
gitdb = 0.6.4
plone.recipe.command = 1.1
pycrypto = 2.6.1
slapos.recipe.template = 2.6
slapos.toolbox = 0.47.3
smmap = 0.9.0
# Required by:
# slapos.toolbox==0.47.3
GitPython = 0.3.6
# Required by:
# slapos.toolbox==0.47.3
atomize = 0.2.0
# Required by:
# apache-libcloud==0.17.0
backports.ssl-match-hostname = 3.4.0.2
# Required by:
# slapos.toolbox==0.47.3
feedparser = 5.1.3
# Required by:
# slapos.toolbox==0.47.3
lockfile = 0.10.2
# Required by:
# re6stnet===0-413.gbec6b3c.dirty
miniupnpc = 1.9
# Required by:
# slapos.toolbox==0.47.3
paramiko = 1.15.2
# Required by:
# slapos.toolbox==0.47.3
rpdb = 0.1.5
software/re6stnet/software.cfg.json
0 → 100644
View file @
bc357712
{
"name"
:
"RE6STNET"
,
"description"
:
"Master instance of re6st (Resilient, Scalable, IPv6 Network application)"
,
"serialisation"
:
"xml"
,
"software-type"
:
{
"default"
:
{
"title"
:
"Default"
,
"description"
:
"Re6st registry"
,
"request"
:
"instance-re6stnet-input-schema.json"
,
"response"
:
"instance-re6stnet-output-schema.json"
,
"index"
:
0
},
"registry"
:
{
"title"
:
"registry"
,
"description"
:
"Re6st registry"
,
"request"
:
"instance-re6stnet-resilient-input-schema.json"
,
"response"
:
"instance-re6stnet-output-schema.json"
,
"index"
:
1
}
}
}
\ No newline at end of file
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