]> git.proxmox.com Git - pve-manager.git/blob - PVE/NodeConfig.pm
Extend node config in the acme section.
[pve-manager.git] / PVE / NodeConfig.pm
1 package PVE::NodeConfig;
2
3 use strict;
4 use warnings;
5
6 use PVE::CertHelpers;
7 use PVE::JSONSchema qw(get_standard_option);
8 use PVE::Tools qw(file_get_contents file_set_contents lock_file);
9 use PVE::ACME;
10
11 # register up to 20 domain names
12 my $MAXDOMAINS = 20;
13
14 my $node_config_lock = '/var/lock/pvenode.lock';
15
16 PVE::JSONSchema::register_format('pve-acme-domain', sub {
17 my ($domain, $noerr) = @_;
18
19 my $label = qr/[a-z0-9][a-z0-9_-]*/i;
20
21 return $domain if $domain =~ /^$label(?:\.$label)+$/;
22 return undef if $noerr;
23 die "value does not look like a valid domain name";
24 });
25
26 sub config_file {
27 my ($node) = @_;
28
29 return "/etc/pve/nodes/${node}/config";
30 }
31
32 sub load_config {
33 my ($node) = @_;
34
35 my $filename = config_file($node);
36 my $raw = eval { PVE::Tools::file_get_contents($filename); };
37 return {} if !$raw;
38
39 return parse_node_config($raw);
40 }
41
42 sub write_config {
43 my ($node, $conf) = @_;
44
45 my $filename = config_file($node);
46
47 my $raw = write_node_config($conf);
48
49 PVE::Tools::file_set_contents($filename, $raw);
50 }
51
52 sub lock_config {
53 my ($node, $code, @param) = @_;
54
55 my $res = lock_file($node_config_lock, 10, $code, @param);
56
57 die $@ if $@;
58
59 return $res;
60 }
61
62 my $confdesc = {
63 description => {
64 type => 'string',
65 description => 'Node description/comment.',
66 optional => 1,
67 },
68 wakeonlan => {
69 type => 'string',
70 description => 'MAC address for wake on LAN',
71 format => 'mac-addr',
72 optional => 1,
73 },
74 'startall-onboot-delay' => {
75 description => 'Initial delay in seconds, before starting all the Virtual Guests with on-boot enabled.',
76 type => 'integer',
77 minimum => 0,
78 maximum => 300,
79 default => 0,
80 optional => 1,
81 },
82 };
83
84 my $acme_additional_desc = {
85 domain => {
86 type => 'string',
87 format => 'pve-acme-domain',
88 format_description => 'domain',
89 description => 'domain for this node\'s ACME certificate',
90 },
91 plugin => {
92 type => 'string',
93 format => 'pve-configid',
94 description => 'The plugin ID, default is standalone http',
95 format_description => 'name of the plugin configuration',
96 },
97 alias => {
98 type => 'string',
99 format => 'pve-acme-domain',
100 format_description => 'domain',
101 description => 'Alias for the Domain to verify ACME Challenge over DNS',
102 optional => 1,
103 },
104 };
105 PVE::JSONSchema::register_format('pve-acme-additional-node-conf', $acme_additional_desc);
106
107 my $acmedesc = {
108 account => get_standard_option('pve-acme-account-name'),
109 domains => {
110 type => 'string',
111 format => 'pve-acme-domain-list',
112 format_description => 'domain[;domain;...]',
113 description => 'List of domains for this node\'s ACME certificate',
114 optional => 1,
115 },
116 };
117 PVE::JSONSchema::register_format('pve-acme-node-conf', $acmedesc);
118
119 $confdesc->{acme} = {
120 type => 'string',
121 description => 'Node specific ACME settings.',
122 format => $acmedesc,
123 optional => 1,
124 };
125
126 for my $i (0..$MAXDOMAINS) {
127 $confdesc->{"acme_additional_domain$i"} = {
128 type => 'string',
129 description => 'ACME additional Domain',
130 format => $acme_additional_desc,
131 optional => 1,
132 };
133 };
134
135 sub check_type {
136 my ($key, $value) = @_;
137
138 die "unknown setting '$key'\n" if !$confdesc->{$key};
139
140 my $type = $confdesc->{$key}->{type};
141
142 if (!defined($value)) {
143 die "got undefined value\n";
144 }
145
146 if ($value =~ m/[\n\r]/) {
147 die "property contains a line feed\n";
148 }
149
150 if ($type eq 'boolean') {
151 return 1 if ($value eq '1') || ($value =~ m/^(on|yes|true)$/i);
152 return 0 if ($value eq '0') || ($value =~ m/^(off|no|false)$/i);
153 die "type check ('boolean') failed - got '$value'\n";
154 } elsif ($type eq 'integer') {
155 return int($1) if $value =~ m/^(\d+)$/;
156 die "type check ('integer') failed - got '$value'\n";
157 } elsif ($type eq 'number') {
158 return $value if $value =~ m/^(\d+)(\.\d+)?$/;
159 die "type check ('number') failed - got '$value'\n";
160 } elsif ($type eq 'string') {
161 if (my $fmt = $confdesc->{$key}->{format}) {
162 PVE::JSONSchema::check_format($fmt, $value);
163 return $value;
164 } elsif (my $pattern = $confdesc->{$key}->{pattern}) {
165 if ($value !~ m/^$pattern$/) {
166 die "value does not match the regex pattern\n";
167 }
168 }
169 return $value;
170 } else {
171 die "internal error"
172 }
173 }
174
175 sub parse_node_config {
176 my ($content) = @_;
177
178 return undef if !defined($content);
179
180 my $conf = {
181 digest => Digest::SHA::sha1_hex($content),
182 };
183 my $descr = '';
184
185 my @lines = split(/\n/, $content);
186 foreach my $line (@lines) {
187 if ($line =~ /^\#(.*)\s*$/ || $line =~ /^description:\s*(.*\S)\s*$/) {
188 $descr .= PVE::Tools::decode_text($1) . "\n";
189 next;
190 }
191 if ($line =~ /^([a-z][a-z-_]*\d*):\s*(\S.*)\s*$/) {
192 my $key = $1;
193 my $value = $2;
194 eval { $value = check_type($key, $value); };
195 warn "cannot parse value of '$key' in node config: $@" if $@;
196 $conf->{$key} = $value;
197 } else {
198 warn "cannot parse line '$line' in node config\n";
199 }
200 }
201
202 $conf->{description} = $descr if $descr;
203
204 return $conf;
205 }
206
207 sub write_node_config {
208 my ($conf) = @_;
209
210 my $raw = '';
211 # add description as comment to top of file
212 my $descr = $conf->{description} || '';
213 foreach my $cl (split(/\n/, $descr)) {
214 $raw .= '#' . PVE::Tools::encode_text($cl) . "\n";
215 }
216
217 for my $key (sort keys %$conf) {
218 next if ($key eq 'description');
219 next if ($key eq 'digest');
220
221 my $value = $conf->{$key};
222 die "detected invalid newline inside property '$key'\n"
223 if $value =~ m/\n/;
224 $raw .= "$key: $value\n";
225 }
226
227 return $raw;
228 }
229
230 sub parse_acme {
231 my ($data, $noerr) = @_;
232
233 $data //= '';
234
235 my $res = eval { PVE::JSONSchema::parse_property_string($acmedesc, $data); };
236 if ($@) {
237 return undef if $noerr;
238 die $@;
239 }
240
241 $res->{domains} = [ PVE::Tools::split_list($res->{domains}) ];
242
243 return $res;
244 }
245
246 sub print_acme {
247 my ($acme) = @_;
248
249 $acme->{domains} = join(';', $acme->{domains}) if $acme->{domains};
250 return PVE::JSONSchema::print_property_string($acme, $acmedesc);
251 }
252
253 sub get_nodeconfig_schema {
254
255 return $confdesc;
256 }
257
258 1;