]> git.proxmox.com Git - pve-firewall.git/blob - src/PVE/API2/Firewall/IPSet.pm
improve concurrent update handling
[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 => 'IPv4orCIDR',
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 load_config {
29 my ($class, $param) = @_;
30
31 die "implement this in subclass";
32
33 #return ($fw_conf, $rules);
34 }
35
36 sub save_rules {
37 my ($class, $param, $fw_conf, $rules) = @_;
38
39 die "implement this in subclass";
40 }
41
42 my $additional_param_hash = {};
43
44 sub additional_parameters {
45 my ($class, $new_value) = @_;
46
47 if (defined($new_value)) {
48 $additional_param_hash->{$class} = $new_value;
49 }
50
51 # return a copy
52 my $copy = {};
53 my $org = $additional_param_hash->{$class} || {};
54 foreach my $p (keys %$org) { $copy->{$p} = $org->{$p}; }
55 return $copy;
56 }
57
58 sub register_get_ipset {
59 my ($class) = @_;
60
61 my $properties = $class->additional_parameters();
62
63 $properties->{name} = $api_properties->{name};
64
65 $class->register_method({
66 name => 'get_ipset',
67 path => '',
68 method => 'GET',
69 description => "List IPSet content",
70 parameters => {
71 additionalProperties => 0,
72 properties => $properties,
73 },
74 returns => {
75 type => 'array',
76 items => {
77 type => "object",
78 properties => {
79 cidr => {
80 type => 'string',
81 },
82 comment => {
83 type => 'string',
84 optional => 1,
85 },
86 nomatch => {
87 type => 'boolean',
88 optional => 1,
89 },
90 digest => get_standard_option('pve-config-digest', { optional => 0} ),
91 },
92 },
93 links => [ { rel => 'child', href => "{cidr}" } ],
94 },
95 code => sub {
96 my ($param) = @_;
97
98 my ($fw_conf, $ipset) = $class->load_config($param);
99
100 return PVE::Firewall::copy_list_with_digest($ipset);
101 }});
102 }
103
104 sub register_create_ip {
105 my ($class) = @_;
106
107 my $properties = $class->additional_parameters();
108
109 $properties->{name} = $api_properties->{name};
110 $properties->{cidr} = $api_properties->{cidr};
111 $properties->{nomatch} = $api_properties->{nomatch};
112 $properties->{comment} = $api_properties->{comment};
113
114 $class->register_method({
115 name => 'create_ip',
116 path => '',
117 method => 'POST',
118 description => "Add IP or Network to IPSet.",
119 protected => 1,
120 parameters => {
121 additionalProperties => 0,
122 properties => $properties,
123 },
124 returns => { type => "null" },
125 code => sub {
126 my ($param) = @_;
127
128 my ($fw_conf, $ipset) = $class->load_config($param);
129
130 my $cidr = $param->{cidr};
131
132 foreach my $entry (@$ipset) {
133 raise_param_exc({ cidr => "address '$cidr' already exists" })
134 if $entry->{cidr} eq $cidr;
135 }
136
137 my $data = { cidr => $cidr };
138 $data->{nomatch} = 1 if $param->{nomatch};
139 $data->{comment} = $param->{comment} if $param->{comment};
140
141 unshift @$ipset, $data;
142
143 $class->save_ipset($param, $fw_conf, $ipset);
144
145 return undef;
146 }});
147 }
148
149 sub register_read_ip {
150 my ($class) = @_;
151
152 my $properties = $class->additional_parameters();
153
154 $properties->{name} = $api_properties->{name};
155 $properties->{cidr} = $api_properties->{cidr};
156
157 $class->register_method({
158 name => 'read_ip',
159 path => '{cidr}',
160 method => 'GET',
161 description => "Read IP or Network settings from IPSet.",
162 protected => 1,
163 parameters => {
164 additionalProperties => 0,
165 properties => $properties,
166 },
167 returns => { type => "object" },
168 code => sub {
169 my ($param) = @_;
170
171 my ($fw_conf, $ipset) = $class->load_config($param);
172
173 my $list = PVE::Firewall::copy_list_with_digest($ipset);
174
175 foreach my $entry (@$list) {
176 if ($entry->{cidr} eq $param->{cidr}) {
177 return $entry;
178 }
179 }
180
181 raise_param_exc({ cidr => "no such IP/Network" });
182 }});
183 }
184
185 sub register_update_ip {
186 my ($class) = @_;
187
188 my $properties = $class->additional_parameters();
189
190 $properties->{name} = $api_properties->{name};
191 $properties->{cidr} = $api_properties->{cidr};
192 $properties->{nomatch} = $api_properties->{nomatch};
193 $properties->{comment} = $api_properties->{comment};
194 $properties->{digest} = get_standard_option('pve-config-digest');
195
196 $class->register_method({
197 name => 'update_ip',
198 path => '{cidr}',
199 method => 'PUT',
200 description => "Update IP or Network settings",
201 protected => 1,
202 parameters => {
203 additionalProperties => 0,
204 properties => $properties,
205 },
206 returns => { type => "null" },
207 code => sub {
208 my ($param) = @_;
209
210 my ($fw_conf, $ipset) = $class->load_config($param);
211
212 my (undef, $digest) = PVE::Firewall::copy_list_with_digest($ipset);
213 PVE::Tools::assert_if_modified($digest, $param->{digest});
214 warn "TEST:$digest:$param->{digest}:\n";
215
216 foreach my $entry (@$ipset) {
217 if($entry->{cidr} eq $param->{cidr}) {
218 $entry->{nomatch} = $param->{nomatch};
219 $entry->{comment} = $param->{comment};
220 $class->save_ipset($param, $fw_conf, $ipset);
221 return;
222 }
223 }
224
225 raise_param_exc({ cidr => "no such IP/Network" });
226 }});
227 }
228
229 sub register_delete_ip {
230 my ($class) = @_;
231
232 my $properties = $class->additional_parameters();
233
234 $properties->{name} = $api_properties->{name};
235 $properties->{cidr} = $api_properties->{cidr};
236 $properties->{digest} = get_standard_option('pve-config-digest');
237
238 $class->register_method({
239 name => 'remove_ip',
240 path => '{cidr}',
241 method => 'DELETE',
242 description => "Remove IP or Network from IPSet.",
243 protected => 1,
244 parameters => {
245 additionalProperties => 0,
246 properties => $properties,
247 },
248 returns => { type => "null" },
249 code => sub {
250 my ($param) = @_;
251
252 my ($fw_conf, $ipset) = $class->load_config($param);
253
254 my (undef, $digest) = PVE::Firewall::copy_list_with_digest($ipset);
255 PVE::Tools::assert_if_modified($digest, $param->{digest});
256
257 my $new = [];
258
259 foreach my $entry (@$ipset) {
260 push @$new, $entry if $entry->{cidr} ne $param->{cidr};
261 }
262
263 $class->save_ipset($param, $fw_conf, $new);
264
265 return undef;
266 }});
267 }
268
269 sub register_handlers {
270 my ($class) = @_;
271
272 $class->register_get_ipset();
273 $class->register_create_ip();
274 $class->register_read_ip();
275 $class->register_update_ip();
276 $class->register_delete_ip();
277 }
278
279 package PVE::API2::Firewall::ClusterIPset;
280
281 use strict;
282 use warnings;
283
284 use base qw(PVE::API2::Firewall::IPSetBase);
285
286 sub load_config {
287 my ($class, $param) = @_;
288
289 my $fw_conf = PVE::Firewall::load_clusterfw_conf();
290 my $ipset = $fw_conf->{ipset}->{$param->{name}};
291 die "no such IPSet '$param->{name}'\n" if !defined($ipset);
292
293 return ($fw_conf, $ipset);
294 }
295
296 sub save_ipset {
297 my ($class, $param, $fw_conf, $ipset) = @_;
298
299 $fw_conf->{ipset}->{$param->{name}} = $ipset;
300 PVE::Firewall::save_clusterfw_conf($fw_conf);
301 }
302
303 __PACKAGE__->register_handlers();
304
305 package PVE::API2::Firewall::BaseIPSetList;
306
307 use strict;
308 use warnings;
309 use PVE::JSONSchema qw(get_standard_option);
310 use PVE::Exception qw(raise_param_exc);
311 use PVE::Firewall;
312
313 use base qw(PVE::RESTHandler);
314
315 my $get_ipset_list = sub {
316 my ($fw_conf) = @_;
317
318 my $res = [];
319 foreach my $name (keys %{$fw_conf->{ipset}}) {
320 my $data = {
321 name => $name,
322 };
323 if (my $comment = $fw_conf->{ipset_comments}->{$name}) {
324 $data->{comment} = $comment;
325 }
326 push @$res, $data;
327 }
328
329 my ($list, $digest) = PVE::Firewall::copy_list_with_digest($res);
330
331 return wantarray ? ($list, $digest) : $list;
332 };
333
334 sub register_index {
335 my ($class) = @_;
336
337 $class->register_method({
338 name => 'ipset_index',
339 path => '',
340 method => 'GET',
341 description => "List IPSets",
342 parameters => {
343 additionalProperties => 0,
344 },
345 returns => {
346 type => 'array',
347 items => {
348 type => "object",
349 properties => {
350 name => get_standard_option('ipset-name'),
351 digest => get_standard_option('pve-config-digest', { optional => 0} ),
352 comment => {
353 type => 'string',
354 optional => 1,
355 }
356 },
357 },
358 links => [ { rel => 'child', href => "{name}" } ],
359 },
360 code => sub {
361 my ($param) = @_;
362
363 my $fw_conf = $class->load_config();
364
365 return &$get_ipset_list($fw_conf);
366 }});
367 }
368
369 sub register_create {
370 my ($class) = @_;
371
372 $class->register_method({
373 name => 'create_ipset',
374 path => '',
375 method => 'POST',
376 description => "Create new IPSet",
377 protected => 1,
378 parameters => {
379 additionalProperties => 0,
380 properties => {
381 name => get_standard_option('ipset-name'),
382 comment => {
383 type => 'string',
384 optional => 1,
385 },
386 rename => get_standard_option('ipset-name', {
387 description => "Rename an existing IPSet. You can set 'rename' to the same value as 'name' to update the 'comment' of an existing IPSet.",
388 optional => 1,
389 }),
390 digest => get_standard_option('pve-config-digest'),
391 }
392 },
393 returns => { type => 'null' },
394 code => sub {
395 my ($param) = @_;
396
397 my $fw_conf = $class->load_config();
398
399 if ($param->{rename}) {
400 my (undef, $digest) = &$get_ipset_list($fw_conf);
401 PVE::Tools::assert_if_modified($digest, $param->{digest});
402
403 raise_param_exc({ name => "IPSet '$param->{rename}' does not exists" })
404 if !$fw_conf->{ipset}->{$param->{rename}};
405
406 my $data = delete $fw_conf->{ipset}->{$param->{rename}};
407 $fw_conf->{ipset}->{$param->{name}} = $data;
408 if (my $comment = delete $fw_conf->{ipset_comments}->{$param->{rename}}) {
409 $fw_conf->{ipset_comments}->{$param->{name}} = $comment;
410 }
411 $fw_conf->{ipset_comments}->{$param->{name}} = $param->{comment} if defined($param->{comment});
412 } else {
413 foreach my $name (keys %{$fw_conf->{ipset}}) {
414 raise_param_exc({ name => "IPSet '$name' already exists" })
415 if $name eq $param->{name};
416 }
417
418 $fw_conf->{ipset}->{$param->{name}} = [];
419 $fw_conf->{ipset_comments}->{$param->{name}} = $param->{comment} if defined($param->{comment});
420 }
421
422 $class->save_config($fw_conf);
423
424 return undef;
425 }});
426 }
427
428 sub register_delete {
429 my ($class) = @_;
430
431 $class->register_method({
432 name => 'delete_ipset',
433 path => '{name}',
434 method => 'DELETE',
435 description => "Delete IPSet",
436 protected => 1,
437 parameters => {
438 additionalProperties => 0,
439 properties => {
440 name => get_standard_option('ipset-name'),
441 digest => get_standard_option('pve-config-digest'),
442 },
443 },
444 returns => { type => 'null' },
445 code => sub {
446 my ($param) = @_;
447
448 my $fw_conf = $class->load_config();
449
450 return undef if !$fw_conf->{ipset}->{$param->{name}};
451
452 my (undef, $digest) = &$get_ipset_list($fw_conf);
453 PVE::Tools::assert_if_modified($digest, $param->{digest});
454
455 die "IPSet '$param->{name}' is not empty\n"
456 if scalar(@{$fw_conf->{ipset}->{$param->{name}}});
457
458 delete $fw_conf->{ipset}->{$param->{name}};
459
460 $class->save_config($fw_conf);
461
462 return undef;
463 }});
464 }
465
466 sub register_handlers {
467 my ($class) = @_;
468
469 $class->register_index();
470 $class->register_create();
471 $class->register_delete();
472 }
473
474 package PVE::API2::Firewall::ClusterIPSetList;
475
476 use strict;
477 use warnings;
478 use PVE::Firewall;
479
480 use base qw(PVE::API2::Firewall::BaseIPSetList);
481
482 sub load_config {
483 my ($class) = @_;
484
485 return PVE::Firewall::load_clusterfw_conf();
486 }
487
488 sub save_config {
489 my ($class, $fw_conf) = @_;
490
491 PVE::Firewall::save_clusterfw_conf($fw_conf);
492 }
493
494 __PACKAGE__->register_handlers();
495
496 __PACKAGE__->register_method ({
497 subclass => "PVE::API2::Firewall::ClusterIPset",
498 path => '{name}',
499 # set fragment delimiter (no subdirs) - we need that, because CIDR address contain a slash '/'
500 fragmentDelimiter => '',
501 });
502
503 1;