Merge branch 'main' into anvil-tools-dev

main
Digimer 1 year ago committed by GitHub
commit c1e4380a64
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      Anvil/Tools.pm
  2. 87
      Anvil/Tools/Server.pm
  3. 2
      Makefile.am
  4. 22
      anvil.spec.in
  5. 1
      configure.ac
  6. 9
      libvirt/Makefile.am
  7. 51
      libvirt/hooks/qemu.d/ws
  8. 124
      scancore-agents/scan-server/scan-server
  9. 2
      striker-ui-api/out/index.js
  10. 14
      striker-ui-api/package-lock.json
  11. 1
      striker-ui-api/package.json
  12. 4
      striker-ui-api/src/app.ts
  13. 106
      striker-ui-api/src/lib/accessModule.ts
  14. 14
      striker-ui-api/src/lib/buildJobData.ts
  15. 6
      striker-ui-api/src/lib/buildQueryResultModifier.ts
  16. 4
      striker-ui-api/src/lib/camel.ts
  17. 3
      striker-ui-api/src/lib/cname.ts
  18. 42
      striker-ui-api/src/lib/consts/ENV.ts
  19. 4
      striker-ui-api/src/lib/consts/NODE_AND_DR_RESERVED_MEMORY_SIZE.ts
  20. 5
      striker-ui-api/src/lib/consts/PROCESS_OWNER.ts
  21. 2
      striker-ui-api/src/lib/consts/REG_EXP_PATTERNS.ts
  22. 7
      striker-ui-api/src/lib/consts/SERVER_PATHS.ts
  23. 1
      striker-ui-api/src/lib/consts/SERVER_PORT.ts
  24. 6
      striker-ui-api/src/lib/consts/index.ts
  25. 56
      striker-ui-api/src/lib/fconfig/buildNetworkConfig.ts
  26. 19
      striker-ui-api/src/lib/fconfig/buildNetworkLinkConfig.ts
  27. 2
      striker-ui-api/src/lib/fconfig/index.ts
  28. 108
      striker-ui-api/src/lib/request_handlers/anvil/buildAnvilSummary.ts
  29. 179
      striker-ui-api/src/lib/request_handlers/anvil/buildQueryAnvilDetail.ts
  30. 30
      striker-ui-api/src/lib/request_handlers/anvil/getAnvil.ts
  31. 74
      striker-ui-api/src/lib/request_handlers/anvil/getAnvilCpu.ts
  32. 45
      striker-ui-api/src/lib/request_handlers/anvil/getAnvilDetail.ts
  33. 121
      striker-ui-api/src/lib/request_handlers/anvil/getAnvilMemory.ts
  34. 159
      striker-ui-api/src/lib/request_handlers/anvil/getAnvilNetwork.ts
  35. 67
      striker-ui-api/src/lib/request_handlers/anvil/getAnvilStore.ts
  36. 33
      striker-ui-api/src/lib/request_handlers/anvil/getAnvilSummary.ts
  37. 7
      striker-ui-api/src/lib/request_handlers/anvil/index.ts
  38. 3
      striker-ui-api/src/lib/request_handlers/auth/login.ts
  39. 12
      striker-ui-api/src/lib/request_handlers/auth/logout.ts
  40. 65
      striker-ui-api/src/lib/request_handlers/buildDeleteRequestHandler.ts
  41. 6
      striker-ui-api/src/lib/request_handlers/buildGetRequestHandler.ts
  42. 51
      striker-ui-api/src/lib/request_handlers/command/buildHostPowerHandler.ts
  43. 123
      striker-ui-api/src/lib/request_handlers/command/buildMembershipHandler.ts
  44. 151
      striker-ui-api/src/lib/request_handlers/command/buildPowerHandler.ts
  45. 14
      striker-ui-api/src/lib/request_handlers/command/index.ts
  46. 3
      striker-ui-api/src/lib/request_handlers/command/joinAn.ts
  47. 3
      striker-ui-api/src/lib/request_handlers/command/leaveAn.ts
  48. 52
      striker-ui-api/src/lib/request_handlers/command/manageVncSshTunnel.ts
  49. 3
      striker-ui-api/src/lib/request_handlers/command/poweroffHost.ts
  50. 3
      striker-ui-api/src/lib/request_handlers/command/poweroffStriker.ts
  51. 3
      striker-ui-api/src/lib/request_handlers/command/rebootHost.ts
  52. 3
      striker-ui-api/src/lib/request_handlers/command/rebootStriker.ts
  53. 70
      striker-ui-api/src/lib/request_handlers/command/setMapNetwork.ts
  54. 3
      striker-ui-api/src/lib/request_handlers/command/startAn.ts
  55. 3
      striker-ui-api/src/lib/request_handlers/command/startServer.ts
  56. 3
      striker-ui-api/src/lib/request_handlers/command/startSubnode.ts
  57. 3
      striker-ui-api/src/lib/request_handlers/command/stopAn.ts
  58. 3
      striker-ui-api/src/lib/request_handlers/command/stopServer.ts
  59. 3
      striker-ui-api/src/lib/request_handlers/command/stopSubnode.ts
  60. 138
      striker-ui-api/src/lib/request_handlers/fence/createFence.ts
  61. 20
      striker-ui-api/src/lib/request_handlers/fence/deleteFence.ts
  62. 4
      striker-ui-api/src/lib/request_handlers/fence/getFence.ts
  63. 6
      striker-ui-api/src/lib/request_handlers/fence/getFenceTemplate.ts
  64. 3
      striker-ui-api/src/lib/request_handlers/fence/index.ts
  65. 3
      striker-ui-api/src/lib/request_handlers/fence/updateFence.ts
  66. 12
      striker-ui-api/src/lib/request_handlers/file/buildQueryFileDetail.ts
  67. 49
      striker-ui-api/src/lib/request_handlers/file/updateFile.ts
  68. 108
      striker-ui-api/src/lib/request_handlers/host/buildQueryHostDetail.ts
  69. 141
      striker-ui-api/src/lib/request_handlers/host/configStriker.ts
  70. 9
      striker-ui-api/src/lib/request_handlers/host/createHost.ts
  71. 1
      striker-ui-api/src/lib/request_handlers/host/index.ts
  72. 154
      striker-ui-api/src/lib/request_handlers/host/prepareNetwork.ts
  73. 6
      striker-ui-api/src/lib/request_handlers/host/updateHost.ts
  74. 18
      striker-ui-api/src/lib/request_handlers/job/getJob.ts
  75. 46
      striker-ui-api/src/lib/request_handlers/network-interface/getNetworkInterface.ts
  76. 66
      striker-ui-api/src/lib/request_handlers/server/deleteServer.ts
  77. 55
      striker-ui-api/src/lib/request_handlers/server/getServer.ts
  78. 143
      striker-ui-api/src/lib/request_handlers/server/getServerDetail.ts
  79. 7
      striker-ui-api/src/lib/request_handlers/server/index.ts
  80. 86
      striker-ui-api/src/lib/request_handlers/ups/createUps.ts
  81. 20
      striker-ui-api/src/lib/request_handlers/ups/deleteUps.ts
  82. 3
      striker-ui-api/src/lib/request_handlers/ups/getUPS.ts
  83. 3
      striker-ui-api/src/lib/request_handlers/ups/index.ts
  84. 3
      striker-ui-api/src/lib/request_handlers/ups/updateUps.ts
  85. 71
      striker-ui-api/src/lib/request_handlers/user/createUser.ts
  86. 10
      striker-ui-api/src/lib/request_handlers/user/deleteUser.ts
  87. 28
      striker-ui-api/src/lib/request_handlers/user/getUser.ts
  88. 2
      striker-ui-api/src/lib/request_handlers/user/index.ts
  89. 140
      striker-ui-api/src/lib/request_handlers/user/updateUser.ts
  90. 2
      striker-ui-api/src/lib/varn.ts
  91. 54
      striker-ui-api/src/middlewares/assertAuthentication.ts
  92. 55
      striker-ui-api/src/middlewares/assertInit.ts
  93. 7
      striker-ui-api/src/middlewares/index.ts
  94. 33
      striker-ui-api/src/middlewares/passport.ts
  95. 18
      striker-ui-api/src/middlewares/session.ts
  96. 19
      striker-ui-api/src/routes/anvil.ts
  97. 3
      striker-ui-api/src/routes/auth.ts
  98. 29
      striker-ui-api/src/routes/command.ts
  99. 15
      striker-ui-api/src/routes/fence.ts
  100. 2
      striker-ui-api/src/routes/host.ts
  101. Some files were not shown because too many files have changed in this diff Show More

@ -1228,6 +1228,7 @@ sub _set_paths
modifyrepo_c => "/usr/bin/modifyrepo_c", modifyrepo_c => "/usr/bin/modifyrepo_c",
modprobe => "/usr/sbin/modprobe", modprobe => "/usr/sbin/modprobe",
mv => "/usr/bin/mv", mv => "/usr/bin/mv",
nc => "/usr/bin/nc",
nmap => "/usr/bin/nmap", nmap => "/usr/bin/nmap",
nmcli => "/bin/nmcli", nmcli => "/bin/nmcli",
ocf_alteeve => "/usr/lib/ocf/resource.d/alteeve/server", ocf_alteeve => "/usr/lib/ocf/resource.d/alteeve/server",

@ -16,6 +16,7 @@ my $THIS_FILE = "Server.pm";
# boot_virsh # boot_virsh
# count_servers # count_servers
# find # find
# find_processes
# get_definition # get_definition
# get_runtime # get_runtime
# get_status # get_status
@ -484,6 +485,92 @@ sub find
} }
=head2 find_processes
Find a list of qemu-kvm processes and extracts server information from the process arguments.
Parameters;
=head3 base_vnc_port (optional)
This value is added to the port offset extracted from -vnc optional to qemu-kvm. Defaults to 5900.
=cut
sub find_processes
{
my $self = shift;
my $parameters = shift;
my $anvil = $self->parent;
my $base_vnc_port = $parameters->{base_vnc_port} // 5900;
my $debug = $parameters->{debug} // 3;
$anvil->Log->variables({ source => $THIS_FILE, line => __LINE__, level => $debug, list => $parameters });
$base_vnc_port = "$base_vnc_port";
return (1) if (not $base_vnc_port =~ /^\d+$/);
$base_vnc_port = int($base_vnc_port);
# Servers only exist on non-striker
return (1) if ($anvil->data->{sys}{host_type} eq "striker");
my $grep = $anvil->data->{path}{exe}{'grep'};
my $nc = $anvil->data->{path}{exe}{'nc'};
my $ps = $anvil->data->{path}{exe}{'ps'};
my $sed = $anvil->data->{path}{exe}{'sed'};
my $ps_call = "$ps -ef | $grep '[q]emu-kvm' | $sed -E 's/^.*guest=([^,]+).*-uuid[[:space:]]+([^[:space:]]+)(.*-vnc[[:space:]]+([[:digit:].:]+))?.*\$/\\2,\\1,\\4/'";
$anvil->Log->variables({ source => $THIS_FILE, line => __LINE__, level => $debug, list => { ps_call => $ps_call }});
my ($call_output, $call_rcode) = $anvil->System->call({ shell_call => $ps_call });
return (1) if ($call_rcode != 0);
my $result = { names => {}, uuids => {} };
foreach my $line (split(/\n/, $call_output))
{
my ($uuid, $name, $vnc) = split(/,/, $line);
$anvil->Log->variables({ source => $THIS_FILE, line => __LINE__, level => $debug, list => {
server_name => $name,
server_uuid => $uuid,
server_vnc => $vnc,
}});
$result->{uuids}{$uuid} = { name => $name };
# Record name to UUID mapping
$result->{names}{$name} = $uuid;
next if (not $vnc);
my ($hostname, $port_offset) = split(/:/, $vnc);
my $vnc_port = $base_vnc_port + int($port_offset);
$anvil->Log->variables({ source => $THIS_FILE, line => __LINE__, level => $debug, list => {
server_vnc_hostname => $hostname,
server_vnc_port_offset => $port_offset,
server_vnc_port => $vnc_port,
}});
$result->{uuids}{$uuid}{vnc_port} = $vnc_port;
my $nc_call = "$nc -z $hostname $vnc_port";
$anvil->Log->variables({ source => $THIS_FILE, line => __LINE__, level => $debug, list => { nc_call => $nc_call }});
my ($nc_output, $nc_rcode) = $anvil->System->call({ shell_call => $nc_call });
$result->{uuids}{$uuid}{vnc_alive} = int($nc_rcode) > 0 ? 0 : 1;
}
return (0, $result);
}
=head2 get_definition =head2 get_definition
This returns the server definition XML for a server. This returns the server definition XML for a server.

@ -20,7 +20,7 @@ TARFILES = $(PACKAGE_NAME)-$(VERSION).tar.bz2 \
ACLOCAL_AMFLAGS = -I m4 ACLOCAL_AMFLAGS = -I m4
SUBDIRS = Anvil cgi-bin html journald.conf.d man ocf \ SUBDIRS = Anvil cgi-bin html journald.conf.d libvirt man ocf \
pxe scancore-agents scripts share striker-ui \ pxe scancore-agents scripts share striker-ui \
striker-ui-api tools units striker-ui-api tools units

