Merge pull request #345 from ylei-tsubame/build-login

Web UI: add login facilities and interface
main
Digimer 2 years ago committed by GitHub
commit d9109db946
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 16
      Anvil/Tools/Account.pm
  2. 43
      anvil.spec.in
  3. 303
      striker-ui-api/package-lock.json
  4. 10
      striker-ui-api/package.json
  5. 42
      striker-ui-api/src/app.ts
  6. 27
      striker-ui-api/src/index.ts
  7. 379
      striker-ui-api/src/lib/accessModule.ts
  8. 55
      striker-ui-api/src/lib/assertAuthentication.ts
  9. 1
      striker-ui-api/src/lib/consts/AN_VARIABLE_NAME_LIST.ts
  10. 3
      striker-ui-api/src/lib/consts/API_ROOT_PATH.ts
  11. 1
      striker-ui-api/src/lib/consts/DELETED.ts
  12. 2
      striker-ui-api/src/lib/consts/EXIT_CODE_LIST.ts
  13. 5
      striker-ui-api/src/lib/consts/PROCESS_OWNER.ts
  14. 22
      striker-ui-api/src/lib/consts/REG_EXP_PATTERNS.ts
  15. 8
      striker-ui-api/src/lib/consts/SERVER_PATHS.ts
  16. 4
      striker-ui-api/src/lib/consts/SERVER_PORT.ts
  17. 10
      striker-ui-api/src/lib/consts/index.ts
  18. 1
      striker-ui-api/src/lib/formatSql.ts
  19. 56
      striker-ui-api/src/lib/getSessionSecret.ts
  20. 10
      striker-ui-api/src/lib/isObject.ts
  21. 3
      striker-ui-api/src/lib/request_handlers/anvil/buildQueryAnvilDetail.ts
  22. 2
      striker-ui-api/src/lib/request_handlers/auth/index.ts
  23. 18
      striker-ui-api/src/lib/request_handlers/auth/login.ts
  24. 17
      striker-ui-api/src/lib/request_handlers/auth/logout.ts
  25. 55
      striker-ui-api/src/lib/request_handlers/buildGetRequestHandler.ts
  26. 18
      striker-ui-api/src/lib/request_handlers/command/buildHostPowerHandler.ts
  27. 93
      striker-ui-api/src/lib/request_handlers/command/getHostSSH.ts
  28. 68
      striker-ui-api/src/lib/request_handlers/command/runManifest.ts
  29. 8
      striker-ui-api/src/lib/request_handlers/command/updateSystem.ts
  30. 10
      striker-ui-api/src/lib/request_handlers/fence/getFenceTemplate.ts
  31. 11
      striker-ui-api/src/lib/request_handlers/file/buildQueryFileDetail.ts
  32. 16
      striker-ui-api/src/lib/request_handlers/file/createFile.ts
  33. 29
      striker-ui-api/src/lib/request_handlers/file/deleteFile.ts
  34. 10
      striker-ui-api/src/lib/request_handlers/file/getFile.ts
  35. 6
      striker-ui-api/src/lib/request_handlers/file/getFileDetail.ts
  36. 5
      striker-ui-api/src/lib/request_handlers/file/index.ts
  37. 134
      striker-ui-api/src/lib/request_handlers/file/updateFile.ts
  38. 101
      striker-ui-api/src/lib/request_handlers/host/configStriker.ts
  39. 96
      striker-ui-api/src/lib/request_handlers/host/createHostConnection.ts
  40. 28
      striker-ui-api/src/lib/request_handlers/host/deleteHostConnection.ts
  41. 8
      striker-ui-api/src/lib/request_handlers/host/getHostConnection.ts
  42. 18
      striker-ui-api/src/lib/request_handlers/host/prepareHost.ts
  43. 38
      striker-ui-api/src/lib/request_handlers/host/setHostInstallTarget.ts
  44. 37
      striker-ui-api/src/lib/request_handlers/manifest/buildManifest.ts
  45. 4
      striker-ui-api/src/lib/request_handlers/manifest/createManifest.ts
  46. 14
      striker-ui-api/src/lib/request_handlers/manifest/deleteManifest.ts
  47. 19
      striker-ui-api/src/lib/request_handlers/manifest/getManifestDetail.ts
  48. 21
      striker-ui-api/src/lib/request_handlers/manifest/getManifestTemplate.ts
  49. 4
      striker-ui-api/src/lib/request_handlers/manifest/updateManifest.ts
  50. 145
      striker-ui-api/src/lib/request_handlers/server/createServer.ts
  51. 3
      striker-ui-api/src/lib/request_handlers/server/getServer.ts
  52. 37
      striker-ui-api/src/lib/request_handlers/server/getServerDetail.ts
  53. 24
      striker-ui-api/src/lib/request_handlers/ssh-key/deleteSSHKeyConflict.ts
  54. 11
      striker-ui-api/src/lib/request_handlers/ssh-key/getSSHKeyConflict.ts
  55. 2
      striker-ui-api/src/lib/request_handlers/ups/getUPS.ts
  56. 13
      striker-ui-api/src/lib/request_handlers/ups/getUPSTemplate.ts
  57. 59
      striker-ui-api/src/lib/request_handlers/user/deleteUser.ts
  58. 1
      striker-ui-api/src/lib/request_handlers/user/index.ts
  59. 44
      striker-ui-api/src/lib/rrouters.ts
  60. 29
      striker-ui-api/src/lib/sanitize.ts
  61. 22
      striker-ui-api/src/lib/shell.ts
  62. 28
      striker-ui-api/src/lib/traverse.ts
  63. 127
      striker-ui-api/src/passport.ts
  64. 13
      striker-ui-api/src/routes/auth.ts
  65. 8
      striker-ui-api/src/routes/echo.ts
  66. 199
      striker-ui-api/src/routes/file.ts
  67. 38
      striker-ui-api/src/routes/index.ts
  68. 2
      striker-ui-api/src/routes/manifest.ts
  69. 51
      striker-ui-api/src/routes/static.ts
  70. 7
      striker-ui-api/src/routes/user.ts
  71. 201
      striker-ui-api/src/session.ts
  72. 12
      striker-ui-api/src/types/AccessModule.d.ts
  73. 8
      striker-ui-api/src/types/AnvilOverview.d.ts
  74. 3
      striker-ui-api/src/types/AnvilSyncSharedFunction.d.ts
  75. 9
      striker-ui-api/src/types/ApiAn.d.ts
  76. 4
      striker-ui-api/src/types/ApiAuth.d.ts
  77. 15
      striker-ui-api/src/types/ApiCommand.d.ts
  78. 0
      striker-ui-api/src/types/ApiFence.d.ts
  79. 89
      striker-ui-api/src/types/ApiHost.d.ts
  80. 0
      striker-ui-api/src/types/ApiManifest.d.ts
  81. 0
      striker-ui-api/src/types/ApiNetworkInterface.d.ts
  82. 0
      striker-ui-api/src/types/ApiServer.d.ts
  83. 12
      striker-ui-api/src/types/ApiSshKey.d.ts
  84. 4
      striker-ui-api/src/types/ApiUps.d.ts
  85. 7
      striker-ui-api/src/types/ApiUser.d.ts
  86. 0
      striker-ui-api/src/types/BuildGetRequestHandlerFunction.d.ts
  87. 2
      striker-ui-api/src/types/BuildQueryFunction.d.ts
  88. 0
      striker-ui-api/src/types/CallFunction.d.ts
  89. 11
      striker-ui-api/src/types/CreateHostConnectionRequestBody.d.ts
  90. 9
      striker-ui-api/src/types/DBInsertOrUpdateFunctionCommon.d.ts
  91. 3
      striker-ui-api/src/types/DBJobAnvilSyncSharedOptions.d.ts
  92. 3
      striker-ui-api/src/types/DeleteHostConnectionRequestBody.d.ts
  93. 1
      striker-ui-api/src/types/DeleteSSHKeyConflictRequestBody.d.ts
  94. 5
      striker-ui-api/src/types/ExecModuleSubroutineFunction.d.ts
  95. 2
      striker-ui-api/src/types/GetAnvilDataFunction.d.ts
  96. 16
      striker-ui-api/src/types/GetPeerDataFunction.d.ts
  97. 25
      striker-ui-api/src/types/HostConnectionOverview.d.ts
  98. 6
      striker-ui-api/src/types/HostOverview.d.ts
  99. 19
      striker-ui-api/src/types/InitializeStrikerForm.d.ts
  100. 4
      striker-ui-api/src/types/InsertOrUpdateJobFunction.d.ts
  101. Some files were not shown because too many files have changed in this diff Show More

@ -253,10 +253,14 @@ sub login
my $self = shift;
my $parameter = shift;
my $anvil = $self->parent;
my $debug = defined $parameter->{debug} ? $parameter->{debug} : 3;
my $debug = defined $parameter->{debug} ? $parameter->{debug} : 3;
my $password = $parameter->{password} // $anvil->data->{cgi}{password}{value};
my $username = $parameter->{username} // $anvil->data->{cgi}{username}{value};
$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => $debug, key => "log_0125", variables => { method => "Account->login()" }});
if ((not $anvil->data->{cgi}{username}{value}) or (not $anvil->data->{cgi}{password}{value}))
if ((not $username) or (not $password))
{
# The user forgot something...
$anvil->data->{form}{error_massage} = $anvil->Template->get({file => "main.html", name => "error_message", variables => { error_message => $anvil->Words->string({key => "error_0027"}) }});
@ -275,7 +279,7 @@ FROM
WHERE
user_algorithm != 'DELETED'
AND
user_name = ".$anvil->Database->quote($anvil->data->{cgi}{username}{value})."
user_name = ".$anvil->Database->quote($username)."
;";
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { query => $query }});
@ -309,7 +313,7 @@ AND
# Test the passed-in password.
my $test_password_answer = $anvil->Account->encrypt_password({
debug => 2,
password => $anvil->data->{cgi}{password}{value},
password => $password,
salt => $user_salt,
algorithm => $user_algorithm,
hash_count => $user_hash_count,
@ -345,7 +349,7 @@ AND
});
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { session_uuid => $session_uuid }});
$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 1, key => "log_0183", variables => { user => $anvil->data->{cgi}{username}{value} }});
$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 1, key => "log_0183", variables => { user => $username }});
$anvil->Account->_write_cookies({
debug => $debug,
hash => $session_hash,
@ -360,7 +364,7 @@ AND
$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 1, key => "log_0184", variables => {
user_agent => $ENV{HTTP_USER_AGENT} ? $ENV{HTTP_USER_AGENT} : "#!string!log_0185!#",
source_ip => $ENV{REMOTE_ADDR} ? $ENV{REMOTE_ADDR} : "#!string!log_0185!#",
user => $anvil->data->{cgi}{username}{value},
user => $username,
}});
# Slow them down a bit...

