Commit 6ee19990 authored by Jérome Perrin's avatar Jérome Perrin

Merge remote-tracking branch 'upstream/master' into zope4py2

parents 351f207b a244c329
...@@ -133,13 +133,17 @@ command = ...@@ -133,13 +133,17 @@ command =
sed -i "s#'/usr/share/'#'${firewalld:location}/share'#" ${firewalld:python-egg}/firewall/config/__init__.py sed -i "s#'/usr/share/'#'${firewalld:location}/share'#" ${firewalld:python-egg}/firewall/config/__init__.py
sed -i "s#import sys#import sys, os\n\nos.environ['GI_TYPELIB_PATH'] = '${gobject-introspection:location}/lib/girepository-1.0/'#" ${:python} sed -i "s#import sys#import sys, os\n\nos.environ['GI_TYPELIB_PATH'] = '${gobject-introspection:location}/lib/girepository-1.0/'#" ${:python}
sed -i 's#<user>messagebus</user>#<user>slapsoft</user>#' ${dbus:location}/share/dbus-1/system.conf
cp -f ${firewalld:location}/lib/firewalld/zones/trusted.xml ${firewalld:etc-dir}/zones/ cp -f ${firewalld:location}/lib/firewalld/zones/trusted.xml ${firewalld:etc-dir}/zones/
cp -f ${firewalld:location}/share/dbus-1/system.d/FirewallD.conf ${dbus:location}/share/dbus-1/system.d/
mkdir -p ${firewalld:location}/sbin mkdir -p ${firewalld:location}/sbin
echo -n '#!/bin/sh\nLD_LIBRARY_PATH=${nftables:location}/lib exec ${firewalld:location}/${firewalld:sbin-dir}/firewalld "$@"' > ${firewalld:location}/sbin/firewalld echo -n '#!/bin/sh\nLD_LIBRARY_PATH=${nftables:location}/lib exec ${firewalld:location}/${firewalld:sbin-dir}/firewalld "$@"' > ${firewalld:location}/sbin/firewalld
chmod a+x ${firewalld:location}/sbin/firewalld chmod a+x ${firewalld:location}/sbin/firewalld
# no need to patch dbus if not in top level compilation
[ "$(stat -c '%U' ${dbus:location}/share/dbus-1/system.conf)" = "slapsoft" ] || exit 0
sed -i 's#<user>messagebus</user>#<user>slapsoft</user>#' ${dbus:location}/share/dbus-1/system.conf
cp -f ${firewalld:location}/share/dbus-1/system.d/FirewallD.conf ${dbus:location}/share/dbus-1/system.d/
update-command = ${:command} update-command = ${:command}
stop-on-error = true stop-on-error = true
......
[buildout] [buildout]
extends =
../cmake/buildout.cfg
parts = mbedtls parts = mbedtls
[mbedtls] [mbedtls]
...@@ -6,5 +8,8 @@ recipe = slapos.recipe.cmmi ...@@ -6,5 +8,8 @@ recipe = slapos.recipe.cmmi
url = https://github.com/Mbed-TLS/mbedtls/archive/refs/tags/v2.28.2.tar.gz url = https://github.com/Mbed-TLS/mbedtls/archive/refs/tags/v2.28.2.tar.gz
md5sum = 421c47c18ef46095e3ad38ffc0543e11 md5sum = 421c47c18ef46095e3ad38ffc0543e11
shared = true shared = true
configure-command = echo configure-command = cmake -DUSE_SHARED_MBEDTLS_LIBRARY=On -DCMAKE_INSTALL_PREFIX=
make-targets = install DESTDIR=@@LOCATION@@ environment =
PATH=${cmake:location}/bin:%(PATH)s
LDFLAGS=-Wl,-rpath=@@LOCATION@@/lib/
make-targets = install DESTDIR=@@LOCATION@@
...@@ -19,8 +19,9 @@ parts = ...@@ -19,8 +19,9 @@ parts =
py py
firewalld-patch firewalld-patch
[python] # Force python3.7 for a while to be compatible with more SR
part = python3 [python3]
<= python3.7
[environment] [environment]
# Note: For now original PATH is appended to the end, as not all tools are # Note: For now original PATH is appended to the end, as not all tools are
......
...@@ -38,10 +38,6 @@ configure-options += ...@@ -38,10 +38,6 @@ configure-options +=
environment += environment +=
DESTDIR=${buildout:destdir} DESTDIR=${buildout:destdir}
# Force python3.7 for a while to be compatible with more SR
[python3]
<= python3.7
[bison] [bison]
configure-options += configure-options +=
--prefix=${buildout:rootdir}/parts/${:_buildout_section_name_} --prefix=${buildout:rootdir}/parts/${:_buildout_section_name_}
......
[instance-profile] [instance-profile]
filename = instance.cfg.in filename = instance.cfg.in
md5sum = 0e109afd93153ecf062ad5e76bc86ea4 md5sum = 0e74c862401f266111552b7a3611f7bf
{ {
"$schema": "http://json-schema.org/draft-04/schema#", "$schema": "http://json-schema.org/draft-04/schema#",
"type": "object", "type": "object",
"additionalProperties": false,
"properties": { "properties": {
"mb_password_complexity": { "mb_password_complexity": {
"title": "Password complexity", "title": "Password complexity",
"description": "Check Metabase documentation for more details.", "description": "Check Metabase documentation for more details.",
"type": "string", "type": "string",
"default": "normal", "default": "normal",
"enum": [ "enum": [
"weak", "weak",
"normal", "normal",
"strong", "strong"
] ]
}, },
"mb_password_length": { "mb_password_length": {
"title": "Password length", "title": "Password length",
"description": "Password length", "description": "Password length",
"type": "integer", "type": "integer",
"default": 6, "default": 6
} }
} }
} }
{
"$schema": "https://json-schema.org/draft-04/schema#",
"type": "object",
"properties": {
"url": {
"title": "URL",
"description": "URL to access metabase.",
"type": "string",
"format": "uri"
}
}
}
...@@ -32,7 +32,7 @@ home = $${buildout:directory} ...@@ -32,7 +32,7 @@ home = $${buildout:directory}
init = init =
default_parameters = options.get('slapparameter-dict') default_parameters = options.get('slapparameter-dict')
options['mb_password_complexity'] = default_parameters.get('mb_password_complexity', 'normal') options['mb_password_complexity'] = default_parameters.get('mb_password_complexity', 'normal')
options['mb_password_length'] = default_parameters.get('mb_password_length', 6) options['mb_password_length'] = default_parameters.get('mb_password_length', '6')
[metabase-instance] [metabase-instance]
recipe = slapos.cookbook:wrapper recipe = slapos.cookbook:wrapper
...@@ -127,7 +127,6 @@ alias = metabase ...@@ -127,7 +127,6 @@ alias = metabase
[postgresql-password] [postgresql-password]
recipe = slapos.cookbook:generate.password recipe = slapos.cookbook:generate.password
bytes = 24
[postgresql] [postgresql]
recipe = slapos.cookbook:postgres recipe = slapos.cookbook:postgres
......
{
"name": "Metabase",
"description": "Business Intelligence and Reporting tool",
"serialisation": "xml",
"software-type": {
"default": {
"title": "Default",
"software-type": "default",
"description": "Default",
"request": "instance-metabase-input-schema.json",
"response": "instance-metabase-output-schema.json",
"index": 1
}
}
}
[instance-profile]
filename = instance.cfg.in
md5sum = fa8d1d0a44720e0ffa4f6a953b65eae4
{
"$schema": "https://json-schema.org/draft/2019-09/schema",
"description": "Parameters to instantiate coupler",
"type": "object",
"configuration": {
"coupler_block_device": {
"description": "The Linux block device using I2C protocol,",
"type": "string",
"default": "/dev/i2c-1"
},
"coupler_i2c_slave_list": {
"description": "The list of comma separated addresses of I2C enabled devices on the I2C bus.",
"type": "string",
"default": "0x58"
},
"opc_ua_port": {
"description": "The OPC UA server bind to bind to.",
"type": "integer",
"default": 4840
},
"mode": {
"description": "The operationg mode of the coupler. By default 0 - i.e. control for real I2C devices attached. If 1 selected emulate them (useful for testing). ",
"type": "integer",
"default": 0
},
"id": {
"description": "The numeric ID of the coupler",
"type": "integer",
"default": 0
},
"username": {
"description": "The username for OPC UA server.",
"type": "string",
"default": ""
},
"password": {
"description": "The password for OPC UA server.",
"type": "string",
"default": ""
},
"heart_beat": {
"description": "Indication if coupler should send heart beats over a keep-alive network.",
"type": "boolean",
"default": 0
},
"heart_beat_interval": {
"description": "The heart beat interval (in ms)",
"type": "integer",
"default": 500
},
"heart_beat_id_list": {
"description": "A comma separated list of couplers' IDs which should send to us keep-alive messages. ",
"type": "string",
"default": ""
},
"heart_beat_timeout_interval": {
"description": "The timeout (in ms) which when expired without a keep alive message will cause the coupler to go to a safe mode. ",
"type": "integer",
"default": 2000
},
"network_address_url_data_type": {
"description": "Network address URL type used for Pub/Sub.",
"type": "string",
"default": "opc.udp://224.0.0.22:4840/"
}
}
}
{
"$schema": "https://json-schema.org/draft/2019-09/schema",
"description": "Values returned by coupler's instantiation.",
"additionalProperties": false,
"properties": {},
"type": "object"
}
#############################
#
# Deploy coupler instance
#
#############################
[buildout]
parts =
directory
publish-connection-parameter
coupler-opc-ua
eggs-directory = {{ buildout['eggs-directory'] }}
develop-eggs-directory = {{ buildout['develop-eggs-directory'] }}
offline = true
extends = {{ template_monitor }}
[coupler-opc-ua]
recipe = slapos.cookbook:wrapper
environment =
LD_LIBRARY_PATH=$LD_LIBRARY_PATH:{{ open62541_location }}/lib:{{ mbedtls_location }}/lib
command-line =
{{ coupler_location }}/server -d ${instance-parameter:configuration.coupler_block_device} -s ${instance-parameter:configuration.coupler_i2c_slave_list} -p ${instance-parameter:configuration.opc_ua_port} -u ${instance-parameter:configuration.username} -w ${instance-parameter:configuration.password} -b ${instance-parameter:configuration.heart_beat} -t ${instance-parameter:configuration.heart_beat_interval} -l ${instance-parameter:configuration.heart_beat_id_list} -n ${instance-parameter:configuration.network_address_url_data_type} -o ${instance-parameter:configuration.heart_beat_timeout_interval} -i ${instance-parameter:configuration.id} -m ${instance-parameter:configuration.mode}
wrapper-path = ${directory:service}/coupler-opc-ua
[instance-parameter]
recipe = slapos.cookbook:slapconfiguration
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}
configuration.coupler_block_device = /dev/i2c-1
configuration.coupler_i2c_slave_list = 0x58
configuration.mode = 0
configuration.username =
configuration.password =
configuration.interface = 0.0.0.0
configuration.opc_ua_port = 4840
configuration.id = 0
configuration.heart_beat = 0
configuration.heart_beat_interval = 500
configuration.heart_beat_id_list =
configuration.network_address_url_data_type = opc.udp://224.0.0.22:4840/
configuration.heart_beat_timeout_interval = 2000
[directory]
recipe = slapos.cookbook:mkdirectory
home = ${buildout:directory}
etc = ${:home}/etc
var = ${:home}/var
script = ${:etc}/run/
service = ${:etc}/service
log = ${:var}/log
[publish-connection-parameter]
recipe = slapos.cookbook:publish
opc_ua_port = ${instance-parameter:configuration.opc_ua_port}
interface = ${instance-parameter:configuration.interface}
[buildout]
parts =
open62541
compile-coupler
slapos-cookbook
instance-profile
extends =
../../component/git/buildout.cfg
../../component/mbedtls/buildout.cfg
../../component/open62541/buildout.cfg
../../stack/monitor/buildout.cfg
../../stack/slapos.cfg
# we need open62541's sources even after compiling and linking in [open62541]
# section. Reasons is that coupler's C application depends on it.
[open62541-source]
recipe = slapos.recipe.build:download-unpacked
shared = true
url = ${open62541:url}
md5sum = ${open62541:md5sum}
[open62541]
configure-options =
-DBUILD_SHARED_LIBS=ON
-DCMAKE_BUILD_TYPE=Release
-DCMAKE_INSTALL_PREFIX=@@LOCATION@@
-DUA_ENABLE_PUBSUB=ON
-DUA_ENABLE_PUBSUB_MONITORING=ON
-DUA_ENABLE_PUBSUB_ETH_UADP=ON
-DUA_NAMESPACE_ZERO=REDUCED
-DUA_ENABLE_ENCRYPTION=MBEDTLS
-DUA_ENABLE_ENCRYPTION_MBEDTLS=ON
-DMBEDTLS_INCLUDE_DIRS=${mbedtls:location}/include
-DMBEDTLS_LIBRARY=${mbedtls:location}/lib/libmbedtls.so
-DMBEDX509_LIBRARY=${mbedtls:location}/lib/libmbedx509.so
-DMBEDCRYPTO_LIBRARY=${mbedtls:location}/lib/libmbedcrypto.so
-DUA_ENABLE_PUBSUB_INFORMATIONMODEL=ON
-DUA_ENABLE_PUBSUB_MQTT=ON
environment +=
LDFLAGS=-L${mbedtls:location}/lib -Wl,-rpath=${mbedtls:location}/lib
[osie-repository]
recipe = slapos.recipe.build:gitclone
git-executable = ${git:location}/bin/git
repository = https://lab.nexedi.com/nexedi/osie.git
revision = 35228392c4f2eb24eee478cd98349e0f613a4ff2
[compile-coupler]
recipe = slapos.recipe.cmmi
path = ${osie-repository:location}/coupler/opc-ua-server/
bin_dir = ${:path}/bin/
environment =
OPEN62541_HOME = ${open62541:location}
OPEN62541_SOURCE_HOME = ${open62541-source:location}
C_COMPILER_EXTRA_FLAGS = -L ${mbedtls:location}/lib -Wl,-rpath=${mbedtls:location}/lib -l:libopen62541.so -L${open62541:location}/lib -Wl,-rpath=${open62541:location}/lib -I${open62541:location}/include -I${open62541-source:location}/src/pubsub/ -I${open62541-source:location}/deps
configure-command = true
[instance-profile]
recipe = slapos.recipe.template:jinja2
template = ${:_profile_base_location_}/instance.cfg.in
mode = 0644
rendered = ${buildout:directory}/instance.cfg
extensions = jinja2.ext.do
context =
section buildout buildout
raw template_monitor ${monitor2-template:output}
key open62541_location open62541:location
key mbedtls_location mbedtls:location
key coupler_location compile-coupler:bin_dir
{
"name": "OSIE coupler",
"description": "Coupler is an open source thin C client application supporting OPC UA protocol and used in conjunction with beremiz-ide / beremiz-runtime to control industrial processes on the shop field.",
"serialisation": "xml",
"software-type": {
"default": {
"title": "Default",
"software-type": "default",
"description": "Default",
"request": "instance-input-schema.json",
"response": "instance-output-schema.json"
}
}
}
Tests for osie-coupler software release
##############################################################################
#
# Copyright (c) 2018 Nexedi SA 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.
#
##############################################################################
from setuptools import setup, find_packages
version = '0.0.1.dev0'
name = 'slapos.test.osie-coupler'
with open("README.md") as f:
long_description = f.read()
setup(
name=name,
version=version,
description="Test for SlapOS' Osie Coupler",
long_description=long_description,
long_description_content_type='text/markdown',
maintainer="Nexedi",
maintainer_email="info@nexedi.com",
url="https://lab.nexedi.com/nexedi/slapos",
packages=find_packages(),
install_requires=[
'slapos.core',
'slapos.libnetworkcache',
'erp5.util',
],
zip_safe=True,
test_suite='test',
)
##############################################################################
# coding: utf-8
#
# Copyright (c) 2022 Nexedi SA 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 os
from slapos.testing.testcase import makeModuleSetUpAndTestCaseClass
setUpModule, SlapOSInstanceTestCase = makeModuleSetUpAndTestCaseClass(
os.path.abspath(
os.path.join(os.path.dirname(__file__), '..', 'software.cfg')))
class OsieTestCase(SlapOSInstanceTestCase):
@classmethod
def getInstanceParameterDict(cls):
return {"mode": 1}
def test(self):
connexion_parameters = self.computer_partition.getConnectionParameterDict()
self.assertIn('opc_ua_port', connexion_parameters)
self.assertIn('interface', connexion_parameters)
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
"default": "", "default": "",
"type": "string" "type": "string"
}, },
"fronted-url": { "frontend-url": {
"title": "Frontend URL", "title": "Frontend URL",
"description": "Frontend URL", "description": "Frontend URL",
"default": "", "default": "",
......
...@@ -257,6 +257,11 @@ setup = ${slapos-repository:location}/software/peertube/test/ ...@@ -257,6 +257,11 @@ setup = ${slapos-repository:location}/software/peertube/test/
egg = slapos.test.js_drone egg = slapos.test.js_drone
setup = ${slapos-repository:location}/software/js-drone/test/ setup = ${slapos-repository:location}/software/js-drone/test/
[slapos.test.osie-coupler-setup]
<= setup-develop-egg
egg = slapos.test.osie_coupler
setup = ${slapos-repository:location}/software/osie-coupler/test/
[slapos.core-repository] [slapos.core-repository]
<= git-clone-repository <= git-clone-repository
repository = https://lab.nexedi.com/nexedi/slapos.core.git repository = https://lab.nexedi.com/nexedi/slapos.core.git
...@@ -333,6 +338,7 @@ eggs += ...@@ -333,6 +338,7 @@ eggs +=
${slapos.test.nextcloud-setup:egg} ${slapos.test.nextcloud-setup:egg}
${slapos.test.nginx-push-stream-setup:egg} ${slapos.test.nginx-push-stream-setup:egg}
${slapos.test.ors-amarisoft-setup:egg} ${slapos.test.ors-amarisoft-setup:egg}
${slapos.test.osie-coupler-setup:egg}
${slapos.test.peertube-setup:egg} ${slapos.test.peertube-setup:egg}
${slapos.test.plantuml-setup:egg} ${slapos.test.plantuml-setup:egg}
${slapos.test.powerdns-setup:egg} ${slapos.test.powerdns-setup:egg}
...@@ -425,6 +431,7 @@ tests = ...@@ -425,6 +431,7 @@ tests =
nextcloud ${slapos.test.nextcloud-setup:setup} nextcloud ${slapos.test.nextcloud-setup:setup}
nginx-push-stream ${slapos.test.nginx-push-stream-setup:setup} nginx-push-stream ${slapos.test.nginx-push-stream-setup:setup}
ors-amarisoft ${slapos.test.ors-amarisoft-setup:setup} ors-amarisoft ${slapos.test.ors-amarisoft-setup:setup}
osie-coupler ${slapos.test.osie-coupler-setup:setup}
peertube ${slapos.test.peertube-setup:setup} peertube ${slapos.test.peertube-setup:setup}
plantuml ${slapos.test.plantuml-setup:setup} plantuml ${slapos.test.plantuml-setup:setup}
powerdns ${slapos.test.powerdns-setup:setup} powerdns ${slapos.test.powerdns-setup:setup}
......
{ {
"$schema": "http://json-schema.org/draft-04/schema#", "$schema": "http://json-schema.org/draft-04/schema#",
"type": "object", "type": "object",
"additionalProperties": false,
"properties": { "properties": {
"user-authorized-key": { "user-authorized-key": {
"title": "User Authorized Key", "title": "User Authorized Key",
......
...@@ -2,9 +2,14 @@ ...@@ -2,9 +2,14 @@
"$schema": "http://json-schema.org/draft-04/schema#", "$schema": "http://json-schema.org/draft-04/schema#",
"description": "Values returned by instanciation", "description": "Values returned by instanciation",
"properties": { "properties": {
"ssh_command": { "ssh-command": {
"description": "SSH command used to access your instance in ssh when you provided a ssh public key", "description": "SSH command used to access the instance",
"type": "string" "type": "string"
},
"ssh-url": {
"description": "ssh:// URL to access the instance",
"type": "string",
"format": "uri"
} }
}, },
"type": "object" "type": "object"
......
{ {
"name": "SSH", "name": "SSH",
"description": "SSH software release which provide the SSH service", "description": "SSH software release which provide the SSH service",
"serialisation": "json-in-xml", "serialisation": "xml",
"software-type": { "software-type": {
"default": { "default": {
"title": "Default", "title": "Default",
......
...@@ -196,6 +196,7 @@ idna = 3.3 ...@@ -196,6 +196,7 @@ idna = 3.3
igmp = 1.0.4 igmp = 1.0.4
Importing = 1.10 Importing = 1.10
importlib-metadata = 1.7.0:whl importlib-metadata = 1.7.0:whl
importlib-resources = 5.10.2:whl
inotify-simple = 1.1.1 inotify-simple = 1.1.1
ipaddress = 1.0.23 ipaddress = 1.0.23
ipykernel = 5.3.4:whl ipykernel = 5.3.4:whl
...@@ -247,6 +248,7 @@ pexpect = 4.8.0 ...@@ -247,6 +248,7 @@ pexpect = 4.8.0
pickleshare = 0.7.4 pickleshare = 0.7.4
pim-dm = 1.4.0nxd001 pim-dm = 1.4.0nxd001
pkgconfig = 1.5.1 pkgconfig = 1.5.1
pkgutil-resolve-name = 1.3.10
plone.recipe.command = 1.1 plone.recipe.command = 1.1
pluggy = 0.13.1:whl pluggy = 0.13.1:whl
ply = 3.11 ply = 3.11
...@@ -332,7 +334,7 @@ zc.buildout.languageserver = 0.8.3 ...@@ -332,7 +334,7 @@ zc.buildout.languageserver = 0.8.3
zc.lockfile = 1.4 zc.lockfile = 1.4
ZConfig = 3.6.1 ZConfig = 3.6.1
zdaemon = 4.2.0 zdaemon = 4.2.0
zipp = 1.2.0:whl zipp = 3.12.0:whl
zodburi = 2.5.0 zodburi = 2.5.0
zope.event = 4.5.0 zope.event = 4.5.0
zope.interface = 5.4.0 zope.interface = 5.4.0
...@@ -359,6 +361,7 @@ smmap2 = 2.0.5 ...@@ -359,6 +361,7 @@ smmap2 = 2.0.5
traitlets = 4.3.3 traitlets = 4.3.3
Werkzeug = 1.0.1 Werkzeug = 1.0.1
wheel = 0.35.1:whl wheel = 0.35.1:whl
zipp = 1.2.0:whl
[versions:sys.version_info < (3,8)] [versions:sys.version_info < (3,8)]
MarkupSafe = 1.0 MarkupSafe = 1.0
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment