]> git.proxmox.com Git - pve-firewall.git/blob - src/PVE/API2/Firewall/IPSet.pm
api: add locking helpers
[pve-firewall.git] / src / PVE / API2 / Firewall / IPSet.pm
1 package PVE::API2::Firewall::IPSetBase;
2
3 use strict;
4 use warnings;
5 use PVE::Exception qw(raise raise_param_exc);
6 use PVE::JSONSchema qw(get_standard_option);
7
8 use PVE::Firewall;
9
10 use base qw(PVE::RESTHandler);
11
12 my $api_properties = {
13 cidr => {
14 description => "Network/IP specification in CIDR format.",
15 type => 'string', format => 'IPorCIDRorAlias',
16 },
17 name => get_standard_option('ipset-name'),
18 comment => {
19 type => 'string',
20 optional => 1,
21 },
22 nomatch => {
23 type => 'boolean',
24 optional => 1,
25 },
26 };
27
28 sub lock_config {
29 my ($class, $param, $code) = @_;
30
31 die "implement this in subclass";
32 }
33
34 sub load_config {
35 my ($class, $param) = @_;
36
37 die "implement this in subclass";
38
39 #return ($cluster_conf, $fw_conf, $ipset);
40 }
41
42 sub save_config {
43 my ($class, $param, $fw_conf) = @_;
44
45 die "implement this in subclass";
46 }
47
48 sub rule_env {
49 my ($class, $param) = @_;
50
51 die "implement this in subclass";
52 }
53
54 sub save_ipset {
55 my ($class, $param, $fw_conf, $ipset) = @_;
56
57 if (!defined($ipset)) {
58 delete $fw_conf->{ipset}->{$param->{name}};
59 } else {
60 $fw_conf->{ipset}->{$param->{name}} = $ipset;
61 }
62
63 $class->save_config($param, $fw_conf);
64 }
65
66 my $additional_param_hash = {};
67
68 sub additional_parameters {
69 my ($class, $new_value) = @_;
70
71 if (defined($new_value)) {
72 $additional_param_hash->{$class} = $new_value;
73 }
74
75 # return a copy
76 my $copy = {};
77 my $org = $additional_param_hash->{$class} || {};
78 foreach my $p (keys %$org) { $copy->{$p} = $org->{$p}; }
79 return $copy;
80 }
81
82 sub register_get_ipset {
83 my ($class) = @_;
84
85 my $properties = $class->additional_parameters();
86
87 $properties->{name} = $api_properties->{name};
88
89 $class->register_method({
90 name => 'get_ipset',
91 path => '',
92 method => 'GET',
93 description => "List IPSet content",
94 permissions => PVE::Firewall::rules_audit_permissions($class->rule_env()),
95 parameters => {
96 additionalProperties => 0,
97 properties => $properties,
98 },
99 returns => {
100 type => 'array',
101 items => {
102 type => "object",
103 properties => {
104 cidr => {
105 type => 'string',
106 },
107 comment => {
108 type => 'string',
109 optional => 1,
110 },
111 nomatch => {
112 type => 'boolean',
113 optional => 1,
114 },
115 digest => get_standard_option('pve-config-digest', { optional => 0} ),
116 },
117 },
118 links => [ { rel => 'child', href => "{cidr}" } ],
119 },
120 code => sub {
121 my ($param) = @_;
122
123 my ($cluster_conf, $fw_conf, $ipset) = $class->load_config($param);
124
125 return PVE::Firewall::copy_list_with_digest($ipset);
126 }});
127 }
128
129 sub register_delete_ipset {
130 my ($class) = @_;
131
132 my $properties = $class->additional_parameters();
133
134 $properties->{name} = get_standard_option('ipset-name');
135
136 $class->register_method({
137 name => 'delete_ipset',
138 path => '',
139 method => 'DELETE',
140 description => "Delete IPSet",
141 protected => 1,
142 permissions => PVE::Firewall::rules_modify_permissions($class->rule_env()),
143 parameters => {
144 additionalProperties => 0,
145 properties => $properties,
146 },
147 returns => { type => 'null' },
148 code => sub {
149 my ($param) = @_;
150
151 my ($cluster_conf, $fw_conf, $ipset) = $class->load_config($param);
152
153 die "IPSet '$param->{name}' is not empty\n"
154 if scalar(@$ipset);
155
156 $class->save_ipset($param, $fw_conf, undef);
157
158 return undef;
159 }});
160 }
161
162 sub register_create_ip {
163 my ($class) = @_;
164
165 my $properties = $class->additional_parameters();
166
167 $properties->{name} = $api_properties->{name};
168 $properties->{cidr} = $api_properties->{cidr};
169 $properties->{nomatch} = $api_properties->{nomatch};
170 $properties->{comment} = $api_properties->{comment};
171
172 $class->register_method({
173 name => 'create_ip',
174 path => '',
175 method => 'POST',
176 description => "Add IP or Network to IPSet.",
177 protected => 1,
178 permissions => PVE::Firewall::rules_modify_permissions($class->rule_env()),
179 parameters => {
180 additionalProperties => 0,
181 properties => $properties,
182 },
183 returns => { type => "null" },
184 code => sub {
185 my ($param) = @_;
186
187 my ($cluster_conf, $fw_conf, $ipset) = $class->load_config($param);
188
189 my $cidr = $param->{cidr};
190
191 foreach my $entry (@$ipset) {
192 raise_param_exc({ cidr => "address '$cidr' already exists" })
193 if $entry->{cidr} eq $cidr;
194 }
195
196 raise_param_exc({ cidr => "a zero prefix is not allowed in ipset entries" })
197 if $cidr =~ m!/0+$!;
198
199 # make sure alias exists (if $cidr is an alias)
200 PVE::Firewall::resolve_alias($cluster_conf, $fw_conf, $cidr)
201 if $cidr =~ m/^${PVE::Firewall::ip_alias_pattern}$/;
202
203 my $data = { cidr => $cidr };
204
205 $data->{nomatch} = 1 if $param->{nomatch};
206 $data->{comment} = $param->{comment} if $param->{comment};
207
208 unshift @$ipset, $data;
209
210 $class->save_ipset($param, $fw_conf, $ipset);
211
212 return undef;
213 }});
214 }
215
216 sub register_read_ip {
217 my ($class) = @_;
218
219 my $properties = $class->additional_parameters();
220
221 $properties->{name} = $api_properties->{name};
222 $properties->{cidr} = $api_properties->{cidr};
223
224 $class->register_method({
225 name => 'read_ip',
226 path => '{cidr}',
227 method => 'GET',
228 description => "Read IP or Network settings from IPSet.",
229 permissions => PVE::Firewall::rules_audit_permissions($class->rule_env()),
230 protected => 1,
231 parameters => {
232 additionalProperties => 0,
233 properties => $properties,
234 },
235 returns => { type => "object" },
236 code => sub {
237 my ($param) = @_;
238
239 my ($cluster_conf, $fw_conf, $ipset) = $class->load_config($param);
240
241 my $list = PVE::Firewall::copy_list_with_digest($ipset);
242
243 foreach my $entry (@$list) {
244 if ($entry->{cidr} eq $param->{cidr}) {
245 return $entry;
246 }
247 }
248
249 raise_param_exc({ cidr => "no such IP/Network" });
250 }});
251 }
252
253 sub register_update_ip {
254 my ($class) = @_;
255
256 my $properties = $class->additional_parameters();
257
258 $properties->{name} = $api_properties->{name};
259 $properties->{cidr} = $api_properties->{cidr};
260 $properties->{nomatch} = $api_properties->{nomatch};
261 $properties->{comment} = $api_properties->{comment};
262 $properties->{digest} = get_standard_option('pve-config-digest');
263
264 $class->register_method({
265 name => 'update_ip',
266 path => '{cidr}',
267 method => 'PUT',
268 description => "Update IP or Network settings",
269 protected => 1,
270 permissions => PVE::Firewall::rules_modify_permissions($class->rule_env()),
271 parameters => {
272 additionalProperties => 0,
273 properties => $properties,
274 },
275 returns => { type => "null" },
276 code => sub {
277 my ($param) = @_;
278
279 my ($cluster_conf, $fw_conf, $ipset) = $class->load_config($param);
280
281 my (undef, $digest) = PVE::Firewall::copy_list_with_digest($ipset);
282 PVE::Tools::assert_if_modified($digest, $param->{digest});
283
284 foreach my $entry (@$ipset) {
285 if($entry->{cidr} eq $param->{cidr}) {
286 $entry->{nomatch} = $param->{nomatch};
287 $entry->{comment} = $param->{comment};
288 $class->save_ipset($param, $fw_conf, $ipset);
289 return;
290 }
291 }
292
293 raise_param_exc({ cidr => "no such IP/Network" });
294 }});
295 }
296
297 sub register_delete_ip {
298 my ($class) = @_;
299
300 my $properties = $class->additional_parameters();
301
302 $properties->{name} = $api_properties->{name};
303 $properties->{cidr} = $api_properties->{cidr};
304 $properties->{digest} = get_standard_option('pve-config-digest');
305
306 $class->register_method({
307 name => 'remove_ip',
308 path => '{cidr}',
309 method => 'DELETE',
310 description => "Remove IP or Network from IPSet.",
311 protected => 1,
312 permissions => PVE::Firewall::rules_modify_permissions($class->rule_env()),
313 parameters => {
314 additionalProperties => 0,
315 properties => $properties,
316 },
317 returns => { type => "null" },
318 code => sub {
319 my ($param) = @_;
320
321 my ($cluster_conf, $fw_conf, $ipset) = $class->load_config($param);
322
323 my (undef, $digest) = PVE::Firewall::copy_list_with_digest($ipset);
324 PVE::Tools::assert_if_modified($digest, $param->{digest});
325
326 my $new = [];
327
328 foreach my $entry (@$ipset) {
329 push @$new, $entry if $entry->{cidr} ne $param->{cidr};
330 }
331
332 $class->save_ipset($param, $fw_conf, $new);
333
334 return undef;
335 }});
336 }
337
338 sub register_handlers {
339 my ($class) = @_;
340
341 $class->register_delete_ipset();
342 $class->register_get_ipset();
343 $class->register_create_ip();
344 $class->register_read_ip();
345 $class->register_update_ip();
346 $class->register_delete_ip();
347 }
348
349 package PVE::API2::Firewall::ClusterIPset;
350
351 use strict;
352 use warnings;
353
354 use base qw(PVE::API2::Firewall::IPSetBase);
355
356 sub rule_env {
357 my ($class, $param) = @_;
358
359 return 'cluster';
360 }
361
362 sub lock_config {
363 my ($class, $param, $code) = @_;
364
365 PVE::Firewall::lock_clusterfw_conf(10, $code, $param);
366 }
367
368 sub load_config {
369 my ($class, $param) = @_;
370
371 my $fw_conf = PVE::Firewall::load_clusterfw_conf();
372 my $ipset = $fw_conf->{ipset}->{$param->{name}};
373 die "no such IPSet '$param->{name}'\n" if !defined($ipset);
374
375 return (undef, $fw_conf, $ipset);
376 }
377
378 sub save_config {
379 my ($class, $param, $fw_conf) = @_;
380
381 PVE::Firewall::save_clusterfw_conf($fw_conf);
382 }
383
384 __PACKAGE__->register_handlers();
385
386 package PVE::API2::Firewall::VMIPset;
387
388 use strict;
389 use warnings;
390 use PVE::JSONSchema qw(get_standard_option);
391
392 use base qw(PVE::API2::Firewall::IPSetBase);
393
394 sub rule_env {
395 my ($class, $param) = @_;
396
397 return 'vm';
398 }
399
400 __PACKAGE__->additional_parameters({
401 node => get_standard_option('pve-node'),
402 vmid => get_standard_option('pve-vmid'),
403 });
404
405 sub lock_config {
406 my ($class, $param, $code) = @_;
407
408 PVE::Firewall::lock_vmfw_conf($param->{vmid}, 10, $code, $param);
409 }
410
411 sub load_config {
412 my ($class, $param) = @_;
413
414 my $cluster_conf = PVE::Firewall::load_clusterfw_conf();
415 my $fw_conf = PVE::Firewall::load_vmfw_conf($cluster_conf, 'vm', $param->{vmid});
416 my $ipset = $fw_conf->{ipset}->{$param->{name}};
417 die "no such IPSet '$param->{name}'\n" if !defined($ipset);
418
419 return ($cluster_conf, $fw_conf, $ipset);
420 }
421
422 sub save_config {
423 my ($class, $param, $fw_conf) = @_;
424
425 PVE::Firewall::save_vmfw_conf($param->{vmid}, $fw_conf);
426 }
427
428 __PACKAGE__->register_handlers();
429
430 package PVE::API2::Firewall::CTIPset;
431
432 use strict;
433 use warnings;
434 use PVE::JSONSchema qw(get_standard_option);
435
436 use base qw(PVE::API2::Firewall::IPSetBase);
437
438 sub rule_env {
439 my ($class, $param) = @_;
440
441 return 'ct';
442 }
443
444 __PACKAGE__->additional_parameters({
445 node => get_standard_option('pve-node'),
446 vmid => get_standard_option('pve-vmid'),
447 });
448
449 sub lock_config {
450 my ($class, $param, $code) = @_;
451
452 PVE::Firewall::lock_vmfw_conf($param->{vmid}, 10, $code, $param);
453 }
454
455 sub load_config {
456 my ($class, $param) = @_;
457
458 my $cluster_conf = PVE::Firewall::load_clusterfw_conf();
459 my $fw_conf = PVE::Firewall::load_vmfw_conf($cluster_conf, 'ct', $param->{vmid});
460 my $ipset = $fw_conf->{ipset}->{$param->{name}};
461 die "no such IPSet '$param->{name}'\n" if !defined($ipset);
462
463 return ($cluster_conf, $fw_conf, $ipset);
464 }
465
466 sub save_config {
467 my ($class, $param, $fw_conf) = @_;
468
469 PVE::Firewall::save_vmfw_conf($param->{vmid}, $fw_conf);
470 }
471
472 __PACKAGE__->register_handlers();
473
474 package PVE::API2::Firewall::BaseIPSetList;
475
476 use strict;
477 use warnings;
478 use PVE::JSONSchema qw(get_standard_option);
479 use PVE::Exception qw(raise_param_exc);
480 use PVE::Firewall;
481
482 use base qw(PVE::RESTHandler);
483
484 sub lock_config {
485 my ($class, $param, $code) = @_;
486
487 die "implement this in subclass";
488 }
489
490 sub load_config {
491 my ($class, $param) = @_;
492
493 die "implement this in subclass";
494
495 #return ($cluster_conf, $fw_conf);
496 }
497
498 sub save_config {
499 my ($class, $param, $fw_conf) = @_;
500
501 die "implement this in subclass";
502 }
503
504 sub rule_env {
505 my ($class, $param) = @_;
506
507 die "implement this in subclass";
508 }
509
510 my $additional_param_hash_list = {};
511
512 sub additional_parameters {
513 my ($class, $new_value) = @_;
514
515 if (defined($new_value)) {
516 $additional_param_hash_list->{$class} = $new_value;
517 }
518
519 # return a copy
520 my $copy = {};
521 my $org = $additional_param_hash_list->{$class} || {};
522 foreach my $p (keys %$org) { $copy->{$p} = $org->{$p}; }
523 return $copy;
524 }
525
526 my $get_ipset_list = sub {
527 my ($fw_conf) = @_;
528
529 my $res = [];
530 foreach my $name (sort keys %{$fw_conf->{ipset}}) {
531 my $data = {
532 name => $name,
533 };
534 if (my $comment = $fw_conf->{ipset_comments}->{$name}) {
535 $data->{comment} = $comment;
536 }
537 push @$res, $data;
538 }
539
540 my ($list, $digest) = PVE::Firewall::copy_list_with_digest($res);
541
542 return wantarray ? ($list, $digest) : $list;
543 };
544
545 sub register_index {
546 my ($class) = @_;
547
548 my $properties = $class->additional_parameters();
549
550 $class->register_method({
551 name => 'ipset_index',
552 path => '',
553 method => 'GET',
554 description => "List IPSets",
555 permissions => PVE::Firewall::rules_audit_permissions($class->rule_env()),
556 parameters => {
557 additionalProperties => 0,
558 properties => $properties,
559 },
560 returns => {
561 type => 'array',
562 items => {
563 type => "object",
564 properties => {
565 name => get_standard_option('ipset-name'),
566 digest => get_standard_option('pve-config-digest', { optional => 0} ),
567 comment => {
568 type => 'string',
569 optional => 1,
570 }
571 },
572 },
573 links => [ { rel => 'child', href => "{name}" } ],
574 },
575 code => sub {
576 my ($param) = @_;
577
578 my ($cluster_conf, $fw_conf) = $class->load_config($param);
579
580 return &$get_ipset_list($fw_conf);
581 }});
582 }
583
584 sub register_create {
585 my ($class) = @_;
586
587 my $properties = $class->additional_parameters();
588
589 $properties->{name} = get_standard_option('ipset-name');
590
591 $properties->{comment} = { type => 'string', optional => 1 };
592
593 $properties->{digest} = get_standard_option('pve-config-digest');
594
595 $properties->{rename} = get_standard_option('ipset-name', {
596 description => "Rename an existing IPSet. You can set 'rename' to the same value as 'name' to update the 'comment' of an existing IPSet.",
597 optional => 1 });
598
599 $class->register_method({
600 name => 'create_ipset',
601 path => '',
602 method => 'POST',
603 description => "Create new IPSet",
604 protected => 1,
605 permissions => PVE::Firewall::rules_modify_permissions($class->rule_env()),
606 parameters => {
607 additionalProperties => 0,
608 properties => $properties,
609 },
610 returns => { type => 'null' },
611 code => sub {
612 my ($param) = @_;
613
614 my ($cluster_conf, $fw_conf) = $class->load_config($param);
615
616 if ($param->{rename}) {
617 my (undef, $digest) = &$get_ipset_list($fw_conf);
618 PVE::Tools::assert_if_modified($digest, $param->{digest});
619
620 raise_param_exc({ name => "IPSet '$param->{rename}' does not exist" })
621 if !$fw_conf->{ipset}->{$param->{rename}};
622
623 # prevent overwriting existing ipset
624 raise_param_exc({ name => "IPSet '$param->{name}' does already exist"})
625 if $fw_conf->{ipset}->{$param->{name}} &&
626 $param->{name} ne $param->{rename};
627
628 my $data = delete $fw_conf->{ipset}->{$param->{rename}};
629 $fw_conf->{ipset}->{$param->{name}} = $data;
630 if (my $comment = delete $fw_conf->{ipset_comments}->{$param->{rename}}) {
631 $fw_conf->{ipset_comments}->{$param->{name}} = $comment;
632 }
633 $fw_conf->{ipset_comments}->{$param->{name}} = $param->{comment} if defined($param->{comment});
634 } else {
635 foreach my $name (keys %{$fw_conf->{ipset}}) {
636 raise_param_exc({ name => "IPSet '$name' already exists" })
637 if $name eq $param->{name};
638 }
639
640 $fw_conf->{ipset}->{$param->{name}} = [];
641 $fw_conf->{ipset_comments}->{$param->{name}} = $param->{comment} if defined($param->{comment});
642 }
643
644 $class->save_config($param, $fw_conf);
645
646 return undef;
647 }});
648 }
649
650 sub register_handlers {
651 my ($class) = @_;
652
653 $class->register_index();
654 $class->register_create();
655 }
656
657 package PVE::API2::Firewall::ClusterIPSetList;
658
659 use strict;
660 use warnings;
661 use PVE::Firewall;
662
663 use base qw(PVE::API2::Firewall::BaseIPSetList);
664
665 sub rule_env {
666 my ($class, $param) = @_;
667
668 return 'cluster';
669 }
670
671 sub lock_config {
672 my ($class, $param, $code) = @_;
673
674 PVE::Firewall::lock_clusterfw_conf(10, $code, $param);
675 }
676
677 sub load_config {
678 my ($class, $param) = @_;
679
680 my $cluster_conf = PVE::Firewall::load_clusterfw_conf();
681 return (undef, $cluster_conf);
682 }
683
684 sub save_config {
685 my ($class, $param, $fw_conf) = @_;
686
687 PVE::Firewall::save_clusterfw_conf($fw_conf);
688 }
689
690 __PACKAGE__->register_handlers();
691
692 __PACKAGE__->register_method ({
693 subclass => "PVE::API2::Firewall::ClusterIPset",
694 path => '{name}',
695 # set fragment delimiter (no subdirs) - we need that, because CIDR address contain a slash '/'
696 fragmentDelimiter => '',
697 });
698
699 package PVE::API2::Firewall::VMIPSetList;
700
701 use strict;
702 use warnings;
703 use PVE::JSONSchema qw(get_standard_option);
704 use PVE::Firewall;
705
706 use base qw(PVE::API2::Firewall::BaseIPSetList);
707
708 __PACKAGE__->additional_parameters({
709 node => get_standard_option('pve-node'),
710 vmid => get_standard_option('pve-vmid'),
711 });
712
713 sub rule_env {
714 my ($class, $param) = @_;
715
716 return 'vm';
717 }
718
719 sub lock_config {
720 my ($class, $param, $code) = @_;
721
722 PVE::Firewall::lock_vmfw_conf($param->{vmid}, 10, $code, $param);
723 }
724
725 sub load_config {
726 my ($class, $param) = @_;
727
728 my $cluster_conf = PVE::Firewall::load_clusterfw_conf();
729 my $fw_conf = PVE::Firewall::load_vmfw_conf($cluster_conf, 'vm', $param->{vmid});
730 return ($cluster_conf, $fw_conf);
731 }
732
733 sub save_config {
734 my ($class, $param, $fw_conf) = @_;
735
736 PVE::Firewall::save_vmfw_conf($param->{vmid}, $fw_conf);
737 }
738
739 __PACKAGE__->register_handlers();
740
741 __PACKAGE__->register_method ({
742 subclass => "PVE::API2::Firewall::VMIPset",
743 path => '{name}',
744 # set fragment delimiter (no subdirs) - we need that, because CIDR address contain a slash '/'
745 fragmentDelimiter => '',
746 });
747
748 package PVE::API2::Firewall::CTIPSetList;
749
750 use strict;
751 use warnings;
752 use PVE::JSONSchema qw(get_standard_option);
753 use PVE::Firewall;
754
755 use base qw(PVE::API2::Firewall::BaseIPSetList);
756
757 __PACKAGE__->additional_parameters({
758 node => get_standard_option('pve-node'),
759 vmid => get_standard_option('pve-vmid'),
760 });
761
762 sub rule_env {
763 my ($class, $param) = @_;
764
765 return 'ct';
766 }
767
768 sub lock_config {
769 my ($class, $param, $code) = @_;
770
771 PVE::Firewall::lock_vmfw_conf($param->{vmid}, 10, $code, $param);
772 }
773
774 sub load_config {
775 my ($class, $param) = @_;
776
777 my $cluster_conf = PVE::Firewall::load_clusterfw_conf();
778 my $fw_conf = PVE::Firewall::load_vmfw_conf($cluster_conf, 'ct', $param->{vmid});
779 return ($cluster_conf, $fw_conf);
780 }
781
782 sub save_config {
783 my ($class, $param, $fw_conf) = @_;
784
785 PVE::Firewall::save_vmfw_conf($param->{vmid}, $fw_conf);
786 }
787
788 __PACKAGE__->register_handlers();
789
790 __PACKAGE__->register_method ({
791 subclass => "PVE::API2::Firewall::CTIPset",
792 path => '{name}',
793 # set fragment delimiter (no subdirs) - we need that, because CIDR address contain a slash '/'
794 fragmentDelimiter => '',
795 });
796
797 1;