App-FuguVM
view release on metacpan or search on metacpan
lib/App/FuguVM/Autoinstall.pm view on Meta::CPAN
# ex:ts=8 sw=4:
# $OpenBSD$
#
# Copyright (c) 2026 Dick Olsson <hi@dickolsson.com>
#
# Permission to use, copy, modify, and distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
use v5.36;
# App::FuguVM::Autoinstall - serve one autoinstall(8) response file.
#
# The module holds the response file and the responder: a small HTTP
# child that answers that one file to one guest. The child binds the
# loopback address only, because a response file can hold the root
# password, and the guest reaches a loopback listener through the QEMU
# gateway. The module is FuguVM policy over generic parts: the file it
# serves is an installer artifact, and the address it serves to is a
# QEMU gateway. Fugu::Proxy stays a proxy, and it gains no
# origin-server mode here.
package App::FuguVM::Autoinstall;
our $VERSION = '0.3.0';
use IO::Socket::INET;
use Fugu::File;
use Fugu::Log;
use Fugu::Process;
use Fugu::Timeout;
use App::FuguVM::Proxy;
use constant {
RESPONSE_PATH => '/install.conf',
# One range above the proxy range, so the two children cannot
# contend for one port.
PORTS => [ 8181, 8280 ],
BIND_ADDRESS => '127.0.0.1',
PROXY_TOKEN => '@PROXY_URL@',
READY_TIMEOUT => 10,
# The bound on one request line. The guest sends one short GET,
# so a longer line is not a request from the installer.
MAX_REQUEST_LINE => 8192,
};
# App::FuguVM::Autoinstall->new(%args):
# file => $path the response file (required)
# pidfile => $pidfile a Fugu::Pidfile for the child (required)
# store => $store a Fugu::StateFile that holds the port (required)
# proxy_url => $url replaces each PROXY_TOKEN, or undef
# logfile => $path where the output of the child goes
# log => $logger default: Fugu::Log->default
#
# The constructor opens nothing.
sub new ( $class, %args )
{
for my $required (qw(pidfile store)) {
die "$required parameter required"
unless defined $args{$required};
}
return bless {
file => $args{file},
pidfile => $args{pidfile},
store => $args{store},
proxy_url => $args{proxy_url},
logfile => $args{logfile} // '/dev/null',
log => $args{log} // Fugu::Log->default,
error => undef,
}, $class;
}
# $self->path:
# Return the response-file path.
sub path ($self)
{
return $self->{file};
}
# $self->error:
# Return the reason of the last failed start, or undef.
sub error ($self)
{
return $self->{error};
}
# $self->port:
lib/App/FuguVM/Autoinstall.pm view on Meta::CPAN
last if index( $head, "\n\n" ) >= 0;
}
return '' if $head eq '';
return if length($head) >= MAX_REQUEST_LINE;
my $end = index( $head, "\n" );
return if $end < 0;
my $line = substr( $head, 0, $end );
$line =~ s/\r\z//;
return $line;
}
# _respond($client, $code, $text, $body, $head_only):
# Write one complete answer. Every answer carries Content-Length
# and Connection: close, and a HEAD answer carries the headers
# only.
sub _respond ( $client, $code, $text, $body, $head_only = 0 )
{
my $head =
"HTTP/1.1 $code $text\r\n"
. "Content-Type: text/plain\r\n"
. 'Content-Length: '
. length($body) . "\r\n"
. "Connection: close\r\n\r\n";
$head .= $body unless $head_only;
return Fugu::File->_write_all( $client, $head, 'client socket' );
}
# $self->_find_free_port:
# Return the first port of the range that binds on the loopback
# address, or undef.
sub _find_free_port ($self)
{
my ( $first, $last ) = @{ +PORTS };
for my $port ( $first .. $last ) {
my $sock = IO::Socket::INET->new(
LocalAddr => BIND_ADDRESS,
LocalPort => $port,
Proto => 'tcp',
ReuseAddr => 1,
Listen => 1,
) or next;
close $sock;
return $port;
}
return;
}
# $self->_wait_ready:
# Wait until the responder takes a connection.
sub _wait_ready ($self)
{
my $port = $self->port;
return 0 if !defined $port;
my $ready = Fugu::Timeout::wait_until(
READY_TIMEOUT,
0.2,
sub {
my $sock = IO::Socket::INET->new(
PeerAddr => BIND_ADDRESS,
PeerPort => $port,
Proto => 'tcp',
Timeout => 1,
) or return 0;
close $sock;
return 1;
} );
return $ready ? 1 : 0;
}
1;
( run in 0.912 second using v1.01-cache-2.11-cpan-85d3896f969 )