]>
Commit | Line | Data |
---|---|---|
5000937e WB |
1 | package PMG::API2::ACMEPlugin; |
2 | ||
3 | use strict; | |
4 | use warnings; | |
5 | ||
6 | use Storable qw(dclone); | |
7 | ||
8 | use PVE::ACME::Challenge; | |
9 | use PVE::ACME::DNSChallenge; | |
10 | use PVE::ACME::StandAlone; | |
11 | use PVE::INotify; | |
12 | use PVE::JSONSchema qw(get_standard_option); | |
13 | use PVE::Tools qw(extract_param); | |
14 | ||
15 | use base qw(PVE::RESTHandler); | |
16 | ||
17 | my $inotify_file_id = 'pmg-acme-plugins-config.conf'; | |
abbb3940 | 18 | my $config_filename = '/etc/pmg/acme/plugins.conf'; |
5000937e WB |
19 | my $lockfile = "/var/lock/pmg-acme-plugins-config.lck"; |
20 | ||
21 | PVE::ACME::DNSChallenge->register(); | |
22 | PVE::ACME::StandAlone->register(); | |
23 | PVE::ACME::Challenge->init(); | |
24 | ||
25 | PVE::JSONSchema::register_standard_option('pmg-acme-pluginid', { | |
26 | type => 'string', | |
27 | format => 'pve-configid', | |
28 | description => 'Unique identifier for ACME plugin instance.', | |
29 | }); | |
30 | ||
31 | sub read_pmg_acme_challenge_config { | |
32 | my ($filename, $fh) = @_; | |
181ef3f1 | 33 | my $raw = defined($fh) ? do { local $/ = undef; <$fh> } : ''; |
5000937e WB |
34 | return PVE::ACME::Challenge->parse_config($filename, $raw); |
35 | } | |
36 | ||
37 | sub write_pmg_acme_challenge_config { | |
38 | my ($filename, $fh, $cfg) = @_; | |
39 | my $raw = PVE::ACME::Challenge->write_config($filename, $cfg); | |
40 | PVE::Tools::safe_print($filename, $fh, $raw); | |
41 | } | |
42 | ||
f68516f0 TL |
43 | PVE::INotify::register_file( |
44 | $inotify_file_id, $config_filename, | |
45 | \&read_pmg_acme_challenge_config, | |
46 | \&write_pmg_acme_challenge_config, | |
47 | undef, | |
48 | always_call_parser => 1, | |
49 | ); | |
5000937e WB |
50 | |
51 | sub lock_config { | |
52 | my ($code) = @_; | |
53 | my $p = PVE::Tools::lock_file($lockfile, undef, $code); | |
54 | die $@ if $@; | |
55 | return $p; | |
56 | } | |
57 | ||
58 | sub load_config { | |
59 | # auto-adds the standalone plugin if no config is there for backwards | |
60 | # compatibility, so ALWAYS call the cfs registered parser | |
61 | return PVE::INotify::read_file($inotify_file_id); | |
62 | } | |
63 | ||
64 | sub write_config { | |
65 | my ($self) = @_; | |
66 | return PVE::INotify::write_file($inotify_file_id, $self); | |
67 | } | |
68 | ||
69 | my $plugin_type_enum = PVE::ACME::Challenge->lookup_types(); | |
70 | ||
71 | my $modify_cfg_for_api = sub { | |
72 | my ($cfg, $pluginid) = @_; | |
73 | ||
74 | die "ACME plugin '$pluginid' not defined\n" if !defined($cfg->{ids}->{$pluginid}); | |
75 | ||
76 | my $plugin_cfg = dclone($cfg->{ids}->{$pluginid}); | |
77 | $plugin_cfg->{plugin} = $pluginid; | |
78 | $plugin_cfg->{digest} = $cfg->{digest}; | |
79 | ||
80 | return $plugin_cfg; | |
81 | }; | |
82 | ||
83 | __PACKAGE__->register_method ({ | |
84 | name => 'index', | |
85 | path => '', | |
86 | method => 'GET', | |
87 | permissions => { check => [ 'admin' ] }, | |
88 | description => "ACME plugin index.", | |
89 | protected => 1, | |
90 | parameters => { | |
91 | additionalProperties => 0, | |
92 | properties => { | |
93 | type => { | |
94 | description => "Only list ACME plugins of a specific type", | |
95 | type => 'string', | |
96 | enum => $plugin_type_enum, | |
97 | optional => 1, | |
98 | }, | |
99 | }, | |
100 | }, | |
101 | returns => { | |
102 | type => 'array', | |
103 | items => { | |
104 | type => "object", | |
105 | properties => { | |
106 | plugin => get_standard_option('pmg-acme-pluginid'), | |
107 | }, | |
108 | }, | |
109 | links => [ { rel => 'child', href => "{plugin}" } ], | |
110 | }, | |
111 | code => sub { | |
112 | my ($param) = @_; | |
113 | ||
114 | my $cfg = load_config(); | |
115 | ||
116 | my $res = []; | |
117 | foreach my $pluginid (keys %{$cfg->{ids}}) { | |
118 | my $plugin_cfg = $modify_cfg_for_api->($cfg, $pluginid); | |
119 | next if $param->{type} && $param->{type} ne $plugin_cfg->{type}; | |
120 | push @$res, $plugin_cfg; | |
121 | } | |
122 | ||
123 | return $res; | |
124 | } | |
125 | }); | |
126 | ||
127 | __PACKAGE__->register_method({ | |
128 | name => 'get_plugin_config', | |
129 | path => '{id}', | |
130 | method => 'GET', | |
131 | description => "Get ACME plugin configuration.", | |
132 | permissions => { check => [ 'admin' ] }, | |
133 | protected => 1, | |
134 | parameters => { | |
135 | additionalProperties => 0, | |
136 | properties => { | |
137 | id => get_standard_option('pmg-acme-pluginid'), | |
138 | }, | |
139 | }, | |
140 | returns => { | |
141 | type => 'object', | |
142 | }, | |
143 | code => sub { | |
144 | my ($param) = @_; | |
145 | ||
146 | my $cfg = load_config(); | |
147 | return $modify_cfg_for_api->($cfg, $param->{id}); | |
148 | } | |
149 | }); | |
150 | ||
151 | __PACKAGE__->register_method({ | |
152 | name => 'add_plugin', | |
153 | path => '', | |
154 | method => 'POST', | |
155 | description => "Add ACME plugin configuration.", | |
156 | permissions => { check => [ 'admin' ] }, | |
157 | protected => 1, | |
158 | parameters => PVE::ACME::Challenge->createSchema(), | |
159 | returns => { | |
160 | type => "null" | |
161 | }, | |
162 | code => sub { | |
163 | my ($param) = @_; | |
164 | ||
165 | my $id = extract_param($param, 'id'); | |
166 | my $type = extract_param($param, 'type'); | |
167 | ||
168 | lock_config(sub { | |
169 | my $cfg = load_config(); | |
170 | die "ACME plugin ID '$id' already exists\n" if defined($cfg->{ids}->{$id}); | |
171 | ||
172 | my $plugin = PVE::ACME::Challenge->lookup($type); | |
173 | my $opts = $plugin->check_config($id, $param, 1, 1); | |
174 | ||
175 | $cfg->{ids}->{$id} = $opts; | |
176 | $cfg->{ids}->{$id}->{type} = $type; | |
177 | ||
178 | write_config($cfg); | |
179 | }); | |
180 | die "$@" if $@; | |
181 | ||
182 | return undef; | |
183 | } | |
184 | }); | |
185 | ||
186 | __PACKAGE__->register_method({ | |
187 | name => 'update_plugin', | |
188 | path => '{id}', | |
189 | method => 'PUT', | |
190 | description => "Update ACME plugin configuration.", | |
191 | permissions => { check => [ 'admin' ] }, | |
192 | protected => 1, | |
193 | parameters => PVE::ACME::Challenge->updateSchema(), | |
194 | returns => { | |
195 | type => "null" | |
196 | }, | |
197 | code => sub { | |
198 | my ($param) = @_; | |
199 | ||
200 | my $id = extract_param($param, 'id'); | |
201 | my $delete = extract_param($param, 'delete'); | |
202 | my $digest = extract_param($param, 'digest'); | |
203 | ||
204 | lock_config(sub { | |
205 | my $cfg = load_config(); | |
206 | PVE::Tools::assert_if_modified($cfg->{digest}, $digest); | |
207 | my $plugin_cfg = $cfg->{ids}->{$id}; | |
208 | die "ACME plugin ID '$id' does not exist\n" if !$plugin_cfg; | |
209 | ||
210 | my $type = $plugin_cfg->{type}; | |
211 | my $plugin = PVE::ACME::Challenge->lookup($type); | |
212 | ||
213 | if (defined($delete)) { | |
214 | my $schema = $plugin->private(); | |
215 | my $options = $schema->{options}->{$type}; | |
216 | for my $k (PVE::Tools::split_list($delete)) { | |
217 | my $d = $options->{$k} || die "no such option '$k'\n"; | |
218 | die "unable to delete required option '$k'\n" if !$d->{optional}; | |
219 | ||
220 | delete $cfg->{ids}->{$id}->{$k}; | |
221 | } | |
222 | } | |
223 | ||
224 | my $opts = $plugin->check_config($id, $param, 0, 1); | |
225 | for my $k (keys %$opts) { | |
226 | $plugin_cfg->{$k} = $opts->{$k}; | |
227 | } | |
228 | ||
229 | write_config($cfg); | |
230 | }); | |
231 | die "$@" if $@; | |
232 | ||
233 | return undef; | |
234 | } | |
235 | }); | |
236 | ||
237 | __PACKAGE__->register_method({ | |
238 | name => 'delete_plugin', | |
239 | path => '{id}', | |
240 | method => 'DELETE', | |
241 | description => "Delete ACME plugin configuration.", | |
242 | permissions => { check => [ 'admin' ] }, | |
243 | protected => 1, | |
244 | parameters => { | |
245 | additionalProperties => 0, | |
246 | properties => { | |
247 | id => get_standard_option('pmg-acme-pluginid'), | |
248 | }, | |
249 | }, | |
250 | returns => { | |
251 | type => "null" | |
252 | }, | |
253 | code => sub { | |
254 | my ($param) = @_; | |
255 | ||
256 | my $id = extract_param($param, 'id'); | |
257 | ||
258 | lock_config(sub { | |
259 | my $cfg = load_config(); | |
260 | ||
261 | delete $cfg->{ids}->{$id}; | |
262 | ||
263 | write_config($cfg); | |
264 | }); | |
265 | die "$@" if $@; | |
266 | ||
267 | return undef; | |
268 | } | |
269 | }); | |
270 | ||
271 | 1; |