]> git.proxmox.com Git - pve-manager.git/blame - PVE/NodeConfig.pm
update shipped appliance info index
[pve-manager.git] / PVE / NodeConfig.pm
CommitLineData
c4f78bb7
FG
1package PVE::NodeConfig;
2
3use strict;
4use warnings;
5
6use PVE::CertHelpers;
7use PVE::JSONSchema qw(get_standard_option);
8use PVE::Tools qw(file_get_contents file_set_contents lock_file);
cc442d3e
WL
9use PVE::ACME;
10
de9aa678
TL
11use PVE::API2::ACMEPlugin;
12
667198c4
TL
13# register up to 5 domain names per node for now
14my $MAXDOMAINS = 5;
c4f78bb7
FG
15
16my $node_config_lock = '/var/lock/pvenode.lock';
17
18PVE::JSONSchema::register_format('pve-acme-domain', sub {
19 my ($domain, $noerr) = @_;
20
4dab82e3 21 my $label = qr/[a-z0-9][a-z0-9_-]*/i;
c4f78bb7
FG
22
23 return $domain if $domain =~ /^$label(?:\.$label)+$/;
24 return undef if $noerr;
85b275b1 25 die "value '$domain' does not look like a valid domain name!\n";
c4f78bb7
FG
26});
27
d1dd680b 28PVE::JSONSchema::register_format('pve-acme-alias', sub {
b3fbf2ac 29 my ($alias, $noerr) = @_;
d1dd680b
FM
30
31 my $label = qr/[a-z0-9_][a-z0-9_-]*/i;
32
b3fbf2ac 33 return $alias if $alias =~ /^$label(?:\.$label)+$/;
d1dd680b 34 return undef if $noerr;
b3fbf2ac 35 die "value '$alias' does not look like a valid alias name!\n";
d1dd680b
FM
36});
37
c4f78bb7
FG
38sub config_file {
39 my ($node) = @_;
40
41 return "/etc/pve/nodes/${node}/config";
42}
43
44sub load_config {
45 my ($node) = @_;
46
47 my $filename = config_file($node);
48 my $raw = eval { PVE::Tools::file_get_contents($filename); };
49 return {} if !$raw;
50
9556af61 51 return parse_node_config($raw, $filename);
c4f78bb7
FG
52}
53
54sub write_config {
55 my ($node, $conf) = @_;
56
57 my $filename = config_file($node);
58
59 my $raw = write_node_config($conf);
60
61 PVE::Tools::file_set_contents($filename, $raw);
62}
63
64sub lock_config {
3c194d9e
FG
65 my ($node, $realcode, @param) = @_;
66
67 # make sure configuration file is up-to-date
68 my $code = sub {
69 PVE::Cluster::cfs_update();
70 $realcode->(@_);
71 };
c4f78bb7
FG
72
73 my $res = lock_file($node_config_lock, 10, $code, @param);
74
75 die $@ if $@;
76
77 return $res;
78}
79
80my $confdesc = {
81 description => {
82 type => 'string',
e466f896
TL
83 description => "Description for the Node. Shown in the web-interface node notes panel."
84 ." This is saved as comment inside the configuration file.",
85 maxLength => 64 * 1024,
c4f78bb7
FG
86 optional => 1,
87 },
1ff3fef0
TL
88 'startall-onboot-delay' => {
89 description => 'Initial delay in seconds, before starting all the Virtual Guests with on-boot enabled.',
90 type => 'integer',
91 minimum => 0,
92 maximum => 300,
93 default => 0,
94 optional => 1,
95 },
c4f78bb7
FG
96};
97
3f83a033
CE
98my $wakeonlan_desc = {
99 mac => {
100 type => 'string',
101 description => 'MAC address for wake on LAN',
102 format => 'mac-addr',
103 format_description => 'MAC address',
104 default_key => 1,
105 },
869c155c
CE
106 'bind-interface' => {
107 type => 'string',
108 description => 'Bind to this interface when sending wake on LAN packet',
f2be47a4 109 default => 'The interface carrying the default route',
869c155c
CE
110 format => 'pve-iface',
111 format_description => 'bind interface',
112 optional => 1,
113 },
a967ff65
CE
114 'broadcast-address' => {
115 type => 'string',
116 description => 'IPv4 broadcast address to use when sending wake on LAN packet',
f2be47a4 117 default => '255.255.255.255',
a967ff65
CE
118 format => 'ipv4',
119 format_description => 'IPv4 broadcast address',
120 optional => 1,
121 },
3f83a033
CE
122};
123
124$confdesc->{wakeonlan} = {
125 type => 'string',
126 description => 'Node specific wake on LAN settings.',
127 format => $wakeonlan_desc,
128 optional => 1,
129};
130
ab53e139 131my $acme_domain_desc = {
cc442d3e
WL
132 domain => {
133 type => 'string',
134 format => 'pve-acme-domain',
135 format_description => 'domain',
136 description => 'domain for this node\'s ACME certificate',
dcfb9f2c 137 default_key => 1,
cc442d3e
WL
138 },
139 plugin => {
140 type => 'string',
141 format => 'pve-configid',
9d17b945 142 description => 'The ACME plugin ID',
cc442d3e 143 format_description => 'name of the plugin configuration',
9d17b945
FG
144 optional => 1,
145 default => 'standalone',
cc442d3e
WL
146 },
147 alias => {
148 type => 'string',
d1dd680b 149 format => 'pve-acme-alias',
cc442d3e
WL
150 format_description => 'domain',
151 description => 'Alias for the Domain to verify ACME Challenge over DNS',
152 optional => 1,
153 },
154};
cc442d3e 155
c4f78bb7
FG
156my $acmedesc = {
157 account => get_standard_option('pve-acme-account-name'),
158 domains => {
159 type => 'string',
160 format => 'pve-acme-domain-list',
161 format_description => 'domain[;domain;...]',
162 description => 'List of domains for this node\'s ACME certificate',
cc442d3e 163 optional => 1,
c4f78bb7
FG
164 },
165};
c4f78bb7
FG
166
167$confdesc->{acme} = {
168 type => 'string',
169 description => 'Node specific ACME settings.',
170 format => $acmedesc,
171 optional => 1,
172};
173
cc442d3e 174for my $i (0..$MAXDOMAINS) {
ab53e139 175 $confdesc->{"acmedomain$i"} = {
cc442d3e 176 type => 'string',
ab53e139
FG
177 description => 'ACME domain and validation plugin',
178 format => $acme_domain_desc,
cc442d3e
WL
179 optional => 1,
180 };
181};
182
9556af61
WB
183my $conf_schema = {
184 type => 'object',
185 properties => $confdesc,
186};
e79894eb 187
9556af61
WB
188sub parse_node_config : prototype($$) {
189 my ($content, $filename) = @_;
c4f78bb7
FG
190
191 return undef if !defined($content);
9556af61 192 my $digest = Digest::SHA::sha1_hex($content);
c4f78bb7 193
9556af61
WB
194 my $conf = PVE::JSONSchema::parse_config($conf_schema, $filename, $content, 'description');
195 $conf->{digest} = $digest;
c4f78bb7
FG
196
197 return $conf;
198}
199
200sub write_node_config {
201 my ($conf) = @_;
202
203 my $raw = '';
204 # add description as comment to top of file
205 my $descr = $conf->{description} || '';
206 foreach my $cl (split(/\n/, $descr)) {
207 $raw .= '#' . PVE::Tools::encode_text($cl) . "\n";
208 }
209
210 for my $key (sort keys %$conf) {
211 next if ($key eq 'description');
212 next if ($key eq 'digest');
213
214 my $value = $conf->{$key};
215 die "detected invalid newline inside property '$key'\n"
216 if $value =~ m/\n/;
217 $raw .= "$key: $value\n";
218 }
219
220 return $raw;
221}
222
3f83a033
CE
223sub get_wakeonlan_config {
224 my ($node_conf) = @_;
225
226 $node_conf //= {};
227
228 my $res = {};
229 if (defined($node_conf->{wakeonlan})) {
230 $res = eval {
231 PVE::JSONSchema::parse_property_string($wakeonlan_desc, $node_conf->{wakeonlan})
232 };
233 die $@ if $@;
234 }
235
236 return $res;
237}
238
4aa89cc0
FG
239# we always convert domain values to lower case, since DNS entries are not case
240# sensitive and ACME implementations might convert the ordered identifiers
241# to lower case
c30e112e 242sub get_acme_conf {
7ffd1550 243 my ($node_conf, $noerr) = @_;
c4f78bb7 244
7ffd1550 245 $node_conf //= {};
c4f78bb7 246
c30e112e 247 my $res = {};
7ffd1550 248 if (defined($node_conf->{acme})) {
b232807d 249 $res = eval {
7ffd1550 250 PVE::JSONSchema::parse_property_string($acmedesc, $node_conf->{acme})
c30e112e 251 };
7ffd1550 252 if (my $err = $@) {
c30e112e 253 return undef if $noerr;
7ffd1550 254 die $err;
c30e112e 255 }
b232807d 256 my $standalone_domains = delete($res->{domains}) // '';
b7def515 257 $res->{domains} = {};
7ffd1550 258 for my $domain (split(";", $standalone_domains)) {
4aa89cc0
FG
259 $domain = lc($domain);
260 die "duplicate domain '$domain' in ACME config properties\n"
261 if defined($res->{domains}->{$domain});
262
b232807d 263 $res->{domains}->{$domain}->{plugin} = 'standalone';
75afd54a 264 $res->{domains}->{$domain}->{_configkey} = 'acme';
b232807d 265 }
c4f78bb7 266 }
b232807d
FG
267
268 $res->{account} //= 'default';
c30e112e
WL
269
270 for my $index (0..$MAXDOMAINS) {
7ffd1550 271 my $domain_rec = $node_conf->{"acmedomain$index"};
c30e112e
WL
272 next if !defined($domain_rec);
273
b232807d 274 my $parsed = eval {
7ffd1550 275 PVE::JSONSchema::parse_property_string($acme_domain_desc, $domain_rec)
c30e112e 276 };
7ffd1550 277 if (my $err = $@) {
c30e112e 278 return undef if $noerr;
7ffd1550 279 die $err;
c30e112e 280 }
4aa89cc0 281 my $domain = lc(delete $parsed->{domain});
75afd54a 282 if (my $exists = $res->{domains}->{$domain}) {
b232807d 283 return undef if $noerr;
75afd54a
TL
284 die "duplicate domain '$domain' in ACME config properties"
285 ." 'acmedomain$index' and '$exists->{_configkey}'\n";
b232807d 286 }
9d17b945 287 $parsed->{plugin} //= 'standalone';
de9aa678
TL
288
289 my $plugin_id = $parsed->{plugin};
290 if ($plugin_id ne 'standalone') {
291 my $plugins = PVE::API2::ACMEPlugin::load_config();
292 die "plugin '$plugin_id' for domain '$domain' not found!\n"
293 if !$plugins->{ids}->{$plugin_id};
294 }
295
75afd54a 296 $parsed->{_configkey} = "acmedomain$index";
b232807d 297 $res->{domains}->{$domain} = $parsed;
c30e112e
WL
298 }
299
c4f78bb7
FG
300 return $res;
301}
302
75afd54a
TL
303# expects that basic format verification was already done, this is more higher
304# level verification
305sub verify_conf {
306 my ($node_conf) = @_;
307
308 # verify ACME domain uniqueness
309 my $tmp = get_acme_conf($node_conf);
310
311 # TODO: what else?
312
313 return 1; # OK
314}
315
c4f78bb7
FG
316sub get_nodeconfig_schema {
317 return $confdesc;
318}
319
3201;