@ -135,6 +135,7 @@ Requires: firefox
Requires: gcc Requires: gcc
Requires: gdm Requires: gdm
Requires: gnome-terminal Requires: gnome-terminal
Requires: netpbm-progs
Requires: nmap Requires: nmap
Requires: nodejs Requires: nodejs
Requires: openssh-askpass Requires: openssh-askpass
@ -156,7 +157,7 @@ NOTE: This installs and enables Gnome desktop.
%package node %package node
Summary: Alteeve's Anvil! node package Summary: Alteeve's Anvil! node package
Requires: anvil-core == %{version}-%{release} Requires: anvil-core == %{version}-%{release}
Requires: drbd90-utils Requires: drbd90-utils
Requires: kmod-drbd Requires: kmod-drbd
Requires: libvirt Requires: libvirt
@ -164,7 +165,7 @@ Requires: libvirt-daemon
Requires: libvirt-daemon-driver-qemu Requires: libvirt-daemon-driver-qemu
Requires: libvirt-daemon-kvm Requires: libvirt-daemon-kvm
Requires: libvirt-docs Requires: libvirt-docs
Requires: netpbm-progs Requires: nmap-ncat
Requires: pacemaker Requires: pacemaker
Requires: pcs Requires: pcs
Requires: python3-websockify Requires: python3-websockify
@ -176,6 +177,7 @@ Requires: virt-top
# allowed to host a database or be a DR host. # allowed to host a database or be a DR host.
Conflicts: anvil-striker Conflicts: anvil-striker
Conflicts: anvil-dr Conflicts: anvil-dr
Conflicts: netcat
%description node %description node
@ -188,7 +190,7 @@ NOTE: LINBIT customers must have access to the LINBIT repositories configured.
%package dr %package dr
Summary: Alteeve's Anvil! DR host package Summary: Alteeve's Anvil! DR host package
Requires: anvil-core == %{version}-%{release} Requires: anvil-core == %{version}-%{release}
Requires: drbd90-utils Requires: drbd90-utils
Requires: kmod-drbd Requires: kmod-drbd
Requires: libvirt Requires: libvirt
@ -196,15 +198,16 @@ Requires: libvirt-daemon
Requires: libvirt-daemon-driver-qemu Requires: libvirt-daemon-driver-qemu
Requires: libvirt-daemon-kvm Requires: libvirt-daemon-kvm
Requires: libvirt-docs Requires: libvirt-docs
Requires: netpbm-progs Requires: nmap-ncat
Requires: python3-websockify Requires: python3-websockify
Requires: qemu-kvm Requires: qemu-kvm
Requires: qemu-kvm-core Requires: qemu-kvm-core
Requires: virt-install Requires: virt-install
Requires: virt-top Requires: virt-top
# A DR host is not allowed to be a live-migration target or host a database. # A DR host is not allowed to be a live-migration target or host a database.
Conflicts: anvil-striker Conflicts: anvil-striker
Conflicts: anvil-node Conflicts: anvil-node
Conflicts: netcat
%description dr %description dr
@ -255,7 +258,7 @@ getent passwd %{suiapi} >/dev/null \
if [ $1 -gt 1 ]; then # >1=Upgrade if [ $1 -gt 1 ]; then # >1=Upgrade
# Transfer files owned by apache to Striker UI API user. # Transfer files owned by apache to Striker UI API user.
chown -R --from apache %{suiapi}: /mnt chown -R --from apache %{suiapi}: /mnt
chown -R --from apache %{suiapi}: %{_localstatedir}/www chown -R --from apache %{suiapi}: %{_localstatedir}/www
fi fi
@ -401,19 +404,20 @@ fi
%{_sbindir}/* %{_sbindir}/*
%{_sysconfdir}/anvil/anvil.version %{_sysconfdir}/anvil/anvil.version
%{_datadir}/perl5/* %{_datadir}/perl5/*
%{_datadir}/%{suiapi}/*
%{_mandir}/* %{_mandir}/*
%files striker %files striker
%{_localstatedir}/www/*/* %{_localstatedir}/www/*/*
%{_datadir}/anvil/striker-auto-initialize-all.example %{_datadir}/anvil/striker-auto-initialize-all.example
%{_datadir}/%{suiapi}/*
%ghost %{_sysconfdir}/anvil/snmp-vendors.txt %ghost %{_sysconfdir}/anvil/snmp-vendors.txt
%files node %files node
%{_sysconfdir}/libvirt/hooks/*
%{_usr}/lib/ocf/resource.d/alteeve/server %{_usr}/lib/ocf/resource.d/alteeve/server
%files dr %files dr
#<placeholder for dr specific files> %{_sysconfdir}/libvirt/hooks/*
%changelog %changelog

@ -159,6 +159,7 @@ AC_CONFIG_FILES([Makefile
cgi-bin/Makefile cgi-bin/Makefile
html/Makefile html/Makefile
journald.conf.d/Makefile journald.conf.d/Makefile
libvirt/Makefile
man/Makefile man/Makefile
ocf/Makefile ocf/Makefile
pxe/Makefile pxe/Makefile

@ -0,0 +1,9 @@
MAINTAINERCLEANFILES = Makefile.in
libvirtdir = ${sysconfdir}/libvirt
hooksdir = ${libvirtdir}/hooks
qemuddir = ${hooksdir}/qemu.d
dist_qemud_SCRIPTS = \
hooks/qemu.d/ws

@ -0,0 +1,51 @@
#!/bin/bash
# TODO: re-enable after the possible libvirt deadlock is fixed.
exit 0
{
echo "wsargs=$@"
domain_xml=$(</dev/stdin)
guest_name="$1"
operation="$2"
# Operation migrate will:
# 1. Trigger migrate->prepare->start->started operation on the destination host.
# 2. Trigger stopped->release operations on the source host.
if [[ "$operation" == "started" || "$operation" == "stopped" ]]
then
ws_open_flag=""
ws_port_flag=""
ws_suuid_flag=""
if [[ "$operation" == "started" ]]
then
ws_open_flag="--open"
# Cannot call $ virsh vncdisplay... because libvirt hooks
# cannot call anything related to libvirt, i.e., virsh, because
# a deadlock will happen.
server_vnc_port=$( grep "<graphics.*type=['\"]vnc['\"]" <<<$domain_xml | grep -oPm1 "(?<=port=['\"])\d+" )
ws_port_flag="--server-vnc-port ${server_vnc_port}"
server_uuid=$( grep -oPm1 "(?<=<uuid>)[^\s]+(?=<)" <<<$domain_xml )
ws_suuid_flag="--server-uuid ${server_uuid}"
local_host_uuid=$(</etc/anvil/host.uuid)
update_sql="UPDATE servers SET server_host_uuid = '$local_host_uuid' WHERE server_name = '$guest_name';"
echo "wsupdate=$update_sql"
anvil-access-module --query "$update_sql" --mode write
fi
ws_command_args="--server \"$guest_name\" $ws_suuid_flag --server-host-uuid local $ws_port_flag --component ws $ws_open_flag"
echo "wscmd=$ws_command_args"
striker-manage-vnc-pipes --server "$guest_name" $ws_suuid_flag --server-host-uuid local $ws_port_flag --component ws $ws_open_flag
echo "wscmd_exit=$?"
# Don't interrupt libvirt regardless of whether websockify gets setup
# successfully.
exit 0
fi
} >>/var/log/anvil.log

@ -88,12 +88,12 @@ collect_data($anvil);
# Look for migration times written out by ocf:alteeve:server. # Look for migration times written out by ocf:alteeve:server.
record_migration_times($anvil); record_migration_times($anvil);
# Check if we need to update the websocket stuff.
check_vnc($anvil);
# Check that there's a DRBD fence rule for each server. # Check that there's a DRBD fence rule for each server.
check_drbd_fence_rules($anvil); check_drbd_fence_rules($anvil);
# Get screenshot from every server that is alive.
get_screenshots($anvil);
# Shut down. # Shut down.
$anvil->ScanCore->agent_shutdown({agent => $THIS_FILE}); $anvil->ScanCore->agent_shutdown({agent => $THIS_FILE});
@ -169,72 +169,66 @@ sub check_drbd_fence_rules
return(0); return(0);
} }
# # Gets a screenshot for each server that is alive.
sub check_vnc sub get_screenshots
{ {
my ($anvil) = @_; my $anvil = shift;
my $parameters = shift;
### NOTE: In the interest of time, this table is not yet in the core schema. Later, when it is, this my $debug = $parameters->{debug} // 3;
### check can be removed.
# See if the 'vnc_pipes' table exists.
my $query = "SELECT COUNT(*) FROM information_schema.tables WHERE table_name = 'vnc_pipes' AND table_schema = 'public';";
$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 2, key => "log_0124", variables => { query => $query }});
my $count = $anvil->Database->query({query => $query, source => $THIS_FILE, line => __LINE__})->[0]->[0]; my $anvil_uuid = $anvil->Cluster->get_anvil_uuid();
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { count => $count }}); my $local_host_uuid = $anvil->Get->host_uuid();
if ($count) my $query = "SELECT host_name FROM hosts WHERE host_type = 'striker';";
my $results = $anvil->Database->query({ query => $query, source => $THIS_FILE, line => __LINE__ });
my $count = @{$results};
$anvil->Log->variables({ source => $THIS_FILE, line => __LINE__, level => $debug, list => { query => $query, count => $count } });
return (1) if ($count == 0);
# Start the CSV with the first element, then append.
my $striker_name_csv = $results->[0]->[0];
foreach my $row ( @{$results}[1 .. $#{$results}] )
{ {
# For each server running here, get the VNC port and record it. my $host_name = $row->[0];
my $anvil_uuid = $anvil->Cluster->get_anvil_uuid;
foreach my $server_name (sort {$a cmp $b} keys %{$anvil->data->{servers}{anvil_uuid}{$anvil_uuid}{server_name}}) $striker_name_csv = "$striker_name_csv,$host_name";
{
my $server_uuid = $anvil->data->{servers}{anvil_uuid}{$anvil_uuid}{server_name}{$server_name}{server_uuid};
my $server_state = $anvil->data->{servers}{server_uuid}{$server_uuid}{server_state};
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => {
server_name => $server_name,
server_uuid => $server_uuid,
server_state => $server_state,
}});
next if $server_state eq "paused";
next if $server_state eq "shut off";
next if $server_state eq "crashed";
# Get the VNC port. Ignore the IP and the port number is +5900.
my $shell_call = $anvil->data->{path}{exe}{setsid}." --wait ".$anvil->data->{path}{exe}{virsh}." vncdisplay --domain ".$server_name;
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { shell_call => $shell_call }});
my ($output, $return_code) = $anvil->System->call({shell_call => $shell_call, source => $THIS_FILE, line => __LINE__});
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => {
output => $output,
return_code => $return_code,
}});
foreach my $line (split/\n/, $output)
{
if ($line =~ /\d.*?:(\d+)$/)
{
my $port = 5900 + $1;
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { port => $port }});
my ($variable_uuid) = $anvil->Database->insert_or_update_variables({
file => $THIS_FILE,
line => __LINE__,
variable_name => "server::vnc_port",
variable_value => $port,
variable_default => "",
variable_description => "message_0255",
variable_section => "servers",
variable_source_uuid => $server_uuid,
variable_source_table => "servers",
});
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { variable_uuid => $variable_uuid }});
}
}
}
} }
return(0); $anvil->Log->variables({ source => $THIS_FILE, line => __LINE__, level => $debug, list => { striker_name_csv => $striker_name_csv } });
foreach my $server_name (sort {$a cmp $b} keys %{$anvil->data->{servers}{anvil_uuid}{$anvil_uuid}{server_name}})
{
my $server_uuid = $anvil->data->{servers}{anvil_uuid}{$anvil_uuid}{server_name}{$server_name}{server_uuid};
my $server_host_uuid = $anvil->data->{servers}{server_uuid}{$server_uuid}{server_host_uuid};
my $server_state = $anvil->data->{servers}{server_uuid}{$server_uuid}{server_state};
$anvil->Log->variables({ source => $THIS_FILE, line => __LINE__, level => $debug, list => {
server_host_uuid => $server_host_uuid,
server_name => $server_name,
server_state => $server_state,
server_uuid => $server_uuid,
} });
next if ( ($server_host_uuid ne $local_host_uuid) || (not $server_state eq "running") );
my ($syscall_output, $syscall_rcode) = $anvil->System->call({
debug => $debug,
line => __LINE__,
shell_call => $anvil->data->{path}{exe}{'anvil-get-server-screenshot'}." --server-uuid '$server_uuid' --request-host-name '$striker_name_csv' &",
source => $THIS_FILE,
});
$anvil->Log->variables({ source => $THIS_FILE, line => __LINE__, level => $debug, list => {
syscall_output => $syscall_output,
syscall_rcode => $syscall_rcode,
}});
}
return (0);
} }
# Look for migration times written out by ocf:alteeve:server. # Look for migration times written out by ocf:alteeve:server.

File diff suppressed because one or more lines are too long

@ -12,6 +12,7 @@
"cors": "^2.8.5", "cors": "^2.8.5",
"express": "^4.18.2", "express": "^4.18.2",
"express-session": "^1.17.3", "express-session": "^1.17.3",
"format-data-size": "^0.1.0",
"multer": "^1.4.4", "multer": "^1.4.4",
"passport": "^0.6.0", "passport": "^0.6.0",
"passport-local": "^1.0.0", "passport-local": "^1.0.0",
@ -4544,6 +4545,14 @@
"integrity": "sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==", "integrity": "sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==",
"dev": true "dev": true
}, },
"node_modules/format-data-size": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/format-data-size/-/format-data-size-0.1.0.tgz",
"integrity": "sha512-iataqDS6c73/MpJal7+GCtXiTbZEOn8HwfHesLvHOzu9MQwQ6LNOLbK/oli9qZmrap7TPGlaJnYMCxNR9Fh4iA==",
"bin": {
"format-data-size": "dist/cli.js"
}
},
"node_modules/forwarded": { "node_modules/forwarded": {
"version": "0.2.0", "version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@ -10303,6 +10312,11 @@
"integrity": "sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==", "integrity": "sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==",
"dev": true "dev": true
}, },
"format-data-size": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/format-data-size/-/format-data-size-0.1.0.tgz",
"integrity": "sha512-iataqDS6c73/MpJal7+GCtXiTbZEOn8HwfHesLvHOzu9MQwQ6LNOLbK/oli9qZmrap7TPGlaJnYMCxNR9Fh4iA=="
},
"forwarded": { "forwarded": {
"version": "0.2.0", "version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",

@ -15,6 +15,7 @@
"cors": "^2.8.5", "cors": "^2.8.5",
"express": "^4.18.2", "express": "^4.18.2",
"express-session": "^1.17.3", "express-session": "^1.17.3",
"format-data-size": "^0.1.0",
"multer": "^1.4.4", "multer": "^1.4.4",
"passport": "^0.6.0", "passport": "^0.6.0",
"passport-local": "^1.0.0", "passport-local": "^1.0.0",

@ -1,11 +1,9 @@
import cors from 'cors'; import cors from 'cors';
import express, { json } from 'express'; import express, { json } from 'express';
import { guardApi } from './lib/assertAuthentication'; import { guardApi, passport, session } from './middlewares';
import passport from './passport';
import routes from './routes'; import routes from './routes';
import { rrouters } from './lib/rrouters'; import { rrouters } from './lib/rrouters';
import session from './session';
export default (async () => { export default (async () => {
const app = express(); const app = express();

@ -2,7 +2,7 @@ import { ChildProcess, spawn, SpawnOptions } from 'child_process';
import EventEmitter from 'events'; import EventEmitter from 'events';
import { readFileSync } from 'fs'; import { readFileSync } from 'fs';
import { SERVER_PATHS, PGID, PUID } from './consts'; import { SERVER_PATHS, PGID, PUID, DEFAULT_JOB_PROGRESS } from './consts';
import { formatSql } from './formatSql'; import { formatSql } from './formatSql';
import { import {
@ -131,17 +131,21 @@ class Access extends EventEmitter {
} }
const access = new Access(); const access = new Access();
const rootAccess = new Access({ spawnOptions: { gid: 0, uid: 0 } });
const subroutine = async <T extends unknown[]>( const subroutine = async <T extends unknown[]>(
subroutine: string, subroutine: string,
{ {
params = [], params = [],
pre = ['Database'], pre = ['Database'],
root,
}: { }: {
params?: unknown[]; params?: unknown[];
pre?: string[]; pre?: string[];
root?: boolean;
} = {}, } = {},
) => { ) => {
const selectedAccess = root ? rootAccess : access;
const chain = `${pre.join('->')}->${subroutine}`; const chain = `${pre.join('->')}->${subroutine}`;
const subParams: string[] = params.map<string>((p) => { const subParams: string[] = params.map<string>((p) => {
@ -153,21 +157,19 @@ const subroutine = async <T extends unknown[]>(
result = String(p); result = String(p);
} }
return `'${result}'`; return `"${result.replaceAll('"', '\\"')}"`;
}); });
const { sub_results: results } = await access.interact<{ sub_results: T }>( const { sub_results: results } = await selectedAccess.interact<{
'x', sub_results: T;
chain, }>('x', chain, ...subParams);
...subParams,
);
shvar(results, `${chain} results: `); shvar(results, `${chain} results: `);
return results; return results;
}; };
const query = <T extends (number | null | string)[][]>(script: string) => const query = <T extends QueryResult>(script: string) =>
access.interact<T>('r', formatSql(script)); access.interact<T>('r', formatSql(script));
const write = async (script: string) => { const write = async (script: string) => {
@ -180,7 +182,7 @@ const write = async (script: string) => {
}; };
const insertOrUpdateJob = async ({ const insertOrUpdateJob = async ({
job_progress = 0, job_progress = DEFAULT_JOB_PROGRESS,
line = 0, line = 0,
...rest ...rest
}: JobParams) => { }: JobParams) => {
@ -191,6 +193,14 @@ const insertOrUpdateJob = async ({
return uuid; return uuid;
}; };
const insertOrUpdateUser: InsertOrUpdateUserFunction = async (params) => {
const [uuid]: [string] = await subroutine('insert_or_update_users', {
params: [params],
});
return uuid;
};
const insertOrUpdateVariable: InsertOrUpdateVariableFunction = async ( const insertOrUpdateVariable: InsertOrUpdateVariableFunction = async (
params, params,
) => { ) => {
@ -238,6 +248,15 @@ const refreshTimestamp = () => {
return result; return result;
}; };
const encrypt: EncryptFunction = async (params) => {
const [result]: [Encrypted] = await subroutine('encrypt_password', {
params: [params],
pre: ['Account'],
});
return result;
};
const getData = async <T>(...keys: string[]) => { const getData = async <T>(...keys: string[]) => {
const chain = `data->${keys.join('->')}`; const chain = `data->${keys.join('->')}`;
@ -250,10 +269,16 @@ const getData = async <T>(...keys: string[]) => {
return data; return data;
}; };
const getAnvilData = async () => {
await subroutine('get_anvils');
return getData<AnvilDataAnvilListHash>('anvils');
};
const getFenceSpec = async () => { const getFenceSpec = async () => {
await subroutine('get_fence_data', { pre: ['Striker'] }); await subroutine('get_fence_data', { pre: ['Striker'] });
return getData<unknown>('fence_data'); return getData<AnvilDataFenceHash>('fence_data');
}; };
const getHostData = async () => { const getHostData = async () => {
@ -303,6 +328,25 @@ const getManifestData = async (manifestUuid?: string) => {
return getData<AnvilDataManifestListHash>('manifests'); return getData<AnvilDataManifestListHash>('manifests');
}; };
const getNetworkData = async (hostUuid: string, hostName?: string) => {
let replacementKey = hostName;
if (!replacementKey) {
({
host_uuid: {
[hostUuid]: { short_host_name: replacementKey },
},
} = await getHostData());
}
await subroutine('load_interfces', {
params: [{ host: replacementKey, host_uuid: hostUuid }],
pre: ['Network'],
});
return getData<AnvilDataNetworkListHash>('network');
};
const getPeerData: GetPeerDataFunction = async ( const getPeerData: GetPeerDataFunction = async (
target, target,
{ password, port } = {}, { password, port } = {},
@ -340,20 +384,62 @@ const getUpsSpec = async () => {
return getData<AnvilDataUPSHash>('ups_data'); return getData<AnvilDataUPSHash>('ups_data');
}; };
const vncpipe = async (serverUuid: string, open?: boolean) => {
const [output, rReturnCode]: [string, string] = await subroutine('call', {
params: [
{
shell_call: `${
SERVER_PATHS.usr.sbin['striker-manage-vnc-pipes'].self
} --server-uuid ${serverUuid} --component st${open ? ' --open' : ''}`,
},
],
pre: ['System'],
root: true,
});
const rcode = Number.parseInt(rReturnCode);
if (rcode !== 0) {
throw new Error(`VNC pipe call failed with code ${rcode}`);
}
const lines = output.split('\n');
const lastLine = lines[lines.length - 1];
const rVncPipeProps = lastLine
.split(',')
.reduce<Record<string, string>>((previous, pair) => {
const [key, value] = pair.trim().split(/\s*:\s*/, 2);
previous[key] = value;
return previous;
}, {});
const forwardPort = Number.parseInt(rVncPipeProps.forward_port);
const protocol = rVncPipeProps.protocol;
return { forwardPort, protocol };
};
export { export {
insertOrUpdateJob as job, insertOrUpdateJob as job,
insertOrUpdateUser,
insertOrUpdateVariable as variable, insertOrUpdateVariable as variable,
anvilSyncShared, anvilSyncShared,
refreshTimestamp as timestamp, refreshTimestamp as timestamp,
encrypt,
getData, getData,
getAnvilData,
getFenceSpec, getFenceSpec,
getHostData, getHostData,
getLocalHostName, getLocalHostName,
getLocalHostUuid as getLocalHostUUID, getLocalHostUuid as getLocalHostUUID,
getManifestData, getManifestData,
getNetworkData,
getPeerData, getPeerData,
getUpsSpec, getUpsSpec,
query, query,
subroutine as sub, subroutine as sub,
vncpipe,
write, write,
}; };

@ -0,0 +1,14 @@
export const buildJobData = <T extends [string, unknown][]>({
entries,
getValue = (v) => String(v),
}: {
entries: T;
getValue?: (value: T[number][1]) => string;
}) =>
entries
.reduce<string>((previous, [key, value]) => {
previous += `${key}=${getValue(value)}\\n`;
return previous;
}, '')
.trim();

@ -1,12 +1,10 @@
type QueryField = string;
export const buildQueryResultModifier = export const buildQueryResultModifier =
<T>(mod: (output: QueryField[][]) => T): QueryResultModifierFunction => <T>(mod: (output: string[][]) => T): QueryResultModifierFunction =>
(output) => (output) =>
output instanceof Array ? mod(output) : output; output instanceof Array ? mod(output) : output;
export const buildQueryResultReducer = <T>( export const buildQueryResultReducer = <T>(
reduce: (previous: T, row: QueryField[]) => T, reduce: (previous: T, row: string[]) => T,
initialValue: T, initialValue: T,
) => ) =>
buildQueryResultModifier<T>((output) => buildQueryResultModifier<T>((output) =>

@ -0,0 +1,4 @@
import { cap } from './cap';
export const camel = (...[head, ...rest]: string[]): string =>
rest.reduce<string>((previous, part) => `${previous}${cap(part)}`, head);

@ -0,0 +1,3 @@
import { COOKIE_PREFIX } from './consts';
export const cname = (postfix: string) => `${COOKIE_PREFIX}.${postfix}`;

@ -0,0 +1,42 @@
import { resolveGid, resolveUid } from '../shell';
/**
* The prefix of every cookie used by the express app.
*
* @default 'suiapi'
*/
export const COOKIE_PREFIX = process.env.COOKIE_PREFIX ?? 'suiapi';
/**
* The fallback job progress value when queuing jobs.
*
* Ignore jobs by setting this to `100`.
*
* @default 0
*/
export const DEFAULT_JOB_PROGRESS: number = Number.parseInt(
process.env.DEFAULT_JOB_PROGRESS ?? '0',
);
/**
* Port to use by the express app.
*
* @default 8080
*/
export const PORT = Number.parseInt(process.env.PORT ?? '8080');
/**
* Process user identifier. Also used to set ownership on the access daemon.
*
* @default 'striker-ui-api'
*/
export const PUID = resolveUid(process.env.PUID ?? 'striker-ui-api');
/**
* Process group identifier. Also used to set ownership on the access daemon.
*
* Defaults to the value set in process user identifier.
*
* @default PUID
*/
export const PGID = resolveGid(process.env.PGID ?? PUID);

@ -1,4 +1,2 @@
// Unit: bytes // Unit: bytes
const NODE_AND_DR_RESERVED_MEMORY_SIZE = 8589934592; export const NODE_AND_DR_RESERVED_MEMORY_SIZE = 8589934592;
export default NODE_AND_DR_RESERVED_MEMORY_SIZE;

@ -1,5 +0,0 @@
import { resolveGid, resolveUid } from '../shell';
export const PUID = resolveUid(process.env.PUID ?? 'striker-ui-api');
export const PGID = resolveGid(process.env.PGID ?? PUID);

@ -1,4 +1,4 @@
export const P_HEX = '[[:xdigit:]]'; export const P_HEX = '[a-f0-9]';
export const P_OCTET = '(?:25[0-5]|(?:2[0-4]|1[0-9]|[1-9]|)[0-9])'; export const P_OCTET = '(?:25[0-5]|(?:2[0-4]|1[0-9]|[1-9]|)[0-9])';
export const P_ALPHANUM = '[a-z0-9]'; export const P_ALPHANUM = '[a-z0-9]';
export const P_ALPHANUM_DASH = '[a-z0-9-]'; export const P_ALPHANUM_DASH = '[a-z0-9-]';

@ -24,17 +24,24 @@ const EMPTY_SERVER_PATHS: ServerPath = {
}, },
sbin: { sbin: {
'anvil-access-module': {}, 'anvil-access-module': {},
'anvil-boot-server': {},
'anvil-configure-host': {}, 'anvil-configure-host': {},
'anvil-delete-server': {},
'anvil-get-server-screenshot': {}, 'anvil-get-server-screenshot': {},
'anvil-join-anvil': {}, 'anvil-join-anvil': {},
'anvil-manage-keys': {}, 'anvil-manage-keys': {},
'anvil-manage-power': {}, 'anvil-manage-power': {},
'anvil-provision-server': {}, 'anvil-provision-server': {},
'anvil-safe-start': {},
'anvil-safe-stop': {},
'anvil-shutdown-server': {},
'anvil-sync-shared': {}, 'anvil-sync-shared': {},
'anvil-update-system': {}, 'anvil-update-system': {},
'striker-boot-machine': {},
'striker-initialize-host': {}, 'striker-initialize-host': {},
'striker-manage-install-target': {}, 'striker-manage-install-target': {},
'striker-manage-peers': {}, 'striker-manage-peers': {},
'striker-manage-vnc-pipes': {},
'striker-parse-os-list': {}, 'striker-parse-os-list': {},
}, },
}, },

