]>
Commit | Line | Data |
---|---|---|
c4f78bb7 FG |
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); | |
cc442d3e WL |
9 | use PVE::ACME; |
10 | ||
11 | # register up to 20 domain names | |
12 | my $MAXDOMAINS = 20; | |
c4f78bb7 FG |
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 | ||
4dab82e3 | 19 | my $label = qr/[a-z0-9][a-z0-9_-]*/i; |
c4f78bb7 FG |
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 | }, | |
b3d84542 CE |
68 | wakeonlan => { |
69 | type => 'string', | |
70 | description => 'MAC address for wake on LAN', | |
0f615ea9 | 71 | format => 'mac-addr', |
b3d84542 CE |
72 | optional => 1, |
73 | }, | |
1ff3fef0 TL |
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 | }, | |
c4f78bb7 FG |
82 | }; |
83 | ||
ab53e139 | 84 | my $acme_domain_desc = { |
cc442d3e WL |
85 | domain => { |
86 | type => 'string', | |
87 | format => 'pve-acme-domain', | |
88 | format_description => 'domain', | |
89 | description => 'domain for this node\'s ACME certificate', | |
dcfb9f2c | 90 | default_key => 1, |
cc442d3e WL |
91 | }, |
92 | plugin => { | |
93 | type => 'string', | |
94 | format => 'pve-configid', | |
9d17b945 | 95 | description => 'The ACME plugin ID', |
cc442d3e | 96 | format_description => 'name of the plugin configuration', |
9d17b945 FG |
97 | optional => 1, |
98 | default => 'standalone', | |
cc442d3e WL |
99 | }, |
100 | alias => { | |
101 | type => 'string', | |
102 | format => 'pve-acme-domain', | |
103 | format_description => 'domain', | |
104 | description => 'Alias for the Domain to verify ACME Challenge over DNS', | |
105 | optional => 1, | |
106 | }, | |
107 | }; | |
cc442d3e | 108 | |
c4f78bb7 FG |
109 | my $acmedesc = { |
110 | account => get_standard_option('pve-acme-account-name'), | |
111 | domains => { | |
112 | type => 'string', | |
113 | format => 'pve-acme-domain-list', | |
114 | format_description => 'domain[;domain;...]', | |
115 | description => 'List of domains for this node\'s ACME certificate', | |
cc442d3e | 116 | optional => 1, |
c4f78bb7 FG |
117 | }, |
118 | }; | |
c4f78bb7 FG |
119 | |
120 | $confdesc->{acme} = { | |
121 | type => 'string', | |
122 | description => 'Node specific ACME settings.', | |
123 | format => $acmedesc, | |
124 | optional => 1, | |
125 | }; | |
126 | ||
cc442d3e | 127 | for my $i (0..$MAXDOMAINS) { |
ab53e139 | 128 | $confdesc->{"acmedomain$i"} = { |
cc442d3e | 129 | type => 'string', |
ab53e139 FG |
130 | description => 'ACME domain and validation plugin', |
131 | format => $acme_domain_desc, | |
cc442d3e WL |
132 | optional => 1, |
133 | }; | |
134 | }; | |
135 | ||
c4f78bb7 FG |
136 | sub check_type { |
137 | my ($key, $value) = @_; | |
138 | ||
139 | die "unknown setting '$key'\n" if !$confdesc->{$key}; | |
140 | ||
141 | my $type = $confdesc->{$key}->{type}; | |
142 | ||
143 | if (!defined($value)) { | |
144 | die "got undefined value\n"; | |
145 | } | |
146 | ||
147 | if ($value =~ m/[\n\r]/) { | |
148 | die "property contains a line feed\n"; | |
149 | } | |
150 | ||
151 | if ($type eq 'boolean') { | |
152 | return 1 if ($value eq '1') || ($value =~ m/^(on|yes|true)$/i); | |
153 | return 0 if ($value eq '0') || ($value =~ m/^(off|no|false)$/i); | |
154 | die "type check ('boolean') failed - got '$value'\n"; | |
155 | } elsif ($type eq 'integer') { | |
156 | return int($1) if $value =~ m/^(\d+)$/; | |
157 | die "type check ('integer') failed - got '$value'\n"; | |
158 | } elsif ($type eq 'number') { | |
159 | return $value if $value =~ m/^(\d+)(\.\d+)?$/; | |
160 | die "type check ('number') failed - got '$value'\n"; | |
161 | } elsif ($type eq 'string') { | |
162 | if (my $fmt = $confdesc->{$key}->{format}) { | |
163 | PVE::JSONSchema::check_format($fmt, $value); | |
164 | return $value; | |
e79894eb TL |
165 | } elsif (my $pattern = $confdesc->{$key}->{pattern}) { |
166 | if ($value !~ m/^$pattern$/) { | |
167 | die "value does not match the regex pattern\n"; | |
168 | } | |
c4f78bb7 FG |
169 | } |
170 | return $value; | |
171 | } else { | |
172 | die "internal error" | |
173 | } | |
174 | } | |
e79894eb | 175 | |
c4f78bb7 FG |
176 | sub parse_node_config { |
177 | my ($content) = @_; | |
178 | ||
179 | return undef if !defined($content); | |
180 | ||
181 | my $conf = { | |
182 | digest => Digest::SHA::sha1_hex($content), | |
183 | }; | |
184 | my $descr = ''; | |
185 | ||
186 | my @lines = split(/\n/, $content); | |
187 | foreach my $line (@lines) { | |
e61f1629 | 188 | if ($line =~ /^\#(.*)\s*$/ || $line =~ /^description:\s*(.*\S)\s*$/) { |
c4f78bb7 FG |
189 | $descr .= PVE::Tools::decode_text($1) . "\n"; |
190 | next; | |
191 | } | |
cc95254e | 192 | if ($line =~ /^([a-z][a-z-_]*\d*):\s*(\S.*)\s*$/) { |
c4f78bb7 FG |
193 | my $key = $1; |
194 | my $value = $2; | |
195 | eval { $value = check_type($key, $value); }; | |
196 | warn "cannot parse value of '$key' in node config: $@" if $@; | |
197 | $conf->{$key} = $value; | |
198 | } else { | |
199 | warn "cannot parse line '$line' in node config\n"; | |
200 | } | |
201 | } | |
202 | ||
203 | $conf->{description} = $descr if $descr; | |
204 | ||
205 | return $conf; | |
206 | } | |
207 | ||
208 | sub write_node_config { | |
209 | my ($conf) = @_; | |
210 | ||
211 | my $raw = ''; | |
212 | # add description as comment to top of file | |
213 | my $descr = $conf->{description} || ''; | |
214 | foreach my $cl (split(/\n/, $descr)) { | |
215 | $raw .= '#' . PVE::Tools::encode_text($cl) . "\n"; | |
216 | } | |
217 | ||
218 | for my $key (sort keys %$conf) { | |
219 | next if ($key eq 'description'); | |
220 | next if ($key eq 'digest'); | |
221 | ||
222 | my $value = $conf->{$key}; | |
223 | die "detected invalid newline inside property '$key'\n" | |
224 | if $value =~ m/\n/; | |
225 | $raw .= "$key: $value\n"; | |
226 | } | |
227 | ||
228 | return $raw; | |
229 | } | |
230 | ||
c30e112e | 231 | sub get_acme_conf { |
c4f78bb7 FG |
232 | my ($data, $noerr) = @_; |
233 | ||
234 | $data //= ''; | |
235 | ||
c30e112e WL |
236 | my $res = {}; |
237 | ||
238 | if (defined($data->{acme})) { | |
b232807d | 239 | $res = eval { |
c30e112e WL |
240 | PVE::JSONSchema::parse_property_string($acmedesc, $data->{acme}); |
241 | }; | |
242 | if ($@) { | |
243 | return undef if $noerr; | |
244 | die $@; | |
245 | } | |
b232807d FG |
246 | my $standalone_domains = delete($res->{domains}) // ''; |
247 | foreach my $domain (split(";", $standalone_domains)) { | |
248 | $res->{domains}->{$domain}->{plugin} = 'standalone'; | |
249 | } | |
c4f78bb7 | 250 | } |
b232807d FG |
251 | |
252 | $res->{account} //= 'default'; | |
c30e112e WL |
253 | |
254 | for my $index (0..$MAXDOMAINS) { | |
ab53e139 | 255 | my $domain_rec = $data->{"acmedomain$index"}; |
c30e112e WL |
256 | next if !defined($domain_rec); |
257 | ||
b232807d | 258 | my $parsed = eval { |
c30e112e | 259 | PVE::JSONSchema::parse_property_string( |
ab53e139 | 260 | $acme_domain_desc, |
c30e112e WL |
261 | $domain_rec); |
262 | }; | |
263 | if ($@) { | |
264 | return undef if $noerr; | |
265 | die $@; | |
266 | } | |
b232807d FG |
267 | my $domain = delete $parsed->{domain}; |
268 | if ($res->{domains}->{$domain}) { | |
269 | return undef if $noerr; | |
270 | die "duplicate ACME config for domain '$domain'\n"; | |
271 | } | |
9d17b945 | 272 | $parsed->{plugin} //= 'standalone'; |
b232807d | 273 | $res->{domains}->{$domain} = $parsed; |
c30e112e WL |
274 | } |
275 | ||
c4f78bb7 FG |
276 | return $res; |
277 | } | |
278 | ||
c4f78bb7 | 279 | sub get_nodeconfig_schema { |
cc442d3e | 280 | |
c4f78bb7 FG |
281 | return $confdesc; |
282 | } | |
283 | ||
284 | 1; |