]> git.proxmox.com Git - pve-manager.git/blob - PVE/NodeConfig.pm
ceph: config hash fallback style improvement
[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 use PVE::API2::ACMEPlugin;
12
13 # register up to 5 domain names per node for now
14 my $MAXDOMAINS = 5;
15
16 my $node_config_lock = '/var/lock/pvenode.lock';
17
18 PVE::JSONSchema::register_format('pve-acme-domain', sub {
19 my ($domain, $noerr) = @_;
20
21 my $label = qr/[a-z0-9][a-z0-9_-]*/i;
22
23 return $domain if $domain =~ /^$label(?:\.$label)+$/;
24 return undef if $noerr;
25 die "value '$domain' does not look like a valid domain name!\n";
26 });
27
28 PVE::JSONSchema::register_format('pve-acme-alias', sub {
29 my ($alias, $noerr) = @_;
30
31 my $label = qr/[a-z0-9_][a-z0-9_-]*/i;
32
33 return $alias if $alias =~ /^$label(?:\.$label)+$/;
34 return undef if $noerr;
35 die "value '$alias' does not look like a valid alias name!\n";
36 });
37
38 sub config_file {
39 my ($node) = @_;
40
41 return "/etc/pve/nodes/${node}/config";
42 }
43
44 sub 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
51 return parse_node_config($raw, $filename);
52 }
53
54 sub 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
64 sub lock_config {
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 };
72
73 my $res = lock_file($node_config_lock, 10, $code, @param);
74
75 die $@ if $@;
76
77 return $res;
78 }
79
80 my $confdesc = {
81 description => {
82 type => 'string',
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,
86 optional => 1,
87 },
88 wakeonlan => {
89 type => 'string',
90 description => 'MAC address for wake on LAN',
91 format => 'mac-addr',
92 optional => 1,
93 },
94 'startall-onboot-delay' => {
95 description => 'Initial delay in seconds, before starting all the Virtual Guests with on-boot enabled.',
96 type => 'integer',
97 minimum => 0,
98 maximum => 300,
99 default => 0,
100 optional => 1,
101 },
102 };
103
104 my $acme_domain_desc = {
105 domain => {
106 type => 'string',
107 format => 'pve-acme-domain',
108 format_description => 'domain',
109 description => 'domain for this node\'s ACME certificate',
110 default_key => 1,
111 },
112 plugin => {
113 type => 'string',
114 format => 'pve-configid',
115 description => 'The ACME plugin ID',
116 format_description => 'name of the plugin configuration',
117 optional => 1,
118 default => 'standalone',
119 },
120 alias => {
121 type => 'string',
122 format => 'pve-acme-alias',
123 format_description => 'domain',
124 description => 'Alias for the Domain to verify ACME Challenge over DNS',
125 optional => 1,
126 },
127 };
128
129 my $acmedesc = {
130 account => get_standard_option('pve-acme-account-name'),
131 domains => {
132 type => 'string',
133 format => 'pve-acme-domain-list',
134 format_description => 'domain[;domain;...]',
135 description => 'List of domains for this node\'s ACME certificate',
136 optional => 1,
137 },
138 };
139
140 $confdesc->{acme} = {
141 type => 'string',
142 description => 'Node specific ACME settings.',
143 format => $acmedesc,
144 optional => 1,
145 };
146
147 for my $i (0..$MAXDOMAINS) {
148 $confdesc->{"acmedomain$i"} = {
149 type => 'string',
150 description => 'ACME domain and validation plugin',
151 format => $acme_domain_desc,
152 optional => 1,
153 };
154 };
155
156 my $conf_schema = {
157 type => 'object',
158 properties => $confdesc,
159 };
160
161 sub parse_node_config : prototype($$) {
162 my ($content, $filename) = @_;
163
164 return undef if !defined($content);
165 my $digest = Digest::SHA::sha1_hex($content);
166
167 my $conf = PVE::JSONSchema::parse_config($conf_schema, $filename, $content, 'description');
168 $conf->{digest} = $digest;
169
170 return $conf;
171 }
172
173 sub write_node_config {
174 my ($conf) = @_;
175
176 my $raw = '';
177 # add description as comment to top of file
178 my $descr = $conf->{description} || '';
179 foreach my $cl (split(/\n/, $descr)) {
180 $raw .= '#' . PVE::Tools::encode_text($cl) . "\n";
181 }
182
183 for my $key (sort keys %$conf) {
184 next if ($key eq 'description');
185 next if ($key eq 'digest');
186
187 my $value = $conf->{$key};
188 die "detected invalid newline inside property '$key'\n"
189 if $value =~ m/\n/;
190 $raw .= "$key: $value\n";
191 }
192
193 return $raw;
194 }
195
196 # we always convert domain values to lower case, since DNS entries are not case
197 # sensitive and ACME implementations might convert the ordered identifiers
198 # to lower case
199 sub get_acme_conf {
200 my ($node_conf, $noerr) = @_;
201
202 $node_conf //= {};
203
204 my $res = {};
205 if (defined($node_conf->{acme})) {
206 $res = eval {
207 PVE::JSONSchema::parse_property_string($acmedesc, $node_conf->{acme})
208 };
209 if (my $err = $@) {
210 return undef if $noerr;
211 die $err;
212 }
213 my $standalone_domains = delete($res->{domains}) // '';
214 $res->{domains} = {};
215 for my $domain (split(";", $standalone_domains)) {
216 $domain = lc($domain);
217 die "duplicate domain '$domain' in ACME config properties\n"
218 if defined($res->{domains}->{$domain});
219
220 $res->{domains}->{$domain}->{plugin} = 'standalone';
221 $res->{domains}->{$domain}->{_configkey} = 'acme';
222 }
223 }
224
225 $res->{account} //= 'default';
226
227 for my $index (0..$MAXDOMAINS) {
228 my $domain_rec = $node_conf->{"acmedomain$index"};
229 next if !defined($domain_rec);
230
231 my $parsed = eval {
232 PVE::JSONSchema::parse_property_string($acme_domain_desc, $domain_rec)
233 };
234 if (my $err = $@) {
235 return undef if $noerr;
236 die $err;
237 }
238 my $domain = lc(delete $parsed->{domain});
239 if (my $exists = $res->{domains}->{$domain}) {
240 return undef if $noerr;
241 die "duplicate domain '$domain' in ACME config properties"
242 ." 'acmedomain$index' and '$exists->{_configkey}'\n";
243 }
244 $parsed->{plugin} //= 'standalone';
245
246 my $plugin_id = $parsed->{plugin};
247 if ($plugin_id ne 'standalone') {
248 my $plugins = PVE::API2::ACMEPlugin::load_config();
249 die "plugin '$plugin_id' for domain '$domain' not found!\n"
250 if !$plugins->{ids}->{$plugin_id};
251 }
252
253 $parsed->{_configkey} = "acmedomain$index";
254 $res->{domains}->{$domain} = $parsed;
255 }
256
257 return $res;
258 }
259
260 # expects that basic format verification was already done, this is more higher
261 # level verification
262 sub verify_conf {
263 my ($node_conf) = @_;
264
265 # verify ACME domain uniqueness
266 my $tmp = get_acme_conf($node_conf);
267
268 # TODO: what else?
269
270 return 1; # OK
271 }
272
273 sub get_nodeconfig_schema {
274 return $confdesc;
275 }
276
277 1;