Skip to content
GitLab
Projects
Groups
Snippets
Help
Loading...
Help
What's new
10
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
Open sidebar
ISC Open Source Projects
Kea
Commits
c6ab3eb8
Commit
c6ab3eb8
authored
Sep 01, 2014
by
Marcin Siodelski
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
[3534] It is now possible to compare two Configuration objects for equality
parent
9c807997
Changes
8
Hide whitespace changes
Inline
Side-by-side
Showing
8 changed files
with
450 additions
and
58 deletions
+450
-58
src/lib/dhcpsrv/Makefile.am
src/lib/dhcpsrv/Makefile.am
+4
-3
src/lib/dhcpsrv/configuration.cc
src/lib/dhcpsrv/configuration.cc
+30
-0
src/lib/dhcpsrv/configuration.h
src/lib/dhcpsrv/configuration.h
+53
-54
src/lib/dhcpsrv/logging_info.cc
src/lib/dhcpsrv/logging_info.cc
+64
-0
src/lib/dhcpsrv/logging_info.h
src/lib/dhcpsrv/logging_info.h
+124
-0
src/lib/dhcpsrv/tests/Makefile.am
src/lib/dhcpsrv/tests/Makefile.am
+1
-0
src/lib/dhcpsrv/tests/configuration_unittest.cc
src/lib/dhcpsrv/tests/configuration_unittest.cc
+50
-1
src/lib/dhcpsrv/tests/logging_info_unittest.cc
src/lib/dhcpsrv/tests/logging_info_unittest.cc
+124
-0
No files found.
src/lib/dhcpsrv/Makefile.am
View file @
c6ab3eb8
...
...
@@ -45,6 +45,9 @@ libkea_dhcpsrv_la_SOURCES =
libkea_dhcpsrv_la_SOURCES
+=
addr_utilities.cc addr_utilities.h
libkea_dhcpsrv_la_SOURCES
+=
alloc_engine.cc alloc_engine.h
libkea_dhcpsrv_la_SOURCES
+=
callout_handle_store.h
libkea_dhcpsrv_la_SOURCES
+=
cfg_iface.cc cfg_iface.h
libkea_dhcpsrv_la_SOURCES
+=
cfgmgr.cc cfgmgr.h
libkea_dhcpsrv_la_SOURCES
+=
configuration.h configuration.cc
libkea_dhcpsrv_la_SOURCES
+=
csv_lease_file4.cc csv_lease_file4.h
libkea_dhcpsrv_la_SOURCES
+=
csv_lease_file6.cc csv_lease_file6.h
libkea_dhcpsrv_la_SOURCES
+=
d2_client_cfg.cc d2_client_cfg.h
...
...
@@ -52,16 +55,14 @@ libkea_dhcpsrv_la_SOURCES += d2_client_mgr.cc d2_client_mgr.h
libkea_dhcpsrv_la_SOURCES
+=
daemon.cc daemon.h
libkea_dhcpsrv_la_SOURCES
+=
dbaccess_parser.cc dbaccess_parser.h
libkea_dhcpsrv_la_SOURCES
+=
dhcpsrv_log.cc dhcpsrv_log.h
libkea_dhcpsrv_la_SOURCES
+=
cfgmgr.cc cfgmgr.h
libkea_dhcpsrv_la_SOURCES
+=
dhcp_config_parser.h
libkea_dhcpsrv_la_SOURCES
+=
dhcp_parsers.cc dhcp_parsers.h
libkea_dhcpsrv_la_SOURCES
+=
cfg_iface.cc cfg_iface.h
libkea_dhcpsrv_la_SOURCES
+=
key_from_key.h
libkea_dhcpsrv_la_SOURCES
+=
lease.cc lease.h
libkea_dhcpsrv_la_SOURCES
+=
lease_mgr.cc lease_mgr.h
libkea_dhcpsrv_la_SOURCES
+=
lease_mgr_factory.cc lease_mgr_factory.h
libkea_dhcpsrv_la_SOURCES
+=
logging.cc logging.h
libkea_dhcpsrv_la_SOURCES
+=
configuration.h configuration.cc
libkea_dhcpsrv_la_SOURCES
+=
logging_info.cc logging_info.h
libkea_dhcpsrv_la_SOURCES
+=
memfile_lease_mgr.cc memfile_lease_mgr.h
if
HAVE_MYSQL
...
...
src/lib/dhcpsrv/configuration.cc
View file @
c6ab3eb8
...
...
@@ -73,5 +73,35 @@ Configuration::sequenceEquals(const Configuration& other) {
return
(
getSequence
()
==
other
.
getSequence
());
}
bool
Configuration
::
equals
(
const
Configuration
&
other
)
const
{
// If number of loggers is different, then configurations aren't equal.
if
(
logging_info_
.
size
()
!=
other
.
logging_info_
.
size
())
{
return
(
false
);
}
// Pass through all loggers and try to find the match for each of them
// with the loggers from the other configuration. The order doesn't
// matter so we can't simply compare the vectors.
for
(
LoggingInfoStorage
::
const_iterator
this_it
=
logging_info_
.
begin
();
this_it
!=
logging_info_
.
end
();
++
this_it
)
{
bool
match
=
false
;
for
(
LoggingInfoStorage
::
const_iterator
other_it
=
other
.
logging_info_
.
begin
();
other_it
!=
other
.
logging_info_
.
end
();
++
other_it
)
{
if
(
this_it
->
equals
(
*
other_it
))
{
match
=
true
;
break
;
}
}
// No match found for the particular logger so return false.
if
(
!
match
)
{
return
(
false
);
}
}
// Logging information is equal between objects, so check other values.
return
(
cfg_iface_
==
other
.
cfg_iface_
);
}
}
}
src/lib/dhcpsrv/configuration.h
View file @
c6ab3eb8
...
...
@@ -16,7 +16,7 @@
#define DHCPSRV_CONFIGURATION_H
#include <dhcpsrv/cfg_iface.h>
#include <
log/logger_level
.h>
#include <
dhcpsrv/logging_info
.h>
#include <boost/shared_ptr.hpp>
#include <vector>
#include <stdint.h>
...
...
@@ -26,59 +26,6 @@ namespace dhcp {
class
CfgMgr
;
/// @brief Defines single logging destination
///
/// This structure is used to keep log4cplus configuration parameters.
struct
LoggingDestination
{
/// @brief defines logging destination output
///
/// Values accepted are: stdout, stderr, syslog, syslog:name.
/// Any other destination will be considered a file name.
std
::
string
output_
;
/// @brief Maximum number of log files in rotation
int
maxver_
;
/// @brief Maximum log file size
uint64_t
maxsize_
;
};
/// @brief structure that describes one logging entry
///
/// This is a structure that conveys one logger entry configuration.
/// The structure in JSON form has the following syntax:
/// {
/// "name": "*",
/// "output_options": [
/// {
/// "output": "/path/to/the/logfile.log",
/// "maxver": 8,
/// "maxsize": 204800
/// }
/// ],
/// "severity": "WARN",
/// "debuglevel": 99
/// },
struct
LoggingInfo
{
/// @brief logging name
std
::
string
name_
;
/// @brief describes logging severity
isc
::
log
::
Severity
severity_
;
/// @brief debuglevel (used when severity_ == DEBUG)
///
/// We use range 0(least verbose)..99(most verbose)
int
debuglevel_
;
/// @brief specific logging destinations
std
::
vector
<
LoggingDestination
>
destinations_
;
};
/// @brief storage for logging information in log4cplus format
typedef
std
::
vector
<
isc
::
dhcp
::
LoggingInfo
>
LoggingInfoStorage
;
/// @brief Specifies current DHCP configuration
///
...
...
@@ -186,6 +133,58 @@ public:
cfg_iface_
=
cfg_iface
;
}
/// @name Methods and operators used to compare configurations.
///
//@{
///
/// @brief Compares two objects for equality.
///
/// It ignores the configuration sequence number when checking for
/// equality of objects.
///
/// @param other An object to be compared with this object.
///
/// @return true if two objects are equal, false otherwise.
bool
equals
(
const
Configuration
&
other
)
const
;
/// @brief Compares two objects for inequality.
///
/// It ignores the configuration sequence number when checking for
/// inequality of objects.
///
/// @param other An object to be compared with this object.
///
/// @return true if two objects are not equal, false otherwise.
bool
nequals
(
const
Configuration
&
other
)
const
{
return
(
!
equals
(
other
));
}
/// @brief Equality operator.
///
/// It ignores the configuration sequence number when checking for
/// equality of objects.
///
/// @param other An object to be compared with this object.
///
/// @return true if two objects are equal, false otherwise.
bool
operator
==
(
const
Configuration
&
other
)
const
{
return
(
equals
(
other
));
}
/// @param other An object to be compared with this object.
///
/// It ignores the configuration sequence number when checking for
/// inequality of objects.
///
/// @param other An object to be compared with this object.
///
/// @return true if two objects are not equal, false otherwise.
bool
operator
!=
(
const
Configuration
&
other
)
const
{
return
(
nequals
(
other
));
}
//@}
private:
/// @brief Sequence number identifying the configuration.
...
...
src/lib/dhcpsrv/logging_info.cc
0 → 100644
View file @
c6ab3eb8
// Copyright (C) 2014 Internet Systems Consortium, Inc. ("ISC")
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
#include <dhcpsrv/logging_info.h>
namespace
isc
{
namespace
dhcp
{
bool
LoggingDestination
::
equals
(
const
LoggingDestination
&
other
)
const
{
return
(
output_
==
other
.
output_
&&
maxver_
==
other
.
maxver_
&&
maxsize_
==
other
.
maxsize_
);
}
bool
LoggingInfo
::
equals
(
const
LoggingInfo
&
other
)
const
{
// If number of destinations aren't equal, the objects are not equal.
if
(
destinations_
.
size
()
!=
other
.
destinations_
.
size
())
{
return
(
false
);
}
// If there is the same number of logging destinations verify that the
// destinations are equal. The order doesn't matter to we don't expect
// that they are at the same index of the vectors.
for
(
std
::
vector
<
LoggingDestination
>::
const_iterator
it_this
=
destinations_
.
begin
();
it_this
!=
destinations_
.
end
();
++
it_this
)
{
bool
match
=
false
;
for
(
std
::
vector
<
LoggingDestination
>::
const_iterator
it_other
=
other
.
destinations_
.
begin
();
it_other
!=
other
.
destinations_
.
end
();
++
it_other
)
{
if
(
it_this
->
equals
(
*
it_other
))
{
match
=
true
;
break
;
}
}
if
(
!
match
)
{
return
(
false
);
}
}
// Logging destinations are equal. Check the rest of the parameters for
// equality.
return
(
name_
==
other
.
name_
&&
severity_
==
other
.
severity_
&&
debuglevel_
==
other
.
debuglevel_
);
}
}
// end of namespace isc::dhcp
}
// end of namespace isc
src/lib/dhcpsrv/logging_info.h
0 → 100644
View file @
c6ab3eb8
// Copyright (C) 2014 Internet Systems Consortium, Inc. ("ISC")
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
#ifndef DHCPSRV_LOGGING_INFO_H
#define DHCPSRV_LOGGING_INFO_H
#include <log/logger_level.h>
#include <stdint.h>
#include <vector>
namespace
isc
{
namespace
dhcp
{
/// @brief Defines single logging destination
///
/// This structure is used to keep log4cplus configuration parameters.
struct
LoggingDestination
{
/// @brief defines logging destination output
///
/// Values accepted are: stdout, stderr, syslog, syslog:name.
/// Any other destination will be considered a file name.
std
::
string
output_
;
/// @brief Maximum number of log files in rotation
int
maxver_
;
/// @brief Maximum log file size
uint64_t
maxsize_
;
/// @brief Compares two objects for equality.
///
/// @param other Object to be compared with this object.
///
/// @return true if objects are equal, false otherwise.
bool
equals
(
const
LoggingDestination
&
other
)
const
;
/// @brief Default constructor.
LoggingDestination
()
:
output_
(
"stdout"
),
maxver_
(
1
),
maxsize_
(
204800
)
{
}
};
/// @brief structure that describes one logging entry
///
/// This is a structure that conveys one logger entry configuration.
/// The structure in JSON form has the following syntax:
/// {
/// "name": "*",
/// "output_options": [
/// {
/// "output": "/path/to/the/logfile.log",
/// "maxver": 8,
/// "maxsize": 204800
/// }
/// ],
/// "severity": "WARN",
/// "debuglevel": 99
/// },
struct
LoggingInfo
{
/// @brief logging name
std
::
string
name_
;
/// @brief describes logging severity
isc
::
log
::
Severity
severity_
;
/// @brief debuglevel (used when severity_ == DEBUG)
///
/// We use range 0(least verbose)..99(most verbose)
int
debuglevel_
;
/// @brief specific logging destinations
std
::
vector
<
LoggingDestination
>
destinations_
;
/// @brief Default constructor.
LoggingInfo
()
:
name_
(
"kea"
),
severity_
(
isc
::
log
::
INFO
),
debuglevel_
(
99
)
{
}
/// @brief Compares two objects for equality.
///
/// @param other An object to be compared with this object.
///
/// @return true if objects are equal, false otherwise.
bool
equals
(
const
LoggingInfo
&
other
)
const
;
/// @brief Compares two objects for equality.
///
/// @param other An object to be compared with this object.
///
/// @return true if objects are equal, false otherwise.
bool
operator
==
(
const
LoggingInfo
&
other
)
const
{
return
(
equals
(
other
));
}
/// @brief Compares two objects for inequality.
///
/// @param other An object to be compared with this object.
///
/// @return true if objects are not equal, false otherwise.
bool
operator
!=
(
const
LoggingInfo
&
other
)
const
{
return
(
!
equals
(
other
));
}
};
/// @brief storage for logging information in log4cplus format
typedef
std
::
vector
<
isc
::
dhcp
::
LoggingInfo
>
LoggingInfoStorage
;
}
}
#endif // DHCPSRV_LOGGING_INFO_H
src/lib/dhcpsrv/tests/Makefile.am
View file @
c6ab3eb8
...
...
@@ -69,6 +69,7 @@ libdhcpsrv_unittests_SOURCES += lease_unittest.cc
libdhcpsrv_unittests_SOURCES
+=
lease_mgr_factory_unittest.cc
libdhcpsrv_unittests_SOURCES
+=
lease_mgr_unittest.cc
libdhcpsrv_unittests_SOURCES
+=
logging_unittest.cc
libdhcpsrv_unittests_SOURCES
+=
logging_info_unittest.cc
libdhcpsrv_unittests_SOURCES
+=
generic_lease_mgr_unittest.cc generic_lease_mgr_unittest.h
libdhcpsrv_unittests_SOURCES
+=
memfile_lease_mgr_unittest.cc
libdhcpsrv_unittests_SOURCES
+=
dhcp_parsers_unittest.cc
...
...
src/lib/dhcpsrv/tests/configuration_unittest.cc
View file @
c6ab3eb8
...
...
@@ -14,6 +14,7 @@
#include <config.h>
#include <dhcp/tests/iface_mgr_test_config.h>
#include <dhcpsrv/cfgmgr.h>
#include <dhcpsrv/configuration.h>
#include <dhcpsrv/subnet.h>
...
...
@@ -38,7 +39,8 @@ public:
///
/// Creates IPv4 and IPv6 subnets for unit test. The number of subnets
/// is @c TEST_SUBNETS_NUM for IPv4 and IPv6 each.
ConfigurationTest
()
{
ConfigurationTest
()
:
iface_mgr_test_config_
(
true
)
{
// Remove any subnets dangling from previous unit tests.
clearSubnets
();
...
...
@@ -124,6 +126,8 @@ public:
Subnet4Collection
test_subnets4_
;
/// @brief A collection of IPv6 subnets used by unit tests.
Subnet6Collection
test_subnets6_
;
/// @brief Fakes interface configuration.
isc
::
dhcp
::
test
::
IfaceMgrTestConfig
iface_mgr_test_config_
;
};
...
...
@@ -256,4 +260,49 @@ TEST_F(ConfigurationTest, summarySubnets) {
conf_
.
getConfigSummary
(
Configuration
::
CFGSEL_SUBNET
));
}
// This test checks that two configurations can be compared for (in)equality.
TEST_F
(
ConfigurationTest
,
equality
)
{
Configuration
conf1
(
32
);
Configuration
conf2
(
64
);
// Initially, both objects should be equal, even though the configuration
// sequences are not matching.
EXPECT_TRUE
(
conf1
==
conf2
);
EXPECT_FALSE
(
conf1
!=
conf2
);
// Differ by logging information.
LoggingInfo
info1
;
LoggingInfo
info2
;
info1
.
name_
=
"foo"
;
info2
.
name_
=
"bar"
;
conf1
.
addLoggingInfo
(
info1
);
conf2
.
addLoggingInfo
(
info2
);
EXPECT_FALSE
(
conf1
==
conf2
);
EXPECT_TRUE
(
conf1
!=
conf2
);
conf1
.
addLoggingInfo
(
info2
);
conf2
.
addLoggingInfo
(
info1
);
EXPECT_TRUE
(
conf1
==
conf2
);
EXPECT_FALSE
(
conf1
!=
conf2
);
// Differ by interface configuration.
CfgIface
cfg_iface1
;
CfgIface
cfg_iface2
;
cfg_iface1
.
use
(
CfgIface
::
V4
,
"eth0"
);
conf1
.
setCfgIface
(
cfg_iface1
);
EXPECT_FALSE
(
conf1
==
conf2
);
EXPECT_TRUE
(
conf1
!=
conf2
);
cfg_iface2
.
use
(
CfgIface
::
V4
,
"eth0"
);
conf2
.
setCfgIface
(
cfg_iface2
);
EXPECT_TRUE
(
conf1
==
conf2
);
EXPECT_FALSE
(
conf1
!=
conf2
);
}
}
// end of anonymous namespace
src/lib/dhcpsrv/tests/logging_info_unittest.cc
0 → 100644
View file @
c6ab3eb8
// Copyright (C) 2014 Internet Systems Consortium, Inc. ("ISC")
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
#include <config.h>
#include <dhcpsrv/logging_info.h>
#include <gtest/gtest.h>
using
namespace
isc
::
dhcp
;
namespace
{
// Checks if two destinations can be compared for equality.
TEST
(
LoggingDestintaion
,
equals
)
{
LoggingDestination
dest1
;
LoggingDestination
dest2
;
EXPECT_TRUE
(
dest1
.
equals
(
dest2
));
dest1
.
output_
=
"stderr"
;
EXPECT_FALSE
(
dest1
.
equals
(
dest2
));
dest2
.
output_
=
"stdout"
;
EXPECT_FALSE
(
dest1
.
equals
(
dest2
));
dest2
.
output_
=
"stderr"
;
EXPECT_TRUE
(
dest1
.
equals
(
dest2
));
dest1
.
maxver_
=
10
;
dest2
.
maxver_
=
5
;
EXPECT_FALSE
(
dest1
.
equals
(
dest2
));
dest2
.
maxver_
=
10
;
EXPECT_TRUE
(
dest1
.
equals
(
dest2
));
dest1
.
maxsize_
=
64
;
dest2
.
maxsize_
=
32
;
EXPECT_FALSE
(
dest1
.
equals
(
dest2
));
dest1
.
maxsize_
=
32
;
EXPECT_TRUE
(
dest1
.
equals
(
dest2
));
}
// Checks if (in)equality operators work for LoggingInfo.
TEST
(
LoggingInfo
,
equality
)
{
LoggingInfo
info1
;
LoggingInfo
info2
;
// Initially, both objects are the same.
EXPECT_TRUE
(
info1
==
info2
);
// Differ by name.
info1
.
name_
=
"foo"
;
info2
.
name_
=
"bar"
;
EXPECT_FALSE
(
info1
==
info2
);
EXPECT_TRUE
(
info1
!=
info2
);
// Names equal.
info2
.
name_
=
"foo"
;
EXPECT_TRUE
(
info1
==
info2
);
EXPECT_FALSE
(
info1
!=
info2
);
// Differ by severity.
info1
.
severity_
=
isc
::
log
::
DEBUG
;
info2
.
severity_
=
isc
::
log
::
INFO
;
EXPECT_FALSE
(
info1
==
info2
);
EXPECT_TRUE
(
info1
!=
info2
);
// Severities equal.
info2
.
severity_
=
isc
::
log
::
DEBUG
;
EXPECT_TRUE
(
info1
==
info2
);
EXPECT_FALSE
(
info1
!=
info2
);
// Differ by debug level.
info1
.
debuglevel_
=
99
;
info2
.
debuglevel_
=
1
;
EXPECT_FALSE
(
info1
==
info2
);
EXPECT_TRUE
(
info1
!=
info2
);
// Debug level equal.
info2
.
debuglevel_
=
99
;
EXPECT_TRUE
(
info1
==
info2
);
EXPECT_FALSE
(
info1
!=
info2
);
// Create two different desinations.
LoggingDestination
dest1
;
LoggingDestination
dest2
;
dest1
.
output_
=
"foo"
;
dest2
.
output_
=
"bar"
;
// Push destinations in some order to info1.
info1
.
destinations_
.
push_back
(
dest1
);
info1
.
destinations_
.
push_back
(
dest2
);
// Push in reverse order to info2. Order shouldn't matter.
info2
.
destinations_
.
push_back
(
dest2
);
info2
.
destinations_
.
push_back
(
dest1
);
EXPECT_TRUE
(
info1
==
info2
);
EXPECT_FALSE
(
info1
!=
info2
);
// Change one of the destinations.
LoggingDestination
dest3
;
dest3
.
output_
=
"foobar"
;
info2
.
destinations_
[
1
]
=
dest3
;
// The should now be unequal.
EXPECT_FALSE
(
info1
==
info2
);
EXPECT_TRUE
(
info1
!=
info2
);
}
}
// end of anonymous namespace
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