Commit 263be04a authored by Marcin Siodelski's avatar Marcin Siodelski

[3604] The CfgIface is returned by a pointer, not reference.

parent 543c1550
......@@ -151,9 +151,8 @@ ControlledDhcpv4Srv::processConfig(isc::data::ConstElementPtr config) {
// log warnings. Since we allow that this fails for some interfaces there
// is no need to rollback configuration if socket fails to open on any
// of the interfaces.
CfgMgr::instance().getStagingCfg()->
getCfgIface().openSockets(AF_INET, srv->getPort(),
getInstance()->useBroadcast());
CfgMgr::instance().getStagingCfg()->getCfgIface()->
openSockets(AF_INET, srv->getPort(), getInstance()->useBroadcast());
return (answer);
}
......
......@@ -964,6 +964,7 @@ TEST_F(Dhcp4ParserTest, nextServerSubnet) {
// Test checks several negative scenarios for next-server configuration: bogus
// address, IPv6 adddress and empty string.
TEST_F(Dhcp4ParserTest, nextServerNegative) {
IfaceMgrTestConfig test_config(true);
ConstElementPtr status;
......@@ -1012,10 +1013,14 @@ TEST_F(Dhcp4ParserTest, nextServerNegative) {
checkResult(status, 1);
EXPECT_TRUE(errorContainsPosition(status, "<string>"));
CfgMgr::instance().clear();
EXPECT_NO_THROW(status = configureDhcp4Server(*srv_, json2));
checkResult(status, 1);
EXPECT_TRUE(errorContainsPosition(status, "<string>"));
CfgMgr::instance().clear();
EXPECT_NO_THROW(status = configureDhcp4Server(*srv_, json3));
checkResult(status, 0);
EXPECT_FALSE(errorContainsPosition(status, "<string>"));
......@@ -2970,8 +2975,7 @@ TEST_F(Dhcp4ParserTest, selectedInterfaces) {
ASSERT_TRUE(status);
checkResult(status, 0);
CfgMgr::instance().getStagingCfg()->
getCfgIface().openSockets(AF_INET, 10000);
CfgMgr::instance().getStagingCfg()->getCfgIface()->openSockets(AF_INET, 10000);
// eth0 and eth1 were explicitly selected. eth2 was not.
EXPECT_TRUE(test_config.socketOpen("eth0", AF_INET));
......@@ -3008,8 +3012,7 @@ TEST_F(Dhcp4ParserTest, allInterfaces) {
ASSERT_TRUE(status);
checkResult(status, 0);
CfgMgr::instance().getStagingCfg()->
getCfgIface().openSockets(AF_INET, 10000);
CfgMgr::instance().getStagingCfg()->getCfgIface()->openSockets(AF_INET, 10000);
// All interfaces should be now active.
ASSERT_TRUE(test_config.socketOpen("eth0", AF_INET));
......@@ -3044,8 +3047,7 @@ TEST_F(Dhcp4ParserTest, selectedInterfacesAndAddresses) {
ASSERT_TRUE(status);
checkResult(status, 0);
CfgMgr::instance().getStagingCfg()->
getCfgIface().openSockets(AF_INET, 10000);
CfgMgr::instance().getStagingCfg()->getCfgIface()->openSockets(AF_INET, 10000);
// An address on eth0 was selected
EXPECT_TRUE(test_config.socketOpen("eth0", "10.0.0.1"));
......
......@@ -3089,7 +3089,7 @@ TEST_F(Dhcpv4SrvTest, vendorOptionsORO) {
TEST_F(Dhcpv4SrvTest, vendorOptionsDocsisDefinitions) {
ConstElementPtr x;
string config_prefix = "{ \"interfaces-config\": {"
" \"interfaces\": [ \"*\" ]"
" \"interfaces\": [ ]"
"},"
"\"rebind-timer\": 2000, "
"\"renew-timer\": 1000, "
......
......@@ -146,8 +146,7 @@ ControlledDhcpv6Srv::processConfig(isc::data::ConstElementPtr config) {
// log warnings. Since we allow that this fails for some interfaces there
// is no need to rollback configuration if socket fails to open on any
// of the interfaces.
CfgMgr::instance().getStagingCfg()->
getCfgIface().openSockets(AF_INET6, srv->getPort());
CfgMgr::instance().getStagingCfg()->getCfgIface()->openSockets(AF_INET6, srv->getPort());
return (answer);
}
......
......@@ -354,7 +354,9 @@ public:
/// test to make sure that contents of the database do not affect the
/// results of subsequent tests.
void resetConfiguration() {
string config = "{ " + genIfaceConfig() + ","
string config = "{ \"interfaces-config\": {"
" \"interfaces\": [ ]"
"},"
"\"hooks-libraries\": [ ],"
"\"preferred-lifetime\": 3000,"
"\"rebind-timer\": 2000, "
......@@ -3104,8 +3106,7 @@ TEST_F(Dhcp6ParserTest, selectedInterfaces) {
// as the pool does not belong to that subnet
checkResult(status, 0);
CfgMgr::instance().getStagingCfg()->
getCfgIface().openSockets(AF_INET6, 10000);
CfgMgr::instance().getStagingCfg()->getCfgIface()->openSockets(AF_INET6, 10000);
// eth0 and eth1 were explicitly selected. eth2 was not.
EXPECT_TRUE(test_config.socketOpen("eth0", AF_INET6));
......@@ -3140,8 +3141,7 @@ TEST_F(Dhcp6ParserTest, allInterfaces) {
EXPECT_NO_THROW(status = configureDhcp6Server(srv_, json));
checkResult(status, 0);
CfgMgr::instance().getStagingCfg()->
getCfgIface().openSockets(AF_INET6, 10000);
CfgMgr::instance().getStagingCfg()->getCfgIface()->openSockets(AF_INET6, 10000);
// All interfaces should be now active.
EXPECT_TRUE(test_config.socketOpen("eth0", AF_INET6));
......
......@@ -1659,7 +1659,7 @@ TEST_F(Dhcpv6SrvTest, vendorOptionsORO) {
TEST_F(Dhcpv6SrvTest, vendorOptionsDocsisDefinitions) {
ConstElementPtr x;
string config_prefix = "{ \"interfaces-config\": {"
" \"interfaces\": [ \"*\" ]"
" \"interfaces\": [ ]"
"},"
"\"preferred-lifetime\": 3000,"
"\"rebind-timer\": 2000, "
......
......@@ -197,7 +197,8 @@ IfaceMgr::IfaceMgr()
:control_buf_len_(CMSG_SPACE(sizeof(struct in6_pktinfo))),
control_buf_(new char[control_buf_len_]),
packet_filter_(new PktFilterInet()),
packet_filter6_(new PktFilterInet6())
packet_filter6_(new PktFilterInet6()),
test_mode_(false)
{
try {
......
......@@ -534,6 +534,26 @@ public:
/// @return the only existing instance of interface manager
static IfaceMgr& instance();
/// @brief Sets or clears the test mode for @c IfaceMgr.
///
/// Various unit test may set this flag to true, to indicate that the
/// @c IfaceMgr is in the test mode. There are places in the code that
/// modify the behavior depending if the @c IfaceMgr is in the test
/// mode or not.
///
/// @param test_mode A flag which indicates that the @c IfaceMgr is in the
/// test mode (if true), or not (if false).
void setTestMode(const bool test_mode) {
test_mode_ = test_mode;
}
/// @brief Checks if the @c IfaceMgr is in the test mode.
///
/// @return true if the @c IfaceMgr is in the test mode, false otherwise.
bool isTestMode() const {
return (test_mode_);
}
/// @brief Check if packet be sent directly to the client having no address.
///
/// Checks if IfaceMgr can send DHCPv4 packet to the client
......@@ -1167,6 +1187,9 @@ private:
/// @brief Contains list of callbacks for external sockets
SocketCallbackInfoContainer callbacks_;
/// @brief Indicates if the IfaceMgr is in the test mode.
bool test_mode_;
};
}; // namespace isc::dhcp
......
......@@ -26,6 +26,7 @@ namespace dhcp {
namespace test {
IfaceMgrTestConfig::IfaceMgrTestConfig(const bool default_config) {
IfaceMgr::instance().setTestMode(true);
IfaceMgr::instance().closeSockets();
IfaceMgr::instance().clearIfaces();
packet_filter4_ = PktFilterPtr(new PktFilterTestStub());
......@@ -44,7 +45,7 @@ IfaceMgrTestConfig::~IfaceMgrTestConfig() {
IfaceMgr::instance().clearIfaces();
IfaceMgr::instance().setPacketFilter(PktFilterPtr(new PktFilterInet()));
IfaceMgr::instance().setPacketFilter(PktFilter6Ptr(new PktFilterInet6()));
IfaceMgr::instance().setTestMode(false);
IfaceMgr::instance().detectIfaces();
}
......
......@@ -26,7 +26,7 @@ namespace dhcp {
const char* CfgIface::ALL_IFACES_KEYWORD = "*";
CfgIface::CfgIface()
: wildcard_used_(false), socket_type_(SOCKET_DEFAULT) {
: wildcard_used_(false), socket_type_(SOCKET_DGRAM) {
}
void
......@@ -60,7 +60,9 @@ CfgIface::openSockets(const uint16_t family, const uint16_t port,
// IP address. This should effectively turn on the use of raw
// sockets. However, this may be unsupported on some operating
// systems, so there is no guarantee.
if ((family == AF_INET) && (socket_type_ != SOCKET_DEFAULT)) {
if ((family == AF_INET) && (!IfaceMgr::instance().isTestMode())) {
LOG_INFO(dhcpsrv_logger, DHCPSRV_CFGMGR_SOCKET_TYPE_SELECT)
.arg(socketTypeToText());
iface_mgr.setMatchingPacketFilter(socket_type_ == SOCKET_RAW);
if ((socket_type_ == SOCKET_RAW) &&
!iface_mgr.isDirectResponseSupported()) {
......@@ -151,7 +153,7 @@ CfgIface::reset() {
wildcard_used_ = false;
iface_set_.clear();
address_map_.clear();
socket_type_ = SOCKET_DEFAULT;
useSocketType(AF_INET, SOCKET_DGRAM);
}
void
......@@ -186,6 +188,22 @@ CfgIface::socketOpenErrorHandler(const std::string& errmsg) {
LOG_WARN(dhcpsrv_logger, DHCPSRV_OPEN_SOCKET_FAIL).arg(errmsg);
}
std::string
CfgIface::socketTypeToText() const {
switch (socket_type_) {
case SOCKET_RAW:
return ("raw");
case SOCKET_DGRAM:
return ("datagram");
default:
;
}
isc_throw(Unexpected, "unsupported socket type " << socket_type_);
}
void
CfgIface::use(const uint16_t family, const std::string& iface_name) {
// The interface name specified may have two formats, e.g.:
......@@ -338,10 +356,6 @@ CfgIface::useSocketType(const uint16_t family,
if (family != AF_INET) {
isc_throw(InvalidSocketType, "socket type must not be specified for"
" the DHCPv6 server");
} else if (socket_type == SOCKET_DEFAULT) {
isc_throw(InvalidSocketType, "invalid value SOCKET_DEFAULT"
" used to specify the socket type to be used by"
" the DHCPv4 server");
}
socket_type_ = socket_type;
}
......
......@@ -16,8 +16,10 @@
#define CFG_IFACE_H
#include <asiolink/io_address.h>
#include <boost/shared_ptr.hpp>
#include <map>
#include <set>
#include <string>
namespace isc {
namespace dhcp {
......@@ -81,12 +83,7 @@ public:
/// in such case the use of datagram sockets is preferred. The type of the
/// sockets to be opened is specified using one of the
/// @c CfgIface::useSocketType method variants. The @c CfgIface::SocketType
/// enumeration specifies the possible values. The @c CfgIface::SOCKET_DEFAULT
/// is a default setting of the @c CfgIface and it indicates that the
/// @c IfaceMgr should continue using the currently used sockets' type.
/// This is mostly used for unit testing to avoid modifying fake
/// configurations of the @c IfaceMgr. In the real case, one of the
/// remaining values should be used.
/// enumeration specifies the possible values.
///
/// @warning This class makes use of the AF_INET and AF_INET6 family literals,
/// but it doesn't verify that the address family value passed as @c uint16_t
......@@ -97,8 +94,6 @@ public:
/// @brief Socket type used by the DHCPv4 server.
enum SocketType {
/// Default socket type, mainly used for testing.
SOCKET_DEFAULT,
/// Raw socket, used for direct DHCPv4 traffic.
SOCKET_RAW,
/// Datagram socket, i.e. IP/UDP socket.
......@@ -247,6 +242,9 @@ private:
/// @param errmsg Error message being logged by the function.
static void socketOpenErrorHandler(const std::string& errmsg);
/// @brief Returns the socket type in the textual format.
std::string socketTypeToText() const;
/// @brief Represents a set of interface names.
typedef std::set<std::string> IfaceSet;
......@@ -269,6 +267,12 @@ private:
SocketType socket_type_;
};
/// @brief A pointer to the @c CfgIface .
typedef boost::shared_ptr<CfgIface> CfgIfacePtr;
/// @brief A pointer to the const @c CfgIface.
typedef boost::shared_ptr<const CfgIface> ConstCfgIfacePtr;
}
}
......
......@@ -111,6 +111,15 @@ back to use of the datagram IP/UDP sockets. The responses to
the directly connected clients will be broadcast. The responses
to relayed clients will be unicast as usual.
% DHCPSRV_CFGMGR_SOCKET_TYPE_SELECT trying to use socket type %1
This informational message is logged when the DHCPv4 server selects the
socket type to be used for all sockets that will be opened on the
interfaces. Typically, the socket type is specified by the server
administrator. If the socket type hasn't been specified, the raw
socket will be selected. If the raw socket has been selected but
Kea doesn't support the use of raw sockets on the particular
OS, it will use a datagram socket instead.
% DHCPSRV_CFGMGR_SUBNET4 retrieved subnet %1 for address hint %2
This is a debug message reporting that the DHCP configuration manager has
returned the specified IPv4 subnet when given the address hint specified
......
......@@ -30,19 +30,18 @@ InterfaceListConfigParser::InterfaceListConfigParser(const int protocol)
void
InterfaceListConfigParser::build(ConstElementPtr value) {
CfgIface cfg_iface;
CfgIfacePtr cfg_iface = CfgMgr::instance().getStagingCfg()->getCfgIface();
BOOST_FOREACH(ConstElementPtr iface, value->listValue()) {
std::string iface_name = iface->stringValue();
try {
cfg_iface.use(protocol_, iface_name);
cfg_iface->use(protocol_, iface_name);
} catch (const std::exception& ex) {
isc_throw(DhcpConfigError, "Failed to select interface: "
<< ex.what() << " (" << value->getPosition() << ")");
}
}
CfgMgr::instance().getStagingCfg()->setCfgIface(cfg_iface);
}
void
......@@ -86,12 +85,17 @@ IfacesConfigParser4::IfacesConfigParser4()
void
IfacesConfigParser4::build(isc::data::ConstElementPtr ifaces_config) {
IfacesConfigParser::build(ifaces_config);
// Get the pointer to the interface configuration.
CfgIfacePtr cfg = CfgMgr::instance().getStagingCfg()->getCfgIface();
// The default is to use the raw sockets, if the "socket-type" parameter
// hasn't been specified.
cfg->useSocketType(AF_INET, CfgIface::SOCKET_RAW);
BOOST_FOREACH(ConfigPair element, ifaces_config->mapValue()) {
try {
if (element.first == "socket-type") {
CfgIface cfg = CfgMgr::instance().getStagingCfg()->getCfgIface();
cfg.useSocketType(AF_INET, element.second->stringValue());
CfgMgr::instance().getStagingCfg()->setCfgIface(cfg);
cfg->useSocketType(AF_INET, element.second->stringValue());
} else if (!isGenericParameter(element.first)) {
isc_throw(DhcpConfigError, "usupported parameter '"
......
// Copyright (C) 2014 Internet Systems Consortium, Inc. ("ISC")
// Copyright (C) 2014-2015 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
......@@ -26,15 +26,17 @@ namespace isc {
namespace dhcp {
SrvConfig::SrvConfig()
: sequence_(0), cfg_option_def_(new CfgOptionDef()),
cfg_option_(new CfgOption()), cfg_subnets4_(new CfgSubnets4()),
cfg_subnets6_(new CfgSubnets6()), cfg_hosts_(new CfgHosts()) {
: sequence_(0), cfg_iface_(new CfgIface()),
cfg_option_def_(new CfgOptionDef()), cfg_option_(new CfgOption()),
cfg_subnets4_(new CfgSubnets4()), cfg_subnets6_(new CfgSubnets6()),
cfg_hosts_(new CfgHosts()) {
}
SrvConfig::SrvConfig(const uint32_t sequence)
: sequence_(sequence), cfg_option_def_(new CfgOptionDef()),
cfg_option_(new CfgOption()), cfg_subnets4_(new CfgSubnets4()),
cfg_subnets6_(new CfgSubnets6()), cfg_hosts_(new CfgHosts()) {
: sequence_(sequence), cfg_iface_(new CfgIface()),
cfg_option_def_(new CfgOptionDef()), cfg_option_(new CfgOption()),
cfg_subnets4_(new CfgSubnets4()), cfg_subnets6_(new CfgSubnets6()),
cfg_hosts_(new CfgHosts()) {
}
std::string
......@@ -92,7 +94,7 @@ SrvConfig::copy(SrvConfig& new_config) const {
new_config.addLoggingInfo(*it);
}
// Replace interface configuration.
new_config.setCfgIface(cfg_iface_);
new_config.cfg_iface_.reset(new CfgIface(*cfg_iface_));
// Replace option definitions.
cfg_option_def_->copyTo(*new_config.cfg_option_def_);
cfg_option_->copyTo(*new_config.cfg_option_);
......@@ -137,7 +139,7 @@ SrvConfig::equals(const SrvConfig& other) const {
}
}
// Logging information is equal between objects, so check other values.
return ((cfg_iface_ == other.cfg_iface_) &&
return ((*cfg_iface_ == *other.cfg_iface_) &&
(*cfg_option_def_ == *other.cfg_option_def_) &&
(*cfg_option_ == *other.cfg_option_));
}
......
// Copyright (C) 2014 Internet Systems Consortium, Inc. ("ISC")
// Copyright (C) 2014-2015 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
......@@ -129,21 +129,24 @@ public:
logging_info_.push_back(logging_info);
}
/// @brief Returns object which represents selection of interfaces.
/// @brief Returns non-const pointer to interface configuration.
///
/// This function returns a reference to the object which represents the
/// set of interfaces being used to receive DHCP traffic.
/// This function returns a non-const pointer to the interface
/// configuration.
///
/// @return Object representing selection of interfaces.
const CfgIface& getCfgIface() const {
/// @return Object representing configuration of interfaces.
CfgIfacePtr getCfgIface() {
return (cfg_iface_);
}
/// @brief Sets the object representing selection of interfaces.
/// @brief Returns const pointer to interface configuration.
///
/// @param cfg_iface Object representing selection of interfaces.
void setCfgIface(const CfgIface& cfg_iface) {
cfg_iface_ = cfg_iface;
/// This function returns a const pointer to the interface
/// configuration.
///
/// @return Object representing configuration of interfaces.
ConstCfgIfacePtr getCfgIface() const {
return (cfg_iface_);
}
/// @brief Return pointer to non-const object representing user-defined
......@@ -340,7 +343,7 @@ private:
///
/// Used to select interfaces on which the DHCP server will listen to
/// queries.
CfgIface cfg_iface_;
CfgIfacePtr cfg_iface_;
/// @brief Pointer to option definitions configuration.
///
......
......@@ -377,8 +377,6 @@ TEST(CfgIfaceNoStubTest, useSocketType) {
ASSERT_TRUE(IfaceMgr::instance().isDirectResponseSupported());
// Test invalid values.
EXPECT_THROW(cfg.useSocketType(AF_INET, CfgIface::SOCKET_DEFAULT),
InvalidSocketType);
EXPECT_THROW(cfg.useSocketType(AF_INET, "default"),
InvalidSocketType);
EXPECT_THROW(cfg.useSocketType(AF_INET6, "datagram"),
......
......@@ -70,14 +70,14 @@ TEST_F(IfacesConfigParserTest, interfaces) {
// Open sockets according to the parsed configuration.
SrvConfigPtr cfg = CfgMgr::instance().getStagingCfg();
ASSERT_TRUE(cfg);
ASSERT_NO_THROW(cfg->getCfgIface().openSockets(AF_INET, 10000));
ASSERT_NO_THROW(cfg->getCfgIface()->openSockets(AF_INET, 10000));
// Only eth0 should have an open socket.
EXPECT_TRUE(test_config.socketOpen("eth0", AF_INET));
EXPECT_FALSE(test_config.socketOpen("eth1", AF_INET));
// Reset configuration.
cfg->getCfgIface().closeSockets();
cfg->getCfgIface()->closeSockets();
CfgMgr::instance().clear();
// Try similar configuration but this time add a wildcard interface
......@@ -88,7 +88,7 @@ TEST_F(IfacesConfigParserTest, interfaces) {
ASSERT_NO_THROW(parser.build(config_element));
cfg = CfgMgr::instance().getStagingCfg();
ASSERT_NO_THROW(cfg->getCfgIface().openSockets(AF_INET, 10000));
ASSERT_NO_THROW(cfg->getCfgIface()->openSockets(AF_INET, 10000));
EXPECT_TRUE(test_config.socketOpen("eth0", AF_INET));
EXPECT_TRUE(test_config.socketOpen("eth1", AF_INET));
......@@ -116,7 +116,7 @@ TEST_F(IfacesConfigParserTest, socketTypeRaw) {
SrvConfigPtr cfg = CfgMgr::instance().getStagingCfg();
ASSERT_TRUE(cfg);
cfg_ref.useSocketType(AF_INET, CfgIface::SOCKET_RAW);
EXPECT_TRUE(cfg->getCfgIface() == cfg_ref);
EXPECT_TRUE(*cfg->getCfgIface() == cfg_ref);
}
// This test verifies that it is possible to select the datagram socket
......@@ -141,7 +141,7 @@ TEST_F(IfacesConfigParserTest, socketTypeDatagram) {
SrvConfigPtr cfg = CfgMgr::instance().getStagingCfg();
ASSERT_TRUE(cfg);
cfg_ref.useSocketType(AF_INET, CfgIface::SOCKET_DGRAM);
EXPECT_TRUE(cfg->getCfgIface() == cfg_ref);
EXPECT_TRUE(*cfg->getCfgIface() == cfg_ref);
}
// Test that the configuration rejects the invalid socket type.
......
......@@ -94,7 +94,7 @@ public:
///
template<typename LeaseObjectType, typename LeaseFileType,
typename StorageType>
void writeLeases(LeaseFileType lease_file,
void writeLeases(LeaseFileType& lease_file,
const StorageType& storage,
const std::string& compare) {
// Prepare for a new file, close and remove the old
......
// Copyright (C) 2014 Internet Systems Consortium, Inc. ("ISC")
// Copyright (C) 2014-2015 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
......@@ -254,11 +254,8 @@ TEST_F(SrvConfigTest, copy) {
info.destinations_.push_back(LoggingDestination());
// Set interface configuration for conf1.
CfgIface cfg_iface;
cfg_iface.use(AF_INET, "eth0");
conf1.getCfgIface()->use(AF_INET, "eth0");
conf1.addLoggingInfo(info);
conf1.setCfgIface(cfg_iface);
// Add option definition.
OptionDefinitionPtr def(new OptionDefinition("option-foo", 5, "string"));
......@@ -310,17 +307,12 @@ TEST_F(SrvConfigTest, equality) {
EXPECT_FALSE(conf1 != conf2);
// Differ by interface configuration.
CfgIface cfg_iface1;
CfgIface cfg_iface2;
cfg_iface1.use(AF_INET, "eth0");
conf1.setCfgIface(cfg_iface1);
conf1.getCfgIface()->use(AF_INET, "eth0");
EXPECT_FALSE(conf1 == conf2);
EXPECT_TRUE(conf1 != conf2);
cfg_iface2.use(AF_INET, "eth0");
conf2.setCfgIface(cfg_iface2);
conf2.getCfgIface()->use(AF_INET, "eth0");
EXPECT_TRUE(conf1 == conf2);
EXPECT_FALSE(conf1 != conf2);
......
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