1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
|
module CT = Vyos1x.Config_tree
module IC = Vyos1x.Internal.Make(CT)
module CC = Commitd_client.Commit
module CD = Vyos1x.Config_diff
module VT = Vyos1x.Vytree
module VL = Vyos1x.Vylist
module RT = Vyos1x.Reference_tree
module D = Directories
module FP = FilePath
exception Session_error of string
type cfg_op =
| CfgSet of string list * string option * CT.value_behaviour
| CfgDelete of string list * string option
[@@deriving yojson]
type world = {
mutable running_config: CT.t;
mutable reference_tree: RT.t;
vyconf_config: Vyconf_config.t;
dirs: Directories.t
}
type aux_op = {
script_name: string;
tag_value: string option;
changeset: cfg_op list;
} [@@deriving yojson]
type session_data = {
modified: bool;
conf_mode: bool;
changeset: cfg_op list;
mutable aux_changeset: aux_op list;
edit_level: string list;
client_app: string;
client_pid: int32;
client_user: string;
client_sudo_user: string;
} [@@deriving yojson]
let make _world client_app sudo_user user pid = {
modified = false;
conf_mode = false;
changeset = [];
aux_changeset = [];
edit_level = [];
client_app = client_app;
client_user = user;
client_sudo_user = sudo_user;
client_pid = pid;
}
let string_of_op op =
match op with
| CfgSet (path, value, _) ->
let path_str = Vyos1x.Util.string_of_list path in
(match value with
| None -> Printf.sprintf "set %s" path_str
| Some v -> Printf.sprintf "set %s \"%s\"" path_str v)
| CfgDelete (path, value) ->
let path_str = Vyos1x.Util.string_of_list path in
(match value with
| None -> Printf.sprintf "delete %s" path_str
| Some v -> Printf.sprintf "delete %s \"%s\"" path_str v)
let sprint_changeset ss =
let ss = List.map (fun x -> aux_op_to_yojson x) ss in
Yojson.Safe.to_string (`List ss)
let set_modified s =
if s.modified = true then s
else {s with modified = true}
let apply_cfg_op w op config =
(* alert exn RT.refpath; RT.is_leaf; CT.set; CT.create_node; RT.set_tag_value:
[Vytree.Empty_path] not possible as checked in set
[Vytree.Nonexistent_path] not possible as checked in validate, in set
alert exn CT.set; CT.create_node:
[Config_tree.Useless_set] caught
alert exn CT.set:
[Config_tree.Duplicate_value] caught
alert exn CT.delete; CT.prune_delete:
[Vytree.Empty_path] not possible as checked in delete
[Vytree.Nonexistent_path] not possible as checked in update_delete
*)
let result =
match op with
| CfgSet (path, value, value_behaviour) ->
begin
let rt = w.reference_tree in
let refp = (RT.refpath[@alert "-exn"]) rt path in
try
let c =
match ((RT.is_leaf[@alert "-exn"]) rt refp) with
| true ->
(CT.set[@alert "-exn"]) config path value value_behaviour
| false ->
(CT.create_node[@alert "-exn"]) config path
in
(RT.set_tag_data[@alert "-exn"]) rt c path
with
| CT.Useless_set | CT.Duplicate_value -> config
end
| CfgDelete (path, value) ->
begin
try
(CT.delete[@alert "-exn"]) config path value |>
(fun c -> (CT.prune_delete[@alert "-exn"]) c path)
with
| VT.Nonexistent_path | CT.No_such_value -> config
end
in result
let rec apply_changes w changeset config =
match changeset with
| [] -> config
| c :: cs -> apply_changes w cs (apply_cfg_op w c config)
let validate w _s path =
(* alert exn RT.validate_path:
[Reference_tree.Validation_error] caught
*)
try
(RT.validate_path[@alert "-exn"]) D.(w.dirs.validators) w.reference_tree path
with RT.Validation_error x -> raise (Session_error x)
let validate_tree w t =
try
let out = (RT.validate_tree[@alert "-exn"]) D.(w.dirs.validators) w.reference_tree t in
match out with
| "" -> ()
| _ -> raise (Session_error out)
with RT.Validation_error x -> raise (Session_error x)
let split_path w path =
RT.split_path w.reference_tree path
let get_proposed_config w s =
let c = w.running_config in
apply_changes w (List.rev s.changeset) c
let update_set w _s changeset path =
(* alert exn RT.refpath; RT.is_multi:
[Vytree.Empty_path] checked or n/a in callers set, aux_set, get_changeset
[Vytree.Nonexistent_path] checked or n/a in callers set, aux_set, get_changeset
*)
let path, value = split_path w path in
let refpath = (RT.refpath[@alert "-exn"]) w.reference_tree path in
let value_behaviour =
if (RT.is_multi[@alert "-exn"]) w.reference_tree refpath
then CT.AddValue else CT.ReplaceValue
in
let op = CfgSet (path, value, value_behaviour) in
(op :: changeset)
let update_delete w s changeset path =
(* alert exn VT.exists; CT.value_exists:
[Vytree.Empty_path] checked or n/a in callers delete, aux_delete, get_changeset
alert exn CT.value_exists:
[Vytree.Nonexistent_path] checked by VT.exists
*)
let path, value = split_path w path in
let proposed_config = get_proposed_config w s in
if not ((VT.exists[@alert "-exn"]) proposed_config path)
then raise (Session_error "Non-existent path")
else
let check_value =
match value with
| None -> true
| Some v -> (CT.value_exists[@alert "-exn"]) proposed_config path v
in
if not check_value
then raise (Session_error "Non-existent value")
else
let op = CfgDelete (path, value) in
(op :: changeset)
let get_changeset w s lt rt =
(* alert exn CD.diff_tree:
[Config_diff.Incommensurable] not possible for base root
[Config_diff.Empty_comparison] not possible for empty path
*)
let diff = (CD.diff_tree[@alert "-exn"]) [] lt rt in
let add_tree = CT.get_subtree diff ["add"] in
let del_tree = CT.get_subtree diff ["del"] in
let add_changeset =
List.fold_left (update_set w s) [] (CT.value_paths_of_tree add_tree)
in
let del_changeset =
List.fold_left (update_delete w s) [] (CT.value_paths_of_tree del_tree)
in
add_changeset @ del_changeset
let set w s path =
if Vyos1x.Util.is_empty path
then raise (Session_error "Path is empty")
else
let path_total = s.edit_level @ path in
let _ = validate w s path_total in
let changeset' = update_set w s s.changeset path_total in
{ s with changeset = changeset' }
let delete w s path =
if Vyos1x.Util.is_empty path
then raise (Session_error "Path is empty")
else
let path_total = s.edit_level @ path in
let changeset' = update_delete w s s.changeset path_total in
{ s with changeset = changeset' }
let aux_set w s path name tagval =
if Vyos1x.Util.is_empty path
then raise (Session_error "Path is empty")
else
let _ = validate w s path in
let aux = s.aux_changeset in
let ident y =
if (y.script_name <> name || y.tag_value <> tagval) then false
else true
in
let op' = VL.find ident aux in
let changeset' =
match op' with
| None ->
update_set w s [] path
| Some o ->
update_set w s o.changeset path
in
let op =
{ script_name = name; tag_value = tagval; changeset = changeset' }
in
let aux_changeset' =
(* Vylist.replace does not raise exception when force=true *)
(VL.replace[@alert "-exn"]) ~force:true ident op aux
in
s.aux_changeset <- aux_changeset'
let aux_delete w s path name tagval =
if Vyos1x.Util.is_empty path
then raise (Session_error "Path is empty")
else
let aux = s.aux_changeset in
let ident y =
if (y.script_name <> name || y.tag_value <> tagval) then false
else true
in
let op' = VL.find ident aux in
let changeset' =
match op' with
| None ->
update_delete w s [] path
| Some o ->
update_delete w s o.changeset path
in
let op =
{ script_name = name; tag_value = tagval; changeset = changeset' }
in
let aux_changeset' =
(* Vylist.replace does not raise exception when force=true *)
(VL.replace[@alert "-exn"]) ~force:true ident op aux
in
s.aux_changeset <- aux_changeset'
let discard _w s =
{ s with changeset = []; }
let copy w s p1 p2 =
if Vyos1x.Util.is_empty p1
then raise (Session_error "Cannot copy an empty path")
else
if Vyos1x.Util.is_empty p2
then raise (Session_error "Cannot copy to an empty path")
else
let p1_total = s.edit_level @ p1 in
let p2_total = s.edit_level @ p2 in
let ct = get_proposed_config w s in
if not ((VT.exists[@alert "-exn"]) ct p1_total)
then
let p1_str = Vyos1x.Util.string_of_list p1_total in
let out = Printf.sprintf "Configuration path \"%s\" does not exist" p1_str in
raise (Session_error out)
else
let _ = validate w s p2_total in
let ct' = (VT.copy[@alert "-exn"]) ct p1_total p2_total in
let changeset' = get_changeset w s ct ct' in
{ s with changeset = changeset' @ s.changeset }
let rename w s p1 p2 =
if Vyos1x.Util.is_empty p1
then raise (Session_error "Cannot rename an empty path")
else
let p2_last =
match Vyos1x.Util.get_last p2 with
| None -> raise (Session_error "Cannot rename to an empty value")
| Some v -> v
in
if (Vyos1x.Util.drop_last p1) <> (Vyos1x.Util.drop_last p2)
then raise (Session_error "Cannot rename a node: paths are inconsistent")
else
let p1_total = s.edit_level @ p1 in
let p2_total = s.edit_level @ p2 in
let ct = get_proposed_config w s in
if not ((VT.exists[@alert "-exn"]) ct p1_total)
then
let p1_str = Vyos1x.Util.string_of_list p1_total in
let out = Printf.sprintf "Configuration path \"%s\" does not exist" p1_str in
raise (Session_error out)
else
let _ = validate w s p2_total in
let ct' = (VT.rename[@alert "-exn"]) ct p1_total p2_last in
let changeset' = get_changeset w s ct ct' in
{ s with changeset = changeset' @ s.changeset }
let edit_env_str s =
(* To maintain consistency with classic CLI, we return env variable for
PS1 on changes to edit level.
*)
let ps1 =
match s.edit_level with
| [] ->
"[edit]\\n\\u@\\H${VRF:+(vrf:$VRF)}${NETNS:+(ns:$NETNS)}# "
| _ as p ->
Printf.sprintf
"[edit %s]\\n\\u@\\H${VRF:+(vrf:$VRF)}${NETNS:+(ns:$NETNS)}# "
(Vyos1x.Util.string_of_list p)
in
Printf.sprintf "export PS1='%s';" ps1
let set_edit_level w s path =
let current_level = s.edit_level in
let new_level = current_level @ path in
let result = RT.allowed_edit_level w.reference_tree new_level in
match result with
| Ok () ->
let c = get_proposed_config w s in
let s' =
if not ((VT.exists[@alert "-exn"]) c path) then
set w s path
else s
in
let session = { s' with edit_level = new_level; } in
session, (edit_env_str session)
| Error msg ->
raise (Session_error msg)
let set_edit_level_up w s =
let current_level = s.edit_level in
let new_level =
if Vyos1x.Util.is_empty current_level then
current_level
else
let up = Vyos1x.Util.drop_last current_level in
match RT.allowed_edit_level w.reference_tree up with
| Ok () -> up
| Error _ -> (* tag_value *)
Vyos1x.Util.drop_last up
in
let session = { s with edit_level = new_level; }
in session, (edit_env_str session)
let get_edit_level _w s =
Vyos1x.Util.string_of_list s.edit_level
let reset_edit_level _w s =
let session = { s with edit_level = []; }
in session, (edit_env_str session)
let edit_level_root _w s =
Vyos1x.Util.is_empty s.edit_level
let session_changed w s =
(* structural equality test requires consistent ordering, which is
* practised, but may be unreliable; test actual difference
*)
(* alert exn CD.diff_tree:
[Config_diff.Incommensurable] not possible for base root
[Config_diff.Empty_comparison] not possible for empty path
*)
let c = get_proposed_config w s in
let diff = (CD.diff_tree[@alert "-exn"]) [] w.running_config c in
let add_tree = CT.get_subtree diff ["add"] in
let del_tree = CT.get_subtree diff ["del"] in
(del_tree <> CT.default) || (add_tree <> CT.default)
let load w s file cached =
(* alert exn Internal.read_internal:
[Internal.Read_error] caught
*)
let ct =
if cached then
try
Ok ((IC.read_internal[@alert "-exn"]) file)
with Vyos1x.Internal.Read_error e ->
Error e
else
Vyos1x.Config_file.load_config file
in
match ct with
| Error e -> raise (Session_error (Printf.sprintf "Error loading config: %s" e))
| Ok config ->
validate_tree w config;
{ s with changeset = get_changeset w s w.running_config config; }
let merge w s file destructive =
(* alert exn CD.tree_merge:
[Tree_alg.Incompatible_union] not possible for base root
[Tree_alg.Nonexistent_child] not reachable
*)
let ct = Vyos1x.Config_file.load_config file in
match ct with
| Error e -> raise (Session_error (Printf.sprintf "Error loading config: %s" e))
| Ok config ->
let () = validate_tree w config in
let proposed = get_proposed_config w s in
let merged =
(CD.tree_merge[@alert "-exn"]) ~destructive:destructive proposed config
in
{ s with changeset = get_changeset w s w.running_config merged; }
let save w s file =
let ct = w.running_config in
let res = Vyos1x.Config_file.save_config ct file in
match res with
| Error e -> raise (Session_error (Printf.sprintf "Error saving config: %s" e))
| Ok () -> s
let remove_file file =
if Sys.file_exists file then Sys.remove file
let config_unsaved w s file id =
let tmp_save = Printf.sprintf "/tmp/config.running_%s" id in
let res =
try
let _ = save w s tmp_save in
not (Vyos1x.Util.file_compare ~ignore_line_prefix:"//" tmp_save file)
with Session_error _ -> true (* false positive on unlikely error *)
in remove_file tmp_save; res
let reference_path_exists w _s path =
RT.reference_path_exists w.reference_tree path
let get_path_type ?(legacy_format=false) w _s path =
RT.get_path_type_str ~legacy_format w.reference_tree path
let get_completion_env ?(legacy_format=false) w s path =
let op, path =
match path with
| [] -> raise (Session_error "Missing op and path")
| h :: tl -> (h, tl)
in
let config = get_proposed_config w s in
let path_total = s.edit_level @ path in
let res =
Vyos1x.Completion.get_completion_env_str ~legacy_format w.reference_tree config op path_total
in
match res with
| Ok env -> env
| Error e -> raise (Session_error e)
let write_running_cache w =
(* alert exn Internal.write_internal:
[Internal.Write_error] caught
*)
let vc = w.vyconf_config in
try
let () =
(IC.write_internal_atomic[@alert "-exn"])
w.running_config
(FP.concat vc.session_dir vc.running_cache);
in Ok ()
with
Vyos1x.Internal.Write_error msg ->
let msg =
Printf.sprintf "Write error caching running config: %s" msg
in Error msg
let write_session_cache w config =
(* alert exn Internal.write_internal:
[Internal.Write_error] caught
*)
let vc = w.vyconf_config in
try
let () =
(IC.write_internal[@alert "-exn"])
config
(FP.concat vc.session_dir vc.session_cache);
in Ok ()
with
Vyos1x.Internal.Write_error msg ->
let msg =
Printf.sprintf "Write error caching session config: %s" msg
in Error msg
let prepare_commit ?(dry_run=false) w s config id =
let opt_running = write_running_cache w in
let opt_session = write_session_cache w config in
match opt_running, opt_session with
| Ok (), Ok () ->
Ok (CC.make_commit_data
~dry_run:dry_run
w.reference_tree
w.running_config
config
id
s.client_pid
s.client_sudo_user
s.client_user)
| Error msg, Ok () -> Error msg
| Ok (), Error msg -> Error msg
| Error msg1, Error msg2 -> Error (Printf.sprintf "%s\n %s" msg1 msg2)
let post_process_commit w s ((c_data: CC.commit_data), proposed_config) =
let ident n v y =
if (y.script_name <> n || y.tag_value <> v) then false
else true
in
let func (running, proposed) (n_data: CC.node_data) =
match n_data.reply with
| None -> (running, proposed)
| Some reply ->
match reply.success with
| false -> (running, proposed)
| true ->
begin
let post =
VL.find
(ident n_data.script_name n_data.tag_value)
s.aux_changeset
in
match post with
| None -> (running, proposed)
| Some p ->
(apply_changes w p.changeset running, apply_changes w p.changeset proposed)
end
in
List.fold_left func (c_data.config_result, proposed_config) c_data.node_list
let get_config w s id =
(* alert exn Internal.write_internal:
[Internal.Write_error] caught
*)
let at = w.running_config in
let wt = get_proposed_config w s in
let vc = w.vyconf_config in
let running_cache = Printf.sprintf "%s_%s" vc.running_cache id in
let session_cache = Printf.sprintf "%s_%s" vc.session_cache id in
let () =
try
(IC.write_internal[@alert "-exn"]) at (FP.concat vc.session_dir running_cache)
with
Vyos1x.Internal.Write_error msg -> raise (Session_error msg)
in
let () =
try
(IC.write_internal[@alert "-exn"]) wt (FP.concat vc.session_dir session_cache)
with
Vyos1x.Internal.Write_error msg -> raise (Session_error msg)
in id
let cleanup_config w id =
let vc = w.vyconf_config in
let running_cache = Printf.sprintf "%s_%s" vc.running_cache id in
let session_cache = Printf.sprintf "%s_%s" vc.session_cache id in
remove_file (FP.concat vc.session_dir running_cache);
remove_file (FP.concat vc.session_dir session_cache)
let get_value w s path =
(* alert exn VT.exists:
[Vytree.Empty_path] checked
alert exn RT.repath; RT.is_leaf; RT.is_multi; RT.is_valueless:
[Vytree.Empty_path] checked
[Vytree.Nonexistent_path] checked by VT.exists
alert exn CT.get_value:
[Vytree.Empty_path] checked
[Vytree.Nonexistent_path] checked by VT.exists
[Config_tree.Node_has_no_value] checked by RT.is_valueless
*)
if Vyos1x.Util.is_empty path
then raise (Session_error "Config path is empty")
else let c = get_proposed_config w s in
if not ((VT.exists[@alert "-exn"]) c path) then
raise (Session_error ("Config path does not exist"))
else let refpath = (RT.refpath[@alert "-exn"]) w.reference_tree path in
if not ((RT.is_leaf[@alert "-exn"]) w.reference_tree refpath) then
raise (Session_error "Cannot get a value of a non-leaf node")
else if ((RT.is_multi[@alert "-exn"]) w.reference_tree refpath) then
raise (Session_error "This node can have more than one value")
else if ((RT.is_valueless[@alert "-exn"]) w.reference_tree refpath) then
raise (Session_error "This node is valueless")
else (CT.get_value[@alert "-exn"]) c path
let get_values w s path =
(* alert exn VT.exists:
[Vytree.Empty_path] checked
alert exn RT.repath; RT.is_leaf; RT.is_multi; RT.is_valueless; CT.get_values:
[Vytree.Empty_path] checked
[Vytree.Nonexistent_path] checked by VT.exists
*)
if Vyos1x.Util.is_empty path
then raise (Session_error "Config path is empty")
else let c = get_proposed_config w s in
if not ((VT.exists[@alert "-exn"]) c path) then
raise (Session_error ("Config path does not exist"))
else let refpath = (RT.refpath[@alert "-exn"]) w.reference_tree path in
if not ((RT.is_leaf[@alert "-exn"]) w.reference_tree refpath) then
raise (Session_error "Cannot get a value of a non-leaf node")
else if not ((RT.is_multi[@alert "-exn"]) w.reference_tree refpath) then
raise (Session_error "This node can have only one value")
else (CT.get_values[@alert "-exn"]) c path
let list_children w s path =
(* alert exn VT.exists:
[Vytree.Empty_path] checked
alert exn RT.repath; RT.is_leaf; VT.children_of_path:
[Vytree.Empty_path] checked
[Vytree.Nonexistent_path] checked by VT.exists
*)
if Vyos1x.Util.is_empty path
then raise (Session_error "Config path is empty")
else let c = get_proposed_config w s in
if not ((VT.exists[@alert "-exn"]) c path) then
raise (Session_error ("Config path does not exist"))
else let refpath = (RT.refpath[@alert "-exn"]) w.reference_tree path in
if ((RT.is_leaf[@alert "-exn"]) w.reference_tree refpath) then
raise (Session_error "Cannot list children of a leaf node")
else (VT.children_of_path[@alert "-exn"]) c path
let exists w s path =
(* alert exn VT.exists:
[Vytree.Empty_path] checked
*)
if Vyos1x.Util.is_empty path
then raise (Session_error "Path is empty")
else let c = get_proposed_config w s in
(VT.exists[@alert "-exn"]) c path
let show_config w s path =
let path_total = s.edit_level @ path in
let path_show =
match RT.get_path_type w.reference_tree path_total with
| `Leaf_value -> Vyos1x.Util.drop_last path_total
| _ -> path_total
in
let proposed_config = get_proposed_config w s in
if not (Vyos1x.Util.is_empty path_show) &&
not ((VT.exists[@alert "-exn"]) proposed_config path_show)
then raise (Session_error "Path does not exist")
else
let res =
(CD.diff_show[@alert "-exn"])
w.reference_tree
path_show
w.running_config
proposed_config
in res
|