]> git.proxmox.com Git - pve-manager.git/blob - PVE/CLI/pvenode.pm
appliance index: fix precedence in size check for log rotation
[pve-manager.git] / PVE / CLI / pvenode.pm
1 package PVE::CLI::pvenode;
2
3 use strict;
4 use warnings;
5
6 use PVE::API2::ACME;
7 use PVE::API2::ACMEAccount;
8 use PVE::API2::ACMEPlugin;
9 use PVE::API2::Certificates;
10 use PVE::API2::NodeConfig;
11 use PVE::API2::Nodes;
12 use PVE::API2::Tasks;
13
14 use PVE::ACME::Challenge;
15 use PVE::CertHelpers;
16 use PVE::Certificate;
17 use PVE::Exception qw(raise_param_exc raise);
18 use PVE::JSONSchema qw(get_standard_option);
19 use PVE::NodeConfig;
20 use PVE::RPCEnvironment;
21 use PVE::CLIFormatter;
22 use PVE::RESTHandler;
23 use PVE::CLIHandler;
24
25 use Term::ReadLine;
26
27 use base qw(PVE::CLIHandler);
28
29 my $nodename = PVE::INotify::nodename();
30
31 sub setup_environment {
32 PVE::RPCEnvironment->setup_default_cli_env();
33 }
34
35 my $upid_exit = sub {
36 my $upid = shift;
37 my $status = PVE::Tools::upid_read_status($upid);
38 print "Task $status\n";
39 exit(PVE::Tools::upid_status_is_error($status) ? -1 : 0);
40 };
41
42 sub param_mapping {
43 my ($name) = @_;
44
45 my $load_file_and_encode = sub {
46 my ($filename) = @_;
47
48 return PVE::ACME::Challenge->encode_value('string', 'data', PVE::Tools::file_get_contents($filename));
49 };
50
51 my $mapping = {
52 'upload_custom_cert' => [
53 'certificates',
54 'key',
55 ],
56 'add_plugin' => [
57 ['data', $load_file_and_encode, "File with one key-value pair per line, will be base64url encode for storage in plugin config.", 0],
58 ],
59 'update_plugin' => [
60 ['data', $load_file_and_encode, "File with one key-value pair per line, will be base64url encode for storage in plugin config.", 0],
61 ],
62 };
63
64 return $mapping->{$name};
65 }
66
67 __PACKAGE__->register_method({
68 name => 'acme_register',
69 path => 'acme_register',
70 method => 'POST',
71 description => "Register a new ACME account with a compatible CA.",
72 parameters => {
73 additionalProperties => 0,
74 properties => {
75 name => get_standard_option('pve-acme-account-name'),
76 contact => get_standard_option('pve-acme-account-contact'),
77 directory => get_standard_option('pve-acme-directory-url', {
78 optional => 1,
79 }),
80 },
81 },
82 returns => { type => 'null' },
83 code => sub {
84 my ($param) = @_;
85
86 my $custom_directory = 0;
87 if (!$param->{directory}) {
88 my $directories = PVE::API2::ACMEAccount->get_directories({});
89 print "Directory endpoints:\n";
90 my $i = 0;
91 while ($i < @$directories) {
92 print $i, ") ", $directories->[$i]->{name}, " (", $directories->[$i]->{url}, ")\n";
93 $i++;
94 }
95 print $i, ") Custom\n";
96
97 my $term = Term::ReadLine->new('pvenode');
98 my $get_dir_selection = sub {
99 my $selection = $term->readline("Enter selection: ");
100 if ($selection =~ /^(\d+)$/) {
101 $selection = $1;
102 if ($selection == $i) {
103 $param->{directory} = $term->readline("Enter custom URL: ");
104 $custom_directory = 1;
105 return;
106 } elsif ($selection < $i && $selection >= 0) {
107 $param->{directory} = $directories->[$selection]->{url};
108 return;
109 }
110 }
111 print "Invalid selection.\n";
112 };
113
114 my $attempts = 0;
115 while (!$param->{directory}) {
116 die "Aborting.\n" if $attempts > 3;
117 $get_dir_selection->();
118 $attempts++;
119 }
120 }
121 print "\nAttempting to fetch Terms of Service from '$param->{directory}'..\n";
122 my $meta = PVE::API2::ACMEAccount->get_meta({ directory => $param->{directory} });
123 if ($meta->{termsOfService}) {
124 my $tos = $meta->{termsOfService};
125 print "Terms of Service: $tos\n";
126 my $term = Term::ReadLine->new('pvenode');
127 my $agreed = $term->readline('Do you agree to the above terms? [y|N]: ');
128 die "Cannot continue without agreeing to ToS, aborting.\n"
129 if ($agreed !~ /^y$/i);
130
131 $param->{tos_url} = $tos;
132 } else {
133 print "No Terms of Service found, proceeding.\n";
134 }
135
136 my $eab_enabled = $meta->{externalAccountRequired};
137 if (!$eab_enabled && $custom_directory) {
138 my $term = Term::ReadLine->new('pvenode');
139 my $agreed = $term->readline('Do you want to use external account binding? [y|N]: ');
140 $eab_enabled = ($agreed =~ /^y$/i);
141 } elsif ($eab_enabled) {
142 print "The CA requires external account binding.\n";
143 }
144 if ($eab_enabled) {
145 print "You should have received a key id and a key from your CA.\n";
146 my $term = Term::ReadLine->new('pvenode');
147 my $eab_kid = $term->readline('Enter EAB key id: ');
148 my $eab_hmac_key = $term->readline('Enter EAB key: ');
149
150 $param->{'eab-kid'} = $eab_kid;
151 $param->{'eab-hmac-key'} = $eab_hmac_key;
152 }
153
154 print "\nAttempting to register account with '$param->{directory}'..\n";
155
156 $upid_exit->(PVE::API2::ACMEAccount->register_account($param));
157 }});
158
159 my $print_cert_info = sub {
160 my ($schema, $cert, $options) = @_;
161
162 my $order = [qw(filename fingerprint subject issuer notbefore notafter public-key-type public-key-bits san)];
163 PVE::CLIFormatter::print_api_result(
164 $cert, $schema, $order, { %$options, noheader => 1, sort_key => 0 });
165 };
166
167 our $cmddef = {
168 config => {
169 get => [ 'PVE::API2::NodeConfig', 'get_config', [], { node => $nodename }, sub {
170 my ($res) = @_;
171 print PVE::NodeConfig::write_node_config($res);
172 }],
173 set => [ 'PVE::API2::NodeConfig', 'set_options', [], { node => $nodename } ],
174 },
175
176 startall => [ 'PVE::API2::Nodes::Nodeinfo', 'startall', [], { node => $nodename } ],
177 stopall => [ 'PVE::API2::Nodes::Nodeinfo', 'stopall', [], { node => $nodename } ],
178 migrateall => [ 'PVE::API2::Nodes::Nodeinfo', 'migrateall', [ 'target' ], { node => $nodename } ],
179
180 cert => {
181 info => [ 'PVE::API2::Certificates', 'info', [], { node => $nodename }, sub {
182 my ($res, $schema, $options) = @_;
183
184 if (!$options->{'output-format'} || $options->{'output-format'} eq 'text') {
185 for my $cert (sort { $a->{filename} cmp $b->{filename} } @$res) {
186 $print_cert_info->($schema->{items}, $cert, $options);
187 }
188 } else {
189 PVE::CLIFormatter::print_api_result($res, $schema, undef, $options);
190 }
191
192 }, $PVE::RESTHandler::standard_output_options],
193 set => [ 'PVE::API2::Certificates', 'upload_custom_cert', ['certificates', 'key'], { node => $nodename }, sub {
194 my ($res, $schema, $options) = @_;
195 $print_cert_info->($schema, $res, $options);
196 }, $PVE::RESTHandler::standard_output_options],
197 delete => [ 'PVE::API2::Certificates', 'remove_custom_cert', ['restart'], { node => $nodename } ],
198 },
199
200 task => {
201 list => [ 'PVE::API2::Tasks', 'node_tasks', [], { node => $nodename }, sub {
202 my ($data, $schema, $options) = @_;
203 foreach my $task (@$data) {
204 if (!defined($task->{status})) {
205 $task->{status} = 'UNKNOWN';
206 # RUNNING is set by the API call and needs to be checked explicitly
207 } elsif (PVE::Tools::upid_status_is_error($task->{status}) &&
208 $task->{status} ne 'RUNNING')
209 {
210 $task->{status} = 'ERROR';
211 }
212 }
213 PVE::CLIFormatter::print_api_result($data, $schema, ['upid', 'type', 'id', 'user', 'starttime', 'endtime', 'status' ], $options);
214 }, $PVE::RESTHandler::standard_output_options],
215 status => [ 'PVE::API2::Tasks', 'read_task_status', [ 'upid' ], { node => $nodename }, sub {
216 my ($data, $schema, $options) = @_;
217 PVE::CLIFormatter::print_api_result($data, $schema, undef, $options);
218 }, $PVE::RESTHandler::standard_output_options],
219 # set limit to 1000000, so we see the whole log, not only the first 50 lines by default
220 log => [ 'PVE::API2::Tasks', 'read_task_log', [ 'upid' ], { node => $nodename, limit => 1000000 }, sub {
221 my ($data, $resultprops) = @_;
222 foreach my $line (@$data) {
223 print $line->{t} . "\n";
224 }
225 }],
226 },
227
228 acme => {
229 account => {
230 list => [ 'PVE::API2::ACMEAccount', 'account_index', [], {}, sub {
231 my ($res) = @_;
232 for my $acc (@$res) {
233 print "$acc->{name}\n";
234 }
235 }],
236 register => [ __PACKAGE__, 'acme_register', ['name', 'contact'], {}, $upid_exit ],
237 deactivate => [ 'PVE::API2::ACMEAccount', 'deactivate_account', ['name'], {}, $upid_exit ],
238 info => [ 'PVE::API2::ACMEAccount', 'get_account', ['name'], {}, sub {
239 my ($data, $schema, $options) = @_;
240 PVE::CLIFormatter::print_api_result($data, $schema, undef, $options);
241 }, $PVE::RESTHandler::standard_output_options],
242 update => [ 'PVE::API2::ACMEAccount', 'update_account', ['name'], {}, $upid_exit ],
243 },
244 cert => {
245 order => [ 'PVE::API2::ACME', 'new_certificate', [], { node => $nodename }, $upid_exit ],
246 renew => [ 'PVE::API2::ACME', 'renew_certificate', [], { node => $nodename }, $upid_exit ],
247 revoke => [ 'PVE::API2::ACME', 'revoke_certificate', [], { node => $nodename }, $upid_exit ],
248 },
249 plugin => {
250 list => [ 'PVE::API2::ACMEPlugin', 'index', [], {}, sub {
251 my ($data, $schema, $options) = @_;
252 PVE::CLIFormatter::print_api_result($data, $schema, undef, $options);
253 }, $PVE::RESTHandler::standard_output_options ],
254 config => [ 'PVE::API2::ACMEPlugin', 'get_plugin_config', ['id'], {}, sub {
255 my ($data, $schema, $options) = @_;
256 PVE::CLIFormatter::print_api_result($data, $schema, undef, $options);
257 }, $PVE::RESTHandler::standard_output_options ],
258 add => [ 'PVE::API2::ACMEPlugin', 'add_plugin', ['type', 'id'] ],
259 set => [ 'PVE::API2::ACMEPlugin', 'update_plugin', ['id'] ],
260 remove => [ 'PVE::API2::ACMEPlugin', 'delete_plugin', ['id'] ],
261 },
262
263 },
264
265 wakeonlan => [ 'PVE::API2::Nodes::Nodeinfo', 'wakeonlan', [ 'node' ], {}, sub {
266 my ($mac_addr) = @_;
267
268 print "Wake on LAN packet send for '$mac_addr'\n";
269 } ],
270
271 };
272
273 1;