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",
modprobe => "/usr/sbin/modprobe",
mv => "/usr/bin/mv",
nc => "/usr/bin/nc",
nmap => "/usr/bin/nmap",
nmcli => "/bin/nmcli",
ocf_alteeve => "/usr/lib/ocf/resource.d/alteeve/server",

@ -16,6 +16,7 @@ my $THIS_FILE = "Server.pm";
# boot_virsh
# count_servers
# find
# find_processes
# get_definition
# get_runtime
# 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
This returns the server definition XML for a server.

@ -20,7 +20,7 @@ TARFILES = $(PACKAGE_NAME)-$(VERSION).tar.bz2 \
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 \
striker-ui-api tools units

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

@ -159,6 +159,7 @@ AC_CONFIG_FILES([Makefile
cgi-bin/Makefile
html/Makefile
journald.conf.d/Makefile
libvirt/Makefile
man/Makefile
ocf/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.
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_drbd_fence_rules($anvil);
# Get screenshot from every server that is alive.
get_screenshots($anvil);
# Shut down.
$anvil->ScanCore->agent_shutdown({agent => $THIS_FILE});
@ -169,72 +169,66 @@ sub check_drbd_fence_rules
return(0);
}
#
sub check_vnc
# Gets a screenshot for each server that is alive.
sub get_screenshots
{
my ($anvil) = @_;
### NOTE: In the interest of time, this table is not yet in the core schema. Later, when it is, this
### 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 $anvil = shift;
my $parameters = shift;
my $debug = $parameters->{debug} // 3;
my $count = $anvil->Database->query({query => $query, source => $THIS_FILE, line => __LINE__})->[0]->[0];
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { count => $count }});
if ($count)
my $anvil_uuid = $anvil->Cluster->get_anvil_uuid();
my $local_host_uuid = $anvil->Get->host_uuid();
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 $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}})
{
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 }});
}
}
}
my $host_name = $row->[0];
$striker_name_csv = "$striker_name_csv,$host_name";
}
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.

File diff suppressed because one or more lines are too long

@ -12,6 +12,7 @@
"cors": "^2.8.5",
"express": "^4.18.2",
"express-session": "^1.17.3",
"format-data-size": "^0.1.0",
"multer": "^1.4.4",
"passport": "^0.6.0",
"passport-local": "^1.0.0",
@ -4544,6 +4545,14 @@
"integrity": "sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==",
"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": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@ -10303,6 +10312,11 @@
"integrity": "sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==",
"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": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",

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

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