@ -5,6 +5,7 @@
%define debug_package %{nil}
%define anviluser admin
%define anvilgroup admin
%define suiapi striker-ui-api
Name: anvil
Version: @version@
@ -134,7 +135,6 @@ Requires: firefox
Requires: gcc
Requires: gdm
Requires: gnome-terminal
Requires: httpd
Requires: nmap
Requires: nodejs
Requires: openssh-askpass
@ -243,14 +243,32 @@ setenforce 0
systemctl enable --now chronyd.service
systemctl enable --now anvil-daemon.service
systemctl enable --now scancore.service
systemctl enable --now striker-ui-api.service
systemctl restart striker-ui-api.service
%pre striker
getent passwd %{suiapi} >/dev/null \
|| useradd \
--comment "Striker UI API" \
--home-dir %{_datadir}/%{suiapi} \
--shell %{_sbindir}/nologin \
--user-group \
%{suiapi}
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}: %{_localstatedir}/www
fi
%post striker
### NOTE: PostgreSQL is initialized and enabled by striker-prep-database later.
echo "Enabling and starting apache."
systemctl enable httpd.service
systemctl start httpd.service
# Always reload to handle service file changes.
systemctl daemon-reload
systemctl enable %{suiapi}.service
# Striker UI API needs explicit restart for changes to take effect.
systemctl restart %{suiapi}.service
restorecon -rv /%{_localstatedir}/www
if ! $(ls -l /etc/systemd/system/default.target | grep -q graphical);
then
@ -329,6 +347,11 @@ touch /etc/anvil/type.dr
# sed -i.anvil 's/SELINUX=permissive/SELINUX=enforcing/' /etc/selinux/config
# setenforce 1
%preun striker
if [ $1 == 0 ]; then # 0=Uninstall, 1=First install, >1=Upgrade (version count)
systemctl disable --now %{suiapi}.service
fi
%postun striker
### TODO: Stopping postgres breaks the Anvil! during OS updates. Need to find a
### way to run this only during uninstalls, and not during updates.
@ -340,11 +363,13 @@ touch /etc/anvil/type.dr
# firewall-cmd --zone=public --remove-service=postgresql
# firewall-cmd --zone=public --remove-service=postgresql --permanent
# echo "Disabling and stopping postgresql-9.6."
# systemctl disable httpd.service
# systemctl stop httpd.service
# systemctl disable postgresql.service
# systemctl stop postgresql.service
if [ $1 == 0 ]; then # 0=Uninstall
systemctl daemon-reload
fi
# Remove the system type file.
if [ -e '/etc/anvil/type.striker' ]
then
@ -376,7 +401,7 @@ fi
%{_sbindir}/*
%{_sysconfdir}/anvil/anvil.version
%{_datadir}/perl5/*
%{_datadir}/striker-ui-api/*
%{_datadir}/%{suiapi}/*
%{_mandir}/*
%files striker

@ -8,9 +8,14 @@
"name": "striker-ui-api",
"version": "0.1.0",
"dependencies": {
"core-js": "^3.30.1",
"cors": "^2.8.5",
"express": "^4.18.2",
"multer": "^1.4.4"
"express-session": "^1.17.3",
"multer": "^1.4.4",
"passport": "^0.6.0",
"passport-local": "^1.0.0",
"regenerator-runtime": "^0.13.11"
},
"devDependencies": {
"@babel/core": "^7.17.8",
@ -18,8 +23,11 @@
"@babel/preset-typescript": "^7.16.7",
"@types/cors": "^2.8.12",
"@types/express": "^4.17.13",
"@types/express-session": "^1.17.7",
"@types/multer": "^1.4.7",
"@types/node": "^17.0.22",
"@types/passport": "^1.0.12",
"@types/passport-local": "^1.0.35",
"@typescript-eslint/eslint-plugin": "^5.16.0",
"@typescript-eslint/parser": "^5.16.0",
"babel-loader": "^8.2.3",
@ -2412,6 +2420,15 @@
"@types/range-parser": "*"
}
},
"node_modules/@types/express-session": {
"version": "1.17.7",
"resolved": "https://registry.npmjs.org/@types/express-session/-/express-session-1.17.7.tgz",
"integrity": "sha512-L25080PBYoRLu472HY/HNCxaXY8AaGgqGC8/p/8+BYMhG0RDOLQ1wpXOpAzr4Gi5TGozTKyJv5BVODM5UNyVMw==",
"dev": true,
"dependencies": {
"@types/express": "*"
}
},
"node_modules/@types/json-schema": {
"version": "7.0.10",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.10.tgz",
@ -2445,6 +2462,36 @@
"integrity": "sha512-8FwbVoG4fy+ykY86XCAclKZDORttqE5/s7dyWZKLXTdv3vRy5HozBEinG5IqhvPXXzIZEcTVbuHlQEI6iuwcmw==",
"dev": true
},
"node_modules/@types/passport": {
"version": "1.0.12",
"resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.12.tgz",
"integrity": "sha512-QFdJ2TiAEoXfEQSNDISJR1Tm51I78CymqcBa8imbjo6dNNu+l2huDxxbDEIoFIwOSKMkOfHEikyDuZ38WwWsmw==",
"dev": true,
"dependencies": {
"@types/express": "*"
}
},
"node_modules/@types/passport-local": {
"version": "1.0.35",
"resolved": "https://registry.npmjs.org/@types/passport-local/-/passport-local-1.0.35.tgz",
"integrity": "sha512-K4eLTJ8R0yYW8TvCqkjB0pTKoqfUSdl5PfZdidTjV2ETV3604fQxtY6BHKjQWAx50WUS0lqzBvKv3LoI1ZBPeA==",
"dev": true,
"dependencies": {
"@types/express": "*",
"@types/passport": "*",
"@types/passport-strategy": "*"
}
},
"node_modules/@types/passport-strategy": {
"version": "0.2.35",
"resolved": "https://registry.npmjs.org/@types/passport-strategy/-/passport-strategy-0.2.35.tgz",
"integrity": "sha512-o5D19Jy2XPFoX2rKApykY15et3Apgax00RRLf0RUotPDUsYrQa7x4howLYr9El2mlUApHmCMv5CZ1IXqKFQ2+g==",
"dev": true,
"dependencies": {
"@types/express": "*",
"@types/passport": "*"
}
},
"node_modules/@types/qs": {
"version": "6.9.7",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz",
@ -3504,6 +3551,16 @@
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
"integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw="
},
"node_modules/core-js": {
"version": "3.30.1",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.30.1.tgz",
"integrity": "sha512-ZNS5nbiSwDTq4hFosEDqm65izl2CWmLz0hARJMyNQBgkUZMIF51cQiMvIQKA6hvuaeWxQDP3hEedM1JZIgTldQ==",
"hasInstallScript": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/core-js"
}
},
"node_modules/core-js-compat": {
"version": "3.21.1",
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.21.1.tgz",
@ -4272,6 +4329,51 @@
"node": ">= 0.10.0"
}
},
"node_modules/express-session": {
"version": "1.17.3",
"resolved": "https://registry.npmjs.org/express-session/-/express-session-1.17.3.tgz",
"integrity": "sha512-4+otWXlShYlG1Ma+2Jnn+xgKUZTMJ5QD3YvfilX3AcocOAbIkVylSWEklzALe/+Pu4qV6TYBj5GwOBFfdKqLBw==",
"dependencies": {
"cookie": "0.4.2",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "~2.0.0",
"on-headers": "~1.0.2",
"parseurl": "~1.3.3",
"safe-buffer": "5.2.1",
"uid-safe": "~2.1.5"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/express-session/node_modules/cookie": {
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz",
"integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/express-session/node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
]
},
"node_modules/express/node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
@ -5485,6 +5587,14 @@
"node": ">= 0.8"
}
},
"node_modules/on-headers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz",
"integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
@ -5582,6 +5692,42 @@
"node": ">= 0.8"
}
},
"node_modules/passport": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/passport/-/passport-0.6.0.tgz",
"integrity": "sha512-0fe+p3ZnrWRW74fe8+SvCyf4a3Pb2/h7gFkQ8yTJpAO50gDzlfjZUZTO1k5Eg9kUct22OxHLqDZoKUWRHOh9ug==",
"dependencies": {
"passport-strategy": "1.x.x",
"pause": "0.0.1",
"utils-merge": "^1.0.1"
},
"engines": {
"node": ">= 0.4.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/jaredhanson"
}
},
"node_modules/passport-local": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/passport-local/-/passport-local-1.0.0.tgz",
"integrity": "sha512-9wCE6qKznvf9mQYYbgJ3sVOHmCWoUNMVFoZzNoznmISbhnNNPhN9xfY3sLmScHMetEJeoY7CXwfhCe7argfQow==",
"dependencies": {
"passport-strategy": "1.x.x"
},
"engines": {
"node": ">= 0.4.0"
}
},
"node_modules/passport-strategy": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz",
"integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==",
"engines": {
"node": ">= 0.4.0"
}
},
"node_modules/path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@ -5629,6 +5775,11 @@
"node": ">=8"
}
},
"node_modules/pause": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz",
"integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg=="
},
"node_modules/picocolors": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
@ -5740,6 +5891,14 @@
}
]
},
"node_modules/random-bytes": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz",
"integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/randombytes": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
@ -5816,10 +5975,9 @@
}
},
"node_modules/regenerator-runtime": {
"version": "0.13.9",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz",
"integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==",
"dev": true
"version": "0.13.11",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg=="
},
"node_modules/regenerator-transform": {
"version": "0.14.5",
@ -6446,6 +6604,17 @@
"node": ">=4.2.0"
}
},
"node_modules/uid-safe": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz",
"integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==",
"dependencies": {
"random-bytes": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/unbox-primitive": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz",
@ -8507,6 +8676,15 @@
"@types/range-parser": "*"
}
},
"@types/express-session": {
"version": "1.17.7",
"resolved": "https://registry.npmjs.org/@types/express-session/-/express-session-1.17.7.tgz",
"integrity": "sha512-L25080PBYoRLu472HY/HNCxaXY8AaGgqGC8/p/8+BYMhG0RDOLQ1wpXOpAzr4Gi5TGozTKyJv5BVODM5UNyVMw==",
"dev": true,
"requires": {
"@types/express": "*"
}
},
"@types/json-schema": {
"version": "7.0.10",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.10.tgz",
@ -8540,6 +8718,36 @@
"integrity": "sha512-8FwbVoG4fy+ykY86XCAclKZDORttqE5/s7dyWZKLXTdv3vRy5HozBEinG5IqhvPXXzIZEcTVbuHlQEI6iuwcmw==",
"dev": true
},
"@types/passport": {
"version": "1.0.12",
"resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.12.tgz",
"integrity": "sha512-QFdJ2TiAEoXfEQSNDISJR1Tm51I78CymqcBa8imbjo6dNNu+l2huDxxbDEIoFIwOSKMkOfHEikyDuZ38WwWsmw==",
"dev": true,
"requires": {
"@types/express": "*"
}
},
"@types/passport-local": {
"version": "1.0.35",
"resolved": "https://registry.npmjs.org/@types/passport-local/-/passport-local-1.0.35.tgz",
"integrity": "sha512-K4eLTJ8R0yYW8TvCqkjB0pTKoqfUSdl5PfZdidTjV2ETV3604fQxtY6BHKjQWAx50WUS0lqzBvKv3LoI1ZBPeA==",
"dev": true,
"requires": {
"@types/express": "*",
"@types/passport": "*",
"@types/passport-strategy": "*"
}
},
"@types/passport-strategy": {
"version": "0.2.35",
"resolved": "https://registry.npmjs.org/@types/passport-strategy/-/passport-strategy-0.2.35.tgz",
"integrity": "sha512-o5D19Jy2XPFoX2rKApykY15et3Apgax00RRLf0RUotPDUsYrQa7x4howLYr9El2mlUApHmCMv5CZ1IXqKFQ2+g==",
"dev": true,
"requires": {
"@types/express": "*",
"@types/passport": "*"
}
},
"@types/qs": {
"version": "6.9.7",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz",
@ -9329,6 +9537,11 @@
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
"integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw="
},
"core-js": {
"version": "3.30.1",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.30.1.tgz",
"integrity": "sha512-ZNS5nbiSwDTq4hFosEDqm65izl2CWmLz0hARJMyNQBgkUZMIF51cQiMvIQKA6hvuaeWxQDP3hEedM1JZIgTldQ=="
},
"core-js-compat": {
"version": "3.21.1",
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.21.1.tgz",
@ -9937,6 +10150,33 @@
}
}
},
"express-session": {
"version": "1.17.3",
"resolved": "https://registry.npmjs.org/express-session/-/express-session-1.17.3.tgz",
"integrity": "sha512-4+otWXlShYlG1Ma+2Jnn+xgKUZTMJ5QD3YvfilX3AcocOAbIkVylSWEklzALe/+Pu4qV6TYBj5GwOBFfdKqLBw==",
"requires": {
"cookie": "0.4.2",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "~2.0.0",
"on-headers": "~1.0.2",
"parseurl": "~1.3.3",
"safe-buffer": "5.2.1",
"uid-safe": "~2.1.5"
},
"dependencies": {
"cookie": {
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz",
"integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA=="
},
"safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
}
}
},
"fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@ -10808,6 +11048,11 @@
"ee-first": "1.1.1"
}
},
"on-headers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz",
"integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA=="
},
"once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
@ -10878,6 +11123,29 @@
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="
},
"passport": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/passport/-/passport-0.6.0.tgz",
"integrity": "sha512-0fe+p3ZnrWRW74fe8+SvCyf4a3Pb2/h7gFkQ8yTJpAO50gDzlfjZUZTO1k5Eg9kUct22OxHLqDZoKUWRHOh9ug==",
"requires": {
"passport-strategy": "1.x.x",
"pause": "0.0.1",
"utils-merge": "^1.0.1"
}
},
"passport-local": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/passport-local/-/passport-local-1.0.0.tgz",
"integrity": "sha512-9wCE6qKznvf9mQYYbgJ3sVOHmCWoUNMVFoZzNoznmISbhnNNPhN9xfY3sLmScHMetEJeoY7CXwfhCe7argfQow==",
"requires": {
"passport-strategy": "1.x.x"
}
},
"passport-strategy": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz",
"integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA=="
},
"path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@ -10913,6 +11181,11 @@
"integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
"dev": true
},
"pause": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz",
"integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg=="
},
"picocolors": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
@ -10980,6 +11253,11 @@
"integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
"dev": true
},
"random-bytes": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz",
"integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ=="
},
"randombytes": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
@ -11044,10 +11322,9 @@
}
},
"regenerator-runtime": {
"version": "0.13.9",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz",
"integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==",
"dev": true
"version": "0.13.11",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg=="
},
"regenerator-transform": {
"version": "0.14.5",
@ -11498,6 +11775,14 @@
"integrity": "sha512-HM/hFigTBHZhLXshn9sN37H085+hQGeJHJ/X7LpBWLID/fbc2acUMfU+lGD98X81sKP+pFa9f0DZmCwB9GnbAg==",
"dev": true
},
"uid-safe": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz",
"integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==",
"requires": {
"random-bytes": "~1.0.0"
}
},
"unbox-primitive": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz",

@ -11,9 +11,14 @@
"start": "npm run build && node out/index.js"
},
"dependencies": {
"core-js": "^3.30.1",
"cors": "^2.8.5",
"express": "^4.18.2",
"multer": "^1.4.4"
"express-session": "^1.17.3",
"multer": "^1.4.4",
"passport": "^0.6.0",
"passport-local": "^1.0.0",
"regenerator-runtime": "^0.13.11"
},
"devDependencies": {
"@babel/core": "^7.17.8",
@ -21,8 +26,11 @@
"@babel/preset-typescript": "^7.16.7",
"@types/cors": "^2.8.12",
"@types/express": "^4.17.13",
"@types/express-session": "^1.17.7",
"@types/multer": "^1.4.7",
"@types/node": "^17.0.22",
"@types/passport": "^1.0.12",
"@types/passport-local": "^1.0.35",
"@typescript-eslint/eslint-plugin": "^5.16.0",
"@typescript-eslint/parser": "^5.16.0",
"babel-loader": "^8.2.3",

@ -1,18 +1,38 @@
import cors from 'cors';
import express from 'express';
import path from 'path';
import API_ROOT_PATH from './lib/consts/API_ROOT_PATH';
import express, { json } from 'express';
import { guardApi } from './lib/assertAuthentication';
import passport from './passport';
import routes from './routes';
import { rrouters } from './lib/rrouters';
import session from './session';
export default (async () => {
const app = express();
app.use(json());
app.use(
cors({
origin: true,
credentials: true,
}),
);
// Add session handler to the chain **after** adding other handlers that do
// not depend on session(s).
app.use(await session);
const app = express();
app.use(passport.initialize());
app.use(passport.authenticate('session'));
app.use(express.json());
app.use(cors());
rrouters(app, routes.private, {
assign: (router) => [guardApi, router],
route: '/api',
});
rrouters(app, routes.public, { route: '/api' });
Object.entries(routes).forEach(([route, router]) => {
app.use(path.join(API_ROOT_PATH, route), router);
});
app.use(routes.static);
export default app;
return app;
})();

@ -1,7 +1,26 @@
import { getgid, getuid, setgid, setuid } from 'process';
import { PGID, PUID, PORT, ECODE_DROP_PRIVILEGES } from './lib/consts';
import app from './app';
import { stderr, stdout } from './lib/shell';
(async () => {
stdout(`Starting process with ownership ${getuid()}:${getgid()}`);
(await app).listen(PORT, () => {
try {
// Group must be set before user to avoid permission error.
setgid(PGID);
setuid(PUID);
stdout(`Process ownership changed to ${getuid()}:${getgid()}.`);
} catch (error) {
stderr(`Failed to change process ownership; CAUSE: ${error}`);
import SERVER_PORT from './lib/consts/SERVER_PORT';
process.exit(ECODE_DROP_PRIVILEGES);
}
app.listen(SERVER_PORT, () => {
console.log(`Listening on localhost:${SERVER_PORT}.`);
});
stdout(`Listening on localhost:${PORT}.`);
});
})();

@ -1,109 +1,214 @@
import { spawnSync, SpawnSyncOptions } from 'child_process';
import { ChildProcess, spawn, SpawnOptions } from 'child_process';
import EventEmitter from 'events';
import { readFileSync } from 'fs';
import { SERVER_PATHS, PGID, PUID } from './consts';
import { formatSql } from './formatSql';
import {
date,
stderr as sherr,
stdout as shout,
stdoutVar as shvar,
uuid,
} from './shell';
class Access extends EventEmitter {
private ps: ChildProcess;
private queue: string[] = [];
constructor({
eventEmitterOptions = {},
spawnOptions = {},
}: {
eventEmitterOptions?: ConstructorParameters<typeof EventEmitter>[0];
spawnOptions?: SpawnOptions;
} = {}) {
super(eventEmitterOptions);
this.ps = this.start(spawnOptions);
}
import SERVER_PATHS from './consts/SERVER_PATHS';
private start({
args = [],
gid = PGID,
stdio = 'pipe',
timeout = 10000,
uid = PUID,
...restSpawnOptions
}: AccessStartOptions = {}) {
shvar(
{ gid, stdio, timeout, uid, ...restSpawnOptions },
`Starting anvil-access-module daemon with options: `,
);
import { stderr as sherr, stdout as shout } from './shell';
const ps = spawn(SERVER_PATHS.usr.sbin['anvil-access-module'].self, args, {
gid,
stdio,
timeout,
uid,
...restSpawnOptions,
});
const formatQuery = (query: string) => query.replace(/\s+/g, ' ');
let stderr = '';
let stdout = '';
const execAnvilAccessModule = (
args: string[],
options: SpawnSyncOptions = {
encoding: 'utf-8',
timeout: 10000,
},
) => {
const { error, stdout, stderr } = spawnSync(
SERVER_PATHS.usr.sbin['anvil-access-module'].self,
args,
options,
);
ps.stderr?.setEncoding('utf-8').on('data', (chunk: string) => {
stderr += chunk;
const scriptId: string | undefined = this.queue[0];
if (scriptId) {
sherr(`${Access.event(scriptId, 'stderr')}: ${stderr}`);
stderr = '';
}
});
ps.stdout?.setEncoding('utf-8').on('data', (chunk: string) => {
stdout += chunk;
let nindex: number = stdout.indexOf('\n');
if (error) {
throw error;
// 1. ~a is the shorthand for -(a + 1)
// 2. negatives are evaluated to true
while (~nindex) {
const scriptId = this.queue.shift();
if (scriptId) this.emit(scriptId, stdout.substring(0, nindex));
stdout = stdout.substring(nindex + 1);
nindex = stdout.indexOf('\n');
}
});
return ps;
}
if (stderr.length > 0) {
throw new Error(stderr.toString());
private stop() {
this.ps.once('error', () => !this.ps.killed && this.ps.kill('SIGKILL'));
this.ps.kill();
}
let output;
private restart(options?: AccessStartOptions) {
this.ps.once('close', () => this.start(options));
try {
output = JSON.parse(stdout.toString());
} catch (stdoutParseError) {
output = stdout;
this.stop();
}
sherr(
`Failed to parse anvil-access-module output [${output}]; CAUSE: [${stdoutParseError}]`,
);
private static event(scriptId: string, category: 'stderr'): string {
return `${scriptId}-${category}`;
}
return {
stdout: output,
};
};
public interact<T>(command: string, ...args: string[]) {
const { stdin } = this.ps;
const scriptId = uuid();
const script = `${command} ${args.join(' ')}\n`;
const promise = new Promise<T>((resolve, reject) => {
this.once(scriptId, (data) => {
let result: T;
try {
result = JSON.parse(data);
} catch (error) {
return reject(`Failed to parse line ${scriptId}; got [${data}]`);
}
return resolve(result);
});
});
shvar({ scriptId, script }, 'Access interact: ');
this.queue.push(scriptId);
stdin?.write(script);
const execModuleSubroutine = (
subName: string,
return promise;
}
}
const access = new Access();
const subroutine = async <T extends unknown[]>(
subroutine: string,
{
spawnSyncOptions,
subModuleName,
subParams,
}: ExecModuleSubroutineOptions = {},
params = [],
pre = ['Database'],
}: {
params?: unknown[];
pre?: string[];
} = {},
) => {
const args = ['--sub', subName];
const chain = `${pre.join('->')}->${subroutine}`;
// Defaults to "Database" in anvil-access-module.
if (subModuleName) {
args.push('--sub-module', subModuleName);
}
const subParams: string[] = params.map<string>((p) => {
let result: string;
if (subParams) {
args.push('--sub-params', JSON.stringify(subParams));
}
try {
result = JSON.stringify(p);
} catch (error) {
result = String(p);
}
return `'${result}'`;
});
shout(
`...${subModuleName}->${subName} with params: ${JSON.stringify(
subParams,
null,
2,
)}`,
const { sub_results: results } = await access.interact<{ sub_results: T }>(
'x',
chain,
...subParams,
);
const { stdout } = execAnvilAccessModule(args, spawnSyncOptions);
shvar(results, `${chain} results: `);
return {
stdout: stdout['sub_results'],
};
return results;
};
const dbInsertOrUpdateJob = (
{ job_progress = 0, line = 0, ...rest }: DBJobParams,
{ spawnSyncOptions }: DBInsertOrUpdateJobOptions = {},
) =>
execModuleSubroutine('insert_or_update_jobs', {
spawnSyncOptions,
subParams: { job_progress, line, ...rest },
}).stdout;
const dbInsertOrUpdateVariable: DBInsertOrUpdateVariableFunction = (
subParams,
{ spawnSyncOptions } = {},
) =>
execModuleSubroutine('insert_or_update_variables', {
spawnSyncOptions,
subParams,
}).stdout;
const dbJobAnvilSyncShared = (
const query = <T extends (number | null | string)[][]>(script: string) =>
access.interact<T>('r', formatSql(script));
const write = async (script: string) => {
const { write_code: wcode } = await access.interact<{ write_code: number }>(
'w',
formatSql(script),
);
return wcode;
};
const insertOrUpdateJob = async ({
job_progress = 0,
line = 0,
...rest
}: JobParams) => {
const [uuid]: [string] = await subroutine('insert_or_update_jobs', {
params: [{ job_progress, line, ...rest }],
});
return uuid;
};
const insertOrUpdateVariable: InsertOrUpdateVariableFunction = async (
params,
) => {
const [uuid]: [string] = await subroutine('insert_or_update_variables', {
params: [params],
});
return uuid;
};
const anvilSyncShared = (
jobName: string,
jobData: string,
jobTitle: string,
jobDescription: string,
{ jobHostUUID }: DBJobAnvilSyncSharedOptions = { jobHostUUID: undefined },
{ jobHostUUID }: JobAnvilSyncSharedOptions = { jobHostUUID: undefined },
) => {
const subParams: DBJobParams = {
const subParams: JobParams = {
file: __filename,
job_command: SERVER_PATHS.usr.sbin['anvil-sync-shared'].self,
job_data: jobData,
@ -116,45 +221,54 @@ const dbJobAnvilSyncShared = (
subParams.job_host_uuid = jobHostUUID;
}
return dbInsertOrUpdateJob(subParams);
return insertOrUpdateJob(subParams);
};
const dbQuery = (query: string, options?: SpawnSyncOptions) => {
shout(formatQuery(query));
const refreshTimestamp = () => {
let result: string;
try {
result = date('--rfc-3339', 'ns').trim();
} catch (shError) {
throw new Error(
`Failed to get timestamp for database use; CAUSE: ${shError}`,
);
}
return execAnvilAccessModule(['--query', query], options);
return result;
};
const dbSubRefreshTimestamp = () =>
execModuleSubroutine('refresh_timestamp').stdout;
const getData = async <T>(...keys: string[]) => {
const chain = `data->${keys.join('->')}`;
const {
sub_results: [data],
} = await access.interact<{ sub_results: [T] }>('x', chain);
shvar(data, `${chain} data: `);
return data;
};
const dbWrite = (query: string, options?: SpawnSyncOptions) => {
shout(formatQuery(query));
const getFenceSpec = async () => {
await subroutine('get_fence_data', { pre: ['Striker'] });
return execAnvilAccessModule(['--query', query, '--mode', 'write'], options);
return getData<unknown>('fence_data');
};
const getAnvilData = <HashType>(
dataStruct: AnvilDataStruct,
{ predata, ...spawnSyncOptions }: GetAnvilDataOptions = {},
): HashType =>
execAnvilAccessModule(
[
'--predata',
JSON.stringify(predata),
'--data',
JSON.stringify(dataStruct),
],
spawnSyncOptions,
).stdout;
const getHostData = async () => {
await subroutine('get_hosts');
return getData<AnvilDataHostListHash>('hosts');
};
const getLocalHostName = () => {
let result: string;
try {
result = execModuleSubroutine('host_name', {
subModuleName: 'Get',
}).stdout;
result = readFileSync(SERVER_PATHS.etc.hostname.self, {
encoding: 'utf-8',
}).trim();
} catch (subError) {
throw new Error(`Failed to get local host name; CAUSE: ${subError}`);
}
@ -164,13 +278,13 @@ const getLocalHostName = () => {
return result;
};
const getLocalHostUUID = () => {
const getLocalHostUuid = () => {
let result: string;
try {
result = execModuleSubroutine('host_uuid', {
subModuleName: 'Get',
}).stdout;
result = readFileSync(SERVER_PATHS.etc.anvil['host.uuid'].self, {
encoding: 'utf-8',
}).trim();
} catch (subError) {
throw new Error(`Failed to get local host UUID; CAUSE: ${subError}`);
}
@ -180,9 +294,18 @@ const getLocalHostUUID = () => {
return result;
};
const getPeerData: GetPeerDataFunction = (
const getManifestData = async (manifestUuid?: string) => {
await subroutine('load_manifest', {
params: [{ manifest_uuid: manifestUuid }],
pre: ['Striker'],
});
return getData<AnvilDataManifestListHash>('manifests');
};
const getPeerData: GetPeerDataFunction = async (
target,
{ password, port, ...restOptions } = {},
{ password, port } = {},
) => {
const [
rawIsConnected,
@ -193,11 +316,13 @@ const getPeerData: GetPeerDataFunction = (
internet: rawIsInetConnected,
os_registered: rawIsOSRegistered,
},
] = execModuleSubroutine('get_peer_data', {
subModuleName: 'Striker',
subParams: { password, port, target },
...restOptions,
}).stdout as [connected: string, data: PeerDataHash];
]: [connected: string, data: PeerDataHash] = await subroutine(
'get_peer_data',
{
params: [{ password, port, target }],
pre: ['Striker'],
},
);
return {
hostName,
@ -209,16 +334,26 @@ const getPeerData: GetPeerDataFunction = (
};
};
const getUpsSpec = async () => {
await subroutine('get_ups_data', { pre: ['Striker'] });
return getData<AnvilDataUPSHash>('ups_data');
};
export {
dbInsertOrUpdateJob as job,
dbInsertOrUpdateVariable as variable,
dbJobAnvilSyncShared,
dbQuery,
dbSubRefreshTimestamp,
dbWrite,
getAnvilData,
insertOrUpdateJob as job,
insertOrUpdateVariable as variable,
anvilSyncShared,
refreshTimestamp as timestamp,
getData,
getFenceSpec,
getHostData,
getLocalHostName,
getLocalHostUUID,
getLocalHostUuid as getLocalHostUUID,
getManifestData,
getPeerData,
execModuleSubroutine as sub,
getUpsSpec,
query,
subroutine as sub,
write,
};

@ -0,0 +1,55 @@
import { Handler } from 'express';
import { stdout } from './shell';
type AssertAuthenticationOptions = {
fail?: string | ((...args: Parameters<Handler>) => void);
failReturnTo?: boolean | string;
succeed?: string | ((...args: Parameters<Handler>) => void);
};
type AssertAuthenticationFunction = (
options?: AssertAuthenticationOptions,
) => Handler;
export const assertAuthentication: AssertAuthenticationFunction = ({
fail: initFail = (request, 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;
if (failReturnTo === true) {
getReturnTo = ({ originalUrl, url }) => originalUrl || url;
} else if (typeof failReturnTo === 'string') {
getReturnTo = () => failReturnTo;
}
return (...args) => {
const { 0: request } = args;
const { originalUrl, session } = request;
const { passport } = session;
if (passport?.user) return succeed(...args);
session.returnTo = getReturnTo?.call(null, ...args);
stdout(
`Unauthenticated access to ${originalUrl}; set return to ${session.returnTo}`,
);
return fail(...args);
};
};
export const guardApi = assertAuthentication();

@ -0,0 +1 @@
export const VNAME_SESSION_SECRET = 'striker-ui-api::session::secret';

@ -1,3 +0,0 @@
const API_ROOT_PATH = '/api';
export default API_ROOT_PATH;

@ -0,0 +1 @@
export const DELETED = 'DELETED';

@ -0,0 +1,2 @@
export const ECODE_DROP_PRIVILEGES = 1;
export const ECODE_SESSION_SECRET = 2;

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

@ -1,23 +1,21 @@
const hex = '[0-9a-f]';
const octet = '(?:25[0-5]|(?:2[0-4]|1[0-9]|[1-9]|)[0-9])';
const alphanumeric = '[a-z0-9]';
const alphanumericDash = '[a-z0-9-]';
const ipv4 = `(?:${octet}[.]){3}${octet}`;
export const P_HEX = '[[:xdigit:]]';
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-]';
export const P_IPV4 = `(?:${P_OCTET}[.]){3}${P_OCTET}`;
export const P_UUID = `${P_HEX}{8}-${P_HEX}{4}-[1-5]${P_HEX}{3}-[89ab]${P_HEX}{3}-${P_HEX}{12}`;
export const REP_DOMAIN = new RegExp(
`^(?:${alphanumeric}(?:${alphanumericDash}{0,61}${alphanumeric})?[.])+${alphanumeric}${alphanumericDash}{0,61}${alphanumeric}$`,
`^(?:${P_ALPHANUM}(?:${P_ALPHANUM_DASH}{0,61}${P_ALPHANUM})?[.])+${P_ALPHANUM}${P_ALPHANUM_DASH}{0,61}${P_ALPHANUM}$`,
);
export const REP_INTEGER = /^\d+$/;
export const REP_IPV4 = new RegExp(`^${ipv4}$`);
export const REP_IPV4 = new RegExp(`^${P_IPV4}$`);
export const REP_IPV4_CSV = new RegExp(`(?:${ipv4},)*${ipv4}`);
export const REP_IPV4_CSV = new RegExp(`(?:${P_IPV4},)*${P_IPV4}`);
// Peaceful string is temporarily defined as a string without single-quote, double-quote, slash (/), backslash (\\), angle brackets (< >), and curly brackets ({ }).
export const REP_PEACEFUL_STRING = /^[^'"/\\><}{]+$/;
export const REP_UUID = new RegExp(
`^${hex}{8}-${hex}{4}-[1-5]${hex}{3}-[89ab]${hex}{3}-${hex}{12}$`,
'i',
);
export const REP_UUID = new RegExp(`^${P_UUID}$`);

@ -1,6 +1,10 @@
import path from 'path';
const EMPTY_SERVER_PATHS: ServerPath = {
etc: {
anvil: { 'host.uuid': {} },
hostname: {},
},
mnt: {
shared: {
incoming: {},
@ -10,10 +14,13 @@ const EMPTY_SERVER_PATHS: ServerPath = {
usr: {
bin: {
date: {},
getent: {},
mkfifo: {},
openssl: {},
psql: {},
rm: {},
sed: {},
uuidgen: {},
},
sbin: {
'anvil-access-module': {},
@ -31,6 +38,7 @@ const EMPTY_SERVER_PATHS: ServerPath = {
'striker-parse-os-list': {},
},
},
var: { www: { html: {} } },
};
const generatePaths = (

@ -1,3 +1 @@
const SERVER_PORT = process.env.SERVER_PORT ?? 8080;
export default SERVER_PORT;
export const PORT = process.env.PORT ?? 8080;

@ -0,0 +1,10 @@
import SERVER_PATHS from './SERVER_PATHS';
export { SERVER_PATHS };
export * from './AN_VARIABLE_NAME_LIST';
export * from './DELETED';
export * from './EXIT_CODE_LIST';
export * from './PROCESS_OWNER';
export * from './REG_EXP_PATTERNS';
export * from './SERVER_PORT';

@ -0,0 +1 @@
export const formatSql = (script: string) => script.replace(/\s+/g, ' ');

@ -0,0 +1,56 @@
import assert from 'assert';
import { ECODE_SESSION_SECRET, VNAME_SESSION_SECRET } from './consts';
import { query, variable } from './accessModule';
import { openssl, stderr, stdout } from './shell';
export const getSessionSecret = async (): Promise<string> => {
let sessionSecret: string;
try {
const rows: [sessionSecret: string][] = await query(
`SELECT variable_value
FROM variables
WHERE variable_name = '${VNAME_SESSION_SECRET}';`,
);
assert(rows.length > 0, 'No existing session secret found.');
({
0: [sessionSecret],
} = rows);
stdout('Found an existing session secret.');
return sessionSecret;
} catch (queryError) {
stderr(`Failed to get session secret from database; CAUSE: ${queryError}`);
}
try {
sessionSecret = openssl('rand', '-base64', '32').trim();
stdout('Generated a new session secret.');
} catch (sysError) {
stderr(`Failed to generate session secret; CAUSE: ${sysError}`);
process.exit(ECODE_SESSION_SECRET);
}
try {
const vuuid = await variable({
file: __filename,
variable_name: VNAME_SESSION_SECRET,
variable_value: sessionSecret,
});
stdout(`Recorded session secret as variable identified by ${vuuid}.`);
} catch (subError) {
stderr(`Failed to record session secret; CAUSE: ${subError}`);
process.exit(ECODE_SESSION_SECRET);
}
return sessionSecret;
};

@ -0,0 +1,10 @@
export const isObject = (value: unknown) => {
const result: { is: boolean; obj: object } = { is: false, obj: {} };
if (typeof value === 'object' && value !== null) {
result.is = true;
result.obj = value;
}
return result;
};

@ -2,6 +2,7 @@ import NODE_AND_DR_RESERVED_MEMORY_SIZE from '../../consts/NODE_AND_DR_RESERVED_
import { OS_LIST } from '../../consts/OS_LIST';
import join from '../../join';
import { stdoutVar } from '../../shell';
const buildQueryAnvilDetail = ({
anvilUUIDs = ['*'],
@ -19,7 +20,7 @@ const buildQueryAnvilDetail = ({
separator: ', ',
});
console.log(`condAnvilsUUID=[${condAnvilsUUID}]`);
stdoutVar({ condAnvilsUUID });
const buildHostQuery = ({
isSummary = false,

@ -0,0 +1,2 @@
export * from './login';
export * from './logout';

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

@ -0,0 +1,17 @@
import { RequestHandler } from 'express';
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}`);
}
response.status(scode).send();
});
};

@ -1,64 +1,53 @@
import { Request, Response } from 'express';
import { dbQuery } from '../accessModule';
import { query } from '../accessModule';
import call from '../call';
import { stderr, stdout, stdoutVar } from '../shell';
const buildGetRequestHandler =
(
query: string | BuildQueryFunction,
scriptOrCallback: string | BuildQueryFunction,
{ beforeRespond }: BuildGetRequestHandlerOptions = {},
) =>
(request: Request, response: Response) => {
console.log('Calling CLI script to get data.');
async (request: Request, response: Response) => {
stdout('Calling CLI script to get data.');
const buildQueryOptions: BuildQueryOptions = {};
let queryStdout;
let result: (number | null | string)[][];
try {
({ stdout: queryStdout } = dbQuery(
call<string>(query, {
parameters: [request, buildQueryOptions],
notCallableReturn: query,
}),
));
const sqlscript: string =
typeof scriptOrCallback === 'function'
? await scriptOrCallback(request, buildQueryOptions)
: scriptOrCallback;
result = await query(sqlscript);
} catch (queryError) {
console.log(`Failed to execute query; CAUSE: ${queryError}`);
stderr(`Failed to execute query; CAUSE: ${queryError}`);
response.status(500).send();
return;
}
console.log(
`Query stdout pre-hooks (type=[${typeof queryStdout}]): ${JSON.stringify(
queryStdout,
null,
2,
)}`,
);
stdoutVar(result, `Query stdout pre-hooks (type=[${typeof result}]): `);
const { afterQueryReturn } = buildQueryOptions;
queryStdout = call(afterQueryReturn, {
parameters: [queryStdout],
notCallableReturn: queryStdout,
result = call(afterQueryReturn, {
parameters: [result],
notCallableReturn: result,
});
queryStdout = call(beforeRespond, {
parameters: [queryStdout],
notCallableReturn: queryStdout,
result = call(beforeRespond, {
parameters: [result],
notCallableReturn: result,
});
console.log(
`Query stdout post-hooks (type=[${typeof queryStdout}]): ${JSON.stringify(
queryStdout,
null,
2,
)}`,
);
stdoutVar(result, `Query stdout post-hooks (type=[${typeof result}]): `);
response.json(queryStdout);
response.json(result);
};
export default buildGetRequestHandler;

@ -5,14 +5,14 @@ import SERVER_PATHS from '../../consts/SERVER_PATHS';
import { job } from '../../accessModule';
import { stderr } from '../../shell';
type DistinctDBJobParams = Omit<
DBJobParams,
type DistinctJobParams = Omit<
JobParams,
'file' | 'line' | 'job_data' | 'job_progress'
>;
const MANAGE_HOST_POWER_JOB_PARAMS: {
poweroff: DistinctDBJobParams;
reboot: DistinctDBJobParams;
poweroff: DistinctJobParams;
reboot: DistinctJobParams;
} = {
poweroff: {
job_command: `${SERVER_PATHS.usr.sbin['anvil-manage-power'].self} --poweroff -y`,
@ -32,21 +32,19 @@ export const buildHostPowerHandler: (
task?: 'poweroff' | 'reboot',
) => RequestHandler =
(task = 'reboot') =>
(request, response) => {
const subParams: DBJobParams = {
async (request, response) => {
const subParams: JobParams = {
file: __filename,
...MANAGE_HOST_POWER_JOB_PARAMS[task],
};
try {
job(subParams);
await job(subParams);
} catch (subError) {
stderr(`Failed to ${task} host; CAUSE: ${subError}`);
response.status(500).send();
return;
return response.status(500).send();
}
response.status(204).send();

@ -1,71 +1,68 @@
import assert from 'assert';
import { RequestHandler } from 'express';
import { REP_IPV4, REP_PEACEFUL_STRING } from '../../consts';
import { HOST_KEY_CHANGED_PREFIX } from '../../consts/HOST_KEY_CHANGED_PREFIX';
import { dbQuery, getLocalHostUUID, getPeerData } from '../../accessModule';
import { sanitizeSQLParam } from '../../sanitizeSQLParam';
import { getLocalHostUUID, getPeerData, query } from '../../accessModule';
import { sanitize } from '../../sanitize';
import { stderr } from '../../shell';
export const getHostSSH: RequestHandler<
unknown,
{
badSSHKeys?: DeleteSSHKeyConflictRequestBody;
hostName: string;
hostOS: string;
hostUUID: string;
isConnected: boolean;
isInetConnected: boolean;
isOSRegistered: boolean;
},
{
password: string;
port?: number;
ipAddress: string;
}
> = (request, response) => {
GetHostSshResponseBody,
GetHostSshRequestBody
> = async (request, response) => {
const {
body: { password, port = 22, ipAddress: target },
body: { password: rPassword, port: rPort = 22, ipAddress: rTarget } = {},
} = request;
let hostName: string;
let hostOS: string;
let hostUUID: string;
let isConnected: boolean;
let isInetConnected: boolean;
let isOSRegistered: boolean;
const password = sanitize(rPassword, 'string');
const port = sanitize(rPort, 'number');
const target = sanitize(rTarget, 'string', { modifierType: 'sql' });
try {
assert(
REP_PEACEFUL_STRING.test(password),
`Password must be a peaceful string; got [${password}]`,
);
assert(
Number.isInteger(port),
`Port must be a valid integer; got [${port}]`,
);
assert(
REP_IPV4.test(target),
`IP address must be a valid IPv4 address; got [${target}]`,
);
} catch (assertError) {
stderr(`Assert failed when getting host SSH data; CAUSE: ${assertError}`);
return response.status(400).send();
}
const localHostUUID = getLocalHostUUID();
let rsbody: GetHostSshResponseBody;
try {
({
hostName,
hostOS,
hostUUID,
isConnected,
isInetConnected,
isOSRegistered,
} = getPeerData(target, { password, port }));
rsbody = await getPeerData(target, { password, port });
} catch (subError) {
stderr(`Failed to get peer data; CAUSE: ${subError}`);
response.status(500).send();
return;
return response.status(500).send();
}
let badSSHKeys: DeleteSSHKeyConflictRequestBody | undefined;
if (!isConnected) {
const rows = dbQuery(`
if (!rsbody.isConnected) {
const rows: [stateNote: string, stateUUID: string][] = await query(`
SELECT sta.state_note, sta.state_uuid
FROM states AS sta
WHERE sta.state_host_uuid = '${localHostUUID}'
AND sta.state_name = '${HOST_KEY_CHANGED_PREFIX}${sanitizeSQLParam(
target,
)}';`).stdout as [stateNote: string, stateUUID: string][];
AND sta.state_name = '${HOST_KEY_CHANGED_PREFIX}${target}';`);
if (rows.length > 0) {
badSSHKeys = rows.reduce<DeleteSSHKeyConflictRequestBody>(
rsbody.badSSHKeys = rows.reduce<DeleteSshKeyConflictRequestBody>(
(previous, [, stateUUID]) => {
previous[localHostUUID].push(stateUUID);
@ -76,13 +73,5 @@ export const getHostSSH: RequestHandler<
}
}
response.status(200).send({
badSSHKeys,
hostName,
hostOS,
hostUUID,
isConnected,
isInetConnected,
isOSRegistered,
});
response.status(200).send(rsbody);
};

@ -4,7 +4,13 @@ import { RequestHandler } from 'express';
import { REP_PEACEFUL_STRING, REP_UUID } from '../../consts/REG_EXP_PATTERNS';
import SERVER_PATHS from '../../consts/SERVER_PATHS';
import { getAnvilData, job, sub } from '../../accessModule';
import {
getData,
getHostData,
getManifestData,
job,
sub,
} from '../../accessModule';
import { sanitize } from '../../sanitize';
import { stderr } from '../../shell';
@ -12,7 +18,7 @@ export const runManifest: RequestHandler<
{ manifestUuid: string },
undefined,
RunManifestRequestBody
> = (request, response) => {
> = async (request, response) => {
const {
params: { manifestUuid },
body: {
@ -75,9 +81,7 @@ export const runManifest: RequestHandler<
assert(isHostListUnique, `Each entry in hosts must be unique`);
} catch (assertError) {
handleAssertError(assertError);
return;
return handleAssertError(assertError);
}
let rawHostListData: AnvilDataHostListHash | undefined;
@ -85,43 +89,19 @@ export const runManifest: RequestHandler<
let rawSysData: AnvilDataSysHash | undefined;
try {
({
hosts: rawHostListData,
manifests: rawManifestListData,
sys: rawSysData,
} = getAnvilData<{
hosts?: AnvilDataHostListHash;
manifests?: AnvilDataManifestListHash;
sys?: AnvilDataSysHash;
}>(
{ hosts: true, manifests: true, sys: true },
{
predata: [
['Database->get_hosts'],
[
'Striker->load_manifest',
{
debug,
manifest_uuid: manifestUuid,
},
],
],
},
));
rawHostListData = await getHostData();
rawManifestListData = await getManifestData(manifestUuid);
rawSysData = await getData('sys');
} catch (subError) {
stderr(
`Failed to get install manifest ${manifestUuid}; CAUSE: ${subError}`,
);
response.status(500).send();
return;
return response.status(500).send();
}
if (!rawHostListData || !rawManifestListData || !rawSysData) {
response.status(404).send();
return;
return response.status(404).send();
}
const { host_uuid: hostUuidMapToData } = rawHostListData;
@ -134,9 +114,9 @@ export const runManifest: RequestHandler<
} = rawManifestListData;
const { hosts: { by_uuid: mapToHostNameData = {} } = {} } = rawSysData;
const joinAnJobs: DBJobParams[] = [];
const joinAnJobs: JobParams[] = [];
let anParams: Record<string, string> | undefined;
let anParams: Record<string, string>;
try {
anParams = Object.values(hostList).reduce<Record<string, string>>(
@ -177,19 +157,19 @@ export const runManifest: RequestHandler<
}
try {
const [newAnUuid] = sub('insert_or_update_anvils', { subParams: anParams })
.stdout as [string];
const [newAnUuid]: [string] = await sub('insert_or_update_anvils', {
params: [anParams],
});
joinAnJobs.forEach((jobParams) => {
for (const jobParams of joinAnJobs) {
jobParams.job_data += `,anvil_uuid=${newAnUuid}`;
job(jobParams);
});
await job(jobParams);
}
} catch (subError) {
stderr(`Failed to record new anvil node entry; CAUSE: ${subError}`);
response.status(500).send();
return;
return response.status(500).send();
}
response.status(204).send();

@ -5,9 +5,9 @@ import SERVER_PATHS from '../../consts/SERVER_PATHS';
import { job } from '../../accessModule';
import { stderr } from '../../shell';
export const updateSystem: RequestHandler = (request, response) => {
export const updateSystem: RequestHandler = async (request, response) => {
try {
job({
await job({
file: __filename,
job_command: SERVER_PATHS.usr.sbin['anvil-update-system'].self,
job_description: 'job_0004',
@ -17,9 +17,7 @@ export const updateSystem: RequestHandler = (request, response) => {
} catch (subError) {
stderr(`Failed to initiate system update; CAUSE: ${subError}`);
response.status(500).send();
return;
return response.status(500).send();
}
response.status(204).send();

@ -1,15 +1,13 @@
import { RequestHandler } from 'express';
import { getAnvilData } from '../../accessModule';
import { getFenceSpec } from '../../accessModule';
import { stderr } from '../../shell';
export const getFenceTemplate: RequestHandler = (request, response) => {
export const getFenceTemplate: RequestHandler = async (request, response) => {
let rawFenceData;
try {
({ fence_data: rawFenceData } = getAnvilData<{ fence_data: unknown }>(
{ fence_data: true },
{ predata: [['Striker->get_fence_data']] },
));
rawFenceData = await getFenceSpec();
} catch (subError) {
stderr(`Failed to get fence device template; CAUSE: ${subError}`);

@ -1,6 +1,9 @@
import { DELETED } from '../../consts';
import join from '../../join';
import { stdoutVar } from '../../shell';
const buildQueryFileDetail = ({
export const buildQueryFileDetail = ({
fileUUIDs = ['*'],
}: {
fileUUIDs?: string[] | '*';
@ -14,7 +17,7 @@ const buildQueryFileDetail = ({
separator: ', ',
});
console.log(`condFilesUUID=[${condFileUUIDs}]`);
stdoutVar({ condFileUUIDs });
return `
SELECT
@ -33,8 +36,6 @@ const buildQueryFileDetail = ({
ON fil.file_uuid = fil_loc.file_location_file_uuid
JOIN anvils AS anv
ON fil_loc.file_location_anvil_uuid = anv.anvil_uuid
WHERE fil.file_type != 'DELETED'
WHERE fil.file_type != '${DELETED}'
${condFileUUIDs};`;
};
export default buildQueryFileDetail;

@ -0,0 +1,16 @@
import { RequestHandler } from 'express';
import { anvilSyncShared } from '../../accessModule';
import { stdout, stdoutVar } from '../../shell';
export const createFile: RequestHandler = async ({ file, body }, response) => {
stdout('Receiving shared file.');
if (!file) return response.status(400).send();
stdoutVar({ body, file });
await anvilSyncShared('move_incoming', `file=${file.path}`, '0132', '0133');
response.status(201).send();
};

@ -0,0 +1,29 @@
import { RequestHandler } from 'express';
import { DELETED } from '../../consts';
import { anvilSyncShared, query, timestamp, write } from '../../accessModule';
export const deleteFile: RequestHandler = async (request, response) => {
const { fileUUID } = request.params;
const [[oldFileType]] = await query(
`SELECT file_type FROM files WHERE file_uuid = '${fileUUID}';`,
);
if (oldFileType !== DELETED) {
await write(
`UPDATE files
SET
file_type = '${DELETED}',
modified_date = '${timestamp()}'
WHERE file_uuid = '${fileUUID}';`,
);
await anvilSyncShared('purge', `file_uuid=${fileUUID}`, '0136', '0137', {
jobHostUUID: 'all',
});
}
response.status(204).send();
};

@ -1,10 +1,12 @@
import { RequestHandler } from 'express';
import { DELETED } from '../../consts';
import buildGetRequestHandler from '../buildGetRequestHandler';
import buildQueryFileDetail from './buildQueryFileDetail';
import { buildQueryFileDetail } from './buildQueryFileDetail';
import { sanitize } from '../../sanitize';
const getFile: RequestHandler = buildGetRequestHandler((request) => {
export const getFile: RequestHandler = buildGetRequestHandler((request) => {
const { fileUUIDs } = request.query;
let query = `
@ -15,7 +17,7 @@ const getFile: RequestHandler = buildGetRequestHandler((request) => {
file_type,
file_md5sum
FROM files
WHERE file_type != 'DELETED';`;
WHERE file_type != '${DELETED}';`;
if (fileUUIDs) {
query = buildQueryFileDetail({
@ -27,5 +29,3 @@ const getFile: RequestHandler = buildGetRequestHandler((request) => {
return query;
});
export default getFile;

@ -1,12 +1,10 @@
import { RequestHandler } from 'express';
import buildGetRequestHandler from '../buildGetRequestHandler';
import buildQueryFileDetail from './buildQueryFileDetail';
import { buildQueryFileDetail } from './buildQueryFileDetail';
import { sanitizeSQLParam } from '../../sanitizeSQLParam';
const getFileDetail: RequestHandler = buildGetRequestHandler(
export const getFileDetail: RequestHandler = buildGetRequestHandler(
({ params: { fileUUID } }) =>
buildQueryFileDetail({ fileUUIDs: [sanitizeSQLParam(fileUUID)] }),
);
export default getFileDetail;

@ -0,0 +1,5 @@
export * from './createFile';
export * from './deleteFile';
export * from './getFile';
export * from './getFileDetail';
export * from './updateFile';

@ -0,0 +1,134 @@
import { RequestHandler } from 'express';
import { anvilSyncShared, query, timestamp, write } from '../../accessModule';
import { stderr, stdoutVar } from '../../shell';
export const updateFile: RequestHandler = async (request, response) => {
const { body = {}, params } = request;
stdoutVar(body, 'Begin edit single file. body=');
const { fileUUID } = params;
const { fileName, fileLocations, fileType } = body;
const anvilSyncSharedFunctions = [];
let sqlscript = '';
if (fileName) {
const [[oldFileName]] = await query(
`SELECT file_name FROM files WHERE file_uuid = '${fileUUID}';`,
);
stdoutVar({ oldFileName, fileName });
if (fileName !== oldFileName) {
sqlscript += `
UPDATE files
SET
file_name = '${fileName}',
modified_date = '${timestamp()}'
WHERE file_uuid = '${fileUUID}';`;
anvilSyncSharedFunctions.push(() =>
anvilSyncShared(
'rename',
`file_uuid=${fileUUID}\nold_name=${oldFileName}\nnew_name=${fileName}`,
'0138',
'0139',
{ jobHostUUID: 'all' },
),
);
}
}
if (fileType) {
sqlscript += `
UPDATE files
SET
file_type = '${fileType}',
modified_date = '${timestamp()}'
WHERE file_uuid = '${fileUUID}';`;
anvilSyncSharedFunctions.push(() =>
anvilSyncShared('check_mode', `file_uuid=${fileUUID}`, '0143', '0144', {
jobHostUUID: 'all',
}),
);
}
if (fileLocations) {
fileLocations.forEach(
async ({
fileLocationUUID,
isFileLocationActive,
}: {
fileLocationUUID: string;
isFileLocationActive: boolean;
}) => {
let fileLocationActive = 0;
let jobName = 'purge';
let jobTitle = '0136';
let jobDescription = '0137';
if (isFileLocationActive) {
fileLocationActive = 1;
jobName = 'pull_file';
jobTitle = '0132';
jobDescription = '0133';
}
sqlscript += `
UPDATE file_locations
SET
file_location_active = '${fileLocationActive}',
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}';`,
);
targetHosts.flat().forEach((hostUUID: null | string) => {
if (hostUUID) {
anvilSyncSharedFunctions.push(() =>
anvilSyncShared(
jobName,
`file_uuid=${fileUUID}`,
jobTitle,
jobDescription,
{ jobHostUUID: hostUUID },
),
);
}
});
},
);
}
let wcode: number;
try {
wcode = await write(sqlscript);
} catch (queryError) {
stderr(`Failed to execute query; CAUSE: ${queryError}`);
return response.status(500).send();
}
anvilSyncSharedFunctions.forEach(async (fn, index) =>
stdoutVar(await fn(), `Anvil sync shared [${index}] output: `),
);
response.status(200).send(wcode);
};

@ -3,13 +3,15 @@ import { RequestHandler } from 'express';
import {
REP_DOMAIN,
REP_INTEGER,
REP_IPV4,
REP_IPV4_CSV,
} from '../../consts/REG_EXP_PATTERNS';
import SERVER_PATHS from '../../consts/SERVER_PATHS';
REP_PEACEFUL_STRING,
SERVER_PATHS,
} from '../../consts';
import { job } from '../../accessModule';
import { sanitize } from '../../sanitize';
import { stderr, stdoutVar } from '../../shell';
const fvar = (configStepCount: number, fieldName: string) =>
['form', `config_step${configStepCount}`, fieldName, 'value'].join('::');
@ -38,91 +40,90 @@ ${fvar(
export const configStriker: RequestHandler<
unknown,
undefined,
InitializeStrikerForm
> = ({ body }, response) => {
console.log('Begin initialize Striker.');
console.dir(body, { depth: null });
Partial<InitializeStrikerForm>
> = async (request, response) => {
const { body = {} } = request;
stdoutVar(body, 'Begin initialize Striker; body=');
const {
adminPassword = '',
domainName = '',
hostName = '',
hostNumber = 0,
networkDNS = '',
networkGateway = '',
adminPassword: rAdminPassword = '',
domainName: rDomainName = '',
hostName: rHostName = '',
hostNumber: rHostNumber = 0,
networkDNS: rNetworkDns = '',
networkGateway: rNetworkGateway = '',
networks = [],
organizationName = '',
organizationPrefix = '',
} = body || {};
const dataAdminPassword = String(adminPassword);
const dataDomainName = String(domainName);
const dataHostName = String(hostName);
const dataHostNumber = String(hostNumber);
const dataNetworkDNS = String(networkDNS);
const dataNetworkGateway = String(networkGateway);
const dataOrganizationName = String(organizationName);
const dataOrganizationPrefix = String(organizationPrefix);
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 organizationName = sanitize(rOrganizationName, 'string');
const organizationPrefix = sanitize(rOrganizationPrefix, 'string');
try {
assert(
!/['"/\\><}{]/g.test(dataAdminPassword),
`Data admin password cannot contain single-quote, double-quote, slash, backslash, angle brackets, and curly brackets; got [${dataAdminPassword}]`,
REP_PEACEFUL_STRING.test(adminPassword),
`Data admin password cannot contain single-quote, double-quote, slash, backslash, angle brackets, and curly brackets; got [${adminPassword}]`,
);
assert(
REP_DOMAIN.test(dataDomainName),
`Data domain name can only contain alphanumeric, hyphen, and dot characters; got [${dataDomainName}]`,
REP_DOMAIN.test(domainName),
`Data domain name can only contain alphanumeric, hyphen, and dot characters; got [${domainName}]`,
);
assert(
REP_DOMAIN.test(dataHostName),
`Data host name can only contain alphanumeric, hyphen, and dot characters; got [${dataHostName}]`,
REP_DOMAIN.test(hostName),
`Data host name can only contain alphanumeric, hyphen, and dot characters; got [${hostName}]`,
);
assert(
REP_INTEGER.test(dataHostNumber) && hostNumber > 0,
`Data host number can only contain digits; got [${dataHostNumber}]`,
Number.isInteger(hostNumber) && hostNumber > 0,
`Data host number can only contain digits; got [${hostNumber}]`,
);
assert(
REP_IPV4_CSV.test(dataNetworkDNS),
`Data network DNS must be a comma separated list of valid IPv4 addresses; got [${dataNetworkDNS}]`,
REP_IPV4_CSV.test(networkDns),
`Data network DNS must be a comma separated list of valid IPv4 addresses; got [${networkDns}]`,
);
assert(
REP_IPV4.test(dataNetworkGateway),
`Data network gateway must be a valid IPv4 address; got [${dataNetworkGateway}]`,
REP_IPV4.test(networkGateway),
`Data network gateway must be a valid IPv4 address; got [${networkGateway}]`,
);
assert(
dataOrganizationName.length > 0,
`Data organization name cannot be empty; got [${dataOrganizationName}]`,
REP_PEACEFUL_STRING.test(organizationName),
`Data organization name cannot be empty; got [${organizationName}]`,
);
assert(
/^[a-z0-9]{1,5}$/.test(dataOrganizationPrefix),
`Data organization prefix can only contain 1 to 5 lowercase alphanumeric characters; got [${dataOrganizationPrefix}]`,
/^[a-z0-9]{1,5}$/.test(organizationPrefix),
`Data organization prefix can only contain 1 to 5 lowercase alphanumeric characters; got [${organizationPrefix}]`,
);
} catch (assertError) {
console.log(
stderr(
`Failed to assert value when trying to initialize striker; CAUSE: ${assertError}.`,
);
response.status(400).send();
return;
return response.status(400).send();
}
try {
job({
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, 'dns')}=${networkDns}
${fvar(2, 'gateway')}=${networkGateway}
${fvar(2, 'host_name')}=${hostName}
${fvar(2, 'striker_password')}=${adminPassword}
@ -153,11 +154,9 @@ ${buildNetworkLinks(2, networkShortName, interfaces)}`;
job_description: 'job_0071',
});
} catch (subError) {
console.log(`Failed to queue striker initialization; CAUSE: ${subError}`);
response.status(500).send();
stderr(`Failed to queue striker initialization; CAUSE: ${subError}`);
return;
return response.status(500).send();
}
response.status(200).send();

@ -3,7 +3,7 @@ import { RequestHandler } from 'express';
import SERVER_PATHS from '../../consts/SERVER_PATHS';
import {
getAnvilData,
getData,
getLocalHostUUID,
getPeerData,
job,
@ -16,7 +16,7 @@ export const createHostConnection: RequestHandler<
unknown,
undefined,
CreateHostConnectionRequestBody
> = (request, response) => {
> = async (request, response) => {
const {
body: {
dbName = 'anvil',
@ -46,16 +46,15 @@ export const createHostConnection: RequestHandler<
let peerHostUUID: string;
try {
({ hostUUID: peerHostUUID, isConnected: isPeerReachable } = getPeerData(
peerIPAddress,
{ password: commonPassword, port: peerSSHPort },
));
({ hostUUID: peerHostUUID, isConnected: isPeerReachable } =
await getPeerData(peerIPAddress, {
password: commonPassword,
port: peerSSHPort,
}));
} catch (subError) {
stderr(`Failed to get peer data; CAUSE: ${subError}`);
response.status(500).send();
return;
return response.status(500).send();
}
stdoutVar({ peerHostUUID, isPeerReachable });
@ -65,22 +64,18 @@ export const createHostConnection: RequestHandler<
`Cannot connect to peer; please verify credentials and SSH keys validity.`,
);
response.status(400).send();
return;
return response.status(400).send();
}
try {
localIPAddress = sub('find_matching_ip', {
subModuleName: 'System',
subParams: { host: peerIPAddress },
}).stdout;
[localIPAddress] = await sub('find_matching_ip', {
params: [{ host: peerIPAddress }],
pre: ['System'],
});
} catch (subError) {
stderr(`Failed to get matching IP address; CAUSE: ${subError}`);
response.status(500).send();
return;
return response.status(500).send();
}
stdoutVar({ localIPAddress });
@ -94,31 +89,34 @@ export const createHostConnection: RequestHandler<
stdoutVar({ pgpassFilePath, pgpassFileBody });
try {
sub('write_file', {
subModuleName: 'Storage',
subParams: {
body: pgpassFileBody,
file: pgpassFilePath,
mode: '0600',
overwrite: 1,
secure: 1,
},
await sub('write_file', {
params: [
{
body: pgpassFileBody,
file: pgpassFilePath,
mode: '0600',
overwrite: 1,
secure: 1,
},
],
pre: ['Storage'],
});
} catch (subError) {
stderr(`Failed to write ${pgpassFilePath}; CAUSE: ${subError}`);
response.status(500).send();
return;
return response.status(500).send();
}
try {
const [rawIsPeerDBReachable] = sub('call', {
subModuleName: 'System',
subParams: {
shell_call: `PGPASSFILE="${pgpassFilePath}" ${SERVER_PATHS.usr.bin.psql.self} --host ${peerIPAddress} --port ${commonDBPort} --dbname ${commonDBName} --username ${commonDBUser} --no-password --tuples-only --no-align --command "SELECT 1"`,
},
}).stdout as [output: string, returnCode: number];
const [rawIsPeerDBReachable]: [output: string, returnCode: number] =
await sub('call', {
params: [
{
shell_call: `PGPASSFILE="${pgpassFilePath}" ${SERVER_PATHS.usr.bin.psql.self} --host ${peerIPAddress} --port ${commonDBPort} --dbname ${commonDBName} --username ${commonDBUser} --no-password --tuples-only --no-align --command "SELECT 1"`,
},
],
pre: ['System'],
});
isPeerDBReachable = rawIsPeerDBReachable === '1';
} catch (subError) {
@ -130,9 +128,7 @@ export const createHostConnection: RequestHandler<
} catch (fsError) {
stderr(`Failed to remove ${pgpassFilePath}; CAUSE: ${fsError}`);
response.status(500).send();
return;
return response.status(500).send();
}
stdoutVar({ isPeerDBReachable });
@ -142,34 +138,28 @@ export const createHostConnection: RequestHandler<
`Cannot connect to peer database; please verify database credentials.`,
);
response.status(400).send();
return;
return response.status(400).send();
}
const localHostUUID = getLocalHostUUID();
try {
const {
database: {
[localHostUUID]: { port: rawLocalDBPort },
},
} = getAnvilData<{ database: AnvilDataDatabaseHash }>({ database: true });
[localHostUUID]: { port: rawLocalDBPort },
} = await getData<AnvilDataDatabaseHash>('database');
localDBPort = sanitize(rawLocalDBPort, 'number');
} catch (subError) {
stderr(`Failed to get local database data from hash; CAUSE: ${subError}`);
response.status(500).send();
return;
return response.status(500).send();
}
const jobCommand = `${SERVER_PATHS.usr.sbin['striker-manage-peers'].self} --add --host-uuid ${peerHostUUID} --host ${peerIPAddress} --port ${commonDBPort} --ping ${commonPing}`;
const peerJobCommand = `${SERVER_PATHS.usr.sbin['striker-manage-peers'].self} --add --host-uuid ${localHostUUID} --host ${localIPAddress} --port ${localDBPort} --ping ${commonPing}`;
try {
job({
await job({
file: __filename,
job_command: jobCommand,
job_data: `password=${commonPassword}
@ -181,9 +171,7 @@ peer_job_command=${peerJobCommand}`,
} catch (subError) {
stderr(`Failed to add peer ${peerHostUUID}; CAUSE: ${subError}`);
response.status(500).send();
return;
return response.status(500).send();
}
response.status(201).send();

@ -10,33 +10,31 @@ export const deleteHostConnection: RequestHandler<
unknown,
undefined,
DeleteHostConnectionRequestBody
> = (request, response) => {
> = async (request, response) => {
const { body } = request;
const hostUUIDs = Object.keys(body);
const hostUuids = Object.keys(body);
hostUUIDs.forEach((key) => {
const hostUUID = toHostUUID(key);
const peerHostUUIDs = body[key];
for (const key of hostUuids) {
const hostUuid = toHostUUID(key);
const peerHostUuids = body[key];
peerHostUUIDs.forEach((peerHostUUID) => {
for (const peerHostUuid of peerHostUuids) {
try {
job({
await job({
file: __filename,
job_command: `${SERVER_PATHS.usr.sbin['striker-manage-peers'].self} --remove --host-uuid ${peerHostUUID}`,
job_command: `${SERVER_PATHS.usr.sbin['striker-manage-peers'].self} --remove --host-uuid ${peerHostUuid}`,
job_description: 'job_0014',
job_host_uuid: hostUUID,
job_host_uuid: hostUuid,
job_name: 'striker-peer::delete',
job_title: 'job_0013',
});
} catch (subError) {
stderr(`Failed to delete peer ${peerHostUUID}; CAUSE: ${subError}`);
stderr(`Failed to delete peer ${peerHostUuid}; CAUSE: ${subError}`);
response.status(500).send();
return;
return response.status(500).send();
}
});
});
}
}
response.status(204).send();
};

@ -1,4 +1,4 @@
import { getAnvilData, getLocalHostUUID } from '../../accessModule';
import { getData, getLocalHostUUID } from '../../accessModule';
import { buildUnknownIDCondition } from '../../buildCondition';
import buildGetRequestHandler from '../buildGetRequestHandler';
import { toLocal } from '../../convertHostUUID';
@ -39,7 +39,7 @@ const buildHostConnections = (
);
export const getHostConnection = buildGetRequestHandler(
(request, buildQueryOptions) => {
async (request, buildQueryOptions) => {
const { hostUUIDs: rawHostUUIDs } = request.query;
let rawDatabaseData: AnvilDataDatabaseHash;
@ -59,9 +59,7 @@ export const getHostConnection = buildGetRequestHandler(
stdout(`condHostUUIDs=[${condHostUUIDs}]`);
try {
({ database: rawDatabaseData } = getAnvilData<{ database: AnvilDataDatabaseHash }>(
{ database: true },
));
rawDatabaseData = await getData<AnvilDataDatabaseHash>('database');
} catch (subError) {
throw new Error(`Failed to get anvil data; CAUSE: ${subError}`);
}

@ -6,8 +6,8 @@ import {
REP_IPV4,
REP_PEACEFUL_STRING,
REP_UUID,
} from '../../consts/REG_EXP_PATTERNS';
import SERVER_PATHS from '../../consts/SERVER_PATHS';
SERVER_PATHS,
} from '../../consts';
import { job, variable } from '../../accessModule';
import { sanitize } from '../../sanitize';
@ -17,7 +17,7 @@ export const prepareHost: RequestHandler<
unknown,
undefined,
PrepareHostRequestBody
> = (request, response) => {
> = async (request, response) => {
const {
body: {
enterpriseUUID,
@ -106,14 +106,12 @@ export const prepareHost: RequestHandler<
`Failed to assert value when trying to prepare host; CAUSE: ${assertError}`,
);
response.status(400).send();
return;
return response.status(400).send();
}
try {
if (isHostUUIDProvided) {
variable({
await variable({
file: __filename,
update_value_only: 1,
variable_name: 'system::configured',
@ -123,7 +121,7 @@ export const prepareHost: RequestHandler<
});
}
job({
await job({
file: __filename,
job_command: SERVER_PATHS.usr.sbin['striker-initialize-host'].self,
job_data: `enterprise_uuid=${dataEnterpriseUUID}
@ -141,9 +139,7 @@ type=${dataHostType}`,
} catch (subError) {
stderr(`Failed to init host; CAUSE: ${subError}`);
response.status(500).send();
return;
return response.status(500).send();
}
response.status(200).send();

@ -4,36 +4,38 @@ import { LOCAL } from '../../consts/LOCAL';
import SERVER_PATHS from '../../consts/SERVER_PATHS';
import { job } from '../../accessModule';
import { stderr, stdout } from '../../shell';
export const setHostInstallTarget: RequestHandler = (request, response) => {
stdout(
`Begin set host install target.\n${JSON.stringify(request.body, null, 2)}`,
);
const { isEnableInstallTarget } =
request.body as SetHostInstallTargetRequestBody;
const { hostUUID: rawHostUUID } = request.params as UpdateHostParams;
const hostUUID: string | undefined =
rawHostUUID === LOCAL ? undefined : rawHostUUID;
import { stderr, stdoutVar } from '../../shell';
export const setHostInstallTarget: RequestHandler<
UpdateHostParams,
undefined,
SetHostInstallTargetRequestBody
> = async (request, response) => {
const { body, params } = request;
stdoutVar(body, `Begin set host install target; body=`);
const { isEnableInstallTarget } = body;
const { hostUUID: rHostUuid } = params;
const hostUuid: string | undefined =
rHostUuid === LOCAL ? undefined : rHostUuid;
const task = isEnableInstallTarget ? 'enable' : 'disable';
try {
job({
await job({
file: __filename,
job_command: `${SERVER_PATHS.usr.sbin['striker-manage-install-target'].self} --${task}`,
job_description: 'job_0016',
job_host_uuid: hostUUID,
job_host_uuid: hostUuid,
job_name: `install-target::${task}`,
job_title: 'job_0015',
});
} catch (subError) {
stderr(`Failed to ${task} install target; CAUSE: ${subError}`);
response.status(500).send();
return;
return response.status(500).send();
}
response.status(200).send();
response.status(204).send();
};

@ -13,7 +13,7 @@ import { sub } from '../../accessModule';
import { sanitize } from '../../sanitize';
import { stdout } from '../../shell';
export const buildManifest = (
export const buildManifest = async (
...[request]: Parameters<
RequestHandler<
{ manifestUuid?: string },
@ -257,24 +257,29 @@ export const buildManifest = (
{},
);
let result: { name: string; uuid: string } | undefined;
let result: { name: string; uuid: string };
try {
const [uuid, name] = sub('generate_manifest', {
subModuleName: 'Striker',
subParams: {
dns,
domain,
manifest_uuid: manifestUuid,
mtu,
ntp,
prefix,
sequence,
...networkCountContainer,
...networkContainer,
...hostContainer,
const [uuid, name]: [manifestUuid: string, anvilName: string] = await sub(
'generate_manifest',
{
params: [
{
dns,
domain,
manifest_uuid: manifestUuid,
mtu,
ntp,
prefix,
sequence,
...networkCountContainer,
...networkContainer,
...hostContainer,
},
],
pre: ['Striker'],
},
}).stdout as [manifestUuid: string, anvilName: string];
);
result = { name, uuid };
} catch (subError) {

@ -4,13 +4,13 @@ import { RequestHandler } from 'express';
import { buildManifest } from './buildManifest';
import { stderr } from '../../shell';
export const createManifest: RequestHandler = (...handlerArgs) => {
export const createManifest: RequestHandler = async (...handlerArgs) => {
const [, response] = handlerArgs;
let result: Record<string, string> = {};
try {
result = buildManifest(...handlerArgs);
result = await buildManifest(...handlerArgs);
} catch (buildError) {
stderr(`Failed to create new install manifest; CAUSE ${buildError}`);

@ -7,7 +7,7 @@ export const deleteManifest: RequestHandler<
{ manifestUuid: string },
undefined,
{ uuids: string[] }
> = (request, response) => {
> = async (request, response) => {
const {
params: { manifestUuid: rawManifestUuid },
body: { uuids: rawManifestUuidList } = {},
@ -17,21 +17,19 @@ export const deleteManifest: RequestHandler<
? rawManifestUuidList
: [rawManifestUuid];
manifestUuidList.forEach((uuid) => {
for (const uuid of manifestUuidList) {
stdout(`Begin delete manifest ${uuid}.`);
try {
sub('insert_or_update_manifests', {
subParams: { delete: 1, manifest_uuid: uuid },
await sub('insert_or_update_manifests', {
params: [{ delete: 1, manifest_uuid: uuid }],
});
} catch (subError) {
stderr(`Failed to delete manifest ${uuid}; CAUSE: ${subError}`);
response.status(500).send();
return;
return response.status(500).send();
}
});
}
response.status(204).send();
};

@ -1,6 +1,6 @@
import { RequestHandler } from 'express';
import { getAnvilData } from '../../accessModule';
import { getManifestData } from '../../accessModule';
import { getEntityParts } from '../../disassembleEntityId';
import { stderr, stdout } from '../../shell';
@ -67,25 +67,18 @@ const handleSortNetworks = <T extends [string, unknown]>(
return result;
};
export const getManifestDetail: RequestHandler = (request, response) => {
export const getManifestDetail: RequestHandler = async (request, response) => {
const {
params: { manifestUUID },
params: { manifestUUID: manifestUuid },
} = request;
let rawManifestListData: AnvilDataManifestListHash | undefined;
try {
({ manifests: rawManifestListData } = getAnvilData<{
manifests?: AnvilDataManifestListHash;
}>(
{ manifests: true },
{
predata: [['Striker->load_manifest', { manifest_uuid: manifestUUID }]],
},
));
rawManifestListData = await getManifestData(manifestUuid);
} catch (subError) {
stderr(
`Failed to get install manifest ${manifestUUID}; CAUSE: ${subError}`,
`Failed to get install manifest ${manifestUuid}; CAUSE: ${subError}`,
);
response.status(500).send();
@ -109,7 +102,7 @@ export const getManifestDetail: RequestHandler = (request, response) => {
const {
manifest_uuid: {
[manifestUUID]: {
[manifestUuid]: {
parsed: {
domain,
fences: fenceUuidList = {},

@ -1,6 +1,6 @@
import { RequestHandler } from 'express';
import { dbQuery, getLocalHostName } from '../../accessModule';
import { getLocalHostName, query } from '../../accessModule';
import {
getHostNameDomain,
getHostNamePrefix,
@ -8,17 +8,18 @@ import {
} from '../../disassembleHostName';
import { stderr } from '../../shell';
export const getManifestTemplate: RequestHandler = (request, response) => {
let localHostName = '';
export const getManifestTemplate: RequestHandler = async (
request,
response,
) => {
let localHostName: string;
try {
localHostName = getLocalHostName();
} catch (subError) {
stderr(String(subError));
response.status(500).send();
return;
return response.status(500).send();
}
const localShortHostName = getShortHostName(localHostName);
@ -38,7 +39,7 @@ export const getManifestTemplate: RequestHandler = (request, response) => {
>;
try {
({ stdout: rawQueryResult } = dbQuery(
rawQueryResult = await query(
`SELECT
a.fence_uuid,
a.fence_name,
@ -71,13 +72,11 @@ export const getManifestTemplate: RequestHandler = (request, response) => {
ORDER BY manifest_name DESC
LIMIT 1
) AS c ON a.row_number = c.row_number;`,
));
);
} catch (queryError) {
stderr(`Failed to execute query; CAUSE: ${queryError}`);
response.status(500).send();
return;
return response.status(500).send();
}
const queryResult = rawQueryResult.reduce<

@ -4,7 +4,7 @@ import { RequestHandler } from 'express';
import { buildManifest } from './buildManifest';
import { stderr } from '../../shell';
export const updateManifest: RequestHandler = (...args) => {
export const updateManifest: RequestHandler = async (...args) => {
const [request, response] = args;
const {
params: { manifestUuid },
@ -13,7 +13,7 @@ export const updateManifest: RequestHandler = (...args) => {
let result: Record<string, string> = {};
try {
result = buildManifest(...args);
result = await buildManifest(...args);
} catch (buildError) {
stderr(
`Failed to update install manifest ${manifestUuid}; CAUSE: ${buildError}`,

@ -1,107 +1,120 @@
import assert from 'assert';
import { RequestHandler } from 'express';
import { REP_UUID, SERVER_PATHS } from '../../consts';
import { OS_LIST_MAP } from '../../consts/OS_LIST';
import { REP_INTEGER, REP_UUID } from '../../consts/REG_EXP_PATTERNS';
import SERVER_PATHS from '../../consts/SERVER_PATHS';
import { dbQuery, job } from '../../accessModule';
import { stderr, stdout } from '../../shell';
import { job, query } from '../../accessModule';
import { sanitize } from '../../sanitize';
import { stderr, stdout, stdoutVar } from '../../shell';
export const createServer: RequestHandler = ({ body }, response) => {
stdout(`Creating server.\n${JSON.stringify(body, null, 2)}`);
export const createServer: RequestHandler = async (request, response) => {
const { body = {} } = request;
stdoutVar(body, 'Creating server; body=');
const {
serverName,
cpuCores,
memory,
serverName: rServerName,
cpuCores: rCpuCores,
memory: rMemory,
virtualDisks: [
{ storageSize = undefined, storageGroupUUID = undefined } = {},
{
storageSize: rStorageSize = undefined,
storageGroupUUID: rStorageGroupUuid = undefined,
} = {},
] = [],
installISOFileUUID,
driverISOFileUUID,
anvilUUID,
optimizeForOS,
} = body || {};
const dataServerName = String(serverName);
const dataOS = String(optimizeForOS);
const dataCPUCores = String(cpuCores);
const dataRAM = String(memory);
const dataStorageGroupUUID = String(storageGroupUUID);
const dataStorageSize = String(storageSize);
const dataInstallISO = String(installISOFileUUID);
const dataDriverISO = String(driverISOFileUUID) || 'none';
const dataAnvilUUID = String(anvilUUID);
installISOFileUUID: rInstallIsoUuid,
driverISOFileUUID: rDriverIsoUuid,
anvilUUID: rAnvilUuid,
optimizeForOS: rOptimizeForOs,
} = body;
const serverName = sanitize(rServerName, 'string');
const os = sanitize(rOptimizeForOs, 'string');
const cpuCores = sanitize(rCpuCores, 'number');
const memory = sanitize(rMemory, 'number');
const storageGroupUUID = sanitize(rStorageGroupUuid, 'string');
const storageSize = sanitize(rStorageSize, 'number');
const installIsoUuid = sanitize(rInstallIsoUuid, 'string');
const driverIsoUuid = sanitize(rDriverIsoUuid, 'string', {
fallback: 'none',
});
const anvilUuid = sanitize(rAnvilUuid, 'string');
try {
assert(
/^[0-9a-z_-]+$/i.test(dataServerName),
`Data server name can only contain alphanumeric, underscore, and hyphen characters; got [${dataServerName}].`,
/^[0-9a-z_-]+$/i.test(serverName),
`Data server name can only contain alphanumeric, underscore, and hyphen characters; got [${serverName}]`,
);
const [[serverNameCount]] = dbQuery(
`SELECT COUNT(server_uuid) FROM servers WHERE server_name = '${dataServerName}'`,
).stdout;
const [[serverNameCount]] = await query(
`SELECT COUNT(server_uuid) FROM servers WHERE server_name = '${serverName}'`,
);
assert(
serverNameCount === 0,
`Data server name already exists; got [${dataServerName}]`,
`Data server name already exists; got [${serverName}]`,
);
assert(
OS_LIST_MAP[dataOS] !== undefined,
`Data OS not recognized; got [${dataOS}].`,
OS_LIST_MAP[os] !== undefined,
`Data OS not recognized; got [${os}]`,
);
assert(
REP_INTEGER.test(dataCPUCores),
`Data CPU cores can only contain digits; got [${dataCPUCores}].`,
Number.isInteger(cpuCores),
`Data CPU cores can only contain digits; got [${cpuCores}]`,
);
assert(
REP_INTEGER.test(dataRAM),
`Data RAM can only contain digits; got [${dataRAM}].`,
Number.isInteger(memory),
`Data RAM can only contain digits; got [${memory}]`,
);
assert(
REP_UUID.test(dataStorageGroupUUID),
`Data storage group UUID must be a valid UUID; got [${dataStorageGroupUUID}].`,
REP_UUID.test(storageGroupUUID),
`Data storage group UUID must be a valid UUID; got [${storageGroupUUID}]`,
);
assert(
REP_INTEGER.test(dataStorageSize),
`Data storage size can only contain digits; got [${dataStorageSize}].`,
Number.isInteger(storageSize),
`Data storage size can only contain digits; got [${storageSize}]`,
);
assert(
REP_UUID.test(dataInstallISO),
`Data install ISO must be a valid UUID; got [${dataInstallISO}].`,
REP_UUID.test(installIsoUuid),
`Data install ISO must be a valid UUID; got [${installIsoUuid}]`,
);
assert(
dataDriverISO === 'none' || REP_UUID.test(dataDriverISO),
`Data driver ISO must be a valid UUID when provided; got [${dataDriverISO}].`,
driverIsoUuid === 'none' || REP_UUID.test(driverIsoUuid),
`Data driver ISO must be a valid UUID when provided; got [${driverIsoUuid}]`,
);
assert(
REP_UUID.test(dataAnvilUUID),
`Data anvil UUID must be a valid UUID; got [${dataAnvilUUID}].`,
REP_UUID.test(anvilUuid),
`Data anvil UUID must be a valid UUID; got [${anvilUuid}]`,
);
} catch (assertError) {
stdout(
`Failed to assert value when trying to provision a server; CAUSE: ${assertError}.`,
`Failed to assert value when trying to provision a server; CAUSE: ${assertError}`,
);
response.status(400).send();
return;
return response.status(400).send();
}
const provisionServerJobData = `server_name=${dataServerName}
os=${dataOS}
cpu_cores=${dataCPUCores}
ram=${dataRAM}
storage_group_uuid=${dataStorageGroupUUID}
storage_size=${dataStorageSize}
install_iso=${dataInstallISO}
driver_iso=${dataDriverISO}`;
const provisionServerJobData = `server_name=${serverName}
os=${os}
cpu_cores=${cpuCores}
ram=${memory}
storage_group_uuid=${storageGroupUUID}
storage_size=${storageSize}
install_iso=${installIsoUuid}
driver_iso=${driverIsoUuid}`;
stdout(`provisionServerJobData=[${provisionServerJobData}]`);
const [[provisionServerJobHostUUID]] = dbQuery(
const [[provisionServerJobHostUUID]]: [[string]] = await query(
`SELECT
CASE
WHEN pri_hos.primary_host_uuid IS NULL
@ -120,7 +133,7 @@ driver_iso=${dataDriverISO}`;
AND sca_clu_nod.scan_cluster_node_crmd_member
AND sca_clu_nod.scan_cluster_node_cluster_member
AND (NOT sca_clu_nod.scan_cluster_node_maintenance_mode)
AND anv.anvil_uuid = '${dataAnvilUUID}'
AND anv.anvil_uuid = '${anvilUuid}'
ORDER BY sca_clu_nod.scan_cluster_node_name
LIMIT 1
) AS pri_hos
@ -129,15 +142,15 @@ driver_iso=${dataDriverISO}`;
1 AS phr,
anv.anvil_node1_host_uuid AS node1_host_uuid
FROM anvils AS anv
WHERE anv.anvil_uuid = '${dataAnvilUUID}'
WHERE anv.anvil_uuid = '${anvilUuid}'
) AS nod_1
ON pri_hos.phl = nod_1.phr;`,
).stdout;
);
stdout(`provisionServerJobHostUUID=[${provisionServerJobHostUUID}]`);
try {
job({
await job({
file: __filename,
job_command: SERVER_PATHS.usr.sbin['anvil-provision-server'].self,
job_data: provisionServerJobData,
@ -149,9 +162,7 @@ driver_iso=${dataDriverISO}`;
} catch (subError) {
stderr(`Failed to provision server; CAUSE: ${subError}`);
response.status(500).send();
return;
return response.status(500).send();
}
response.status(202).send();

@ -1,6 +1,7 @@
import buildGetRequestHandler from '../buildGetRequestHandler';
import join from '../../join';
import { sanitize } from '../../sanitize';
import { stdoutVar } from '../../shell';
export const getServer = buildGetRequestHandler(
(request, buildQueryOptions) => {
@ -13,7 +14,7 @@ export const getServer = buildGetRequestHandler(
separator: ', ',
});
console.log(`condAnvilsUUID=[${condAnvilUUIDs}]`);
stdoutVar({ condAnvilUUIDs });
if (buildQueryOptions) {
buildQueryOptions.afterQueryReturn = (queryStdout) => {

@ -3,10 +3,9 @@ import { RequestHandler } from 'express';
import { createReadStream } from 'fs';
import path from 'path';
import { REP_UUID } from '../../consts/REG_EXP_PATTERNS';
import SERVER_PATHS from '../../consts/SERVER_PATHS';
import { REP_UUID, SERVER_PATHS } from '../../consts';
import { dbQuery, getLocalHostUUID, job } from '../../accessModule';
import { getLocalHostUUID, job, query } from '../../accessModule';
import { sanitize } from '../../sanitize';
import { mkfifo, rm, stderr, stdout } from '../../shell';
@ -18,7 +17,7 @@ const rmfifo = (path: string) => {
}
};
export const getServerDetail: RequestHandler = (request, response) => {
export const getServerDetail: RequestHandler = async (request, response) => {
const { serverUUID } = request.params;
const { ss, resize } = request.query;
@ -39,9 +38,7 @@ export const getServerDetail: RequestHandler = (request, response) => {
`Failed to assert value when trying to get server detail; CAUSE: ${assertError}.`,
);
response.status(500).send();
return;
return response.status(500).send();
}
if (isScreenshot) {
@ -52,24 +49,20 @@ export const getServerDetail: RequestHandler = (request, response) => {
} catch (subError) {
stderr(String(subError));
response.status(500).send();
return;
return response.status(500).send();
}
stdout(`requestHostUUID=[${requestHostUUID}]`);
try {
[[serverHostUUID]] = dbQuery(`
[[serverHostUUID]] = await query(`
SELECT server_host_uuid
FROM servers
WHERE server_uuid = '${serverUUID}';`).stdout;
WHERE server_uuid = '${serverUUID}';`);
} catch (queryError) {
stderr(`Failed to get server host UUID; CAUSE: ${queryError}`);
response.status(500).send();
return;
return response.status(500).send();
}
stdout(`serverHostUUID=[${serverHostUUID}]`);
@ -94,9 +87,9 @@ export const getServerDetail: RequestHandler = (request, response) => {
namedPipeReadStream.once('close', () => {
stdout(`On close; removing named pipe at ${imageFilePath}.`);
response.status(200).send({ screenshot: imageData });
rmfifo(imageFilePath);
return response.status(200).send({ screenshot: imageData });
});
namedPipeReadStream.on('data', (data) => {
@ -123,11 +116,9 @@ export const getServerDetail: RequestHandler = (request, response) => {
`Failed to prepare named pipe and/or receive image data; CAUSE: ${prepPipeError}`,
);
response.status(500).send();
rmfifo(imageFilePath);
return;
return response.status(500).send();
}
let resizeArgs = sanitize(resize, 'string');
@ -137,7 +128,7 @@ export const getServerDetail: RequestHandler = (request, response) => {
}
try {
job({
await job({
file: __filename,
job_command: SERVER_PATHS.usr.sbin['anvil-get-server-screenshot'].self,
job_data: `server-uuid=${serverUUID}
@ -152,9 +143,7 @@ out-file-id=${epoch}`,
} catch (subError) {
stderr(`Failed to queue fetch server screenshot job; CAUSE: ${subError}`);
response.status(500).send();
return;
return response.status(500).send();
}
} else {
// For getting sever detail data.

@ -9,33 +9,31 @@ import { stderr } from '../../shell';
export const deleteSSHKeyConflict: RequestHandler<
unknown,
undefined,
DeleteSSHKeyConflictRequestBody
> = (request, response) => {
DeleteSshKeyConflictRequestBody
> = async (request, response) => {
const { body } = request;
const hostUUIDs = Object.keys(body);
const hostUuids = Object.keys(body);
hostUUIDs.forEach((key) => {
const hostUUID = toHostUUID(key);
const stateUUIDs = body[key];
for (const uuid of hostUuids) {
const hostUuid = toHostUUID(uuid);
const stateUuids = body[uuid];
try {
job({
await job({
file: __filename,
job_command: SERVER_PATHS.usr.sbin['anvil-manage-keys'].self,
job_data: stateUUIDs.join(','),
job_data: stateUuids.join(','),
job_description: 'job_0057',
job_host_uuid: hostUUID,
job_host_uuid: hostUuid,
job_name: 'manage::broken_keys',
job_title: 'job_0056',
});
} catch (subError) {
stderr(`Failed to delete bad SSH keys; CAUSE: ${subError}`);
response.status(500).send();
return;
return response.status(500).send();
}
});
}
response.status(204).send();
};

@ -22,16 +22,7 @@ export const getSSHKeyConflict = buildGetRequestHandler(
ON sta.state_host_uuid = hos.host_uuid
WHERE sta.state_name LIKE '${HOST_KEY_CHANGED_PREFIX}%';`;
const afterQueryReturn = buildQueryResultReducer<{
[hostUUID: string]: {
[stateUUID: string]: {
badFile: string;
badLine: number;
hostName: string;
hostUUID: string;
ipAddress: string;
stateUUID: string;
};
};
[hostUUID: string]: SshKeyConflict;
}>((previous, [hostName, hostUUID, stateName, stateNote, stateUUID]) => {
const hostUUIDKey = toLocal(hostUUID, localHostUUID);

@ -14,7 +14,7 @@ export const getUPS: RequestHandler = buildGetRequestHandler(
FROM upses
ORDER BY ups_name ASC;`;
const afterQueryReturn: QueryResultModifierFunction | undefined =
buildQueryResultReducer<{ [upsUUID: string]: UPSOverview }>(
buildQueryResultReducer<{ [upsUUID: string]: UpsOverview }>(
(previous, [upsUUID, upsName, upsAgent, upsIPAddress]) => {
previous[upsUUID] = {
upsAgent,

@ -1,16 +1,13 @@
import { RequestHandler } from 'express';
import { getAnvilData } from '../../accessModule';
import { getUpsSpec } from '../../accessModule';
import { stderr } from '../../shell';
export const getUPSTemplate: RequestHandler = (request, response) => {
export const getUPSTemplate: RequestHandler = async (request, response) => {
let rawUPSData: AnvilDataUPSHash;
try {
({ ups_data: rawUPSData } = getAnvilData<{ ups_data: AnvilDataUPSHash }>(
{ ups_data: true },
{ predata: [['Striker->get_ups_data']] },
));
rawUPSData = await getUpsSpec();
} catch (subError) {
stderr(`Failed to get ups template; CAUSE: ${subError}`);
@ -21,13 +18,13 @@ export const getUPSTemplate: RequestHandler = (request, response) => {
const upsData: AnvilDataUPSHash = Object.entries(
rawUPSData,
).reduce<UPSTemplate>((previous, [upsTypeId, value]) => {
).reduce<UpsTemplate>((previous, [upsTypeId, value]) => {
const { brand, description: rawDescription, ...rest } = value;
const matched = rawDescription.match(
/^(.+)\s+[-]\s+[<][^>]+href=[\\"]+([^\s]+)[\\"]+.+[>]([^<]+)[<]/,
);
const result: UPSTemplate[string] = {
const result: UpsTemplate[string] = {
...rest,
brand,
description: rawDescription,

@ -0,0 +1,59 @@
import assert from 'assert';
import { RequestHandler } from 'express';
import { DELETED, REP_UUID } from '../../consts';
import { write } from '../../accessModule';
import join from '../../join';
import { sanitize } from '../../sanitize';
import { stderr, stdoutVar } from '../../shell';
export const deleteUser: RequestHandler<
DeleteUserParamsDictionary,
undefined,
DeleteUserRequestBody
> = async (request, response) => {
const {
body: { uuids: rawUserUuidList } = {},
params: { userUuid },
} = request;
const userUuidList = sanitize(rawUserUuidList, 'string[]');
const ulist = userUuidList.length > 0 ? userUuidList : [userUuid];
stdoutVar({ ulist });
try {
let failedIndex = 0;
assert(
ulist.every((uuid, index) => {
failedIndex = index;
return REP_UUID.test(uuid);
}),
`All UUIDs must be valid UUIDv4; failed at ${failedIndex}, got [${ulist[failedIndex]}]`,
);
} catch (assertError) {
stderr(`Failed to assert value during delete user; CAUSE: ${assertError}`);
return response.status(400).send();
}
try {
const wcode = await write(
`UPDATE users
SET user_algorithm = '${DELETED}'
WHERE user_uuid IN (${join(ulist)});`,
);
assert(wcode === 0, `Write exited with code ${wcode}`);
} catch (error) {
stderr(`Failed to delete user(s); CAUSE: ${error}`);
return response.status(500).send();
}
response.status(204).send();
};

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

@ -0,0 +1,44 @@
import { Application, Handler, Router } from 'express';
import path from 'path';
import { stdout } from './shell';
export const rrouters = <
A extends Application,
M extends MapToRouter<R>,
R extends Router,
H extends Handler,
>(
app: A,
union: Readonly<M> | R,
{
assign = (router) => [router],
key,
route = '/',
}: {
assign?: (router: R) => Array<R | H>;
key?: keyof M;
route?: string;
} = {},
) => {
if ('route' in union) {
const handlers = assign(union as R);
const { length: hcount } = handlers;
stdout(`Set up route ${route} with ${hcount} handler(s)`);
app.use(route, ...handlers);
} else if (key) {
rrouters(app, union[key], {
assign,
route: path.posix.join(route, String(key)),
});
} else {
Object.entries(union).forEach(([extend, subunion]) => {
rrouters(app, subunion, {
assign,
route: path.posix.join(route, extend),
});
});
}
};

@ -12,6 +12,7 @@ type MapToReturnFunction = {
[ReturnTypeName in keyof MapToReturnType]: (
value: unknown,
modifier: (unmodified: unknown) => string,
fallback?: MapToReturnType[ReturnTypeName],
) => MapToReturnType[ReturnTypeName];
};
@ -29,10 +30,10 @@ const MAP_TO_MODIFIER_FUNCTION: MapToModifierFunction = {
const MAP_TO_RETURN_FUNCTION: MapToReturnFunction = {
boolean: (value) => value !== undefined,
number: (value) => parseFloat(String(value)) || 0,
string: (value, mod) => (value ? mod(value) : ''),
'string[]': (value, mod) => {
let result: string[] = [];
number: (value, mod, fallback = 0) => parseFloat(String(value)) || fallback,
string: (value, mod, fallback = '') => (value ? mod(value) : fallback),
'string[]': (value, mod, fallback = []) => {
let result: string[] = fallback;
if (value instanceof Array) {
result = value.reduce<string[]>((reduceContainer, element) => {
@ -54,18 +55,24 @@ export const sanitize = <ReturnTypeName extends keyof MapToReturnType>(
value: unknown,
returnType: ReturnTypeName,
{
fallback,
modifierType = 'none',
modifier = MAP_TO_MODIFIER_FUNCTION[modifierType],
}: {
fallback?: MapToReturnType[ReturnTypeName];
modifier?: ModifierFunction;
modifierType?: keyof MapToModifierFunction;
} = {},
): MapToReturnType[ReturnTypeName] =>
MAP_TO_RETURN_FUNCTION[returnType](value, (unmodified: unknown) => {
const input = String(unmodified);
MAP_TO_RETURN_FUNCTION[returnType](
value,
(unmodified: unknown) => {
const input = String(unmodified);
return call<string>(modifier, {
notCallableReturn: input,
parameters: [input],
});
}) as MapToReturnType[ReturnTypeName];
return call<string>(modifier, {
notCallableReturn: input,
parameters: [input],
});
},
fallback,
) as MapToReturnType[ReturnTypeName];

@ -32,15 +32,33 @@ const systemCall = (
export const date = (...args: string[]) =>
systemCall(SERVER_PATHS.usr.bin.date.self, args);
export const getent = (...args: string[]) =>
systemCall(SERVER_PATHS.usr.bin.getent.self, args);
export const mkfifo = (...args: string[]) =>
systemCall(SERVER_PATHS.usr.bin.mkfifo.self, args);
export const openssl = (...args: string[]) =>
systemCall(SERVER_PATHS.usr.bin.openssl.self, args);
export const rm = (...args: string[]) =>
systemCall(SERVER_PATHS.usr.bin.rm.self, args);
export const uuidgen = (...args: string[]) =>
systemCall(SERVER_PATHS.usr.bin.uuidgen.self, args);
export const resolveId = (id: number | string, database: string) =>
Number.parseInt(getent(database, String(id)).split(':', 3)[2]);
export const resolveGid = (id: number | string) => resolveId(id, 'group');
export const resolveUid = (id: number | string) => resolveId(id, 'passwd');
export const stderr = (message: string) => print(message, { stream: 'stderr' });
export const stdout = (message: string) => print(message);
export const stdoutVar = (variable: { [name: string]: unknown }) =>
print(`Variables: ${JSON.stringify(variable, null, 2)}`);
export const stdoutVar = (variable: unknown, label = 'Variables: ') =>
print(`${label}${JSON.stringify(variable, null, 2)}`);
export const uuid = () => uuidgen('--random').trim();

@ -0,0 +1,28 @@
type NestedObject<T> = {
[key: number | string]: NestedObject<T> | T;
};
export const traverse = <T, O extends NestedObject<V>, V = unknown>(
obj: O,
init: T,
onKey: (previous: T, obj: O, key: string) => { is: boolean; next: O },
{
onEnd,
previous = init,
}: {
onEnd?: (previous: T, obj: O, key: string) => void;
previous?: T;
} = {},
) => {
Object.keys(obj).forEach((key: string) => {
const { is: proceed, next } = onKey(previous, obj, key);
if (proceed) {
traverse(next, init, onKey, { previous });
} else {
onEnd?.call(null, previous, obj, key);
}
});
return previous;
};

@ -0,0 +1,127 @@
import passport from 'passport';
import { Strategy as LocalStrategy } from 'passport-local';
import { DELETED } from './lib/consts';
import { query, sub } from './lib/accessModule';
import { sanitize } from './lib/sanitize';
import { stdout } from './lib/shell';
passport.use(
'login',
new LocalStrategy(async (username, password, done) => {
stdout(`Attempting passport local strategy "login" for user [${username}]`);
let rows: [
userUuid: string,
userName: string,
userPasswordHash: string,
userSalt: string,
userAlgorithm: string,
userHashCount: string,
][];
try {
rows = await query(
`SELECT
user_uuid,
user_name,
user_password_hash,
user_salt,
user_algorithm,
user_hash_count
FROM users
WHERE user_algorithm != 'DELETED'
AND user_name = '${username}'
LIMIT 1;`,
);
} catch (queryError) {
return done(queryError);
}
if (!rows.length) {
return done(null, false);
}
const {
0: [userUuid, , userPasswordHash, userSalt, userAlgorithm, userHashCount],
} = rows;
let encryptResult: {
user_password_hash: string;
user_salt: string;
user_hash_count: number;
user_algorithm: string;
};
try {
[encryptResult] = await sub('encrypt_password', {
params: [
{
algorithm: userAlgorithm,
hash_count: userHashCount,
password,
salt: userSalt,
},
],
pre: ['Account'],
});
} catch (subError) {
return done(subError);
}
const { user_password_hash: inputPasswordHash } = encryptResult;
if (inputPasswordHash !== userPasswordHash) {
return done(null, false);
}
const user: Express.User = {
name: username,
uuid: userUuid,
};
return done(null, user);
}),
);
passport.serializeUser((user, done) => {
const { name, uuid } = user;
stdout(`Serialize user [${name}]`);
return done(null, uuid);
});
passport.deserializeUser(async (id, done) => {
const uuid = sanitize(id, 'string', { modifierType: 'sql' });
stdout(`Deserialize user identified by ${uuid}`);
let rows: [userName: string][];
try {
rows = await query(
`SELECT user_name
FROM users
WHERE user_algorithm != '${DELETED}'
AND user_uuid = '${uuid}';`,
);
} catch (error) {
return done(error);
}
if (!rows.length) {
return done(null, false);
}
const {
0: [userName],
} = rows;
const user: Express.User = { name: userName, uuid };
return done(null, user);
});
export default passport;

@ -0,0 +1,13 @@
import express from 'express';
import { guardApi } from '../lib/assertAuthentication';
import { login, logout } from '../lib/request_handlers/auth';
import passport from '../passport';
const router = express.Router();
router
.post('/login', passport.authenticate('login'), login)
.put('/logout', guardApi, logout);
export default router;

@ -1,5 +1,7 @@
import express from 'express';
import { stdoutVar } from '../lib/shell';
const router = express.Router();
router
@ -7,9 +9,11 @@ router
response.status(200).send({ message: 'Empty echo.' });
})
.post('/', (request, response) => {
console.log('echo:post', JSON.stringify(request.body, undefined, 2));
const { body = {} } = request;
stdoutVar(body, 'echo:post\n');
const message = request.body.message ?? 'No message.';
const { message = 'No message.' } = body;
response.status(200).send({ message });
});

@ -1,202 +1,21 @@
import express from 'express';
import {
dbJobAnvilSyncShared,
dbQuery,
dbSubRefreshTimestamp,
dbWrite,
} from '../lib/accessModule';
import getFile from '../lib/request_handlers/file/getFile';
import getFileDetail from '../lib/request_handlers/file/getFileDetail';
createFile,
deleteFile,
getFile,
getFileDetail,
updateFile,
} from '../lib/request_handlers/file';
import uploadSharedFiles from '../middlewares/uploadSharedFiles';
const router = express.Router();
router
.delete('/:fileUUID', (request, response) => {
const { fileUUID } = request.params;
const FILE_TYPE_DELETED = 'DELETED';
const [[oldFileType]] = dbQuery(
`SELECT file_type FROM files WHERE file_uuid = '${fileUUID}';`,
).stdout;
if (oldFileType !== FILE_TYPE_DELETED) {
dbWrite(
`UPDATE files
SET
file_type = '${FILE_TYPE_DELETED}',
modified_date = '${dbSubRefreshTimestamp()}'
WHERE file_uuid = '${fileUUID}';`,
).stdout;
dbJobAnvilSyncShared('purge', `file_uuid=${fileUUID}`, '0136', '0137', {
jobHostUUID: 'all',
});
}
response.status(204).send();
})
.delete('/:fileUUID', deleteFile)
.get('/', getFile)
.get('/:fileUUID', getFileDetail)
.post('/', uploadSharedFiles.single('file'), ({ file, body }, response) => {
console.log('Receiving shared file.');
if (file) {
console.log(`file: ${JSON.stringify(file, null, 2)}`);
console.log(`body: ${JSON.stringify(body, null, 2)}`);
dbJobAnvilSyncShared(
'move_incoming',
`file=${file.path}`,
'0132',
'0133',
);
response.status(200).send();
}
})
.put('/:fileUUID', (request, response) => {
console.log('Begin edit single file.');
console.dir(request.body);
const { fileUUID } = request.params;
const { fileName, fileLocations, fileType } = request.body;
const anvilSyncSharedFunctions = [];
let query = '';
if (fileName) {
const [[oldFileName]] = dbQuery(
`SELECT file_name FROM files WHERE file_uuid = '${fileUUID}';`,
).stdout;
console.log(`oldFileName=[${oldFileName}],newFileName=[${fileName}]`);
if (fileName !== oldFileName) {
query += `
UPDATE files
SET
file_name = '${fileName}',
modified_date = '${dbSubRefreshTimestamp()}'
WHERE file_uuid = '${fileUUID}';`;
anvilSyncSharedFunctions.push(() =>
dbJobAnvilSyncShared(
'rename',
`file_uuid=${fileUUID}\nold_name=${oldFileName}\nnew_name=${fileName}`,
'0138',
'0139',
{ jobHostUUID: 'all' },
),
);
}
}
if (fileType) {
query += `
UPDATE files
SET
file_type = '${fileType}',
modified_date = '${dbSubRefreshTimestamp()}'
WHERE file_uuid = '${fileUUID}';`;
anvilSyncSharedFunctions.push(() =>
dbJobAnvilSyncShared(
'check_mode',
`file_uuid=${fileUUID}`,
'0143',
'0144',
{ jobHostUUID: 'all' },
),
);
}
if (fileLocations) {
fileLocations.forEach(
({
fileLocationUUID,
isFileLocationActive,
}: {
fileLocationUUID: string;
isFileLocationActive: boolean;
}) => {
let fileLocationActive = 0;
let jobName = 'purge';
let jobTitle = '0136';
let jobDescription = '0137';
if (isFileLocationActive) {
fileLocationActive = 1;
jobName = 'pull_file';
jobTitle = '0132';
jobDescription = '0133';
}
query += `
UPDATE file_locations
SET
file_location_active = '${fileLocationActive}',
modified_date = '${dbSubRefreshTimestamp()}'
WHERE file_location_uuid = '${fileLocationUUID}';`;
const targetHosts = dbQuery(
`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}';`,
).stdout;
targetHosts.flat().forEach((hostUUID: string) => {
if (hostUUID) {
anvilSyncSharedFunctions.push(() =>
dbJobAnvilSyncShared(
jobName,
`file_uuid=${fileUUID}`,
jobTitle,
jobDescription,
{ jobHostUUID: hostUUID },
),
);
}
});
},
);
}
console.log(`Query (type=[${typeof query}]): [${query}]`);
let queryStdout;
try {
({ stdout: queryStdout } = dbWrite(query));
} catch (queryError) {
console.log(`Failed to execute query; CAUSE: ${queryError}`);
response.status(500).send();
}
console.log(
`Query stdout (type=[${typeof queryStdout}]): ${JSON.stringify(
queryStdout,
null,
2,
)}`,
);
anvilSyncSharedFunctions.forEach((fn, index) => {
console.log(
`Anvil sync shared [${index}] output: [${JSON.stringify(
fn(),
null,
2,
)}]`,
);
});
response.status(200).send(queryStdout);
});
.post('/', uploadSharedFiles.single('file'), createFile)
.put('/:fileUUID', updateFile);
export default router;

@ -1,6 +1,5 @@
import { Router } from 'express';
import anvilRouter from './anvil';
import authRouter from './auth';
import commandRouter from './command';
import echoRouter from './echo';
import fenceRouter from './fence';
@ -11,23 +10,30 @@ import manifestRouter from './manifest';
import networkInterfaceRouter from './network-interface';
import serverRouter from './server';
import sshKeyRouter from './ssh-key';
import staticRouter from './static';
import upsRouter from './ups';
import userRouter from './user';
const routes: Readonly<Record<string, Router>> = {
anvil: anvilRouter,
command: commandRouter,
echo: echoRouter,
fence: fenceRouter,
file: fileRouter,
host: hostRouter,
job: jobRouter,
manifest: manifestRouter,
'network-interface': networkInterfaceRouter,
server: serverRouter,
'ssh-key': sshKeyRouter,
ups: upsRouter,
user: userRouter,
const routes = {
private: {
anvil: anvilRouter,
command: commandRouter,
fence: fenceRouter,
file: fileRouter,
host: hostRouter,
job: jobRouter,
manifest: manifestRouter,
'network-interface': networkInterfaceRouter,
server: serverRouter,
'ssh-key': sshKeyRouter,
ups: upsRouter,
user: userRouter,
},
public: {
auth: authRouter,
echo: echoRouter,
},
static: staticRouter,
};
export default routes;

@ -13,7 +13,7 @@ const router = express.Router();
router
.delete('/', deleteManifest)
.delete('/manifestUuid', deleteManifest)
.delete('/:manifestUuid', deleteManifest)
.get('/', getManifest)
.get('/template', getManifestTemplate)
.get('/:manifestUUID', getManifestDetail)

@ -0,0 +1,51 @@
import express from 'express';
import { existsSync } from 'fs';
import path from 'path';
import { SERVER_PATHS } from '../lib/consts';
import { assertAuthentication } from '../lib/assertAuthentication';
import { stdout } from '../lib/shell';
const router = express.Router();
const htmlDir = SERVER_PATHS.var.www.html.self;
router.use(
(...args) => {
const { 0: request, 2: next } = args;
const { originalUrl } = request;
if (/^[/]login/.test(originalUrl)) {
stdout(`Static:login requested`);
return assertAuthentication({ fail: (rq, rs, nx) => nx(), succeed: '/' })(
...args,
);
}
const parts = originalUrl.replace(/[/]$/, '').split('/');
const tail = parts.pop() || 'index';
const extended = /[.]html$/.test(tail) ? tail : `${tail}.html`;
parts.push(extended);
const htmlPath = path.posix.join(htmlDir, ...parts);
const isHtmlExists = existsSync(htmlPath);
if (isHtmlExists) {
stdout(`Static:[${htmlPath}] requested`);
return assertAuthentication({ fail: '/login', failReturnTo: true })(
...args,
);
}
return next();
},
express.static(htmlDir, {
extensions: ['htm', 'html'],
}),
);
export default router;

@ -1,9 +1,12 @@
import express from 'express';
import { getUser } from '../lib/request_handlers/user';
import { deleteUser, getUser } from '../lib/request_handlers/user';
const router = express.Router();
router.get('/', getUser);
router
.get('/', getUser)
.delete('/', deleteUser)
.delete('/:userUuid', deleteUser);
export default router;

@ -0,0 +1,201 @@
import assert from 'assert';
import expressSession, {
SessionData,
Store as BaseSessionStore,
} from 'express-session';
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';
const DEFAULT_COOKIE_ORIGINAL_MAX_AGE = 28800000; // 8 hours
export class SessionStore extends BaseSessionStore {
constructor(options = {}) {
super(options);
}
public async destroy(
sid: string,
done?: ((err?: unknown) => void) | undefined,
): Promise<void> {
stdout(`Destroy session ${sid}`);
try {
const wcode = await write(
`UPDATE sessions
SET session_salt = '${DELETED}', modified_date = '${timestamp()}'
WHERE session_uuid = '${sid}';`,
);
assert(wcode === 0, `Write exited with code ${wcode}`);
} catch (error) {
stderr(
`Failed to complete DB write in destroy session ${sid}; CAUSE: ${error}`,
);
return done?.call(null, error);
}
return done?.call(null);
}
public async get(
sid: string,
done: (err: unknown, session?: SessionData | null | undefined) => void,
): Promise<void> {
stdout(`Get session ${sid}`);
let rows: [
sessionUuid: string,
userUuid: string,
sessionModifiedDate: string,
][];
try {
rows = await query(
`SELECT
s.session_uuid,
u.user_uuid,
s.modified_date
FROM sessions AS s
JOIN users AS u
ON s.session_user_uuid = u.user_uuid
WHERE s.session_salt != '${DELETED}'
AND s.session_uuid = '${sid}';`,
);
} catch (queryError) {
return done(queryError);
}
if (!rows.length) {
return done(null);
}
const {
0: [, userUuid, sessionModifiedDate],
} = rows;
const cookieMaxAge =
SessionStore.calculateCookieMaxAge(sessionModifiedDate);
const data: SessionData = {
cookie: {
maxAge: cookieMaxAge,
originalMaxAge: DEFAULT_COOKIE_ORIGINAL_MAX_AGE,
},
passport: { user: userUuid },
};
return done(null, data);
}
public async set(
sid: string,
session: SessionData,
done?: ((err?: unknown) => void) | undefined,
): Promise<void> {
stdoutVar({ session }, `Set session ${sid}`);
const { passport: { user: userUuid } = {} } = session;
try {
assert.ok(userUuid, 'Missing user identifier');
const localHostUuid = getLocalHostUUID();
const modifiedDate = timestamp();
const wcode = await write(
`INSERT INTO
sessions (
session_uuid,
session_host_uuid,
session_user_uuid,
session_salt,
modified_date
)
VALUES
(
'${sid}',
'${localHostUuid}',
'${userUuid}',
'',
'${modifiedDate}'
)
ON CONFLICT (session_uuid)
DO UPDATE SET modified_date = '${modifiedDate}';`,
);
assert(wcode === 0, `Write exited with code ${wcode}`);
} catch (error) {
stderr(
`Failed to complete DB write in set session ${sid}; CAUSE: ${error}`,
);
return done?.call(null, error);
}
return done?.call(null);
}
public async touch(
sid: string,
session: SessionData,
done?: ((err?: unknown) => void) | undefined,
): Promise<void> {
stdoutVar({ session }, `Touch session ${sid}`);
try {
const wcode = await write(
`UPDATE sessions
SET modified_date = '${timestamp()}'
WHERE session_uuid = '${sid}';`,
);
assert(wcode === 0, `Write exited with code ${wcode}`);
} catch (error) {
stderr(
`Failed to complete DB write in touch session ${sid}; CAUSE: ${error}`,
);
return done?.call(null, error);
}
return done?.call(null);
}
public static calculateCookieMaxAge(
sessionModifiedDate: string,
cookieOriginalMaxAge: number = DEFAULT_COOKIE_ORIGINAL_MAX_AGE,
) {
const sessionModifiedEpoch = Date.parse(sessionModifiedDate);
const sessionDeadlineEpoch = sessionModifiedEpoch + cookieOriginalMaxAge;
const cookieMaxAge = sessionDeadlineEpoch - Date.now();
stdoutVar({ sessionModifiedDate, sessionDeadlineEpoch, cookieMaxAge });
return cookieMaxAge;
}
}
export default (async () =>
expressSession({
cookie: {
httpOnly: true,
maxAge: DEFAULT_COOKIE_ORIGINAL_MAX_AGE,
secure: false,
},
genid: ({ path }) => {
const sid = uuid();
stdout(`Generated session identifier ${sid}; request.path=${path}`);
return sid;
},
resave: false,
saveUninitialized: false,
secret: await getSessionSecret(),
store: new SessionStore(),
}))();

@ -0,0 +1,12 @@
type AccessStartOptions = {
args?: readonly string[];
} & import('child_process').SpawnOptions;
type SubroutineCommonParams = {
debug?: number;
};
type InsertOrUpdateFunctionCommonParams = SubroutineCommonParams & {
file: string;
line?: number;
};

@ -1,8 +0,0 @@
type AnvilOverview = {
anvilName: string;
anvilUUID: string;
hosts: Array<{
hostName: string;
hostUUID: string;
}>;
};

@ -0,0 +1,3 @@
type JobAnvilSyncSharedOptions = {
jobHostUUID?: string;
};

@ -31,3 +31,12 @@ type AnvilDetailForProvisionServer = {
fileName: string;
}>;
};
type AnvilOverview = {
anvilName: string;
anvilUUID: string;
hosts: Array<{
hostName: string;
hostUUID: string;
}>;
};

@ -0,0 +1,4 @@
type AuthLoginRequestBody = {
username: string;
password: string;
};

@ -0,0 +1,15 @@
type GetHostSshRequestBody = {
password: string;
port?: number;
ipAddress: string;
};
type GetHostSshResponseBody = {
badSSHKeys?: DeleteSshKeyConflictRequestBody;
hostName: string;
hostOS: string;
hostUUID: string;
isConnected: boolean;
isInetConnected: boolean;
isOSRegistered: boolean;
};

@ -0,0 +1,89 @@
type CreateHostConnectionRequestBody = {
dbName?: string;
ipAddress: string;
isPing?: boolean;
/** Host password; same as database password */
password: string;
port?: number;
sshPort?: number;
/** Database user */
user?: string;
};
type DeleteHostConnectionRequestBody = {
[hostUUID: string]: string[];
};
type HostConnectionOverview = {
inbound: {
ipAddress: {
[ipAddress: string]: {
hostUUID: string;
ipAddress: string;
ipAddressUUID: string;
networkLinkNumber: number;
networkNumber: number;
networkType: string;
};
};
port: number;
user: string;
};
peer: {
[ipAddress: string]: {
hostUUID: string;
ipAddress: string;
isPing: boolean;
port: number;
user: string;
};
};
};
type HostOverview = {
hostName: string;
hostType: string;
hostUUID: string;
shortHostName: string;
};
type InitializeStrikerNetworkForm = {
interfaces: Array<NetworkInterfaceOverview | null | undefined>;
ipAddress: string;
name: string;
subnetMask: string;
type: string;
};
type InitializeStrikerForm = {
adminPassword: string;
domainName: string;
hostName: string;
hostNumber: number;
networkDNS: string;
networkGateway: string;
networks: InitializeStrikerNetworkForm[];
organizationName: string;
organizationPrefix: string;
};
type PrepareHostRequestBody = {
enterpriseUUID?: string;
hostIPAddress: string;
hostName: string;
hostPassword: string;
hostSSHPort?: number;
hostType: string;
hostUser?: string;
hostUUID?: string;
redhatPassword: string;
redhatUser: string;
};
type SetHostInstallTargetRequestBody = {
isEnableInstallTarget: boolean;
};
type UpdateHostParams = {
hostUUID: string;
};