@ -1 +0,0 @@
export const PORT = process.env.PORT ?? 8080;

@ -4,7 +4,9 @@ export { SERVER_PATHS };
export * from './AN_VARIABLE_NAME_LIST'; export * from './AN_VARIABLE_NAME_LIST';
export * from './DELETED'; export * from './DELETED';
export * from './ENV';
export * from './EXIT_CODE_LIST'; export * from './EXIT_CODE_LIST';
export * from './PROCESS_OWNER'; export * from './LOCAL';
export * from './NODE_AND_DR_RESERVED_MEMORY_SIZE';
export * from './OS_LIST';
export * from './REG_EXP_PATTERNS'; export * from './REG_EXP_PATTERNS';
export * from './SERVER_PORT';

@ -0,0 +1,56 @@
import { buildNetworkLinkConfig } from './buildNetworkLinkConfig';
import { cvar } from '../varn';
export const buildNetworkConfig = (
networks: InitializeStrikerNetworkForm[],
{
netconfStep = 2,
netcountStep = 1,
}: {
netconfStep?: number;
netcountStep?: number;
} = {},
): FormConfigData => {
const { counters: ncounts, data: cdata } = networks.reduce<{
counters: Record<InitializeStrikerNetworkForm['type'], number>;
data: FormConfigData;
}>(
(previous, { createBridge, interfaces, ipAddress, subnetMask, type }) => {
const { counters } = previous;
counters[type] = counters[type] ? counters[type] + 1 : 1;
const networkShortName = `${type}${counters[type]}`;
previous.data = {
...previous.data,
[cvar(netconfStep, `${networkShortName}_ip`)]: {
step: netconfStep,
value: ipAddress,
},
[cvar(netconfStep, `${networkShortName}_subnet_mask`)]: {
step: netconfStep,
value: subnetMask,
},
...buildNetworkLinkConfig(networkShortName, interfaces),
};
if (createBridge) {
previous.data[cvar(netconfStep, `${networkShortName}_create_bridge`)] =
{
step: netconfStep,
value: createBridge,
};
}
return previous;
},
{ counters: {}, data: {} },
);
Object.entries(ncounts).forEach(([ntype, ncount]) => {
cdata[cvar(netcountStep, `${ntype}_count`)] = { value: ncount };
});
return cdata;
};

@ -0,0 +1,19 @@
import { cvar } from '../varn';
export const buildNetworkLinkConfig = (
networkShortName: string,
interfaces: InitializeStrikerNetworkForm['interfaces'],
configStep = 2,
) =>
interfaces.reduce<FormConfigData>((previous, iface, index) => {
if (iface) {
const { networkInterfaceMACAddress } = iface;
const linkNumber = index + 1;
previous[
cvar(configStep, `${networkShortName}_link${linkNumber}_mac_to_set`)
] = { step: configStep, value: networkInterfaceMACAddress };
}
return previous;
}, {});

@ -0,0 +1,2 @@
export * from './buildNetworkConfig';
export * from './buildNetworkLinkConfig';

@ -0,0 +1,108 @@
import assert from 'assert';
import { query } from '../../accessModule';
import { stderr } from '../../shell';
const buildHostStateMessage = (postfix = 2) => `message_022${postfix}`;
export const buildAnvilSummary = async ({
anvils,
anvilUuid,
hosts,
}: {
anvils: AnvilDataAnvilListHash;
anvilUuid: string;
hosts: AnvilDataHostListHash;
}) => {
const {
anvil_uuid: { [anvilUuid]: ainfo },
} = anvils;
if (!ainfo)
throw new Error(`Anvil information not found with UUID ${anvilUuid}`);
const {
anvil_name: aname,
anvil_node1_host_uuid: n1uuid,
anvil_node2_host_uuid: n2uuid,
} = ainfo;
const result: AnvilDetailSummary = {
anvil_name: aname,
anvil_state: 'optimal',
anvil_uuid: anvilUuid,
hosts: [],
};
for (const huuid of [n1uuid, n2uuid]) {
const {
host_uuid: {
[huuid]: { host_status: hstatus, short_host_name: hname },
},
} = hosts;
const { hosts: rhosts } = result;
const hsummary: AnvilDetailHostSummary = {
host_name: hname,
host_uuid: huuid,
maintenance_mode: false,
state: 'offline',
state_message: buildHostStateMessage(),
state_percent: 0,
};
rhosts.push(hsummary);
if (hstatus !== 'online') continue;
let rows: [
inCcm: NumberBoolean,
crmdMember: NumberBoolean,
clusterMember: NumberBoolean,
maintenanceMode: NumberBoolean,
][];
try {
rows = await query(`SELECT
scan_cluster_node_in_ccm,
scan_cluster_node_crmd_member,
scan_cluster_node_cluster_member,
scan_cluster_node_maintenance_mode
FROM
scan_cluster_nodes
WHERE
scan_cluster_node_host_uuid = '${huuid}';`);
assert.ok(rows.length, 'No node cluster info');
} catch (error) {
stderr(`Failed to get node ${huuid} cluster status; CAUSE: ${error}`);
continue;
}
const [[ccm, crmd, cluster, maintenance]] = rows;
hsummary.maintenance_mode = Boolean(maintenance);
if (cluster) {
hsummary.state = 'online';
hsummary.state_message = buildHostStateMessage(3);
hsummary.state_percent = 100;
} else if (crmd) {
hsummary.state = 'crmd';
hsummary.state_message = buildHostStateMessage(4);
hsummary.state_percent = 75;
} else if (ccm) {
hsummary.state = 'in_ccm';
hsummary.state_message = buildHostStateMessage(5);
hsummary.state_percent = 50;
} else {
hsummary.state = 'booted';
hsummary.state_message = buildHostStateMessage(6);
hsummary.state_percent = 25;
}
}
return result;
};