@ -2,7 +2,7 @@ import { ChildProcess, spawn, SpawnOptions } from 'child_process';
import EventEmitter from 'events';
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 {
@ -131,17 +131,21 @@ class Access extends EventEmitter {
}
const access = new Access();
const rootAccess = new Access({ spawnOptions: { gid: 0, uid: 0 } });
const subroutine = async <T extends unknown[]>(
subroutine: string,
{
params = [],
pre = ['Database'],
root,
}: {
params?: unknown[];
pre?: string[];
root?: boolean;
} = {},
) => {
const selectedAccess = root ? rootAccess : access;
const chain = `${pre.join('->')}->${subroutine}`;
const subParams: string[] = params.map<string>((p) => {
@ -153,21 +157,19 @@ const subroutine = async <T extends unknown[]>(
result = String(p);
}
return `'${result}'`;
return `"${result.replaceAll('"', '\\"')}"`;
});
const { sub_results: results } = await access.interact<{ sub_results: T }>(
'x',
chain,
...subParams,
);
const { sub_results: results } = await selectedAccess.interact<{
sub_results: T;
}>('x', chain, ...subParams);
shvar(results, `${chain} 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));
const write = async (script: string) => {
@ -180,7 +182,7 @@ const write = async (script: string) => {
};
const insertOrUpdateJob = async ({
job_progress = 0,
job_progress = DEFAULT_JOB_PROGRESS,
line = 0,
...rest
}: JobParams) => {
@ -191,6 +193,14 @@ const insertOrUpdateJob = async ({
return uuid;
};
const insertOrUpdateUser: InsertOrUpdateUserFunction = async (params) => {
const [uuid]: [string] = await subroutine('insert_or_update_users', {
params: [params],
});
return uuid;
};
const insertOrUpdateVariable: InsertOrUpdateVariableFunction = async (
params,
) => {
@ -238,6 +248,15 @@ const refreshTimestamp = () => {
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 chain = `data->${keys.join('->')}`;
@ -250,10 +269,16 @@ const getData = async <T>(...keys: string[]) => {
return data;
};
const getAnvilData = async () => {
await subroutine('get_anvils');
return getData<AnvilDataAnvilListHash>('anvils');
};
const getFenceSpec = async () => {
await subroutine('get_fence_data', { pre: ['Striker'] });
return getData<unknown>('fence_data');
return getData<AnvilDataFenceHash>('fence_data');
};
const getHostData = async () => {
@ -303,6 +328,25 @@ const getManifestData = async (manifestUuid?: string) => {
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 (
target,
{ password, port } = {},
@ -340,20 +384,62 @@ const getUpsSpec = async () => {
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 {
insertOrUpdateJob as job,
insertOrUpdateUser,
insertOrUpdateVariable as variable,
anvilSyncShared,
refreshTimestamp as timestamp,
encrypt,
getData,
getAnvilData,
getFenceSpec,
getHostData,
getLocalHostName,
getLocalHostUuid as getLocalHostUUID,
getManifestData,
getNetworkData,
getPeerData,
getUpsSpec,
query,
subroutine as sub,
vncpipe,
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 =
<T>(mod: (output: QueryField[][]) => T): QueryResultModifierFunction =>
<T>(mod: (output: string[][]) => T): QueryResultModifierFunction =>
(output) =>
output instanceof Array ? mod(output) : output;
export const buildQueryResultReducer = <T>(
reduce: (previous: T, row: QueryField[]) => T,
reduce: (previous: T, row: string[]) => T,
initialValue: T,
) =>
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
const NODE_AND_DR_RESERVED_MEMORY_SIZE = 8589934592;
export default NODE_AND_DR_RESERVED_MEMORY_SIZE;
export const NODE_AND_DR_RESERVED_MEMORY_SIZE = 8589934592;

@ -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_ALPHANUM = '[a-z0-9]';
export const P_ALPHANUM_DASH = '[a-z0-9-]';

@ -24,17 +24,24 @@ const EMPTY_SERVER_PATHS: ServerPath = {
},
sbin: {
'anvil-access-module': {},
'anvil-boot-server': {},
'anvil-configure-host': {},
'anvil-delete-server': {},
'anvil-get-server-screenshot': {},
'anvil-join-anvil': {},
'anvil-manage-keys': {},
'anvil-manage-power': {},
'anvil-provision-server': {},
'anvil-safe-start': {},
'anvil-safe-stop': {},
'anvil-shutdown-server': {},
'anvil-sync-shared': {},
'anvil-update-system': {},
'striker-boot-machine': {},
'striker-initialize-host': {},
'striker-manage-install-target': {},
'striker-manage-peers': {},
'striker-manage-vnc-pipes': {},
'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 './DELETED';
export * from './ENV';
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 './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 { OS_LIST } from '../../consts/OS_LIST';
import { dSize } from 'format-data-size';
import { NODE_AND_DR_RESERVED_MEMORY_SIZE, OS_LIST } from '../../consts';
import join from '../../join';
import { stdoutVar } from '../../shell';
@ -63,13 +64,13 @@ const buildQueryAnvilDetail = ({
server_uuid,
server_name,
server_cpu_cores,
server_memory`;
server_memory_value,
server_memory_unit`;
let groupByPhrase = '';
if (isSummary) {
fieldsToSelect = `
SUM(server_cpu_cores) AS anvil_total_allocated_cpu_cores,
SUM(server_memory) AS anvil_total_allocated_memory`;
fieldsToSelect =
'SUM(server_cpu_cores) AS anvil_total_allocated_cpu_cores';
groupByPhrase = 'GROUP BY server_anvil_uuid';
}
@ -82,29 +83,20 @@ const buildQueryAnvilDetail = ({
JOIN (
SELECT
server_definition_server_uuid,
server_cpu_cores,
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,
CAST(
SUBSTRING(
server_definition_xml, '%memory%unit=''#"[A-Za-z]+#"''%', '#'
) AS server_memory_unit
FROM server_definitions AS ser_def
) AS ser_def_memory_converted
server_definition_xml, 'cores=''([\\d]*)'''
) AS INTEGER
) AS server_cpu_cores,
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
ON server_uuid = server_definition_server_uuid
${groupByPhrase}`;
@ -129,7 +121,7 @@ const buildQueryAnvilDetail = ({
const buildFileQuery = () => `
SELECT
file_location_anvil_uuid,
file_location_host_uuid,
file_uuid,
file_name
FROM file_locations as fil_loc
@ -153,16 +145,12 @@ const buildQueryAnvilDetail = ({
server_list.server_uuid,
server_list.server_name,
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_memory,
(host_summary.anvil_total_cpu_cores
- server_summary.anvil_total_allocated_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_name,
storage_group_list.storage_group_size,
@ -181,7 +169,11 @@ const buildQueryAnvilDetail = ({
LEFT JOIN (${buildStorageGroupQuery()}) AS storage_group_list
ON anv.anvil_uuid = storage_group_list.storage_group_anvil_uuid
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 = `
@ -195,15 +187,25 @@ const buildQueryAnvilDetail = ({
if (isForProvisionServer) {
query = buildQueryForProvisionServer();
afterQueryReturn = (queryStdout: unknown) => {
let results = queryStdout;
afterQueryReturn = (qoutput: unknown) => {
let results = qoutput;
if (qoutput instanceof Array) {
const lasti = qoutput.length - 1;
if (queryStdout instanceof Array) {
let rowStage: AnvilDetailForProvisionServer | undefined;
let puuid = '';
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,
anvilName,
@ -217,11 +219,10 @@ const buildQueryAnvilDetail = ({
serverUUID,
serverName,
serverCPUCores,
serverMemory,
serverMemoryValue,
serverMemoryUnit,
anvilTotalAllocatedCPUCores,
anvilTotalAllocatedMemory,
anvilTotalAvailableCPUCores,
anvilTotalAvailableMemory,
storageGroupUUID,
storageGroupName,
storageGroupSize,
@ -229,9 +230,31 @@ const buildQueryAnvilDetail = ({
fileUUID,
fileName,
],
index,
) => {
if (!rowStage || anvilUUID !== rowStage.anvilUUID) {
rowStage = {
if (index === lasti || (puuid.length && anvilUUID !== puuid)) {
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,
anvilName,
anvilDescription,
@ -240,73 +263,63 @@ const buildQueryAnvilDetail = ({
anvilTotalAllocatedCPUCores: parseInt(
anvilTotalAllocatedCPUCores,
),
anvilTotalAllocatedMemory: String(anvilTotalAllocatedMemory),
anvilTotalAvailableCPUCores: parseInt(
anvilTotalAvailableCPUCores,
),
anvilTotalAvailableMemory: String(anvilTotalAvailableMemory),
hosts: [],
servers: [],
storageGroups: [],
files: [],
};
} as AnvilDetailForProvisionServer;
reducedRows.push(rowStage);
puuid = anvilUUID;
}
if (
!rowStage.hosts.find(({ hostUUID: added }) => added === hostUUID)
) {
rowStage.hosts.push({
if (!hosts[hostUUID]) {
hosts[hostUUID] = {
hostUUID,
hostName,
hostCPUCores: parseInt(hostCPUCores),
hostMemory: String(hostMemory),
});
};
}
if (
!rowStage.servers.find(
({ serverUUID: added }) => added === serverUUID,
)
) {
rowStage.servers.push({
if (!servers[serverUUID]) {
const serverMemory =
dSize(serverMemoryValue, {
fromUnit: serverMemoryUnit,
toUnit: 'B',
})?.value ?? '0';
anvilTotalAllocatedMemory += BigInt(serverMemory);
servers[serverUUID] = {
serverUUID,
serverName,
serverCPUCores: parseInt(serverCPUCores),
serverMemory: String(serverMemory),
});
serverMemory,
};
}
if (
!rowStage.storageGroups.find(
({ storageGroupUUID: added }) => added === storageGroupUUID,
)
) {
rowStage.storageGroups.push({
if (!stores[storageGroupUUID]) {
stores[storageGroupUUID] = {
storageGroupUUID,
storageGroupName,
storageGroupSize: String(storageGroupSize),
storageGroupFree: String(storageGroupFree),
});
};
}
if (
!rowStage.files.find(({ fileUUID: added }) => added === fileUUID)
) {
rowStage.files.push({
if (!files[fileUUID]) {
files[fileUUID] = {
fileUUID,
fileName,
});
};
}
return reducedRows;
return previous;
},
[],
{},
);
results = {
anvils,
anvils: Object.values(anvils),
osList: OS_LIST,
};
}

@ -4,24 +4,24 @@ import buildGetRequestHandler from '../buildGetRequestHandler';
import buildQueryAnvilDetail from './buildQueryAnvilDetail';
import { sanitize } from '../../sanitize';
const getAnvil: RequestHandler = buildGetRequestHandler(
export const getAnvil: RequestHandler = buildGetRequestHandler(
(request, buildQueryOptions) => {
const { anvilUUIDs, isForProvisionServer } = request.query;
let query = `
SELECT
anv.anvil_name,
anv.anvil_uuid,
hos.host_name,
hos.host_uuid
FROM anvils AS anv
JOIN hosts AS hos
ON hos.host_uuid IN (
anv.anvil_node1_host_uuid,
anv.anvil_node2_host_uuid,
anv.anvil_dr1_host_uuid
)
ORDER BY anv.anvil_uuid;`;
SELECT
anv.anvil_name,
anv.anvil_uuid,
hos.host_name,
hos.host_uuid
FROM anvils AS anv
JOIN hosts AS hos
ON hos.host_uuid IN (
anv.anvil_node1_host_uuid,
anv.anvil_node2_host_uuid,
anv.anvil_dr1_host_uuid
)
ORDER BY anv.anvil_uuid;`;
if (buildQueryOptions) {
buildQueryOptions.afterQueryReturn = (queryStdout) => {
@ -77,5 +77,3 @@ const getAnvil: RequestHandler = buildGetRequestHandler(
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 { stdout } from '../../shell';
import { cname } from '../../cname';
export const login: RequestHandler<unknown, unknown, AuthLoginRequestBody> = (
request,
@ -12,6 +13,8 @@ export const login: RequestHandler<unknown, unknown, AuthLoginRequestBody> = (
const { name: userName } = user;
stdout(`Successfully authenticated user [${userName}]`);
response.cookie(cname('user'), user);
}
response.status(204).send();

@ -1,17 +1,19 @@
import { RequestHandler } from 'express';
import { cname } from '../../cname';
import { stdout } from '../../shell';
export const logout: RequestHandler = (request, response) => {
request.session.destroy((error) => {
let scode = 204;
if (error) {
scode = 500;
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 = {};
let result: (number | null | string)[][];
let result: unknown;
try {
const sqlscript: string =
@ -26,9 +26,7 @@ const buildGetRequestHandler =
} catch (queryError) {
stderr(`Failed to execute query; CAUSE: ${queryError}`);
response.status(500).send();
return;
return response.status(500).send();
}
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 './poweroffHost';
export * from './rebootHost';
export * from './joinAn';
export * from './leaveAn';
export * from './manageVncSshTunnel';
export * from './poweroffStriker';
export * from './rebootStriker';
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';

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

@ -4,10 +4,10 @@ import { getFenceSpec } from '../../accessModule';
import { stderr } from '../../shell';
export const getFenceTemplate: RequestHandler = async (request, response) => {
let rawFenceData;
let rFenceData: AnvilDataFenceHash;
try {
rawFenceData = await getFenceSpec();
rFenceData = await getFenceSpec();
} catch (subError) {
stderr(`Failed to get fence device template; CAUSE: ${subError}`);
@ -16,5 +16,5 @@ export const getFenceTemplate: RequestHandler = async (request, response) => {
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 './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,
anv.anvil_uuid,
anv.anvil_name,
anv.anvil_description
anv.anvil_description,
hos.host_uuid,
hos.host_name
FROM files AS fil
JOIN file_locations AS fil_loc
ON fil.file_uuid = fil_loc.file_location_file_uuid
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}'
${condFileUUIDs};`;
};

@ -1,3 +1,4 @@
import assert from 'assert';
import { RequestHandler } from 'express';
import { anvilSyncShared, query, timestamp, write } from '../../accessModule';
@ -84,34 +85,26 @@ export const updateFile: RequestHandler = async (request, response) => {
modified_date = '${timestamp()}'
WHERE file_location_uuid = '${fileLocationUUID}';`;
const targetHosts: [
n1uuid: string,
n2uuid: string,
dr1uuid: null | string,
][] = await query(
`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}';`,
// Each file location entry is for 1 host.
const rows = await query<[[string]]>(
`SELECT file_location_host_uuid
FROM file_locations
WHERE file_location_uuid = '${fileLocationUUID}';`,
);
targetHosts.flat().forEach((hostUUID: null | string) => {
if (hostUUID) {
anvilSyncSharedFunctions.push(() =>
anvilSyncShared(
jobName,
`file_uuid=${fileUUID}`,
jobTitle,
jobDescription,
{ jobHostUUID: hostUUID },
),
);
}
});
if (rows.length) {
const [[hostUuid]] = rows;
anvilSyncSharedFunctions.push(() =>
anvilSyncShared(
jobName,
`file_uuid=${fileUUID}`,
jobTitle,
jobDescription,
{ jobHostUUID: hostUuid },
),
);
}
},
);
}
@ -120,6 +113,8 @@ export const updateFile: RequestHandler = async (request, response) => {
try {
wcode = await write(sqlscript);
assert(wcode === 0, `Write exited with code ${wcode}`);
} catch (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: `),
);
response.status(200).send(wcode);
response.status(200).send();
};

@ -1,45 +1,82 @@
import { buildKnownIDCondition } from '../../buildCondition';
import { buildQueryResultModifier } from '../../buildQueryResultModifier';
import { cap } from '../../cap';
import { camel } from '../../camel';
import { getShortHostName } from '../../disassembleHostName';
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]) => {
const [head, ...rest] = part2.split('_');
const [rHead, ...rest] = part2.split('_');
const head = rHead.toLowerCase();
return rest.reduce<string>(
(previous, part) => `${previous}${cap(part)}`,
head,
);
return /^[a-z]+n[0-9]+/.test(head)
? ['networks', head, camel(...rest)]
: [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 = ({
keys: hostUUIDs = '*',
} = {}) => {
const condHostUUIDs = buildKnownIDCondition(hostUUIDs, 'AND hos.host_uuid');
const condHostUUIDs = buildKnownIDCondition(hostUUIDs, 'AND b.host_uuid');
stdout(`condHostUUIDs=[${condHostUUIDs}]`);
const query = `
SELECT
hos.host_name,
hos.host_type,
hos.host_uuid,
var.variable_name,
var.variable_value
FROM variables AS var
JOIN hosts AS hos
ON var.variable_source_uuid = hos.host_uuid
b.host_name,
b.host_type,
b.host_uuid,
a.variable_name,
a.variable_value,
SUBSTRING(
a.variable_name, '${CVAR_PREFIX_PATTERN}([^:]+)'
) 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 (
variable_name LIKE 'form::config_%'
variable_name LIKE '${CVAR_PREFIX}%'
OR variable_name = 'install-target::enabled'
)
${condHostUUIDs};`;
${condHostUUIDs}
ORDER BY cvar_name ASC,
a.variable_name ASC;`;
const afterQueryReturn: QueryResultModifierFunction =
buildQueryResultModifier((output) => {
@ -52,14 +89,37 @@ export const buildQueryHostDetail: BuildQueryDetailFunction = ({
hostType: string;
hostUUID: string;
shortHostName: string;
} & Record<string, string>
} & Tree
>(
(previous, [, , , variableName, variableValue]) => {
(
previous,
[
,
,
,
variableName,
variableValue,
,
networkType,
networkLink,
networkInterfaceUuid,
],
) => {
const [variablePrefix, ...restVariableParts] =
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;
},

@ -6,36 +6,16 @@ import {
REP_IPV4,
REP_IPV4_CSV,
REP_PEACEFUL_STRING,
REP_UUID,
SERVER_PATHS,
} 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 { stderr, stdoutVar } from '../../shell';
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;
}, '');
import { cvar } from '../../varn';
export const configStriker: RequestHandler<
unknown,
@ -47,23 +27,25 @@ export const configStriker: RequestHandler<
stdoutVar(body, 'Begin initialize Striker; body=');
const {
adminPassword: rAdminPassword = '',
domainName: rDomainName = '',
hostName: rHostName = '',
hostNumber: rHostNumber = 0,
networkDNS: rNetworkDns = '',
networkGateway: rNetworkGateway = '',
adminPassword: rAdminPassword,
domainName: rDomainName,
hostName: rHostName,
hostNumber: rHostNumber,
dns: rDns,
gateway: rGateway,
gatewayInterface: rGatewayInterface,
networks = [],
organizationName: rOrganizationName = '',
organizationPrefix: rOrganizationPrefix = '',
organizationName: rOrganizationName,
organizationPrefix: rOrganizationPrefix,
} = body;
const adminPassword = sanitize(rAdminPassword, 'string');
const domainName = sanitize(rDomainName, 'string');
const hostName = sanitize(rHostName, 'string');
const hostNumber = sanitize(rHostNumber, 'number');
const networkDns = sanitize(rNetworkDns, 'string');
const networkGateway = sanitize(rNetworkGateway, 'string');
const dns = sanitize(rDns, 'string');
const gateway = sanitize(rGateway, 'string');
const gatewayInterface = sanitize(rGatewayInterface, 'string');
const organizationName = sanitize(rOrganizationName, 'string');
const organizationPrefix = sanitize(rOrganizationPrefix, 'string');
@ -89,17 +71,22 @@ export const configStriker: RequestHandler<
);
assert(
REP_IPV4_CSV.test(networkDns),
`Data network DNS must be a comma separated list of valid IPv4 addresses; got [${networkDns}]`,
REP_IPV4_CSV.test(dns),
`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(
REP_IPV4.test(networkGateway),
`Data network gateway must be a valid IPv4 address; got [${networkGateway}]`,
REP_PEACEFUL_STRING.test(gatewayInterface),
`Data gateway interface must be a peaceful string; got [${gatewayInterface}]`,
);
assert(
REP_PEACEFUL_STRING.test(organizationName),
organizationName.length > 0,
`Data organization name cannot be empty; got [${organizationName}]`,
);
@ -115,40 +102,54 @@ export const configStriker: RequestHandler<
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 {
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({
file: __filename,
job_command: SERVER_PATHS.usr.sbin['anvil-configure-host'].self,
job_data: `${fvar(1, 'domain')}=${domainName}
${fvar(1, 'organization')}=${organizationName}
${fvar(1, 'prefix')}=${organizationPrefix}
${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_data: buildJobData({
entries: configEntries,
getValue: ({ value }) => String(value),
}),
job_name: 'configure::network',
job_title: 'job_0001',
job_description: 'job_0071',

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

@ -1,3 +1,4 @@
export * from './configStriker';
export * from './createHost';
export * from './createHostConnection';
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 { buildBranchRequestHandler } from '../buildBranchRequestHandler';
import { configStriker } from './configStriker';
import { prepareNetwork } from './prepareNetwork';
import { setHostInstallTarget } from './setHostInstallTarget';
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 { sanitize } from '../../sanitize';
import { date, stdout } from '../../shell';
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 = '';
@ -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}]`);
if (buildQueryOptions) {
@ -77,6 +86,7 @@ export const getJob = buildGetRequestHandler((request, buildQueryOptions) => {
FROM jobs AS job
JOIN hosts AS hos
ON job.job_host_uuid = hos.host_uuid
WHERE job.job_progress < 100
${condModifiedDate};`;
WHERE (job.job_progress < 100 ${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 { toHostUUID } from '../../convertHostUUID';
export const getNetworkInterface = buildGetRequestHandler(
({ params: { hostUUID: rawHostUUID } }, buildQueryOptions) => {
const hostUUID = toHostUUID(rawHostUUID ?? 'local');
(request, buildQueryOptions) => {
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) {
buildQueryOptions.afterQueryReturn = (queryStdout) => {
@ -34,21 +57,6 @@ export const getNetworkInterface = buildGetRequestHandler(
};
}
return `
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}';`;
return query;
},
);

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

@ -1,37 +1,31 @@
import assert from 'assert';
import { RequestHandler } from 'express';
import { createReadStream } from 'fs';
import path from 'path';
import { REP_UUID, SERVER_PATHS } from '../../consts';
import { getLocalHostUUID, job, query } from '../../accessModule';
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');
stdout(
`serverUUID=[${serverUUID}],epoch=[${epoch}],isScreenshot=[${isScreenshot}]`,
);
stdout(`serverUUID=[${serverUuid}],isScreenshot=[${isScreenshot}]`);
try {
assert(
REP_UUID.test(serverUUID),
`Server UUID must be a valid UUID; got [${serverUUID}]`,
REP_UUID.test(serverUuid),
`Server UUID must be a valid UUID; got [${serverUuid}]`,
);
} catch (assertError) {
stderr(
@ -42,112 +36,23 @@ export const getServerDetail: RequestHandler = async (request, response) => {
}
if (isScreenshot) {
let requestHostUUID: string, serverHostUUID: string;
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);
const rsbody = { screenshot: '' };
try {
mkfifo(imageFilePath);
const namedPipeReadStream = createReadStream(imageFilePath, {
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}`,
rsbody.screenshot = execSync(
`${SERVER_PATHS.usr.sbin['anvil-get-server-screenshot'].self} --convert --resize 500x500 --server-uuid '${serverUuid}'`,
{ encoding: 'utf-8' },
);
rmfifo(imageFilePath);
} catch (error) {
stderr(`Failed to server ${serverUuid} screenshot; CAUSE: ${error}`);
return response.status(500).send();
}
let resizeArgs = sanitize(resize, 'string');
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();
}
return response.send(rsbody);
} else {
// For getting sever detail data.
response.status(200).send();
response.send();
}
};

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

@ -1,2 +1,5 @@
export * from './createUps';
export * from './deleteUps';
export * from './getUPS';
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';
export const deleteUser: RequestHandler<
DeleteUserParamsDictionary,
UserParamsDictionary,
undefined,
DeleteUserRequestBody
> = async (request, response) => {
const {
body: { uuids: rawUserUuidList } = {},
params: { userUuid },
user: { name: sessionUserName } = {},
} = request;
if (sessionUserName !== 'admin') return response.status(401).send();
const userUuidList = sanitize(rawUserUuidList, 'string[]');
const ulist = userUuidList.length > 0 ? userUuidList : [userUuid];
@ -45,7 +48,10 @@ export const deleteUser: RequestHandler<
const wcode = await write(
`UPDATE users
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}`);

@ -1,19 +1,35 @@
import { DELETED } from '../../consts';
import buildGetRequestHandler from '../buildGetRequestHandler';
import { buildQueryResultReducer } from '../../buildQueryResultModifier';
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 = `
SELECT
use.user_name,
use.user_uuid
FROM users AS use;`;
a.user_name,
a.user_uuid
FROM users AS a
WHERE a.user_algorithm != '${DELETED}'
${condLimitRegular};`;
const afterQueryReturn: QueryResultModifierFunction | undefined =
buildQueryResultReducer<
Record<string, { userName: string; userUUID: string }>
>((previous, [userName, userUUID]) => {
previous[userUUID] = {
>((previous, [userName, userUuid]) => {
const key = userUuid === sessionUserUuid ? 'current' : userUuid;
previous[key] = {
userName,
userUUID,
userUUID: userUuid,
};
return previous;

@ -1,2 +1,4 @@
export * from './createUser';
export * from './deleteUser';
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 { 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 = {
fail?: string | ((...args: Parameters<Handler>) => void);
fail?: string | AssertAuthenticationFailFunction;
failReturnTo?: boolean | string;
succeed?: string | ((...args: Parameters<Handler>) => void);
succeed?: string | AssertAuthenticationSucceedFunction;
};
type AssertAuthenticationFunction = (
@ -13,21 +22,11 @@ type AssertAuthenticationFunction = (
) => Handler;
export const assertAuthentication: AssertAuthenticationFunction = ({
fail: initFail = (request, response) => response.status(404).send(),
fail: initFail = (rt, rq, response) => response.status(404).send(),
failReturnTo,
succeed: initSucceed = (request, response, next) => next(),
}: AssertAuthenticationOptions = {}) => {
const fail: (...args: Parameters<Handler>) => void =
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;
let getReturnTo: ((...args: HandlerParameters) => string) | undefined;
if (failReturnTo === true) {
getReturnTo = ({ originalUrl, url }) => originalUrl || url;
@ -35,6 +34,17 @@ export const assertAuthentication: AssertAuthenticationFunction = ({
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) => {
const { 0: request } = args;
const { originalUrl, session } = request;
@ -42,13 +52,17 @@ export const assertAuthentication: AssertAuthenticationFunction = ({
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(
`Unauthenticated access to ${originalUrl}; set return to ${session.returnTo}`,
);
session.returnTo = rt;
}
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 { Strategy as LocalStrategy } from 'passport-local';
import { DELETED } from './lib/consts';
import { DELETED } from '../lib/consts';
import { query, sub } from './lib/accessModule';
import { sanitize } from './lib/sanitize';
import { stdout } from './lib/shell';
import { encrypt, query } from '../lib/accessModule';
import { sanitize } from '../lib/sanitize';
import { stdout } from '../lib/shell';
passport.use(
'login',
@ -31,8 +31,9 @@ passport.use(
user_algorithm,
user_hash_count
FROM users
WHERE user_algorithm != 'DELETED'
WHERE user_algorithm != '${DELETED}'
AND user_name = '${username}'
ORDER BY modified_date DESC
LIMIT 1;`,
);
} catch (queryError) {
@ -47,24 +48,14 @@ passport.use(
0: [userUuid, , userPasswordHash, userSalt, userAlgorithm, userHashCount],
} = rows;
let encryptResult: {
user_password_hash: string;
user_salt: string;
user_hash_count: number;
user_algorithm: string;
};
let encryptResult: Encrypted;
try {
[encryptResult] = await sub('encrypt_password', {
params: [
{
algorithm: userAlgorithm,
hash_count: userHashCount,
password,
salt: userSalt,
},
],
pre: ['Account'],
encryptResult = await encrypt({
algorithm: userAlgorithm,
hash_count: userHashCount,
password,
salt: userSalt,
});
} catch (subError) {
return done(subError);

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

@ -1,9 +1,24 @@
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();
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;

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

@ -2,9 +2,19 @@ import express from 'express';
import {
getHostSSH,
poweroffHost,
rebootHost,
joinAn,
leaveAn,
manageVncSshTunnel,
poweroffStriker,
rebootStriker,
runManifest,
setMapNetwork,
startAn,
startServer,
startSubnode,
stopAn,
stopServer,
stopSubnode,
updateSystem,
} from '../lib/request_handlers/command';
@ -12,9 +22,20 @@ const router = express.Router();
router
.put('/inquire-host', getHostSSH)
.put('/poweroff-host', poweroffHost)
.put('/reboot-host', rebootHost)
.put('/join-an/:uuid', joinAn)
.put('/leave-an/:uuid', leaveAn)
.put('/vnc-pipe', manageVncSshTunnel)
.put('/poweroff-host', poweroffStriker)
.put('/reboot-host', rebootStriker)
.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);
export default router;

@ -1,9 +1,20 @@
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();
router.get('/', getFence).get('/template', getFenceTemplate);
router
.delete('/:uuid?', deleteFence)
.get('/', getFence)
.get('/template', getFenceTemplate)
.post('/', createFence)
.put('/:uuid', updateFence);
export default router;

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

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

Loading…
Cancel
Save