Commit d9c8010d authored by Daniel Salzman's avatar Daniel Salzman

Merge branch 'ctl-zone-edit' into 'master'

Ctl zone edit

See merge request !551
parents a8dd1867 334b7cfd
......@@ -38,7 +38,7 @@ DNS features:
Server features:
* Adding/removing zones on-the-fly
* Adding/removing/editing zones on-the-fly
* Reconfiguring server instance on-the-fly
* Dynamic configuration
* IPv4 and IPv6 support
......
......@@ -75,7 +75,8 @@ Check if the server is running.
Stop the server if running.
.TP
\fBreload\fP
Reload the server configuration and modified zone files.
Reload the server configuration and modified zone files. All open zone
transactions will be aborted!
.TP
\fBzone\-check\fP [\fIzone\fP\&...]
Test if the server can load the zone. Semantic checks are executed if enabled
......@@ -90,7 +91,8 @@ Show the zone status. (*)
\fBzone\-reload\fP [\fIzone\fP\&...]
Trigger a zone reload from a disk without checking its modification time. For
slave zone, the refresh from a master server is scheduled; for master zone,
the notification of slave servers is scheduled.
the notification of slave servers is scheduled. An open zone transaction
will be aborted!
.TP
\fBzone\-refresh\fP [\fIzone\fP\&...]
Trigger a check for the zone serial on the zone\(aqs master. If the master has a
......@@ -107,6 +109,31 @@ Trigger a zone journal flush into the zone file.
Trigger a DNSSEC re\-sign of the zone. Existing signatures will be dropped.
This command is valid for zones with automatic DNSSEC signing.
.TP
\fBzone\-read\fP \fIzone\fP [\fIowner\fP [\fItype\fP]]
Get zone data that are currently being presented.
.TP
\fBzone\-begin\fP \fIzone\fP\&...
Begin a zone transaction.
.TP
\fBzone\-commit\fP \fIzone\fP\&...
Commit the zone transaction. All changes are applied to the zone.
.TP
\fBzone\-abort\fP \fIzone\fP\&...
Abort the zone transaction. All changes are discarded.
.TP
\fBzone\-diff\fP \fIzone\fP
Get zone changes within the transaction.
.TP
\fBzone\-get\fP \fIzone\fP [\fIowner\fP [\fItype\fP]]
Get zone data within the transaction.
.TP
\fBzone\-set\fP \fIzone\fP \fIowner\fP [\fIttl\fP] \fItype\fP \fIrdata\fP
Add zone record within the transaction. The first record in a rrset
requires a ttl value specified.
.TP
\fBzone\-unset\fP \fIzone\fP \fIowner\fP [\fItype\fP [\fIrdata\fP]]
Remove zone data within the transaction.
.TP
\fBconf\-init\fP
Initialize the configuration database. (*)
.TP
......@@ -150,7 +177,9 @@ Unset the item data in the transaction.
.UNINDENT
.SS Note
.sp
Empty \fIzone\fP parameter means all zones.
Empty or \fB\-\-\fP \fIzone\fP parameter means all zones or all zones with a transaction.
.sp
Use \fB@\fP \fIowner\fP to denote the zone name.
.sp
Type \fIitem\fP parameter in the form of \fIsection\fP[\fB[\fP\fIid\fP\fB]\fP][\fB\&.\fP\fIname\fP].
.sp
......@@ -234,6 +263,17 @@ $ knotc conf\-commit
.fi
.UNINDENT
.UNINDENT
.SS Get the SOA record for each configured zone
.INDENT 0.0
.INDENT 3.5
.sp
.nf
.ft C
$ knotc zone\-read \-\- @ SOA
.ft P
.fi
.UNINDENT
.UNINDENT
.SH SEE ALSO
.sp
\fIknotd(8)\fP, \fIknot.conf(5)\fP, \fIeditrc(5)\fP\&.
......
......@@ -52,7 +52,8 @@ Actions
Stop the server if running.
**reload**
Reload the server configuration and modified zone files.
Reload the server configuration and modified zone files. All open zone
transactions will be aborted!
**zone-check** [*zone*...]
Test if the server can load the zone. Semantic checks are executed if enabled
......@@ -67,7 +68,8 @@ Actions
**zone-reload** [*zone*...]
Trigger a zone reload from a disk without checking its modification time. For
slave zone, the refresh from a master server is scheduled; for master zone,
the notification of slave servers is scheduled.
the notification of slave servers is scheduled. An open zone transaction
will be aborted!
**zone-refresh** [*zone*...]
Trigger a check for the zone serial on the zone's master. If the master has a
......@@ -84,6 +86,31 @@ Actions
Trigger a DNSSEC re-sign of the zone. Existing signatures will be dropped.
This command is valid for zones with automatic DNSSEC signing.
**zone-read** *zone* [*owner* [*type*]]
Get zone data that are currently being presented.
**zone-begin** *zone*...
Begin a zone transaction.
**zone-commit** *zone*...
Commit the zone transaction. All changes are applied to the zone.
**zone-abort** *zone*...
Abort the zone transaction. All changes are discarded.
**zone-diff** *zone*
Get zone changes within the transaction.
**zone-get** *zone* [*owner* [*type*]]
Get zone data within the transaction.
**zone-set** *zone* *owner* [*ttl*] *type* *rdata*
Add zone record within the transaction. The first record in a rrset
requires a ttl value specified.
**zone-unset** *zone* *owner* [*type* [*rdata*]]
Remove zone data within the transaction.
**conf-init**
Initialize the configuration database. (*)
......@@ -128,7 +155,9 @@ Actions
Note
....
Empty *zone* parameter means all zones.
Empty or **--** *zone* parameter means all zones or all zones with a transaction.
Use **@** *owner* to denote the zone name.
Type *item* parameter in the form of *section*\ [**[**\ *id*\ **]**\ ][**.**\ *name*].
......@@ -193,6 +222,13 @@ Add example.org zone with a zonefile location
$ knotc conf-set 'zone[example.org].file' '/var/zones/example.org.zone'
$ knotc conf-commit
Get the SOA record for each configured zone
...........................................
::
$ knotc zone-read -- @ SOA
See Also
--------
......
......@@ -180,6 +180,68 @@ actual consumption. Also, for slave servers with incoming transfers
enabled, be aware that the actual memory consumption might be double
or higher during transfers.
.. _Editing zones:
Reading and editing zones
=========================
Knot DNS allows you to read or change zone contents online using server
control interface.
To get contents of all configured zones, or a specific zone contents, or zone
records with a specific owner, or even with a specific record type::
$ knotc zone-read --
$ knotc zone-read example.com
$ knotc zone-read example.com ns1
$ knotc zone-read example.com ns1 NS
.. NOTE::
If the record owner is not a fully qualified domain name, then it is
considered as a relative name to the zone name.
To start a writing transaction on all zones or on specific zones::
$ knotc zone-begin --
$ knotc zone-begin example.com example.net
Now you can list all nodes within the transaction using the ```zone-get```
command, which always returns current data with all changes included. The
command has the same syntax as ```zone-read```.
Within the transaction, you can add a record to a specific zone or to all
zones with an open transaction::
$ knotc zone-add example.com ns1 3600 A 192.168.0.1
$ knotc zone-add -- ns1 3600 A 192.168.0.1
To remove all records with a specific owner, or a specific rrset, or a
specific record data::
$ knotc zone-remove example.com ns1
$ knotc zone-remove example.com ns1 A
$ knotc zone-remove example.com ns1 A 192.168.0.2
To see the difference between the original zone and the current version::
$ knotc zone-diff example.com
Finally, either commit or abort your transaction::
$ knotc zone-commit example.com
$ knotc zone-abort example.com
A full example of setting up a completely new zone from scratch::
$ knotc conf-begin
$ knotc conf-set zone.domain example.com
$ knotc conf-commit
$ knotc zone-begin example.com
$ knotc zone-add example.com @ 7200 SOA ns hostmaster 1 86400 900 691200 3600
$ knotc zone-add example.com ns 3600 A 192.168.0.1
$ knotc zone-add example.com www 3600 A 192.168.0.100
$ knotc zone-commit example.com
.. _Controlling running daemon:
Daemon controls
......
This diff is collapsed.
......@@ -46,6 +46,15 @@ typedef enum {
CTL_ZONE_FLUSH,
CTL_ZONE_SIGN,
CTL_ZONE_READ,
CTL_ZONE_BEGIN,
CTL_ZONE_COMMIT,
CTL_ZONE_ABORT,
CTL_ZONE_DIFF,
CTL_ZONE_GET,
CTL_ZONE_SET,
CTL_ZONE_UNSET,
CTL_CONF_LIST,
CTL_CONF_READ,
CTL_CONF_BEGIN,
......
......@@ -121,6 +121,11 @@ int event_load(conf_t *conf, zone_t *zone)
log_zone_info(zone->name, "loaded, serial %u", current_serial);
}
if (zone->control_update != NULL) {
log_zone_warning(zone->name, "control transaction aborted");
zone_control_clear(zone);
}
return KNOT_EOK;
fail:
......
......@@ -344,35 +344,6 @@ static int add_rr_to_chgset(const knot_rrset_t *rr,
return zone_update_add(update, rr);
}
/*!< \brief Adds RR into remove section of changeset if it is deemed worthy. */
static int rem_rr_to_chgset(const knot_rrset_t *rr,
zone_update_t *update)
{
return zone_update_remove(update, rr);
}
/*!< \brief Adds all RRs from RRSet into remove section of changeset. */
static int rem_rrset_to_chgset(const knot_rrset_t *rrset,
zone_update_t *update)
{
knot_rrset_t rr;
knot_rrset_init(&rr, rrset->owner, rrset->type, rrset->rclass);
for (uint16_t i = 0; i < rrset->rrs.rr_count; ++i) {
knot_rdata_t *rr_add = knot_rdataset_at(&rrset->rrs, i);
int ret = knot_rdataset_add(&rr.rrs, rr_add, NULL);
if (ret != KNOT_EOK) {
return ret;
}
ret = rem_rr_to_chgset(&rr, update);
knot_rdataset_clear(&rr.rrs, NULL);
if (ret != KNOT_EOK) {
return ret;
}
}
return KNOT_EOK;
}
/* ------------------------ RR processing logic ----------------------------- */
/* --------------------------- RR additions --------------------------------- */
......@@ -389,7 +360,7 @@ static int process_add_cname(const zone_node_t *node,
return KNOT_EOK;
}
int ret = rem_rr_to_chgset(&cname, update);
int ret = zone_update_remove(update, &cname);
if (ret != KNOT_EOK) {
return ret;
}
......@@ -526,7 +497,7 @@ static int process_rem_rr(const knot_rrset_t *rr,
return KNOT_EOK;
}
return rem_rr_to_chgset(rr, update);
return zone_update_remove(update, rr);
}
/*!< \brief Removes RRSet from zone. */
......@@ -556,7 +527,7 @@ static int process_rem_rrset(const knot_rrset_t *rrset,
}
knot_rrset_t to_remove = node_rrset(node, rrset->type);
return rem_rrset_to_chgset(&to_remove, update);
return zone_update_remove(update, &to_remove);
}
/*!< \brief Removes node from zone. */
......
......@@ -162,6 +162,35 @@ static const zone_node_t *get_synth_node(zone_update_t *update, const knot_dname
{
const zone_node_t *old_node =
zone_contents_find_node(update->zone->contents, dname);
if (old_node == update->zone->contents->apex && update->change.soa_to != NULL) {
/* We have an apex and a SOA change, make a copy and apply the change. */
zone_node_t *synth_node = node_deep_copy(old_node, &update->mm);
if (synth_node == NULL) {
return NULL;
}
/* Remove the old SOA */
knot_rdataset_t *from = node_rdataset(synth_node, KNOT_RRTYPE_SOA);
knot_rdataset_t *what = node_rdataset(old_node, KNOT_RRTYPE_SOA);
int ret = knot_rdataset_subtract(from, what, &update->mm);
if (ret != KNOT_EOK) {
node_free_rrsets(synth_node, &update->mm);
node_free(&synth_node, &update->mm);
return NULL;
}
/* Add the new SOA */
ret = node_add_rrset(synth_node, update->change.soa_to, &update->mm);
if (ret != KNOT_EOK) {
node_free_rrsets(synth_node, &update->mm);
node_free(&synth_node, &update->mm);
return NULL;
}
old_node = synth_node;
}
const zone_node_t *add_node =
zone_contents_find_node(update->change.add, dname);
const zone_node_t *rem_node =
......@@ -294,17 +323,19 @@ const knot_rdataset_t *zone_update_to(zone_update_t *update)
void zone_update_clear(zone_update_t *update)
{
if (update != NULL) {
if (update->flags & UPDATE_INCREMENTAL) {
/* Revert any changes on error, do nothing on success. */
update_rollback(&update->a_ctx);
changeset_clear(&update->change);
} else if (update->flags & UPDATE_FULL) {
zone_contents_deep_free(&update->new_cont);
}
mp_delete(update->mm.ctx);
memset(update, 0, sizeof(*update));
if (update == NULL) {
return;
}
if (update->flags & UPDATE_INCREMENTAL) {
/* Revert any changes on error, do nothing on success. */
update_rollback(&update->a_ctx);
changeset_clear(&update->change);
} else if (update->flags & UPDATE_FULL) {
zone_contents_deep_free(&update->new_cont);
}
mp_delete(update->mm.ctx);
memset(update, 0, sizeof(*update));
}
int zone_update_add(zone_update_t *update, const knot_rrset_t *rrset)
......@@ -333,12 +364,118 @@ int zone_update_remove(zone_update_t *update, const knot_rrset_t *rrset)
return changeset_rem_rrset(&update->change, rrset, CHANGESET_CHECK);
} else if (update->flags & UPDATE_FULL) {
zone_node_t *n = NULL;
return zone_contents_remove_rr(update->new_cont, rrset, &n);
knot_rrset_t *rrs_copy = knot_rrset_copy(rrset, &update->mm);
int ret = zone_contents_remove_rr(update->new_cont, rrs_copy, &n);
knot_rrset_free(&rrs_copy, &update->mm);
return ret;
} else {
return KNOT_EINVAL;
}
}
int zone_update_remove_rrset(zone_update_t *update, knot_dname_t *owner, uint16_t type)
{
if (update == NULL || owner == NULL) {
return KNOT_EINVAL;
}
if (update->flags & UPDATE_INCREMENTAL) {
/* Remove the RRSet from the original node */
const zone_node_t *node = zone_contents_find_node(update->zone->contents, owner);
if (node != NULL) {
knot_rrset_t rrset = node_rrset(node, type);
int ret = changeset_rem_rrset(&update->change, &rrset, CHANGESET_CHECK);
if (ret != KNOT_EOK) {
return ret;
}
}
/* Remove the RRSet from the additions in the changeset */
const zone_node_t *additions = zone_contents_find_node(update->change.add, owner);
if (additions != NULL) {
knot_rrset_t rrset = node_rrset(additions, type);
int ret = changeset_rem_rrset(&update->change, &rrset, CHANGESET_CHECK);
if (ret != KNOT_EOK) {
return ret;
}
}
if (node == NULL && additions == NULL) {
return KNOT_ENONODE;
}
} else if (update->flags & UPDATE_FULL) {
/* Remove the RRSet from the non-synthesized new node */
const zone_node_t *node = zone_contents_find_node(update->new_cont, owner);
if (node == NULL) {
return KNOT_ENONODE;
}
knot_rrset_t rrset = node_rrset(node, type);
int ret = zone_update_remove(update, &rrset);
if (ret != KNOT_EOK) {
return ret;
}
}
return KNOT_EOK;
}
int zone_update_remove_node(zone_update_t *update, const knot_dname_t *owner)
{
if (update == NULL || owner == NULL) {
return KNOT_EINVAL;
}
if (update->flags & UPDATE_INCREMENTAL) {
/* Remove all RRSets from the original node */
const zone_node_t *node = zone_contents_find_node(update->zone->contents, owner);
if (node != NULL) {
size_t rrset_count = node->rrset_count;
for (int i = 0; i < rrset_count; ++i) {
knot_rrset_t rrset = node_rrset_at(node, rrset_count - 1 - i);
int ret = changeset_rem_rrset(&update->change, &rrset, CHANGESET_CHECK);
if (ret != KNOT_EOK) {
return ret;
}
}
}
/* Remove all RRSets from the additions in the changeset */
const zone_node_t *additions = zone_contents_find_node(update->change.add, owner);
if (additions != NULL) {
size_t rrset_count = additions->rrset_count;
for (int i = 0; i < rrset_count; ++i) {
knot_rrset_t rrset = node_rrset_at(additions, rrset_count - 1 - i);
int ret = changeset_rem_rrset(&update->change, &rrset, CHANGESET_CHECK);
if (ret != KNOT_EOK) {
return ret;
}
}
}
if (node == NULL && additions == NULL) {
return KNOT_ENONODE;
}
} else if (update->flags & UPDATE_FULL) {
/* Remove all RRSets from the non-synthesized new node */
const zone_node_t *node = zone_contents_find_node(update->new_cont, owner);
if (node == NULL) {
return KNOT_ENONODE;
}
size_t rrset_count = node->rrset_count;
for (int i = 0; i < rrset_count; ++i) {
knot_rrset_t rrset = node_rrset_at(node, rrset_count - 1 - i);
int ret = zone_update_remove(update, &rrset);
if (ret != KNOT_EOK) {
return ret;
}
}
}
return KNOT_EOK;
}
static bool apex_rr_changed(const zone_node_t *old_apex,
const zone_node_t *new_apex,
uint16_t type)
......
......@@ -137,6 +137,7 @@ void zone_update_clear(zone_update_t *update);
* in the zone_update iterator(s).
*
* \param update Zone update.
* \param rrset RRSet to add.
*
* \return KNOT_E*
*/
......@@ -150,11 +151,41 @@ int zone_update_add(zone_update_t *update, const knot_rrset_t *rrset);
* in the zone_update iterator(s).
*
* \param update Zone update.
* \param rrset RRSet to remove.
*
* \return KNOT_E*
*/
int zone_update_remove(zone_update_t *update, const knot_rrset_t *rrset);
/*!
* \brief Removes a whole RRSet of specified type from the zone.
*
* \warning Do not edit the zone_update when any iterator is active. Any
* zone_update modifications will invalidate the trie iterators
* in the zone_update iterator(s).
*
* \param update Zone update.
* \param owner Node name to remove.
* \param type RRSet type to remove.
*
* \return KNOT_E*
*/
int zone_update_remove_rrset(zone_update_t *update, knot_dname_t *owner, uint16_t type);
/*!
* \brief Removes a whole node from the zone.
*
* \warning Do not edit the zone_update when any iterator is active. Any
* zone_update modifications will invalidate the trie iterators
* in the zone_update iterator(s).
*
* \param update Zone update.
* \param owner Node name to remove.
*
* \return KNOT_E*
*/
int zone_update_remove_node(zone_update_t *update, const knot_dname_t *owner);
/*!
* \brief Commits all changes to the zone, signs it, saves changes to journal.
*
......
......@@ -22,6 +22,7 @@
#include "knot/common/log.h"
#include "knot/nameserver/process_query.h"
#include "knot/query/requestor.h"
#include "knot/updates/zone-update.h"
#include "knot/zone/contents.h"
#include "knot/zone/serial.h"
#include "knot/zone/zone.h"
......@@ -77,6 +78,17 @@ zone_t* zone_new(const knot_dname_t *name)
return zone;
}
void zone_control_clear(zone_t *zone)
{
if (zone == NULL) {
return;
}
zone_update_clear(zone->control_update);
free(zone->control_update);
zone->control_update = NULL;
}
void zone_free(zone_t **zone_ptr)
{
if (zone_ptr == NULL || *zone_ptr == NULL) {
......@@ -93,6 +105,9 @@ void zone_free(zone_t **zone_ptr)
pthread_mutex_destroy(&zone->ddns_lock);
pthread_mutex_destroy(&zone->journal_lock);
/* Control update. */
zone_control_clear(zone);
/* Free preferred master. */
pthread_mutex_destroy(&zone->preferred_lock);
free(zone->preferred_master);
......
......@@ -36,6 +36,7 @@
#include "libknot/packet/pkt.h"
struct process_query_param;
struct zone_update;
/*!
* \brief Zone flags.
......@@ -72,6 +73,9 @@ typedef struct zone
size_t ddns_queue_size;
list_t ddns_queue;
/*! \brief Control update context. */
struct zone_update *control_update;
/*! \brief Journal access lock. */
pthread_mutex_t journal_lock;
......@@ -103,6 +107,13 @@ zone_t* zone_new(const knot_dname_t *name);
*/
void zone_free(zone_t **zone_ptr);
/*!
* \brief Clears possible control update transaction.
*
* \param zone Zone to be cleared.
*/
void zone_control_clear(zone_t *zone);
/*!
* \note Zone change API below, subject to change.
* \ref #223 New zone API
......
......@@ -157,6 +157,11 @@ static zone_t *create_zone_reload(conf_t *conf, const knot_dname_t *name,
assert(0);
}
if (old_zone->control_update != NULL) {
log_zone_warning(old_zone->name, "control transaction aborted");
zone_control_clear(old_zone);
}
return zone;
}
......
This diff is collapsed.
......@@ -36,7 +36,8 @@ typedef enum {
CMD_FREQ_ITEM = 1 << 3, /*!< Required item argument. */
CMD_FOPT_DATA = 1 << 4, /*!< Optional item data argument. */
CMD_FOPT_ZONE = 1 << 5, /*!< Optional zone name argument. */
CMD_FREQ_TXN = 1 << 6, /*!< Required open confdb transaction. */
CMD_FREQ_ZONE = 1 << 6, /*!< Required zone name argument. */
CMD_FREQ_TXN = 1 << 7, /*!< Required open confdb transaction. */
} cmd_flag_t;
struct cmd_desc;
......
......@@ -325,7 +325,11 @@ static unsigned char complete(EditLine *el, int ch)
}
// Complete the zone name.
if (desc->flags & CMD_FOPT_ZONE) {
if (desc->flags & (CMD_FREQ_ZONE | CMD_FOPT_ZONE)) {
if (token > 1 && !(desc->flags & CMD_FOPT_ZONE)) {
goto complete_exit;
}
if (desc->flags & CMD_FREAD) {
local_zones_lookup(el, argv[token], pos);
} else {
......
......@@ -28,8 +28,10 @@
static const char *zone_str1 = "test. 600 IN SOA ns.test. m.test. 1 900 300 4800 900 \n";
static const char *zone_str2 = "test. IN TXT \"test\"\n";
static const char *add_str = "test. IN TXT \"test2\"\n";
static const char *del_str = "test. IN TXT \"test\"\n";
static const char *add_str = "test. IN TXT \"test2\"\n";
static const char *del_str = "test. IN TXT \"test\"\n";
static const char *node_str1 = "node.test. IN TXT \"abc\"\n";
static const char *node_str2 = "node.test. IN TXT \"def\"\n";
knot_rrset_t rrset;
......@@ -37,16 +39,18 @@ knot_rrset_t rrset;
static bool node_contains_rr(const zone_node_t *node,
const knot_rrset_t *rr)
{
const knot_rdataset_t *zone_rrs = node_rdataset(node, rr->type);
if (zone_rrs) {
assert(rr->rrs.rr_count == 1);
const bool compare_ttls = false;
return knot_rdataset_member(zone_rrs,
knot_rdataset_at(&rr->rrs, 0),
compare_ttls);
} else {
return false;
}
const knot_rdataset_t *zone_rrs = node_rdataset(node, rr->type);
if (zone_rrs) {
for (size_t i = 0; i < rr->rrs.rr_count; ++i) {
if (!knot_rdataset_member(zone_rrs, knot_rdataset_at(&rr->rrs, i), false)) {
return false;
}
}
return true;
} else {
return false;
}
}
static void process_rr(zs_scanner_t *scanner)
......@@ -62,6 +66,7 @@ static void process_rr(zs_scanner_t *scanner)
void test_full(zone_t *zone, zs_scanner_t *sc)
{
zone_update_t update;
/* Init update */
int ret = zone_update_init(&update, zone, UPDATE_FULL);
ok(ret == KNOT_EOK, "zone update: init full");
......@@ -70,6 +75,7 @@ void test_full(zone_t *zone, zs_scanner_t *sc)
assert(0);
}
/* First addition */
ret = zone_update_add(&update, &rrset);
knot_rdataset_clear(&rrset.rrs, NULL);
ok(ret == KNOT_EOK, "full zone update: first addition");
......@@ -79,23 +85,52 @@ void test_full(zone_t *zone, zs_scanner_t *sc)
assert(0);
}
/* Second addition */
ret = zone_update_add(&update, &rrset);
zone_node_t *node = zone_contents_find_node_for_rr(update.new_cont, &rrset);
zone_node_t *node = (zone_node_t *) zone_update_get_node(&update, rrset.owner);
bool rrset_present = node_contains_rr(node, &rrset);
ok(ret == KNOT_EOK && rrset_present, "full zone update: second addition");
/* Removal */
ret = zone_update_remove(&update, &rrset);
node = zone_contents_find_node_for_rr(update.new_cont, &rrset);
node = (zone_node_t *) zone_update_get_node(&update, rrset.owner);
rrset_present = node_contains_rr(node, &rrset);
ok(ret == KNOT_EOK && !rrset_present, "full zone update: removal");
/* Last addition */
ret = zone_update_add(&update, &rrset);
node = zone_contents_find_node_for_rr(update.new_cont, &rrset);
node = (zone_node_t *) zone_update_get_node(&update, rrset.owner);
rr