]> git.proxmox.com Git - pmg-api.git/blob - src/PMG/API2/ACMEPlugin.pm
tree-wide: make slurp mode as local as possible for future-proofing
[pmg-api.git] / src / PMG / API2 / ACMEPlugin.pm
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';
18 my $config_filename = '/etc/pmg/acme/plugins.conf';
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) = @_;
33 my $raw = defined($fh) ? do { local $/ = undef; <$fh> } : '';
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
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 );
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;