Commit 98cd6903 authored by Jan Kadlec's avatar Jan Kadlec

new_node: Fixes, store changes into changeset_t

- Do not merge incoming RRs
- Duplicated add/remove checks improved in ddns
- Remove stuff directly from RRSet, do not return
parent 93aba8bc
......@@ -279,7 +279,7 @@ static int zones_process_update_auth(struct query_data *qdata)
sec_chs = knot_changesets_create();
sec_ch = knot_changesets_create_changeset(sec_chs);
if (sec_chs == NULL || sec_ch == NULL) {
xfrin_rollback_update(zone, old_contents, &new_contents);
xfrin_rollback_update(chgsets, old_contents, &new_contents);
knot_changesets_free(&chgsets);
free(msg);
return KNOT_ENOMEM;
......@@ -311,7 +311,8 @@ static int zones_process_update_auth(struct query_data *qdata)
if (ret != KNOT_EOK) {
log_zone_error("%s: Failed to sign incoming update (%s)"
"\n", msg, knot_strerror(ret));
xfrin_rollback_update(zone, old_contents, &new_contents);
1 == 1; // TODO: rollback
xfrin_rollback_update(chgsets, old_contents, &new_contents);
knot_changesets_free(&chgsets);
knot_changesets_free(&sec_chs);
free(msg);
......@@ -326,7 +327,7 @@ static int zones_process_update_auth(struct query_data *qdata)
if (ret != KNOT_EOK) {
log_zone_error("%s: Failed to save new entry to journal (%s)\n",
msg, knot_strerror(ret));
xfrin_rollback_update(zone, old_contents, &new_contents);
xfrin_rollback_update(chgsets, old_contents, &new_contents);
zones_free_merged_changesets(chgsets, sec_chs);
free(msg);
return ret;
......@@ -362,8 +363,8 @@ static int zones_process_update_auth(struct query_data *qdata)
ret = knot_zone_contents_adjust_nsec3_pointers(new_contents);
if (ret != KNOT_EOK) {
zones_store_changesets_rollback(transaction);
xfrin_rollback_update(chgsets, old_contents, &new_contents);
zones_free_merged_changesets(chgsets, sec_chs);
xfrin_rollback_update(zone, old_contents, &new_contents);
free(msg);
return ret;
}
......@@ -375,7 +376,7 @@ static int zones_process_update_auth(struct query_data *qdata)
if (ret != KNOT_EOK) {
log_zone_error("%s: Failed to commit new journal entry "
"(%s).\n", msg, knot_strerror(ret));
xfrin_rollback_update(zone, old_contents, &new_contents);
xfrin_rollback_update(chgsets, old_contents, &new_contents);
zones_free_merged_changesets(chgsets, sec_chs);
free(msg);
return ret;
......@@ -391,7 +392,7 @@ static int zones_process_update_auth(struct query_data *qdata)
log_zone_error("%s: Failed to replace current zone (%s)\n",
msg, knot_strerror(ret));
// Cleanup old and new contents
xfrin_rollback_update(zone, old_contents, &new_contents);
xfrin_rollback_update(chgsets, old_contents, &new_contents);
/* Free changesets, but not the data. */
zones_free_merged_changesets(chgsets, sec_chs);
......@@ -401,7 +402,7 @@ static int zones_process_update_auth(struct query_data *qdata)
}
// Cleanup.
xfrin_cleanup_successful_update(zone);
xfrin_cleanup_successful_update(chgsets);
// Free changesets, but not the data.
zones_free_merged_changesets(chgsets, sec_chs);
......
......@@ -2016,10 +2016,7 @@ static int diff_after_load(zone_t *zone, zone_t *old_zone,
if (*diff_chs != NULL) {
assert(!zones_changesets_empty(*diff_chs));
/* Apply DNSSEC changeset to the new zone. */
ret = xfrin_apply_changesets_directly(zone,
zone->contents,
*diff_chs);
ret = xfrin_apply_changesets_directly(zone->contents, *diff_chs);
if (ret == KNOT_EOK) {
ret = xfrin_finalize_updated_zone(
zone->contents, true);
......@@ -2105,7 +2102,7 @@ static int store_chgsets_after_load(zone_t *old_zone, zone_t *zone,
if (zone_changed) {
assert(!old_zone ||
old_zone->contents != zone->contents);
ret = xfrin_apply_changesets_directly(zone, zone->contents,
ret = xfrin_apply_changesets_directly(zone->contents,
diff_chs);
if (ret == KNOT_EOK) {
ret = xfrin_finalize_updated_zone(
......
......@@ -90,6 +90,10 @@ knot_changeset_t *knot_changesets_create_changeset(knot_changesets_t *ch)
init_list(&set->add);
init_list(&set->remove);
// Init change lists
init_list(&set->new_data);
init_list(&set->old_data);
// Insert into list of sets
add_tail(&ch->sets, (node_t *)set);
......
......@@ -55,6 +55,8 @@ typedef struct knot_changeset {
uint32_t serial_from; /*!< SOA start serial. */
uint32_t serial_to; /*!< SOA destination serial. */
uint32_t flags; /*!< DDNS / IXFR flags. */
list_t old_data;
list_t new_data;
} knot_changeset_t;
/*----------------------------------------------------------------------------*/
......
......@@ -31,6 +31,60 @@
#include "common/descriptor.h"
#include "common/lists.h"
static bool list_contains_rr(const list_t *l, const knot_rrset_t *rr)
{
knot_rr_ln_t *n;
WALK_LIST(n, *l) {
const knot_rrset_t *list_rr = n->rr;
if (knot_rrset_equal(rr, list_rr, KNOT_RRSET_COMPARE_WHOLE)) {
return true;
}
};
return false;
}
static bool removed_rr(const knot_changeset_t *changeset, const knot_rrset_t *rr)
{
return list_contains_rr(&changeset->remove, rr);
}
static void remove_rr_from_list(list_t *l, const knot_rrset_t *rr)
{
knot_rr_ln_t *rr_node = NULL;
node_t *nxt = NULL;
WALK_LIST_DELSAFE(rr_node, nxt, *l) {
knot_rrset_t *rrset = rr_node->rr;
if (knot_rrset_equal(rrset, rr, KNOT_RRSET_COMPARE_WHOLE)) {
knot_rrset_free(&rrset, NULL);
rem_node((node_t *)rr_node);
return;
}
}
}
static bool node_empty(const knot_node_t *node, const knot_changeset_t *changeset)
{
if (node == NULL) {
return true;
}
for (uint16_t i = 0; i < node->rrset_count; ++i) {
knot_rrset_t node_rrset = RRSET_INIT_N(node, i);
knot_rrset_t node_rr;
knot_rrset_init(&node_rr, node->owner, node_rrset.type, KNOT_CLASS_IN);
for (uint16_t j = 0; j < node_rrset.rrs.rr_count; ++j) {
knot_rrset_add_rr_from_rrset(&node_rr, &node_rrset, j, NULL);
if (!removed_rr(changeset, &node_rr)) {
knot_rrs_clear(&node_rr.rrs, NULL);
return false;
}
}
}
return true;
}
static bool rrset_empty(const knot_rrset_t *rrset)
{
uint16_t rr_count = knot_rrset_rr_count(rrset);
......@@ -311,8 +365,6 @@ int knot_ddns_check_zone(const knot_zone_contents_t *zone,
return KNOT_EOK;
}
int knot_ddns_process_prereqs(const knot_pkt_t *query, const knot_zone_contents_t *zone,
uint16_t *rcode)
{
......@@ -534,7 +586,7 @@ static int process_add_cname(const knot_node_t *node,
knot_changeset_t *changeset)
{
knot_rrset_t cname = RRSET_INIT(node, KNOT_RRTYPE_CNAME);
if (!knot_rrset_empty(&cname)) {
if (!knot_rrset_empty(&cname) && !removed_rr(changeset, &cname)) {
// If they are identical, ignore.
if (knot_rrset_equal(&cname, rr, KNOT_RRSET_COMPARE_WHOLE)) {
return KNOT_EOK;
......@@ -546,10 +598,10 @@ static int process_add_cname(const knot_node_t *node,
}
return add_rr_to_chgset(rr, changeset);
} else if (node && knot_node_rrset_count(node) > 0) {
} else if (!node_empty(node, changeset)) {
// Other occupied node => ignore.
return KNOT_EOK;
} else if (node) {
} else {
return add_rr_to_chgset(rr, changeset);
}
......@@ -565,7 +617,7 @@ static int process_add_nsec3param(const knot_node_t *node,
return KNOT_EOK;
}
knot_rrset_t param = RRSET_INIT(node, KNOT_RRTYPE_NSEC3PARAM);
if (!knot_rrset_empty(&param)) {
if (!knot_rrset_empty(&param) && !removed_rr(changeset, &param)) {
// If they are identical, ignore.
if (knot_rrset_equal(&param, rr, KNOT_RRSET_COMPARE_WHOLE)) {
return KNOT_EOK;
......@@ -576,11 +628,8 @@ static int process_add_nsec3param(const knot_node_t *node,
if (ret != KNOT_EOK) {
return ret;
}
return add_rr_to_chgset(rr, changeset);
} else {
return add_rr_to_chgset(rr, changeset);
}
return add_rr_to_chgset(rr, changeset);
}
static int process_rem_rr(const knot_rrset_t *rr,
......@@ -598,12 +647,13 @@ static int process_rem_rr(const knot_rrset_t *rr,
return KNOT_EOK;
}
}
knot_rrset_t to_modify = RRSET_INIT(node, rr->type);
if (knot_rrset_empty(&to_modify)) {
// Nothing to remove from
return KNOT_EOK;
}
knot_rrset_t intersection;
int ret = knot_rrset_intersection(&to_modify, rr, &intersection, NULL);
if (ret != KNOT_EOK) {
......@@ -611,9 +661,10 @@ static int process_rem_rr(const knot_rrset_t *rr,
}
if (knot_rrset_empty(&intersection)) {
// No such RR
return KNOT_EOK;
// No such RR, but check duplicates
return rem_rrset_to_chgset(rr, changeset, NULL);
}
assert(intersection.rrs.rr_count == 1);
ret = rem_rrset_to_chgset(&intersection, changeset,
apex_ns ? apex_ns_removals : NULL);
......@@ -698,21 +749,6 @@ static bool node_contains_rr(const knot_node_t *node,
}
}
static void remove_rr_from_changeset(knot_changeset_t *changeset,
const knot_rrset_t *rr)
{
knot_rr_ln_t *rr_node = NULL;
node_t *nxt = NULL;
WALK_LIST_DELSAFE(rr_node, nxt, changeset->remove) {
knot_rrset_t *rrset = rr_node->rr;
if (knot_rrset_equal(rrset, rr, KNOT_RRSET_COMPARE_WHOLE)) {
knot_rrset_free(&rrset, NULL);
rem_node((node_t *)rr_node);
return;
}
}
}
static int process_add_normal(const knot_node_t *node,
const knot_rrset_t *rr,
knot_changeset_t *changeset)
......@@ -723,7 +759,7 @@ static int process_add_normal(const knot_node_t *node,
}
if (node && node_contains_rr(node, rr)) {
remove_rr_from_changeset(changeset, rr);
remove_rr_from_list(&changeset->remove, rr);
return KNOT_EOK;
}
......@@ -774,6 +810,20 @@ static int knot_ddns_final_soa_to_chgset(knot_rrset_t *soa,
return KNOT_EOK;
}
static bool sem_check(const knot_rrset_t *rr,
const knot_zone_contents_t *zone)
{
1 == 1; // DNAME added to parent, not to children
const knot_node_t *parent =
knot_zone_contents_find_node(zone,
knot_wire_next_label(rr->owner, NULL));
if (parent == NULL) {
return true;
}
return !knot_node_rrtype_exists(parent, KNOT_RRTYPE_DNAME);
}
static int knot_ddns_process_rr(const knot_rrset_t *rr,
knot_zone_contents_t *zone,
knot_changeset_t *changeset,
......@@ -781,18 +831,19 @@ static int knot_ddns_process_rr(const knot_rrset_t *rr,
{
const knot_node_t *node = knot_zone_contents_find_node(zone, rr->owner);
int ret = KNOT_EOK;
if (is_addition(rr)) {
return process_add(rr, node, changeset);
int ret = process_add(rr, node, changeset);
if (ret == KNOT_EOK) {
if (!sem_check(rr, zone)) {
return KNOT_EDENIED;
}
}
return ret;
} else if (is_deletion(rr)) {
return process_remove(rr, node, changeset, apex_ns_removals);
} else {
return KNOT_EMALF;
}
if (ret == KNOT_EOK) {
1 == 1; // no node now, semantic check will need to be done during application
}
}
/*
......
......@@ -628,16 +628,17 @@ void xfrin_zone_contents_free(knot_zone_contents_t **contents)
/*----------------------------------------------------------------------------*/
void xfrin_cleanup_successful_update(zone_t *zone)
void xfrin_cleanup_successful_update(knot_changesets_t *chgs)
{
if (zone == NULL) {
return;
}
rrs_list_clear(&zone->old_data, NULL);
ptrlist_free(&zone->new_data, NULL);
init_list(&zone->new_data);
init_list(&zone->old_data);
knot_changeset_t *change = NULL;
WALK_LIST(change, chgs->sets) {
// Delete old RR data
rrs_list_clear(&change->old_data, NULL);
// Keep new RR data
ptrlist_free(&change->new_data, NULL);
init_list(&change->new_data);
init_list(&change->old_data);
};
}
/*----------------------------------------------------------------------------*/
......@@ -731,14 +732,19 @@ static void xfrin_cleanup_failed_update(knot_zone_contents_t *old_contents,
/*----------------------------------------------------------------------------*/
void xfrin_rollback_update(zone_t *zone,
void xfrin_rollback_update(knot_changesets_t *chgs,
knot_zone_contents_t *old_contents,
knot_zone_contents_t **new_contents)
{
rrs_list_clear(&zone->new_data, NULL);
ptrlist_free(&zone->old_data, NULL);
init_list(&zone->new_data);
init_list(&zone->old_data);
knot_changeset_t *change = NULL;
WALK_LIST(change, chgs->sets) {
// Delete new RR data
rrs_list_clear(&change->new_data, NULL);
// Keep old RR data
ptrlist_free(&change->old_data, NULL);
init_list(&change->new_data);
init_list(&change->old_data);
};
xfrin_cleanup_failed_update(old_contents, new_contents);
}
......@@ -801,7 +807,7 @@ static int xfrin_apply_remove(knot_zone_contents_t *contents,
{
knot_rr_ln_t *rr_node = NULL;
WALK_LIST(rr_node, chset->remove) {
knot_rrset_t *rr = rr_node->rr;
const knot_rrset_t *rr = rr_node->rr;
knot_node_t *node = zone_contents_find_node_for_rr(contents,
rr);
if (!can_remove(node, rr)) {
......@@ -822,20 +828,19 @@ static int xfrin_apply_remove(knot_zone_contents_t *contents,
}
knot_rrset_t rrset = RRSET_INIT(node, rr->type);
knot_rrset_t *removed = NULL;
ret = knot_rrset_remove_rr_using_rrset(&rrset, rr, &removed, NULL);
ret = knot_rrset_remove_rr_using_rrset(&rrset, rr, NULL);
if (ret != KNOT_EOK) {
clear_new_rrs(node, rr->type);
return ret;
}
assert(removed->rrs.rr_count > 0);
knot_rrset_free(&removed, NULL);
if (rrset.rrs.rr_count > 0) {
if (ptrlist_add(new_rrs, rrset.rrs.data, NULL) == NULL) {
knot_rrs_clear(rrs, NULL);
return KNOT_ENOMEM;
}
rrs = knot_node_get_rrs(node, rr->type);
*rrs = rrset.rrs;
} else {
knot_node_remove_rrset(node, rr->type);
}
......@@ -873,7 +878,7 @@ static int xfrin_apply_add(knot_zone_contents_t *contents,
// Either node did not exist before, and we add new RR, or merge
int ret = knot_node_add_rrset(node, rr);
if (ret != KNOT_EOK) {
if (ret < 0) {
clear_new_rrs(node, rr->type);
return ret;
}
......@@ -952,8 +957,7 @@ static int xfrin_apply_changeset(list_t *old_rrs, list_t *new_rrs,
return ret;
}
ret = xfrin_apply_replace_soa(contents, chset, old_rrs, new_rrs);
return ret;
return xfrin_apply_replace_soa(contents, chset, old_rrs, new_rrs);
}
/*----------------------------------------------------------------------------*/
......@@ -1163,8 +1167,7 @@ int xfrin_finalize_updated_zone(knot_zone_contents_t *contents_copy,
/*----------------------------------------------------------------------------*/
int xfrin_apply_changesets_directly(zone_t *zone,
knot_zone_contents_t *contents,
int xfrin_apply_changesets_directly(knot_zone_contents_t *contents,
knot_changesets_t *chsets)
{
if (contents == NULL || chsets == NULL) {
......@@ -1173,8 +1176,8 @@ int xfrin_apply_changesets_directly(zone_t *zone,
knot_changeset_t *set = NULL;
WALK_LIST(set, chsets->sets) {
int ret = xfrin_apply_changeset(&zone->old_data,
&zone->new_data,
int ret = xfrin_apply_changeset(&set->old_data,
&set->new_data,
contents, set);
if (ret != KNOT_EOK) {
return ret;
......@@ -1202,10 +1205,9 @@ int xfrin_apply_changesets_dnssec_ddns(zone_t *zone,
knot_zone_contents_set_gen_old(z_new);
/* Apply changes. */
int ret = xfrin_apply_changesets_directly(zone, z_new,
sec_chsets);
int ret = xfrin_apply_changesets_directly(z_new, sec_chsets);
if (ret != KNOT_EOK) {
xfrin_rollback_update(zone, z_old, &z_new);
xfrin_rollback_update(sec_chsets, z_old, &z_new);
dbg_xfrin("Failed to apply changesets to zone: "
"%s\n", knot_strerror(ret));
return ret;
......@@ -1216,7 +1218,7 @@ int xfrin_apply_changesets_dnssec_ddns(zone_t *zone,
if (ret != KNOT_EOK) {
dbg_xfrin("Failed to finalize updated zone: %s\n",
knot_strerror(ret));
xfrin_rollback_update(zone, z_old, &z_new);
xfrin_rollback_update(sec_chsets, z_old, &z_new);
return ret;
}
......@@ -1259,11 +1261,11 @@ int xfrin_apply_changesets(zone_t *zone,
old_contents->apex, contents_copy->apex);
knot_changeset_t *set = NULL;
WALK_LIST(set, chsets->sets) {
ret = xfrin_apply_changeset(&zone->old_data,
&zone->new_data,
ret = xfrin_apply_changeset(&set->old_data,
&set->new_data,
contents_copy, set);
if (ret != KNOT_EOK) {
xfrin_rollback_update(zone, old_contents,
xfrin_rollback_update(chsets, old_contents,
&contents_copy);
dbg_xfrin("Failed to apply changesets to zone: "
"%s\n", knot_strerror(ret));
......@@ -1281,7 +1283,7 @@ int xfrin_apply_changesets(zone_t *zone,
if (ret != KNOT_EOK) {
dbg_xfrin("Failed to finalize updated zone: %s\n",
knot_strerror(ret));
xfrin_rollback_update(zone, old_contents, &contents_copy);
xfrin_rollback_update(chsets, old_contents, &contents_copy);
return ret;
}
......
......@@ -174,16 +174,13 @@ int xfrin_apply_changesets_dnssec_ddns(zone_t *zone, knot_zone_contents_t *z_old
* \brief Applies changesets directly to the zone, without copying it.
*
* \param contents Zone contents to apply the changesets to. Will be modified.
* \param changes Structure to store changes made during application. It
* doesn't have to be empty, present changes will not be
* modified.
* \param chsets Changesets to be applied to the zone.
*
* \retval KNOT_EOK if successful.
* \retval KNOT_EINVAL if given one of the arguments is NULL.
* \return Other error code if the application went wrong.
*/
int xfrin_apply_changesets_directly(zone_t *zone, knot_zone_contents_t *contents,
int xfrin_apply_changesets_directly(knot_zone_contents_t *contents,
knot_changesets_t *chsets);
int xfrin_prepare_zone_copy(knot_zone_contents_t *old_contents,
......@@ -202,7 +199,7 @@ int xfrin_switch_zone(zone_t *zone,
knot_zone_contents_t *new_contents,
int transfer_type);
void xfrin_rollback_update(zone_t *zone, knot_zone_contents_t *old_contents,
void xfrin_rollback_update(knot_changesets_t *chgs, knot_zone_contents_t *old_contents,
knot_zone_contents_t **new_contents);
int xfrin_copy_rrset(knot_node_t *node, uint16_t type,
......@@ -215,7 +212,7 @@ int xfrin_replace_rrset_in_node(knot_node_t *node,
knot_zone_contents_t *contents);
void xfrin_zone_contents_free(knot_zone_contents_t **contents);
void xfrin_cleanup_successful_update(zone_t *zone);
void xfrin_cleanup_successful_update(knot_changesets_t *chgs);
#endif /* _KNOTXFR_IN_H_ */
......
......@@ -212,9 +212,6 @@ zone_t* zone_new(conf_zone_t *conf)
}
}
init_list(&zone->old_data);
init_list(&zone->new_data);
return zone;
}
......
......@@ -95,8 +95,6 @@ typedef struct zone_t {
/*! \brief Zone IXFR history. */
journal_t *ixfr_db;
event_t *ixfr_dbsync; /*!< Syncing IXFR db to zonefile. */
list_t old_data;
list_t new_data;
} zone_t;
/*----------------------------------------------------------------------------*/
......
......@@ -82,7 +82,7 @@ int knot_process_in(const uint8_t *wire, uint16_t wire_len, knot_process_t *ctx)
}
knot_pkt_t *pkt = knot_pkt_new((uint8_t *)wire, wire_len, &ctx->mm);
knot_pkt_parse(pkt, 0);
knot_pkt_parse(pkt, KNOT_PF_NO_MERGE);
ctx->state = ctx->module->in(pkt, ctx);
dbg_ns("%s -> %s\n", __func__, PROCESSING_STATE_STR(ctx->state));
......
......@@ -124,6 +124,7 @@ int knot_rrs_remove_rr_at_pos(knot_rrs_t *rrs, size_t pos, mm_ctx_t *mm)
total_size);
if (tmp == NULL) {
ERR_ALLOC_FAILED;
return KNOT_ENOMEM;
} else {
rrs->data = tmp;
}
......
......@@ -1228,41 +1228,22 @@ int knot_rrset_add_rr_from_rrset(knot_rrset_t *dest, const knot_rrset_t *source,
int knot_rrset_remove_rr_using_rrset(knot_rrset_t *from,
const knot_rrset_t *what,
knot_rrset_t **rr_deleted,
mm_ctx_t *mm)
{
if (from == NULL || what == NULL || rr_deleted == NULL) {
if (from == NULL || what == NULL) {
return KNOT_EINVAL;
}
knot_rrset_t *return_rr = knot_rrset_new_from(what, NULL);
if (return_rr == NULL) {
return KNOT_ENOMEM;
}
uint16_t what_rdata_count = knot_rrset_rr_count(what);
for (uint16_t i = 0; i < what_rdata_count; ++i) {
int ret = knot_rrset_remove_rr(from, what, i, mm);
if (ret == KNOT_EOK) {
/* RR was removed, can be added to 'return' RRSet. */
ret = knot_rrset_add_rr_from_rrset(return_rr, what, i, NULL);
if (ret != KNOT_EOK) {
knot_rrset_free(&return_rr, NULL);
dbg_xfrin("xfr: Could not add RR (%s).\n",
knot_strerror(ret));
if (ret != KNOT_EOK) {
if (ret != KNOT_ENOENT) {
return ret;
}
} else if (ret != KNOT_ENOENT) {
/* NOENT is OK, but other errors are not. */
dbg_rrset("rrset: remove_using_rrset: "
"RRSet removal failed (%s).\n",
knot_strerror(ret));
knot_rrset_free(&return_rr, NULL);
return ret;
}
}
*rr_deleted = return_rr;
return KNOT_EOK;
}
......
......@@ -355,18 +355,16 @@ int knot_rrset_add_rr_from_rrset(knot_rrset_t *dest, const knot_rrset_t *source,
/*!
* \brief Removes RRs contained in 'what' RRSet from 'from' RRSet.
* Deleted RRs are returned in 'rr_deleted'.
*
* \param from Delete from.
* \param what Delete what.
* \param rr_deleted Deleted RRs stored here.
* \param mm Memory context.
*
* \return KNOT_E*
*/
int knot_rrset_remove_rr_using_rrset(knot_rrset_t *from,
const knot_rrset_t *what,
knot_rrset_t **rr_deleted, mm_ctx_t *mm);
mm_ctx_t *mm);
/*!
* \brief Finds RR at 'pos' position in 'rr_reference' RRSet in
......
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