]>
Commit | Line | Data |
---|---|---|
adf285bf DM |
1 | package PVE::APIClient::Commands::lxc; |
2 | ||
3 | use strict; | |
4 | use warnings; | |
678b245c | 5 | use Errno qw(EINTR EAGAIN); |
aa570b38 | 6 | use JSON; |
1824383b DM |
7 | use URI::Escape; |
8 | use IO::Select; | |
9 | use IO::Socket::SSL; | |
10 | use MIME::Base64; | |
11 | use Digest::SHA; | |
2779eec9 | 12 | use HTTP::Response; |
adf285bf | 13 | |
c9138c03 DM |
14 | use PVE::APIClient::JSONSchema qw(get_standard_option); |
15 | use PVE::APIClient::CLIHandler; | |
16 | use PVE::APIClient::PTY; | |
adf285bf | 17 | |
b7db4587 RJ |
18 | use PVE::APIClient::Helpers; |
19 | ||
c9138c03 | 20 | use base qw(PVE::APIClient::CLIHandler); |
3454a319 | 21 | use PVE::APIClient::Config; |
84ba32e5 | 22 | |
1824383b | 23 | my $CRLF = "\x0D\x0A"; |
962d470a | 24 | my $max_payload_size = 128*1024; |
1824383b DM |
25 | |
26 | my $build_web_socket_request = sub { | |
6bb43016 | 27 | my ($host, $path, $ticket, $termproxy) = @_; |
1824383b DM |
28 | |
29 | my $key = ''; | |
30 | $key .= chr(int(rand(256))) for 1 .. 16; | |
31 | my $enckey = MIME::Base64::encode_base64($key, ''); | |
32 | ||
33 | my $encticket = uri_escape($ticket); | |
34 | my $cookie = "PVEAuthCookie=$encticket; path=/; secure;"; | |
35 | ||
36 | $path .= "?port=$termproxy->{port}" . | |
37 | "&vncticket=" . uri_escape($termproxy->{ticket}); | |
38 | ||
39 | my $request = "GET $path HTTP/1.1$CRLF" | |
40 | . "Upgrade: WebSocket$CRLF" | |
41 | . "Connection: Upgrade$CRLF" | |
6bb43016 | 42 | . "Host: $host$CRLF" |
1824383b DM |
43 | . "Sec-WebSocket-Key: $enckey$CRLF" |
44 | . "Sec-WebSocket-Version: 13$CRLF" | |
45 | . "Sec-WebSocket-Protocol: binary$CRLF" | |
46 | . "Cookie: $cookie$CRLF" | |
47 | . "$CRLF"; | |
48 | ||
49 | return ($request, $enckey); | |
50 | }; | |
51 | ||
1824383b DM |
52 | my $create_websockt_frame = sub { |
53 | my ($payload) = @_; | |
54 | ||
55 | my $string = "\x82"; # binary frame | |
56 | my $payload_len = length($payload); | |
57 | if ($payload_len <= 125) { | |
58 | $string .= pack 'C', $payload_len | 128; | |
59 | } elsif ($payload_len <= 0xffff) { | |
60 | $string .= pack 'C', 126 | 128; | |
61 | $string .= pack 'n', $payload_len; | |
62 | } else { | |
63 | $string .= pack 'C', 127 | 128; | |
64 | $string .= pack 'Q>', $payload_len; | |
65 | } | |
66 | ||
67 | $string .= pack 'N', 0; # we simply use 0 as mask | |
68 | $string .= $payload; | |
69 | ||
70 | return $string; | |
71 | }; | |
72 | ||
1824383b | 73 | my $parse_web_socket_frame = sub { |
84d4c3e2 | 74 | my ($wsbuf_ref) = @_; |
1824383b | 75 | |
1824383b DM |
76 | my $payload; |
77 | my $req_close = 0; | |
78 | ||
1334102e | 79 | while (my $len = length($$wsbuf_ref)) { |
1824383b DM |
80 | last if $len < 2; |
81 | ||
1334102e | 82 | my $hdr = unpack('C', substr($$wsbuf_ref, 0, 1)); |
1824383b DM |
83 | my $opcode = $hdr & 0b00001111; |
84 | my $fin = $hdr & 0b10000000; | |
85 | ||
86 | die "received fragmented websocket frame\n" if !$fin; | |
87 | ||
88 | my $rsv = $hdr & 0b01110000; | |
89 | die "received websocket frame with RSV flags\n" if $rsv; | |
90 | ||
1334102e | 91 | my $payload_len = unpack 'C', substr($$wsbuf_ref, 1, 1); |
1824383b DM |
92 | |
93 | my $masked = $payload_len & 0b10000000; | |
84d4c3e2 | 94 | die "received masked websocket frame from server\n" if $masked; |
1824383b DM |
95 | |
96 | my $offset = 2; | |
97 | $payload_len = $payload_len & 0b01111111; | |
98 | if ($payload_len == 126) { | |
99 | last if $len < 4; | |
1334102e | 100 | $payload_len = unpack('n', substr($$wsbuf_ref, $offset, 2)); |
1824383b DM |
101 | $offset += 2; |
102 | } elsif ($payload_len == 127) { | |
103 | last if $len < 10; | |
1334102e | 104 | $payload_len = unpack('Q>', substr($$wsbuf_ref, $offset, 8)); |
1824383b DM |
105 | $offset += 8; |
106 | } | |
107 | ||
108 | die "received too large websocket frame (len = $payload_len)\n" | |
109 | if ($payload_len > $max_payload_size) || ($payload_len < 0); | |
110 | ||
1824383b DM |
111 | last if $len < ($offset + $payload_len); |
112 | ||
1334102e | 113 | my $data = substr($$wsbuf_ref, 0, $offset + $payload_len, ''); # now consume data |
1824383b DM |
114 | |
115 | my $frame_data = substr($data, $offset, $payload_len); | |
116 | ||
1824383b DM |
117 | $payload = '' if !defined($payload); |
118 | $payload .= $frame_data; | |
119 | ||
120 | if ($opcode == 1 || $opcode == 2) { | |
121 | # continue | |
122 | } elsif ($opcode == 8) { | |
123 | my $statuscode = unpack ("n", $frame_data); | |
124 | $req_close = 1; | |
125 | } else { | |
126 | die "received unhandled websocket opcode $opcode\n"; | |
127 | } | |
128 | } | |
129 | ||
130 | return ($payload, $req_close); | |
131 | }; | |
132 | ||
678b245c DM |
133 | my $full_write = sub { |
134 | my ($fh, $data) = @_; | |
135 | ||
136 | my $len = length($data); | |
137 | my $todo = $len; | |
138 | my $offset = 0; | |
139 | while(1) { | |
140 | my $nr = syswrite($fh, $data, $todo, $offset); | |
141 | if (!defined($nr)) { | |
142 | next if $! == EINTR || $! == EAGAIN; | |
143 | die "console write error - $!\n" | |
144 | } | |
145 | $offset += $nr; | |
146 | $todo -= $nr; | |
147 | last if $todo <= 0; | |
148 | } | |
149 | ||
150 | return $len; | |
151 | }; | |
152 | ||
83a7ab4c WB |
153 | # Takes an escape character with an optional '^' prefix and returns an escape |
154 | # character code. | |
155 | my $escapekey_to_char = sub { | |
156 | my ($def) = @_; | |
157 | if ($def =~ /^\^?([a-zA-Z])$/) { | |
158 | return 1 + ord(lc($1)) - ord('a'); | |
159 | } | |
160 | die "bad escape key definition: $def\n"; | |
161 | }; | |
162 | ||
adf285bf DM |
163 | __PACKAGE__->register_method ({ |
164 | name => 'enter', | |
165 | path => 'enter', | |
166 | method => 'POST', | |
167 | description => "Enter container console.", | |
168 | parameters => { | |
169 | additionalProperties => 0, | |
170 | properties => { | |
3454a319 | 171 | remote => get_standard_option('pveclient-remote-name'), |
7acede20 | 172 | vmid => get_standard_option('pve-vmid') |
adf285bf DM |
173 | }, |
174 | }, | |
175 | returns => { type => 'null'}, | |
176 | code => sub { | |
177 | my ($param) = @_; | |
178 | ||
69aa81b3 DM |
179 | my $config = PVE::APIClient::Config->load(); |
180 | my $conn = PVE::APIClient::Config->remote_conn($config, $param->{remote}); | |
1824383b | 181 | |
83a7ab4c WB |
182 | # FIXME: This should come from $config |
183 | my $escape_char = $escapekey_to_char->('a'); | |
184 | ||
1824383b DM |
185 | # Get the real node from the resources endpoint |
186 | my $resource_list = $conn->get("api2/json/cluster/resources", { type => 'vm'}); | |
187 | my ($resource) = grep { $_->{type} eq "lxc" && $_->{vmid} eq $param->{vmid}} @$resource_list; | |
188 | ||
189 | die "container '$param->{vmid}' does not exist\n" | |
190 | if !(defined($resource) && defined($resource->{node})); | |
191 | ||
192 | my $node = $resource->{node}; | |
84ba32e5 DM |
193 | |
194 | my $api_path = "api2/json/nodes/$node/lxc/$param->{vmid}"; | |
195 | ||
1824383b | 196 | my $termproxy = $conn->post("${api_path}/termproxy", {}); |
84ba32e5 | 197 | |
1824383b DM |
198 | my $web_socket = IO::Socket::SSL->new( |
199 | PeerHost => $conn->{host}, | |
200 | PeerPort => $conn->{port}, | |
201 | SSL_verify_mode => SSL_VERIFY_NONE, # fixme: ??? | |
202 | timeout => 30) || | |
203 | die "failed to connect: $!\n"; | |
204 | ||
205 | # WebSocket Handshake | |
206 | ||
207 | my ($request, $wskey) = $build_web_socket_request->( | |
6bb43016 | 208 | $conn->{host}, "/$api_path/vncwebsocket", $conn->{ticket}, $termproxy); |
1824383b | 209 | |
678b245c | 210 | $full_write->($web_socket, $request); |
1824383b DM |
211 | |
212 | my $wsbuf = ''; | |
213 | ||
214 | my $wb_socket_read_available_bytes = sub { | |
215 | my $nr = $web_socket->sysread($wsbuf, $max_payload_size, length($wsbuf)); | |
bf705a1e DM |
216 | if (!defined($nr) && !($! == EINTR || $! == EAGAIN)) { |
217 | die "web socket read error - $!\n"; | |
218 | } | |
1824383b DM |
219 | return $nr; |
220 | }; | |
221 | ||
222 | my $raw_response = ''; | |
223 | ||
224 | while(1) { | |
225 | my $nr = $wb_socket_read_available_bytes->(); | |
226 | if ($wsbuf =~ s/^(.*?)$CRLF$CRLF//s) { | |
227 | $raw_response = $1; | |
228 | last; | |
229 | } | |
230 | last if !$nr; | |
231 | }; | |
232 | ||
233 | # Note: we keep any remaining data in $wsbuf | |
234 | ||
235 | my $response = HTTP::Response->parse($raw_response); | |
236 | ||
237 | # Note: Digest::SHA::sha1_base64 has wrong padding | |
238 | my $wsaccept = Digest::SHA::sha1_base64("${wskey}258EAFA5-E914-47DA-95CA-C5AB0DC85B11") . "="; | |
239 | ||
240 | die "got invalid websocket reponse: $raw_response\n" | |
241 | if !(($response->code == 101) && | |
6bb43016 RJ |
242 | (lc $response->header('connection') eq 'upgrade') && |
243 | (lc $response->header('upgrade') eq 'websocket') && | |
1824383b DM |
244 | ($response->header('sec-websocket-protocol') eq 'binary') && |
245 | ($response->header('sec-websocket-accept') eq $wsaccept)); | |
246 | ||
247 | # send auth again... | |
248 | my $frame = $create_websockt_frame->($termproxy->{user} . ":" . $termproxy->{ticket} . "\n"); | |
678b245c | 249 | $full_write->($web_socket, $frame); |
1824383b | 250 | |
95b87dd3 | 251 | # Send resize command |
b2123baa | 252 | my ($columns, $rows) = PVE::APIClient::PTY::tcgetsize(*STDIN); |
95b87dd3 | 253 | $frame = $create_websockt_frame->("1:$columns:$rows:"); |
678b245c | 254 | $full_write->($web_socket, $frame); |
95b87dd3 RJ |
255 | |
256 | # Set STDIN to "raw -echo" mode | |
b2123baa | 257 | my $old_termios = PVE::APIClient::PTY::tcgetattr(*STDIN); |
95b87dd3 | 258 | my $raw_termios = {%$old_termios}; |
95b87dd3 | 259 | |
1ec5788c DM |
260 | my $read_select = IO::Select->new; |
261 | my $write_select = IO::Select->new; | |
262 | ||
263 | my $output_buffer = ''; # write buffer for STDOUT | |
264 | my $websock_buffer = ''; # write buffer for $web_socket | |
1824383b | 265 | |
e9d84865 DM |
266 | eval { |
267 | $SIG{TERM} = $SIG{INT} = $SIG{KILL} = sub { die "received interrupt\n"; }; | |
95b87dd3 | 268 | |
b2123baa DM |
269 | PVE::APIClient::PTY::cfmakeraw($raw_termios); |
270 | PVE::APIClient::PTY::tcsetattr(*STDIN, $raw_termios); | |
95b87dd3 | 271 | |
e9d84865 DM |
272 | # And set it to non-blocking so we can every char with IO::Select. |
273 | STDIN->blocking(0); | |
d956d618 | 274 | STDOUT->blocking(0); |
48e35b78 | 275 | $web_socket->blocking(0); |
1ec5788c | 276 | $read_select->add($web_socket); |
8a58c001 | 277 | my $input_fh = \*STDIN; |
1ec5788c DM |
278 | $read_select->add($input_fh); |
279 | ||
8a58c001 | 280 | my $output_fh = \*STDOUT; |
95b87dd3 | 281 | |
83a7ab4c | 282 | my $in_escape_sequence; |
95b87dd3 | 283 | |
9657c731 RJ |
284 | my $winch_received = 0; |
285 | $SIG{WINCH} = sub { $winch_received = 1; }; | |
286 | ||
287 | my $check_terminal_size = sub { | |
b2123baa | 288 | my ($ncols, $nrows) = PVE::APIClient::PTY::tcgetsize(*STDIN); |
9657c731 RJ |
289 | if ($ncols != $columns or $nrows != $rows) { |
290 | $columns = $ncols; | |
291 | $rows = $nrows; | |
1ec5788c DM |
292 | $websock_buffer .= $create_websockt_frame->("1:$columns:$rows:"); |
293 | $write_select->add($web_socket); | |
9657c731 RJ |
294 | } |
295 | $winch_received = 0; | |
296 | }; | |
297 | ||
032dc44c DM |
298 | my $max_buffer_len = 256*1024; |
299 | ||
1ec5788c DM |
300 | my $drain_buffer = sub { |
301 | my ($fh, $buffer_ref) = @_; | |
302 | ||
303 | my $len = length($$buffer_ref); | |
304 | my $nr = syswrite($fh, $$buffer_ref); | |
305 | if (!defined($nr)) { | |
83a7ab4c | 306 | return if $! == EINTR || $! == EAGAIN; |
1ec5788c DM |
307 | die "drain buffer - write error - $!\n"; |
308 | } | |
309 | return $nr if !$nr; | |
310 | substr($$buffer_ref, 0, $nr, ''); | |
032dc44c DM |
311 | $len = length($$buffer_ref); |
312 | $write_select->remove($fh) if !$len; | |
1ec5788c DM |
313 | }; |
314 | ||
e9d84865 | 315 | while (1) { |
1ec5788c | 316 | while(my ($readable, $writable) = IO::Select->select($read_select, $write_select, undef, 3)) { |
9657c731 RJ |
317 | $check_terminal_size->() if $winch_received; |
318 | ||
1ec5788c DM |
319 | foreach my $fh (@$writable) { |
320 | if ($fh == $output_fh) { | |
8a58c001 | 321 | $drain_buffer->($output_fh, \$output_buffer); |
032dc44c | 322 | $read_select->add($web_socket) if length($output_buffer) <= $max_buffer_len; |
1ec5788c DM |
323 | } elsif ($fh == $web_socket) { |
324 | $drain_buffer->($web_socket, \$websock_buffer); | |
325 | } | |
326 | } | |
327 | ||
e4c01635 | 328 | foreach my $fh (@$readable) { |
e9d84865 DM |
329 | |
330 | if ($fh == $web_socket) { | |
331 | # Read from WebSocket | |
332 | ||
333 | my $nr = $wb_socket_read_available_bytes->(); | |
334 | if (!defined($nr)) { | |
bf705a1e | 335 | # wait |
e9d84865 DM |
336 | } elsif ($nr == 0) { |
337 | return; # EOF | |
338 | } else { | |
339 | my ($payload, $req_close) = $parse_web_socket_frame->(\$wsbuf); | |
032dc44c | 340 | if (defined($payload) && length($payload)) { |
1ec5788c DM |
341 | $output_buffer .= $payload; |
342 | $write_select->add($output_fh); | |
032dc44c DM |
343 | if (length($output_buffer) > $max_buffer_len) { |
344 | $read_select->remove($web_socket); | |
345 | } | |
e9d84865 DM |
346 | } |
347 | return if $req_close; | |
348 | } | |
95b87dd3 | 349 | |
e9d84865 DM |
350 | } elsif ($fh == $input_fh) { |
351 | # Read from STDIN | |
95b87dd3 | 352 | |
8a58c001 | 353 | my $nr = sysread($input_fh, my $buff, 4096); |
e9d84865 | 354 | return if !$nr; # EOF or error |
95b87dd3 | 355 | |
e9d84865 | 356 | my $char = ord($buff); |
95b87dd3 | 357 | |
83a7ab4c WB |
358 | # Handle escape sequences: |
359 | if ($in_escape_sequence) { | |
360 | $in_escape_sequence = 0; | |
361 | if ($char == 0x71) { | |
362 | # (escape, 'q') | |
363 | return; | |
364 | } elsif ($char == $escape_char) { | |
365 | # (escape, escape) | |
366 | # Pass this one through as a single escapekey | |
367 | } else { | |
368 | # Unknown escape sequence | |
369 | # We could generate a bell or something... | |
370 | # but for now just skip it | |
371 | next; | |
372 | } | |
373 | } elsif ($char == $escape_char) { | |
374 | $in_escape_sequence = 1; | |
375 | next; | |
376 | } | |
95b87dd3 | 377 | |
83a7ab4c | 378 | # Pass the key through: |
1ec5788c DM |
379 | $websock_buffer .= $create_websockt_frame->("0:" . $nr . ":" . $buff); |
380 | $write_select->add($web_socket); | |
95b87dd3 | 381 | } |
95b87dd3 | 382 | } |
1824383b | 383 | } |
9657c731 RJ |
384 | $check_terminal_size->() if $winch_received; |
385 | ||
e9d84865 | 386 | # got timeout |
1ec5788c DM |
387 | $websock_buffer .= $create_websockt_frame->("2"); # ping server to keep connection alive |
388 | $write_select->add($web_socket); | |
e9d84865 DM |
389 | } |
390 | }; | |
391 | my $err = $@; | |
392 | ||
393 | eval { # cleanup | |
394 | ||
395 | # switch back to blocking mode (else later shell commands will fail). | |
396 | STDIN->blocking(1); | |
397 | ||
398 | if ($web_socket->connected) { | |
399 | # close connection | |
1ec5788c DM |
400 | $websock_buffer .= "\x88" . pack('N', 0) . pack('n', 0); # Opcode, mask, statuscode |
401 | $full_write->($web_socket, $websock_buffer); | |
402 | $websock_buffer = ''; | |
e9d84865 | 403 | close($web_socket); |
1824383b | 404 | } |
e9d84865 DM |
405 | |
406 | # Reset the terminal parameters. | |
1ec5788c DM |
407 | $output_buffer .= "\e[24H\r\n"; |
408 | $full_write->(\*STDOUT, $output_buffer); | |
409 | $output_buffer = ''; | |
410 | ||
b2123baa | 411 | PVE::APIClient::PTY::tcsetattr(*STDIN, $old_termios); |
95b87dd3 | 412 | }; |
e9d84865 | 413 | warn $@ if $@; # show cleanup errors |
95b87dd3 | 414 | |
e9d84865 | 415 | print STDERR "\nERROR: $err" if $err; |
adf285bf | 416 | |
e9d84865 | 417 | return undef; |
adf285bf DM |
418 | }}); |
419 | ||
b7db4587 RJ |
420 | __PACKAGE__->register_method ({ |
421 | name => 'create', | |
422 | path => 'create', | |
423 | method => 'POST', | |
424 | description => "Create a container", | |
425 | parameters => { | |
426 | additionalProperties => 0, | |
427 | properties => PVE::APIClient::Helpers::merge_api_definition_properties( | |
428 | '/nodes/{node}/lxc', 'POST', { | |
429 | remote => get_standard_option('pveclient-remote-name'), | |
430 | vmid => get_standard_option('pve-vmid'), | |
431 | node => get_standard_option('pve-node'), | |
2b267ba2 RJ |
432 | quiet => { |
433 | description => "Suppress log output.", | |
434 | type => 'boolean', | |
435 | optional => 1, | |
436 | }, | |
437 | background => { | |
438 | description => "Do not wait for the command to complete.", | |
439 | type => 'boolean', | |
440 | optional => 1, | |
441 | }, | |
b7db4587 RJ |
442 | }), |
443 | }, | |
444 | returns => { type => 'null'}, | |
445 | code => sub { | |
446 | my ($param) = @_; | |
447 | ||
448 | my $remote = PVE::APIClient::Tools::extract_param($param, 'remote'); | |
449 | my $vmid = $param->{vmid}; | |
450 | my $node = PVE::APIClient::Tools::extract_param($param, 'node'); | |
451 | ||
2b267ba2 RJ |
452 | my $quiet = PVE::APIClient::Tools::extract_param($param, 'quiet'); |
453 | my $background = PVE::APIClient::Tools::extract_param($param, 'background'); | |
454 | ||
b7db4587 RJ |
455 | my $config = PVE::APIClient::Config->load(); |
456 | my $conn = PVE::APIClient::Config->remote_conn($config, $remote); | |
457 | ||
458 | my $upid = $conn->post("/nodes/$node/lxc", $param); | |
459 | ||
2b267ba2 RJ |
460 | if (!$background) { |
461 | print PVE::APIClient::Helpers::poll_task($conn, $node, $upid, $quiet) . "\n"; | |
462 | } | |
b7db4587 RJ |
463 | |
464 | return undef; | |
465 | }}); | |
466 | ||
467 | __PACKAGE__->register_method ({ | |
468 | name => 'destroy', | |
469 | path => 'destroy', | |
470 | method => 'DELETE', | |
471 | description => "Destroy a container", | |
472 | parameters => { | |
473 | additionalProperties => 0, | |
474 | properties => { | |
475 | remote => get_standard_option('pveclient-remote-name'), | |
476 | vmid => get_standard_option('pve-vmid'), | |
477 | }, | |
478 | }, | |
479 | returns => { type => 'null'}, | |
480 | code => sub { | |
481 | my ($param) = @_; | |
482 | ||
483 | my $remote = PVE::APIClient::Tools::extract_param($param, 'remote'); | |
484 | my $vmid = PVE::APIClient::Tools::extract_param($param, 'vmid'); | |
485 | ||
486 | my $config = PVE::APIClient::Config->load(); | |
487 | my $conn = PVE::APIClient::Config->remote_conn($config, $remote); | |
488 | ||
489 | my $resource = PVE::APIClient::Helpers::get_vmid_resource($conn, $vmid); | |
490 | ||
491 | my $upid = $conn->delete("/nodes/$resource->{node}/lxc/$resource->{vmid}", $param); | |
492 | ||
2b267ba2 | 493 | print PVE::APIClient::Helpers::poll_task($conn, $resource->{node}, $upid, 1) . "\n"; |
b7db4587 RJ |
494 | |
495 | return undef; | |
496 | }}); | |
497 | ||
adf285bf | 498 | our $cmddef = { |
b7db4587 RJ |
499 | create => [ __PACKAGE__, 'create', ['remote', 'vmid', 'node']], |
500 | destroy => [ __PACKAGE__, 'destroy', ['remote', 'vmid']], | |
84ba32e5 | 501 | enter => [ __PACKAGE__, 'enter', ['remote', 'vmid']], |
adf285bf DM |
502 | }; |
503 | ||
504 | 1; |