]>
Commit | Line | Data |
---|---|---|
1c2facd1 RW |
1 | /* |
2 | * Copyright (C) 2018 NetDEF, Inc. | |
3 | * Renato Westphal | |
4 | * | |
5 | * This program is free software; you can redistribute it and/or modify it | |
6 | * under the terms of the GNU General Public License as published by the Free | |
7 | * Software Foundation; either version 2 of the License, or (at your option) | |
8 | * any later version. | |
9 | * | |
10 | * This program is distributed in the hope that it will be useful, but WITHOUT | |
11 | * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or | |
12 | * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for | |
13 | * more details. | |
14 | * | |
15 | * You should have received a copy of the GNU General Public License along | |
16 | * with this program; see the file COPYING; if not, write to the Free Software | |
17 | * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA | |
18 | */ | |
19 | ||
20 | #include <zebra.h> | |
21 | ||
22 | #include "log.h" | |
23 | #include "lib_errors.h" | |
24 | #include "hash.h" | |
25 | #include "yang.h" | |
26 | #include "yang_translator.h" | |
27 | ||
28 | DEFINE_MTYPE_STATIC(LIB, YANG_TRANSLATOR, "YANG Translator") | |
29 | DEFINE_MTYPE_STATIC(LIB, YANG_TRANSLATOR_MODULE, "YANG Translator Module") | |
30 | DEFINE_MTYPE_STATIC(LIB, YANG_TRANSLATOR_MAPPING, "YANG Translator Mapping") | |
31 | ||
32 | /* Generate the yang_translators tree. */ | |
33 | static inline int yang_translator_compare(const struct yang_translator *a, | |
34 | const struct yang_translator *b) | |
35 | { | |
36 | return strcmp(a->family, b->family); | |
37 | } | |
38 | RB_GENERATE(yang_translators, yang_translator, entry, yang_translator_compare) | |
39 | ||
40 | struct yang_translators yang_translators = RB_INITIALIZER(&yang_translators); | |
41 | ||
42 | /* Separate libyang context for the translator module. */ | |
43 | static struct ly_ctx *ly_translator_ctx; | |
44 | ||
45 | static unsigned int | |
46 | yang_translator_validate(struct yang_translator *translator); | |
47 | static unsigned int yang_module_nodes_count(const struct lys_module *module); | |
48 | static void str_replace(char *o_string, const char *s_string, | |
49 | const char *r_string); | |
50 | ||
51 | struct yang_mapping_node { | |
52 | char xpath_from_canonical[XPATH_MAXLEN]; | |
53 | char xpath_from_fmt[XPATH_MAXLEN]; | |
54 | char xpath_to_fmt[XPATH_MAXLEN]; | |
55 | }; | |
56 | ||
57 | static bool yang_mapping_hash_cmp(const void *value1, const void *value2) | |
58 | { | |
59 | const struct yang_mapping_node *c1 = value1; | |
60 | const struct yang_mapping_node *c2 = value2; | |
61 | ||
62 | return strmatch(c1->xpath_from_canonical, c2->xpath_from_canonical); | |
63 | } | |
64 | ||
65 | static unsigned int yang_mapping_hash_key(void *value) | |
66 | { | |
67 | return string_hash_make(value); | |
68 | } | |
69 | ||
70 | static void *yang_mapping_hash_alloc(void *p) | |
71 | { | |
72 | struct yang_mapping_node *new, *key = p; | |
73 | ||
74 | new = XCALLOC(MTYPE_YANG_TRANSLATOR_MAPPING, sizeof(*new)); | |
75 | strlcpy(new->xpath_from_canonical, key->xpath_from_canonical, | |
76 | sizeof(new->xpath_from_canonical)); | |
77 | ||
78 | return new; | |
79 | } | |
80 | ||
81 | static void yang_mapping_hash_free(void *arg) | |
82 | { | |
83 | XFREE(MTYPE_YANG_TRANSLATOR_MAPPING, arg); | |
84 | } | |
85 | ||
86 | static struct yang_mapping_node * | |
87 | yang_mapping_lookup(const struct yang_translator *translator, int dir, | |
88 | const char *xpath) | |
89 | { | |
90 | struct yang_mapping_node s; | |
91 | ||
92 | strlcpy(s.xpath_from_canonical, xpath, sizeof(s.xpath_from_canonical)); | |
93 | return hash_lookup(translator->mappings[dir], &s); | |
94 | } | |
95 | ||
96 | static void yang_mapping_add(struct yang_translator *translator, int dir, | |
97 | const struct lys_node *snode, | |
98 | const char *xpath_from_fmt, | |
99 | const char *xpath_to_fmt) | |
100 | { | |
101 | struct yang_mapping_node *mapping, s; | |
102 | ||
103 | yang_snode_get_path(snode, YANG_PATH_DATA, s.xpath_from_canonical, | |
104 | sizeof(s.xpath_from_canonical)); | |
105 | mapping = hash_get(translator->mappings[dir], &s, | |
106 | yang_mapping_hash_alloc); | |
107 | strlcpy(mapping->xpath_from_fmt, xpath_from_fmt, | |
108 | sizeof(mapping->xpath_from_fmt)); | |
109 | strlcpy(mapping->xpath_to_fmt, xpath_to_fmt, | |
110 | sizeof(mapping->xpath_to_fmt)); | |
111 | str_replace(mapping->xpath_from_fmt, "KEY1", "%[^']"); | |
112 | str_replace(mapping->xpath_from_fmt, "KEY2", "%[^']"); | |
113 | str_replace(mapping->xpath_from_fmt, "KEY3", "%[^']"); | |
114 | str_replace(mapping->xpath_from_fmt, "KEY4", "%[^']"); | |
115 | str_replace(mapping->xpath_to_fmt, "KEY1", "%s"); | |
116 | str_replace(mapping->xpath_to_fmt, "KEY2", "%s"); | |
117 | str_replace(mapping->xpath_to_fmt, "KEY3", "%s"); | |
118 | str_replace(mapping->xpath_to_fmt, "KEY4", "%s"); | |
119 | } | |
120 | ||
121 | struct yang_translator *yang_translator_load(const char *path) | |
122 | { | |
123 | struct yang_translator *translator; | |
124 | struct yang_tmodule *tmodule; | |
125 | const char *family; | |
126 | struct lyd_node *dnode; | |
127 | struct ly_set *set; | |
128 | struct listnode *ln; | |
129 | ||
130 | /* Load module translator (JSON file). */ | |
131 | dnode = lyd_parse_path(ly_translator_ctx, path, LYD_JSON, | |
132 | LYD_OPT_CONFIG); | |
133 | if (!dnode) { | |
134 | flog_warn(EC_LIB_YANG_TRANSLATOR_LOAD, | |
135 | "%s: lyd_parse_path() failed", __func__); | |
136 | return NULL; | |
137 | } | |
138 | dnode = yang_dnode_get(dnode, | |
139 | "/frr-module-translator:frr-module-translator"); | |
140 | /* | |
141 | * libyang guarantees the "frr-module-translator" top-level container is | |
142 | * always present since it contains mandatory child nodes. | |
143 | */ | |
144 | assert(dnode); | |
145 | ||
146 | family = yang_dnode_get_string(dnode, "./family"); | |
147 | translator = yang_translator_find(family); | |
148 | if (translator != NULL) { | |
149 | flog_warn(EC_LIB_YANG_TRANSLATOR_LOAD, | |
150 | "%s: module translator \"%s\" is loaded already", | |
151 | __func__, family); | |
152 | return NULL; | |
153 | } | |
154 | ||
155 | translator = XCALLOC(MTYPE_YANG_TRANSLATOR, sizeof(*translator)); | |
156 | strlcpy(translator->family, family, sizeof(translator->family)); | |
157 | translator->modules = list_new(); | |
158 | for (size_t i = 0; i < YANG_TRANSLATE_MAX; i++) | |
159 | translator->mappings[i] = hash_create(yang_mapping_hash_key, | |
160 | yang_mapping_hash_cmp, | |
161 | "YANG translation table"); | |
162 | RB_INSERT(yang_translators, &yang_translators, translator); | |
163 | ||
164 | /* Initialize the translator libyang context. */ | |
165 | translator->ly_ctx = ly_ctx_new(NULL, LY_CTX_DISABLE_SEARCHDIR_CWD); | |
166 | if (!translator->ly_ctx) { | |
167 | flog_warn(EC_LIB_LIBYANG, "%s: ly_ctx_new() failed", __func__); | |
168 | goto error; | |
169 | } | |
170 | ly_ctx_set_searchdir(translator->ly_ctx, YANG_MODELS_PATH); | |
171 | ||
172 | /* Load modules and deviations. */ | |
173 | set = lyd_find_path(dnode, "./module"); | |
174 | assert(set); | |
175 | for (size_t i = 0; i < set->number; i++) { | |
176 | const char *module_name; | |
177 | ||
178 | tmodule = | |
179 | XCALLOC(MTYPE_YANG_TRANSLATOR_MODULE, sizeof(*tmodule)); | |
180 | ||
181 | module_name = yang_dnode_get_string(set->set.d[i], "./name"); | |
182 | tmodule->module = ly_ctx_load_module(translator->ly_ctx, | |
183 | module_name, NULL); | |
184 | if (!tmodule->module) { | |
185 | flog_warn(EC_LIB_YANG_TRANSLATOR_LOAD, | |
186 | "%s: failed to load module: %s", __func__, | |
187 | module_name); | |
188 | ly_set_free(set); | |
189 | goto error; | |
190 | } | |
191 | ||
192 | module_name = | |
193 | yang_dnode_get_string(set->set.d[i], "./deviations"); | |
194 | tmodule->deviations = ly_ctx_load_module(translator->ly_ctx, | |
195 | module_name, NULL); | |
196 | if (!tmodule->deviations) { | |
197 | flog_warn(EC_LIB_YANG_TRANSLATOR_LOAD, | |
198 | "%s: failed to load module: %s", __func__, | |
199 | module_name); | |
200 | ly_set_free(set); | |
201 | goto error; | |
202 | } | |
203 | lys_set_disabled(tmodule->deviations); | |
204 | ||
205 | listnode_add(translator->modules, tmodule); | |
206 | } | |
207 | ly_set_free(set); | |
208 | ||
209 | /* Calculate the coverage. */ | |
210 | for (ALL_LIST_ELEMENTS_RO(translator->modules, ln, tmodule)) { | |
211 | tmodule->nodes_before_deviations = | |
212 | yang_module_nodes_count(tmodule->module); | |
213 | ||
214 | lys_set_enabled(tmodule->deviations); | |
215 | ||
216 | tmodule->nodes_after_deviations = | |
217 | yang_module_nodes_count(tmodule->module); | |
218 | tmodule->coverage = ((double)tmodule->nodes_after_deviations | |
219 | / (double)tmodule->nodes_before_deviations) | |
220 | * 100; | |
221 | } | |
222 | ||
223 | /* Load mappings. */ | |
224 | set = lyd_find_path(dnode, "./module/mappings"); | |
225 | assert(set); | |
226 | for (size_t i = 0; i < set->number; i++) { | |
227 | const char *xpath_custom, *xpath_native; | |
228 | const struct lys_node *snode_custom, *snode_native; | |
229 | ||
230 | xpath_custom = yang_dnode_get_string(set->set.d[i], "./custom"); | |
231 | snode_custom = ly_ctx_get_node(translator->ly_ctx, NULL, | |
232 | xpath_custom, 0); | |
233 | if (!snode_custom) { | |
234 | flog_warn(EC_LIB_YANG_TRANSLATOR_LOAD, | |
235 | "%s: unknown data path: %s", __func__, | |
236 | xpath_custom); | |
237 | ly_set_free(set); | |
238 | goto error; | |
239 | } | |
240 | ||
241 | xpath_native = yang_dnode_get_string(set->set.d[i], "./native"); | |
242 | snode_native = | |
243 | ly_ctx_get_node(ly_native_ctx, NULL, xpath_native, 0); | |
244 | if (!snode_native) { | |
245 | flog_warn(EC_LIB_YANG_TRANSLATOR_LOAD, | |
246 | "%s: unknown data path: %s", __func__, | |
247 | xpath_native); | |
248 | ly_set_free(set); | |
249 | goto error; | |
250 | } | |
251 | ||
252 | yang_mapping_add(translator, YANG_TRANSLATE_TO_NATIVE, | |
253 | snode_custom, xpath_custom, xpath_native); | |
254 | yang_mapping_add(translator, YANG_TRANSLATE_FROM_NATIVE, | |
255 | snode_native, xpath_native, xpath_custom); | |
256 | } | |
257 | ly_set_free(set); | |
258 | ||
259 | /* Validate mappings. */ | |
260 | if (yang_translator_validate(translator) != 0) | |
261 | goto error; | |
262 | ||
263 | yang_dnode_free(dnode); | |
264 | ||
265 | return translator; | |
266 | ||
267 | error: | |
268 | yang_dnode_free(dnode); | |
269 | yang_translator_unload(translator); | |
270 | ||
271 | return NULL; | |
272 | } | |
273 | ||
274 | static void yang_tmodule_delete(struct yang_tmodule *tmodule) | |
275 | { | |
276 | XFREE(MTYPE_YANG_TRANSLATOR_MODULE, tmodule); | |
277 | } | |
278 | ||
279 | void yang_translator_unload(struct yang_translator *translator) | |
280 | { | |
281 | for (size_t i = 0; i < YANG_TRANSLATE_MAX; i++) | |
282 | hash_clean(translator->mappings[i], yang_mapping_hash_free); | |
283 | translator->modules->del = (void (*)(void *))yang_tmodule_delete; | |
284 | list_delete(&translator->modules); | |
285 | ly_ctx_destroy(translator->ly_ctx, NULL); | |
286 | RB_REMOVE(yang_translators, &yang_translators, translator); | |
287 | XFREE(MTYPE_YANG_TRANSLATOR, translator); | |
288 | } | |
289 | ||
290 | struct yang_translator *yang_translator_find(const char *family) | |
291 | { | |
292 | struct yang_translator s; | |
293 | ||
294 | strlcpy(s.family, family, sizeof(s.family)); | |
295 | return RB_FIND(yang_translators, &yang_translators, &s); | |
296 | } | |
297 | ||
298 | enum yang_translate_result | |
299 | yang_translate_xpath(const struct yang_translator *translator, int dir, | |
300 | char *xpath, size_t xpath_len) | |
301 | { | |
302 | struct ly_ctx *ly_ctx; | |
303 | const struct lys_node *snode; | |
304 | struct yang_mapping_node *mapping; | |
305 | char xpath_canonical[XPATH_MAXLEN]; | |
306 | char keys[4][LIST_MAXKEYLEN]; | |
307 | int n; | |
308 | ||
309 | if (dir == YANG_TRANSLATE_TO_NATIVE) | |
310 | ly_ctx = translator->ly_ctx; | |
311 | else | |
312 | ly_ctx = ly_native_ctx; | |
313 | ||
314 | snode = ly_ctx_get_node(ly_ctx, NULL, xpath, 0); | |
315 | if (!snode) { | |
316 | flog_warn(EC_LIB_YANG_TRANSLATION_ERROR, | |
317 | "%s: unknown data path: %s", __func__, xpath); | |
318 | return YANG_TRANSLATE_FAILURE; | |
319 | } | |
320 | ||
321 | yang_snode_get_path(snode, YANG_PATH_DATA, xpath_canonical, | |
322 | sizeof(xpath_canonical)); | |
323 | mapping = yang_mapping_lookup(translator, dir, xpath_canonical); | |
324 | if (!mapping) | |
325 | return YANG_TRANSLATE_NOTFOUND; | |
326 | ||
327 | n = sscanf(xpath, mapping->xpath_from_fmt, keys[0], keys[1], keys[2], | |
328 | keys[3]); | |
329 | if (n < 0) { | |
330 | flog_warn(EC_LIB_YANG_TRANSLATION_ERROR, | |
331 | "%s: sscanf() failed: %s", __func__, | |
332 | safe_strerror(errno)); | |
333 | return YANG_TRANSLATE_FAILURE; | |
334 | } | |
335 | ||
336 | snprintf(xpath, xpath_len, mapping->xpath_to_fmt, keys[0], keys[1], | |
337 | keys[2], keys[3]); | |
338 | ||
339 | return YANG_TRANSLATE_SUCCESS; | |
340 | } | |
341 | ||
342 | int yang_translate_dnode(const struct yang_translator *translator, int dir, | |
343 | struct lyd_node **dnode) | |
344 | { | |
345 | struct ly_ctx *ly_ctx; | |
346 | struct lyd_node *new; | |
347 | struct lyd_node *root, *next, *dnode_iter; | |
348 | ||
349 | /* Create new libyang data node to hold the translated data. */ | |
350 | if (dir == YANG_TRANSLATE_TO_NATIVE) | |
351 | ly_ctx = ly_native_ctx; | |
352 | else | |
353 | ly_ctx = translator->ly_ctx; | |
354 | new = yang_dnode_new(ly_ctx); | |
355 | ||
356 | /* Iterate over all nodes from the data tree. */ | |
357 | LY_TREE_FOR (*dnode, root) { | |
358 | LY_TREE_DFS_BEGIN (root, next, dnode_iter) { | |
359 | char xpath[XPATH_MAXLEN]; | |
360 | enum yang_translate_result ret; | |
361 | ||
362 | yang_dnode_get_path(dnode_iter, xpath, sizeof(xpath)); | |
363 | ret = yang_translate_xpath(translator, dir, xpath, | |
364 | sizeof(xpath)); | |
365 | switch (ret) { | |
366 | case YANG_TRANSLATE_SUCCESS: | |
367 | break; | |
368 | case YANG_TRANSLATE_NOTFOUND: | |
369 | goto next; | |
370 | case YANG_TRANSLATE_FAILURE: | |
371 | goto error; | |
372 | } | |
373 | ||
374 | /* Create new node in the tree of translated data. */ | |
375 | ly_errno = 0; | |
376 | if (!lyd_new_path(new, ly_ctx, xpath, | |
377 | (void *)yang_dnode_get_string( | |
378 | dnode_iter, NULL), | |
379 | 0, LYD_PATH_OPT_UPDATE) | |
380 | && ly_errno) { | |
381 | flog_err(EC_LIB_LIBYANG, | |
382 | "%s: lyd_new_path() failed", __func__); | |
383 | goto error; | |
384 | } | |
385 | ||
386 | next: | |
387 | LY_TREE_DFS_END(root, next, dnode_iter); | |
388 | } | |
389 | } | |
390 | ||
391 | /* Replace dnode by the new translated dnode. */ | |
392 | yang_dnode_free(*dnode); | |
393 | *dnode = new; | |
394 | ||
395 | return YANG_TRANSLATE_SUCCESS; | |
396 | ||
397 | error: | |
398 | yang_dnode_free(new); | |
399 | ||
400 | return YANG_TRANSLATE_FAILURE; | |
401 | } | |
402 | ||
403 | static void yang_translator_validate_cb(const struct lys_node *snode_custom, | |
404 | void *arg1, void *arg2) | |
405 | { | |
406 | struct yang_translator *translator = arg1; | |
407 | unsigned int *errors = arg2; | |
408 | struct yang_mapping_node *mapping; | |
409 | const struct lys_node *snode_native; | |
410 | const struct lys_type *stype_custom, *stype_native; | |
411 | char xpath[XPATH_MAXLEN]; | |
412 | ||
413 | yang_snode_get_path(snode_custom, YANG_PATH_DATA, xpath, sizeof(xpath)); | |
414 | mapping = yang_mapping_lookup(translator, YANG_TRANSLATE_TO_NATIVE, | |
415 | xpath); | |
416 | if (!mapping) { | |
417 | flog_warn(EC_LIB_YANG_TRANSLATOR_LOAD, | |
418 | "%s: missing mapping for \"%s\"", __func__, xpath); | |
419 | *errors += 1; | |
420 | return; | |
421 | } | |
422 | ||
423 | snode_native = | |
424 | ly_ctx_get_node(ly_native_ctx, NULL, mapping->xpath_to_fmt, 0); | |
425 | assert(snode_native); | |
426 | ||
427 | /* Check if the YANG types are compatible. */ | |
428 | stype_custom = yang_snode_get_type(snode_custom); | |
429 | stype_native = yang_snode_get_type(snode_native); | |
430 | if (stype_custom && stype_native) { | |
431 | if (stype_custom->base != stype_native->base) { | |
432 | flog_warn( | |
433 | EC_LIB_YANG_TRANSLATOR_LOAD, | |
434 | "%s: YANG types are incompatible (xpath: \"%s\")", | |
435 | __func__, xpath); | |
436 | *errors += 1; | |
437 | return; | |
438 | } | |
439 | ||
440 | /* TODO: check if the value spaces are identical. */ | |
441 | } | |
442 | } | |
443 | ||
444 | /* | |
445 | * Check if the modules from the translator have a mapping for all of their | |
446 | * schema nodes (after loading the deviations). | |
447 | */ | |
448 | static unsigned int yang_translator_validate(struct yang_translator *translator) | |
449 | { | |
450 | struct yang_tmodule *tmodule; | |
451 | struct listnode *ln; | |
452 | unsigned int errors = 0; | |
453 | ||
454 | for (ALL_LIST_ELEMENTS_RO(translator->modules, ln, tmodule)) { | |
455 | yang_module_snodes_iterate( | |
456 | tmodule->module, yang_translator_validate_cb, | |
457 | YANG_ITER_FILTER_NPCONTAINERS | |
458 | | YANG_ITER_FILTER_LIST_KEYS | |
459 | | YANG_ITER_FILTER_INPUT_OUTPUT, | |
460 | translator, &errors); | |
461 | } | |
462 | ||
463 | if (errors) | |
464 | flog_warn( | |
465 | EC_LIB_YANG_TRANSLATOR_LOAD, | |
466 | "%s: failed to validate \"%s\" module translator: %u error(s)", | |
467 | __func__, translator->family, errors); | |
468 | ||
469 | return errors; | |
470 | } | |
471 | ||
472 | static void yang_module_nodes_count_cb(const struct lys_node *snode, void *arg1, | |
473 | void *arg2) | |
474 | { | |
475 | unsigned int *total = arg1; | |
476 | ||
477 | *total += 1; | |
478 | } | |
479 | ||
480 | /* Calculate the number of nodes for the given module. */ | |
481 | static unsigned int yang_module_nodes_count(const struct lys_module *module) | |
482 | { | |
483 | unsigned int total = 0; | |
484 | ||
485 | yang_module_snodes_iterate(module, yang_module_nodes_count_cb, | |
486 | YANG_ITER_FILTER_NPCONTAINERS | |
487 | | YANG_ITER_FILTER_LIST_KEYS | |
488 | | YANG_ITER_FILTER_INPUT_OUTPUT, | |
489 | &total, NULL); | |
490 | ||
491 | return total; | |
492 | } | |
493 | ||
494 | /* TODO: rewrite this function. */ | |
495 | static void str_replace(char *o_string, const char *s_string, | |
496 | const char *r_string) | |
497 | { | |
498 | char buffer[BUFSIZ]; | |
499 | char *ch; | |
500 | ||
501 | ch = strstr(o_string, s_string); | |
502 | if (!ch) | |
503 | return; | |
504 | ||
505 | strncpy(buffer, o_string, ch - o_string); | |
506 | buffer[ch - o_string] = 0; | |
507 | ||
508 | sprintf(buffer + (ch - o_string), "%s%s", r_string, | |
509 | ch + strlen(s_string)); | |
510 | ||
511 | o_string[0] = 0; | |
512 | strcpy(o_string, buffer); | |
513 | return str_replace(o_string, s_string, r_string); | |
514 | } | |
515 | ||
516 | void yang_translator_init(void) | |
517 | { | |
518 | ly_translator_ctx = ly_ctx_new(NULL, LY_CTX_DISABLE_SEARCHDIR_CWD); | |
519 | if (!ly_translator_ctx) { | |
520 | flog_err(EC_LIB_LIBYANG, "%s: ly_ctx_new() failed", __func__); | |
521 | exit(1); | |
522 | } | |
523 | ly_ctx_set_searchdir(ly_translator_ctx, YANG_MODELS_PATH); | |
524 | ||
525 | if (!ly_ctx_load_module(ly_translator_ctx, "frr-module-translator", | |
526 | NULL)) { | |
527 | flog_err( | |
528 | EC_LIB_YANG_MODULE_LOAD, | |
529 | "%s: failed to load the \"frr-module-translator\" module", | |
530 | __func__); | |
531 | exit(1); | |
532 | } | |
533 | } | |
534 | ||
535 | void yang_translator_terminate(void) | |
536 | { | |
537 | while (!RB_EMPTY(yang_translators, &yang_translators)) { | |
538 | struct yang_translator *translator; | |
539 | ||
540 | translator = RB_ROOT(yang_translators, &yang_translators); | |
541 | yang_translator_unload(translator); | |
542 | } | |
543 | ||
544 | ly_ctx_destroy(ly_translator_ctx, NULL); | |
545 | } |