@ -1,5 +1,6 @@
import NODE_AND_DR_RESERVED_MEMORY_SIZE from '../../consts/NODE_AND_DR_RESERVED_MEMORY_SIZE'; import { dSize } from 'format-data-size';
import { OS_LIST } from '../../consts/OS_LIST';
import { NODE_AND_DR_RESERVED_MEMORY_SIZE, OS_LIST } from '../../consts';
import join from '../../join'; import join from '../../join';
import { stdoutVar } from '../../shell'; import { stdoutVar } from '../../shell';
@ -63,13 +64,13 @@ const buildQueryAnvilDetail = ({
server_uuid, server_uuid,
server_name, server_name,
server_cpu_cores, server_cpu_cores,
server_memory`; server_memory_value,
server_memory_unit`;
let groupByPhrase = ''; let groupByPhrase = '';
if (isSummary) { if (isSummary) {
fieldsToSelect = ` fieldsToSelect =
SUM(server_cpu_cores) AS anvil_total_allocated_cpu_cores, 'SUM(server_cpu_cores) AS anvil_total_allocated_cpu_cores';
SUM(server_memory) AS anvil_total_allocated_memory`;
groupByPhrase = 'GROUP BY server_anvil_uuid'; groupByPhrase = 'GROUP BY server_anvil_uuid';
} }
@ -82,29 +83,20 @@ const buildQueryAnvilDetail = ({
JOIN ( JOIN (
SELECT SELECT
server_definition_server_uuid, server_definition_server_uuid,
server_cpu_cores, CAST(
CASE server_memory_unit
WHEN 'KiB' THEN server_memory_value * 1024
ELSE server_memory_value
END AS server_memory
FROM (
SELECT
server_definition_server_uuid,
CAST(
SUBSTRING(
server_definition_xml, '%cores=''#"[0-9]+#"''%', '#'
) AS INTEGER
) AS server_cpu_cores,
CAST(
SUBSTRING(
server_definition_xml, '%memory%>#"[0-9]+#"</memory%', '#'
) AS BIGINT
) AS server_memory_value,
SUBSTRING( SUBSTRING(
server_definition_xml, '%memory%unit=''#"[A-Za-z]+#"''%', '#' server_definition_xml, 'cores=''([\\d]*)'''
) AS server_memory_unit ) AS INTEGER
FROM server_definitions AS ser_def ) AS server_cpu_cores,
) AS ser_def_memory_converted CAST(
SUBSTRING(
server_definition_xml, 'memory.*>([\\d]*)</memory'
) AS BIGINT
) AS server_memory_value,
SUBSTRING(
server_definition_xml, 'memory.*unit=''([A-Za-z]*)'''
) AS server_memory_unit
FROM server_definitions AS ser_def
) AS pos_ser_def ) AS pos_ser_def
ON server_uuid = server_definition_server_uuid ON server_uuid = server_definition_server_uuid
${groupByPhrase}`; ${groupByPhrase}`;
@ -129,7 +121,7 @@ const buildQueryAnvilDetail = ({
const buildFileQuery = () => ` const buildFileQuery = () => `
SELECT SELECT
file_location_anvil_uuid, file_location_host_uuid,
file_uuid, file_uuid,
file_name file_name
FROM file_locations as fil_loc FROM file_locations as fil_loc
@ -153,16 +145,12 @@ const buildQueryAnvilDetail = ({
server_list.server_uuid, server_list.server_uuid,
server_list.server_name, server_list.server_name,
server_list.server_cpu_cores, server_list.server_cpu_cores,
server_list.server_memory, server_list.server_memory_value,
server_list.server_memory_unit,
server_summary.anvil_total_allocated_cpu_cores, server_summary.anvil_total_allocated_cpu_cores,
server_summary.anvil_total_allocated_memory,
(host_summary.anvil_total_cpu_cores (host_summary.anvil_total_cpu_cores
- server_summary.anvil_total_allocated_cpu_cores - server_summary.anvil_total_allocated_cpu_cores
) AS anvil_total_available_cpu_cores, ) AS anvil_total_available_cpu_cores,
(host_summary.anvil_total_memory
- server_summary.anvil_total_allocated_memory
- ${NODE_AND_DR_RESERVED_MEMORY_SIZE}
) AS anvil_total_available_memory,
storage_group_list.storage_group_uuid, storage_group_list.storage_group_uuid,
storage_group_list.storage_group_name, storage_group_list.storage_group_name,
storage_group_list.storage_group_size, storage_group_list.storage_group_size,
@ -181,7 +169,11 @@ const buildQueryAnvilDetail = ({
LEFT JOIN (${buildStorageGroupQuery()}) AS storage_group_list LEFT JOIN (${buildStorageGroupQuery()}) AS storage_group_list
ON anv.anvil_uuid = storage_group_list.storage_group_anvil_uuid ON anv.anvil_uuid = storage_group_list.storage_group_anvil_uuid
LEFT JOIN (${buildFileQuery()}) AS file_list LEFT JOIN (${buildFileQuery()}) AS file_list
ON anv.anvil_uuid = file_list.file_location_anvil_uuid ON file_list.file_location_host_uuid IN (
anv.anvil_node1_host_uuid,
anv.anvil_node2_host_uuid,
anv.anvil_dr1_host_uuid
)
;`; ;`;
let query = ` let query = `
@ -195,15 +187,25 @@ const buildQueryAnvilDetail = ({
if (isForProvisionServer) { if (isForProvisionServer) {
query = buildQueryForProvisionServer(); query = buildQueryForProvisionServer();
afterQueryReturn = (queryStdout: unknown) => { afterQueryReturn = (qoutput: unknown) => {
let results = queryStdout; let results = qoutput;
if (qoutput instanceof Array) {
const lasti = qoutput.length - 1;
if (queryStdout instanceof Array) { let puuid = '';
let rowStage: AnvilDetailForProvisionServer | undefined;
const anvils = queryStdout.reduce<AnvilDetailForProvisionServer[]>( let anvilTotalAllocatedMemory = BigInt(0);
let files: Record<string, AnvilDetailFileForProvisionServer> = {};
let hosts: Record<string, AnvilDetailHostForProvisionServer> = {};
let servers: Record<string, AnvilDetailServerForProvisionServer> = {};
let stores: Record<string, AnvilDetailStoreForProvisionServer> = {};
const anvils = qoutput.reduce<
Record<string, AnvilDetailForProvisionServer>
>(
( (
reducedRows, previous,
[ [
anvilUUID, anvilUUID,
anvilName, anvilName,
@ -217,11 +219,10 @@ const buildQueryAnvilDetail = ({
serverUUID, serverUUID,
serverName, serverName,
serverCPUCores, serverCPUCores,
serverMemory, serverMemoryValue,
serverMemoryUnit,
anvilTotalAllocatedCPUCores, anvilTotalAllocatedCPUCores,
anvilTotalAllocatedMemory,
anvilTotalAvailableCPUCores, anvilTotalAvailableCPUCores,
anvilTotalAvailableMemory,
storageGroupUUID, storageGroupUUID,
storageGroupName, storageGroupName,
storageGroupSize, storageGroupSize,
@ -229,9 +230,31 @@ const buildQueryAnvilDetail = ({
fileUUID, fileUUID,
fileName, fileName,
], ],
index,
) => { ) => {
if (!rowStage || anvilUUID !== rowStage.anvilUUID) { if (index === lasti || (puuid.length && anvilUUID !== puuid)) {
rowStage = { const { [puuid]: p } = previous;
p.anvilTotalAllocatedMemory = String(anvilTotalAllocatedMemory);
p.anvilTotalAvailableMemory = String(
BigInt(p.anvilTotalMemory) -
anvilTotalAllocatedMemory -
BigInt(NODE_AND_DR_RESERVED_MEMORY_SIZE),
);
p.files = Object.values(files);
p.hosts = Object.values(hosts);
p.servers = Object.values(servers);
p.storageGroups = Object.values(stores);
anvilTotalAllocatedMemory = BigInt(0);
files = {};
hosts = {};
servers = {};
stores = {};
}
if (!previous[anvilUUID]) {
previous[anvilUUID] = {
anvilUUID, anvilUUID,
anvilName, anvilName,
anvilDescription, anvilDescription,
@ -240,73 +263,63 @@ const buildQueryAnvilDetail = ({
anvilTotalAllocatedCPUCores: parseInt( anvilTotalAllocatedCPUCores: parseInt(
anvilTotalAllocatedCPUCores, anvilTotalAllocatedCPUCores,
), ),
anvilTotalAllocatedMemory: String(anvilTotalAllocatedMemory),
anvilTotalAvailableCPUCores: parseInt( anvilTotalAvailableCPUCores: parseInt(
anvilTotalAvailableCPUCores, anvilTotalAvailableCPUCores,
), ),
anvilTotalAvailableMemory: String(anvilTotalAvailableMemory), } as AnvilDetailForProvisionServer;
hosts: [],
servers: [],
storageGroups: [],
files: [],
};
reducedRows.push(rowStage); puuid = anvilUUID;
} }
if ( if (!hosts[hostUUID]) {
!rowStage.hosts.find(({ hostUUID: added }) => added === hostUUID) hosts[hostUUID] = {
) {
rowStage.hosts.push({
hostUUID, hostUUID,
hostName, hostName,
hostCPUCores: parseInt(hostCPUCores), hostCPUCores: parseInt(hostCPUCores),
hostMemory: String(hostMemory), hostMemory: String(hostMemory),
}); };
} }
if ( if (!servers[serverUUID]) {
!rowStage.servers.find( const serverMemory =
({ serverUUID: added }) => added === serverUUID, dSize(serverMemoryValue, {
) fromUnit: serverMemoryUnit,
) { toUnit: 'B',
rowStage.servers.push({ })?.value ?? '0';
anvilTotalAllocatedMemory += BigInt(serverMemory);
servers[serverUUID] = {
serverUUID, serverUUID,
serverName, serverName,
serverCPUCores: parseInt(serverCPUCores), serverCPUCores: parseInt(serverCPUCores),
serverMemory: String(serverMemory), serverMemory,
}); };
} }
if ( if (!stores[storageGroupUUID]) {
!rowStage.storageGroups.find( stores[storageGroupUUID] = {
({ storageGroupUUID: added }) => added === storageGroupUUID,
)
) {
rowStage.storageGroups.push({
storageGroupUUID, storageGroupUUID,
storageGroupName, storageGroupName,
storageGroupSize: String(storageGroupSize), storageGroupSize: String(storageGroupSize),
storageGroupFree: String(storageGroupFree), storageGroupFree: String(storageGroupFree),
}); };
} }
if ( if (!files[fileUUID]) {
!rowStage.files.find(({ fileUUID: added }) => added === fileUUID) files[fileUUID] = {
) {
rowStage.files.push({
fileUUID, fileUUID,
fileName, fileName,
}); };
} }
return reducedRows; return previous;
}, },
[], {},
); );
results = { results = {
anvils, anvils: Object.values(anvils),
osList: OS_LIST, osList: OS_LIST,
}; };
} }

@ -4,24 +4,24 @@ import buildGetRequestHandler from '../buildGetRequestHandler';
import buildQueryAnvilDetail from './buildQueryAnvilDetail'; import buildQueryAnvilDetail from './buildQueryAnvilDetail';
import { sanitize } from '../../sanitize'; import { sanitize } from '../../sanitize';
const getAnvil: RequestHandler = buildGetRequestHandler( export const getAnvil: RequestHandler = buildGetRequestHandler(
(request, buildQueryOptions) => { (request, buildQueryOptions) => {
const { anvilUUIDs, isForProvisionServer } = request.query; const { anvilUUIDs, isForProvisionServer } = request.query;
let query = ` let query = `
SELECT SELECT
anv.anvil_name, anv.anvil_name,
anv.anvil_uuid, anv.anvil_uuid,
hos.host_name, hos.host_name,
hos.host_uuid hos.host_uuid
FROM anvils AS anv FROM anvils AS anv
JOIN hosts AS hos JOIN hosts AS hos
ON hos.host_uuid IN ( ON hos.host_uuid IN (
anv.anvil_node1_host_uuid, anv.anvil_node1_host_uuid,
anv.anvil_node2_host_uuid, anv.anvil_node2_host_uuid,
anv.anvil_dr1_host_uuid anv.anvil_dr1_host_uuid
) )
ORDER BY anv.anvil_uuid;`; ORDER BY anv.anvil_uuid;`;
if (buildQueryOptions) { if (buildQueryOptions) {
buildQueryOptions.afterQueryReturn = (queryStdout) => { buildQueryOptions.afterQueryReturn = (queryStdout) => {
@ -77,5 +77,3 @@ const getAnvil: RequestHandler = buildGetRequestHandler(
return query; return query;
}, },
); );
export default getAnvil;

@ -0,0 +1,74 @@
import { RequestHandler } from 'express';
import { query } from '../../accessModule';
import { stderr } from '../../shell';
export const getAnvilCpu: RequestHandler<AnvilDetailParamsDictionary> = async (
request,
response,
) => {
const {
params: { anvilUuid },
} = request;
let rCores: null | string;
let rThreads: null | string;
try {
[[rCores = '', rThreads = '']] = await query<
[[cpuCores: null | string, cpuThreads: null | string]]
>(
`SELECT
MIN(c.scan_hardware_cpu_cores) AS cores,
MIN(c.scan_hardware_cpu_threads) AS threads
FROM anvils AS a
JOIN hosts AS b
ON b.host_uuid IN (
a.anvil_node1_host_uuid,
a.anvil_node2_host_uuid,
a.anvil_dr1_host_uuid
)
JOIN scan_hardware AS c
ON b.host_uuid = c.scan_hardware_host_uuid
WHERE a.anvil_uuid = '${anvilUuid}';`,
);
} catch (error) {
stderr(`Failed to get anvil ${anvilUuid} cpu info; CAUSE: ${error}`);
return response.status(500).send();
}
const cores = Number.parseInt(rCores);
const threads = Number.parseInt(rThreads);
let rAllocated: null | string;
try {
[[rAllocated = '']] = await query<[[cpuAllocated: null | string]]>(
`SELECT
SUM(
CAST(
SUBSTRING(
b.server_definition_xml, 'cores=''([\\d]*)'''
) AS INTEGER
)
) AS allocated
FROM servers AS a
JOIN server_definitions AS b
ON a.server_uuid = b.server_definition_server_uuid
WHERE a.server_anvil_uuid = '${anvilUuid}';`,
);
} catch (error) {
stderr(`Failed to get anvil ${anvilUuid} server cpu info; CAUSE: ${error}`);
return response.status(500).send();
}
const allocated = Number.parseInt(rAllocated);
response.status(200).send({
allocated,
cores,
threads,
});
};

@ -0,0 +1,45 @@
import { RequestHandler } from 'express';
import { getAnvilData, getHostData } from '../../accessModule';
import { stderr } from '../../shell';
import { buildAnvilSummary } from './buildAnvilSummary';
export const getAnvilDetail: RequestHandler<
AnvilDetailParamsDictionary,
AnvilDetailSummary,
undefined
> = async (request, response) => {
const {
params: { anvilUuid },
} = request;
let anvils: AnvilDataAnvilListHash;
let hosts: AnvilDataHostListHash;
try {
anvils = await getAnvilData();
hosts = await getHostData();
} catch (error) {
stderr(`Failed to get anvil and/or host data; CAUSE: ${error}`);
return response.status(500).send();
}
let result: AnvilDetailSummary;
try {
result = await buildAnvilSummary({
anvils,
anvilUuid,
hosts,
});
} catch (error) {
stderr(
`Failed to get summary of anvil node pair ${anvilUuid}; CAUSE: ${error}`,
);
return response.status(500).send();
}
response.status(200).send(result);
};

@ -0,0 +1,121 @@
import { RequestHandler } from 'express';
import { DataSizeUnit, dSize } from 'format-data-size';
import { NODE_AND_DR_RESERVED_MEMORY_SIZE } from '../../consts';
import { query } from '../../accessModule';
import { stderr } from '../../shell';
export const getAnvilMemory: RequestHandler<
AnvilDetailParamsDictionary
> = async (request, response) => {
const {
params: { anvilUuid },
} = request;
let hostMemoryRows: [
hostUuid: string,
minMemoryTotal: null | string,
hostMemoryTotal: string,
hostMemoryFree: string,
hostSwapTotal: string,
hostSwapFree: string,
][];
try {
hostMemoryRows = await query(
`SELECT
b.host_uuid,
MIN(c.scan_hardware_ram_total) AS min_memory_total,
c.scan_hardware_ram_total,
c.scan_hardware_memory_free,
c.scan_hardware_swap_total,
c.scan_hardware_swap_free
FROM anvils AS a
JOIN hosts AS b
ON b.host_uuid IN (
a.anvil_node1_host_uuid,
a.anvil_node2_host_uuid,
a.anvil_dr1_host_uuid
)
JOIN scan_hardware AS c
ON b.host_uuid = c.scan_hardware_host_uuid
WHERE a.anvil_uuid = '${anvilUuid}'
GROUP BY
b.host_uuid,
c.scan_hardware_ram_total,
c.scan_hardware_memory_free,
c.scan_hardware_swap_total,
c.scan_hardware_swap_free
ORDER BY b.host_name;`,
);
} catch (error) {
stderr(`Failed to get anvil ${anvilUuid} memory info; CAUSE: ${error}`);
return response.status(500).send();
}
const {
0: { 1: minTotal },
} = hostMemoryRows;
if (minTotal === null) return response.status(404).send();
const hosts: AnvilDetailHostMemory[] =
hostMemoryRows.map<AnvilDetailHostMemory>(
([host_uuid, , total, free, swap_total, swap_free]) => ({
free,
host_uuid,
swap_free,
swap_total,
total,
}),
);
let serverMemoryRows: [serverMemoryValue: string, serverMemoryUnit: string][];
try {
serverMemoryRows = await query(
`SELECT
CAST(
SUBSTRING(
a.server_definition_xml, 'memory.*>([\\d]*)</memory'
) AS BIGINT
) AS server_memory_value,
SUBSTRING(
a.server_definition_xml, 'memory.*unit=''([A-Za-z]*)'''
) AS server_memory_unit
FROM server_definitions AS a
JOIN servers AS b
ON b.server_uuid = a.server_definition_server_uuid
WHERE server_anvil_uuid = '${anvilUuid}';`,
);
} catch (error) {
stderr(`Failed to get anvil ${anvilUuid} server info; CAUSE: ${error}`);
return response.status(500).send();
}
let allocated = '0';
if (serverMemoryRows.length > 0) {
allocated = String(
serverMemoryRows.reduce<bigint>((previous, [mvalue, munit]) => {
const serverMemory =
dSize(mvalue, {
fromUnit: munit as DataSizeUnit,
toUnit: 'B',
})?.value ?? '0';
return previous + BigInt(serverMemory);
}, BigInt(0)),
);
}
return response.status(200).send({
allocated,
hosts,
reserved: String(NODE_AND_DR_RESERVED_MEMORY_SIZE),
total: minTotal,
});
};

@ -0,0 +1,159 @@
import assert from 'assert';
import { RequestHandler } from 'express';
import { REP_UUID } from '../../consts';
import { getAnvilData, getHostData, getNetworkData } from '../../accessModule';
import { sanitize } from '../../sanitize';
import { stderr } from '../../shell';
const degrade = (current: string) =>
current === 'optimal' ? 'degraded' : current;
const compare = (a: string, b: string) => (a > b ? 1 : -1);
const buildSubnodeBonds = (
ifaces: AnvilDataSubnodeNetwork['interface'],
): AnvilDetailSubnodeBond[] => {
const bondList = Object.entries(ifaces)
.sort(([an, { type: at }], [bn, { type: bt }]) => {
const ab = at === 'bond';
const bb = bt === 'bond';
if (ab && bb) return compare(an, bn);
if (ab) return -1;
if (bb) return 1;
return compare(an, bn);
})
.reduce<{
[bondUuid: string]: AnvilDetailSubnodeBond;
}>((previous, [ifname, ifvalue]) => {
const { type } = ifvalue;
if (type === 'bond') {
const { active_interface, uuid: bondUuid } =
ifvalue as AnvilDataHostNetworkBond;
previous[bondUuid] = {
active_interface,
bond_name: ifname,
bond_uuid: bondUuid,
links: [],
};
} else if (type === 'interface') {
const {
bond_uuid: bondUuid,
operational,
speed,
uuid: linkUuid,
} = ifvalue as AnvilDataHostNetworkLink;
// Link without bond UUID can be ignored
if (!REP_UUID.test(bondUuid)) return previous;
const {
[bondUuid]: { active_interface, links },
} = previous;
let linkState: string = operational === 'up' ? 'optimal' : 'down';
links.forEach((xLink) => {
const { link_speed: xlSpeed, link_state: xlState } = xLink;
if (xlSpeed < speed) {
// Seen link is slower than current link, mark seen link as 'degraded'
xLink.link_state = degrade(xlState);
} else if (xlSpeed > speed) {
// Current link is slower than seen link, mark current link as 'degraded'
linkState = degrade(linkState);
}
});
links.push({
is_active: ifname === active_interface,
link_name: ifname,
link_speed: speed,
link_state: linkState,
link_uuid: linkUuid,
});
}
return previous;
}, {});
return Object.values(bondList);
};
export const getAnvilNetwork: RequestHandler<
AnvilDetailParamsDictionary
> = async (request, response) => {
const {
params: { anvilUuid: rAnUuid },
} = request;
const anUuid = sanitize(rAnUuid, 'string', { modifierType: 'sql' });
try {
assert(
REP_UUID.test(anUuid),
`Param UUID must be a valid UUIDv4; got [${anUuid}]`,
);
} catch (error) {
stderr(`Failed to assert value during get anvil network; CAUSE: ${error}`);
return response.status(400).send();
}
let ans: AnvilDataAnvilListHash;
let hosts: AnvilDataHostListHash;
try {
ans = await getAnvilData();
hosts = await getHostData();
} catch (error) {
stderr(`Failed to get anvil and host data; CAUSE: ${error}`);
return response.status(500).send();
}
const {
anvil_uuid: {
[anUuid]: {
anvil_node1_host_uuid: n1uuid,
anvil_node2_host_uuid: n2uuid,
},
},
} = ans;
const rsbody: AnvilDetailNetworkSummary = { hosts: [] };
for (const hostUuid of [n1uuid, n2uuid]) {
try {
const {
host_uuid: {
[hostUuid]: { short_host_name: hostName },
},
} = hosts;
const { [hostName]: subnodeNetwork } = await getNetworkData(
hostUuid,
hostName,
);
const { interface: ifaces } = subnodeNetwork as AnvilDataSubnodeNetwork;
rsbody.hosts.push({
bonds: buildSubnodeBonds(ifaces),
host_name: hostName,
host_uuid: hostUuid,
});
} catch (error) {
stderr(`Failed to get host ${hostUuid} network data; CAUSE: ${error}`);
return response.status(500).send();
}
}
return response.json(rsbody);
};

@ -0,0 +1,67 @@
import assert from 'assert';
import { RequestHandler } from 'express';
import { REP_UUID } from '../../consts';
import { query } from '../../accessModule';
import { sanitize } from '../../sanitize';
import { stderr } from '../../shell';
export const getAnvilStore: RequestHandler<
AnvilDetailParamsDictionary
> = async (request, response) => {
const {
params: { anvilUuid: rAnUuid },
} = request;
const anUuid = sanitize(rAnUuid, 'string', { modifierType: 'sql' });
try {
assert(
REP_UUID.test(anUuid),
`Param UUID must be a valid UUIDv4; got [${anUuid}]`,
);
} catch (error) {
stderr(`Failed to assert value during get anvil storage; CAUSE: ${error}`);
return response.status(400).send();
}
let rows: [uuid: string, name: string, size: string, free: string][];
try {
rows = await query(
`SELECT
DISTINCT ON (b.storage_group_uuid) storage_group_uuid,
b.storage_group_name,
d.scan_lvm_vg_size,
d.scan_lvm_vg_free
FROM anvils AS a
JOIN storage_groups AS b
ON a.anvil_uuid = b.storage_group_anvil_uuid
JOIN storage_group_members AS c
ON b.storage_group_uuid = c.storage_group_member_storage_group_uuid
JOIN scan_lvm_vgs AS d
ON c.storage_group_member_vg_uuid = d.scan_lvm_vg_internal_uuid
WHERE a.anvil_uuid = '${anUuid}'
ORDER BY b.storage_group_uuid, d.scan_lvm_vg_free;`,
);
} catch (error) {
stderr(`Failed to get anvil storage summary; CAUSE: ${error}`);
return response.status(500).send();
}
const rsbody: AnvilDetailStoreSummary = {
storage_groups: rows.map<AnvilDetailStore>(
([sgUuid, sgName, sgSize, sgFree]) => ({
storage_group_free: sgFree,
storage_group_name: sgName,
storage_group_total: sgSize,
storage_group_uuid: sgUuid,
}),
),
};
return response.json(rsbody);
};

@ -0,0 +1,33 @@
import { RequestHandler } from 'express';
import { getAnvilData, getHostData } from '../../accessModule';
import { buildAnvilSummary } from './buildAnvilSummary';
import { stderr } from '../../shell';
export const getAnvilSummary: RequestHandler<unknown, AnvilSummary> = async (
request,
response,
) => {
let anvils: AnvilDataAnvilListHash;
let hosts: AnvilDataHostListHash;
try {
anvils = await getAnvilData();
hosts = await getHostData();
} catch (error) {
stderr(`Failed to get anvil and/or host data; CAUSE: ${error}`);
return response.status(500).send();
}
const { anvil_uuid: alist } = anvils;
const result: AnvilSummary = { anvils: [] };
for (const auuid of Object.keys(alist)) {
result.anvils.push(
await buildAnvilSummary({ anvils, anvilUuid: auuid, hosts }),
);
}
response.json(result);
};

@ -0,0 +1,7 @@
export * from './getAnvil';
export * from './getAnvilCpu';
export * from './getAnvilDetail';
export * from './getAnvilMemory';
export * from './getAnvilNetwork';
export * from './getAnvilStore';
export * from './getAnvilSummary';

@ -1,6 +1,7 @@
import { RequestHandler } from 'express'; import { RequestHandler } from 'express';
import { stdout } from '../../shell'; import { stdout } from '../../shell';
import { cname } from '../../cname';
export const login: RequestHandler<unknown, unknown, AuthLoginRequestBody> = ( export const login: RequestHandler<unknown, unknown, AuthLoginRequestBody> = (
request, request,
@ -12,6 +13,8 @@ export const login: RequestHandler<unknown, unknown, AuthLoginRequestBody> = (
const { name: userName } = user; const { name: userName } = user;
stdout(`Successfully authenticated user [${userName}]`); stdout(`Successfully authenticated user [${userName}]`);
response.cookie(cname('user'), user);
} }
response.status(204).send(); response.status(204).send();

@ -1,17 +1,19 @@
import { RequestHandler } from 'express'; import { RequestHandler } from 'express';
import { cname } from '../../cname';
import { stdout } from '../../shell'; import { stdout } from '../../shell';
export const logout: RequestHandler = (request, response) => { export const logout: RequestHandler = (request, response) => {
request.session.destroy((error) => { request.session.destroy((error) => {
let scode = 204;
if (error) { if (error) {
scode = 500;
stdout(`Failed to destroy session upon logout; CAUSE: ${error}`); stdout(`Failed to destroy session upon logout; CAUSE: ${error}`);
return response.status(500).send();
} }
response.status(scode).send(); response.clearCookie(cname('user'));
response.clearCookie(cname('sid'));
return response.status(204).send();
}); });
}; };

@ -0,0 +1,65 @@
import { AssertionError } from 'assert';
import { RequestHandler } from 'express';
import { sanitize } from '../sanitize';
import { stderr, stdoutVar } from '../shell';
export const buildDeleteRequestHandler =
<
P extends Record<string, string> = Record<string, string>,
ResBody = undefined,
ReqBody extends Record<string, unknown> | undefined = Record<
string,
unknown
>,
ReqQuery = qs.ParsedQs,
Locals extends Record<string, unknown> = Record<string, unknown>,
>({
delete: handleDelete,
key = 'uuid',
listKey = 'uuids',
}: {
delete: (
list: string[],
...handlerArgs: Parameters<
RequestHandler<P, ResBody, ReqBody, ReqQuery, Locals>
>
) => Promise<void>;
key?: string;
listKey?: string;
}): RequestHandler<P, ResBody, ReqBody, ReqQuery, Locals> =>
async (...handlerArgs) => {
const { 0: request, 1: response } = handlerArgs;
const {
body: { [listKey]: rList } = {},
params: { [key]: rId },
} = request;
const list = sanitize(rList, 'string[]');
if (rId !== undefined) {
list.push(rId);
}
stdoutVar(list, `Process delete request with list: `);
try {
await handleDelete(list, ...handlerArgs);
} catch (error) {
let scode;
if (error instanceof AssertionError) {
scode = 400;
stderr(`Failed to assert value during delete request; CAUSE: ${error}`);
} else {
scode = 500;
stderr(`Failed to complete delete request; CAUSE: ${error}`);
}
return response.status(scode).send();
}
return response.status(204).send();
};

@ -14,7 +14,7 @@ const buildGetRequestHandler =
const buildQueryOptions: BuildQueryOptions = {}; const buildQueryOptions: BuildQueryOptions = {};
let result: (number | null | string)[][]; let result: unknown;
try { try {
const sqlscript: string = const sqlscript: string =
@ -26,9 +26,7 @@ const buildGetRequestHandler =
} catch (queryError) { } catch (queryError) {
stderr(`Failed to execute query; CAUSE: ${queryError}`); stderr(`Failed to execute query; CAUSE: ${queryError}`);
response.status(500).send(); return response.status(500).send();
return;
} }
stdoutVar(result, `Query stdout pre-hooks (type=[${typeof result}]): `); stdoutVar(result, `Query stdout pre-hooks (type=[${typeof result}]): `);

@ -1,51 +0,0 @@
import { RequestHandler } from 'express';
import SERVER_PATHS from '../../consts/SERVER_PATHS';
import { job } from '../../accessModule';
import { stderr } from '../../shell';
type DistinctJobParams = Omit<
JobParams,
'file' | 'line' | 'job_data' | 'job_progress'
>;
const MANAGE_HOST_POWER_JOB_PARAMS: {
poweroff: DistinctJobParams;
reboot: DistinctJobParams;
} = {
poweroff: {
job_command: `${SERVER_PATHS.usr.sbin['anvil-manage-power'].self} --poweroff -y`,
job_name: 'poweroff::system',
job_title: 'job_0010',
job_description: 'job_0008',
},
reboot: {
job_command: `${SERVER_PATHS.usr.sbin['anvil-manage-power'].self} --reboot -y`,
job_name: 'reboot::system',
job_title: 'job_0009',
job_description: 'job_0006',
},
};
export const buildHostPowerHandler: (
task?: 'poweroff' | 'reboot',
) => RequestHandler =
(task = 'reboot') =>
async (request, response) => {
const subParams: JobParams = {
file: __filename,
...MANAGE_HOST_POWER_JOB_PARAMS[task],
};
try {
await job(subParams);
} catch (subError) {
stderr(`Failed to ${task} host; CAUSE: ${subError}`);
return response.status(500).send();
}
response.status(204).send();
};

@ -0,0 +1,123 @@
import assert from 'assert';
import { RequestHandler } from 'express';
import { REP_UUID, SERVER_PATHS } from '../../consts';
import { job, query } from '../../accessModule';
import { sanitize } from '../../sanitize';
import { stderr } from '../../shell';
const MAP_TO_MEMBERSHIP_JOB_PARAMS_BUILDER: Record<
MembershipTask,
BuildMembershipJobParamsFunction
> = {
join: async (uuid, { isActiveMember } = {}) => {
// Host is already a cluster member
if (isActiveMember) return undefined;
const rows: [[number]] = await query(
`SELECT
CASE
WHEN host_status = 'online'
THEN CAST('1' AS BOOLEAN)
ELSE CAST('0' AS BOOLEAN)
END
FROM hosts WHERE host_uuid = '${uuid}';`,
);
assert.ok(rows.length, 'No entry found');
const [[isOnline]] = rows;
return isOnline
? {
job_command: SERVER_PATHS.usr.sbin['anvil-safe-start'].self,
job_description: 'job_0337',
job_host_uuid: uuid,
job_name: 'set_membership::join',
job_title: 'job_0336',
}
: undefined;
},
leave: async (uuid, { isActiveMember } = {}) =>
isActiveMember
? {
job_command: SERVER_PATHS.usr.sbin['anvil-safe-stop'].self,
job_description: 'job_0339',
job_host_uuid: uuid,
job_name: 'set_membership::leave',
job_title: 'job_0338',
}
: undefined,
};
export const buildMembershipHandler: (
task: MembershipTask,
) => RequestHandler<{ uuid: string }> = (task) => async (request, response) => {
const {
params: { uuid },
} = request;
const hostUuid = sanitize(uuid, 'string', { modifierType: 'sql' });
try {
assert(
REP_UUID.test(hostUuid),
`Param UUID must be a valid UUIDv4; got: [${hostUuid}]`,
);
} catch (error) {
stderr(
`Failed to assert value when changing host membership; CAUSE: ${error}`,
);
return response.status(500).send();
}
let rows: [
[
hostInCcm: NumberBoolean,
hostCrmdMember: NumberBoolean,
hostClusterMember: NumberBoolean,
],
];
try {
rows = await query(
`SELECT
scan_cluster_node_in_ccm,
scan_cluster_node_crmd_member,
scan_cluster_node_cluster_member
FROM scan_cluster_nodes
WHERE scan_cluster_node_host_uuid = '${hostUuid}';`,
);
assert.ok(rows.length, `No entry found`);
} catch (error) {
stderr(`Failed to get cluster status of host ${hostUuid}; CAUSE: ${error}`);
return response.status(500).send();
}
const isActiveMember = rows[0].every((stage) => Boolean(stage));
try {
const restParams = await MAP_TO_MEMBERSHIP_JOB_PARAMS_BUILDER[task](
hostUuid,
{
isActiveMember,
},
);
if (restParams) {
await job({ file: __filename, ...restParams });
}
} catch (error) {
stderr(
`Failed to initiate ${task} cluster for host ${hostUuid}; CAUSE: ${error}`,
);
return response.status(500).send();
}
return response.status(204).send();
};

@ -0,0 +1,151 @@
import assert from 'assert';
import { RequestHandler } from 'express';
import { LOCAL, REP_UUID, SERVER_PATHS } from '../../consts';
import { job, query } from '../../accessModule';
import { sanitize } from '../../sanitize';
import { stderr } from '../../shell';
/**
* Notes on power functions:
* * poweroff, reboot targets the striker this express app operates on
* * start, stop targets subnode or DR host
*/
const MAP_TO_POWER_JOB_PARAMS_BUILDER: Record<
PowerTask,
BuildPowerJobParamsFunction
> = {
poweroff: () => ({
job_command: `${SERVER_PATHS.usr.sbin['anvil-manage-power'].self} --poweroff -y`,
job_description: 'job_0008',
job_name: 'poweroff::system',
job_title: 'job_0010',
}),
reboot: () => ({
job_command: `${SERVER_PATHS.usr.sbin['anvil-manage-power'].self} --reboot -y`,
job_description: 'job_0006',
job_name: 'reboot::system',
job_title: 'job_0009',
}),
start: ({ uuid } = {}) => ({
job_command: `${SERVER_PATHS.usr.sbin['striker-boot-machine'].self} --host-uuid '${uuid}'`,
job_description: 'job_0335',
job_name: `set_power::on`,
job_title: 'job_0334',
}),
startserver: ({ uuid } = {}) => ({
job_command: `${SERVER_PATHS.usr.sbin['anvil-boot-server'].self} --server-uuid '${uuid}'`,
job_description: 'job_0341',
job_name: 'set_power::server::on',
job_title: 'job_0340',
}),
stop: ({ isStopServers, uuid } = {}) => ({
job_command: `${SERVER_PATHS.usr.sbin['anvil-safe-stop'].self} --power-off${
isStopServers ? ' --stop-servers' : ''
}`,
job_description: 'job_0333',
job_host_uuid: uuid,
job_name: 'set_power::off',
job_title: 'job_0332',
}),
stopserver: ({ uuid } = {}) => ({
job_command: `${SERVER_PATHS.usr.sbin['anvil-shutdown-server'].self} --server-uuid '${uuid}'`,
job_description: 'job_0343',
job_name: 'set_power::server::off',
job_title: 'job_0342',
}),
};
const queuePowerJob = async (
task: PowerTask,
options?: BuildPowerJobParamsOptions,
) => {
const params: JobParams = {
file: __filename,
...MAP_TO_POWER_JOB_PARAMS_BUILDER[task](options),
};
return await job(params);
};
export const buildPowerHandler: (
task: PowerTask,
) => RequestHandler<{ uuid?: string }> =
(task) => async (request, response) => {
const {
params: { uuid },
} = request;
try {
if (uuid) {
assert(
REP_UUID.test(uuid),
`Param UUID must be a valid UUIDv4; got [${uuid}]`,
);
}
} catch (error) {
stderr(`Failed to ${task}; CAUSE: ${error}`);
return response.status(400).send();
}
try {
await queuePowerJob(task, { uuid });
} catch (error) {
stderr(`Failed to ${task} ${uuid ?? LOCAL}; CAUSE: ${error}`);
return response.status(500).send();
}
response.status(204).send();
};
export const buildAnPowerHandler: (
task: Extract<PowerTask, 'start' | 'stop'>,
) => RequestHandler<{ uuid: string }> = (task) => async (request, response) => {
const {
params: { uuid },
} = request;
const anUuid = sanitize(uuid, 'string', { modifierType: 'sql' });
try {
assert(
REP_UUID.test(anUuid),
`Param UUID must be a valid UUIDv4; got: [${anUuid}]`,
);
} catch (error) {
stderr(`Failed to assert value during power operation on anvil subnode`);
return response.status(400).send();
}
let rows: [[node1Uuid: string, node2Uuid: string]];
try {
rows = await query(
`SELECT anvil_node1_host_uuid, anvil_node2_host_uuid
FROM anvils WHERE anvil_uuid = '${anUuid}';`,
);
assert.ok(rows.length, 'No entry found');
} catch (error) {
stderr(`Failed to get anvil subnodes' UUID; CAUSE: ${error}`);
return response.status(500).send();
}
for (const hostUuid of rows[0]) {
try {
await queuePowerJob(task, { isStopServers: true, uuid: hostUuid });
} catch (error) {
stderr(`Failed to ${task} host ${hostUuid}; CAUSE: ${error}`);
return response.status(500).send();
}
}
return response.status(204).send();
};

@ -1,5 +1,15 @@
export * from './getHostSSH'; export * from './getHostSSH';
export * from './poweroffHost'; export * from './joinAn';
export * from './rebootHost'; export * from './leaveAn';
export * from './manageVncSshTunnel';
export * from './poweroffStriker';
export * from './rebootStriker';
export * from './runManifest'; export * from './runManifest';
export * from './setMapNetwork';
export * from './startAn';
export * from './startServer';
export * from './startSubnode';
export * from './stopAn';
export * from './stopServer';
export * from './stopSubnode';
export * from './updateSystem'; export * from './updateSystem';

@ -0,0 +1,3 @@
import { buildMembershipHandler } from './buildMembershipHandler';
export const joinAn = buildMembershipHandler('join');

@ -0,0 +1,3 @@
import { buildMembershipHandler } from './buildMembershipHandler';
export const leaveAn = buildMembershipHandler('leave');

@ -0,0 +1,52 @@
import assert from 'assert';
import { RequestHandler } from 'express';
import { REP_UUID } from '../../consts';
import { vncpipe } from '../../accessModule';
import { sanitize } from '../../sanitize';
import { stderr, stdoutVar } from '../../shell';
export const manageVncSshTunnel: RequestHandler<
unknown,
{ forwardPort: number; protocol: string },
{ open: boolean; serverUuid: string }
> = async (request, response) => {
const { body: { open: rOpen, serverUuid: rServerUuid } = {} } = request;
const isOpen = sanitize(rOpen, 'boolean');
const serverUuid = sanitize(rServerUuid, 'string');
try {
assert(
REP_UUID.test(serverUuid),
`Server UUID must be a valid UUIDv4; got: [${serverUuid}]`,
);
} catch (error) {
stderr(`Assert input failed when manage VNC SSH tunnel; CAUSE: ${error}`);
return response.status(400).send();
}
stdoutVar({ isOpen, serverUuid }, 'Manage VNC SSH tunnel params: ');
let operation = 'close';
if (isOpen) {
operation = 'open';
}
let rsbody: { forwardPort: number; protocol: string };
try {
rsbody = await vncpipe(serverUuid, isOpen);
} catch (error) {
stderr(
`Failed to ${operation} VNC SSH tunnel to server ${serverUuid}; CAUSE: ${error}`,
);
return response.status(500).send();
}
return response.json(rsbody);
};

@ -1,3 +0,0 @@
import { buildHostPowerHandler } from './buildHostPowerHandler';
export const poweroffHost = buildHostPowerHandler('poweroff');

@ -0,0 +1,3 @@
import { buildPowerHandler } from './buildPowerHandler';
export const poweroffStriker = buildPowerHandler('poweroff');

@ -1,3 +0,0 @@
import { buildHostPowerHandler } from './buildHostPowerHandler';
export const rebootHost = buildHostPowerHandler('reboot');

@ -0,0 +1,3 @@
import { buildPowerHandler } from './buildPowerHandler';
export const rebootStriker = buildPowerHandler('reboot');

@ -0,0 +1,70 @@
import assert from 'assert';
import { RequestHandler } from 'express';
import { LOCAL, REP_UUID } from '../../consts';
import { variable } from '../../accessModule';
import { toHostUUID } from '../../convertHostUUID';
import { sanitize } from '../../sanitize';
import { stderr, stdoutVar } from '../../shell';
export const setMapNetwork: RequestHandler<
{ uuid: string },
undefined,
SetMapNetworkRequestBody
> = async (request, response) => {
const {
body: { value: rValue } = {},
params: { uuid: rHostUuid },
} = request;
const value = sanitize(rValue, 'number');
let hostUuid = sanitize(rHostUuid, 'string', { fallback: LOCAL });
hostUuid = toHostUUID(hostUuid);
stdoutVar({ hostUuid, value }, `Set map network variable with: `);
try {
assert(
value in [0, 1],
`Variable value must be a number boolean (0 or 1); got [${value}]`,
);
assert(
REP_UUID.test(hostUuid),
`Host UUID must be a valid UUIDv4; got [${hostUuid}]`,
);
} catch (error) {
stderr(`Assert failed when set map network variable; CAUSE: ${error}`);
return response.status(400).send();
}
try {
const result = await variable({
file: __filename,
variable_default: 0,
varaible_description: 'striker_0202',
variable_name: 'config::map_network',
variable_section: 'config',
variable_source_table: 'hosts',
variable_source_uuid: hostUuid,
variable_value: value,
});
assert(
REP_UUID.test(result),
`Result must be UUID of modified record; got: [${result}]`,
);
} catch (error) {
stderr(
`Failed to set map network variable for host ${hostUuid}; CAUSE: ${error}`,
);
return response.status(500).send();
}
return response.status(204).send();
};

@ -0,0 +1,3 @@
import { buildAnPowerHandler } from './buildPowerHandler';
export const startAn = buildAnPowerHandler('start');

@ -0,0 +1,3 @@
import { buildPowerHandler } from './buildPowerHandler';
export const startServer = buildPowerHandler('startserver');

@ -0,0 +1,3 @@
import { buildPowerHandler } from './buildPowerHandler';
export const startSubnode = buildPowerHandler('start');

@ -0,0 +1,3 @@
import { buildAnPowerHandler } from './buildPowerHandler';
export const stopAn = buildAnPowerHandler('stop');

@ -0,0 +1,3 @@
import { buildPowerHandler } from './buildPowerHandler';
export const stopServer = buildPowerHandler('stopserver');

@ -0,0 +1,3 @@
import { buildPowerHandler } from './buildPowerHandler';
export const stopSubnode = buildPowerHandler('stop');

@ -0,0 +1,138 @@
import assert from 'assert';
import { RequestHandler } from 'express';
import { REP_PEACEFUL_STRING } from '../../consts';
import { getFenceSpec, timestamp, write } from '../../accessModule';
import { sanitize } from '../../sanitize';
import { stderr, stdoutVar, uuid } from '../../shell';
const handleNumberType = (v: unknown) => String(sanitize(v, 'number'));
const handleStringType = (v: unknown) => sanitize(v, 'string');
const MAP_TO_VAR_TYPE: Record<
AnvilDataFenceParameterType,
(v: unknown) => string
> = {
boolean: (v) => (sanitize(v, 'boolean') ? '1' : ''),
integer: handleNumberType,
second: handleNumberType,
select: handleStringType,
string: handleStringType,
};
export const createFence: RequestHandler<
{ uuid?: string },
undefined,
{
agent: string;
name: string;
parameters: { [parameterId: string]: string };
}
> = async (request, response) => {
const {
body: { agent: rAgent, name: rName, parameters: rParameters },
params: { uuid: rUuid },
} = request;
let fenceSpec: AnvilDataFenceHash;
try {
fenceSpec = await getFenceSpec();
} catch (error) {
stderr(`Failed to get fence devices specification; CAUSE: ${error}`);
return response.status(500).send();
}
const agent = sanitize(rAgent, 'string');
const name = sanitize(rName, 'string');
const fenceUuid = sanitize(rUuid, 'string', { fallback: uuid() });
const { [agent]: agentSpec } = fenceSpec;
try {
assert.ok(agentSpec, `Agent is unknown to spec; got [${agent}]`);
assert(
REP_PEACEFUL_STRING.test(name),
`Name must be a peaceful string; got [${name}]`,
);
const rParamsType = typeof rParameters;
assert(
rParamsType === 'object',
`Parameters must be an object; got type [${rParamsType}]`,
);
} catch (error) {
assert(
`Failed to assert value when working with fence device; CAUSE: ${error}`,
);
return response.status(400).send();
}
const { parameters: agentSpecParams } = agentSpec;
const args = Object.entries(agentSpecParams)
.reduce<string[]>((previous, [paramId, paramSpec]) => {
const { content_type: paramType, default: paramDefault } = paramSpec;
const { [paramId]: rParamValue } = rParameters;
if (
[paramDefault, '', null, undefined].some((bad) => rParamValue === bad)
)
return previous;
// TODO: add SQL modifier after finding a way to escape single quotes
const paramValue = MAP_TO_VAR_TYPE[paramType](rParamValue);
previous.push(`${paramId}="${paramValue}"`);
return previous;
}, [])
.join(' ');
stdoutVar(
{ agent, args, name },
`Proceed to record fence device (${fenceUuid}): `,
);
const modifiedDate = timestamp();
try {
const wcode = await write(
`INSERT INTO
fences (
fence_uuid,
fence_name,
fence_agent,
fence_arguments,
modified_date
) VALUES (
'${fenceUuid}',
'${name}',
'${agent}',
'${args}',
'${modifiedDate}'
) ON CONFLICT (fence_uuid)
DO UPDATE SET
fence_name = '${name}',
fence_agent = '${agent}',
fence_arguments = '${args}',
modified_date = '${modifiedDate}';`,
);
assert(wcode === 0, `Write exited with code ${wcode}`);
} catch (error) {
stderr(`Failed to write fence record; CAUSE: ${error}`);
return response.status(500).send();
}
const scode = rUuid ? 200 : 201;
return response.status(scode).send();
};

@ -0,0 +1,20 @@
import { DELETED } from '../../consts';
import { write } from '../../accessModule';
import { buildDeleteRequestHandler } from '../buildDeleteRequestHandler';
import join from '../../join';
export const deleteFence = buildDeleteRequestHandler({
delete: async (fenceUuids) => {
const wcode = await write(
`UPDATE fences
SET fence_arguments = '${DELETED}'
WHERE fence_uuid IN (${join(fenceUuids, {
elementWrapper: "'",
separator: ',',
})});`,
);
if (wcode !== 0) throw Error(`Write exited with code ${wcode}`);
},
});

@ -1,5 +1,7 @@
import { RequestHandler } from 'express'; import { RequestHandler } from 'express';
import { DELETED } from '../../consts';
import buildGetRequestHandler from '../buildGetRequestHandler'; import buildGetRequestHandler from '../buildGetRequestHandler';
import { buildQueryResultReducer } from '../../buildQueryResultModifier'; import { buildQueryResultReducer } from '../../buildQueryResultModifier';
import { stdout } from '../../shell'; import { stdout } from '../../shell';
@ -13,7 +15,9 @@ export const getFence: RequestHandler = buildGetRequestHandler(
fence_agent, fence_agent,
fence_arguments fence_arguments
FROM fences FROM fences
WHERE fence_arguments != '${DELETED}'
ORDER BY fence_name ASC;`; ORDER BY fence_name ASC;`;
const afterQueryReturn: QueryResultModifierFunction | undefined = const afterQueryReturn: QueryResultModifierFunction | undefined =
buildQueryResultReducer<{ [fenceUUID: string]: FenceOverview }>( buildQueryResultReducer<{ [fenceUUID: string]: FenceOverview }>(
(previous, [fenceUUID, fenceName, fenceAgent, fenceArgumentString]) => { (previous, [fenceUUID, fenceName, fenceAgent, fenceArgumentString]) => {

@ -4,10 +4,10 @@ import { getFenceSpec } from '../../accessModule';
import { stderr } from '../../shell'; import { stderr } from '../../shell';
export const getFenceTemplate: RequestHandler = async (request, response) => { export const getFenceTemplate: RequestHandler = async (request, response) => {
let rawFenceData; let rFenceData: AnvilDataFenceHash;
try { try {
rawFenceData = await getFenceSpec(); rFenceData = await getFenceSpec();
} catch (subError) { } catch (subError) {
stderr(`Failed to get fence device template; CAUSE: ${subError}`); stderr(`Failed to get fence device template; CAUSE: ${subError}`);
@ -16,5 +16,5 @@ export const getFenceTemplate: RequestHandler = async (request, response) => {
return; return;
} }
response.status(200).send(rawFenceData); response.status(200).send(rFenceData);
}; };

@ -1,2 +1,5 @@
export * from './createFence';
export * from './deleteFence';
export * from './getFence'; export * from './getFence';
export * from './getFenceTemplate'; export * from './getFenceTemplate';
export * from './updateFence';

@ -0,0 +1,3 @@
import { createFence } from './createFence';
export const updateFence = createFence;

@ -30,12 +30,20 @@ export const buildQueryFileDetail = ({
fil_loc.file_location_active, fil_loc.file_location_active,
anv.anvil_uuid, anv.anvil_uuid,
anv.anvil_name, anv.anvil_name,
anv.anvil_description anv.anvil_description,
hos.host_uuid,
hos.host_name
FROM files AS fil FROM files AS fil
JOIN file_locations AS fil_loc JOIN file_locations AS fil_loc
ON fil.file_uuid = fil_loc.file_location_file_uuid ON fil.file_uuid = fil_loc.file_location_file_uuid
JOIN anvils AS anv JOIN anvils AS anv
ON fil_loc.file_location_anvil_uuid = anv.anvil_uuid ON fil_loc.file_location_host_uuid IN (
anv.anvil_node1_host_uuid,
anv.anvil_node2_host_uuid,
anv.anvil_dr1_host_uuid
)
JOIN hosts AS hos
ON fil_loc.file_location_host_uuid = hos.host_uuid
WHERE fil.file_type != '${DELETED}' WHERE fil.file_type != '${DELETED}'
${condFileUUIDs};`; ${condFileUUIDs};`;
}; };

@ -1,3 +1,4 @@
import assert from 'assert';
import { RequestHandler } from 'express'; import { RequestHandler } from 'express';
import { anvilSyncShared, query, timestamp, write } from '../../accessModule'; import { anvilSyncShared, query, timestamp, write } from '../../accessModule';
@ -84,34 +85,26 @@ export const updateFile: RequestHandler = async (request, response) => {
modified_date = '${timestamp()}' modified_date = '${timestamp()}'
WHERE file_location_uuid = '${fileLocationUUID}';`; WHERE file_location_uuid = '${fileLocationUUID}';`;
const targetHosts: [ // Each file location entry is for 1 host.
n1uuid: string, const rows = await query<[[string]]>(
n2uuid: string, `SELECT file_location_host_uuid
dr1uuid: null | string, FROM file_locations
][] = await query( WHERE file_location_uuid = '${fileLocationUUID}';`,
`SELECT
anv.anvil_node1_host_uuid,
anv.anvil_node2_host_uuid,
anv.anvil_dr1_host_uuid
FROM anvils AS anv
JOIN file_locations AS fil_loc
ON anv.anvil_uuid = fil_loc.file_location_anvil_uuid
WHERE fil_loc.file_location_uuid = '${fileLocationUUID}';`,
); );
targetHosts.flat().forEach((hostUUID: null | string) => { if (rows.length) {
if (hostUUID) { const [[hostUuid]] = rows;
anvilSyncSharedFunctions.push(() =>
anvilSyncShared( anvilSyncSharedFunctions.push(() =>
jobName, anvilSyncShared(
`file_uuid=${fileUUID}`, jobName,
jobTitle, `file_uuid=${fileUUID}`,
jobDescription, jobTitle,
{ jobHostUUID: hostUUID }, jobDescription,
), { jobHostUUID: hostUuid },
); ),
} );
}); }
}, },
); );
} }
@ -120,6 +113,8 @@ export const updateFile: RequestHandler = async (request, response) => {
try { try {
wcode = await write(sqlscript); wcode = await write(sqlscript);
assert(wcode === 0, `Write exited with code ${wcode}`);
} catch (queryError) { } catch (queryError) {
stderr(`Failed to execute query; CAUSE: ${queryError}`); stderr(`Failed to execute query; CAUSE: ${queryError}`);
@ -130,5 +125,5 @@ export const updateFile: RequestHandler = async (request, response) => {
stdoutVar(await fn(), `Anvil sync shared [${index}] output: `), stdoutVar(await fn(), `Anvil sync shared [${index}] output: `),
); );
response.status(200).send(wcode); response.status(200).send();
}; };

@ -1,45 +1,82 @@
import { buildKnownIDCondition } from '../../buildCondition'; import { buildKnownIDCondition } from '../../buildCondition';
import { buildQueryResultModifier } from '../../buildQueryResultModifier'; import { buildQueryResultModifier } from '../../buildQueryResultModifier';
import { cap } from '../../cap'; import { camel } from '../../camel';
import { getShortHostName } from '../../disassembleHostName'; import { getShortHostName } from '../../disassembleHostName';
import { stdout } from '../../shell'; import { stdout } from '../../shell';
type ExtractVariableKeyFunction = (parts: string[]) => string; const CVAR_PREFIX = 'form::config_step';
const CVAR_PREFIX_PATTERN = `^${CVAR_PREFIX}\\d+::`;
const MAP_TO_EXTRACTOR: { [prefix: string]: ExtractVariableKeyFunction } = { const MAP_TO_EXTRACTOR: Record<string, (parts: string[]) => string[]> = {
form: ([, part2]) => { form: ([, part2]) => {
const [head, ...rest] = part2.split('_'); const [rHead, ...rest] = part2.split('_');
const head = rHead.toLowerCase();
return rest.reduce<string>( return /^[a-z]+n[0-9]+/.test(head)
(previous, part) => `${previous}${cap(part)}`, ? ['networks', head, camel(...rest)]
head, : [camel(head, ...rest)];
);
}, },
'install-target': () => 'installTarget', 'install-target': () => ['installTarget'],
};
const setCvar = (
keychain: string[],
value: string,
parent: Tree = {},
): Tree | string => {
const { 0: key, length } = keychain;
if (!key) return value;
const next = 1;
const { [key]: xv } = parent;
parent[key] =
next < length && typeof xv !== 'string'
? setCvar(keychain.slice(next), value, xv)
: value;
return parent;
}; };
export const buildQueryHostDetail: BuildQueryDetailFunction = ({ export const buildQueryHostDetail: BuildQueryDetailFunction = ({
keys: hostUUIDs = '*', keys: hostUUIDs = '*',
} = {}) => { } = {}) => {
const condHostUUIDs = buildKnownIDCondition(hostUUIDs, 'AND hos.host_uuid'); const condHostUUIDs = buildKnownIDCondition(hostUUIDs, 'AND b.host_uuid');
stdout(`condHostUUIDs=[${condHostUUIDs}]`); stdout(`condHostUUIDs=[${condHostUUIDs}]`);
const query = ` const query = `
SELECT SELECT
hos.host_name, b.host_name,
hos.host_type, b.host_type,
hos.host_uuid, b.host_uuid,
var.variable_name, a.variable_name,
var.variable_value a.variable_value,
FROM variables AS var SUBSTRING(
JOIN hosts AS hos a.variable_name, '${CVAR_PREFIX_PATTERN}([^:]+)'
ON var.variable_source_uuid = hos.host_uuid ) as cvar_name,
SUBSTRING(
a.variable_name, '${CVAR_PREFIX_PATTERN}([a-z]{2,3})\\d+'
) AS network_type,
SUBSTRING(
a.variable_name, '${CVAR_PREFIX_PATTERN}[a-z]{2,3}\\d+_(link\\d+)'
) AS network_link,
c.network_interface_uuid
FROM variables AS a
JOIN hosts AS b
ON a.variable_source_uuid = b.host_uuid
LEFT JOIN network_interfaces AS c
ON a.variable_name LIKE '%link%_mac%'
AND a.variable_value = c.network_interface_mac_address
AND b.host_uuid = c.network_interface_host_uuid
WHERE ( WHERE (
variable_name LIKE 'form::config_%' variable_name LIKE '${CVAR_PREFIX}%'
OR variable_name = 'install-target::enabled' OR variable_name = 'install-target::enabled'
) )
${condHostUUIDs};`; ${condHostUUIDs}
ORDER BY cvar_name ASC,
a.variable_name ASC;`;
const afterQueryReturn: QueryResultModifierFunction = const afterQueryReturn: QueryResultModifierFunction =
buildQueryResultModifier((output) => { buildQueryResultModifier((output) => {
@ -52,14 +89,37 @@ export const buildQueryHostDetail: BuildQueryDetailFunction = ({
hostType: string; hostType: string;
hostUUID: string; hostUUID: string;
shortHostName: string; shortHostName: string;
} & Record<string, string> } & Tree
>( >(
(previous, [, , , variableName, variableValue]) => { (
previous,
[
,
,
,
variableName,
variableValue,
,
networkType,
networkLink,
networkInterfaceUuid,
],
) => {
const [variablePrefix, ...restVariableParts] = const [variablePrefix, ...restVariableParts] =
variableName.split('::'); variableName.split('::');
const key = MAP_TO_EXTRACTOR[variablePrefix](restVariableParts); const keychain = MAP_TO_EXTRACTOR[variablePrefix](restVariableParts);
setCvar(keychain, variableValue, previous);
if (networkLink) {
keychain[keychain.length - 1] = `${networkLink}Uuid`;
setCvar(keychain, networkInterfaceUuid, previous);
} else if (networkType) {
keychain[keychain.length - 1] = 'type';
previous[key] = variableValue; setCvar(keychain, networkType, previous);
}
return previous; return previous;
}, },

@ -6,36 +6,16 @@ import {
REP_IPV4, REP_IPV4,
REP_IPV4_CSV, REP_IPV4_CSV,
REP_PEACEFUL_STRING, REP_PEACEFUL_STRING,
REP_UUID,
SERVER_PATHS, SERVER_PATHS,
} from '../../consts'; } from '../../consts';
import { job } from '../../accessModule'; import { getLocalHostUUID, job, variable } from '../../accessModule';
import { buildJobData } from '../../buildJobData';
import { buildNetworkConfig } from '../../fconfig';
import { sanitize } from '../../sanitize'; import { sanitize } from '../../sanitize';
import { stderr, stdoutVar } from '../../shell'; import { stderr, stdoutVar } from '../../shell';
import { cvar } from '../../varn';
const fvar = (configStepCount: number, fieldName: string) =>
['form', `config_step${configStepCount}`, fieldName, 'value'].join('::');
const buildNetworkLinks = (
configStepCount: number,
networkShortName: string,
interfaces: InitializeStrikerNetworkForm['interfaces'],
) =>
interfaces.reduce<string>((reduceContainer, iface, index) => {
let result = reduceContainer;
if (iface) {
const { networkInterfaceMACAddress } = iface;
result += `
${fvar(
configStepCount,
`${networkShortName}_link${index + 1}_mac_to_set`,
)}=${networkInterfaceMACAddress}`;
}
return result;
}, '');
export const configStriker: RequestHandler< export const configStriker: RequestHandler<
unknown, unknown,
@ -47,23 +27,25 @@ export const configStriker: RequestHandler<
stdoutVar(body, 'Begin initialize Striker; body='); stdoutVar(body, 'Begin initialize Striker; body=');
const { const {
adminPassword: rAdminPassword = '', adminPassword: rAdminPassword,
domainName: rDomainName = '', domainName: rDomainName,
hostName: rHostName = '', hostName: rHostName,
hostNumber: rHostNumber = 0, hostNumber: rHostNumber,
networkDNS: rNetworkDns = '', dns: rDns,
networkGateway: rNetworkGateway = '', gateway: rGateway,
gatewayInterface: rGatewayInterface,
networks = [], networks = [],
organizationName: rOrganizationName = '', organizationName: rOrganizationName,
organizationPrefix: rOrganizationPrefix = '', organizationPrefix: rOrganizationPrefix,
} = body; } = body;
const adminPassword = sanitize(rAdminPassword, 'string'); const adminPassword = sanitize(rAdminPassword, 'string');
const domainName = sanitize(rDomainName, 'string'); const domainName = sanitize(rDomainName, 'string');
const hostName = sanitize(rHostName, 'string'); const hostName = sanitize(rHostName, 'string');
const hostNumber = sanitize(rHostNumber, 'number'); const hostNumber = sanitize(rHostNumber, 'number');
const networkDns = sanitize(rNetworkDns, 'string'); const dns = sanitize(rDns, 'string');
const networkGateway = sanitize(rNetworkGateway, 'string'); const gateway = sanitize(rGateway, 'string');
const gatewayInterface = sanitize(rGatewayInterface, 'string');
const organizationName = sanitize(rOrganizationName, 'string'); const organizationName = sanitize(rOrganizationName, 'string');
const organizationPrefix = sanitize(rOrganizationPrefix, 'string'); const organizationPrefix = sanitize(rOrganizationPrefix, 'string');
@ -89,17 +71,22 @@ export const configStriker: RequestHandler<
); );
assert( assert(
REP_IPV4_CSV.test(networkDns), REP_IPV4_CSV.test(dns),
`Data network DNS must be a comma separated list of valid IPv4 addresses; got [${networkDns}]`, `Data network DNS must be a comma separated list of valid IPv4 addresses; got [${dns}]`,
);
assert(
REP_IPV4.test(gateway),
`Data network gateway must be a valid IPv4 address; got [${gateway}]`,
); );
assert( assert(
REP_IPV4.test(networkGateway), REP_PEACEFUL_STRING.test(gatewayInterface),
`Data network gateway must be a valid IPv4 address; got [${networkGateway}]`, `Data gateway interface must be a peaceful string; got [${gatewayInterface}]`,
); );
assert( assert(
REP_PEACEFUL_STRING.test(organizationName), organizationName.length > 0,
`Data organization name cannot be empty; got [${organizationName}]`, `Data organization name cannot be empty; got [${organizationName}]`,
); );
@ -115,40 +102,54 @@ export const configStriker: RequestHandler<
return response.status(400).send(); return response.status(400).send();
} }
const configData: FormConfigData = {
[cvar(1, 'domain')]: { value: domainName },
[cvar(1, 'organization')]: { value: organizationName },
[cvar(1, 'prefix')]: { value: organizationPrefix },
[cvar(1, 'sequence')]: { value: hostNumber },
[cvar(2, 'dns')]: { step: 2, value: dns },
[cvar(2, 'gateway')]: { step: 2, value: gateway },
[cvar(2, 'gateway_interface')]: { step: 2, value: gatewayInterface },
[cvar(2, 'host_name')]: { step: 2, value: hostName },
[cvar(2, 'striker_password')]: { step: 2, value: adminPassword },
[cvar(2, 'striker_user')]: { step: 2, value: 'admin' },
...buildNetworkConfig(networks),
};
stdoutVar(configData, `Config data before initiating striker config: `);
const configEntries = Object.entries(configData);
try { try {
const localHostUuid = getLocalHostUUID();
for (const [ckey, cdetail] of configEntries) {
const { step = 1, value } = cdetail;
const vuuid = await variable({
file: __filename,
variable_default: '',
varaible_description: '',
variable_name: ckey,
variable_section: `config_step${step}`,
variable_source_uuid: localHostUuid,
variable_source_table: 'hosts',
variable_value: value,
});
assert(
REP_UUID.test(vuuid),
`Not a UUIDv4 post insert or update of ${ckey} with [${cdetail}]`,
);
}
await job({ await job({
file: __filename, file: __filename,
job_command: SERVER_PATHS.usr.sbin['anvil-configure-host'].self, job_command: SERVER_PATHS.usr.sbin['anvil-configure-host'].self,
job_data: `${fvar(1, 'domain')}=${domainName} job_data: buildJobData({
${fvar(1, 'organization')}=${organizationName} entries: configEntries,
${fvar(1, 'prefix')}=${organizationPrefix} getValue: ({ value }) => String(value),
${fvar(1, 'sequence')}=${hostNumber} }),
${fvar(2, 'dns')}=${networkDns}
${fvar(2, 'gateway')}=${networkGateway}
${fvar(2, 'host_name')}=${hostName}
${fvar(2, 'striker_password')}=${adminPassword}
${fvar(2, 'striker_user')}=admin${
networks.reduce<{
counters: Record<InitializeStrikerNetworkForm['type'], number>;
result: string;
}>(
(reduceContainer, { interfaces, ipAddress, subnetMask, type }) => {
const { counters } = reduceContainer;
counters[type] = counters[type] ? counters[type] + 1 : 1;
const networkShortName = `${type}${counters[type]}`;
reduceContainer.result += `
${fvar(2, `${networkShortName}_ip`)}=${ipAddress}
${fvar(2, `${networkShortName}_subnet_mask`)}=${subnetMask}
${buildNetworkLinks(2, networkShortName, interfaces)}`;
return reduceContainer;
},
{ counters: {}, result: '' },
).result
}`,
job_name: 'configure::network', job_name: 'configure::network',
job_title: 'job_0001', job_title: 'job_0001',
job_description: 'job_0071', job_description: 'job_0071',

@ -1,8 +1,5 @@
import { RequestHandler } from 'express'; import { RequestHandler } from 'express';
import { buildBranchRequestHandler } from '../buildBranchRequestHandler'; export const createHost: RequestHandler = (request, response) => {
import { configStriker } from './configStriker'; return response.status(204).send();
};
export const createHost: RequestHandler = buildBranchRequestHandler({
striker: configStriker,
});

@ -1,3 +1,4 @@
export * from './configStriker';
export * from './createHost'; export * from './createHost';
export * from './createHostConnection'; export * from './createHostConnection';
export * from './deleteHostConnection'; export * from './deleteHostConnection';

@ -0,0 +1,154 @@
import assert from 'assert';
import { RequestHandler } from 'express';
import {
REP_IPV4,
REP_IPV4_CSV,
REP_PEACEFUL_STRING,
REP_UUID,
SERVER_PATHS,
} from '../../consts';
import { job, query, variable } from '../../accessModule';
import { buildJobData } from '../../buildJobData';
import { buildNetworkConfig } from '../../fconfig';
import { sanitize } from '../../sanitize';
import { stderr, stdoutVar } from '../../shell';
import { cvar } from '../../varn';
export const prepareNetwork: RequestHandler<
UpdateHostParams,
undefined,
PrepareNetworkRequestBody
> = async (request, response) => {
const {
body: {
dns: rDns,
gateway: rGateway,
hostName: rHostName,
gatewayInterface: rGatewayInterface,
networks = [],
} = {},
params: { hostUUID },
} = request;
const dns = sanitize(rDns, 'string');
const gateway = sanitize(rGateway, 'string');
const hostName = sanitize(rHostName, 'string');
const gatewayInterface = sanitize(rGatewayInterface, 'string');
try {
assert(
REP_UUID.test(hostUUID),
`Host UUID must be a valid UUIDv4; got [${hostUUID}]`,
);
assert(
REP_IPV4_CSV.test(dns),
`DNS must be a valid IPv4 CSV; got [${dns}]`,
);
assert(
REP_IPV4.test(gateway),
`Gateway must be a valid IPv4; got [${gateway}]`,
);
assert(
REP_PEACEFUL_STRING.test(hostName),
`Host name must be a peaceful string; got [${hostName}]`,
);
assert(
REP_PEACEFUL_STRING.test(gatewayInterface),
`Gateway interface must be a peaceful string; got [${gatewayInterface}]`,
);
} catch (error) {
stderr(`Failed to assert value when prepare network; CAUSE: ${error}`);
return response.status(400).send();
}
let hostType: string;
try {
const rows = await query<[[string]]>(
`SELECT host_type FROM hosts WHERE host_uuid = '${hostUUID}';`,
);
assert.ok(rows.length, `No record found`);
[[hostType]] = rows;
} catch (error) {
stderr(`Failed to get host type with ${hostUUID}; CAUSE: ${error}`);
return response.status(500).send();
}
networks.forEach((network) => {
const { interfaces: ifaces, type } = network;
if (
hostType === 'node' &&
['bcn', 'ifn'].includes(type) &&
ifaces.length === 2 &&
!ifaces.some((iface) => !iface)
) {
network.createBridge = '1';
}
});
const configData: FormConfigData = {
[cvar(2, 'dns')]: { step: 2, value: dns },
[cvar(2, 'gateway')]: { step: 2, value: gateway },
[cvar(2, 'gateway_interface')]: { step: 2, value: gatewayInterface },
[cvar(2, 'host_name')]: { step: 2, value: hostName },
...buildNetworkConfig(networks),
};
stdoutVar(
configData,
`Config data before prepare network on host ${hostUUID}: `,
);
const configEntries = Object.entries(configData);
try {
for (const [ckey, cdetail] of configEntries) {
const { step = 1, value } = cdetail;
const vuuid = await variable({
file: __filename,
variable_default: '',
varaible_description: '',
variable_name: ckey,
variable_section: `config_step${step}`,
variable_source_uuid: hostUUID,
variable_source_table: 'hosts',
variable_value: value,
});
assert(
REP_UUID.test(vuuid),
`Not a UUIDv4 post insert or update of ${ckey} with [${cdetail}]`,
);
}
await job({
file: __filename,
job_command: SERVER_PATHS.usr.sbin['anvil-configure-host'].self,
job_data: buildJobData({
entries: configEntries,
getValue: ({ value }) => String(value),
}),
job_name: 'configure::network',
job_title: 'job_0001',
job_description: 'job_0071',
});
} catch (error) {
stderr(`Failed to queue prepare network; CAUSE: ${error}`);
return response.status(500).send();
}
return response.send();
};

@ -1,8 +1,12 @@
import { RequestHandler } from 'express'; import { RequestHandler } from 'express';
import { buildBranchRequestHandler } from '../buildBranchRequestHandler'; import { buildBranchRequestHandler } from '../buildBranchRequestHandler';
import { configStriker } from './configStriker';
import { prepareNetwork } from './prepareNetwork';
import { setHostInstallTarget } from './setHostInstallTarget'; import { setHostInstallTarget } from './setHostInstallTarget';
export const updateHost: RequestHandler = buildBranchRequestHandler({ export const updateHost: RequestHandler = buildBranchRequestHandler({
'install-target': setHostInstallTarget, 'install-target': setHostInstallTarget as RequestHandler,
'subnode-network': prepareNetwork as RequestHandler,
striker: configStriker,
}); });

@ -1,11 +1,14 @@
import { REP_PEACEFUL_STRING } from '../../consts';
import buildGetRequestHandler from '../buildGetRequestHandler'; import buildGetRequestHandler from '../buildGetRequestHandler';
import { sanitize } from '../../sanitize'; import { sanitize } from '../../sanitize';
import { date, stdout } from '../../shell'; import { date, stdout } from '../../shell';
export const getJob = buildGetRequestHandler((request, buildQueryOptions) => { export const getJob = buildGetRequestHandler((request, buildQueryOptions) => {
const { start: rawStart } = request.query; const { start: rStart, command: rCommand } = request.query;
const start = sanitize(rawStart, 'number'); const start = sanitize(rStart, 'number');
const jcmd = sanitize(rCommand, 'string');
let condModifiedDate = ''; let condModifiedDate = '';
@ -19,6 +22,12 @@ export const getJob = buildGetRequestHandler((request, buildQueryOptions) => {
); );
} }
let condJobCommand = '';
if (REP_PEACEFUL_STRING.test(jcmd)) {
condJobCommand = `AND job.job_command LIKE '%${jcmd}%'`;
}
stdout(`condModifiedDate=[${condModifiedDate}]`); stdout(`condModifiedDate=[${condModifiedDate}]`);
if (buildQueryOptions) { if (buildQueryOptions) {
@ -77,6 +86,7 @@ export const getJob = buildGetRequestHandler((request, buildQueryOptions) => {
FROM jobs AS job FROM jobs AS job
JOIN hosts AS hos JOIN hosts AS hos
ON job.job_host_uuid = hos.host_uuid ON job.job_host_uuid = hos.host_uuid
WHERE job.job_progress < 100 WHERE (job.job_progress < 100 ${condModifiedDate})
${condModifiedDate};`; ${condJobCommand}
AND job_name NOT LIKE 'get_server_screenshot%';`;
}); });

@ -1,10 +1,33 @@
import { toHostUUID } from '../../convertHostUUID'; import { DELETED, LOCAL } from '../../consts';
import buildGetRequestHandler from '../buildGetRequestHandler'; import buildGetRequestHandler from '../buildGetRequestHandler';
import { toHostUUID } from '../../convertHostUUID';
export const getNetworkInterface = buildGetRequestHandler( export const getNetworkInterface = buildGetRequestHandler(
({ params: { hostUUID: rawHostUUID } }, buildQueryOptions) => { (request, buildQueryOptions) => {
const hostUUID = toHostUUID(rawHostUUID ?? 'local'); const {
params: { hostUUID: rHostUUID = LOCAL },
} = request;
const hostUUID = toHostUUID(rHostUUID);
const query = `
SELECT
network_interface_uuid,
network_interface_mac_address,
network_interface_name,
CASE
WHEN network_interface_link_state = '1'
AND network_interface_operational = 'up'
THEN 'up'
ELSE 'down'
END AS network_interface_state,
network_interface_speed,
ROW_NUMBER() OVER(ORDER BY modified_date ASC) AS network_interface_order
FROM network_interfaces
WHERE network_interface_operational != '${DELETED}'
AND network_interface_name NOT SIMILAR TO '(vnet\\d+|virbr\\d+-nic)%'
AND network_interface_host_uuid = '${hostUUID}';`;
if (buildQueryOptions) { if (buildQueryOptions) {
buildQueryOptions.afterQueryReturn = (queryStdout) => { buildQueryOptions.afterQueryReturn = (queryStdout) => {
@ -34,21 +57,6 @@ export const getNetworkInterface = buildGetRequestHandler(
}; };
} }
return ` return query;
SELECT
network_interface_uuid,
network_interface_mac_address,
network_interface_name,
CASE
WHEN network_interface_link_state = '1'
AND network_interface_operational = 'up'
THEN 'up'
ELSE 'down'
END AS network_interface_state,
network_interface_speed,
ROW_NUMBER() OVER(ORDER BY modified_date ASC) AS network_interface_order
FROM network_interfaces
WHERE network_interface_operational != 'DELETED'
AND network_interface_host_uuid = '${hostUUID}';`;
}, },
); );

@ -0,0 +1,66 @@
import assert from 'assert';
import { RequestHandler } from 'express';
import { REP_UUID, SERVER_PATHS } from '../../consts';
import { job, query } from '../../accessModule';
import { sanitize } from '../../sanitize';
import { stderr, stdoutVar } from '../../shell';
export const deleteServer: RequestHandler<
{ serverUuid?: string },
undefined,
{ serverUuids: string[] }
> = async (request, response) => {
const {
body: { serverUuids: rServerUuids } = {},
params: { serverUuid: rServerUuid },
} = request;
const serverUuids = sanitize(rServerUuids, 'string[]', {
modifierType: 'sql',
});
if (rServerUuid) {
serverUuids.push(
sanitize(rServerUuid, 'string', {
modifierType: 'sql',
}),
);
}
stdoutVar(serverUuids, `Delete servers with: `);
for (const serverUuid of serverUuids) {
try {
assert(
REP_UUID.test(serverUuid),
`Server UUID must be a valid UUIDv4; got [${serverUuid}]`,
);
const rows: [[string]] = await query(
`SELECT server_host_uuid FROM servers WHERE server_uuid = '${serverUuid}';`,
);
assert.ok(rows.length, `Server ${serverUuid} not found`);
const [[serverHostUuid]] = rows;
job({
file: __filename,
job_command: `${SERVER_PATHS.usr.sbin['anvil-delete-server'].self}`,
job_data: `server_uuid=${serverUuid}`,
job_description: 'job_0209',
job_host_uuid: serverHostUuid,
job_name: 'server::delete',
job_title: 'job_0208',
});
} catch (error) {
stderr(`Failed to initiate delete server ${serverUuid}; CAUSE: ${error}`);
return response.status(500).send();
}
}
return response.status(204).send();
};

@ -1,4 +1,7 @@
import { DELETED } from '../../consts';
import buildGetRequestHandler from '../buildGetRequestHandler'; import buildGetRequestHandler from '../buildGetRequestHandler';
import { buildQueryResultReducer } from '../../buildQueryResultModifier';
import join from '../../join'; import join from '../../join';
import { sanitize } from '../../sanitize'; import { sanitize } from '../../sanitize';
import { stdoutVar } from '../../shell'; import { stdoutVar } from '../../shell';
@ -17,31 +20,33 @@ export const getServer = buildGetRequestHandler(
stdoutVar({ condAnvilUUIDs }); stdoutVar({ condAnvilUUIDs });
if (buildQueryOptions) { if (buildQueryOptions) {
buildQueryOptions.afterQueryReturn = (queryStdout) => { buildQueryOptions.afterQueryReturn = buildQueryResultReducer<
let result = queryStdout; ServerOverview[]
>(
if (queryStdout instanceof Array) { (
result = queryStdout.map<ServerOverview>( previous,
([ [
serverUUID, serverUUID,
serverName, serverName,
serverState, serverState,
serverHostUUID, serverHostUUID,
anvilUUID, anvilUUID,
anvilName, anvilName,
]) => ({ ],
serverHostUUID, ) => {
serverName, previous.push({
serverState, anvilName,
serverUUID, anvilUUID,
anvilUUID, serverHostUUID,
anvilName, serverName,
}), serverState,
); serverUUID,
} });
return result; return previous;
}; },
[],
);
} }
return ` return `
@ -55,7 +60,7 @@ export const getServer = buildGetRequestHandler(
FROM servers AS ser FROM servers AS ser
JOIN anvils AS anv JOIN anvils AS anv
ON ser.server_anvil_uuid = anv.anvil_uuid ON ser.server_anvil_uuid = anv.anvil_uuid
WHERE ser.server_state != 'DELETED' WHERE ser.server_state != '${DELETED}'
${condAnvilUUIDs};`; ${condAnvilUUIDs};`;
}, },
); );

@ -1,37 +1,31 @@
import assert from 'assert'; import assert from 'assert';
import { RequestHandler } from 'express'; import { RequestHandler } from 'express';
import { createReadStream } from 'fs';
import path from 'path';
import { REP_UUID, SERVER_PATHS } from '../../consts'; import { REP_UUID, SERVER_PATHS } from '../../consts';
import { getLocalHostUUID, job, query } from '../../accessModule';
import { sanitize } from '../../sanitize'; import { sanitize } from '../../sanitize';
import { mkfifo, rm, stderr, stdout } from '../../shell'; import { stderr, stdout } from '../../shell';
import { execSync } from 'child_process';
export const getServerDetail: RequestHandler<
ServerDetailParamsDictionary,
unknown,
unknown,
ServerDetailParsedQs
> = async (request, response) => {
const {
params: { serverUUID: serverUuid },
query: { ss },
} = request;
const rmfifo = (path: string) => {
try {
rm(path);
} catch (rmfifoError) {
stderr(`Failed to clean up named pipe; CAUSE: ${rmfifoError}`);
}
};
export const getServerDetail: RequestHandler = async (request, response) => {
const { serverUUID } = request.params;
const { ss, resize } = request.query;
const epoch = Date.now();
const isScreenshot = sanitize(ss, 'boolean'); const isScreenshot = sanitize(ss, 'boolean');
stdout( stdout(`serverUUID=[${serverUuid}],isScreenshot=[${isScreenshot}]`);
`serverUUID=[${serverUUID}],epoch=[${epoch}],isScreenshot=[${isScreenshot}]`,
);
try { try {
assert( assert(
REP_UUID.test(serverUUID), REP_UUID.test(serverUuid),
`Server UUID must be a valid UUID; got [${serverUUID}]`, `Server UUID must be a valid UUID; got [${serverUuid}]`,
); );
} catch (assertError) { } catch (assertError) {
stderr( stderr(
@ -42,112 +36,23 @@ export const getServerDetail: RequestHandler = async (request, response) => {
} }
if (isScreenshot) { if (isScreenshot) {
let requestHostUUID: string, serverHostUUID: string; const rsbody = { screenshot: '' };
try {
requestHostUUID = getLocalHostUUID();
} catch (subError) {
stderr(String(subError));
return response.status(500).send();
}
stdout(`requestHostUUID=[${requestHostUUID}]`);
try {
[[serverHostUUID]] = await query(`
SELECT server_host_uuid
FROM servers
WHERE server_uuid = '${serverUUID}';`);
} catch (queryError) {
stderr(`Failed to get server host UUID; CAUSE: ${queryError}`);
return response.status(500).send();
}
stdout(`serverHostUUID=[${serverHostUUID}]`);
const imageFileName = `${serverUUID}_screenshot_${epoch}`;
const imageFilePath = path.join(SERVER_PATHS.tmp.self, imageFileName);
try { try {
mkfifo(imageFilePath); rsbody.screenshot = execSync(
`${SERVER_PATHS.usr.sbin['anvil-get-server-screenshot'].self} --convert --resize 500x500 --server-uuid '${serverUuid}'`,
const namedPipeReadStream = createReadStream(imageFilePath, { { encoding: 'utf-8' },
autoClose: true,
encoding: 'utf-8',
});
let imageData = '';
namedPipeReadStream.once('error', (readError) => {
stderr(`Failed to read from named pipe; CAUSE: ${readError}`);
});
namedPipeReadStream.once('close', () => {
stdout(`On close; removing named pipe at ${imageFilePath}.`);
rmfifo(imageFilePath);
return response.status(200).send({ screenshot: imageData });
});
namedPipeReadStream.on('data', (data) => {
const imageChunk = data.toString().trim();
const peekLength = 10;
stdout(
`${serverUUID} image chunk: ${
imageChunk.length > 0
? `${imageChunk.substring(
0,
peekLength,
)}...${imageChunk.substring(
imageChunk.length - peekLength - 1,
)}`
: 'empty'
}`,
);
imageData += imageChunk;
});
} catch (prepPipeError) {
stderr(
`Failed to prepare named pipe and/or receive image data; CAUSE: ${prepPipeError}`,
); );
} catch (error) {
rmfifo(imageFilePath); stderr(`Failed to server ${serverUuid} screenshot; CAUSE: ${error}`);
return response.status(500).send(); return response.status(500).send();
} }
let resizeArgs = sanitize(resize, 'string'); return response.send(rsbody);
if (!/^\d+x\d+$/.test(resizeArgs)) {
resizeArgs = '';
}
try {
await job({
file: __filename,
job_command: SERVER_PATHS.usr.sbin['anvil-get-server-screenshot'].self,
job_data: `server-uuid=${serverUUID}
request-host-uuid=${requestHostUUID}
resize=${resizeArgs}
out-file-id=${epoch}`,
job_name: `get_server_screenshot::${serverUUID}::${epoch}`,
job_title: 'job_0356',
job_description: 'job_0357',
job_host_uuid: serverHostUUID,
});
} catch (subError) {
stderr(`Failed to queue fetch server screenshot job; CAUSE: ${subError}`);
return response.status(500).send();
}
} else { } else {
// For getting sever detail data. // For getting sever detail data.
response.status(200).send(); response.send();
} }
}; };

@ -1,3 +1,4 @@
export { createServer } from './createServer'; export * from './createServer';
export { getServer } from './getServer'; export * from './deleteServer';
export { getServerDetail } from './getServerDetail'; export * from './getServer';
export * from './getServerDetail';

@ -0,0 +1,86 @@
import assert from 'assert';
import { RequestHandler } from 'express';
import { REP_IPV4, REP_PEACEFUL_STRING, REP_UUID } from '../../consts';
import { timestamp, write } from '../../accessModule';
import { sanitize } from '../../sanitize';
import { stderr, uuid } from '../../shell';
export const createUps: RequestHandler<
{ uuid?: string },
undefined,
{ agent: string; ipAddress: string; name: string }
> = async (request, response) => {
const {
body: { agent: rAgent, ipAddress: rIpAddress, name: rName } = {},
params: { uuid: rUuid },
} = request;
const agent = sanitize(rAgent, 'string');
const ipAddress = sanitize(rIpAddress, 'string');
const name = sanitize(rName, 'string');
const upsUuid = sanitize(rUuid, 'string', { fallback: uuid() });
try {
assert(
REP_PEACEFUL_STRING.test(agent),
`Agent must be a peaceful string; got [${agent}]`,
);
assert(
REP_IPV4.test(ipAddress),
`IP address must be a valid IPv4; got [${ipAddress}]`,
);
assert(
REP_PEACEFUL_STRING.test(name),
`Name must be a peaceful string; got [${name}]`,
);
assert(
REP_UUID.test(upsUuid),
`UPS UUID must be a valid UUIDv4; got [${upsUuid}]`,
);
} catch (error) {
stderr(`Assert value failed when working with UPS; CAUSE: ${error}`);
return response.status(400).send();
}
const modifiedDate = timestamp();
try {
const wcode = await write(
`INSERT INTO
upses (
ups_uuid,
ups_name,
ups_agent,
ups_ip_address,
modified_date
) VALUES (
'${upsUuid}',
'${name}',
'${agent}',
'${ipAddress}',
'${modifiedDate}'
) ON CONFLICT (ups_uuid)
DO UPDATE SET
ups_name = '${name}',
ups_agent = '${agent}',
ups_ip_address = '${ipAddress}',
modified_date = '${modifiedDate}';`,
);
assert(wcode === 0, `Write exited with code ${wcode}`);
} catch (error) {
stderr(`Failed to write UPS record; CAUSE: ${error}`);
return response.status(500).send();
}
const scode = rUuid ? 200 : 201;
return response.status(scode).send();
};

@ -0,0 +1,20 @@
import { DELETED } from '../../consts';
import { write } from '../../accessModule';
import { buildDeleteRequestHandler } from '../buildDeleteRequestHandler';
import join from '../../join';
export const deleteUps = buildDeleteRequestHandler({
delete: async (upsUuids) => {
const wcode = await write(
`UPDATE upses
SET ups_ip_address = '${DELETED}'
WHERE ups_uuid IN (${join(upsUuids, {
elementWrapper: "'",
separator: ',',
})});`,
);
if (wcode !== 0) throw Error(`Write exited with code ${wcode}`);
},
});

@ -1,5 +1,7 @@
import { RequestHandler } from 'express'; import { RequestHandler } from 'express';
import { DELETED } from '../../consts';
import buildGetRequestHandler from '../buildGetRequestHandler'; import buildGetRequestHandler from '../buildGetRequestHandler';
import { buildQueryResultReducer } from '../../buildQueryResultModifier'; import { buildQueryResultReducer } from '../../buildQueryResultModifier';
@ -12,6 +14,7 @@ export const getUPS: RequestHandler = buildGetRequestHandler(
ups_agent, ups_agent,
ups_ip_address ups_ip_address
FROM upses FROM upses
WHERE ups_ip_address != '${DELETED}'
ORDER BY ups_name ASC;`; ORDER BY ups_name ASC;`;
const afterQueryReturn: QueryResultModifierFunction | undefined = const afterQueryReturn: QueryResultModifierFunction | undefined =
buildQueryResultReducer<{ [upsUUID: string]: UpsOverview }>( buildQueryResultReducer<{ [upsUUID: string]: UpsOverview }>(

@ -1,2 +1,5 @@
export * from './createUps';
export * from './deleteUps';
export * from './getUPS'; export * from './getUPS';
export * from './getUPSTemplate'; export * from './getUPSTemplate';
export * from './updateUps';

@ -0,0 +1,3 @@
import { createUps } from './createUps';
export const updateUps = createUps;

@ -0,0 +1,71 @@
import assert from 'assert';
import { RequestHandler } from 'express';
import { DELETED, REP_PEACEFUL_STRING, REP_UUID } from '../../consts';
import { insertOrUpdateUser, query } from '../../accessModule';
import { sanitize } from '../../sanitize';
import { openssl, stderr, stdoutVar } from '../../shell';
export const createUser: RequestHandler<
unknown,
CreateUserResponseBody,
CreateUserRequestBody
> = async (request, response) => {
const {
body: { password: rPassword, userName: rUserName } = {},
user: { name: sessionUserName } = {},
} = request;
if (sessionUserName !== 'admin') return response.status(401).send();
const password = sanitize(rPassword, 'string', {
fallback: openssl('rand', '-base64', '12').trim().replaceAll('/', '!'),
});
const userName = sanitize(rUserName, 'string', { modifierType: 'sql' });
stdoutVar({ password, userName }, 'Create user with params: ');
try {
assert(
REP_PEACEFUL_STRING.test(password),
`Password must be a peaceful string; got: [${password}]`,
);
assert(
REP_PEACEFUL_STRING.test(userName),
`User name must be a peaceful string; got: [${userName}]`,
);
const [[userCount]]: [[number]] = await query(
`SELECT COUNT(user_uuid)
FROM users
WHERE user_algorithm != '${DELETED}' AND user_name = '${userName}';`,
);
assert(userCount === 0, `User name [${userName}] already used`);
} catch (error) {
stderr(`Failed to assert value when creating user; CAUSE: ${error}`);
return response.status(400).send();
}
try {
const result = await insertOrUpdateUser({
file: __filename,
user_name: userName,
user_password_hash: password,
});
assert(
REP_UUID.test(result),
`Insert or update failed with result [${result}]`,
);
} catch (error) {
stderr(`Failed to record user to database; CAUSE: ${error}`);
return response.status(500).send();
}
return response.status(201).send({ password });
};

@ -9,15 +9,18 @@ import { sanitize } from '../../sanitize';
import { stderr, stdoutVar } from '../../shell'; import { stderr, stdoutVar } from '../../shell';
export const deleteUser: RequestHandler< export const deleteUser: RequestHandler<
DeleteUserParamsDictionary, UserParamsDictionary,
undefined, undefined,
DeleteUserRequestBody DeleteUserRequestBody
> = async (request, response) => { > = async (request, response) => {
const { const {
body: { uuids: rawUserUuidList } = {}, body: { uuids: rawUserUuidList } = {},
params: { userUuid }, params: { userUuid },
user: { name: sessionUserName } = {},
} = request; } = request;
if (sessionUserName !== 'admin') return response.status(401).send();
const userUuidList = sanitize(rawUserUuidList, 'string[]'); const userUuidList = sanitize(rawUserUuidList, 'string[]');
const ulist = userUuidList.length > 0 ? userUuidList : [userUuid]; const ulist = userUuidList.length > 0 ? userUuidList : [userUuid];
@ -45,7 +48,10 @@ export const deleteUser: RequestHandler<
const wcode = await write( const wcode = await write(
`UPDATE users `UPDATE users
SET user_algorithm = '${DELETED}' SET user_algorithm = '${DELETED}'
WHERE user_uuid IN (${join(ulist)});`, WHERE user_uuid IN (${join(ulist, {
elementWrapper: "'",
separator: ',',
})});`,
); );
assert(wcode === 0, `Write exited with code ${wcode}`); assert(wcode === 0, `Write exited with code ${wcode}`);

@ -1,19 +1,35 @@
import { DELETED } from '../../consts';
import buildGetRequestHandler from '../buildGetRequestHandler'; import buildGetRequestHandler from '../buildGetRequestHandler';
import { buildQueryResultReducer } from '../../buildQueryResultModifier'; import { buildQueryResultReducer } from '../../buildQueryResultModifier';
export const getUser = buildGetRequestHandler((request, buildQueryOptions) => { export const getUser = buildGetRequestHandler((request, buildQueryOptions) => {
const { user: { name: sessionUserName, uuid: sessionUserUuid } = {} } =
request;
let condLimitRegular = '';
if (sessionUserName !== 'admin') {
condLimitRegular = `AND user_uuid = '${sessionUserUuid}'`;
}
const query = ` const query = `
SELECT SELECT
use.user_name, a.user_name,
use.user_uuid a.user_uuid
FROM users AS use;`; FROM users AS a
WHERE a.user_algorithm != '${DELETED}'
${condLimitRegular};`;
const afterQueryReturn: QueryResultModifierFunction | undefined = const afterQueryReturn: QueryResultModifierFunction | undefined =
buildQueryResultReducer< buildQueryResultReducer<
Record<string, { userName: string; userUUID: string }> Record<string, { userName: string; userUUID: string }>
>((previous, [userName, userUUID]) => { >((previous, [userName, userUuid]) => {
previous[userUUID] = { const key = userUuid === sessionUserUuid ? 'current' : userUuid;
previous[key] = {
userName, userName,
userUUID, userUUID: userUuid,
}; };
return previous; return previous;

@ -1,2 +1,4 @@
export * from './createUser';
export * from './deleteUser'; export * from './deleteUser';
export * from './getUser'; export * from './getUser';
export * from './updateUser';

@ -0,0 +1,140 @@
import assert from 'assert';
import { RequestHandler } from 'express';
import { REP_PEACEFUL_STRING, REP_UUID } from '../../consts';
import { encrypt, query, write } from '../../accessModule';
import { sanitize } from '../../sanitize';
import { stderr, stdoutVar } from '../../shell';
export const updateUser: RequestHandler<
UserParamsDictionary,
undefined,
UpdateUserRequestBody
> = async (request, response) => {
const {
body: { password: rPassword, userName: rUserName } = {},
params: { userUuid },
user: { name: sessionUserName, uuid: sessionUserUuid } = {},
} = request;
if (sessionUserName !== 'admin' && userUuid !== sessionUserUuid)
return response.status(401).send();
const password = sanitize(rPassword, 'string');
const userName = sanitize(rUserName, 'string', { modifierType: 'sql' });
stdoutVar({ password, userName }, `Update user ${userUuid} with params: `);
try {
if (password.length) {
assert(
REP_PEACEFUL_STRING.test(password),
`Password must be a valid peaceful string; got: [${password}]`,
);
}
if (userName.length) {
assert(
REP_PEACEFUL_STRING.test(userName),
`User name must be a peaceful string; got: [${userName}]`,
);
}
assert(
REP_UUID.test(userUuid),
`User UUID must be a valid UUIDv4; got: [${userUuid}]`,
);
const [[existingUserName]]: [[string]] = await query(
`SELECT user_name FROM users WHERE user_uuid = '${userUuid}';`,
);
assert(existingUserName !== 'admin' || userName, 'Cannot ');
} catch (error) {
stderr(`Assert failed when update user; CAUSE: ${error}`);
return response.status(400).send();
}
let existingUser: [
[
user_name: string,
user_password_hash: string,
user_salt: string,
user_algorithm: string,
user_hash_count: string,
],
];
try {
existingUser = await query(
`SELECT
user_name,
user_password_hash,
user_salt,
user_algorithm,
user_hash_count
FROM users
WHERE user_uuid = '${userUuid}'
ORDER BY modified_date DESC
LIMIT 1;`,
);
} catch (error) {
stderr(`Failed to find existing user ${userUuid}; CAUSE: ${error}`);
return response.status(500).send();
}
if (existingUser.length !== 1) {
return response.status(404).send();
}
const [[xUserName, xPasswordHash, xSalt, xAlgorithm, xHashCount]] =
existingUser;
const assigns: string[] = [];
if (password.length) {
let passwordHash: string;
try {
({ user_password_hash: passwordHash } = await encrypt({
algorithm: xAlgorithm,
hash_count: xHashCount,
password,
salt: xSalt,
}));
} catch (error) {
stderr(`Encrypt failed when update user; CAUSE ${error}`);
return response.status(500).send();
}
if (passwordHash !== xPasswordHash) {
assigns.push(`user_password_hash = '${passwordHash}'`);
}
}
if (userName.length && xUserName !== 'admin' && userName !== xUserName) {
assigns.push(`user_name = '${userName}'`);
}
if (assigns.length) {
try {
const wcode = await write(
`UPDATE users SET ${assigns.join(
',',
)} WHERE user_uuid = '${userUuid}';`,
);
assert(wcode === 0, `Update users failed with code: ${wcode}`);
} catch (error) {
stderr(`Failed to record user changes to database; CAUSE: ${error}`);
return response.status(500).send();
}
}
return response.send();
};

@ -0,0 +1,2 @@
export const cvar = (step: number, name: string) =>
['form', `config_step${step}`, name, 'value'].join('::');

@ -1,11 +1,20 @@
import { Handler } from 'express'; import { Handler } from 'express';
import { stdout } from './shell'; import { stdout } from '../lib/shell';
type HandlerParameters = Parameters<Handler>;
type AssertAuthenticationFailFunction = (
returnTo?: string,
...args: HandlerParameters
) => void;
type AssertAuthenticationSucceedFunction = (...args: HandlerParameters) => void;
type AssertAuthenticationOptions = { type AssertAuthenticationOptions = {
fail?: string | ((...args: Parameters<Handler>) => void); fail?: string | AssertAuthenticationFailFunction;
failReturnTo?: boolean | string; failReturnTo?: boolean | string;
succeed?: string | ((...args: Parameters<Handler>) => void); succeed?: string | AssertAuthenticationSucceedFunction;
}; };
type AssertAuthenticationFunction = ( type AssertAuthenticationFunction = (
@ -13,21 +22,11 @@ type AssertAuthenticationFunction = (
) => Handler; ) => Handler;
export const assertAuthentication: AssertAuthenticationFunction = ({ export const assertAuthentication: AssertAuthenticationFunction = ({
fail: initFail = (request, response) => response.status(404).send(), fail: initFail = (rt, rq, response) => response.status(404).send(),
failReturnTo, failReturnTo,
succeed: initSucceed = (request, response, next) => next(), succeed: initSucceed = (request, response, next) => next(),
}: AssertAuthenticationOptions = {}) => { }: AssertAuthenticationOptions = {}) => {
const fail: (...args: Parameters<Handler>) => void = let getReturnTo: ((...args: HandlerParameters) => string) | undefined;
typeof initFail === 'string'
? (request, response) => response.redirect(initFail)
: initFail;
const succeed: (...args: Parameters<Handler>) => void =
typeof initSucceed === 'string'
? (request, response) => response.redirect(initSucceed)
: initSucceed;
let getReturnTo: ((...args: Parameters<Handler>) => string) | undefined;
if (failReturnTo === true) { if (failReturnTo === true) {
getReturnTo = ({ originalUrl, url }) => originalUrl || url; getReturnTo = ({ originalUrl, url }) => originalUrl || url;
@ -35,6 +34,17 @@ export const assertAuthentication: AssertAuthenticationFunction = ({
getReturnTo = () => failReturnTo; getReturnTo = () => failReturnTo;
} }
const fail: AssertAuthenticationFailFunction =
typeof initFail === 'string'
? (returnTo, rq, response) =>
response.redirect(returnTo ? `${initFail}?rt=${returnTo}` : initFail)
: initFail;
const succeed: AssertAuthenticationSucceedFunction =
typeof initSucceed === 'string'
? (request, response) => response.redirect(initSucceed)
: initSucceed;
return (...args) => { return (...args) => {
const { 0: request } = args; const { 0: request } = args;
const { originalUrl, session } = request; const { originalUrl, session } = request;
@ -42,13 +52,17 @@ export const assertAuthentication: AssertAuthenticationFunction = ({
if (passport?.user) return succeed(...args); if (passport?.user) return succeed(...args);
session.returnTo = getReturnTo?.call(null, ...args); const rt = getReturnTo?.call(null, ...args);
stdout(`Unauthenticated access to ${originalUrl}`);
if (rt) {
stdout(`Set session.returnTo=${rt}`);
stdout( session.returnTo = rt;
`Unauthenticated access to ${originalUrl}; set return to ${session.returnTo}`, }
);
return fail(...args); return fail(rt, ...args);
}; };
}; };

@ -0,0 +1,55 @@
import { Handler } from 'express';
import { LOCAL } from '../lib/consts';
import { query } from '../lib/accessModule';
import { toHostUUID } from '../lib/convertHostUUID';
import { stderr, stdoutVar } from '../lib/shell';
export const assertInit =
({
fail = (rq, rs) => rs.status(401).send(),
hostUuid: rHostUuid = LOCAL,
invert,
succeed = (rq, rs, nx) => nx(),
}: {
fail?: (...args: Parameters<Handler>) => void;
hostUuid?: string;
invert?: boolean;
succeed?: (...args: Parameters<Handler>) => void;
} = {}): Handler =>
async (...args) => {
const { 1: response } = args;
const hostUuid = toHostUUID(rHostUuid);
let rows: [[string]];
try {
rows = await query(
`SELECT variable_value
FROM variables
WHERE variable_name = 'system::configured'
AND variable_source_table = 'hosts'
AND variable_source_uuid = '${hostUuid}'
LIMIT 1;`,
);
} catch (error) {
stderr(`Failed to get system configured flag; CAUSE: ${error}`);
return response.status(500).send();
}
stdoutVar(rows, `Configured variable of host ${hostUuid}: `);
let condition = rows.length === 1 && rows[0][0] === '1';
if (invert) condition = !condition;
if (condition) {
stderr(`Assert init failed; invert=${invert}`);
return fail(...args);
}
return succeed(...args);
};

@ -0,0 +1,7 @@
import passport from './passport';
import session from './session';
export * from './assertAuthentication';
export * from './assertInit';
export { passport, session };

@ -1,11 +1,11 @@
import passport from 'passport'; import passport from 'passport';
import { Strategy as LocalStrategy } from 'passport-local'; import { Strategy as LocalStrategy } from 'passport-local';
import { DELETED } from './lib/consts'; import { DELETED } from '../lib/consts';
import { query, sub } from './lib/accessModule'; import { encrypt, query } from '../lib/accessModule';
import { sanitize } from './lib/sanitize'; import { sanitize } from '../lib/sanitize';
import { stdout } from './lib/shell'; import { stdout } from '../lib/shell';
passport.use( passport.use(
'login', 'login',
@ -31,8 +31,9 @@ passport.use(
user_algorithm, user_algorithm,
user_hash_count user_hash_count
FROM users FROM users
WHERE user_algorithm != 'DELETED' WHERE user_algorithm != '${DELETED}'
AND user_name = '${username}' AND user_name = '${username}'
ORDER BY modified_date DESC
LIMIT 1;`, LIMIT 1;`,
); );
} catch (queryError) { } catch (queryError) {
@ -47,24 +48,14 @@ passport.use(
0: [userUuid, , userPasswordHash, userSalt, userAlgorithm, userHashCount], 0: [userUuid, , userPasswordHash, userSalt, userAlgorithm, userHashCount],
} = rows; } = rows;
let encryptResult: { let encryptResult: Encrypted;
user_password_hash: string;
user_salt: string;
user_hash_count: number;
user_algorithm: string;
};
try { try {
[encryptResult] = await sub('encrypt_password', { encryptResult = await encrypt({
params: [ algorithm: userAlgorithm,
{ hash_count: userHashCount,
algorithm: userAlgorithm, password,
hash_count: userHashCount, salt: userSalt,
password,
salt: userSalt,
},
],
pre: ['Account'],
}); });
} catch (subError) { } catch (subError) {
return done(subError); return done(subError);

@ -4,11 +4,12 @@ import expressSession, {
Store as BaseSessionStore, Store as BaseSessionStore,
} from 'express-session'; } from 'express-session';
import { DELETED } from './lib/consts'; import { DELETED } from '../lib/consts';
import { getLocalHostUUID, query, timestamp, write } from './lib/accessModule'; import { getLocalHostUUID, query, timestamp, write } from '../lib/accessModule';
import { getSessionSecret } from './lib/getSessionSecret'; import { cname } from '../lib/cname';
import { stderr, stdout, stdoutVar, uuid } from './lib/shell'; import { getSessionSecret } from '../lib/getSessionSecret';
import { stderr, stdout, stdoutVar, uuid } from '../lib/shell';
const DEFAULT_COOKIE_ORIGINAL_MAX_AGE = 28800000; // 8 hours const DEFAULT_COOKIE_ORIGINAL_MAX_AGE = 28800000; // 8 hours
@ -97,7 +98,7 @@ export class SessionStore extends BaseSessionStore {
session: SessionData, session: SessionData,
done?: ((err?: unknown) => void) | undefined, done?: ((err?: unknown) => void) | undefined,
): Promise<void> { ): Promise<void> {
stdoutVar({ session }, `Set session ${sid}`); stdoutVar({ session }, `Set session ${sid}: `);
const { passport: { user: userUuid } = {} } = session; const { passport: { user: userUuid } = {} } = session;
@ -145,7 +146,7 @@ export class SessionStore extends BaseSessionStore {
session: SessionData, session: SessionData,
done?: ((err?: unknown) => void) | undefined, done?: ((err?: unknown) => void) | undefined,
): Promise<void> { ): Promise<void> {
stdoutVar({ session }, `Touch session ${sid}`); stdoutVar({ session }, `Touch session ${sid}: `);
try { try {
const wcode = await write( const wcode = await write(
@ -187,13 +188,14 @@ export default (async () =>
maxAge: DEFAULT_COOKIE_ORIGINAL_MAX_AGE, maxAge: DEFAULT_COOKIE_ORIGINAL_MAX_AGE,
secure: false, secure: false,
}, },
genid: ({ path }) => { genid: ({ originalUrl }) => {
const sid = uuid(); const sid = uuid();
stdout(`Generated session identifier ${sid}; request.path=${path}`); stdout(`Generated session identifier ${sid}; access to ${originalUrl}`);
return sid; return sid;
}, },
name: cname('sid'),
resave: false, resave: false,
saveUninitialized: false, saveUninitialized: false,
secret: await getSessionSecret(), secret: await getSessionSecret(),

@ -1,9 +1,24 @@
import express from 'express'; import express from 'express';
import getAnvil from '../lib/request_handlers/anvil/getAnvil'; import {
getAnvil,
getAnvilCpu,
getAnvilSummary,
getAnvilDetail,
getAnvilMemory,
getAnvilNetwork,
getAnvilStore,
} from '../lib/request_handlers/anvil';
const router = express.Router(); const router = express.Router();
router.get('/', getAnvil); router
.get('/', getAnvil)
.get('/summary', getAnvilSummary)
.get('/:anvilUuid/cpu', getAnvilCpu)
.get('/:anvilUuid/memory', getAnvilMemory)
.get('/:anvilUuid/network', getAnvilNetwork)
.get('/:anvilUuid/store', getAnvilStore)
.get('/:anvilUuid', getAnvilDetail);
export default router; export default router;

@ -1,8 +1,7 @@
import express from 'express'; import express from 'express';
import { guardApi } from '../lib/assertAuthentication';
import { login, logout } from '../lib/request_handlers/auth'; import { login, logout } from '../lib/request_handlers/auth';
import passport from '../passport'; import { guardApi, passport } from '../middlewares';
const router = express.Router(); const router = express.Router();

@ -2,9 +2,19 @@ import express from 'express';
import { import {
getHostSSH, getHostSSH,
poweroffHost, joinAn,
rebootHost, leaveAn,
manageVncSshTunnel,
poweroffStriker,
rebootStriker,
runManifest, runManifest,
setMapNetwork,
startAn,
startServer,
startSubnode,
stopAn,
stopServer,
stopSubnode,
updateSystem, updateSystem,
} from '../lib/request_handlers/command'; } from '../lib/request_handlers/command';
@ -12,9 +22,20 @@ const router = express.Router();
router router
.put('/inquire-host', getHostSSH) .put('/inquire-host', getHostSSH)
.put('/poweroff-host', poweroffHost) .put('/join-an/:uuid', joinAn)
.put('/reboot-host', rebootHost) .put('/leave-an/:uuid', leaveAn)
.put('/vnc-pipe', manageVncSshTunnel)
.put('/poweroff-host', poweroffStriker)
.put('/reboot-host', rebootStriker)
.put('/run-manifest/:manifestUuid', runManifest) .put('/run-manifest/:manifestUuid', runManifest)
.put('/set-map-network', setMapNetwork)
.put('/set-map-network/:uuid', setMapNetwork)
.put('/start-an/:uuid', startAn)
.put('/start-server/:uuid', startServer)
.put('/start-subnode/:uuid', startSubnode)
.put('/stop-an/:uuid', stopAn)
.put('/stop-server/:uuid', stopServer)
.put('/stop-subnode/:uuid', stopSubnode)
.put('/update-system', updateSystem); .put('/update-system', updateSystem);
export default router; export default router;

@ -1,9 +1,20 @@
import express from 'express'; import express from 'express';
import { getFence, getFenceTemplate } from '../lib/request_handlers/fence'; import {
createFence,
deleteFence,
getFence,
getFenceTemplate,
updateFence,
} from '../lib/request_handlers/fence';
const router = express.Router(); const router = express.Router();
router.get('/', getFence).get('/template', getFenceTemplate); router
.delete('/:uuid?', deleteFence)
.get('/', getFence)
.get('/template', getFenceTemplate)
.post('/', createFence)
.put('/:uuid', updateFence);
export default router; export default router;

@ -22,7 +22,7 @@ router
.post('/', createHost) .post('/', createHost)
.post(CONNECTION_PATH, createHostConnection) .post(CONNECTION_PATH, createHostConnection)
.put('/prepare', prepareHost) .put('/prepare', prepareHost)
.put('/:hostUUID', updateHost) .put('/:hostUUID?', updateHost)
.delete(CONNECTION_PATH, deleteHostConnection); .delete(CONNECTION_PATH, deleteHostConnection);
export default router; export default router;

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save