@ -0,0 +1,12 @@
type SshKeyConflict = {
[stateUUID: string]: {
badFile: string;
badLine: number;
hostName: string;
hostUUID: string;
ipAddress: string;
stateUUID: string;
};
};
type DeleteSshKeyConflictRequestBody = { [hostUUID: string]: string[] };

@ -1,11 +1,11 @@
type UPSOverview = {
type UpsOverview = {
upsAgent: string;
upsIPAddress: string;
upsName: string;
upsUUID: string;
};
type UPSTemplate = {
type UpsTemplate = {
[upsName: string]: AnvilDataUPSHash[string] & {
links: {
[linkId: string]: {

@ -0,0 +1,7 @@
type DeleteUserParamsDictionary = {
userUuid: string;
};
type DeleteUserRequestBody = {
uuids?: string[];
};

@ -7,4 +7,4 @@ type BuildQueryOptions = {
type BuildQueryFunction = (
request: import('express').Request,
options?: BuildQueryOptions,
) => string;
) => string | Promise<string>;

@ -1,11 +0,0 @@
type CreateHostConnectionRequestBody = {
dbName?: string;
ipAddress: string;
isPing?: boolean;
// Host password; same as database password.
password: string;
port?: number;
sshPort?: number;
// database user.
user?: string;
};

@ -1,9 +0,0 @@
type DBInsertOrUpdateFunctionCommonParams = ModuleSubroutineCommonParams & {
file: string;
line?: number;
};
type DBInsertOrUpdateFunctionCommonOptions = Omit<
ExecModuleSubroutineOptions,
'subParams' | 'subModuleName'
>;

@ -1,3 +0,0 @@
type DBJobAnvilSyncSharedOptions = {
jobHostUUID?: string;
};

@ -1,3 +0,0 @@
type DeleteHostConnectionRequestBody = {
[hostUUID: string]: string[];
};

@ -1 +0,0 @@
type DeleteSSHKeyConflictRequestBody = { [hostUUID: string]: string[] };

@ -1,5 +0,0 @@
type ExecModuleSubroutineOptions = {
spawnSyncOptions?: import('child_process').SpawnSyncOptions;
subModuleName?: string;
subParams?: Record<string, unknown>;
};

@ -111,6 +111,6 @@ type AnvilDataUPSHash = {
};
};
type GetAnvilDataOptions = import('child_process').SpawnSyncOptions & {
type GetAnvilDataOptions = {
predata?: Array<[string, ...unknown[]]>;
};

@ -6,23 +6,19 @@ type PeerDataHash = {
os_registered: string;
};
type GetPeerDataOptions = Omit<
ExecModuleSubroutineOptions,
'subModuleName' | 'subParams'
> &
ModuleSubroutineCommonParams & {
password?: string;
port?: number;
};
type GetPeerDataOptions = SubroutineCommonParams & {
password?: string;
port?: number;
};
type GetPeerDataFunction = (
target: string,
options?: GetPeerDataOptions,
) => {
) => Promise<{
hostName: string;
hostOS: string;
hostUUID: string;
isConnected: boolean;
isInetConnected: boolean;
isOSRegistered: boolean;
};
}>;

@ -1,25 +0,0 @@
type HostConnectionOverview = {
inbound: {
ipAddress: {
[ipAddress: string]: {
hostUUID: string;
ipAddress: string;
ipAddressUUID: string;
networkLinkNumber: number;
networkNumber: number;
networkType: string;
};
};
port: number;
user: string;
};
peer: {
[ipAddress: string]: {
hostUUID: string;
ipAddress: string;
isPing: boolean;
port: number;
user: string;
};
};
};

@ -1,6 +0,0 @@
type HostOverview = {
hostName: string;
hostType: string;
hostUUID: string;
shortHostName: string;
};

@ -1,19 +0,0 @@
type InitializeStrikerNetworkForm = {
interfaces: Array<NetworkInterfaceOverview | null | undefined>;
ipAddress: string;
name: string;
subnetMask: string;
type: string;
};
type InitializeStrikerForm = {
adminPassword: string;
domainName: string;
hostName: string;
hostNumber: number;
networkDNS: string;
networkGateway: string;
networks: InitializeStrikerNetworkForm[];
organizationName: string;
organizationPrefix: string;
};

@ -1,4 +1,4 @@
type DBJobParams = DBInsertOrUpdateFunctionCommonParams & {
type JobParams = InsertOrUpdateFunctionCommonParams & {
job_command: string;
job_data?: string;
job_name: string;
@ -7,5 +7,3 @@ type DBJobParams = DBInsertOrUpdateFunctionCommonParams & {
job_host_uuid?: string;
job_progress?: number;
};
type DBInsertOrUpdateJobOptions = DBInsertOrUpdateFunctionCommonOptions;

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

Loading…
Cancel
Save