Currently job artifacts in CI/CD pipelines on LRZ GitLab never expire. Starting from Wed 26.1.2022 the default expiration time will be 30 days (GitLab default). Currently existing artifacts in already completed jobs will not be affected by the change. The latest artifacts for all jobs in the latest successful pipelines will be kept. More information: https://gitlab.lrz.de/help/user/admin_area/settings/continuous_integration.html#default-artifacts-expiration

Commit 835657e6 authored by Alessio Netti's avatar Alessio Netti
Browse files

Operations metadata field now uses set<string>

- The new implementation allows to publish multiple operations
concurrently from different sources
parent 3debc5c1
......@@ -28,7 +28,7 @@
#ifndef PROJECT_METADATASTORE_H
#define PROJECT_METADATASTORE_H
#include <vector>
#include <set>
#include <unordered_map>
#include <string>
#include <boost/property_tree/ptree.hpp>
......@@ -42,6 +42,7 @@
using namespace std;
//TODO: evaluate if necessary to change TTL type to int64 with default -1 value
class SensorMetadata {
public:
......@@ -62,52 +63,52 @@ public:
SensorMetadata(const SensorMetadata& other) {
SensorMetadata();
if(other.isOperation)
this->isOperation = std::make_shared<bool>(*other.isOperation);
setIsOperation(*other.isOperation);
if(other.isVirtual)
this->isVirtual = std::make_shared<bool>(*other.isVirtual);
setIsVirtual(*other.isVirtual);
if(other.integrable)
this->integrable = std::make_shared<bool>(*other.integrable);
setIntegrable(*other.integrable);
if(other.monotonic)
this->monotonic = std::make_shared<bool>(*other.monotonic);
setMonotonic(*other.monotonic);
if(other.publicName)
this->publicName = std::make_shared<string>(*other.publicName);
setPublicName(*other.publicName);
if(other.pattern)
this->pattern = std::make_shared<string>(*other.pattern);
setPattern(*other.pattern);
if(other.unit)
this->unit = std::make_shared<string>(*other.unit);
setUnit(*other.unit);
if(other.scale)
this->scale = std::make_shared<double>(*other.scale);
setScale(*other.scale);
if(other.ttl)
this->ttl = std::make_shared<uint64_t>(*other.ttl);
setTTL(*other.ttl);
if(other.interval)
this->interval = std::make_shared<uint64_t>(*other.interval);
setInterval(*other.interval);
if(other.operations)
this->operations = std::make_shared<string>(*other.operations);
setOperations(*other.operations);
}
SensorMetadata& operator=(const SensorMetadata& other) {
if(other.isOperation)
this->isOperation = std::make_shared<bool>(*other.isOperation);
setIsOperation(*other.isOperation);
if(other.isVirtual)
this->isVirtual = std::make_shared<bool>(*other.isVirtual);
setIsVirtual(*other.isVirtual);
if(other.integrable)
this->integrable = std::make_shared<bool>(*other.integrable);
setIntegrable(*other.integrable);
if(other.monotonic)
this->monotonic = std::make_shared<bool>(*other.monotonic);
setMonotonic(*other.monotonic);
if(other.publicName)
this->publicName = std::make_shared<string>(*other.publicName);
setPublicName(*other.publicName);
if(other.pattern)
this->pattern = std::make_shared<string>(*other.pattern);
setPattern(*other.pattern);
if(other.unit)
this->unit = std::make_shared<string>(*other.unit);
setUnit(*other.unit);
if(other.scale)
this->scale = std::make_shared<double>(*other.scale);
setScale(*other.scale);
if(other.ttl)
this->ttl = std::make_shared<uint64_t>(*other.ttl);
setTTL(*other.ttl);
if(other.interval)
this->interval = std::make_shared<uint64_t>(*other.interval);
setInterval(*other.interval);
if(other.operations)
this->operations = std::make_shared<string>(*other.operations);
setOperations(*other.operations);
return *this;
}
......@@ -135,27 +136,27 @@ public:
void parsePTREE(boost::property_tree::iptree& config) {
BOOST_FOREACH(boost::property_tree::iptree::value_type &val, config) {
if (boost::iequals(val.first, "monotonic")) {
this->monotonic = std::make_shared<bool>(to_bool(val.second.data()));
setMonotonic(to_bool(val.second.data()));
} else if (boost::iequals(val.first, "isVirtual")) {
this->isVirtual = std::make_shared<bool>(to_bool(val.second.data()));
setIsVirtual(to_bool(val.second.data()));
} else if (boost::iequals(val.first, "isOperation")) {
this->isOperation = std::make_shared<bool>(to_bool(val.second.data()));
setIsOperation(to_bool(val.second.data()));
} else if (boost::iequals(val.first, "integrable")) {
this->integrable = std::make_shared<bool>(to_bool(val.second.data()));
setIntegrable(to_bool(val.second.data()));
} else if (boost::iequals(val.first, "unit")) {
this->unit = std::make_shared<string>(val.second.data());
setUnit(val.second.data());
} else if (boost::iequals(val.first, "publicName")) {
this->publicName = std::make_shared<string>(val.second.data());
setPublicName(val.second.data());
} else if (boost::iequals(val.first, "pattern")) {
this->pattern = std::make_shared<string>(val.second.data());
setPattern(val.second.data());
} else if (boost::iequals(val.first, "scale")) {
this->scale = std::make_shared<double>(stod(val.second.data()));
setScale(stod(val.second.data()));
} else if (boost::iequals(val.first, "interval")) {
this->interval = std::make_shared<uint64_t>(stoull(val.second.data()) * 1000000);
setInterval(stoull(val.second.data()) * 1000000);
} else if (boost::iequals(val.first, "ttl")) {
this->ttl = std::make_shared<uint64_t>(stoull(val.second.data()) * 1000000);
setTTL(stoull(val.second.data()) * 1000000);
} else if (boost::iequals(val.first, "operations")) {
this->operations = std::make_shared<string>(_sanitizeOperations(val.second.data()));
setOperations(val.second.data());
}
}
}
......@@ -197,7 +198,8 @@ public:
const double* getScale() const { return scale.get(); }
const uint64_t* getTTL() const { return ttl.get(); }
const uint64_t* getInterval() const { return interval.get(); }
const string* getOperations() const { return operations.get(); }
const set<string>* getOperations() const { return operations.get(); }
const string getOperationsString() const { return operations ? _dumpOperations() : ""; }
void setIsOperation(bool o) { isOperation = std::make_shared<bool>(o); }
void setIsVirtual(bool v) { isVirtual = std::make_shared<bool>(v); }
......@@ -209,16 +211,23 @@ public:
void setScale(double s) { scale = std::make_shared<double>(s); }
void setTTL(uint64_t t) { ttl = std::make_shared<uint64_t>(t); }
void setInterval(uint64_t i) { interval = std::make_shared<uint64_t>(i); }
void setOperations(const string& o) { operations = std::make_shared<string>(o); }
void setOperations(const string& o) { setOperations(_parseOperations(o)); }
void clearOperations() { operations.reset(); }
// Merges a set of operations with the local one
void setOperations(const set<string>& o) {
if(!operations)
operations = std::make_shared<set<string>>(o);
else
operations->insert(o.begin(), o.end());
}
// Adds a single operation. Requires the publicName field to be set.
// Here the operation is assumed to be the full sensor name, from which the actual operation name is extracted.
bool addOperation(const string& opName) {
if (publicName && publicName->length()>0 && opName.length()>publicName->length()
&& !opName.compare(0, publicName->length(), *publicName)) {
if(!operations || operations->length()==0)
operations = std::make_shared<string>(opName.substr(publicName->length()));
else
*operations += "," + opName.substr(publicName->length());
setOperations(opName.substr(publicName->length()));
return true;
}
else
......@@ -228,9 +237,8 @@ public:
protected:
// Parses a operations string and sanitizes it from excess whitespace
string _sanitizeOperations(const string& str, const char sep=',') {
vector<string> v;
string out="";
set<string> _parseOperations(const string& str, const char sep=',') {
set<string> v;
// We split the string into the comma-separated tokens
std::stringstream ss(str);
......@@ -238,17 +246,22 @@ protected:
while (std::getline(ss, token, sep)) {
if(!token.empty()) {
boost::algorithm::trim(token);
v.push_back(token);
v.insert(token);
}
}
return v;
}
string _dumpOperations(const char sep=',') const {
string out="";
// We re-write the vector into a string, this time properly formatted
string sepStr = string(1,sep);
for(const auto& el : v)
out += el + sepStr;
if(!out.empty() && out.back() == sep)
out.erase(out.size()-1, 1);
if(operations) {
for (const auto &el : *operations)
out += el + sepStr;
if (!out.empty() && out.back() == sep)
out.erase(out.size() - 1, 1);
}
return out;
}
......@@ -279,7 +292,7 @@ protected:
if(ttl)
config.push_back(boost::property_tree::ptree::value_type("ttl", boost::property_tree::ptree(to_string(*ttl / 1000000))));
if(operations)
config.push_back(boost::property_tree::ptree::value_type("operations", boost::property_tree::ptree(*operations)));
config.push_back(boost::property_tree::ptree::value_type("operations", boost::property_tree::ptree(_dumpOperations())));
}
// Protected class members
......@@ -293,7 +306,7 @@ protected:
std::shared_ptr<double> scale;
std::shared_ptr<uint64_t> ttl;
std::shared_ptr<uint64_t> interval;
std::shared_ptr<string> operations;
std::shared_ptr<set<string>> operations;
};
......
......@@ -38,6 +38,7 @@
#include <string>
#include <list>
#include <set>
#include "connection.h"
#include "timestamp.h"
......@@ -62,18 +63,18 @@ class SensorConfigImpl;
class PublicSensor
{
public:
std::string name; /**< The public sensor's (public) name. */
bool is_virtual; /**< Denotes whether the sensor is a virtual sensor. */
std::string pattern; /**< For non-virtual sensors, this holds a pattern describing the (internal) sensor IDs to which this public sensor matches. */
double scaling_factor; /**< Scaling factor for every sensor reading */
std::string unit; /**< Describes the unit of the sensor. See unitconv.h for known units. */
uint64_t sensor_mask; /**< Determines the properties of the sensor. Currently defined are: integrable, monotonic. */
std::string expression; /**< For virtual sensors, this field holds the expression through which the virtual sensor's value is calculated. */
std::string v_sensorid; /**< For virtual sensors, this field holds a SensorID used for storing cached values in the database. (FIXME: Cache to be implemented) */
uint64_t t_zero; /**< For virtual sensors, this field holds the first point in time at which the sensor carries a value. */
uint64_t interval; /**< This field holds the interval at which the sensor evaluates (in nanoseconds). */
std::string operations; /**< Defines the operations on the sensor, e.g. avg, std deviation, etc. */
uint64_t ttl; /**< Defines the time to live (in nanoseconds) for the readings of this sensor. */
std::string name; /**< The public sensor's (public) name. */
bool is_virtual; /**< Denotes whether the sensor is a virtual sensor. */
std::string pattern; /**< For non-virtual sensors, this holds a pattern describing the (internal) sensor IDs to which this public sensor matches. */
double scaling_factor; /**< Scaling factor for every sensor reading */
std::string unit; /**< Describes the unit of the sensor. See unitconv.h for known units. */
uint64_t sensor_mask; /**< Determines the properties of the sensor. Currently defined are: integrable, monotonic. */
std::string expression; /**< For virtual sensors, this field holds the expression through which the virtual sensor's value is calculated. */
std::string v_sensorid; /**< For virtual sensors, this field holds a SensorID used for storing cached values in the database. (FIXME: Cache to be implemented) */
uint64_t t_zero; /**< For virtual sensors, this field holds the first point in time at which the sensor carries a value. */
uint64_t interval; /**< This field holds the interval at which the sensor evaluates (in nanoseconds). */
std::set<std::string> operations; /**< Defines the operations on the sensor, e.g. avg, std deviation, etc. */
uint64_t ttl; /**< Defines the time to live (in nanoseconds) for the readings of this sensor. */
PublicSensor();
PublicSensor(const PublicSensor &copy);
......@@ -282,10 +283,10 @@ public:
* @brief Set an operation for the sensor.
*
* @param publicName Name of the sensor.
* @param operstion New operation for the sensor.
* @param operation Set of operations for the sensor.
* @return See SCError.
*/
SCError setOperations(std::string publicName, std::string operations);
SCError setOperations(std::string publicName, std::set<std::string> operations);
/**
* @brief Set a new sensor expression for a virtual sensor.
......
......@@ -80,7 +80,7 @@ public:
SCError setSensorScalingFactor(std::string publicName, double scalingFactor);
SCError setSensorUnit(std::string publicName, std::string unit);
SCError setSensorMask(std::string publicName, uint64_t mask);
SCError setOperations(std::string publicName, std::string operations);
SCError setOperations(std::string publicName, std::set<std::string> operations);
SCError setTimeToLive(std::string publicName, uint64_t ttl);
SCError setSensorInterval(std::string publicName, uint64_t interval);
......
......@@ -460,7 +460,7 @@ bool ConnectionImpl::initSchema() {
"sensor_mask bigint, " /* Bit mask that specifies sensor properties. Currently defined ones are:
Integrable: indicates whether the sensor is integrable over time;
Monotonic : indicates whether the collected sensor data is monotonic. */
"operations varchar, " /* Operations for the sensor (e.g., avg, stdev,...). */
"operations set<varchar>, " /* Operations for the sensor (e.g., avg, stdev,...). */
"expression varchar, " /* For virtual sensors: arithmetic expression to derive the virtual sensor's value */
"vsensorid varchar, " /* For virtual sensors: Unique sensorId for the sensor in the virtualsensors table */
"tzero bigint, " /* For virtual sensors: time of the first reading */
......@@ -468,7 +468,7 @@ bool ConnectionImpl::initSchema() {
"ttl bigint", /* Time to live in nanoseconds for readings of this sensor */
"name", /* Make the "name" column the primary key */
"COMPACT STORAGE AND CACHING = {'keys' : 'all'} "); /* Enable compact storage and maximum caching */
"CACHING = {'keys' : 'all'} "); /* Enable compact storage and maximum caching */
}
/* Keyspace and column family for raw and virtual sensor data */
......
......@@ -58,7 +58,6 @@ PublicSensor::PublicSensor()
v_sensorid = "";
t_zero = 0;
interval = 0;
operations = "";
ttl = 0;
}
......@@ -172,7 +171,7 @@ SCError SensorConfig::setSensorMask(std::string publicName, uint64_t mask)
return impl->setSensorMask(publicName, mask);
}
SCError SensorConfig::setOperations(std::string publicName, std::string operations)
SCError SensorConfig::setOperations(std::string publicName, std::set<std::string> operations)
{
return impl->setOperations(publicName,operations);
}
......@@ -369,7 +368,7 @@ SCError SensorConfigImpl::publishSensor(const PublicSensor& sensor)
CassStatement* statement = nullptr;
CassFuture* future = nullptr;
const CassPrepared* prepared = nullptr;
const char* query = "INSERT INTO " CONFIG_KEYSPACE_NAME "." CF_PUBLISHEDSENSORS " (name, pattern, virtual, scaling_factor, unit, sensor_mask, operations, interval, ttl) VALUES (?,?, FALSE, ?, ?, ?, ?, ?, ?);";
const char* query = "INSERT INTO " CONFIG_KEYSPACE_NAME "." CF_PUBLISHEDSENSORS " (name, pattern, virtual, scaling_factor, unit, sensor_mask, interval, ttl) VALUES (?,?, FALSE, ?, ?, ?, ?, ?);";
future = cass_session_prepare(session, query);
cass_future_wait(future);
......@@ -392,10 +391,9 @@ SCError SensorConfigImpl::publishSensor(const PublicSensor& sensor)
cass_statement_bind_double_by_name(statement, "scaling_factor", sensor.scaling_factor);
cass_statement_bind_string_by_name(statement, "unit", sensor.unit.c_str());
cass_statement_bind_int64_by_name(statement, "sensor_mask", sensor.sensor_mask);
cass_statement_bind_string_by_name(statement, "operations", sensor.operations.c_str());
cass_statement_bind_int64_by_name(statement, "interval", sensor.interval);
cass_statement_bind_int64_by_name(statement, "ttl", sensor.ttl);
future = cass_session_execute(session, statement);
cass_future_wait(future);
......@@ -414,7 +412,11 @@ SCError SensorConfigImpl::publishSensor(const PublicSensor& sensor)
cass_future_free(future);
cass_statement_free(statement);
return SC_OK;
// Operations are inserted as an update statement, if required
if(sensor.operations.size() > 0)
return setOperations(sensor.name, sensor.operations);
else
return SC_OK;
}
SCError SensorConfigImpl::publishSensor(const SensorMetadata& sensor)
......@@ -457,10 +459,6 @@ SCError SensorConfigImpl::publishSensor(const SensorMetadata& sensor)
queryBuf += ", sensor_mask";
valuesBuf += ", ?";
}
if(sensor.getOperations()) {
queryBuf += ", operations";
valuesBuf += ", ?";
}
if(sensor.getInterval()) {
queryBuf += ", interval";
valuesBuf += ", ?";
......@@ -494,8 +492,6 @@ SCError SensorConfigImpl::publishSensor(const SensorMetadata& sensor)
cass_statement_bind_double_by_name(statement, "scaling_factor", *sensor.getScale());
if(sensor.getUnit())
cass_statement_bind_string_by_name(statement, "unit", sensor.getUnit()->c_str());
if(sensor.getOperations())
cass_statement_bind_string_by_name(statement, "operations", sensor.getOperations()->c_str());
if(sensor.getInterval())
cass_statement_bind_int64_by_name(statement, "interval", *sensor.getInterval());
if(sensor.getTTL())
......@@ -508,7 +504,7 @@ SCError SensorConfigImpl::publishSensor(const SensorMetadata& sensor)
sensorMask = sensorMask | MONOTONIC;
cass_statement_bind_int64_by_name(statement, "sensor_mask", sensorMask);
}
future = cass_session_execute(session, statement);
cass_future_wait(future);
......@@ -526,8 +522,12 @@ SCError SensorConfigImpl::publishSensor(const SensorMetadata& sensor)
cass_prepared_free(prepared);
cass_future_free(future);
cass_statement_free(statement);
return SC_OK;
// Operations are inserted as an update statement, if required
if(sensor.getOperations() && sensor.getOperations()->size()>0)
return setOperations(*sensor.getPublicName(), *sensor.getOperations());
else
return SC_OK;
}
SCError SensorConfigImpl::publishVirtualSensor(std::string publicName, std::string vSensorExpression, std::string vSensorId, TimeStamp tZero, uint64_t interval)
......@@ -769,8 +769,7 @@ SCError SensorConfigImpl::getPublicSensorsVerbose(std::list<PublicSensor>& publi
int64_t tzero;
int64_t interval;
int64_t ttl;
const char* operations;
size_t operations_len;
set<string> operations;
PublicSensor sensor;
const CassRow* row = cass_iterator_get_row(iterator);
......@@ -808,10 +807,24 @@ SCError SensorConfigImpl::getPublicSensorsVerbose(std::list<PublicSensor>& publi
if (cass_value_get_int64(cass_row_get_column_by_name(row, "ttl"), &ttl) != CASS_OK) {
ttl = 0;
}
if (cass_value_get_string(cass_row_get_column_by_name(row, "operations"), &operations, &operations_len) != CASS_OK) {
operations = ""; operations_len = 0;
}
const CassValue* opSet = nullptr;
CassIterator *opSetIt = nullptr;
if((opSet=cass_row_get_column_by_name(row, "operations")) && (opSetIt=cass_iterator_from_collection(opSet))) {
const char *opString;
size_t opLen;
while (cass_iterator_next(opSetIt)) {
if (cass_value_get_string(cass_iterator_get_value(opSetIt), &opString, &opLen) != CASS_OK) {
operations.clear();
break;
} else
operations.insert(std::string(opString, opLen));
}
cass_iterator_free(opSetIt);
}
sensor.name = std::string(name, name_len);
sensor.is_virtual = is_virtual == cass_true ? true : false;
sensor.pattern = std::string(pattern, pattern_len);
......@@ -904,8 +917,7 @@ SCError SensorConfigImpl::getPublicSensorByName(PublicSensor& sensor, const char
int64_t tzero;
int64_t interval;
int64_t ttl;
const char* operations;
size_t operations_len;
set<string> operations;
const CassRow* row = cass_iterator_get_row(iterator);
......@@ -942,8 +954,23 @@ SCError SensorConfigImpl::getPublicSensorByName(PublicSensor& sensor, const char
if (cass_value_get_int64(cass_row_get_column_by_name(row, "ttl"), &ttl) != CASS_OK) {
ttl = 0;
}
if (cass_value_get_string(cass_row_get_column_by_name(row, "operations"), &operations, &operations_len) != CASS_OK) {
operations = ""; operations_len = 0;
const CassValue* opSet = nullptr;
CassIterator *opSetIt = nullptr;
if((opSet=cass_row_get_column_by_name(row, "operations")) && (opSetIt=cass_iterator_from_collection(opSet))) {
CassIterator *opSetIt = cass_iterator_from_collection(opSet);
const char *opString;
size_t opLen;
while (cass_iterator_next(opSetIt)) {
if (cass_value_get_string(cass_iterator_get_value(opSetIt), &opString, &opLen) != CASS_OK) {
operations.clear();
break;
} else
operations.insert(std::string(opString, opLen));
}
cass_iterator_free(opSetIt);
}
sensor.name = std::string(name, name_len);
......@@ -1443,7 +1470,7 @@ SCError SensorConfigImpl::setSensorMask(std::string publicName, uint64_t mask)
return error;
}
SCError SensorConfigImpl::setOperations(std::string publicName, std::string operations)
SCError SensorConfigImpl::setOperations(std::string publicName, std::set<std::string> operations)
{
SCError error = SC_UNKNOWNERROR;
......@@ -1451,7 +1478,11 @@ SCError SensorConfigImpl::setOperations(std::string publicName, std::string oper
CassStatement* statement = nullptr;
CassFuture* future = nullptr;
const CassPrepared* prepared = nullptr;
const char* query = "UPDATE " CONFIG_KEYSPACE_NAME "." CF_PUBLISHEDSENSORS " SET operations = ? WHERE name = ? ;";
const char* query = "UPDATE " CONFIG_KEYSPACE_NAME "." CF_PUBLISHEDSENSORS " SET operations = operations + ? WHERE name = ? ;";
CassCollection* cassSet = cass_collection_new(CASS_COLLECTION_TYPE_SET, operations.size());
for(const auto& op : operations)
cass_collection_append_string(cassSet, op.c_str());
future = cass_session_prepare(session, query);
cass_future_wait(future);
......@@ -1467,7 +1498,7 @@ SCError SensorConfigImpl::setOperations(std::string publicName, std::string oper
statement = cass_prepared_bind(prepared);
cass_statement_bind_string(statement, 0, operations.c_str());
cass_statement_bind_collection(statement, 0, cassSet);
cass_statement_bind_string(statement, 1, publicName.c_str());
future = cass_session_execute(session, statement);
......@@ -1484,6 +1515,7 @@ SCError SensorConfigImpl::setOperations(std::string publicName, std::string oper
cass_statement_free(statement);
cass_prepared_free(prepared);
cass_collection_free(cassSet);
return error;
}
......
......@@ -35,6 +35,7 @@
#include <dcdb/sensorconfig.h>
#include <dcdb/unitconv.h>
#include "cassandra.h"
#include "metadatastore.h"
#include "sensoraction.h"
......@@ -323,6 +324,9 @@ void SensorAction::doShow(const char* publicName)
DCDB::PublicSensor publicSensor;
DCDB::SCError err = sensorConfig.getPublicSensorByName(publicSensor, publicName);
SensorMetadata sm;
sm.setOperations(publicSensor.operations);
switch (err) {
case DCDB::SC_OK:
if (!publicSensor.is_virtual) {
......@@ -338,7 +342,7 @@ void SensorAction::doShow(const char* publicName)
}
std::cout << "Unit: " << publicSensor.unit << std::endl;
std::cout << "Scaling factor: " << publicSensor.scaling_factor << std::endl;
std::cout << "Operations: " << publicSensor.operations << std::endl;
std::cout << "Operations: " << sm.getOperationsString() << std::endl;
std::cout << "Interval: " << publicSensor.interval << std::endl;
std::cout << "TTL: " << publicSensor.ttl << std::endl;
std::cout << "Sensor Properties: ";
......@@ -559,9 +563,11 @@ void SensorAction::doTTL(const char* publicName, const char *ttl) {
void SensorAction::doOperations(const char* publicName, const char *operations)
{
SensorMetadata sm;
sm.setOperations(std::string(operations));
DCDB::SensorConfig sensorConfig(connection);
DCDB::SCError err = sensorConfig.setOperations(publicName, operations);
DCDB::SCError err = sensorConfig.setOperations(publicName, *sm.getOperations());
switch (err) {
case DCDB::SC_OK:
......
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