From efebd135eb9a999fcb816f7ad470fd4513200fcb Mon Sep 17 00:00:00 2001 From: digimer Date: Thu, 30 Mar 2023 12:50:44 -0400 Subject: [PATCH 01/85] * Removed more references to 'dr1_host_uuid' from the old way of linking DR hosts to Anvil! nodes. * Fixed a bug where servers protected by DR hosts aren't deleted when the server itself is deleted. * Updated DRBD->delete_resource() to remove the server's XML file if the host is a DR host. * Updated anvil-version-change and anvil.sql to enable update_audits and the audits table. Signed-off-by: digimer --- Anvil/Tools/Cluster.pm | 25 +--- Anvil/Tools/DRBD.pm | 24 +-- Anvil/Tools/Database.pm | 28 +--- Anvil/Tools/Storage.pm | 41 +---- Anvil/Tools/System.pm | 7 +- notes | 14 ++ scancore-agents/scan-ipmitool/scan-ipmitool | 14 +- share/anvil.sql | 88 +++++------ tools/anvil-delete-server | 42 +++++- tools/anvil-join-anvil | 2 - tools/anvil-manage-server | 19 --- tools/anvil-provision-server | 2 - tools/anvil-sync-shared | 158 +------------------- tools/anvil-version-changes | 2 +- tools/striker-open-ssh-tunnel | 3 +- tools/striker-purge-target | 16 -- 16 files changed, 133 insertions(+), 352 deletions(-) diff --git a/Anvil/Tools/Cluster.pm b/Anvil/Tools/Cluster.pm index 3b9fe02a..278962dd 100644 --- a/Anvil/Tools/Cluster.pm +++ b/Anvil/Tools/Cluster.pm @@ -2204,17 +2204,14 @@ sub get_anvil_uuid my $anvil_name = $anvil->data->{anvils}{anvil_uuid}{$anvil_uuid}{anvil_name}; my $anvil_node1_host_uuid = $anvil->data->{anvils}{anvil_uuid}{$anvil_uuid}{anvil_node1_host_uuid}; my $anvil_node2_host_uuid = $anvil->data->{anvils}{anvil_uuid}{$anvil_uuid}{anvil_node2_host_uuid}; - my $anvil_dr1_host_uuid = $anvil->data->{anvils}{anvil_uuid}{$anvil_uuid}{anvil_dr1_host_uuid}; $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { anvil_name => $anvil_name, anvil_node1_host_uuid => $anvil_node1_host_uuid, anvil_node2_host_uuid => $anvil_node2_host_uuid, - anvil_dr1_host_uuid => $anvil_dr1_host_uuid, }}); if (($host_uuid eq $anvil_node1_host_uuid) or - ($host_uuid eq $anvil_node2_host_uuid) or - ($host_uuid eq $anvil_dr1_host_uuid)) + ($host_uuid eq $anvil_node2_host_uuid)) { # Found ot! $member_anvil_uuid = $anvil_uuid; @@ -2237,13 +2234,11 @@ The data is stored as; sys::anvil::node1::host_name sys::anvil::node2::host_uuid sys::anvil::node2::host_name - sys::anvil::dr1::host_uuid - sys::anvil::dr1::host_name To assist with lookup, the following are also set; - sys::anvil::i_am = {node1,node2,dr1} - sys::anvil::peer_is = {node1,node2} # Not set if this host is 'dr1' + sys::anvil::i_am = {node1,node2} + sys::anvil::peer_is = {node1,node2} This method takes no parameters. @@ -2260,8 +2255,6 @@ sub get_peers $anvil->data->{sys}{anvil}{node1}{host_name} = ""; $anvil->data->{sys}{anvil}{node2}{host_uuid} = ""; $anvil->data->{sys}{anvil}{node2}{host_name} = ""; - $anvil->data->{sys}{anvil}{dr1}{host_uuid} = ""; - $anvil->data->{sys}{anvil}{dr1}{host_name} = ""; $anvil->data->{sys}{anvil}{i_am} = ""; $anvil->data->{sys}{anvil}{peer_is} = ""; @@ -2279,11 +2272,9 @@ sub get_peers { my $anvil_node1_host_uuid = $anvil->data->{anvils}{anvil_uuid}{$anvil_uuid}{anvil_node1_host_uuid}; my $anvil_node2_host_uuid = $anvil->data->{anvils}{anvil_uuid}{$anvil_uuid}{anvil_node2_host_uuid}; - my $anvil_dr1_host_uuid = $anvil->data->{anvils}{anvil_uuid}{$anvil_uuid}{anvil_dr1_host_uuid}; $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { anvil_node1_host_uuid => $anvil_node1_host_uuid, anvil_node2_host_uuid => $anvil_node2_host_uuid, - anvil_dr1_host_uuid => $anvil_dr1_host_uuid, }}); if ($host_uuid eq $anvil_node1_host_uuid) @@ -2310,27 +2301,17 @@ sub get_peers "sys::anvil::peer_is" => $anvil->data->{sys}{anvil}{peer_is}, }}); } - elsif ($host_uuid eq $anvil_dr1_host_uuid) - { - # Found our Anvil!, and we're node 1. - $found = 1; - $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { found => $found }}); - } if ($found) { $anvil->data->{sys}{anvil}{node1}{host_uuid} = $anvil_node1_host_uuid; $anvil->data->{sys}{anvil}{node1}{host_name} = $anvil->data->{hosts}{host_uuid}{$anvil_node1_host_uuid}{host_name}; $anvil->data->{sys}{anvil}{node2}{host_uuid} = $anvil_node2_host_uuid; $anvil->data->{sys}{anvil}{node2}{host_name} = $anvil->data->{hosts}{host_uuid}{$anvil_node2_host_uuid}{host_name}; - $anvil->data->{sys}{anvil}{dr1}{host_uuid} = $anvil_dr1_host_uuid ? $anvil_dr1_host_uuid : ""; - $anvil->data->{sys}{anvil}{dr1}{host_name} = $anvil_dr1_host_uuid ? $anvil->data->{hosts}{host_uuid}{$anvil_dr1_host_uuid}{host_name} : ""; $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { "sys::anvil::node1::host_uuid" => $anvil->data->{sys}{anvil}{node1}{host_uuid}, "sys::anvil::node1::host_name" => $anvil->data->{sys}{anvil}{node1}{host_name}, "sys::anvil::node2::host_uuid" => $anvil->data->{sys}{anvil}{node2}{host_uuid}, "sys::anvil::node2::host_name" => $anvil->data->{sys}{anvil}{node2}{host_name}, - "sys::anvil::dr1::host_uuid" => $anvil->data->{sys}{anvil}{dr1}{host_uuid}, - "sys::anvil::dr1::host_name" => $anvil->data->{sys}{anvil}{dr1}{host_name}, }}); # If this is a node, return the peer's short host name. diff --git a/Anvil/Tools/DRBD.pm b/Anvil/Tools/DRBD.pm index 40c7264f..93cfc116 100644 --- a/Anvil/Tools/DRBD.pm +++ b/Anvil/Tools/DRBD.pm @@ -663,6 +663,19 @@ sub delete_resource } } + # If we're DR, delete the definition file also. + my $definition_file = $anvil->data->{path}{directories}{shared}{definitions}."/".$resource.".xml"; + my $host_type = $anvil->Get->host_type({debug => $debug}); + $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { + definition_file => $definition_file, + host_type => $host_type, + }}); + if (($host_type eq "dr") && (-f $definition_file)) + { + unlink $definition_file; + $anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 1, key => "job_0134", variables => { file_path => $definition_file }}); + } + return(0); } @@ -1764,11 +1777,9 @@ sub get_next_resource # certain. my $node1_host_uuid = $anvil->data->{anvils}{anvil_uuid}{$anvil_uuid}{anvil_node1_host_uuid}; my $node2_host_uuid = $anvil->data->{anvils}{anvil_uuid}{$anvil_uuid}{anvil_node2_host_uuid}; - my $dr1_host_uuid = $anvil->data->{anvils}{anvil_uuid}{$anvil_uuid}{anvil_dr1_host_uuid}; $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { node1_host_uuid => $node1_host_uuid, node2_host_uuid => $node2_host_uuid, - dr1_host_uuid => $dr1_host_uuid, }}); my $query = " @@ -1805,14 +1816,7 @@ AND ( scan_drbd_resource_host_uuid = ".$anvil->Database->quote($node1_host_uuid)." OR - scan_drbd_resource_host_uuid = ".$anvil->Database->quote($node2_host_uuid)." "; - if ($dr1_host_uuid) - { - $query .= " - OR - scan_drbd_resource_host_uuid = ".$anvil->Database->quote($dr1_host_uuid)." "; - } - $query .= " + scan_drbd_resource_host_uuid = ".$anvil->Database->quote($node2_host_uuid)." ) ORDER BY b.scan_drbd_resource_name ASC, diff --git a/Anvil/Tools/Database.pm b/Anvil/Tools/Database.pm index 0f3d05e6..2a081c0d 100644 --- a/Anvil/Tools/Database.pm +++ b/Anvil/Tools/Database.pm @@ -2661,7 +2661,6 @@ Data is stored in two hashes, one sorted by C<< anvil_uuid >> and one by C<< anv anvils::anvil_uuid::::anvil_password anvils::anvil_uuid::::anvil_node1_host_uuid anvils::anvil_uuid::::anvil_node2_host_uuid - anvils::anvil_uuid::::anvil_dr1_host_uuid anvils::anvil_uuid::::modified_date anvils::anvil_name::::anvil_uuid @@ -2669,7 +2668,6 @@ Data is stored in two hashes, one sorted by C<< anvil_uuid >> and one by C<< anv anvils::anvil_name::::anvil_password anvils::anvil_name::::anvil_node1_host_uuid anvils::anvil_name::::anvil_node2_host_uuid - anvils::anvil_name::::anvil_dr1_host_uuid anvils::anvil_name::::modified_date When a host UUID is stored for either node or the DR host, it will be stored at: @@ -6999,12 +6997,6 @@ If set, this is the file line number logged as the source of any INSERTs or UPDA This is a free-form description for this Anvil! system. If this is set to C<< DELETED >>, the Anvil! will be considered to be deleted and no longer used. -=head3 anvil_dr1_host_uuid (optional) - -This is the C<< hosts >> -> C<< host_uuid >> of the machine that is used as the DR host. - -B<< Note >>: If set, there must be a matching C<< hosts >> -> C<< host_uuid >> in the database. - =head3 anvil_name (required) This is the anvil's name. It is usually in the format C<< -anvil- >>. @@ -7052,7 +7044,6 @@ sub insert_or_update_anvils my $anvil_password = defined $parameter->{anvil_password} ? $parameter->{anvil_password} : ""; my $anvil_node1_host_uuid = defined $parameter->{anvil_node1_host_uuid} ? $parameter->{anvil_node1_host_uuid} : "NULL"; my $anvil_node2_host_uuid = defined $parameter->{anvil_node2_host_uuid} ? $parameter->{anvil_node2_host_uuid} : "NULL"; - my $anvil_dr1_host_uuid = defined $parameter->{anvil_dr1_host_uuid} ? $parameter->{anvil_dr1_host_uuid} : "NULL"; $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { uuid => $uuid, file => $file, @@ -7064,7 +7055,6 @@ sub insert_or_update_anvils anvil_password => $anvil->Log->is_secure($anvil_password), anvil_node1_host_uuid => $anvil_node1_host_uuid, anvil_node2_host_uuid => $anvil_node2_host_uuid, - anvil_dr1_host_uuid => $anvil_dr1_host_uuid, }}); if (not $delete) @@ -7133,11 +7123,6 @@ WHERE $anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 0, priority => "err", key => "error_0128", variables => { uuid => $anvil_node2_host_uuid, column => "anvil_node2_host_uuid" }}); return(""); } - if (($anvil_dr1_host_uuid) && (not $anvil->data->{hosts}{host_uuid}{$anvil_dr1_host_uuid}{host_name})) - { - $anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 0, priority => "err", key => "error_0128", variables => { uuid => $anvil_dr1_host_uuid, column => "anvil_dr1_host_uuid" }}); - return(""); - } if ($delete) { @@ -7190,8 +7175,6 @@ WHERE # NULL values can't be quoted my $say_anvil_node1_host_uuid = $anvil_node1_host_uuid eq "" ? "NULL" : $anvil->Database->quote($anvil_node1_host_uuid); my $say_anvil_node2_host_uuid = $anvil_node2_host_uuid eq "" ? "NULL" : $anvil->Database->quote($anvil_node2_host_uuid); - my $say_anvil_dr1_host_uuid = $anvil_dr1_host_uuid eq "" ? "NULL" : $anvil->Database->quote($anvil_dr1_host_uuid); - # If I still don't have an anvil_uuid, we're INSERT'ing . $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { anvil_uuid => $anvil_uuid }}); @@ -7211,7 +7194,6 @@ INSERT INTO anvil_password, anvil_node1_host_uuid, anvil_node2_host_uuid, - anvil_dr1_host_uuid, modified_date ) VALUES ( ".$anvil->Database->quote($anvil_uuid).", @@ -7220,7 +7202,6 @@ INSERT INTO ".$anvil->Database->quote($anvil_password).", ".$say_anvil_node1_host_uuid.", ".$say_anvil_node2_host_uuid.", - ".$say_anvil_dr1_host_uuid.", ".$anvil->Database->quote($anvil->Database->refresh_timestamp)." ); "; @@ -7236,8 +7217,7 @@ SELECT anvil_description, anvil_password, anvil_node1_host_uuid, - anvil_node2_host_uuid, - anvil_dr1_host_uuid + anvil_node2_host_uuid FROM anvils WHERE @@ -7264,14 +7244,12 @@ WHERE my $old_anvil_password = $row->[2]; my $old_anvil_node1_host_uuid = defined $row->[3] ? $row->[3] : "NULL"; my $old_anvil_node2_host_uuid = defined $row->[4] ? $row->[4] : "NULL"; - my $old_anvil_dr1_host_uuid = defined $row->[5] ? $row->[5] : "NULL"; $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { old_anvil_name => $old_anvil_name, old_anvil_description => $old_anvil_description, old_anvil_password => $anvil->Log->is_secure($old_anvil_password), old_anvil_node1_host_uuid => $old_anvil_node1_host_uuid, old_anvil_node2_host_uuid => $old_anvil_node2_host_uuid, - old_anvil_dr1_host_uuid => $old_anvil_dr1_host_uuid, }}); # Anything change? @@ -7279,8 +7257,7 @@ WHERE ($old_anvil_description ne $anvil_description) or ($old_anvil_password ne $anvil_password) or ($old_anvil_node1_host_uuid ne $anvil_node1_host_uuid) or - ($old_anvil_node2_host_uuid ne $anvil_node2_host_uuid) or - ($old_anvil_dr1_host_uuid ne $anvil_dr1_host_uuid)) + ($old_anvil_node2_host_uuid ne $anvil_node2_host_uuid)) { # Something changed, save. my $query = " @@ -7292,7 +7269,6 @@ SET anvil_password = ".$anvil->Database->quote($anvil_password).", anvil_node1_host_uuid = ".$say_anvil_node1_host_uuid.", anvil_node2_host_uuid = ".$say_anvil_node2_host_uuid.", - anvil_dr1_host_uuid = ".$say_anvil_dr1_host_uuid.", modified_date = ".$anvil->Database->quote($anvil->Database->refresh_timestamp)." WHERE anvil_uuid = ".$anvil->Database->quote($anvil_uuid)." diff --git a/Anvil/Tools/Storage.pm b/Anvil/Tools/Storage.pm index f325390e..92595c26 100644 --- a/Anvil/Tools/Storage.pm +++ b/Anvil/Tools/Storage.pm @@ -1767,16 +1767,13 @@ sub get_size_of_block_device $anvil->Database->get_anvils({debug => $debug}); my $node1_host_uuid = ""; my $node2_host_uuid = ""; - my $dr1_host_uuid = ""; if ($anvil_uuid) { $node1_host_uuid = $anvil->data->{anvils}{anvil_uuid}{$anvil_uuid}{anvil_node1_host_uuid}; $node2_host_uuid = $anvil->data->{anvils}{anvil_uuid}{$anvil_uuid}{anvil_node2_host_uuid}; - $dr1_host_uuid = $anvil->data->{anvils}{anvil_uuid}{$anvil_uuid}{anvil_dr1_host_uuid}; $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { node1_host_uuid => $node1_host_uuid, node1_host_uuid => $node2_host_uuid, - dr1_host_uuid => $dr1_host_uuid, }}); } @@ -1807,19 +1804,11 @@ AND ( scan_lvm_lv_host_uuid = ".$anvil->Database->quote($node1_host_uuid)." OR - scan_lvm_lv_host_uuid = ".$anvil->Database->quote($node2_host_uuid); - if ($dr1_host_uuid) - { - $query .= " - OR - scan_lvm_lv_host_uuid = ".$anvil->Database->quote($dr1_host_uuid); - } - $query .= " - )"; - } - $query .= " + scan_lvm_lv_host_uuid = ".$anvil->Database->quote($node2_host_uuid)." + ) LIMIT 1 ;"; + } $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { query => $query }}); my $results = $anvil->Database->query({query => $query, source => $THIS_FILE, line => __LINE__}); my $count = @{$results}; @@ -1911,11 +1900,7 @@ LIMIT 1 ($anvil_uuid) && ( ($this_host_uuid eq $node1_host_uuid) or - ($this_host_uuid eq $node2_host_uuid) or - ( - ($dr1_host_uuid) && - ($this_host_uuid eq $dr1_host_uuid) - ) + ($this_host_uuid eq $node2_host_uuid) ) ) ) @@ -1976,14 +1961,7 @@ AND ( a.scan_drbd_resource_host_uuid = ".$anvil->Database->quote($node1_host_uuid)." OR - a.scan_drbd_resource_host_uuid = ".$anvil->Database->quote($node2_host_uuid); - if ($dr1_host_uuid) - { - $query .= " - OR - a.scan_drbd_resource_host_uuid = ".$anvil->Database->quote($dr1_host_uuid); - } - $query .= " + a.scan_drbd_resource_host_uuid = ".$anvil->Database->quote($node2_host_uuid)." )"; } $query .= " @@ -2222,16 +2200,13 @@ sub get_storage_group_from_path $anvil->Database->get_anvils({debug => $debug}); my $node1_host_uuid = ""; my $node2_host_uuid = ""; - my $dr1_host_uuid = ""; if ($anvil_uuid) { $node1_host_uuid = $anvil->data->{anvils}{anvil_uuid}{$anvil_uuid}{anvil_node1_host_uuid}; $node2_host_uuid = $anvil->data->{anvils}{anvil_uuid}{$anvil_uuid}{anvil_node2_host_uuid}; - $dr1_host_uuid = $anvil->data->{anvils}{anvil_uuid}{$anvil_uuid}{anvil_dr1_host_uuid}; $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { node1_host_uuid => $node1_host_uuid, node1_host_uuid => $node2_host_uuid, - dr1_host_uuid => $dr1_host_uuid, }}); } @@ -2272,11 +2247,7 @@ sub get_storage_group_from_path ($anvil_uuid) && ( ($this_host_uuid eq $node1_host_uuid) or - ($this_host_uuid eq $node2_host_uuid) or - ( - ($dr1_host_uuid) && - ($this_host_uuid eq $dr1_host_uuid) - ) + ($this_host_uuid eq $node2_host_uuid) ) ) ) diff --git a/Anvil/Tools/System.pm b/Anvil/Tools/System.pm index b395a793..41df7d65 100644 --- a/Anvil/Tools/System.pm +++ b/Anvil/Tools/System.pm @@ -1960,11 +1960,8 @@ sub configure_ipmi $machine = "node2"; $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { machine => $machine }}); } - elsif ($anvil->data->{anvils}{anvil_uuid}{$anvil_uuid}{anvil_dr1_host_uuid} eq $host_uuid) - { - $machine = "dr1"; - $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { machine => $machine }}); - } + + # TODO: Make sure this works on DR hosts. if (not $machine) { diff --git a/notes b/notes index 27fcdbb5..745044cc 100644 --- a/notes +++ b/notes @@ -1,3 +1,17 @@ + +Tasks; +1. When provisioning a server; + - First, check if either node is SyncSource, if so use that node. + - Second, check which node has the most servers by RAM count, use that node. +2. Provision; + - Create DRBD resource, force primary on install target + - Create pacemaker resource in stopped state + - Set location constraint to prefer target node + - Boot server + +Create "Node status" which returns "degraded" if the peer is gone + + Common queries; * SELECT a.job_uuid, b.host_name, a.job_command, a.job_data, a.job_progress, a.job_status FROM jobs a, hosts b WHERE a.job_host_uuid = b.host_uuid AND a.job_progress != 100; * SELECT a.host_name, b.file_name, c.file_location_active FROM hosts a, files b, file_locations c WHERE a.host_uuid = c.file_location_host_uuid AND b.file_uuid = c.file_location_file_uuid ORDER BY b.file_name ASC, a.host_name ASC; diff --git a/scancore-agents/scan-ipmitool/scan-ipmitool b/scancore-agents/scan-ipmitool/scan-ipmitool index 6f45d591..422d0336 100755 --- a/scancore-agents/scan-ipmitool/scan-ipmitool +++ b/scancore-agents/scan-ipmitool/scan-ipmitool @@ -2086,8 +2086,7 @@ sub find_ipmi_targets SELECT anvil_name, anvil_node1_host_uuid, - anvil_node2_host_uuid, - anvil_dr1_host_uuid + anvil_node2_host_uuid FROM anvils WHERE @@ -2107,12 +2106,10 @@ WHERE my $anvil_name = $row->[0]; my $anvil_node1_host_uuid = $row->[1]; my $anvil_node2_host_uuid = $row->[2]; - my $anvil_dr1_host_uuid = defined $row->[3] ? $row->[3] : ""; $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { 's1:anvil_name' => $anvil_name, 's2:anvil_node1_host_uuid' => $anvil_node1_host_uuid, 's3:anvil_node2_host_uuid' => $anvil_node2_host_uuid, - 's4:anvil_dr1_host_uuid' => $anvil_dr1_host_uuid, }}); my $query = " @@ -2128,14 +2125,7 @@ AND ( host_uuid = ".$anvil->Database->quote($anvil_node1_host_uuid)." OR - host_uuid = ".$anvil->Database->quote($anvil_node2_host_uuid); - if ($anvil_dr1_host_uuid) - { - $query .= " - OR - host_uuid = ".$anvil->Database->quote($anvil_dr1_host_uuid); - } - $query .= " + host_uuid = ".$anvil->Database->quote($anvil_node2_host_uuid)." ) ;"; $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { query => $query }}); diff --git a/share/anvil.sql b/share/anvil.sql index 70074137..055c431b 100644 --- a/share/anvil.sql +++ b/share/anvil.sql @@ -1979,52 +1979,52 @@ CREATE TRIGGER trigger_temperature AFTER INSERT OR UPDATE ON temperature FOR EACH ROW EXECUTE PROCEDURE history_temperature(); --- -- Enable after Yan's review + -- This is used to audit major events by user. ---CREATE TABLE audits ( - --audit_uuid uuid primary key, - --audit_user_uuid uuid not null, -- This is the users -> user_uuid the audit is tracking - --audit_details text not null, -- This is the information explaining the action being audited. - --modified_date timestamp with time zone not null, +CREATE TABLE audits ( + audit_uuid uuid primary key, + audit_user_uuid uuid not null, -- This is the users -> user_uuid the audit is tracking + audit_details text not null, -- This is the information explaining the action being audited. + modified_date timestamp with time zone not null, - --FOREIGN KEY(audit_user_uuid) REFERENCES users(user_uuid) ---); ---ALTER TABLE audits OWNER TO admin; - ---CREATE TABLE history.audits ( - --history_id bigserial, - --audit_uuid uuid, - --audit_user_uuid uuid, - --audit_details text, - --modified_date timestamp with time zone not null ---); ---ALTER TABLE history.audits OWNER TO admin; - ---CREATE FUNCTION history_audits() RETURNS trigger ---AS $$ ---DECLARE - --history_audits RECORD; ---BEGIN - --SELECT INTO history_audits * FROM audits WHERE audit_uuid = new.audit_uuid; - --INSERT INTO history.audits - --(audit_uuid, - --audit_user_uuid, - --audit_details, - --modified_date) - --VALUES - --(history_audit.audit_uuid, - --history_audit.audit_user_uuid, - --history_audit.audit_details, - --history_audit.modified_date); - --RETURN NULL; ---END; ---$$ ---LANGUAGE plpgsql; ---ALTER FUNCTION history_audits() OWNER TO admin; - ---CREATE TRIGGER trigger_audits - --AFTER INSERT OR UPDATE ON audits - --FOR EACH ROW EXECUTE PROCEDURE history_audits(); + FOREIGN KEY(audit_user_uuid) REFERENCES users(user_uuid) +); +ALTER TABLE audits OWNER TO admin; + +CREATE TABLE history.audits ( + history_id bigserial, + audit_uuid uuid, + audit_user_uuid uuid, + audit_details text, + modified_date timestamp with time zone not null +); +ALTER TABLE history.audits OWNER TO admin; + +CREATE FUNCTION history_audits() RETURNS trigger +AS $$ +DECLARE + history_audits RECORD; +BEGIN + SELECT INTO history_audits * FROM audits WHERE audit_uuid = new.audit_uuid; + INSERT INTO history.audits + (audit_uuid, + audit_user_uuid, + audit_details, + modified_date) + VALUES + (history_audit.audit_uuid, + history_audit.audit_user_uuid, + history_audit.audit_details, + history_audit.modified_date); + RETURN NULL; +END; +$$ +LANGUAGE plpgsql; +ALTER FUNCTION history_audits() OWNER TO admin; + +CREATE TRIGGER trigger_audits + AFTER INSERT OR UPDATE ON audits + FOR EACH ROW EXECUTE PROCEDURE history_audits(); -- ------------------------------------------------------------------------------------------------------- -- diff --git a/tools/anvil-delete-server b/tools/anvil-delete-server index 0d031eeb..6b60bd8b 100755 --- a/tools/anvil-delete-server +++ b/tools/anvil-delete-server @@ -75,8 +75,10 @@ if ($anvil->data->{switches}{'job-uuid'}) message => "message_0217", }); - # Are we in an Anvil! system? - if (not $anvil->data->{sys}{anvil_uuid}) + # Are we in an Anvil! system or are we a DR host? + my $host_type = $anvil->Get->host_type(); + $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { host_type => $host_type }}); + if ((not $anvil->data->{sys}{anvil_uuid}) && ($host_type ne "dr")) { # We're not in an Anvil!. if ($anvil->data->{switches}{'job-uuid'}) @@ -135,6 +137,32 @@ sub run_jobs server_name => $server_name, }}); + # Before we start, we need to know if this server is on DR hosts. To do this, we'll parse the DRBD + # config file and look for DR hosts. + $anvil->Database->get_hosts({debug => 2}); + $anvil->DRBD->gather_data({debug => 2}); + foreach my $peer (sort {$a cmp $b} keys %{$anvil->data->{new}{resource}{$server_name}{peer}}) + { + my $peer_host_uuid = $anvil->Database->get_host_uuid_from_string({debug => 2, string => $peer}); + $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { + peer => $peer, + peer_host_uuid => $peer_host_uuid, + }}); + if (($peer_host_uuid) && (exists $anvil->data->{hosts}{host_uuid}{$peer_host_uuid})) + { + my $host_type = $anvil->data->{hosts}{host_uuid}{$peer_host_uuid}{host_type}; + $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { host_type => $host_type }}); + + if ($host_type eq "dr") + { + $anvil->data->{dr_hosts}{$peer_host_uuid} = 1; + $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { + "dr_hosts::${peer_host_uuid}" => $anvil->data->{dr_hosts}{$peer_host_uuid}, + }}); + } + } + } + if (not $anvil->data->{job}{peer_mode}) { # Remove the server from pacemaker (stopping it, if necessary). @@ -360,11 +388,15 @@ sub remove_from_pacemaker # We're node 2 push @{$peers}, $anvil->data->{anvils}{anvil_uuid}{$anvil_uuid}{anvil_node1_host_uuid} } - if ($anvil->data->{anvils}{anvil_uuid}{$anvil_uuid}{anvil_dr1_host_uuid}) + + if (exists $anvil->data->{dr_hosts}) { - # There's a DR host. - push @{$peers}, $anvil->data->{anvils}{anvil_uuid}{$anvil_uuid}{anvil_dr1_host_uuid}; + foreach my $peer_host_uuid (keys %{$anvil->data->{dr_hosts}}) + { + push @{$peers}, $peer_host_uuid; + } } + my $progress = 30; foreach my $host_uuid (@{$peers}) { diff --git a/tools/anvil-join-anvil b/tools/anvil-join-anvil index 6462bec1..6f968345 100755 --- a/tools/anvil-join-anvil +++ b/tools/anvil-join-anvil @@ -2339,11 +2339,9 @@ sub load_job # Load the anvil $anvil->data->{sys}{node1_host_uuid} = $anvil->data->{anvils}{anvil_uuid}{$anvil_uuid}{anvil_node1_host_uuid}; $anvil->data->{sys}{node2_host_uuid} = $anvil->data->{anvils}{anvil_uuid}{$anvil_uuid}{anvil_node2_host_uuid}; - $anvil->data->{sys}{dr1_host_uuid} = $anvil->data->{anvils}{anvil_uuid}{$anvil_uuid}{anvil_dr1_host_uuid}; $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 3, list => { "sys::node1_host_uuid" => $anvil->data->{sys}{node1_host_uuid}, "sys::node2_host_uuid" => $anvil->data->{sys}{node2_host_uuid}, - "sys::dr1_host_uuid" => $anvil->data->{sys}{dr1_host_uuid}, }}); update_progress($anvil, ($anvil->data->{job}{progress} += 1), "job_0075,!!machine!".$anvil->data->{sys}{machine}."!!,!!manifest_uuid!".$anvil->data->{sys}{manifest_uuid}."!!"); $anvil->Log->entry({source => $THIS_FILE, line => __LINE__, 'print' => 1, level => 2, key => "job_0075", variables => { diff --git a/tools/anvil-manage-server b/tools/anvil-manage-server index 5a3b4863..7dfe2c02 100755 --- a/tools/anvil-manage-server +++ b/tools/anvil-manage-server @@ -112,8 +112,6 @@ sub process_interactive $anvil->data->{target_server}{anvil_node1_host_name} = "" if not exists $anvil->data->{target_server}{anvil_node1_host_name}; $anvil->data->{target_server}{anvil_node2_host_uuid} = "" if not exists $anvil->data->{target_server}{anvil_node2_host_uuid}; $anvil->data->{target_server}{anvil_node2_host_name} = "" if not exists $anvil->data->{target_server}{anvil_node2_host_name}; - $anvil->data->{target_server}{anvil_dr1_host_uuid} = "" if not exists $anvil->data->{target_server}{anvil_dr1_host_uuid}; - $anvil->data->{target_server}{anvil_dr1_host_name} = "" if not exists $anvil->data->{target_server}{anvil_dr1_host_name}; $anvil->data->{new_config}{cpu}{sockets} = ""; @@ -216,8 +214,6 @@ sub interactive_question $anvil->data->{target_server}{anvil_node1_host_name} = "" if not defined $anvil->data->{target_server}{anvil_node1_host_name}; $anvil->data->{target_server}{anvil_node2_host_uuid} = "" if not defined $anvil->data->{target_server}{anvil_node2_host_uuid}; $anvil->data->{target_server}{anvil_node2_host_name} = "" if not defined $anvil->data->{target_server}{anvil_node2_host_name}; - $anvil->data->{target_server}{anvil_dr1_host_uuid} = "" if not defined $anvil->data->{target_server}{anvil_dr1_host_uuid}; - $anvil->data->{target_server}{anvil_dr1_host_name} = "" if not defined $anvil->data->{target_server}{anvil_dr1_host_name}; ### Anvil # If 'switches::anvil' is set, see if it's a UUID and then set either 'anvil-uuid' or 'anvil-name'. @@ -405,14 +401,12 @@ sub interactive_question my $server_host_uuid = $anvil->data->{servers}{server_uuid}{$server_uuid}{server_host_uuid}; my $node1_host_uuid = $anvil->data->{anvils}{anvil_uuid}{$anvil_uuid}{anvil_node1_host_uuid}; my $node2_host_uuid = $anvil->data->{anvils}{anvil_uuid}{$anvil_uuid}{anvil_node1_host_uuid}; - my $dr1_host_uuid = $anvil->data->{anvils}{anvil_uuid}{$anvil_uuid}{anvil_dr1_host_uuid}; $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { anvil_uuid => $anvil_uuid, server_name => $server_name, server_host_uuid => $server_host_uuid, node1_host_uuid => $node1_host_uuid, node2_host_uuid => $node2_host_uuid, - dr1_host_uuid => $dr1_host_uuid, }}); $anvil->data->{target_server}{anvil_uuid} = $anvil_uuid; @@ -424,8 +418,6 @@ sub interactive_question $anvil->data->{target_server}{anvil_node1_host_name} = $anvil->data->{hosts}{host_uuid}{$node1_host_uuid}{host_name}; $anvil->data->{target_server}{anvil_node2_host_uuid} = $anvil->data->{anvils}{anvil_uuid}{$anvil_uuid}{anvil_node2_host_uuid}; $anvil->data->{target_server}{anvil_node2_host_name} = $anvil->data->{hosts}{host_uuid}{$node2_host_uuid}{host_name}; - $anvil->data->{target_server}{anvil_dr1_host_uuid} = $anvil->data->{anvils}{anvil_uuid}{$anvil_uuid}{anvil_node1_host_uuid}; - $anvil->data->{target_server}{anvil_dr1_host_name} = $anvil->data->{anvils}{anvil_uuid}{$anvil_uuid}{anvil_node1_host_uuid}; $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { "target_server::anvil_uuid" => $anvil->data->{target_server}{anvil_uuid}, "target_server::server_name" => $anvil->data->{target_server}{server_name}, @@ -435,18 +427,7 @@ sub interactive_question "target_server::anvil_node1_host_name" => $anvil->data->{target_server}{anvil_node1_host_name}, "target_server::anvil_node2_host_uuid" => $anvil->data->{target_server}{anvil_node2_host_uuid}, "target_server::anvil_node2_host_name" => $anvil->data->{target_server}{anvil_node2_host_name}, - "target_server::anvil_dr1_host_uuid" => $anvil->data->{target_server}{anvil_dr1_host_uuid}, - "target_server::anvil_dr1_host_name" => $anvil->data->{target_server}{anvil_dr1_host_name}, }}); - if ($dr1_host_uuid) - { - $anvil->data->{target_server}{anvil_dr1_host_uuid} = $anvil->data->{anvils}{anvil_uuid}{$anvil_uuid}{anvil_dr1_host_uuid}; - $anvil->data->{target_server}{anvil_dr1_host_name} = $anvil->data->{hosts}{host_uuid}{$dr1_host_uuid}{host_name};; - $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { - "target_server::anvil_dr1_host_uuid" => $anvil->data->{target_server}{anvil_dr1_host_uuid}, - "target_server::anvil_dr1_host_name" => $anvil->data->{target_server}{anvil_dr1_host_name}, - }}); - } ### Pull out details of the server. # How much RAM are we using and how much is configured? diff --git a/tools/anvil-provision-server b/tools/anvil-provision-server index 8a8435cb..f21c92ec 100755 --- a/tools/anvil-provision-server +++ b/tools/anvil-provision-server @@ -2231,7 +2231,6 @@ sub interactive_ask_server_ram } my $node1_host_uuid = $anvil->data->{anvils}{anvil_uuid}{$anvil_uuid}{anvil_node1_host_uuid}; my $node2_host_uuid = $anvil->data->{anvils}{anvil_uuid}{$anvil_uuid}{anvil_node2_host_uuid}; - my $dr1_host_uuid = $anvil->data->{anvil_resources}{$anvil_uuid}{has_dr} ? $anvil->data->{anvils}{anvil_uuid}{$anvil_uuid}{anvil_dr1_host_uuid} : ""; print $anvil->Words->string({key => "job_0167", variables => { ram_available => $anvil->Convert->bytes_to_human_readable({'bytes' => $anvil->data->{anvil_resources}{$anvil_uuid}{ram}{available}})." (".$anvil->Convert->bytes_to_human_readable({'bytes' => $anvil->data->{anvil_resources}{$anvil_uuid}{ram}{available}, unit => "M"}).")", @@ -2690,7 +2689,6 @@ sub interactive_ask_server_driver_disc } my $node1_host_uuid = $anvil->data->{anvils}{anvil_uuid}{$anvil_uuid}{anvil_node1_host_uuid}; my $node2_host_uuid = $anvil->data->{anvils}{anvil_uuid}{$anvil_uuid}{anvil_node2_host_uuid}; - my $dr1_host_uuid = $anvil->data->{anvil_resources}{$anvil_uuid}{has_dr} ? $anvil->data->{anvils}{anvil_uuid}{$anvil_uuid}{anvil_dr1_host_uuid} : ""; print $anvil->Words->string({key => "job_0182"})."\n"; print $iso_list."\n"; diff --git a/tools/anvil-sync-shared b/tools/anvil-sync-shared index 4fb02354..be8d839c 100755 --- a/tools/anvil-sync-shared +++ b/tools/anvil-sync-shared @@ -733,31 +733,19 @@ sub process_pull_file my $anvil_node1_host_uuid = $anvil->data->{anvils}{anvil_uuid}{$anvil_uuid}{anvil_node1_host_uuid}; my $anvil_node2_host_uuid = $anvil->data->{anvils}{anvil_uuid}{$anvil_uuid}{anvil_node2_host_uuid}; - my $anvil_dr1_host_uuid = $anvil->data->{anvils}{anvil_uuid}{$anvil_uuid}{anvil_dr1_host_uuid}; $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { anvil_node1_host_uuid => $anvil_node1_host_uuid, anvil_node2_host_uuid => $anvil_node2_host_uuid, - anvil_dr1_host_uuid => $anvil_dr1_host_uuid, }}); # Where we pull from will depend on which machine we are and how many strikers we have. If we have - # one Anvil!, node 1 and 2 download at the same time, and DR waits. If there are two strikers, Each - # node will download from a different striker (if possible) and DR waits. If there are 3 or more - # Strikers, DR does not wait, and downloads from a different striker than the striker's nodes use. + # one Anvil!, sub-node 1 and 2 download at the same time. If there are two strikers, Each node will + # download from a different striker (if possible). my $i_am = "node1"; if ($anvil->Get->host_uuid eq $anvil_node2_host_uuid) { $i_am = "node2"; } - elsif ($anvil->Get->host_uuid eq $anvil_dr1_host_uuid) - { - $i_am = "dr1"; - - # As we're DR, we'll likely be pinging the nodes to seee if they're up when we wait for them - # to finish jobs. As such, load their IPs into memory. - $anvil->Network->load_ips({debug => 2, host_uuid => $anvil_node1_host_uuid}); - $anvil->Network->load_ips({debug => 2, host_uuid => $anvil_node2_host_uuid}); - } my $striker_count = @{$anvil->data->{target_strikers}}; $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { @@ -777,158 +765,26 @@ sub process_pull_file $anvil->nice_exit({exit_code => 2}); } - my $use = 0; - my $dr_wait = 1; + my $use = 0; if ($striker_count >= 3) { - $dr_wait = 0; if ($i_am eq "node1") { $use = 0; } elsif ($i_am eq "node2") { $use = 1; } - elsif ($i_am eq "dr1") { $use = 2; } - $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { - 'use' => $use, - dr_wait => $dr_wait, - }}); + $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { 'use' => $use }}); } elsif ($striker_count == 2) { - # Two strikers, each node will use the other Striker, DR waits for both to be done. + # Two strikers, each node will use the other Striker if ($i_am eq "node1") { $use = 0; } elsif ($i_am eq "node2") { $use = 1; } - elsif ($i_am eq "dr1") { $use = 1; } - $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { - 'use' => $use, - dr_wait => $dr_wait, - }}); + $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { 'use' => $use }}); } elsif ($striker_count == 1) { # Only 1 Striker if ($i_am eq "node1") { $use = 0; } elsif ($i_am eq "node2") { $use = 0; } - elsif ($i_am eq "dr1") { $use = 0; } - $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { - 'use' => $use, - dr_wait => $dr_wait, - }}); - } - - # If I'm DR and need to wait, look for jobs on node1 and node2 and wait until both are done (or is - # offline). - $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { - i_am => $i_am, - dr_wait => $dr_wait, - }}); - if (($i_am eq "dr1") && ($dr_wait)) - { - my $node1_job_uuid = ""; - my $node1_online = 1; - my $node2_job_uuid = ""; - my $node2_online = 1; - my $node1_waiting = 1; - my $node2_waiting = 1; - - my $query = " -SELECT - job_uuid, - job_host_uuid -FROM - jobs -WHERE - job_name = 'storage::pull_file' -AND - ( - job_host_uuid = ".$anvil->Database->quote($anvil_node1_host_uuid)." - OR - job_host_uuid = ".$anvil->Database->quote($anvil_node2_host_uuid)." - ) -;"; - $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { query => $query }}); - - my $results = $anvil->Database->query({query => $query, source => $THIS_FILE, line => __LINE__}); - my $count = @{$results}; - $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { - results => $results, - count => $count, - }}); - foreach my $row (@{$results}) - { - my $job_uuid = $row->[0]; - my $job_host_uuid = $row->[1]; - $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { - job_uuid => $job_uuid, - job_host_uuid => $job_host_uuid, - }}); - - if ($job_host_uuid eq $anvil_node1_host_uuid) - { - $node1_job_uuid = $job_uuid; - $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { node1_job_uuid => $node1_job_uuid }}); - } - if ($job_host_uuid eq $anvil_node2_host_uuid) - { - $node2_job_uuid = $job_uuid; - $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { node2_job_uuid => $node2_job_uuid }}); - } - } - - my $waiting = 1; - while($waiting) - { - if ($node1_waiting) - { - if (not $node1_job_uuid) - { - $node1_waiting = 0; - $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { node1_waiting => $node1_waiting }}); - } - else - { - $node1_waiting = wait_on_host($anvil, $anvil_node1_host_uuid, $node1_job_uuid); - $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { node1_waiting => $node1_waiting }}); - } - } - if ($node2_waiting) - { - if (not $node2_job_uuid) - { - $node2_waiting = 0; - $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { node2_waiting => $node2_waiting }}); - } - else - { - $node2_waiting = wait_on_host($anvil, $anvil_node2_host_uuid, $node2_job_uuid); - $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { node2_waiting => $node2_waiting }}); - } - } - - if ((not $node1_waiting) && (not $node2_waiting)) - { - # We can proceed. - $waiting = 0; - $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { waiting => $waiting }}); - } - else - { - # Sleep for a bit. - my $sleep_time = 30; - my $wait_until = $anvil->Get->date_and_time({offset => $sleep_time}); - - $anvil->Job->update_progress({ - progress => 1, - message => "message_0199,!!strikers!".$striker_count."!!,!!node1_waiting!".$node1_waiting."!!,!!node2_waiting!".$node2_waiting."!!,!!wait_until!".$wait_until."!!", - job_status => "failed", - }); - $anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 1, key => "message_0199", variables => { - strikers => $striker_count, - node1_waiting => $node1_waiting, - node2_waiting => $node2_waiting, - wait_until => $wait_until, - }}); - - sleep $sleep_time; - } - } + $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { 'use' => $use }}); } # Now proceed with the download! diff --git a/tools/anvil-version-changes b/tools/anvil-version-changes index ea0d4e4d..b303a2ac 100755 --- a/tools/anvil-version-changes +++ b/tools/anvil-version-changes @@ -75,7 +75,7 @@ sub striker_checks ### NOTE: Disabled until review complete # This checks to make sure that the 'audits' table exists (added late into M3.0 pre-release) - #update_audits($anvil); + update_audits($anvil); ### NOTE: Disabled until review complete # This checks to make sure that the new dr_links table exists, and that existing anvil_dr1_host_uuid diff --git a/tools/striker-open-ssh-tunnel b/tools/striker-open-ssh-tunnel index bd0323f8..3d1b6bec 100755 --- a/tools/striker-open-ssh-tunnel +++ b/tools/striker-open-ssh-tunnel @@ -49,8 +49,7 @@ SELECT anv.anvil_password FROM hosts AS hos JOIN anvils AS anv ON hos.host_uuid = anv.anvil_node1_host_uuid - OR hos.host_uuid = anv.anvil_node2_host_uuid - OR hos.host_uuid = anv.anvil_dr1_host_uuid +OR hos.host_uuid = anv.anvil_node2_host_uuid WHERE hos.host_name = ".$anvil->Database->quote($target)." ;"; diff --git a/tools/striker-purge-target b/tools/striker-purge-target index e0bc6cc7..82b14c74 100755 --- a/tools/striker-purge-target +++ b/tools/striker-purge-target @@ -103,10 +103,6 @@ if ($anvil->data->{switches}{'anvil'}) $anvil_uuid = $anvil->data->{purge}{anvil_uuid}; push @{$anvil->data->{purge}{hosts}}, $anvil->data->{anvils}{anvil_uuid}{$anvil_uuid}{anvil_node1_host_uuid}; push @{$anvil->data->{purge}{hosts}}, $anvil->data->{anvils}{anvil_uuid}{$anvil_uuid}{anvil_node2_host_uuid}; - if ($anvil->data->{anvils}{anvil_uuid}{$anvil_uuid}{anvil_dr1_host_uuid}) - { - push @{$anvil->data->{purge}{hosts}}, $anvil->data->{anvils}{anvil_uuid}{$anvil_uuid}{anvil_dr1_host_uuid}; - } } elsif($anvil->data->{switches}{'host'}) { @@ -198,14 +194,6 @@ else }}); # DR, if set. - if ($anvil->data->{anvils}{anvil_uuid}{$anvil_uuid}{anvil_dr1_host_uuid}) - { - my $dr1_host_uuid = $anvil->data->{anvils}{anvil_uuid}{$anvil_uuid}{anvil_dr1_host_uuid}; - $anvil->Log->entry({source => $THIS_FILE, line => __LINE__, 'print' => 1, level => 1, key => "message_0244", variables => { - host_name => $anvil->data->{hosts}{host_uuid}{$dr1_host_uuid}{host_name}, - host_uuid => $dr1_host_uuid, - }}); - } } else { @@ -261,10 +249,6 @@ foreach my $host_uuid (@{$anvil->data->{purge}{hosts}}) { $host_key = "anvil_node2_host_uuid"; } - elsif ($host_uuid eq $anvil->data->{anvils}{anvil_uuid}{$anvil_uuid}{anvil_dr1_host_uuid}) - { - $host_key = "anvil_dr1_host_uuid"; - } if ($host_key) { my $query = " From ddc6965b60adf8b2c20f61b2f85d2c3d49bc0dd8 Mon Sep 17 00:00:00 2001 From: digimer Date: Thu, 30 Mar 2023 17:33:49 -0400 Subject: [PATCH 02/85] * Fixed a bug where references to files on Anvil! nodes was broken in anvil-provision-server and anvil-manage-files. Signed-off-by: digimer --- Anvil/Tools/Database.pm | 30 ++++++++++++++++-------------- tools/anvil-manage-files | 2 +- tools/anvil-provision-server | 8 ++++---- 3 files changed, 21 insertions(+), 19 deletions(-) diff --git a/Anvil/Tools/Database.pm b/Anvil/Tools/Database.pm index 2a081c0d..a05d6a43 100644 --- a/Anvil/Tools/Database.pm +++ b/Anvil/Tools/Database.pm @@ -3590,21 +3590,23 @@ FROM if ($file_name) { - $anvil->data->{anvils}{anvil_uuid}{$anvil_uuid}{file_name}{$file_name}{file_uuid} = $file_location_file_uuid; - $anvil->data->{anvils}{anvil_uuid}{$anvil_uuid}{file_uuid}{$file_location_file_uuid}{file_name} = $file_name; - $anvil->data->{anvils}{anvil_uuid}{$anvil_uuid}{file_uuid}{$file_location_file_uuid}{file_directory} = $file_directory; - $anvil->data->{anvils}{anvil_uuid}{$anvil_uuid}{file_uuid}{$file_location_file_uuid}{file_size} = $file_size; - $anvil->data->{anvils}{anvil_uuid}{$anvil_uuid}{file_uuid}{$file_location_file_uuid}{file_md5sum} = $file_md5sum; - $anvil->data->{anvils}{anvil_uuid}{$anvil_uuid}{file_uuid}{$file_location_file_uuid}{file_type} = $file_type; - $anvil->data->{anvils}{anvil_uuid}{$anvil_uuid}{file_uuid}{$file_location_file_uuid}{file_mtime} = $file_mtime; + $anvil->data->{anvils}{anvil_uuid}{$anvil_uuid}{file_name}{$file_name}{file_uuid} = $file_location_file_uuid; + $anvil->data->{anvils}{anvil_uuid}{$anvil_uuid}{file_uuid}{$file_location_file_uuid}{file_name} = $file_name; + $anvil->data->{anvils}{anvil_uuid}{$anvil_uuid}{file_uuid}{$file_location_file_uuid}{file_directory} = $file_directory; + $anvil->data->{anvils}{anvil_uuid}{$anvil_uuid}{file_uuid}{$file_location_file_uuid}{file_size} = $file_size; + $anvil->data->{anvils}{anvil_uuid}{$anvil_uuid}{file_uuid}{$file_location_file_uuid}{file_md5sum} = $file_md5sum; + $anvil->data->{anvils}{anvil_uuid}{$anvil_uuid}{file_uuid}{$file_location_file_uuid}{file_type} = $file_type; + $anvil->data->{anvils}{anvil_uuid}{$anvil_uuid}{file_uuid}{$file_location_file_uuid}{file_mtime} = $file_mtime; + $anvil->data->{anvils}{anvil_uuid}{$anvil_uuid}{file_uuid}{$file_location_file_uuid}{file_location_uuid} = $file_location_uuid; $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { - "anvils::anvil_uuid::${anvil_uuid}::file_name::${file_name}::file_uuid" => $anvil->data->{anvils}{anvil_uuid}{$anvil_uuid}{file_name}{$file_name}{file_uuid}, - "anvils::anvil_uuid::${anvil_uuid}::file_uuid::${file_location_file_uuid}::file_name" => $anvil->data->{anvils}{anvil_uuid}{$anvil_uuid}{file_uuid}{$file_location_file_uuid}{file_name}, - "anvils::anvil_uuid::${anvil_uuid}::file_uuid::${file_location_file_uuid}::file_directory" => $anvil->data->{anvils}{anvil_uuid}{$anvil_uuid}{file_uuid}{$file_location_file_uuid}{file_directory}, - "anvils::anvil_uuid::${anvil_uuid}::file_uuid::${file_location_file_uuid}::file_size" => $anvil->data->{anvils}{anvil_uuid}{$anvil_uuid}{file_uuid}{$file_location_file_uuid}{file_size}." (".$anvil->Convert->bytes_to_human_readable({'bytes' => $anvil->data->{anvils}{anvil_uuid}{$anvil_uuid}{file_uuid}{$file_location_file_uuid}{file_size}}).")", - "anvils::anvil_uuid::${anvil_uuid}::file_uuid::${file_location_file_uuid}::file_md5sum" => $anvil->data->{anvils}{anvil_uuid}{$anvil_uuid}{file_uuid}{$file_location_file_uuid}{file_md5sum}, - "anvils::anvil_uuid::${anvil_uuid}::file_uuid::${file_location_file_uuid}::file_type" => $anvil->data->{anvils}{anvil_uuid}{$anvil_uuid}{file_uuid}{$file_location_file_uuid}{file_type}, - "anvils::anvil_uuid::${anvil_uuid}::file_uuid::${file_location_file_uuid}::file_mtime" => $anvil->data->{anvils}{anvil_uuid}{$anvil_uuid}{file_uuid}{$file_location_file_uuid}{file_mtime}, + "anvils::anvil_uuid::${anvil_uuid}::file_name::${file_name}::file_uuid" => $anvil->data->{anvils}{anvil_uuid}{$anvil_uuid}{file_name}{$file_name}{file_uuid}, + "anvils::anvil_uuid::${anvil_uuid}::file_uuid::${file_location_file_uuid}::file_name" => $anvil->data->{anvils}{anvil_uuid}{$anvil_uuid}{file_uuid}{$file_location_file_uuid}{file_name}, + "anvils::anvil_uuid::${anvil_uuid}::file_uuid::${file_location_file_uuid}::file_directory" => $anvil->data->{anvils}{anvil_uuid}{$anvil_uuid}{file_uuid}{$file_location_file_uuid}{file_directory}, + "anvils::anvil_uuid::${anvil_uuid}::file_uuid::${file_location_file_uuid}::file_size" => $anvil->data->{anvils}{anvil_uuid}{$anvil_uuid}{file_uuid}{$file_location_file_uuid}{file_size}." (".$anvil->Convert->bytes_to_human_readable({'bytes' => $anvil->data->{anvils}{anvil_uuid}{$anvil_uuid}{file_uuid}{$file_location_file_uuid}{file_size}}).")", + "anvils::anvil_uuid::${anvil_uuid}::file_uuid::${file_location_file_uuid}::file_md5sum" => $anvil->data->{anvils}{anvil_uuid}{$anvil_uuid}{file_uuid}{$file_location_file_uuid}{file_md5sum}, + "anvils::anvil_uuid::${anvil_uuid}::file_uuid::${file_location_file_uuid}::file_type" => $anvil->data->{anvils}{anvil_uuid}{$anvil_uuid}{file_uuid}{$file_location_file_uuid}{file_type}, + "anvils::anvil_uuid::${anvil_uuid}::file_uuid::${file_location_file_uuid}::file_mtime" => $anvil->data->{anvils}{anvil_uuid}{$anvil_uuid}{file_uuid}{$file_location_file_uuid}{file_mtime}, + "anvils::anvil_uuid::${anvil_uuid}::file_uuid::${file_location_file_uuid}::file_location_uuid" => $anvil->data->{anvils}{anvil_uuid}{$anvil_uuid}{file_uuid}{$file_location_file_uuid}{file_location_uuid}, }}); } } diff --git a/tools/anvil-manage-files b/tools/anvil-manage-files index 1145ebed..f98fd8ea 100755 --- a/tools/anvil-manage-files +++ b/tools/anvil-manage-files @@ -242,7 +242,7 @@ ORDER BY # Do we have a file_location_uuid? If not, there will be soon but nothing to do until # then. - my $file_location_uuid = $anvil->data->{file_locations}{anvil_uuid}{$anvil_uuid}{file_uuid}{$file_uuid}{file_location_uuid}; + my $file_location_uuid = $anvil->data->{anvils}{anvil_uuid}{$anvil_uuid}{file_uuid}{$file_uuid}{file_location_uuid}; $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { file_location_uuid => $file_location_uuid }}); next if not $file_location_uuid; diff --git a/tools/anvil-provision-server b/tools/anvil-provision-server index f21c92ec..55132889 100755 --- a/tools/anvil-provision-server +++ b/tools/anvil-provision-server @@ -2533,8 +2533,8 @@ sub interactive_ask_server_install_media my $retry = 0; while(1) { - $anvil->Database->get_files(); - $anvil->Database->get_file_locations(); + $anvil->Database->get_files({debug => 2}); + $anvil->Database->get_file_locations({debug => 2}); my $anvil_uuid = $anvil->data->{new_server}{anvil_uuid}; $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { anvil_uuid => $anvil_uuid }}); @@ -2549,7 +2549,7 @@ sub interactive_ask_server_install_media my $file_size = $anvil->data->{files}{file_name}{$file_name}{file_size}; my $file_type = $anvil->data->{files}{file_name}{$file_name}{file_type}; my $file_path = $file_directory."/".$file_name; - my $file_location_uuid = $anvil->data->{file_locations}{anvil_uuid}{$anvil_uuid}{file_uuid}{$file_uuid}{file_location_uuid}; + my $file_location_uuid = $anvil->data->{anvils}{anvil_uuid}{$anvil_uuid}{file_uuid}{$file_uuid}{file_location_uuid}; my $file_location_active = $anvil->data->{file_locations}{file_location_uuid}{$file_location_uuid}{file_location_active}; $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { file_uuid => $file_uuid, @@ -2649,7 +2649,7 @@ sub interactive_ask_server_driver_disc my $file_size = $anvil->data->{files}{file_name}{$file_name}{file_size}; my $file_type = $anvil->data->{files}{file_name}{$file_name}{file_type}; my $file_path = $file_directory."/".$file_name; - my $file_location_uuid = $anvil->data->{file_locations}{anvil_uuid}{$anvil_uuid}{file_uuid}{$file_uuid}{file_location_uuid}; + my $file_location_uuid = $anvil->data->{anvils}{anvil_uuid}{$anvil_uuid}{file_uuid}{$file_uuid}{file_location_uuid}; my $file_location_active = $anvil->data->{file_locations}{file_location_uuid}{$file_location_uuid}{file_location_active}; $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { file_uuid => $file_uuid, From 7d5f18b20d6ade59c3df0828f7a4e6630c3f726f Mon Sep 17 00:00:00 2001 From: Deezzir Date: Thu, 30 Mar 2023 20:57:17 -0400 Subject: [PATCH 03/85] fix: introduced optional arg for clean_spaces --- Anvil/Tools/Words.pm | 10 ++++++---- scancore-agents/scan-storcli/scan-storcli | 4 ++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/Anvil/Tools/Words.pm b/Anvil/Tools/Words.pm index a038e879..acd619c7 100644 --- a/Anvil/Tools/Words.pm +++ b/Anvil/Tools/Words.pm @@ -203,10 +203,12 @@ sub clean_spaces # Setup default values my $string = defined $parameter->{string} ? $parameter->{string} : ""; - $string =~ s/^\s+//; - $string =~ s/\s+$//; - $string =~ s/\r//g; - $string =~ s/\s+/ /g; + my $merge_spaces = defined $parameter->{merge_spaces} ? $parameter->{merge_spaces} : 1; + + $string =~ s/^\s+//; + $string =~ s/\s+$//; + $string =~ s/\r//g; + $string =~ s/\s+/ /g if $merge_spaces; return($string); } diff --git a/scancore-agents/scan-storcli/scan-storcli b/scancore-agents/scan-storcli/scan-storcli index e45b66be..a499ff9e 100755 --- a/scancore-agents/scan-storcli/scan-storcli +++ b/scancore-agents/scan-storcli/scan-storcli @@ -8755,7 +8755,7 @@ sub get_bbu_data }}); foreach my $line (split/\n/, $output) { - $line = $anvil->Words->clean_spaces({string => $line}); + $line = $anvil->Words->clean_spaces({string => $line, merge_spaces => 0}); $line =~ s/\s+:/:/; $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { line => $line }}); last if $line =~ /$adapter Failed /i; @@ -8987,7 +8987,7 @@ sub get_cachevault_data }}); foreach my $line (split/\n/, $output) { - $line = $anvil->Words->clean_spaces({string => $line}); + $line = $anvil->Words->clean_spaces({string => $line, merge_spaces => 0}); $line =~ s/\s+:/:/; $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { line => $line }}); last if $line =~ /Cachevault doesn't exist/i; From 109aa1ba3d910f0b59924936b88f02246fd5d3f9 Mon Sep 17 00:00:00 2001 From: Deezzir Date: Thu, 30 Mar 2023 21:03:05 -0400 Subject: [PATCH 04/85] docs: added annotation for the new arg --- Anvil/Tools/Words.pm | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Anvil/Tools/Words.pm b/Anvil/Tools/Words.pm index acd619c7..4746e8e5 100644 --- a/Anvil/Tools/Words.pm +++ b/Anvil/Tools/Words.pm @@ -192,6 +192,10 @@ Parameters; This sets the string to be cleaned. If it is not passed in, or if the string is empty, then an empty string will be returned without error. +=head3 merge_spaces (optional) + +This is a boolean value (0 or 1) that, if set, will merge multiple spaces into a single space. If not set, multiple spaces will be left as is. The default is '1'. + =cut sub clean_spaces { From 1c274ba58d88749493736edca36c833958104315 Mon Sep 17 00:00:00 2001 From: digimer Date: Mon, 3 Apr 2023 12:39:20 -0400 Subject: [PATCH 05/85] * Fixed a bug in anvil-delete-server that was preventing the complete deletion of a server if the DRBD resource had already been removed. Signed-off-by: digimer --- Anvil/Tools/Words.pm | 6 +++--- tools/anvil-delete-server | 1 - 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/Anvil/Tools/Words.pm b/Anvil/Tools/Words.pm index 4746e8e5..ff6fb0b8 100644 --- a/Anvil/Tools/Words.pm +++ b/Anvil/Tools/Words.pm @@ -192,9 +192,9 @@ Parameters; This sets the string to be cleaned. If it is not passed in, or if the string is empty, then an empty string will be returned without error. -=head3 merge_spaces (optional) +=head3 merge_spaces (optional, default 1) -This is a boolean value (0 or 1) that, if set, will merge multiple spaces into a single space. If not set, multiple spaces will be left as is. The default is '1'. +This is a boolean value (0 or 1) that, if set, will merge multiple spaces into a single space. If not set, multiple spaces will be left as is. =cut sub clean_spaces @@ -206,7 +206,7 @@ sub clean_spaces $anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => $debug, key => "log_0125", variables => { method => "Words->clean_spaces()" }}); # Setup default values - my $string = defined $parameter->{string} ? $parameter->{string} : ""; + my $string = defined $parameter->{string} ? $parameter->{string} : ""; my $merge_spaces = defined $parameter->{merge_spaces} ? $parameter->{merge_spaces} : 1; $string =~ s/^\s+//; diff --git a/tools/anvil-delete-server b/tools/anvil-delete-server index 6b60bd8b..3dac3791 100755 --- a/tools/anvil-delete-server +++ b/tools/anvil-delete-server @@ -194,7 +194,6 @@ sub run_jobs job_status => "failed", }); $anvil->Log->entry({source => $THIS_FILE, line => __LINE__, 'print' => 1, level => 0, priority => 'err', key => "error_0228", variables => { resource => $server_name }}); - $anvil->nice_exit({exit_code => 1}); } $anvil->Job->update_progress({ From bf2e3e25fb40fdc44c1c86f1f3c6dc8b33ea600b Mon Sep 17 00:00:00 2001 From: digimer Date: Mon, 3 Apr 2023 13:17:01 -0400 Subject: [PATCH 06/85] * Added a check for undefined variable/value pairs in cachevault data that was causeing SQL UPDATE errors. Signed-off-by: digimer --- scancore-agents/scan-storcli/scan-storcli | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/scancore-agents/scan-storcli/scan-storcli b/scancore-agents/scan-storcli/scan-storcli index a499ff9e..de8eeea4 100755 --- a/scancore-agents/scan-storcli/scan-storcli +++ b/scancore-agents/scan-storcli/scan-storcli @@ -4381,6 +4381,7 @@ INSERT INTO foreach my $variable (sort {$a cmp $b} keys %{$anvil->data->{sql}{scan_storcli_variables}{scan_storcli_variable_uuid}{source_table}{'scan_storcli_cachevaults'}{source_uuid}{$cachevault_uuid}{$type}}) { # This variable has vanished + next if not defined $anvil->data->{sql}{scan_storcli_variables}{scan_storcli_variable_uuid}{source_table}{'scan_storcli_cachevaults'}{source_uuid}{$cachevault_uuid}{$type}{$variable}{scan_storcli_variable_value}; my $old_variable_value = $anvil->data->{sql}{scan_storcli_variables}{scan_storcli_variable_uuid}{source_table}{'scan_storcli_cachevaults'}{source_uuid}{$cachevault_uuid}{$type}{$variable}{scan_storcli_variable_value}; my $variable_uuid = $anvil->data->{sql}{scan_storcli_variables}{scan_storcli_variable_uuid}{source_table}{'scan_storcli_cachevaults'}{source_uuid}{$cachevault_uuid}{$type}{$variable}{scan_storcli_variable_uuid}; $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { @@ -9070,8 +9071,8 @@ sub get_cachevault_data ($variable, $value) = ($line =~ /^(.{$split_point})(.*)$/); $variable =~ s/\s+$//; $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { - variable => $variable, - value => $value, + 's1:variable' => $variable, + 's2:value' => $value, }}); } From c7a923fdfb8135a2bc4116af768b08999c74a60d Mon Sep 17 00:00:00 2001 From: digimer Date: Mon, 3 Apr 2023 14:19:31 -0400 Subject: [PATCH 07/85] * Fixed a bug in scan-server where DELETED servers were being set to 'shut off'. Signed-off-by: digimer --- scancore-agents/scan-server/scan-server | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scancore-agents/scan-server/scan-server b/scancore-agents/scan-server/scan-server index 3e28c369..090bbd33 100755 --- a/scancore-agents/scan-server/scan-server +++ b/scancore-agents/scan-server/scan-server @@ -1081,7 +1081,7 @@ DELETED - Marks a server as no longer existing next if $server_host_uuid ne $anvil->Get->host_uuid; # If we're here, the server used to be on us, and isn't on the peer, so mark it as off. - if ($server_state ne "shut off") + if (($server_state ne "shut off") && ($server_state ne "DELETED")) { # Mark it as being off now. my $query = " From d77f309e73de8db8a9ff7ecfff45a43ba0b59cb6 Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Tue, 14 Feb 2023 22:40:29 -0500 Subject: [PATCH 08/85] feat(striker-ui): add AddFenceDeviceForm --- striker-ui/components/AddFenceDeviceForm.tsx | 125 +++++++++++++++++++ striker-ui/types/APIFence.d.ts | 23 ++++ 2 files changed, 148 insertions(+) create mode 100644 striker-ui/components/AddFenceDeviceForm.tsx create mode 100644 striker-ui/types/APIFence.d.ts diff --git a/striker-ui/components/AddFenceDeviceForm.tsx b/striker-ui/components/AddFenceDeviceForm.tsx new file mode 100644 index 00000000..b30b7150 --- /dev/null +++ b/striker-ui/components/AddFenceDeviceForm.tsx @@ -0,0 +1,125 @@ +import { Box } from '@mui/material'; +import { FC, useMemo, useState } from 'react'; +import useIsFirstRender from '../hooks/useIsFirstRender'; +import useProtectedState from '../hooks/useProtectedState'; +import api from '../lib/api'; +import handleAPIError from '../lib/handleAPIError'; +import Autocomplete from './Autocomplete'; +import FlexBox from './FlexBox'; +import Spinner from './Spinner'; +import { BodyText } from './Text'; + +type FenceDeviceAutocompleteOption = { + fenceDeviceDescription: string; + fenceDeviceId: string; + label: string; +}; + +const AddFenceDeivceForm: FC = () => { + const isFirstRender = useIsFirstRender(); + + const [fenceDeviceTemplate, setFenceDeviceTemplate] = useProtectedState< + APIFenceTemplate | undefined + >(undefined); + const [inputFenceDeviceTypeValue, setInputFenceDeviceTypeValue] = + useState(null); + const [isLoadingTemplate, setIsLoadingTemplate] = + useProtectedState(true); + + const fenceDeviceTypeOptions = useMemo( + () => + fenceDeviceTemplate + ? Object.entries(fenceDeviceTemplate).map( + ([id, { description: rawDescription }]) => { + const description = + typeof rawDescription === 'string' + ? rawDescription + : 'No description.'; + + return { + fenceDeviceDescription: description, + fenceDeviceId: id, + label: id, + }; + }, + ) + : [], + [fenceDeviceTemplate], + ); + + const autocompleteFenceDeviceType = useMemo( + () => ( + + option.fenceDeviceId === value.fenceDeviceId + } + label="Fence device type" + onChange={(event, newFenceDeviceType) => { + setInputFenceDeviceTypeValue(newFenceDeviceType); + }} + openOnFocus + options={fenceDeviceTypeOptions} + renderOption={( + props, + { fenceDeviceDescription, fenceDeviceId }, + { selected }, + ) => ( + *': { + width: '100%', + }, + }} + {...props} + > + + {fenceDeviceId} + + {fenceDeviceDescription} + + )} + value={inputFenceDeviceTypeValue} + /> + ), + [fenceDeviceTypeOptions, inputFenceDeviceTypeValue], + ); + + const formContent = useMemo( + () => + isLoadingTemplate ? ( + + ) : ( + <>{autocompleteFenceDeviceType} + ), + [autocompleteFenceDeviceType, isLoadingTemplate], + ); + + if (isFirstRender) { + api + .get(`/fence/template`) + .then(({ data }) => { + setFenceDeviceTemplate(data); + }) + .catch((error) => { + handleAPIError(error); + }) + .finally(() => { + setIsLoadingTemplate(false); + }); + } + + return {formContent}; +}; + +export default AddFenceDeivceForm; diff --git a/striker-ui/types/APIFence.d.ts b/striker-ui/types/APIFence.d.ts new file mode 100644 index 00000000..e44d522d --- /dev/null +++ b/striker-ui/types/APIFence.d.ts @@ -0,0 +1,23 @@ +type APIFenceTemplate = { + [fenceId: string]: { + actions: string[]; + description: string; + parameters: { + [parameterId: string]: { + content_type: 'boolean' | 'integer' | 'second' | 'select' | 'string'; + default: string; + deprecated: number; + description: string; + obsoletes: number; + options?: string[]; + replacement: string; + required: '0' | '1'; + switches: string; + unique: '0' | '1'; + }; + }; + switch: { + [switchId: string]: { name: string }; + }; + }; +}; From 8502f546ce905dd57e806c8c52dc2df7057af418 Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Wed, 15 Feb 2023 16:51:08 -0500 Subject: [PATCH 09/85] fix(striker-ui): allow array options in SelectWithLabel --- striker-ui/components/SelectWithLabel.tsx | 125 ++++++++++++++-------- 1 file changed, 80 insertions(+), 45 deletions(-) diff --git a/striker-ui/components/SelectWithLabel.tsx b/striker-ui/components/SelectWithLabel.tsx index b062403a..8da8980f 100644 --- a/striker-ui/components/SelectWithLabel.tsx +++ b/striker-ui/components/SelectWithLabel.tsx @@ -1,4 +1,4 @@ -import { FC } from 'react'; +import { FC, useCallback, useMemo } from 'react'; import { Checkbox as MUICheckbox, FormControl as MUIFormControl, @@ -28,7 +28,7 @@ type SelectWithLabelOptionalProps = { type SelectWithLabelProps = SelectWithLabelOptionalProps & { id: string; - selectItems: SelectItem[]; + selectItems: Array; }; const SELECT_WITH_LABEL_DEFAULT_PROPS: Required = @@ -54,51 +54,86 @@ const SelectWithLabel: FC = ({ inputLabelProps, isReadOnly, messageBoxProps, - selectProps, + selectProps = {}, isCheckableItems = selectProps?.multiple, -}) => ( - - {label && ( - - {label} - - )} - - - -); + const combinedSx = useMemo( + () => + isReadOnly + ? { + [`& .${muiSelectClasses.icon}`]: { + visibility: 'hidden', + }, + + ...selectSx, + } + : selectSx, + [isReadOnly, selectSx], + ); + + const createCheckbox = useCallback( + (value) => + isCheckableItems && ( + + ), + [checkItem, isCheckableItems], + ); + const createMenuItem = useCallback( + (value, displayValue) => ( + + {createCheckbox(value)} + {displayValue} + + ), + [createCheckbox, disableItem, hideItem, id], + ); + + const inputElement = useMemo(() => , [label]); + const labelElement = useMemo( + () => + label && ( + + {label} + + ), + [id, inputLabelProps, label], + ); + const menuItemElements = useMemo( + () => + selectItems.map((item) => { + const { value, displayValue = value }: SelectItem = + typeof item === 'string' ? { value: item } : item; + + return createMenuItem(value, displayValue); + }), + [createMenuItem, selectItems], + ); + + return ( + + {labelElement} + + + + ); +}; SelectWithLabel.defaultProps = SELECT_WITH_LABEL_DEFAULT_PROPS; From 964dcf231f37b0128784bbe1e5d1e57ec301d344 Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Wed, 15 Feb 2023 16:55:47 -0500 Subject: [PATCH 10/85] fix(striker-ui): allow string header in ExpandablePanel --- striker-ui/components/Panels/ExpandablePanel.tsx | 7 ++++++- striker-ui/components/StrikerConfig/ConfigPeersForm.tsx | 5 +---- .../components/StrikerConfig/ManageChangedSSHKeysForm.tsx | 5 +---- striker-ui/components/StrikerConfig/ManageUsersForm.tsx | 5 +---- 4 files changed, 9 insertions(+), 13 deletions(-) diff --git a/striker-ui/components/Panels/ExpandablePanel.tsx b/striker-ui/components/Panels/ExpandablePanel.tsx index 63fd0bcf..d64e6f97 100644 --- a/striker-ui/components/Panels/ExpandablePanel.tsx +++ b/striker-ui/components/Panels/ExpandablePanel.tsx @@ -12,6 +12,7 @@ import InnerPanel from './InnerPanel'; import InnerPanelBody from './InnerPanelBody'; import InnerPanelHeader from './InnerPanelHeader'; import Spinner from '../Spinner'; +import { BodyText } from '../Text'; type ExpandablePanelOptionalProps = { expandInitially?: boolean; @@ -46,6 +47,10 @@ const ExpandablePanel: FC = ({ [isExpand], ); const contentHeight = useMemo(() => (isExpand ? 'auto' : '.2em'), [isExpand]); + const headerElement = useMemo( + () => (typeof header === 'string' ? {header} : header), + [header], + ); const headerSpinner = useMemo( () => isShowHeaderSpinner && !isExpand && isLoading ? ( @@ -74,7 +79,7 @@ const ExpandablePanel: FC = ({ - {header} + {headerElement} {headerSpinner} = ({ return ( <> - Configure striker peers} - loading={isLoading} - > + = ({ return ( <> - Manage changed SSH keys} - loading={isLoading} - > + The identity of the following targets have unexpectedly changed. diff --git a/striker-ui/components/StrikerConfig/ManageUsersForm.tsx b/striker-ui/components/StrikerConfig/ManageUsersForm.tsx index 4af37416..a861b3e4 100644 --- a/striker-ui/components/StrikerConfig/ManageUsersForm.tsx +++ b/striker-ui/components/StrikerConfig/ManageUsersForm.tsx @@ -36,10 +36,7 @@ const ManageUsersForm: FC = () => { }, [setListMessage, setUsers, users]); return ( - Manage users} - loading={!users} - > + } From 49b157f81b5bcf7829e672802015c38ce984db50 Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Wed, 15 Feb 2023 19:00:16 -0500 Subject: [PATCH 11/85] fix(striker-ui): allow specify valueKey in InputWithRef --- striker-ui/components/InputWithRef.tsx | 36 +++++++++++++++++--------- 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/striker-ui/components/InputWithRef.tsx b/striker-ui/components/InputWithRef.tsx index 13dedb74..7600e3d7 100644 --- a/striker-ui/components/InputWithRef.tsx +++ b/striker-ui/components/InputWithRef.tsx @@ -20,6 +20,7 @@ type InputWithRefOptionalPropsWithDefault< > = { createInputOnChangeHandlerOptions?: CreateInputOnChangeHandlerOptions; required?: boolean; + valueKey?: string; valueType?: TypeName; }; type InputWithRefOptionalPropsWithoutDefault = { @@ -57,6 +58,7 @@ const INPUT_WITH_REF_DEFAULT_PROPS: Required< InputWithRefOptionalPropsWithoutDefault = { createInputOnChangeHandlerOptions: {}, required: false, + valueKey: 'value', valueType: 'string', }; @@ -71,6 +73,7 @@ const InputWithRef = forwardRef( inputTestBatch, onFirstRender, required: isRequired = INPUT_WITH_REF_DEFAULT_PROPS.required, + valueKey = INPUT_WITH_REF_DEFAULT_PROPS.valueKey, valueType = INPUT_WITH_REF_DEFAULT_PROPS.valueType as TypeName, }: InputWithRefProps, ref: ForwardedRef>, @@ -123,6 +126,26 @@ const InputWithRef = forwardRef( })), [initOnBlur, testInput], ); + const onChange = useMemo( + () => + createInputOnChangeHandler({ + postSet: (...args) => { + setIsChangedByUser(true); + initOnChange?.call(null, ...args); + postSetAppend?.call(null, ...args); + }, + set: setValue, + setType: valueType, + ...restCreateInputOnChangeHandlerOptions, + }), + [ + initOnChange, + postSetAppend, + restCreateInputOnChangeHandlerOptions, + setValue, + valueType, + ], + ); const onFocus = useMemo( () => initOnFocus ?? @@ -133,17 +156,6 @@ const InputWithRef = forwardRef( [initOnFocus, inputTestBatch], ); - const onChange = createInputOnChangeHandler({ - postSet: (...args) => { - setIsChangedByUser(true); - initOnChange?.call(null, ...args); - postSetAppend?.call(null, ...args); - }, - set: setValue, - setType: valueType, - ...restCreateInputOnChangeHandlerOptions, - }); - useEffect(() => { if (isFirstRender) { onFirstRender?.call(null, { isRequired }); @@ -167,7 +179,7 @@ const InputWithRef = forwardRef( onChange, onFocus, required: isRequired, - value: inputValue, + [valueKey]: inputValue, }); }, ); From dceeae9c614d0cae41c557d572e9d4f56b147b71 Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Wed, 15 Feb 2023 23:05:01 -0500 Subject: [PATCH 12/85] fix(striker-ui): revise inner and expandable panel types --- .../components/Panels/ExpandablePanel.tsx | 30 +++----------- striker-ui/components/Panels/InnerPanel.tsx | 40 +++++++++---------- striker-ui/types/ExpandablePanel.d.ts | 10 +++++ striker-ui/types/InnerPanel.d.ts | 1 + 4 files changed, 36 insertions(+), 45 deletions(-) create mode 100644 striker-ui/types/ExpandablePanel.d.ts create mode 100644 striker-ui/types/InnerPanel.d.ts diff --git a/striker-ui/components/Panels/ExpandablePanel.tsx b/striker-ui/components/Panels/ExpandablePanel.tsx index d64e6f97..e2ab8081 100644 --- a/striker-ui/components/Panels/ExpandablePanel.tsx +++ b/striker-ui/components/Panels/ExpandablePanel.tsx @@ -3,7 +3,7 @@ import { ExpandMore as ExpandMoreIcon, } from '@mui/icons-material'; import { Box, IconButton } from '@mui/material'; -import { FC, ReactNode, useMemo, useState } from 'react'; +import { FC, useMemo, useState } from 'react'; import { GREY } from '../../lib/consts/DEFAULT_THEME'; @@ -14,31 +14,15 @@ import InnerPanelHeader from './InnerPanelHeader'; import Spinner from '../Spinner'; import { BodyText } from '../Text'; -type ExpandablePanelOptionalProps = { - expandInitially?: boolean; - loading?: boolean; - showHeaderSpinner?: boolean; -}; - -type ExpandablePanelProps = ExpandablePanelOptionalProps & { - header: ReactNode; -}; - -const EXPANDABLE_PANEL_DEFAULT_PROPS: Required = { - expandInitially: false, - loading: false, - showHeaderSpinner: false, -}; const HEADER_SPINNER_LENGTH = '1.2em'; const ExpandablePanel: FC = ({ children, - expandInitially: - isExpandInitially = EXPANDABLE_PANEL_DEFAULT_PROPS.expandInitially, + expandInitially: isExpandInitially = false, header, - loading: isLoading = EXPANDABLE_PANEL_DEFAULT_PROPS.loading, - showHeaderSpinner: - isShowHeaderSpinner = EXPANDABLE_PANEL_DEFAULT_PROPS.showHeaderSpinner, + loading: isLoading = false, + panelProps, + showHeaderSpinner: isShowHeaderSpinner = false, }) => { const [isExpand, setIsExpand] = useState(isExpandInitially); @@ -76,7 +60,7 @@ const ExpandablePanel: FC = ({ ); return ( - + {headerElement} @@ -96,6 +80,4 @@ const ExpandablePanel: FC = ({ ); }; -ExpandablePanel.defaultProps = EXPANDABLE_PANEL_DEFAULT_PROPS; - export default ExpandablePanel; diff --git a/striker-ui/components/Panels/InnerPanel.tsx b/striker-ui/components/Panels/InnerPanel.tsx index 32896fef..65239514 100644 --- a/striker-ui/components/Panels/InnerPanel.tsx +++ b/striker-ui/components/Panels/InnerPanel.tsx @@ -1,28 +1,26 @@ -import { FC } from 'react'; -import { Box as MUIBox, BoxProps as MUIBoxProps } from '@mui/material'; +import { FC, useMemo } from 'react'; +import { Box as MUIBox, SxProps, Theme } from '@mui/material'; import { BORDER_RADIUS, DIVIDER } from '../../lib/consts/DEFAULT_THEME'; -type InnerPanelProps = MUIBoxProps; +const InnerPanel: FC = ({ sx, ...muiBoxRestProps }) => { + const combinedSx = useMemo>( + () => ({ + borderWidth: '1px', + borderRadius: BORDER_RADIUS, + borderStyle: 'solid', + borderColor: DIVIDER, + marginTop: '1.4em', + marginBottom: '1.4em', + paddingBottom: 0, + position: 'relative', -const InnerPanel: FC = ({ sx, ...muiBoxRestProps }) => ( - -); + return ; +}; export default InnerPanel; diff --git a/striker-ui/types/ExpandablePanel.d.ts b/striker-ui/types/ExpandablePanel.d.ts new file mode 100644 index 00000000..5bbc2d79 --- /dev/null +++ b/striker-ui/types/ExpandablePanel.d.ts @@ -0,0 +1,10 @@ +type ExpandablePanelOptionalProps = { + expandInitially?: boolean; + loading?: boolean; + panelProps?: InnerPanelProps; + showHeaderSpinner?: boolean; +}; + +type ExpandablePanelProps = ExpandablePanelOptionalProps & { + header: import('react').ReactNode; +}; diff --git a/striker-ui/types/InnerPanel.d.ts b/striker-ui/types/InnerPanel.d.ts new file mode 100644 index 00000000..0f0b3adb --- /dev/null +++ b/striker-ui/types/InnerPanel.d.ts @@ -0,0 +1 @@ +type InnerPanelProps = import('@mui/material').BoxProps; From 2cd28856dfb576f99d0ed3d7e91ffc36d4d0135a Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Wed, 15 Feb 2023 23:06:25 -0500 Subject: [PATCH 13/85] fix(striker-ui): generate fence parameter inputs --- striker-ui/components/AddFenceDeviceForm.tsx | 157 +++++++++++++++++-- striker-ui/types/APIFence.d.ts | 11 +- 2 files changed, 154 insertions(+), 14 deletions(-) diff --git a/striker-ui/components/AddFenceDeviceForm.tsx b/striker-ui/components/AddFenceDeviceForm.tsx index b30b7150..fd67584d 100644 --- a/striker-ui/components/AddFenceDeviceForm.tsx +++ b/striker-ui/components/AddFenceDeviceForm.tsx @@ -1,13 +1,19 @@ -import { Box } from '@mui/material'; -import { FC, useMemo, useState } from 'react'; -import useIsFirstRender from '../hooks/useIsFirstRender'; -import useProtectedState from '../hooks/useProtectedState'; +import { Box, Switch } from '@mui/material'; +import { FC, ReactElement, ReactNode, useMemo, useState } from 'react'; + import api from '../lib/api'; -import handleAPIError from '../lib/handleAPIError'; import Autocomplete from './Autocomplete'; +import ContainedButton from './ContainedButton'; import FlexBox from './FlexBox'; +import handleAPIError from '../lib/handleAPIError'; +import InputWithRef from './InputWithRef'; +import OutlinedInputWithLabel from './OutlinedInputWithLabel'; +import { ExpandablePanel } from './Panels'; +import SelectWithLabel from './SelectWithLabel'; import Spinner from './Spinner'; import { BodyText } from './Text'; +import useIsFirstRender from '../hooks/useIsFirstRender'; +import useProtectedState from '../hooks/useProtectedState'; type FenceDeviceAutocompleteOption = { fenceDeviceDescription: string; @@ -15,13 +21,54 @@ type FenceDeviceAutocompleteOption = { label: string; }; +type FenceParameterInputBuilder = (args: { + id: string; + isChecked?: boolean; + isRequired?: boolean; + label?: string; + selectOptions?: string[]; + value?: string; +}) => ReactElement; + +const MAP_TO_INPUT_BUILDER: Partial< + Record, FenceParameterInputBuilder> +> & { string: FenceParameterInputBuilder } = { + boolean: ({ id, isChecked = false, label }) => ( + + {label} + + + ), + select: ({ id, isRequired, label, selectOptions = [], value = '' }) => ( + + } + required={isRequired} + /> + ), + string: ({ id, isRequired, label = '', value }) => ( + } + required={isRequired} + /> + ), +}; + const AddFenceDeivceForm: FC = () => { const isFirstRender = useIsFirstRender(); const [fenceDeviceTemplate, setFenceDeviceTemplate] = useProtectedState< APIFenceTemplate | undefined >(undefined); - const [inputFenceDeviceTypeValue, setInputFenceDeviceTypeValue] = + const [fenceDeviceTypeValue, setInputFenceDeviceTypeValue] = useState(null); const [isLoadingTemplate, setIsLoadingTemplate] = useProtectedState(true); @@ -47,7 +94,7 @@ const AddFenceDeivceForm: FC = () => { [fenceDeviceTemplate], ); - const autocompleteFenceDeviceType = useMemo( + const fenceDeviceTypeElement = useMemo( () => ( { {fenceDeviceDescription} )} - value={inputFenceDeviceTypeValue} + value={fenceDeviceTypeValue} /> ), - [fenceDeviceTypeOptions, inputFenceDeviceTypeValue], + [fenceDeviceTypeOptions, fenceDeviceTypeValue], ); + const fenceParameterElements = useMemo(() => { + let result: ReactNode; + + if (fenceDeviceTemplate && fenceDeviceTypeValue) { + const { fenceDeviceId } = fenceDeviceTypeValue; + const { parameters: fenceDeviceParameters } = + fenceDeviceTemplate[fenceDeviceId]; + + const { optional: optionalInputs, required: requiredInputs } = + Object.entries(fenceDeviceParameters).reduce<{ + optional: ReactElement[]; + required: ReactElement[]; + }>( + ( + previous, + [ + parameterId, + { + content_type: contentType, + default: parameterDefault, + options: parameterSelectOptions, + required: isRequired, + }, + ], + ) => { + const { optional, required } = previous; + const buildInput = + MAP_TO_INPUT_BUILDER[contentType] ?? MAP_TO_INPUT_BUILDER.string; + + const fenceJoinParameterId = `${fenceDeviceId}-${parameterId}`; + const parameterIsRequired = isRequired === '1'; + const parameterInput = buildInput({ + id: fenceJoinParameterId, + isChecked: parameterDefault === '1', + isRequired: parameterIsRequired, + label: parameterId, + selectOptions: parameterSelectOptions, + value: parameterDefault, + }); + + if (parameterIsRequired) { + required.push(parameterInput); + } else { + optional.push(parameterInput); + } + + return previous; + }, + { + optional: [], + required: [ + MAP_TO_INPUT_BUILDER.string({ + id: `${fenceDeviceId}-name`, + isRequired: true, + label: 'Fence device name', + }), + ], + }, + ); + + result = ( + <> + + {requiredInputs} + + + {optionalInputs} + + + ); + } + + return result; + }, [fenceDeviceTemplate, fenceDeviceTypeValue]); const formContent = useMemo( () => isLoadingTemplate ? ( ) : ( - <>{autocompleteFenceDeviceType} + { + event.preventDefault(); + }} + sx={{ '& > div': { marginBottom: 0 } }} + > + {fenceDeviceTypeElement} + {fenceParameterElements} + + Add fence device + + ), - [autocompleteFenceDeviceType, isLoadingTemplate], + [fenceDeviceTypeElement, fenceParameterElements, isLoadingTemplate], ); if (isFirstRender) { @@ -119,7 +252,7 @@ const AddFenceDeivceForm: FC = () => { }); } - return {formContent}; + return <>{formContent}; }; export default AddFenceDeivceForm; diff --git a/striker-ui/types/APIFence.d.ts b/striker-ui/types/APIFence.d.ts index e44d522d..2ce11650 100644 --- a/striker-ui/types/APIFence.d.ts +++ b/striker-ui/types/APIFence.d.ts @@ -1,11 +1,18 @@ +type FenceParameterType = + | 'boolean' + | 'integer' + | 'second' + | 'select' + | 'string'; + type APIFenceTemplate = { [fenceId: string]: { actions: string[]; description: string; parameters: { [parameterId: string]: { - content_type: 'boolean' | 'integer' | 'second' | 'select' | 'string'; - default: string; + content_type: FenceParameterType; + default?: string; deprecated: number; description: string; obsoletes: number; From 4cce7b06f4c26fc1f7ed22fe799c88770ada2577 Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Fri, 17 Feb 2023 00:19:48 -0500 Subject: [PATCH 14/85] fix(striker-ui): separate types from Select component --- striker-ui/components/Select.tsx | 102 ++++++++++++++----------------- striker-ui/types/Select.d.ts | 5 ++ 2 files changed, 50 insertions(+), 57 deletions(-) create mode 100644 striker-ui/types/Select.d.ts diff --git a/striker-ui/components/Select.tsx b/striker-ui/components/Select.tsx index 9e037ae8..fffbc715 100644 --- a/striker-ui/components/Select.tsx +++ b/striker-ui/components/Select.tsx @@ -1,82 +1,70 @@ -import { FC } from 'react'; +import { Close as CloseIcon } from '@mui/icons-material'; import { IconButton as MUIIconButton, - IconButtonProps as MUIIconButtonProps, iconButtonClasses as muiIconButtonClasses, inputClasses, Select as MUISelect, selectClasses as muiSelectClasses, - SelectProps as MUISelectProps, InputAdornment as MUIInputAdornment, inputAdornmentClasses as muiInputAdornmentClasses, } from '@mui/material'; -import { Close as CloseIcon } from '@mui/icons-material'; +import { FC, useMemo } from 'react'; import { GREY } from '../lib/consts/DEFAULT_THEME'; -type SelectOptionalProps = { - onClearIndicatorClick?: MUIIconButtonProps['onClick'] | null; -}; - -type SelectProps = MUISelectProps & SelectOptionalProps; +const Select: FC = ({ + onClearIndicatorClick, + ...muiSelectProps +}) => { + const { sx: selectSx, value, ...restMuiSelectProps } = muiSelectProps; -const SELECT_DEFAULT_PROPS: Required = { - onClearIndicatorClick: null, -}; - -const Select: FC = (selectProps) => { - const { - onClearIndicatorClick = SELECT_DEFAULT_PROPS.onClearIndicatorClick, - ...muiSelectProps - } = selectProps; - const { children, sx, value } = muiSelectProps; - const clearIndicator: JSX.Element | undefined = - String(value).length > 0 && onClearIndicatorClick ? ( - - - - - - ) : undefined; - - const combinedSx = { - [`& .${muiSelectClasses.icon}`]: { - color: GREY, - }, + const combinedSx = useMemo( + () => ({ + [`& .${muiSelectClasses.icon}`]: { + color: GREY, + }, - [`& .${muiInputAdornmentClasses.root}`]: { - marginRight: '.8em', - }, + [`& .${muiInputAdornmentClasses.root}`]: { + marginRight: '.8em', + }, - [`& .${muiIconButtonClasses.root}`]: { - color: GREY, - visibility: 'hidden', - }, + [`& .${muiIconButtonClasses.root}`]: { + color: GREY, + visibility: 'hidden', + }, - [`&:hover .${muiInputAdornmentClasses.root} .${muiIconButtonClasses.root}, + [`&:hover .${muiInputAdornmentClasses.root} .${muiIconButtonClasses.root}, &.${inputClasses.focused} .${muiInputAdornmentClasses.root} .${muiIconButtonClasses.root}`]: - { - visibility: 'visible', - }, + { + visibility: 'visible', + }, + + ...selectSx, + }), + [selectSx], + ); - ...sx, - }; + const clearIndicatorElement = useMemo( + () => + String(value).length > 0 && + onClearIndicatorClick && ( + + + + + + ), + [onClearIndicatorClick, value], + ); return ( - {children} - + endAdornment={clearIndicatorElement} + value={value} + {...restMuiSelectProps} + sx={combinedSx} + /> ); }; -Select.defaultProps = SELECT_DEFAULT_PROPS; - -export type { SelectProps }; - export default Select; diff --git a/striker-ui/types/Select.d.ts b/striker-ui/types/Select.d.ts new file mode 100644 index 00000000..6b656304 --- /dev/null +++ b/striker-ui/types/Select.d.ts @@ -0,0 +1,5 @@ +type SelectOptionalProps = { + onClearIndicatorClick?: import('@mui/material').IconButtonProps['onClick']; +}; + +type SelectProps = import('@mui/material').SelectProps & SelectOptionalProps; From f8d395299cc1538073b4604236b67a0d435be251 Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Fri, 17 Feb 2023 00:28:21 -0500 Subject: [PATCH 15/85] fix(striker-ui): separate types from SelectWithLabel component --- .../OutlinedLabeledInputWithSelect.tsx | 18 ++--- striker-ui/components/SelectWithLabel.tsx | 67 ++++++------------- striker-ui/types/SelectWithLabel.d.ts | 22 ++++++ 3 files changed, 46 insertions(+), 61 deletions(-) diff --git a/striker-ui/components/OutlinedLabeledInputWithSelect.tsx b/striker-ui/components/OutlinedLabeledInputWithSelect.tsx index c473810c..2dce2deb 100644 --- a/striker-ui/components/OutlinedLabeledInputWithSelect.tsx +++ b/striker-ui/components/OutlinedLabeledInputWithSelect.tsx @@ -12,7 +12,7 @@ import { MessageBoxProps } from './MessageBox'; import OutlinedInputWithLabel, { OutlinedInputWithLabelProps, } from './OutlinedInputWithLabel'; -import SelectWithLabel, { SelectWithLabelProps } from './SelectWithLabel'; +import SelectWithLabel from './SelectWithLabel'; type OutlinedLabeledInputWithSelectOptionalProps = { inputWithLabelProps?: Partial; @@ -66,19 +66,11 @@ const OutlinedLabeledInputWithSelect: FC< }, }} > - + diff --git a/striker-ui/components/SelectWithLabel.tsx b/striker-ui/components/SelectWithLabel.tsx index 8da8980f..e2a343d6 100644 --- a/striker-ui/components/SelectWithLabel.tsx +++ b/striker-ui/components/SelectWithLabel.tsx @@ -1,48 +1,15 @@ -import { FC, useCallback, useMemo } from 'react'; import { Checkbox as MUICheckbox, FormControl as MUIFormControl, selectClasses as muiSelectClasses, } from '@mui/material'; +import { FC, useCallback, useMemo } from 'react'; import InputMessageBox from './InputMessageBox'; import MenuItem from './MenuItem'; -import { MessageBoxProps } from './MessageBox'; import OutlinedInput from './OutlinedInput'; -import OutlinedInputLabel, { - OutlinedInputLabelProps, -} from './OutlinedInputLabel'; -import Select, { SelectProps } from './Select'; - -type SelectWithLabelOptionalProps = { - checkItem?: ((value: string) => boolean) | null; - disableItem?: ((value: string) => boolean) | null; - hideItem?: ((value: string) => boolean) | null; - isCheckableItems?: boolean; - isReadOnly?: boolean; - inputLabelProps?: Partial; - label?: string | null; - messageBoxProps?: Partial; - selectProps?: Partial; -}; - -type SelectWithLabelProps = SelectWithLabelOptionalProps & { - id: string; - selectItems: Array; -}; - -const SELECT_WITH_LABEL_DEFAULT_PROPS: Required = - { - checkItem: null, - disableItem: null, - hideItem: null, - isReadOnly: false, - isCheckableItems: false, - inputLabelProps: {}, - label: null, - messageBoxProps: {}, - selectProps: {}, - }; +import OutlinedInputLabel from './OutlinedInputLabel'; +import Select from './Select'; const SelectWithLabel: FC = ({ id, @@ -51,14 +18,19 @@ const SelectWithLabel: FC = ({ checkItem, disableItem, hideItem, - inputLabelProps, - isReadOnly, - messageBoxProps, - selectProps = {}, - isCheckableItems = selectProps?.multiple, + inputLabelProps = {}, + isReadOnly = false, + messageBoxProps = {}, + onChange, + selectProps: { + multiple: selectMultiple, + sx: selectSx, + ...restSelectProps + } = {}, + value: selectValue, + // Props with initial value that depend on others. + isCheckableItems = selectMultiple, }) => { - const { sx: selectSx } = selectProps; - const combinedSx = useMemo( () => isReadOnly @@ -124,8 +96,11 @@ const SelectWithLabel: FC = ({ boolean; type SelectWithLabelOptionalProps = { checkItem?: OperateSelectItemFunction; disableItem?: OperateSelectItemFunction; + formControlProps?: import('@mui/material').FormControlProps; hideItem?: OperateSelectItemFunction; isCheckableItems?: boolean; isReadOnly?: boolean; From 23445b07357f7cdd705814da1e01ebb37500ecdf Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Wed, 22 Feb 2023 22:19:37 -0500 Subject: [PATCH 40/85] fix(striker-ui): expand OutlinedInputWithLabel to full width --- striker-ui/components/OutlinedInputWithLabel.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/striker-ui/components/OutlinedInputWithLabel.tsx b/striker-ui/components/OutlinedInputWithLabel.tsx index ca16c777..7ded6bfe 100644 --- a/striker-ui/components/OutlinedInputWithLabel.tsx +++ b/striker-ui/components/OutlinedInputWithLabel.tsx @@ -132,6 +132,7 @@ const OutlinedInputWithLabel: FC = ({ return ( From 227a2d056a6f197edfc2387f6bef4d806449a7e6 Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Mon, 27 Feb 2023 18:16:51 -0500 Subject: [PATCH 41/85] fix(striker-ui): add growFirst and fullWidth switches to FlexBox --- striker-ui/components/FlexBox.tsx | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/striker-ui/components/FlexBox.tsx b/striker-ui/components/FlexBox.tsx index 4a2f8899..08f6750f 100644 --- a/striker-ui/components/FlexBox.tsx +++ b/striker-ui/components/FlexBox.tsx @@ -6,6 +6,8 @@ type FlexBoxDirection = 'column' | 'row'; type FlexBoxSpacing = number | string; type FlexBoxOptionalPropsWithDefault = { + fullWidth?: boolean; + growFirst?: boolean; row?: boolean; spacing?: FlexBoxSpacing; xs?: FlexBoxDirection; @@ -28,6 +30,8 @@ type FlexBoxProps = MUIBoxProps & FlexBoxOptionalProps; const FLEX_BOX_DEFAULT_PROPS: Required & FlexBoxOptionalPropsWithoutDefault = { columnSpacing: undefined, + fullWidth: false, + growFirst: false, row: false, rowSpacing: undefined, lg: undefined, @@ -39,6 +43,8 @@ const FLEX_BOX_DEFAULT_PROPS: Required & }; const FlexBox: FC = ({ + fullWidth, + growFirst, lg: dLg = FLEX_BOX_DEFAULT_PROPS.lg, md: dMd = FLEX_BOX_DEFAULT_PROPS.md, row: isRow, @@ -50,7 +56,6 @@ const FlexBox: FC = ({ // Input props that depend on other input props. columnSpacing = spacing, rowSpacing = spacing, - ...muiBoxRestProps }) => { const xs = useMemo(() => (isRow ? 'row' : dXs), [dXs, isRow]); @@ -81,6 +86,11 @@ const FlexBox: FC = ({ }), [columnSpacing, rowSpacing], ); + const firstChildFlexGrow = useMemo( + () => (growFirst ? 1 : undefined), + [growFirst], + ); + const width = useMemo(() => (fullWidth ? '100%' : undefined), [fullWidth]); return ( = ({ }, display: 'flex', flexDirection: { xs, sm, md, lg, xl }, + width, + + '& > :first-child': { + flexGrow: firstChildFlexGrow, + }, '& > :not(:first-child)': { marginLeft: { From ec25d434321c21a7f241c76e2d401eb120285149 Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Mon, 27 Feb 2023 18:19:59 -0500 Subject: [PATCH 42/85] fix(striker-ui): add input sensitivity and expose tooltip props in CommonFenceInputGroup --- .../components/CommonFenceInputGroup.tsx | 184 ++++++++++++------ striker-ui/types/CommonFenceInputGroup.d.ts | 10 +- 2 files changed, 131 insertions(+), 63 deletions(-) diff --git a/striker-ui/components/CommonFenceInputGroup.tsx b/striker-ui/components/CommonFenceInputGroup.tsx index b1fb333d..95756237 100644 --- a/striker-ui/components/CommonFenceInputGroup.tsx +++ b/striker-ui/components/CommonFenceInputGroup.tsx @@ -1,72 +1,105 @@ +import { Box, styled, Tooltip } from '@mui/material'; import { FC, ReactElement, ReactNode, useMemo } from 'react'; +import INPUT_TYPES from '../lib/consts/INPUT_TYPES'; + import FlexBox from './FlexBox'; import InputWithRef from './InputWithRef'; import OutlinedInputWithLabel from './OutlinedInputWithLabel'; import { ExpandablePanel } from './Panels'; import SelectWithLabel from './SelectWithLabel'; import SwitchWithLabel from './SwitchWithLabel'; +import { BodyText } from './Text'; const CHECKED_STATES: Array = ['1', 'on']; const ID_SEPARATOR = '-'; const MAP_TO_INPUT_BUILDER: MapToInputBuilder = { - boolean: ({ id, isChecked = false, label, name = id }) => ( - - } - valueType="boolean" - /> - ), - select: ({ - id, - isRequired, - label, - name = id, - selectOptions = [], - value = '', - }) => ( - - } - required={isRequired} - /> - ), - string: ({ id, isRequired, label = '', name = id, value }) => ( - - } - required={isRequired} - /> - ), + boolean: (args) => { + const { id, isChecked = false, label, name = id } = args; + + return ( + + } + valueType="boolean" + /> + ); + }, + select: (args) => { + const { + id, + isRequired, + label, + name = id, + selectOptions = [], + value = '', + } = args; + + return ( + + } + required={isRequired} + /> + ); + }, + string: (args) => { + const { + id, + isRequired, + isSensitive = false, + label = '', + name = id, + value, + } = args; + + return ( + + } + required={isRequired} + /> + ); + }, }; const combineIds = (...pieces: string[]) => pieces.join(ID_SEPARATOR); +const FenceInputWrapper = styled(FlexBox)({ + margin: '.4em 0', +}); + const CommonFenceInputGroup: FC = ({ fenceId, + fenceParameterTooltipProps, fenceTemplate, previousFenceName, previousFenceParameters, @@ -103,41 +136,64 @@ const CommonFenceInputGroup: FC = ({ [ parameterId, { - content_type: contentType, + content_type: parameterType, default: parameterDefault, - deprecated: rawDeprecated, + deprecated: rawParameterDeprecated, + description: parameterDescription, options: parameterSelectOptions, - required: rawRequired, + required: rawParameterRequired, }, ], ) => { - const isParameterDeprecated = String(rawDeprecated) === '1'; + const isParameterDeprecated = + String(rawParameterDeprecated) === '1'; if (!isParameterDeprecated) { const { optional, required } = previous; const buildInput = - MAP_TO_INPUT_BUILDER[contentType] ?? + MAP_TO_INPUT_BUILDER[parameterType] ?? MAP_TO_INPUT_BUILDER.string; const fenceJoinParameterId = combineIds(fenceId, parameterId); const initialValue = mapToPreviousFenceParameterValues[fenceJoinParameterId] ?? parameterDefault; - const isParameterRequired = String(rawRequired) === '1'; + const isParameterRequired = + String(rawParameterRequired) === '1'; + const isParameterSensitive = /passw/i.test(parameterId); const parameterInput = buildInput({ id: fenceJoinParameterId, isChecked: CHECKED_STATES.includes(initialValue), isRequired: isParameterRequired, + isSensitive: isParameterSensitive, label: parameterId, selectOptions: parameterSelectOptions, value: initialValue, }); + const parameterInputWithTooltip = ( + {parameterDescription}} + {...fenceParameterTooltipProps} + > + {parameterInput} + + ); if (isParameterRequired) { - required.push(parameterInput); + required.push(parameterInputWithTooltip); } else { - optional.push(parameterInput); + optional.push(parameterInputWithTooltip); } } @@ -164,17 +220,23 @@ const CommonFenceInputGroup: FC = ({ }} > - {requiredInputs} + {requiredInputs} - {optionalInputs} + {optionalInputs} ); } return result; - }, [fenceId, fenceTemplate, previousFenceName, previousFenceParameters]); + }, [ + fenceId, + fenceParameterTooltipProps, + fenceTemplate, + previousFenceName, + previousFenceParameters, + ]); return <>{fenceParameterElements}; }; diff --git a/striker-ui/types/CommonFenceInputGroup.d.ts b/striker-ui/types/CommonFenceInputGroup.d.ts index fca8a606..4e1ca15d 100644 --- a/striker-ui/types/CommonFenceInputGroup.d.ts +++ b/striker-ui/types/CommonFenceInputGroup.d.ts @@ -1,12 +1,17 @@ -type FenceParameterInputBuilder = (args: { +type FenceParameterInputBuilderParameters = { id: string; isChecked?: boolean; isRequired?: boolean; + isSensitive?: boolean; label?: string; name?: string; selectOptions?: string[]; value?: string; -}) => ReactElement; +}; + +type FenceParameterInputBuilder = ( + args: FenceParameterInputBuilderParameters, +) => ReactElement; type MapToInputBuilder = Partial< Record, FenceParameterInputBuilder> @@ -17,6 +22,7 @@ type CommonFenceInputGroupOptionalProps = { fenceTemplate?: APIFenceTemplate; previousFenceName?: string; previousFenceParameters?: FenceParameters; + fenceParameterTooltipProps?: import('@mui/material').TooltipProps; }; type CommonFenceInputGroupProps = CommonFenceInputGroupOptionalProps; From ce18242193c9f2c8b1222fd93f62bc7cc5d71d47 Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Tue, 28 Feb 2023 01:31:18 -0500 Subject: [PATCH 43/85] fix(striker-ui): make IconButton change based on state --- .../components/IconButton/IconButton.tsx | 121 ++++++++++++++---- striker-ui/types/IconButton.d.ts | 16 +++ 2 files changed, 109 insertions(+), 28 deletions(-) create mode 100644 striker-ui/types/IconButton.d.ts diff --git a/striker-ui/components/IconButton/IconButton.tsx b/striker-ui/components/IconButton/IconButton.tsx index fb22451a..2782db4a 100644 --- a/striker-ui/components/IconButton/IconButton.tsx +++ b/striker-ui/components/IconButton/IconButton.tsx @@ -1,9 +1,16 @@ -import { FC } from 'react'; +import { + Done as MUIDoneIcon, + Edit as MUIEditIcon, + Visibility as MUIVisibilityIcon, + VisibilityOff as MUIVisibilityOffIcon, +} from '@mui/icons-material'; import { IconButton as MUIIconButton, IconButtonProps as MUIIconButtonProps, inputClasses as muiInputClasses, + styled, } from '@mui/material'; +import { createElement, FC, ReactNode, useMemo } from 'react'; import { BLACK, @@ -13,35 +20,93 @@ import { TEXT, } from '../../lib/consts/DEFAULT_THEME'; -export type IconButtonProps = MUIIconButtonProps; +type IconButtonProps = IconButtonOptionalProps & MUIIconButtonProps; + +const ContainedIconButton = styled(MUIIconButton)({ + borderRadius: BORDER_RADIUS, + backgroundColor: GREY, + color: BLACK, + + '&:hover': { + backgroundColor: TEXT, + }, + + [`&.${muiInputClasses.disabled}`]: { + backgroundColor: DISABLED, + }, +}); + +const NormalIconButton = styled(MUIIconButton)({ + color: GREY, +}); + +const MAP_TO_VISIBILITY_ICON: IconButtonMapToStateIcon = { + false: MUIVisibilityIcon, + true: MUIVisibilityOffIcon, +}; + +const MAP_TO_EDIT_ICON: IconButtonMapToStateIcon = { + false: MUIEditIcon, + true: MUIDoneIcon, +}; + +const MAP_TO_MAP_PRESET: Record< + IconButtonPresetMapToStateIcon, + IconButtonMapToStateIcon +> = { + edit: MAP_TO_EDIT_ICON, + visibility: MAP_TO_VISIBILITY_ICON, +}; + +const MAP_TO_VARIANT: Record = { + contained: ContainedIconButton, + normal: NormalIconButton, +}; const IconButton: FC = ({ children, - sx, - ...iconButtonRestProps -}) => ( - - {children} - -); + defaultIcon, + iconProps, + mapPreset, + mapToIcon: externalMapToIcon, + state, + variant = 'contained', + ...restIconButtonProps +}) => { + const mapToIcon = useMemo( + () => externalMapToIcon ?? (mapPreset && MAP_TO_MAP_PRESET[mapPreset]), + [externalMapToIcon, mapPreset], + ); + + const iconButtonContent = useMemo(() => { + let result: ReactNode; + + if (mapToIcon) { + const iconElementType: CreatableComponent | undefined = state + ? mapToIcon[state] ?? defaultIcon + : defaultIcon; + + if (iconElementType) { + result = createElement(iconElementType, iconProps); + } + } else { + result = children; + } + + return result; + }, [children, mapToIcon, state, defaultIcon, iconProps]); + const iconButtonElementType = useMemo( + () => MAP_TO_VARIANT[variant], + [variant], + ); + + return createElement( + iconButtonElementType, + restIconButtonProps, + iconButtonContent, + ); +}; + +export type { IconButtonProps }; export default IconButton; diff --git a/striker-ui/types/IconButton.d.ts b/striker-ui/types/IconButton.d.ts new file mode 100644 index 00000000..af8ecf5a --- /dev/null +++ b/striker-ui/types/IconButton.d.ts @@ -0,0 +1,16 @@ +type CreatableComponent = Parameters[0]; + +type IconButtonPresetMapToStateIcon = 'edit' | 'visibility'; + +type IconButtonMapToStateIcon = Record; + +type IconButtonVariant = 'contained' | 'normal'; + +type IconButtonOptionalProps = { + defaultIcon?: CreatableComponent; + iconProps?: import('@mui/material').SvgIconProps; + mapPreset?: IconButtonPresetMapToStateIcon; + mapToIcon?: IconButtonMapToStateIcon; + state?: string; + variant?: IconButtonVariant; +}; From 64c6fedf013ab483e0f511bb0bfcb2aa7c22026a Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Tue, 28 Feb 2023 01:33:18 -0500 Subject: [PATCH 44/85] feat(striker-ui): add SensitiveText --- striker-ui/components/Text/InlineMonoText.tsx | 9 +-- striker-ui/components/Text/MonoText.tsx | 6 +- striker-ui/components/Text/SensitiveText.tsx | 64 +++++++++++++++++++ striker-ui/components/Text/SmallText.tsx | 6 +- striker-ui/components/Text/index.tsx | 10 ++- striker-ui/types/SensitiveText.d.ts | 7 ++ 6 files changed, 83 insertions(+), 19 deletions(-) create mode 100644 striker-ui/components/Text/SensitiveText.tsx create mode 100644 striker-ui/types/SensitiveText.d.ts diff --git a/striker-ui/components/Text/InlineMonoText.tsx b/striker-ui/components/Text/InlineMonoText.tsx index 2032152f..85823d5c 100644 --- a/striker-ui/components/Text/InlineMonoText.tsx +++ b/striker-ui/components/Text/InlineMonoText.tsx @@ -3,12 +3,7 @@ import { FC } from 'react'; import { BodyTextProps } from './BodyText'; import SmallText from './SmallText'; -type InlineMonoTextProps = BodyTextProps; - -const InlineMonoText: FC = ({ - sx, - ...bodyTextRestProps -}) => ( +const InlineMonoText: FC = ({ sx, ...bodyTextRestProps }) => ( = ({ /> ); -export type { InlineMonoTextProps }; - export default InlineMonoText; diff --git a/striker-ui/components/Text/MonoText.tsx b/striker-ui/components/Text/MonoText.tsx index 2c409219..c2cffa45 100644 --- a/striker-ui/components/Text/MonoText.tsx +++ b/striker-ui/components/Text/MonoText.tsx @@ -3,9 +3,7 @@ import { FC } from 'react'; import { BodyTextProps } from './BodyText'; import SmallText from './SmallText'; -type MonoTextProps = BodyTextProps; - -const MonoText: FC = ({ sx, ...bodyTextRestProps }) => ( +const MonoText: FC = ({ sx, ...bodyTextRestProps }) => ( = ({ sx, ...bodyTextRestProps }) => ( /> ); -export type { MonoTextProps }; - export default MonoText; diff --git a/striker-ui/components/Text/SensitiveText.tsx b/striker-ui/components/Text/SensitiveText.tsx new file mode 100644 index 00000000..16aa3509 --- /dev/null +++ b/striker-ui/components/Text/SensitiveText.tsx @@ -0,0 +1,64 @@ +import { createElement, FC, ReactNode, useMemo, useState } from 'react'; + +import BodyText from './BodyText'; +import FlexBox from '../FlexBox'; +import IconButton from '../IconButton'; +import MonoText from './MonoText'; + +const SensitiveText: FC = ({ + children, + monospaced: isMonospaced = false, + revealInitially: isRevealInitially = false, + textProps, +}) => { + const [isReveal, setIsReveal] = useState(isRevealInitially); + + const textElementType = useMemo( + () => (isMonospaced ? MonoText : BodyText), + [isMonospaced], + ); + const contentElement = useMemo(() => { + let content: ReactNode; + + if (isReveal) { + content = + typeof children === 'string' + ? createElement( + textElementType, + { + sx: { + lineHeight: 2.8, + maxWidth: '20em', + overflowY: 'scroll', + whiteSpace: 'nowrap', + }, + ...textProps, + }, + children, + ) + : children; + } else { + content = createElement(textElementType, textProps, '*****'); + } + + return content; + }, [children, isReveal, textElementType, textProps]); + + return ( + + {contentElement} + { + setIsReveal((previous) => !previous); + }} + state={String(isReveal)} + sx={{ marginRight: '-.2em', padding: '.2em' }} + variant="normal" + /> + + ); +}; + +export default SensitiveText; diff --git a/striker-ui/components/Text/SmallText.tsx b/striker-ui/components/Text/SmallText.tsx index 9fdc7bb1..bfe56ee7 100644 --- a/striker-ui/components/Text/SmallText.tsx +++ b/striker-ui/components/Text/SmallText.tsx @@ -2,12 +2,8 @@ import { FC } from 'react'; import BodyText, { BodyTextProps } from './BodyText'; -type SmallTextProps = BodyTextProps; - -const SmallText: FC = ({ ...bodyTextRestProps }) => ( +const SmallText: FC = ({ ...bodyTextRestProps }) => ( ); -export type { SmallTextProps }; - export default SmallText; diff --git a/striker-ui/components/Text/index.tsx b/striker-ui/components/Text/index.tsx index 13f0260b..f72cbbb2 100644 --- a/striker-ui/components/Text/index.tsx +++ b/striker-ui/components/Text/index.tsx @@ -2,8 +2,16 @@ import BodyText, { BodyTextProps } from './BodyText'; import HeaderText from './HeaderText'; import InlineMonoText from './InlineMonoText'; import MonoText from './MonoText'; +import SensitiveText from './SensitiveText'; import SmallText from './SmallText'; export type { BodyTextProps }; -export { BodyText, HeaderText, InlineMonoText, MonoText, SmallText }; +export { + BodyText, + HeaderText, + InlineMonoText, + MonoText, + SensitiveText, + SmallText, +}; diff --git a/striker-ui/types/SensitiveText.d.ts b/striker-ui/types/SensitiveText.d.ts new file mode 100644 index 00000000..2a7921cb --- /dev/null +++ b/striker-ui/types/SensitiveText.d.ts @@ -0,0 +1,7 @@ +type SensitiveTextOptionalProps = { + monospaced?: boolean; + revealInitially?: boolean; + textProps?: import('../components/Text').BodyTextProps; +}; + +type SensitiveTextProps = SensitiveTextOptionalProps; From ac7ce0dcb6f88053ebf9ac31e200c899fc529353 Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Tue, 28 Feb 2023 01:34:52 -0500 Subject: [PATCH 45/85] fix(striker-ui): add confirm dialog to ManageFencesPanel --- striker-ui/components/ManageFencesPanel.tsx | 164 +++++++++++++++++--- 1 file changed, 144 insertions(+), 20 deletions(-) diff --git a/striker-ui/components/ManageFencesPanel.tsx b/striker-ui/components/ManageFencesPanel.tsx index 081e65c2..6b52d587 100644 --- a/striker-ui/components/ManageFencesPanel.tsx +++ b/striker-ui/components/ManageFencesPanel.tsx @@ -1,4 +1,12 @@ -import { FC, FormEventHandler, useMemo, useRef, useState } from 'react'; +import { Box } from '@mui/material'; +import { + FC, + FormEventHandler, + ReactElement, + useMemo, + useRef, + useState, +} from 'react'; import API_BASE_URL from '../lib/consts/API_BASE_URL'; @@ -13,9 +21,29 @@ import List from './List'; import { Panel, PanelHeader } from './Panels'; import periodicFetch from '../lib/fetchers/periodicFetch'; import Spinner from './Spinner'; -import { BodyText, HeaderText, InlineMonoText } from './Text'; +import { + BodyText, + HeaderText, + InlineMonoText, + MonoText, + SmallText, +} from './Text'; import useIsFirstRender from '../hooks/useIsFirstRender'; import useProtectedState from '../hooks/useProtectedState'; +import SensitiveText from './Text/SensitiveText'; + +type FormFenceParameterData = { + fenceAgent: string; + fenceName: string; + parameterInputs: { + [parameterInputId: string]: { + isParameterSensitive: boolean; + parameterId: string; + parameterType: string; + parameterValue: string; + }; + }; +}; const fenceParameterBooleanToString = (value: boolean) => (value ? '1' : '0'); @@ -25,12 +53,7 @@ const getFormFenceParameters = ( ) => { const { elements } = target as HTMLFormElement; - return Object.values(elements).reduce<{ - fenceAgent: string; - parameters: { - [parameterId: string]: { type: string; value: string }; - }; - }>( + return Object.values(elements).reduce( (previous, formElement) => { const { id: inputId } = formElement; const reExtract = new RegExp(`^(fence[^-]+)${ID_SEPARATOR}([^\\s]+)$`); @@ -42,7 +65,16 @@ const getFormFenceParameters = ( previous.fenceAgent = fenceId; const inputElement = formElement as HTMLInputElement; - const { checked, value } = inputElement; + const { + checked, + dataset: { sensitive: rawSensitive }, + value, + } = inputElement; + + if (parameterId === 'name') { + previous.fenceName = value; + } + const { [fenceId]: { parameters: { @@ -51,9 +83,11 @@ const getFormFenceParameters = ( }, } = fenceTemplate; - previous.parameters[parameterId] = { - type: parameterType, - value: + previous.parameterInputs[inputId] = { + isParameterSensitive: rawSensitive === 'true', + parameterId, + parameterType, + parameterValue: parameterType === 'boolean' ? fenceParameterBooleanToString(checked) : value, @@ -62,14 +96,58 @@ const getFormFenceParameters = ( return previous; }, - { fenceAgent: '', parameters: {} }, + { fenceAgent: '', fenceName: '', parameterInputs: {} }, ); }; +const buildConfirmFenceParameters = ( + parameterInputs: FormFenceParameterData['parameterInputs'], +) => ( + { + let textElement: ReactElement; + + if (parameterValue) { + textElement = isParameterSensitive ? ( + {parameterValue} + ) : ( + + + {parameterValue} + + + ); + } else { + textElement = none; + } + + return ( + + {parameterId} + {textElement} + + ); + }} + /> +); + const ManageFencesPanel: FC = () => { const isFirstRender = useIsFirstRender(); const confirmDialogRef = useRef({}); + const formDialogRef = useRef({}); const [confirmDialogProps, setConfirmDialogProps] = useState({ @@ -77,6 +155,11 @@ const ManageFencesPanel: FC = () => { content: '', titleText: '', }); + const [formDialogProps, setFormDialogProps] = useState({ + actionProceedText: '', + content: '', + titleText: '', + }); const [fenceTemplate, setFenceTemplate] = useProtectedState< APIFenceTemplate | undefined >(undefined); @@ -98,7 +181,7 @@ const ManageFencesPanel: FC = () => { header listItems={fenceOverviews} onAdd={() => { - setConfirmDialogProps({ + setFormDialogProps({ actionProceedText: 'Add', content: , onSubmitAppend: (event) => { @@ -106,18 +189,34 @@ const ManageFencesPanel: FC = () => { return; } - getFormFenceParameters(fenceTemplate, event); + const addData = getFormFenceParameters(fenceTemplate, event); + + setConfirmDialogProps({ + actionProceedText: 'Add', + content: buildConfirmFenceParameters(addData.parameterInputs), + titleText: ( + + Add a{' '} + + {addData.fenceAgent} + {' '} + fence device with the following parameters? + + ), + }); + + confirmDialogRef.current.setOpen?.call(null, true); }, titleText: 'Add a fence device', }); - confirmDialogRef.current.setOpen?.call(null, true); + formDialogRef.current.setOpen?.call(null, true); }} onEdit={() => { setIsEditFences((previous) => !previous); }} onItemClick={({ fenceAgent: fenceId, fenceName, fenceParameters }) => { - setConfirmDialogProps({ + setFormDialogProps({ actionProceedText: 'Update', content: ( { return; } - getFormFenceParameters(fenceTemplate, event); + const editData = getFormFenceParameters(fenceTemplate, event); + + setConfirmDialogProps({ + actionProceedText: 'Update', + content: buildConfirmFenceParameters(editData.parameterInputs), + titleText: ( + + Update{' '} + + {editData.fenceName} + {' '} + fence device with the following parameters? + + ), + }); + + confirmDialogRef.current.setOpen?.call(null, true); }, titleText: ( Update fence device{' '} - {fenceName}{' '} + {fenceName}{' '} parameters ), }); - confirmDialogRef.current.setOpen?.call(null, true); + formDialogRef.current.setOpen?.call(null, true); }} renderListItem={( fenceUUID, @@ -201,6 +316,15 @@ const ManageFencesPanel: FC = () => { PaperProps: { sx: { minWidth: { xs: '90%', md: '50em' } } }, }} formContent + scrollBoxProps={{ + padding: '.3em .5em', + }} + scrollContent + {...formDialogProps} + ref={formDialogRef} + /> + Date: Tue, 28 Feb 2023 14:03:37 -0500 Subject: [PATCH 46/85] refactor(striker-ui): group manage fence related components --- .../{ => ManageFence}/AddFenceInputGroup.tsx | 8 ++--- .../CommonFenceInputGroup.tsx | 18 +++++------ .../{ => ManageFence}/EditFenceInputGroup.tsx | 2 +- .../ManageFencePanel.tsx} | 30 +++++++++---------- striker-ui/components/ManageFence/index.tsx | 3 ++ striker-ui/pages/manage-element/index.tsx | 26 ++++++++++++---- 6 files changed, 52 insertions(+), 35 deletions(-) rename striker-ui/components/{ => ManageFence}/AddFenceInputGroup.tsx (94%) rename striker-ui/components/{ => ManageFence}/CommonFenceInputGroup.tsx (94%) rename striker-ui/components/{ => ManageFence}/EditFenceInputGroup.tsx (96%) rename striker-ui/components/{ManageFencesPanel.tsx => ManageFence/ManageFencePanel.tsx} (93%) create mode 100644 striker-ui/components/ManageFence/index.tsx diff --git a/striker-ui/components/AddFenceInputGroup.tsx b/striker-ui/components/ManageFence/AddFenceInputGroup.tsx similarity index 94% rename from striker-ui/components/AddFenceInputGroup.tsx rename to striker-ui/components/ManageFence/AddFenceInputGroup.tsx index 5f083fdf..8657a6d5 100644 --- a/striker-ui/components/AddFenceInputGroup.tsx +++ b/striker-ui/components/ManageFence/AddFenceInputGroup.tsx @@ -1,11 +1,11 @@ import { Box } from '@mui/material'; import { FC, useMemo, useState } from 'react'; -import Autocomplete from './Autocomplete'; +import Autocomplete from '../Autocomplete'; import CommonFenceInputGroup from './CommonFenceInputGroup'; -import FlexBox from './FlexBox'; -import Spinner from './Spinner'; -import { BodyText } from './Text'; +import FlexBox from '../FlexBox'; +import Spinner from '../Spinner'; +import { BodyText } from '../Text'; const AddFenceInputGroup: FC = ({ fenceTemplate: externalFenceTemplate, diff --git a/striker-ui/components/CommonFenceInputGroup.tsx b/striker-ui/components/ManageFence/CommonFenceInputGroup.tsx similarity index 94% rename from striker-ui/components/CommonFenceInputGroup.tsx rename to striker-ui/components/ManageFence/CommonFenceInputGroup.tsx index 95756237..9c70146e 100644 --- a/striker-ui/components/CommonFenceInputGroup.tsx +++ b/striker-ui/components/ManageFence/CommonFenceInputGroup.tsx @@ -1,15 +1,15 @@ import { Box, styled, Tooltip } from '@mui/material'; import { FC, ReactElement, ReactNode, useMemo } from 'react'; -import INPUT_TYPES from '../lib/consts/INPUT_TYPES'; - -import FlexBox from './FlexBox'; -import InputWithRef from './InputWithRef'; -import OutlinedInputWithLabel from './OutlinedInputWithLabel'; -import { ExpandablePanel } from './Panels'; -import SelectWithLabel from './SelectWithLabel'; -import SwitchWithLabel from './SwitchWithLabel'; -import { BodyText } from './Text'; +import INPUT_TYPES from '../../lib/consts/INPUT_TYPES'; + +import FlexBox from '../FlexBox'; +import InputWithRef from '../InputWithRef'; +import OutlinedInputWithLabel from '../OutlinedInputWithLabel'; +import { ExpandablePanel } from '../Panels'; +import SelectWithLabel from '../SelectWithLabel'; +import SwitchWithLabel from '../SwitchWithLabel'; +import { BodyText } from '../Text'; const CHECKED_STATES: Array = ['1', 'on']; const ID_SEPARATOR = '-'; diff --git a/striker-ui/components/EditFenceInputGroup.tsx b/striker-ui/components/ManageFence/EditFenceInputGroup.tsx similarity index 96% rename from striker-ui/components/EditFenceInputGroup.tsx rename to striker-ui/components/ManageFence/EditFenceInputGroup.tsx index 2c382b54..f9989d14 100644 --- a/striker-ui/components/EditFenceInputGroup.tsx +++ b/striker-ui/components/ManageFence/EditFenceInputGroup.tsx @@ -1,7 +1,7 @@ import { FC, useMemo } from 'react'; import CommonFenceInputGroup from './CommonFenceInputGroup'; -import Spinner from './Spinner'; +import Spinner from '../Spinner'; const EditFenceInputGroup: FC = ({ fenceId, diff --git a/striker-ui/components/ManageFencesPanel.tsx b/striker-ui/components/ManageFence/ManageFencePanel.tsx similarity index 93% rename from striker-ui/components/ManageFencesPanel.tsx rename to striker-ui/components/ManageFence/ManageFencePanel.tsx index 6b52d587..efa60324 100644 --- a/striker-ui/components/ManageFencesPanel.tsx +++ b/striker-ui/components/ManageFence/ManageFencePanel.tsx @@ -8,29 +8,29 @@ import { useState, } from 'react'; -import API_BASE_URL from '../lib/consts/API_BASE_URL'; +import API_BASE_URL from '../../lib/consts/API_BASE_URL'; import AddFenceInputGroup from './AddFenceInputGroup'; -import api from '../lib/api'; +import api from '../../lib/api'; import { ID_SEPARATOR } from './CommonFenceInputGroup'; -import ConfirmDialog from './ConfirmDialog'; +import ConfirmDialog from '../ConfirmDialog'; import EditFenceInputGroup from './EditFenceInputGroup'; -import FlexBox from './FlexBox'; -import handleAPIError from '../lib/handleAPIError'; -import List from './List'; -import { Panel, PanelHeader } from './Panels'; -import periodicFetch from '../lib/fetchers/periodicFetch'; -import Spinner from './Spinner'; +import FlexBox from '../FlexBox'; +import handleAPIError from '../../lib/handleAPIError'; +import List from '../List'; +import { Panel, PanelHeader } from '../Panels'; +import periodicFetch from '../../lib/fetchers/periodicFetch'; +import Spinner from '../Spinner'; import { BodyText, HeaderText, InlineMonoText, MonoText, + SensitiveText, SmallText, -} from './Text'; -import useIsFirstRender from '../hooks/useIsFirstRender'; -import useProtectedState from '../hooks/useProtectedState'; -import SensitiveText from './Text/SensitiveText'; +} from '../Text'; +import useIsFirstRender from '../../hooks/useIsFirstRender'; +import useProtectedState from '../../hooks/useProtectedState'; type FormFenceParameterData = { fenceAgent: string; @@ -143,7 +143,7 @@ const buildConfirmFenceParameters = ( /> ); -const ManageFencesPanel: FC = () => { +const ManageFencePanel: FC = () => { const isFirstRender = useIsFirstRender(); const confirmDialogRef = useRef({}); @@ -333,4 +333,4 @@ const ManageFencesPanel: FC = () => { ); }; -export default ManageFencesPanel; +export default ManageFencePanel; diff --git a/striker-ui/components/ManageFence/index.tsx b/striker-ui/components/ManageFence/index.tsx new file mode 100644 index 00000000..b2bd1a47 --- /dev/null +++ b/striker-ui/components/ManageFence/index.tsx @@ -0,0 +1,3 @@ +import ManageFencePanel from './ManageFencePanel'; + +export default ManageFencePanel; diff --git a/striker-ui/pages/manage-element/index.tsx b/striker-ui/pages/manage-element/index.tsx index 5add77d6..18eceb99 100644 --- a/striker-ui/pages/manage-element/index.tsx +++ b/striker-ui/pages/manage-element/index.tsx @@ -7,6 +7,7 @@ import getQueryParam from '../../lib/getQueryParam'; import Grid from '../../components/Grid'; import handleAPIError from '../../lib/handleAPIError'; import Header from '../../components/Header'; +import ManageFencePanel from '../../components/ManageFence'; import { Panel } from '../../components/Panels'; import PrepareHostForm from '../../components/PrepareHostForm'; import PrepareNetworkForm from '../../components/PrepareNetworkForm'; @@ -21,9 +22,9 @@ import useProtectedState from '../../hooks/useProtectedState'; const MAP_TO_PAGE_TITLE: Record = { 'prepare-host': 'Prepare Host', 'prepare-network': 'Prepare Network', - 'manage-fence-devices': 'Manage Fence Devices', - 'manage-upses': 'Manage UPSes', - 'manage-manifests': 'Manage Manifests', + 'manage-fence': 'Manage Fence Devices', + 'manage-ups': 'Manage UPSes', + 'manage-manifest': 'Manage Manifests', }; const PAGE_TITLE_LOADING = 'Loading'; const STEP_CONTENT_GRID_COLUMNS = { md: 8, sm: 6, xs: 1 }; @@ -116,6 +117,19 @@ const PrepareNetworkTabContent: FC = () => { ); }; +const ManageFenceTabContent: FC = () => ( + , + ...STEP_CONTENT_GRID_CENTER_COLUMN, + }, + }} + /> +); + const ManageElement: FC = () => { const { isReady, @@ -162,7 +176,7 @@ const ManageElement: FC = () => { > - + @@ -171,8 +185,8 @@ const ManageElement: FC = () => { - - {} + + ); From aa2dbff079a452b60e0e38ff7c6526d667da0aa3 Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Tue, 28 Feb 2023 16:03:32 -0500 Subject: [PATCH 47/85] fix(striker-ui): align colouring in buttons --- striker-ui/components/ContainedButton.tsx | 16 ++++------------ striker-ui/components/IconButton/IconButton.tsx | 3 +-- 2 files changed, 5 insertions(+), 14 deletions(-) diff --git a/striker-ui/components/ContainedButton.tsx b/striker-ui/components/ContainedButton.tsx index bd807dee..97138438 100644 --- a/striker-ui/components/ContainedButton.tsx +++ b/striker-ui/components/ContainedButton.tsx @@ -1,17 +1,17 @@ import { Button as MUIButton, SxProps, Theme } from '@mui/material'; import { FC, useMemo } from 'react'; -import { BLACK, GREY, TEXT } from '../lib/consts/DEFAULT_THEME'; +import { BLACK, GREY } from '../lib/consts/DEFAULT_THEME'; const ContainedButton: FC = ({ sx, ...restProps }) => { const combinedSx = useMemo>( () => ({ - backgroundColor: TEXT, + backgroundColor: GREY, color: BLACK, textTransform: 'none', '&:hover': { - backgroundColor: GREY, + backgroundColor: `${GREY}F0`, }, ...sx, @@ -19,15 +19,7 @@ const ContainedButton: FC = ({ sx, ...restProps }) => { [sx], ); - return ( - - ); + return ; }; export default ContainedButton; diff --git a/striker-ui/components/IconButton/IconButton.tsx b/striker-ui/components/IconButton/IconButton.tsx index 2782db4a..1eae04e5 100644 --- a/striker-ui/components/IconButton/IconButton.tsx +++ b/striker-ui/components/IconButton/IconButton.tsx @@ -17,7 +17,6 @@ import { BORDER_RADIUS, DISABLED, GREY, - TEXT, } from '../../lib/consts/DEFAULT_THEME'; type IconButtonProps = IconButtonOptionalProps & MUIIconButtonProps; @@ -28,7 +27,7 @@ const ContainedIconButton = styled(MUIIconButton)({ color: BLACK, '&:hover': { - backgroundColor: TEXT, + backgroundColor: `${GREY}F0`, }, [`&.${muiInputClasses.disabled}`]: { From 472f634a0054382976012fa29d831430feaf731f Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Tue, 28 Feb 2023 16:08:22 -0500 Subject: [PATCH 48/85] fix(striker-ui): add inline type SensitiveText and BodyText --- striker-ui/components/Text/BodyText.tsx | 54 ++++++------ striker-ui/components/Text/SensitiveText.tsx | 87 +++++++++++++++----- striker-ui/lib/consts/DEFAULT_THEME.ts | 1 + striker-ui/types/SensitiveText.d.ts | 1 + 4 files changed, 101 insertions(+), 42 deletions(-) diff --git a/striker-ui/components/Text/BodyText.tsx b/striker-ui/components/Text/BodyText.tsx index 8fef39e9..434727ed 100644 --- a/striker-ui/components/Text/BodyText.tsx +++ b/striker-ui/components/Text/BodyText.tsx @@ -8,6 +8,7 @@ import { BLACK, TEXT, UNSELECTED } from '../../lib/consts/DEFAULT_THEME'; type BodyTextOptionalProps = { inheritColour?: boolean; + inline?: boolean; inverted?: boolean; monospaced?: boolean; selected?: boolean; @@ -20,6 +21,7 @@ const BODY_TEXT_CLASS_PREFIX = 'BodyText'; const BODY_TEXT_DEFAULT_PROPS: Required = { inheritColour: false, + inline: false, inverted: false, monospaced: false, selected: true, @@ -68,6 +70,7 @@ const BodyText: FC = ({ children, className, inheritColour: isInheritColour = BODY_TEXT_DEFAULT_PROPS.inheritColour, + inline: isInline = BODY_TEXT_DEFAULT_PROPS.inline, inverted: isInvert = BODY_TEXT_DEFAULT_PROPS.inverted, monospaced: isMonospace = BODY_TEXT_DEFAULT_PROPS.monospaced, selected: isSelect = BODY_TEXT_DEFAULT_PROPS.selected, @@ -75,6 +78,11 @@ const BodyText: FC = ({ text = BODY_TEXT_DEFAULT_PROPS.text, ...muiTypographyRestProps }) => { + const sxDisplay = useMemo( + () => (isInline ? 'inline' : undefined), + [isInline], + ); + const baseClassName = useMemo( () => buildBodyTextClasses({ @@ -89,30 +97,30 @@ const BodyText: FC = ({ return ( {content} diff --git a/striker-ui/components/Text/SensitiveText.tsx b/striker-ui/components/Text/SensitiveText.tsx index 16aa3509..2d1ca11c 100644 --- a/striker-ui/components/Text/SensitiveText.tsx +++ b/striker-ui/components/Text/SensitiveText.tsx @@ -1,18 +1,51 @@ -import { createElement, FC, ReactNode, useMemo, useState } from 'react'; +import { Button, styled } from '@mui/material'; +import { + createElement, + FC, + ReactElement, + ReactNode, + useCallback, + useMemo, + useState, +} from 'react'; + +import { BORDER_RADIUS, EERIE_BLACK } from '../../lib/consts/DEFAULT_THEME'; import BodyText from './BodyText'; import FlexBox from '../FlexBox'; import IconButton from '../IconButton'; import MonoText from './MonoText'; +const InlineButton = styled(Button)({ + backgroundColor: EERIE_BLACK, + borderRadius: BORDER_RADIUS, + minWidth: 'initial', + padding: '0 .6em', + textTransform: 'none', + + ':hover': { + backgroundColor: `${EERIE_BLACK}F0`, + }, +}); + const SensitiveText: FC = ({ children, + inline: isInline = false, monospaced: isMonospaced = false, revealInitially: isRevealInitially = false, textProps, }) => { const [isReveal, setIsReveal] = useState(isRevealInitially); + const clickEventHandler = useCallback(() => { + setIsReveal((previous) => !previous); + }, []); + + const textSxLineHeight = useMemo( + () => (isInline ? undefined : 2.8), + [isInline], + ); + const textElementType = useMemo( () => (isMonospaced ? MonoText : BodyText), [isMonospaced], @@ -27,7 +60,7 @@ const SensitiveText: FC = ({ textElementType, { sx: { - lineHeight: 2.8, + lineHeight: textSxLineHeight, maxWidth: '20em', overflowY: 'scroll', whiteSpace: 'nowrap', @@ -38,27 +71,43 @@ const SensitiveText: FC = ({ ) : children; } else { - content = createElement(textElementType, textProps, '*****'); + content = createElement( + textElementType, + { + sx: { + lineHeight: textSxLineHeight, + }, + ...textProps, + }, + '*****', + ); } return content; - }, [children, isReveal, textElementType, textProps]); - - return ( - - {contentElement} - { - setIsReveal((previous) => !previous); - }} - state={String(isReveal)} - sx={{ marginRight: '-.2em', padding: '.2em' }} - variant="normal" - /> - + }, [children, isReveal, textElementType, textProps, textSxLineHeight]); + const rootElement = useMemo( + () => + isInline ? ( + + {contentElement} + + ) : ( + + {contentElement} + + + ), + [clickEventHandler, contentElement, isInline, isReveal], ); + + return rootElement; }; export default SensitiveText; diff --git a/striker-ui/lib/consts/DEFAULT_THEME.ts b/striker-ui/lib/consts/DEFAULT_THEME.ts index 6c0b27c4..e226100f 100644 --- a/striker-ui/lib/consts/DEFAULT_THEME.ts +++ b/striker-ui/lib/consts/DEFAULT_THEME.ts @@ -13,6 +13,7 @@ export const DIVIDER = '#888'; export const SELECTED_ANVIL = '#00ff00'; export const DISABLED = '#AAA'; export const BLACK = '#343434'; +export const EERIE_BLACK = '#1F1F1F'; // TODO: remove when old icons are completely replaced. export const OLD_ICON = '#9da2a7'; diff --git a/striker-ui/types/SensitiveText.d.ts b/striker-ui/types/SensitiveText.d.ts index 2a7921cb..65081fa5 100644 --- a/striker-ui/types/SensitiveText.d.ts +++ b/striker-ui/types/SensitiveText.d.ts @@ -1,4 +1,5 @@ type SensitiveTextOptionalProps = { + inline?: boolean; monospaced?: boolean; revealInitially?: boolean; textProps?: import('../components/Text').BodyTextProps; From 3dd6eb487e10d8deb71e22aa427801ae35ac206b Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Tue, 28 Feb 2023 16:10:53 -0500 Subject: [PATCH 49/85] fix(striker-ui): hide password related in fence list --- .../ManageFence/ManageFencePanel.tsx | 26 ++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/striker-ui/components/ManageFence/ManageFencePanel.tsx b/striker-ui/components/ManageFence/ManageFencePanel.tsx index efa60324..212d181a 100644 --- a/striker-ui/components/ManageFence/ManageFencePanel.tsx +++ b/striker-ui/components/ManageFence/ManageFencePanel.tsx @@ -3,6 +3,7 @@ import { FC, FormEventHandler, ReactElement, + ReactNode, useMemo, useRef, useState, @@ -267,9 +268,28 @@ const ManageFencePanel: FC = () => { {fenceName} - {Object.entries(fenceParameters).reduce( - (previous, [parameterId, parameterValue]) => - `${previous} ${parameterId}="${parameterValue}"`, + {Object.entries(fenceParameters).reduce( + (previous, [parameterId, parameterValue]) => { + let current: ReactNode = <>{parameterId}="; + + current = /passw/i.test(parameterId) ? ( + <> + {current} + {parameterValue} + + ) : ( + <> + {current} + {parameterValue} + + ); + + return ( + <> + {previous} {current}" + + ); + }, fenceAgent, )} From dd6315caf961e23e1283a029d90e6da2365665b9 Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Tue, 28 Feb 2023 16:40:49 -0500 Subject: [PATCH 50/85] fix(striker-ui-api): make getAnvilData generic to identify return value --- striker-ui-api/src/lib/accessModule.ts | 4 +-- .../fence/getFenceTemplate.ts | 2 +- .../host/createHostConnection.ts | 2 +- .../host/getHostConnection.ts | 8 +++--- striker-ui-api/src/types/AnvilDataStruct.d.ts | 3 --- striker-ui-api/src/types/DatabaseHash.d.ts | 10 ------- .../src/types/GetAnvilDataFunction.d.ts | 26 +++++++++++++++++++ .../src/types/GetAnvilDataOptions.d.ts | 3 --- 8 files changed, 35 insertions(+), 23 deletions(-) delete mode 100644 striker-ui-api/src/types/AnvilDataStruct.d.ts delete mode 100644 striker-ui-api/src/types/DatabaseHash.d.ts create mode 100644 striker-ui-api/src/types/GetAnvilDataFunction.d.ts delete mode 100644 striker-ui-api/src/types/GetAnvilDataOptions.d.ts diff --git a/striker-ui-api/src/lib/accessModule.ts b/striker-ui-api/src/lib/accessModule.ts index 94bed8fb..f1b02268 100644 --- a/striker-ui-api/src/lib/accessModule.ts +++ b/striker-ui-api/src/lib/accessModule.ts @@ -134,10 +134,10 @@ const dbWrite = (query: string, options?: SpawnSyncOptions) => { return execAnvilAccessModule(['--query', query, '--mode', 'write'], options); }; -const getAnvilData = ( +const getAnvilData = ( dataStruct: AnvilDataStruct, { predata, ...spawnSyncOptions }: GetAnvilDataOptions = {}, -) => +): HashType => execAnvilAccessModule( [ '--predata', diff --git a/striker-ui-api/src/lib/request_handlers/fence/getFenceTemplate.ts b/striker-ui-api/src/lib/request_handlers/fence/getFenceTemplate.ts index e285cc1a..beec0a0e 100644 --- a/striker-ui-api/src/lib/request_handlers/fence/getFenceTemplate.ts +++ b/striker-ui-api/src/lib/request_handlers/fence/getFenceTemplate.ts @@ -6,7 +6,7 @@ export const getFenceTemplate: RequestHandler = (request, response) => { let rawFenceData; try { - ({ fence_data: rawFenceData } = getAnvilData( + ({ fence_data: rawFenceData } = getAnvilData<{ fence_data: unknown }>( { fence_data: true }, { predata: [['Striker->get_fence_data']] }, )); diff --git a/striker-ui-api/src/lib/request_handlers/host/createHostConnection.ts b/striker-ui-api/src/lib/request_handlers/host/createHostConnection.ts index c688dc9c..38a66989 100644 --- a/striker-ui-api/src/lib/request_handlers/host/createHostConnection.ts +++ b/striker-ui-api/src/lib/request_handlers/host/createHostConnection.ts @@ -154,7 +154,7 @@ export const createHostConnection: RequestHandler< database: { [localHostUUID]: { port: rawLocalDBPort }, }, - } = getAnvilData({ database: true }) as { database: DatabaseHash }; + } = getAnvilData<{ database: AnvilDataDatabaseHash }>({ database: true }); localDBPort = sanitize(rawLocalDBPort, 'number'); } catch (subError) { diff --git a/striker-ui-api/src/lib/request_handlers/host/getHostConnection.ts b/striker-ui-api/src/lib/request_handlers/host/getHostConnection.ts index 8671ac32..cf5e5453 100644 --- a/striker-ui-api/src/lib/request_handlers/host/getHostConnection.ts +++ b/striker-ui-api/src/lib/request_handlers/host/getHostConnection.ts @@ -7,7 +7,7 @@ import { stdout } from '../../shell'; const buildHostConnections = ( fromHostUUID: string, - databaseHash: DatabaseHash, + databaseHash: AnvilDataDatabaseHash, { defaultPort = 5432, defaultUser = 'admin', @@ -42,7 +42,7 @@ export const getHostConnection = buildGetRequestHandler( (request, buildQueryOptions) => { const { hostUUIDs: rawHostUUIDs } = request.query; - let rawDatabaseData: DatabaseHash; + let rawDatabaseData: AnvilDataDatabaseHash; const hostUUIDField = 'ip_add.ip_address_host_uuid'; const localHostUUID: string = getLocalHostUUID(); @@ -59,7 +59,9 @@ export const getHostConnection = buildGetRequestHandler( stdout(`condHostUUIDs=[${condHostUUIDs}]`); try { - ({ database: rawDatabaseData } = getAnvilData({ database: true })); + ({ database: rawDatabaseData } = getAnvilData<{ database: AnvilDataDatabaseHash }>( + { database: true }, + )); } catch (subError) { throw new Error(`Failed to get anvil data; CAUSE: ${subError}`); } diff --git a/striker-ui-api/src/types/AnvilDataStruct.d.ts b/striker-ui-api/src/types/AnvilDataStruct.d.ts deleted file mode 100644 index 984875f9..00000000 --- a/striker-ui-api/src/types/AnvilDataStruct.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -interface AnvilDataStruct { - [key: string]: AnvilDataStruct | boolean; -} diff --git a/striker-ui-api/src/types/DatabaseHash.d.ts b/striker-ui-api/src/types/DatabaseHash.d.ts deleted file mode 100644 index 1acbab2d..00000000 --- a/striker-ui-api/src/types/DatabaseHash.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -type DatabaseHash = { - [hostUUID: string]: { - host: string; - name: string; - password: string; - ping: string; - port: string; - user: string; - }; -}; diff --git a/striker-ui-api/src/types/GetAnvilDataFunction.d.ts b/striker-ui-api/src/types/GetAnvilDataFunction.d.ts new file mode 100644 index 00000000..191337b3 --- /dev/null +++ b/striker-ui-api/src/types/GetAnvilDataFunction.d.ts @@ -0,0 +1,26 @@ +interface AnvilDataStruct { + [key: string]: AnvilDataStruct | boolean; +} + +type AnvilDataDatabaseHash = { + [hostUUID: string]: { + host: string; + name: string; + password: string; + ping: string; + port: string; + user: string; + }; +}; + +type AnvilDataUPSHash = { + [upsName: string]: { + agent: string; + brand: string; + description: string; + }; +}; + +type GetAnvilDataOptions = import('child_process').SpawnSyncOptions & { + predata?: Array<[string, ...unknown[]]>; +}; diff --git a/striker-ui-api/src/types/GetAnvilDataOptions.d.ts b/striker-ui-api/src/types/GetAnvilDataOptions.d.ts deleted file mode 100644 index baf39d7a..00000000 --- a/striker-ui-api/src/types/GetAnvilDataOptions.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -type GetAnvilDataOptions = import('child_process').SpawnSyncOptions & { - predata?: Array<[string, ...unknown[]]>; -}; From 2079c6e722a2263a30bbd62709d606b2af324412 Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Tue, 28 Feb 2023 16:42:09 -0500 Subject: [PATCH 51/85] feat(striker-ui-api): add /ups --- .../request_handlers/ups/getUPSTemplate.ts | 23 +++++++++++++++++++ .../src/lib/request_handlers/ups/index.ts | 1 + striker-ui-api/src/routes/index.ts | 2 ++ striker-ui-api/src/routes/ups.ts | 9 ++++++++ 4 files changed, 35 insertions(+) create mode 100644 striker-ui-api/src/lib/request_handlers/ups/getUPSTemplate.ts create mode 100644 striker-ui-api/src/lib/request_handlers/ups/index.ts create mode 100644 striker-ui-api/src/routes/ups.ts diff --git a/striker-ui-api/src/lib/request_handlers/ups/getUPSTemplate.ts b/striker-ui-api/src/lib/request_handlers/ups/getUPSTemplate.ts new file mode 100644 index 00000000..b966f806 --- /dev/null +++ b/striker-ui-api/src/lib/request_handlers/ups/getUPSTemplate.ts @@ -0,0 +1,23 @@ +import { RequestHandler } from 'express'; + +import { getAnvilData } from '../../accessModule'; +import { stderr } from '../../shell'; + +export const getUPSTemplate: RequestHandler = (request, response) => { + let rawUPSData; + + try { + ({ ups_data: rawUPSData } = getAnvilData<{ ups_data: AnvilDataUPSHash }>( + { ups_data: true }, + { predata: [['Striker->get_ups_data']] }, + )); + } catch (subError) { + stderr(`Failed to get ups template; CAUSE: ${subError}`); + + response.status(500).send(); + + return; + } + + response.status(200).send(rawUPSData); +}; diff --git a/striker-ui-api/src/lib/request_handlers/ups/index.ts b/striker-ui-api/src/lib/request_handlers/ups/index.ts new file mode 100644 index 00000000..969616cb --- /dev/null +++ b/striker-ui-api/src/lib/request_handlers/ups/index.ts @@ -0,0 +1 @@ +export * from './getUPSTemplate'; diff --git a/striker-ui-api/src/routes/index.ts b/striker-ui-api/src/routes/index.ts index e37e78f8..618e952c 100644 --- a/striker-ui-api/src/routes/index.ts +++ b/striker-ui-api/src/routes/index.ts @@ -10,6 +10,7 @@ import jobRouter from './job'; import networkInterfaceRouter from './network-interface'; import serverRouter from './server'; import sshKeyRouter from './ssh-key'; +import upsRouter from './ups'; import userRouter from './user'; const routes: Readonly> = { @@ -23,6 +24,7 @@ const routes: Readonly> = { 'network-interface': networkInterfaceRouter, server: serverRouter, 'ssh-key': sshKeyRouter, + ups: upsRouter, user: userRouter, }; diff --git a/striker-ui-api/src/routes/ups.ts b/striker-ui-api/src/routes/ups.ts new file mode 100644 index 00000000..1d19b86f --- /dev/null +++ b/striker-ui-api/src/routes/ups.ts @@ -0,0 +1,9 @@ +import express from 'express'; + +import { getUPSTemplate } from '../lib/request_handlers/ups'; + +const router = express.Router(); + +router.get('/template', getUPSTemplate); + +export default router; From 15674d41eebed7f083d4e9ecfb217154a57a0e40 Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Tue, 28 Feb 2023 22:14:43 -0500 Subject: [PATCH 52/85] fix(striker-ui): add FormDialog --- striker-ui/components/FormDialog.tsx | 34 ++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 striker-ui/components/FormDialog.tsx diff --git a/striker-ui/components/FormDialog.tsx b/striker-ui/components/FormDialog.tsx new file mode 100644 index 00000000..09630391 --- /dev/null +++ b/striker-ui/components/FormDialog.tsx @@ -0,0 +1,34 @@ +import { forwardRef, useMemo } from 'react'; + +import ConfirmDialog from './ConfirmDialog'; + +const FormDialog = forwardRef< + ConfirmDialogForwardedRefContent, + ConfirmDialogProps +>((props, ref) => { + const { scrollContent: isScrollContent } = props; + + const scrollBoxPaddingRight = useMemo( + () => (isScrollContent ? '.5em' : undefined), + [isScrollContent], + ); + + return ( + + ); +}); + +FormDialog.displayName = 'FormDialog'; + +export default FormDialog; From c9c3092d22cde4caf2fc2cc6193fda70f91ff50e Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Tue, 28 Feb 2023 22:15:50 -0500 Subject: [PATCH 53/85] fix(striker-ui): simplify ConfirmDialogProps init --- striker-ui/hooks/useConfirmDialogProps.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 striker-ui/hooks/useConfirmDialogProps.ts diff --git a/striker-ui/hooks/useConfirmDialogProps.ts b/striker-ui/hooks/useConfirmDialogProps.ts new file mode 100644 index 00000000..c638a41d --- /dev/null +++ b/striker-ui/hooks/useConfirmDialogProps.ts @@ -0,0 +1,19 @@ +import { Dispatch, SetStateAction, useState } from 'react'; + +const useConfirmDialogProps = ({ + actionProceedText = '', + content = '', + titleText = '', + ...restProps +}: Partial = {}): [ + ConfirmDialogProps, + Dispatch>, +] => + useState({ + actionProceedText, + content, + titleText, + ...restProps, + }); + +export default useConfirmDialogProps; From 882ec15738f7756d89a02b06e12c6422c1358db1 Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Tue, 28 Feb 2023 22:21:15 -0500 Subject: [PATCH 54/85] feat(striker-ui): add manage UPS related components --- .../components/ManageUps/AddUpsInputGroup.tsx | 78 ++++++++++++++++ .../ManageUps/CommonUpsInputGroup.tsx | 58 ++++++++++++ .../components/ManageUps/ManageUpsPanel.tsx | 88 +++++++++++++++++++ striker-ui/components/ManageUps/index.tsx | 3 + striker-ui/types/APIUps.d.ts | 7 ++ striker-ui/types/AddUpsInputGroup.d.ts | 6 ++ striker-ui/types/CommonUpsInputGroup.d.ts | 9 ++ 7 files changed, 249 insertions(+) create mode 100644 striker-ui/components/ManageUps/AddUpsInputGroup.tsx create mode 100644 striker-ui/components/ManageUps/CommonUpsInputGroup.tsx create mode 100644 striker-ui/components/ManageUps/ManageUpsPanel.tsx create mode 100644 striker-ui/components/ManageUps/index.tsx create mode 100644 striker-ui/types/APIUps.d.ts create mode 100644 striker-ui/types/AddUpsInputGroup.d.ts create mode 100644 striker-ui/types/CommonUpsInputGroup.d.ts diff --git a/striker-ui/components/ManageUps/AddUpsInputGroup.tsx b/striker-ui/components/ManageUps/AddUpsInputGroup.tsx new file mode 100644 index 00000000..43e4c749 --- /dev/null +++ b/striker-ui/components/ManageUps/AddUpsInputGroup.tsx @@ -0,0 +1,78 @@ +import { FC, ReactElement, useMemo, useState } from 'react'; + +import CommonUpsInputGroup from './CommonUpsInputGroup'; +import FlexBox from '../FlexBox'; +import SelectWithLabel from '../SelectWithLabel'; +import Spinner from '../Spinner'; +import { BodyText } from '../Text'; + +const AddUpsInputGroup: FC = ({ + loading: isExternalLoading, + upsTemplate, +}) => { + const [inputUpsAgentValue, setInputUpsAgentValue] = useState(''); + + const upsAgentOptions = useMemo( + () => + upsTemplate + ? Object.entries(upsTemplate).map( + ([upsTypeId, { brand, description }]) => ({ + displayValue: ( + + {brand} + {description} + + ), + value: upsTypeId, + }), + ) + : [], + [upsTemplate], + ); + + const pickUpsAgentElement = useMemo( + () => + upsTemplate && ( + { + const newValue = String(rawNewValue); + + setInputUpsAgentValue(newValue); + }} + selectItems={upsAgentOptions} + selectProps={{ + onClearIndicatorClick: () => { + setInputUpsAgentValue(''); + }, + renderValue: (rawValue) => { + const upsTypeId = String(rawValue); + const { brand } = upsTemplate[upsTypeId]; + + return brand; + }, + }} + value={inputUpsAgentValue} + /> + ), + [inputUpsAgentValue, upsAgentOptions, upsTemplate], + ); + const content = useMemo( + () => + isExternalLoading ? ( + + ) : ( + + {pickUpsAgentElement} + {inputUpsAgentValue && } + + ), + [inputUpsAgentValue, isExternalLoading, pickUpsAgentElement], + ); + + return content; +}; + +export default AddUpsInputGroup; diff --git a/striker-ui/components/ManageUps/CommonUpsInputGroup.tsx b/striker-ui/components/ManageUps/CommonUpsInputGroup.tsx new file mode 100644 index 00000000..15477906 --- /dev/null +++ b/striker-ui/components/ManageUps/CommonUpsInputGroup.tsx @@ -0,0 +1,58 @@ +import { FC } from 'react'; + +import Grid from '../Grid'; +import InputWithRef from '../InputWithRef'; +import OutlinedInputWithLabel from '../OutlinedInputWithLabel'; + +const CommonUpsInputGroup: FC = ({ + previous: { + hostName: previousHostName, + ipAddress: previousIpAddress, + upsName: previousUpsName, + } = {}, +}) => ( + <> + + } + required + /> + ), + }, + 'common-ups-input-cell-ip-address': { + children: ( + + } + required + /> + ), + }, + }} + spacing="1em" + /> + + +); + +export default CommonUpsInputGroup; diff --git a/striker-ui/components/ManageUps/ManageUpsPanel.tsx b/striker-ui/components/ManageUps/ManageUpsPanel.tsx new file mode 100644 index 00000000..99b2799a --- /dev/null +++ b/striker-ui/components/ManageUps/ManageUpsPanel.tsx @@ -0,0 +1,88 @@ +import { FC, useMemo, useRef, useState } from 'react'; + +import AddUpsInputGroup from './AddUpsInputGroup'; +import api from '../../lib/api'; +import ConfirmDialog from '../ConfirmDialog'; +import FormDialog from '../FormDialog'; +import handleAPIError from '../../lib/handleAPIError'; +import List from '../List'; +import { Panel, PanelHeader } from '../Panels'; +import Spinner from '../Spinner'; +import { HeaderText } from '../Text'; +import useConfirmDialogProps from '../../hooks/useConfirmDialogProps'; +import useIsFirstRender from '../../hooks/useIsFirstRender'; +import useProtectedState from '../../hooks/useProtectedState'; + +const ManageUpsPanel: FC = () => { + const isFirstRender = useIsFirstRender(); + + const confirmDialogRef = useRef({}); + const formDialogRef = useRef({}); + + const [confirmDialogProps] = useConfirmDialogProps(); + const [formDialogProps, setFormDialogProps] = useConfirmDialogProps(); + const [isEditUpses, setIsEditUpses] = useState(false); + const [isLoadingUpsTemplate, setIsLoadingUpsTemplate] = + useProtectedState(true); + const [upsTemplate, setUpsTemplate] = useProtectedState< + APIUpsTemplate | undefined + >(undefined); + + const listElement = useMemo( + () => ( + { + setFormDialogProps({ + actionProceedText: 'Add', + content: , + titleText: 'Add a UPS', + }); + + formDialogRef.current.setOpen?.call(null, true); + }} + onEdit={() => { + setIsEditUpses((previous) => !previous); + }} + /> + ), + [isEditUpses, setFormDialogProps, upsTemplate], + ); + const panelContent = useMemo( + () => (isLoadingUpsTemplate ? : listElement), + [isLoadingUpsTemplate, listElement], + ); + + if (isFirstRender) { + api + .get('/ups/template') + .then(({ data }) => { + setUpsTemplate(data); + }) + .catch((error) => { + handleAPIError(error); + }) + .finally(() => { + setIsLoadingUpsTemplate(false); + }); + } + + return ( + <> + + + Manage Upses + + {panelContent} + + + + + ); +}; + +export default ManageUpsPanel; diff --git a/striker-ui/components/ManageUps/index.tsx b/striker-ui/components/ManageUps/index.tsx new file mode 100644 index 00000000..b99eceb8 --- /dev/null +++ b/striker-ui/components/ManageUps/index.tsx @@ -0,0 +1,3 @@ +import ManageUpsPanel from './ManageUpsPanel'; + +export default ManageUpsPanel; diff --git a/striker-ui/types/APIUps.d.ts b/striker-ui/types/APIUps.d.ts new file mode 100644 index 00000000..c73d68d4 --- /dev/null +++ b/striker-ui/types/APIUps.d.ts @@ -0,0 +1,7 @@ +type APIUpsTemplate = { + [upsTypeId: string]: { + agent: string; + brand: string; + description: string; + }; +}; diff --git a/striker-ui/types/AddUpsInputGroup.d.ts b/striker-ui/types/AddUpsInputGroup.d.ts new file mode 100644 index 00000000..03a891ec --- /dev/null +++ b/striker-ui/types/AddUpsInputGroup.d.ts @@ -0,0 +1,6 @@ +type AddUpsInputGroupOptionalProps = { + loading?: boolean; + upsTemplate?: APIUpsTemplate; +}; + +type AddUpsInputGroupProps = AddUpsInputGroupOptionalProps; diff --git a/striker-ui/types/CommonUpsInputGroup.d.ts b/striker-ui/types/CommonUpsInputGroup.d.ts new file mode 100644 index 00000000..f8af2908 --- /dev/null +++ b/striker-ui/types/CommonUpsInputGroup.d.ts @@ -0,0 +1,9 @@ +type CommonUpsInputGroupOptionalProps = { + previous?: { + hostName?: string; + ipAddress?: string; + upsName?: string; + }; +}; + +type CommonUpsInputGroupProps = CommonUpsInputGroupOptionalProps; From d408b22f24f75d0ea307bb7de6285a197bf726fd Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Tue, 28 Feb 2023 22:32:00 -0500 Subject: [PATCH 55/85] fix(striker-ui-api): limit UPS types to APC --- .../lib/request_handlers/ups/getUPSTemplate.ts | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/striker-ui-api/src/lib/request_handlers/ups/getUPSTemplate.ts b/striker-ui-api/src/lib/request_handlers/ups/getUPSTemplate.ts index b966f806..ea014049 100644 --- a/striker-ui-api/src/lib/request_handlers/ups/getUPSTemplate.ts +++ b/striker-ui-api/src/lib/request_handlers/ups/getUPSTemplate.ts @@ -4,7 +4,7 @@ import { getAnvilData } from '../../accessModule'; import { stderr } from '../../shell'; export const getUPSTemplate: RequestHandler = (request, response) => { - let rawUPSData; + let rawUPSData: AnvilDataUPSHash; try { ({ ups_data: rawUPSData } = getAnvilData<{ ups_data: AnvilDataUPSHash }>( @@ -19,5 +19,17 @@ export const getUPSTemplate: RequestHandler = (request, response) => { return; } - response.status(200).send(rawUPSData); + const upsData: AnvilDataUPSHash = Object.entries( + rawUPSData, + ).reduce((previous, [upsTypeId, value]) => { + const { brand } = value; + + if (/apc/i.test(brand)) { + previous[upsTypeId] = value; + } + + return previous; + }, {}); + + response.status(200).send(upsData); }; From 84fb75f5e1420baae8ab10c0d5720d62ff25464d Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Tue, 28 Feb 2023 23:22:07 -0500 Subject: [PATCH 56/85] fix(striker-ui-api): add GET UPS overviews --- .../src/lib/request_handlers/ups/getUPS.ts | 37 +++++++++++++++++++ .../src/lib/request_handlers/ups/index.ts | 1 + striker-ui-api/src/routes/ups.ts | 4 +- striker-ui-api/src/types/APIUPS.d.ts | 6 +++ 4 files changed, 46 insertions(+), 2 deletions(-) create mode 100644 striker-ui-api/src/lib/request_handlers/ups/getUPS.ts create mode 100644 striker-ui-api/src/types/APIUPS.d.ts diff --git a/striker-ui-api/src/lib/request_handlers/ups/getUPS.ts b/striker-ui-api/src/lib/request_handlers/ups/getUPS.ts new file mode 100644 index 00000000..e884e54f --- /dev/null +++ b/striker-ui-api/src/lib/request_handlers/ups/getUPS.ts @@ -0,0 +1,37 @@ +import { RequestHandler } from 'express'; + +import buildGetRequestHandler from '../buildGetRequestHandler'; +import { buildQueryResultReducer } from '../../buildQueryResultModifier'; + +export const getUPS: RequestHandler = buildGetRequestHandler( + (request, buildQueryOptions) => { + const query = ` + SELECT + ups_uuid, + ups_name, + ups_agent, + ups_ip_address + FROM upses + ORDER BY ups_name ASC;`; + const afterQueryReturn: QueryResultModifierFunction | undefined = + buildQueryResultReducer<{ [upsUUID: string]: UPSOverview }>( + (previous, [upsUUID, upsName, upsAgent, upsIPAddress]) => { + previous[upsUUID] = { + upsAgent, + upsIPAddress, + upsName, + upsUUID, + }; + + return previous; + }, + {}, + ); + + if (buildQueryOptions) { + buildQueryOptions.afterQueryReturn = afterQueryReturn; + } + + return query; + }, +); diff --git a/striker-ui-api/src/lib/request_handlers/ups/index.ts b/striker-ui-api/src/lib/request_handlers/ups/index.ts index 969616cb..191a8f79 100644 --- a/striker-ui-api/src/lib/request_handlers/ups/index.ts +++ b/striker-ui-api/src/lib/request_handlers/ups/index.ts @@ -1 +1,2 @@ +export * from './getUPS'; export * from './getUPSTemplate'; diff --git a/striker-ui-api/src/routes/ups.ts b/striker-ui-api/src/routes/ups.ts index 1d19b86f..c6e6c637 100644 --- a/striker-ui-api/src/routes/ups.ts +++ b/striker-ui-api/src/routes/ups.ts @@ -1,9 +1,9 @@ import express from 'express'; -import { getUPSTemplate } from '../lib/request_handlers/ups'; +import { getUPS, getUPSTemplate } from '../lib/request_handlers/ups'; const router = express.Router(); -router.get('/template', getUPSTemplate); +router.get('/', getUPS).get('/template', getUPSTemplate); export default router; diff --git a/striker-ui-api/src/types/APIUPS.d.ts b/striker-ui-api/src/types/APIUPS.d.ts new file mode 100644 index 00000000..4eb25534 --- /dev/null +++ b/striker-ui-api/src/types/APIUPS.d.ts @@ -0,0 +1,6 @@ +type UPSOverview = { + upsAgent: string; + upsIPAddress: string; + upsName: string; + upsUUID: string; +}; From 80fad36ad74cf05cf491350af76315637a125f6e Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Wed, 1 Mar 2023 15:23:27 -0500 Subject: [PATCH 57/85] fix(striker-ui): center List empty text --- striker-ui/components/List.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/striker-ui/components/List.tsx b/striker-ui/components/List.tsx index e73543e1..084c1039 100644 --- a/striker-ui/components/List.tsx +++ b/striker-ui/components/List.tsx @@ -176,7 +176,7 @@ const List = forwardRef( const listEmptyElement = useMemo( () => typeof listEmpty === 'string' ? ( - {listEmpty} + {listEmpty} ) : ( listEmpty ), From 8fa4d3441e5370b24704a3af5ae489b13e8139ae Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Wed, 1 Mar 2023 15:56:10 -0500 Subject: [PATCH 58/85] fix(striker-ui): remove duplicate parameter in CommonUpsInputGroup --- .../components/ManageUps/CommonUpsInputGroup.tsx | 14 ++------------ striker-ui/types/CommonUpsInputGroup.d.ts | 3 +-- 2 files changed, 3 insertions(+), 14 deletions(-) diff --git a/striker-ui/components/ManageUps/CommonUpsInputGroup.tsx b/striker-ui/components/ManageUps/CommonUpsInputGroup.tsx index 15477906..3550ee24 100644 --- a/striker-ui/components/ManageUps/CommonUpsInputGroup.tsx +++ b/striker-ui/components/ManageUps/CommonUpsInputGroup.tsx @@ -5,11 +5,7 @@ import InputWithRef from '../InputWithRef'; import OutlinedInputWithLabel from '../OutlinedInputWithLabel'; const CommonUpsInputGroup: FC = ({ - previous: { - hostName: previousHostName, - ipAddress: previousIpAddress, - upsName: previousUpsName, - } = {}, + previous: { upsIPAddress: previousIpAddress, upsName: previousUpsName } = {}, }) => ( <> = ({ } required @@ -46,12 +42,6 @@ const CommonUpsInputGroup: FC = ({ }} spacing="1em" /> - ); diff --git a/striker-ui/types/CommonUpsInputGroup.d.ts b/striker-ui/types/CommonUpsInputGroup.d.ts index f8af2908..da058a40 100644 --- a/striker-ui/types/CommonUpsInputGroup.d.ts +++ b/striker-ui/types/CommonUpsInputGroup.d.ts @@ -1,7 +1,6 @@ type CommonUpsInputGroupOptionalProps = { previous?: { - hostName?: string; - ipAddress?: string; + upsIPAddress?: string; upsName?: string; }; }; From 4aa42e680dffa7245874de5181371c3a6aa9acad Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Wed, 1 Mar 2023 16:07:36 -0500 Subject: [PATCH 59/85] feat(striker-ui): add EditUpsInputGroup --- .../ManageUps/EditUpsInputGroup.tsx | 27 +++++++++++++++++++ striker-ui/types/EditUpsInputGroup.d.ts | 6 +++++ 2 files changed, 33 insertions(+) create mode 100644 striker-ui/components/ManageUps/EditUpsInputGroup.tsx create mode 100644 striker-ui/types/EditUpsInputGroup.d.ts diff --git a/striker-ui/components/ManageUps/EditUpsInputGroup.tsx b/striker-ui/components/ManageUps/EditUpsInputGroup.tsx new file mode 100644 index 00000000..a9ab02f7 --- /dev/null +++ b/striker-ui/components/ManageUps/EditUpsInputGroup.tsx @@ -0,0 +1,27 @@ +import { FC, ReactElement, useMemo } from 'react'; + +import CommonUpsInputGroup from './CommonUpsInputGroup'; +import Spinner from '../Spinner'; + +const EditUpsInputGroup: FC = ({ + loading: isExternalLoading, + previous, + upsUUID, +}) => { + const content = useMemo( + () => + isExternalLoading ? ( + + ) : ( + <> + + + + ), + [isExternalLoading, previous, upsUUID], + ); + + return content; +}; + +export default EditUpsInputGroup; diff --git a/striker-ui/types/EditUpsInputGroup.d.ts b/striker-ui/types/EditUpsInputGroup.d.ts new file mode 100644 index 00000000..610b4ed8 --- /dev/null +++ b/striker-ui/types/EditUpsInputGroup.d.ts @@ -0,0 +1,6 @@ +type EditUpsInputGroupOptionalProps = { + loading?: boolean; +}; + +type EditUpsInputGroupProps = EditUpsInputGroupOptionalProps & + Pick & { upsUUID: string }; From c85cea0094709c0a169af783dd7994a624962166 Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Wed, 1 Mar 2023 18:23:12 -0500 Subject: [PATCH 60/85] fix(striker-ui): pass id to input element in SelectWithLabel --- striker-ui/components/SelectWithLabel.tsx | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/striker-ui/components/SelectWithLabel.tsx b/striker-ui/components/SelectWithLabel.tsx index eabda51a..00ed6e02 100644 --- a/striker-ui/components/SelectWithLabel.tsx +++ b/striker-ui/components/SelectWithLabel.tsx @@ -71,15 +71,20 @@ const SelectWithLabel: FC = ({ [createCheckbox, disableItem, hideItem, id], ); - const inputElement = useMemo(() => , [label]); + const selectId = useMemo(() => `${id}-select-element`, [id]); + + const inputElement = useMemo( + () => , + [id, label], + ); const labelElement = useMemo( () => label && ( - + {label} ), - [id, inputLabelProps, label], + [inputLabelProps, label, selectId], ); const menuItemElements = useMemo( () => @@ -96,7 +101,7 @@ const SelectWithLabel: FC = ({ {labelElement} + + ), - [isExternalLoading, previous, upsUUID], + [isExternalLoading, previous, upsTemplate, upsUUID], ); return content; }; +export { INPUT_ID_UPS_UUID }; + export default EditUpsInputGroup; diff --git a/striker-ui/types/EditUpsInputGroup.d.ts b/striker-ui/types/EditUpsInputGroup.d.ts index 610b4ed8..ebff2166 100644 --- a/striker-ui/types/EditUpsInputGroup.d.ts +++ b/striker-ui/types/EditUpsInputGroup.d.ts @@ -3,4 +3,6 @@ type EditUpsInputGroupOptionalProps = { }; type EditUpsInputGroupProps = EditUpsInputGroupOptionalProps & - Pick & { upsUUID: string }; + Pick & { + upsUUID: string; + }; From b94e459e0aaac51f9515e5948ff4d7de2dad6b53 Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Wed, 1 Mar 2023 22:43:07 -0500 Subject: [PATCH 64/85] fix(striker-ui): connect add and edit forms with ManageUpsPanel --- .../components/ManageUps/ManageUpsPanel.tsx | 218 ++++++++++++++++-- striker-ui/types/APIUps.d.ts | 9 + 2 files changed, 213 insertions(+), 14 deletions(-) diff --git a/striker-ui/components/ManageUps/ManageUpsPanel.tsx b/striker-ui/components/ManageUps/ManageUpsPanel.tsx index 99b2799a..7c4d3f04 100644 --- a/striker-ui/components/ManageUps/ManageUpsPanel.tsx +++ b/striker-ui/components/ManageUps/ManageUpsPanel.tsx @@ -1,25 +1,108 @@ -import { FC, useMemo, useRef, useState } from 'react'; +import { + FC, + FormEventHandler, + useCallback, + useMemo, + useRef, + useState, +} from 'react'; -import AddUpsInputGroup from './AddUpsInputGroup'; +import API_BASE_URL from '../../lib/consts/API_BASE_URL'; + +import AddUpsInputGroup, { INPUT_ID_UPS_TYPE_ID } from './AddUpsInputGroup'; import api from '../../lib/api'; +import { INPUT_ID_UPS_IP, INPUT_ID_UPS_NAME } from './CommonUpsInputGroup'; import ConfirmDialog from '../ConfirmDialog'; +import EditUpsInputGroup, { INPUT_ID_UPS_UUID } from './EditUpsInputGroup'; +import FlexBox from '../FlexBox'; import FormDialog from '../FormDialog'; import handleAPIError from '../../lib/handleAPIError'; import List from '../List'; import { Panel, PanelHeader } from '../Panels'; +import periodicFetch from '../../lib/fetchers/periodicFetch'; import Spinner from '../Spinner'; -import { HeaderText } from '../Text'; +import { BodyText, HeaderText, InlineMonoText, MonoText } from '../Text'; import useConfirmDialogProps from '../../hooks/useConfirmDialogProps'; import useIsFirstRender from '../../hooks/useIsFirstRender'; import useProtectedState from '../../hooks/useProtectedState'; +type UpsFormData = { + upsAgent: string; + upsBrand: string; + upsIPAddress: string; + upsName: string; + upsTypeId: string; + upsUUID: string; +}; + +const getUpsFormData = ( + upsTemplate: APIUpsTemplate, + ...[{ target }]: Parameters> +): UpsFormData => { + const { elements } = target as HTMLFormElement; + + const { value: upsName } = elements.namedItem( + INPUT_ID_UPS_NAME, + ) as HTMLInputElement; + const { value: upsIPAddress } = elements.namedItem( + INPUT_ID_UPS_IP, + ) as HTMLInputElement; + + const inputUpsTypeId = elements.namedItem(INPUT_ID_UPS_TYPE_ID); + + let upsAgent = ''; + let upsBrand = ''; + let upsTypeId = ''; + + if (inputUpsTypeId) { + ({ value: upsTypeId } = inputUpsTypeId as HTMLInputElement); + ({ agent: upsAgent, brand: upsBrand } = upsTemplate[upsTypeId]); + } + + const inputUpsUUID = elements.namedItem(INPUT_ID_UPS_UUID); + + let upsUUID = ''; + + if (inputUpsUUID) { + ({ value: upsUUID } = inputUpsUUID as HTMLInputElement); + } + + return { upsAgent, upsBrand, upsIPAddress, upsName, upsTypeId, upsUUID }; +}; + +const buildConfirmUpsFormData = ({ + upsBrand, + upsIPAddress, + upsName, + upsUUID, +}: UpsFormData) => { + const listItems: Record = { + 'ups-brand': { label: 'Brand', value: upsBrand }, + 'ups-name': { label: 'Host name', value: upsName }, + 'ups-ip-address': { label: 'IP address', value: upsIPAddress }, + }; + + return ( + ( + + {label} + {value} + + )} + /> + ); +}; + const ManageUpsPanel: FC = () => { const isFirstRender = useIsFirstRender(); const confirmDialogRef = useRef({}); const formDialogRef = useRef({}); - const [confirmDialogProps] = useConfirmDialogProps(); + const [confirmDialogProps, setConfirmDialogProps] = useConfirmDialogProps(); const [formDialogProps, setFormDialogProps] = useConfirmDialogProps(); const [isEditUpses, setIsEditUpses] = useState(false); const [isLoadingUpsTemplate, setIsLoadingUpsTemplate] = @@ -28,6 +111,99 @@ const ManageUpsPanel: FC = () => { APIUpsTemplate | undefined >(undefined); + const { data: upsOverviews, isLoading: isUpsOverviewLoading } = + periodicFetch(`${API_BASE_URL}/ups`, { + refreshInterval: 60000, + }); + + const buildEditUpsFormDialogProps = useCallback< + (args: APIUpsOverview[string]) => ConfirmDialogProps + >( + ({ upsAgent, upsIPAddress, upsName, upsUUID }) => { + // Determine the type of existing UPS based on its scan agent. + // TODO: should identity an existing UPS's type in the DB. + const upsTypeId: string = + Object.entries(upsTemplate ?? {}).find( + ([, { agent }]) => upsAgent === agent, + )?.[0] ?? ''; + + return { + actionProceedText: 'Update', + content: ( + + ), + onSubmitAppend: (event) => { + if (!upsTemplate) { + return; + } + + const editData = getUpsFormData(upsTemplate, event); + const { upsName: newUpsName } = editData; + + setConfirmDialogProps({ + actionProceedText: 'Update', + content: buildConfirmUpsFormData(editData), + titleText: ( + + Update{' '} + {newUpsName}{' '} + with the following data? + + ), + }); + + confirmDialogRef.current.setOpen?.call(null, true); + }, + titleText: ( + + Update UPS{' '} + {upsName} + + ), + }; + }, + [setConfirmDialogProps, upsTemplate], + ); + + const addUpsFormDialogProps = useMemo( + () => ({ + actionProceedText: 'Add', + content: , + onSubmitAppend: (event) => { + if (!upsTemplate) { + return; + } + + const addData = getUpsFormData(upsTemplate, event); + const { upsBrand } = addData; + + setConfirmDialogProps({ + actionProceedText: 'Add', + content: buildConfirmUpsFormData(addData), + titleText: ( + + Add a{' '} + {upsBrand} UPS + with the following data? + + ), + }); + + confirmDialogRef.current.setOpen?.call(null, true); + }, + titleText: 'Add a UPS', + }), + [setConfirmDialogProps, upsTemplate], + ); + const listElement = useMemo( () => ( { edit={isEditUpses} header listEmpty="No Ups(es) registered." + listItems={upsOverviews} onAdd={() => { - setFormDialogProps({ - actionProceedText: 'Add', - content: , - titleText: 'Add a UPS', - }); - + setFormDialogProps(addUpsFormDialogProps); formDialogRef.current.setOpen?.call(null, true); }} onEdit={() => { setIsEditUpses((previous) => !previous); }} + onItemClick={(value) => { + setFormDialogProps(buildEditUpsFormDialogProps(value)); + formDialogRef.current.setOpen?.call(null, true); + }} + renderListItem={(upsUUID, { upsAgent, upsIPAddress, upsName }) => ( + + {upsName} + agent="{upsAgent}" + ip="{upsIPAddress}" + + )} /> ), - [isEditUpses, setFormDialogProps, upsTemplate], + [ + addUpsFormDialogProps, + buildEditUpsFormDialogProps, + isEditUpses, + setFormDialogProps, + upsOverviews, + ], ); const panelContent = useMemo( - () => (isLoadingUpsTemplate ? : listElement), - [isLoadingUpsTemplate, listElement], + () => + isLoadingUpsTemplate || isUpsOverviewLoading ? : listElement, + [isLoadingUpsTemplate, isUpsOverviewLoading, listElement], ); if (isFirstRender) { @@ -75,7 +265,7 @@ const ManageUpsPanel: FC = () => { <> - Manage Upses + Manage UPSes {panelContent} diff --git a/striker-ui/types/APIUps.d.ts b/striker-ui/types/APIUps.d.ts index c73d68d4..d0194de5 100644 --- a/striker-ui/types/APIUps.d.ts +++ b/striker-ui/types/APIUps.d.ts @@ -5,3 +5,12 @@ type APIUpsTemplate = { description: string; }; }; + +type APIUpsOverview = { + [upsUUID: string]: { + upsAgent: string; + upsIPAddress: string; + upsName: string; + upsUUID: string; + }; +}; From d8efd72c21559c497e8b16ee93d1aa32b5879afa Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Wed, 1 Mar 2023 23:34:36 -0500 Subject: [PATCH 65/85] fix(striker-ui-api): extract link from UPS type description --- .../request_handlers/ups/getUPSTemplate.ts | 23 ++++++++++++++++--- striker-ui-api/src/types/APIUPS.d.ts | 11 +++++++++ 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/striker-ui-api/src/lib/request_handlers/ups/getUPSTemplate.ts b/striker-ui-api/src/lib/request_handlers/ups/getUPSTemplate.ts index ea014049..3c4a8b9c 100644 --- a/striker-ui-api/src/lib/request_handlers/ups/getUPSTemplate.ts +++ b/striker-ui-api/src/lib/request_handlers/ups/getUPSTemplate.ts @@ -21,11 +21,28 @@ export const getUPSTemplate: RequestHandler = (request, response) => { const upsData: AnvilDataUPSHash = Object.entries( rawUPSData, - ).reduce((previous, [upsTypeId, value]) => { - const { brand } = value; + ).reduce((previous, [upsTypeId, value]) => { + const { brand, description: rawDescription, ...rest } = value; + + const matched = rawDescription.match( + /^(.+)\s+[-]\s+[<][^>]+href=[\\"]+([^\s]+)[\\"]+.+[>]([^<]+)[<]/, + ); + const result: UPSTemplate[string] = { + ...rest, + brand, + description: rawDescription, + links: {}, + }; + + if (matched) { + const [, description, linkHref, linkLabel] = matched; + + result.description = description; + result.links[0] = { linkHref, linkLabel }; + } if (/apc/i.test(brand)) { - previous[upsTypeId] = value; + previous[upsTypeId] = result; } return previous; diff --git a/striker-ui-api/src/types/APIUPS.d.ts b/striker-ui-api/src/types/APIUPS.d.ts index 4eb25534..5199e348 100644 --- a/striker-ui-api/src/types/APIUPS.d.ts +++ b/striker-ui-api/src/types/APIUPS.d.ts @@ -4,3 +4,14 @@ type UPSOverview = { upsName: string; upsUUID: string; }; + +type UPSTemplate = { + [upsName: string]: AnvilDataUPSHash[string] & { + links: { + [linkId: string]: { + linkHref: string; + linkLabel: string; + }; + }; + }; +}; From 051dde3e80fd33fb73498c2decf0afe9b0354175 Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Thu, 2 Mar 2023 15:56:11 -0500 Subject: [PATCH 66/85] fix(striker-ui): add Link to UPS type options --- .../components/ManageUps/AddUpsInputGroup.tsx | 54 +++++++++++++++---- striker-ui/types/APIUps.d.ts | 6 +++ 2 files changed, 50 insertions(+), 10 deletions(-) diff --git a/striker-ui/components/ManageUps/AddUpsInputGroup.tsx b/striker-ui/components/ManageUps/AddUpsInputGroup.tsx index 476d3310..2aa5bf51 100644 --- a/striker-ui/components/ManageUps/AddUpsInputGroup.tsx +++ b/striker-ui/components/ManageUps/AddUpsInputGroup.tsx @@ -1,7 +1,10 @@ -import { FC, ReactElement, useMemo, useState } from 'react'; +import { FC, ReactElement, ReactNode, useMemo, useState } from 'react'; + +import { BLACK } from '../../lib/consts/DEFAULT_THEME'; import CommonUpsInputGroup from './CommonUpsInputGroup'; import FlexBox from '../FlexBox'; +import Link from '../Link'; import SelectWithLabel from '../SelectWithLabel'; import Spinner from '../Spinner'; import { BodyText } from '../Text'; @@ -22,15 +25,46 @@ const AddUpsInputGroup: FC = ({ () => upsTemplate ? Object.entries(upsTemplate).map( - ([upsTypeId, { brand, description }]) => ({ - displayValue: ( - - {brand} - {description} - - ), - value: upsTypeId, - }), + ([ + upsTypeId, + { + brand, + description, + links: { 0: link }, + }, + ]) => { + let linkElement: ReactNode; + + if (link) { + const { linkHref, linkLabel } = link; + + linkElement = ( + { + // Don't trigger the (parent) item selection event. + event.stopPropagation(); + }} + sx={{ display: 'inline-flex', color: BLACK }} + target="_blank" + > + {linkLabel} + + ); + } + + return { + displayValue: ( + + {brand} + + {description} ({linkElement}) + + + ), + value: upsTypeId, + }; + }, ) : [], [upsTemplate], diff --git a/striker-ui/types/APIUps.d.ts b/striker-ui/types/APIUps.d.ts index d0194de5..4956eada 100644 --- a/striker-ui/types/APIUps.d.ts +++ b/striker-ui/types/APIUps.d.ts @@ -3,6 +3,12 @@ type APIUpsTemplate = { agent: string; brand: string; description: string; + links: { + [linkId: string]: { + linkHref: string; + linkLabel: string; + }; + }; }; }; From 1345e026ccb01c99c60b141b95be86453dadaf39 Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Thu, 2 Mar 2023 16:25:58 -0500 Subject: [PATCH 67/85] fix(striker-ui): expose required in SelectWithLabel --- striker-ui/components/SelectWithLabel.tsx | 9 +++++++-- striker-ui/types/SelectWithLabel.d.ts | 1 + 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/striker-ui/components/SelectWithLabel.tsx b/striker-ui/components/SelectWithLabel.tsx index 00ed6e02..8a7d63e2 100644 --- a/striker-ui/components/SelectWithLabel.tsx +++ b/striker-ui/components/SelectWithLabel.tsx @@ -24,6 +24,7 @@ const SelectWithLabel: FC = ({ messageBoxProps = {}, name, onChange, + required: isRequired, selectProps: { multiple: selectMultiple, sx: selectSx, @@ -80,11 +81,15 @@ const SelectWithLabel: FC = ({ const labelElement = useMemo( () => label && ( - + {label} ), - [inputLabelProps, label, selectId], + [inputLabelProps, isRequired, label, selectId], ); const menuItemElements = useMemo( () => diff --git a/striker-ui/types/SelectWithLabel.d.ts b/striker-ui/types/SelectWithLabel.d.ts index 384e48ff..d799a1b5 100644 --- a/striker-ui/types/SelectWithLabel.d.ts +++ b/striker-ui/types/SelectWithLabel.d.ts @@ -20,6 +20,7 @@ type SelectWithLabelOptionalProps = { >; label?: string; messageBoxProps?: Partial; + required?: boolean; selectProps?: Partial; }; From 301b2790002d0040f8fd021efbc3f3183df57f8c Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Thu, 2 Mar 2023 16:28:22 -0500 Subject: [PATCH 68/85] fix(striker-ui): show UPS type as required --- striker-ui/components/ManageUps/AddUpsInputGroup.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/striker-ui/components/ManageUps/AddUpsInputGroup.tsx b/striker-ui/components/ManageUps/AddUpsInputGroup.tsx index 2aa5bf51..a81a4e2f 100644 --- a/striker-ui/components/ManageUps/AddUpsInputGroup.tsx +++ b/striker-ui/components/ManageUps/AddUpsInputGroup.tsx @@ -82,6 +82,7 @@ const AddUpsInputGroup: FC = ({ setInputUpsTypeIdValue(newValue); }} + required selectItems={upsTypeOptions} selectProps={{ onClearIndicatorClick: () => { From 4400bf6645a64f7eb6a64a87db02b67efce012b8 Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Thu, 2 Mar 2023 23:00:53 -0500 Subject: [PATCH 69/85] fix(striker-ui): make buildMapToMessageSetter() handle array ids --- striker-ui/lib/buildMapToMessageSetter.ts | 54 ++++++++++++++++------- striker-ui/types/MapToMessageSetter.d.ts | 16 +++++++ 2 files changed, 55 insertions(+), 15 deletions(-) create mode 100644 striker-ui/types/MapToMessageSetter.d.ts diff --git a/striker-ui/lib/buildMapToMessageSetter.ts b/striker-ui/lib/buildMapToMessageSetter.ts index fe0b5dbd..5152ab33 100644 --- a/striker-ui/lib/buildMapToMessageSetter.ts +++ b/striker-ui/lib/buildMapToMessageSetter.ts @@ -2,25 +2,49 @@ import { MutableRefObject } from 'react'; import { MessageGroupForwardedRefContent } from '../components/MessageGroup'; -type BuildMapToMessageSetterReturnType = { - [MessageSetterID in keyof T]: MessageSetterFunction; +const buildMessageSetter = ( + id: string, + messageGroupRef: MutableRefObject, + container?: MapToMessageSetter, + key: string = id, +): MessageSetterFunction => { + const setter: MessageSetterFunction = (message?) => { + messageGroupRef.current.setMessage?.call(null, id, message); + }; + + if (container) { + container[key as keyof T] = setter; + } + + return setter; }; -const buildMapToMessageSetter = ( - mapToID: T, +const buildMapToMessageSetter = < + U extends string, + I extends InputIds, + M extends MapToInputId, +>( + ids: I, messageGroupRef: MutableRefObject, -): BuildMapToMessageSetterReturnType => - Object.entries(mapToID).reduce>( - (previous, [key, id]) => { - const setter: MessageSetterFunction = (message?) => { - messageGroupRef.current.setMessage?.call(null, id, message); - }; - - previous[key as keyof T] = setter; +): MapToMessageSetter => { + let result: MapToMessageSetter = {} as MapToMessageSetter; + if (ids instanceof Array) { + result = ids.reduce>((previous, id) => { + buildMessageSetter(id, messageGroupRef, previous); return previous; - }, - {} as BuildMapToMessageSetterReturnType, - ); + }, result); + } else { + result = Object.entries(ids).reduce>( + (previous, [key, id]) => { + buildMessageSetter(id, messageGroupRef, previous, key); + return previous; + }, + result, + ); + } + + return result; +}; export default buildMapToMessageSetter; diff --git a/striker-ui/types/MapToMessageSetter.d.ts b/striker-ui/types/MapToMessageSetter.d.ts new file mode 100644 index 00000000..537f2644 --- /dev/null +++ b/striker-ui/types/MapToMessageSetter.d.ts @@ -0,0 +1,16 @@ +type MapToMessageSetter = { + [MessageSetterID in keyof T]: MessageSetterFunction; +}; + +type InputIds = ReadonlyArray | MapToInputTestID; + +/** + * Given either: + * 1. an array of input identifiers, or + * 2. a key-value object of input indentifiers, + * transform it into a key-value object of identifiers. + */ +type MapToInputId< + U extends string, + I extends InputIds, +> = I extends ReadonlyArray ? { [K in I[number]]: K } : I; From 442427cf6313020ff1d184996f370f355a2dd367 Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Fri, 3 Mar 2023 00:01:45 -0500 Subject: [PATCH 70/85] fix(striker-ui): add arbitrary slot before action area in ConfirmDialog --- striker-ui/components/ConfirmDialog.tsx | 2 ++ striker-ui/types/ConfirmDialog.d.ts | 1 + 2 files changed, 3 insertions(+) diff --git a/striker-ui/components/ConfirmDialog.tsx b/striker-ui/components/ConfirmDialog.tsx index 3298375a..5259db49 100644 --- a/striker-ui/components/ConfirmDialog.tsx +++ b/striker-ui/components/ConfirmDialog.tsx @@ -49,6 +49,7 @@ const ConfirmDialog = forwardRef< onProceedAppend, onSubmitAppend, openInitially = false, + preActionArea, proceedButtonProps = {}, proceedColour: proceedColourKey = 'blue', scrollContent: isScrollContent = false, @@ -234,6 +235,7 @@ const ConfirmDialog = forwardRef< {contentElement} + {preActionArea} {actionAreaElement} diff --git a/striker-ui/types/ConfirmDialog.d.ts b/striker-ui/types/ConfirmDialog.d.ts index f9fdde02..24e94d11 100644 --- a/striker-ui/types/ConfirmDialog.d.ts +++ b/striker-ui/types/ConfirmDialog.d.ts @@ -10,6 +10,7 @@ type ConfirmDialogOptionalProps = { onCancelAppend?: ContainedButtonProps['onClick']; onSubmitAppend?: import('react').FormEventHandler; openInitially?: boolean; + preActionArea?: import('react').ReactNode; proceedButtonProps?: ContainedButtonProps; proceedColour?: 'blue' | 'red'; scrollContent?: boolean; From 737850f9d0fc350b569a3adf090a4eeb639979bb Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Fri, 3 Mar 2023 00:03:24 -0500 Subject: [PATCH 71/85] fix(striker-ui): add hook useFormUtils --- striker-ui/hooks/useFormUtils.ts | 78 ++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 striker-ui/hooks/useFormUtils.ts diff --git a/striker-ui/hooks/useFormUtils.ts b/striker-ui/hooks/useFormUtils.ts new file mode 100644 index 00000000..8f71dfc3 --- /dev/null +++ b/striker-ui/hooks/useFormUtils.ts @@ -0,0 +1,78 @@ +import { + Dispatch, + MutableRefObject, + SetStateAction, + useCallback, + useMemo, + useState, +} from 'react'; + +import buildMapToMessageSetter from '../lib/buildMapToMessageSetter'; +import buildObjectStateSetterCallback from '../lib/buildObjectStateSetterCallback'; +import { MessageGroupForwardedRefContent } from '../components/MessageGroup'; + +type FormValidity = { + [K in keyof T]?: boolean; +}; + +const useFormUtils = < + U extends string, + I extends InputIds, + M extends MapToInputId, +>( + ids: I, + messageGroupRef: MutableRefObject, +): { + buildFinishInputTestBatchFunction: ( + key: keyof M, + ) => (result: boolean) => void; + buildInputFirstRenderFunction: ( + key: keyof M, + ) => ({ isRequired }: { isRequired: boolean }) => void; + formValidity: FormValidity; + isFormInvalid: boolean; + msgSetters: MapToMessageSetter; + setFormValidity: Dispatch>>; +} => { + const [formValidity, setFormValidity] = useState>({}); + + const buildFinishInputTestBatchFunction = useCallback( + (key: keyof M) => (result: boolean) => { + setFormValidity( + buildObjectStateSetterCallback>(key, result), + ); + }, + [], + ); + + const buildInputFirstRenderFunction = useCallback( + (key: keyof M) => + ({ isRequired }: { isRequired: boolean }) => { + setFormValidity( + buildObjectStateSetterCallback>(key, !isRequired), + ); + }, + [], + ); + + const isFormInvalid = useMemo( + () => Object.values(formValidity).some((isInputValid) => !isInputValid), + [formValidity], + ); + + const msgSetters = useMemo( + () => buildMapToMessageSetter(ids, messageGroupRef), + [ids, messageGroupRef], + ); + + return { + buildFinishInputTestBatchFunction, + buildInputFirstRenderFunction, + formValidity, + isFormInvalid, + msgSetters, + setFormValidity, + }; +}; + +export default useFormUtils; From 26881c04365d18bbfd8e3ded6949ad46eeb46881 Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Fri, 3 Mar 2023 19:28:48 -0500 Subject: [PATCH 72/85] fix(striker-ui): expose isRequired in build test batch functions --- striker-ui/lib/test_input/buildDomainTestBatch.tsx | 3 ++- striker-ui/lib/test_input/buildIPAddressTestBatch.tsx | 3 ++- striker-ui/lib/test_input/buildNumberTestBatch.tsx | 3 ++- striker-ui/lib/test_input/buildPeacefulStringTestBatch.tsx | 3 ++- striker-ui/lib/test_input/buildUUIDTestBatch.tsx | 3 ++- striker-ui/types/TestInputFunction.d.ts | 3 ++- 6 files changed, 12 insertions(+), 6 deletions(-) diff --git a/striker-ui/lib/test_input/buildDomainTestBatch.tsx b/striker-ui/lib/test_input/buildDomainTestBatch.tsx index f7d4a23c..dad7374a 100644 --- a/striker-ui/lib/test_input/buildDomainTestBatch.tsx +++ b/striker-ui/lib/test_input/buildDomainTestBatch.tsx @@ -6,10 +6,11 @@ import { InlineMonoText } from '../../components/Text'; const buildDomainTestBatch: BuildInputTestBatchFunction = ( inputName, onSuccess, - { onFinishBatch, ...defaults } = {}, + { isRequired, onFinishBatch, ...defaults } = {}, onDomainTestFailure, ) => ({ defaults: { ...defaults, onSuccess }, + isRequired, onFinishBatch, tests: [ { diff --git a/striker-ui/lib/test_input/buildIPAddressTestBatch.tsx b/striker-ui/lib/test_input/buildIPAddressTestBatch.tsx index b8edddd7..418fb322 100644 --- a/striker-ui/lib/test_input/buildIPAddressTestBatch.tsx +++ b/striker-ui/lib/test_input/buildIPAddressTestBatch.tsx @@ -5,10 +5,11 @@ import testNotBlank from './testNotBlank'; const buildIPAddressTestBatch: BuildInputTestBatchFunction = ( inputName, onSuccess, - { onFinishBatch, ...defaults } = {}, + { isRequired, onFinishBatch, ...defaults } = {}, onIPv4TestFailure, ) => ({ defaults: { ...defaults, onSuccess }, + isRequired, onFinishBatch, tests: [ { diff --git a/striker-ui/lib/test_input/buildNumberTestBatch.tsx b/striker-ui/lib/test_input/buildNumberTestBatch.tsx index 5a5d0447..846a5138 100644 --- a/striker-ui/lib/test_input/buildNumberTestBatch.tsx +++ b/striker-ui/lib/test_input/buildNumberTestBatch.tsx @@ -4,7 +4,7 @@ import toNumber from '../toNumber'; const buildNumberTestBatch: BuildInputTestBatchFunction = ( inputName, onSuccess, - { onFinishBatch, ...defaults } = {}, + { isRequired, onFinishBatch, ...defaults } = {}, onIntTestFailure?, onFloatTestFailure?, onRangeTestFailure?, @@ -48,6 +48,7 @@ const buildNumberTestBatch: BuildInputTestBatchFunction = ( return { defaults: { ...defaults, onSuccess }, + isRequired, onFinishBatch, tests, }; diff --git a/striker-ui/lib/test_input/buildPeacefulStringTestBatch.tsx b/striker-ui/lib/test_input/buildPeacefulStringTestBatch.tsx index 93c3cfbf..594f387e 100644 --- a/striker-ui/lib/test_input/buildPeacefulStringTestBatch.tsx +++ b/striker-ui/lib/test_input/buildPeacefulStringTestBatch.tsx @@ -6,10 +6,11 @@ import { InlineMonoText } from '../../components/Text'; const buildPeacefulStringTestBatch: BuildInputTestBatchFunction = ( inputName, onSuccess, - { onFinishBatch, ...defaults } = {}, + { isRequired, onFinishBatch, ...defaults } = {}, onTestPeacefulStringFailureAppend, ) => ({ defaults: { ...defaults, onSuccess }, + isRequired, onFinishBatch, tests: [ { diff --git a/striker-ui/lib/test_input/buildUUIDTestBatch.tsx b/striker-ui/lib/test_input/buildUUIDTestBatch.tsx index f417f548..d5173fbf 100644 --- a/striker-ui/lib/test_input/buildUUIDTestBatch.tsx +++ b/striker-ui/lib/test_input/buildUUIDTestBatch.tsx @@ -5,10 +5,11 @@ import testNotBlank from './testNotBlank'; const buildUUIDTestBatch: BuildInputTestBatchFunction = ( inputName, onSuccess, - { onFinishBatch, ...defaults } = {}, + { isRequired, onFinishBatch, ...defaults } = {}, onUUIDTestFailure, ) => ({ defaults: { ...defaults, onSuccess }, + isRequired, onFinishBatch, tests: [ { diff --git a/striker-ui/types/TestInputFunction.d.ts b/striker-ui/types/TestInputFunction.d.ts index 0cf81cb4..a8f5ce63 100644 --- a/striker-ui/types/TestInputFunction.d.ts +++ b/striker-ui/types/TestInputFunction.d.ts @@ -65,7 +65,8 @@ type InputTestBatch = { type BuildInputTestBatchFunction = ( inputName: string, onSuccess: InputTestSuccessCallback, - options?: InputTestBatch['defaults'] & Pick, + options?: InputTestBatch['defaults'] & + Pick, ...onFailureAppends: InputTestFailureAppendCallback[] ) => InputTestBatch; From 36f9938767230f29dfc66d1f379302ef992fc8ec Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Fri, 3 Mar 2023 21:04:21 -0500 Subject: [PATCH 73/85] fix(striker-ui): organize types in useFormUtils hook --- striker-ui/hooks/useFormUtils.ts | 47 ++++++++++---------------------- striker-ui/types/FormUtils.d.ts | 27 ++++++++++++++++++ 2 files changed, 41 insertions(+), 33 deletions(-) create mode 100644 striker-ui/types/FormUtils.d.ts diff --git a/striker-ui/hooks/useFormUtils.ts b/striker-ui/hooks/useFormUtils.ts index 8f71dfc3..70727b88 100644 --- a/striker-ui/hooks/useFormUtils.ts +++ b/striker-ui/hooks/useFormUtils.ts @@ -1,20 +1,9 @@ -import { - Dispatch, - MutableRefObject, - SetStateAction, - useCallback, - useMemo, - useState, -} from 'react'; +import { MutableRefObject, useCallback, useMemo, useState } from 'react'; import buildMapToMessageSetter from '../lib/buildMapToMessageSetter'; import buildObjectStateSetterCallback from '../lib/buildObjectStateSetterCallback'; import { MessageGroupForwardedRefContent } from '../components/MessageGroup'; -type FormValidity = { - [K in keyof T]?: boolean; -}; - const useFormUtils = < U extends string, I extends InputIds, @@ -22,37 +11,28 @@ const useFormUtils = < >( ids: I, messageGroupRef: MutableRefObject, -): { - buildFinishInputTestBatchFunction: ( - key: keyof M, - ) => (result: boolean) => void; - buildInputFirstRenderFunction: ( - key: keyof M, - ) => ({ isRequired }: { isRequired: boolean }) => void; - formValidity: FormValidity; - isFormInvalid: boolean; - msgSetters: MapToMessageSetter; - setFormValidity: Dispatch>>; -} => { +): FormUtils => { const [formValidity, setFormValidity] = useState>({}); + const setValidity = useCallback((key: keyof M, value: boolean) => { + setFormValidity( + buildObjectStateSetterCallback>(key, value), + ); + }, []); + const buildFinishInputTestBatchFunction = useCallback( (key: keyof M) => (result: boolean) => { - setFormValidity( - buildObjectStateSetterCallback>(key, result), - ); + setValidity(key, result); }, - [], + [setValidity], ); const buildInputFirstRenderFunction = useCallback( (key: keyof M) => - ({ isRequired }: { isRequired: boolean }) => { - setFormValidity( - buildObjectStateSetterCallback>(key, !isRequired), - ); + ({ isValid }: InputFirstRenderFunctionArgs) => { + setValidity(key, isValid); }, - [], + [setValidity], ); const isFormInvalid = useMemo( @@ -72,6 +52,7 @@ const useFormUtils = < isFormInvalid, msgSetters, setFormValidity, + setValidity, }; }; diff --git a/striker-ui/types/FormUtils.d.ts b/striker-ui/types/FormUtils.d.ts new file mode 100644 index 00000000..1f106121 --- /dev/null +++ b/striker-ui/types/FormUtils.d.ts @@ -0,0 +1,27 @@ +type FormValidity = { + [K in keyof T]?: boolean; +}; + +type InputTestBatchFinishCallbackBuilder = ( + key: keyof M, +) => InputTestBatchFinishCallback; + +type InputFirstRenderFunctionArgs = { isValid: boolean }; + +type InputFirstRenderFunction = (args: InputFirstRenderFunctionArgs) => void; + +type InputFirstRenderFunctionBuilder = ( + key: keyof M, +) => InputFirstRenderFunction; + +type FormUtils = { + buildFinishInputTestBatchFunction: InputTestBatchFinishCallbackBuilder; + buildInputFirstRenderFunction: InputFirstRenderFunctionBuilder; + formValidity: FormValidity; + isFormInvalid: boolean; + msgSetters: MapToMessageSetter; + setFormValidity: import('react').Dispatch< + import('react').SetStateAction> + >; + setValidity: (key: keyof M, value: boolean) => void; +}; From 0c1ec5a88a6c6a5003aab29560250818ab01855b Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Fri, 3 Mar 2023 21:06:07 -0500 Subject: [PATCH 74/85] fix(striker-ui): expose blur and focus event handler slots in SelectWithLabel --- striker-ui/components/SelectWithLabel.tsx | 4 ++++ striker-ui/types/SelectWithLabel.d.ts | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/striker-ui/components/SelectWithLabel.tsx b/striker-ui/components/SelectWithLabel.tsx index 8a7d63e2..fabac9d8 100644 --- a/striker-ui/components/SelectWithLabel.tsx +++ b/striker-ui/components/SelectWithLabel.tsx @@ -23,7 +23,9 @@ const SelectWithLabel: FC = ({ isReadOnly = false, messageBoxProps = {}, name, + onBlur, onChange, + onFocus, required: isRequired, selectProps: { multiple: selectMultiple, @@ -110,7 +112,9 @@ const SelectWithLabel: FC = ({ input={inputElement} multiple={selectMultiple} name={name} + onBlur={onBlur} onChange={onChange} + onFocus={onFocus} readOnly={isReadOnly} value={selectValue} {...restSelectProps} diff --git a/striker-ui/types/SelectWithLabel.d.ts b/striker-ui/types/SelectWithLabel.d.ts index d799a1b5..5d020eb5 100644 --- a/striker-ui/types/SelectWithLabel.d.ts +++ b/striker-ui/types/SelectWithLabel.d.ts @@ -25,7 +25,7 @@ type SelectWithLabelOptionalProps = { }; type SelectWithLabelProps = SelectWithLabelOptionalProps & - Pick & { + Pick & { id: string; selectItems: Array; }; From afdd3767592d20efc7da9febee87bfad20751d7a Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Fri, 3 Mar 2023 21:09:44 -0500 Subject: [PATCH 75/85] fix(striker-ui): correct validity test on first render in InputWithRef --- striker-ui/components/InputWithRef.tsx | 17 ++++++++++------- .../components/StrikerConfig/AddPeerDialog.tsx | 4 ++-- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/striker-ui/components/InputWithRef.tsx b/striker-ui/components/InputWithRef.tsx index 89c016f4..16f769aa 100644 --- a/striker-ui/components/InputWithRef.tsx +++ b/striker-ui/components/InputWithRef.tsx @@ -5,7 +5,6 @@ import { forwardRef, ReactElement, useCallback, - useEffect, useImperativeHandle, useMemo, useState, @@ -26,7 +25,7 @@ type InputWithRefOptionalPropsWithoutDefault< TypeName extends keyof MapToInputType, > = { inputTestBatch?: InputTestBatch; - onFirstRender?: (args: { isRequired: boolean }) => void; + onFirstRender?: InputFirstRenderFunction; valueKey?: CreateInputOnChangeHandlerOptions['valueKey']; }; @@ -167,11 +166,15 @@ const InputWithRef = forwardRef( [initOnFocus, inputTestBatch], ); - useEffect(() => { - if (isFirstRender) { - onFirstRender?.call(null, { isRequired }); - } - }, [isFirstRender, isRequired, onFirstRender]); + if (isFirstRender) { + const isValid = + testInput?.call(null, { + inputs: { [INPUT_TEST_ID]: { value: inputValue } }, + isIgnoreOnCallbacks: true, + }) ?? false; + + onFirstRender?.call(null, { isValid }); + } useImperativeHandle( ref, diff --git a/striker-ui/components/StrikerConfig/AddPeerDialog.tsx b/striker-ui/components/StrikerConfig/AddPeerDialog.tsx index e8cb8dc3..1ccfcaad 100644 --- a/striker-ui/components/StrikerConfig/AddPeerDialog.tsx +++ b/striker-ui/components/StrikerConfig/AddPeerDialog.tsx @@ -61,8 +61,8 @@ const AddPeerDialog = forwardRef< const buildInputFirstRenderFunction = useCallback( (key: string) => - ({ isRequired }: { isRequired: boolean }) => { - setFormValidity(buildObjectStateSetterCallback(key, !isRequired)); + ({ isValid }: InputFirstRenderFunctionArgs) => { + setFormValidity(buildObjectStateSetterCallback(key, isValid)); }, [], ); From d3894081f63cef1845080603fdd6a652d65256dd Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Fri, 3 Mar 2023 21:12:24 -0500 Subject: [PATCH 76/85] fix(striker-ui): add input tests to CommonUpsInputGroup --- .../ManageUps/CommonUpsInputGroup.tsx | 131 ++++++++++++------ striker-ui/types/CommonUpsInputGroup.d.ts | 5 +- 2 files changed, 95 insertions(+), 41 deletions(-) diff --git a/striker-ui/components/ManageUps/CommonUpsInputGroup.tsx b/striker-ui/components/ManageUps/CommonUpsInputGroup.tsx index 6e1d49a9..7e744061 100644 --- a/striker-ui/components/ManageUps/CommonUpsInputGroup.tsx +++ b/striker-ui/components/ManageUps/CommonUpsInputGroup.tsx @@ -1,53 +1,104 @@ -import { FC } from 'react'; +import { ReactElement } from 'react'; import Grid from '../Grid'; import InputWithRef from '../InputWithRef'; import OutlinedInputWithLabel from '../OutlinedInputWithLabel'; +import { + buildIPAddressTestBatch, + buildPeacefulStringTestBatch, +} from '../../lib/test_input'; const INPUT_ID_UPS_IP = 'common-ups-input-ip-address'; const INPUT_ID_UPS_NAME = 'common-ups-input-host-name'; -const CommonUpsInputGroup: FC = ({ +const INPUT_LABEL_UPS_IP = 'IP address'; +const INPUT_LABEL_UPS_NAME = 'Host name'; + +const CommonUpsInputGroup = < + M extends { + [K in typeof INPUT_ID_UPS_IP | typeof INPUT_ID_UPS_NAME]: string; + }, +>({ + formUtils: { + buildFinishInputTestBatchFunction, + buildInputFirstRenderFunction, + msgSetters, + }, previous: { upsIPAddress: previousIpAddress, upsName: previousUpsName } = {}, -}) => ( - <> - - } - required - /> - ), - }, - 'common-ups-input-cell-ip-address': { - children: ( - - } - required - /> - ), - }, - }} - spacing="1em" - /> - +}: CommonUpsInputGroupProps): ReactElement => ( + + } + inputTestBatch={buildPeacefulStringTestBatch( + INPUT_LABEL_UPS_NAME, + () => { + msgSetters[INPUT_ID_UPS_NAME](); + }, + { + onFinishBatch: + buildFinishInputTestBatchFunction(INPUT_ID_UPS_NAME), + }, + (message) => { + msgSetters[INPUT_ID_UPS_NAME]({ + children: message, + }); + }, + )} + onFirstRender={buildInputFirstRenderFunction(INPUT_ID_UPS_NAME)} + required + /> + ), + }, + 'common-ups-input-cell-ip-address': { + children: ( + + } + inputTestBatch={buildIPAddressTestBatch( + INPUT_LABEL_UPS_IP, + () => { + msgSetters[INPUT_ID_UPS_IP](); + }, + { + onFinishBatch: + buildFinishInputTestBatchFunction(INPUT_ID_UPS_IP), + }, + (message) => { + msgSetters[INPUT_ID_UPS_IP]({ + children: message, + }); + }, + )} + onFirstRender={buildInputFirstRenderFunction(INPUT_ID_UPS_IP)} + required + /> + ), + }, + }} + spacing="1em" + /> ); -export { INPUT_ID_UPS_IP, INPUT_ID_UPS_NAME }; +export { + INPUT_ID_UPS_IP, + INPUT_ID_UPS_NAME, + INPUT_LABEL_UPS_IP, + INPUT_LABEL_UPS_NAME, +}; export default CommonUpsInputGroup; diff --git a/striker-ui/types/CommonUpsInputGroup.d.ts b/striker-ui/types/CommonUpsInputGroup.d.ts index da058a40..020f814f 100644 --- a/striker-ui/types/CommonUpsInputGroup.d.ts +++ b/striker-ui/types/CommonUpsInputGroup.d.ts @@ -5,4 +5,7 @@ type CommonUpsInputGroupOptionalProps = { }; }; -type CommonUpsInputGroupProps = CommonUpsInputGroupOptionalProps; +type CommonUpsInputGroupProps = + CommonUpsInputGroupOptionalProps & { + formUtils: FormUtils; + }; From aa5aad4689bd3a2b0fc7f4878d3d3da486112a2f Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Fri, 3 Mar 2023 21:13:40 -0500 Subject: [PATCH 77/85] fix(striker-ui): add input validation to AddUpsInputGroup --- .../components/ManageUps/AddUpsInputGroup.tsx | 57 +++++++++++++++---- striker-ui/types/AddUpsInputGroup.d.ts | 8 ++- 2 files changed, 52 insertions(+), 13 deletions(-) diff --git a/striker-ui/components/ManageUps/AddUpsInputGroup.tsx b/striker-ui/components/ManageUps/AddUpsInputGroup.tsx index a81a4e2f..e0896c23 100644 --- a/striker-ui/components/ManageUps/AddUpsInputGroup.tsx +++ b/striker-ui/components/ManageUps/AddUpsInputGroup.tsx @@ -1,23 +1,41 @@ -import { FC, ReactElement, ReactNode, useMemo, useState } from 'react'; +import { ReactElement, ReactNode, useMemo, useState } from 'react'; import { BLACK } from '../../lib/consts/DEFAULT_THEME'; -import CommonUpsInputGroup from './CommonUpsInputGroup'; +import CommonUpsInputGroup, { + INPUT_ID_UPS_IP, + INPUT_ID_UPS_NAME, +} from './CommonUpsInputGroup'; import FlexBox from '../FlexBox'; import Link from '../Link'; import SelectWithLabel from '../SelectWithLabel'; import Spinner from '../Spinner'; import { BodyText } from '../Text'; +import useIsFirstRender from '../../hooks/useIsFirstRender'; -const INPUT_ID_UPS_TYPE_ID = 'add-ups-select-ups-type-id'; +const INPUT_ID_UPS_TYPE = 'add-ups-select-ups-type-id'; -const AddUpsInputGroup: FC = ({ +const INPUT_LABEL_UPS_TYPE = 'UPS type'; + +const AddUpsInputGroup = < + M extends { + [K in + | typeof INPUT_ID_UPS_IP + | typeof INPUT_ID_UPS_NAME + | typeof INPUT_ID_UPS_TYPE]: string; + }, +>({ + formUtils, loading: isExternalLoading, previous = {}, upsTemplate, -}) => { +}: AddUpsInputGroupProps): ReactElement => { + const { buildInputFirstRenderFunction, setValidity } = formUtils; + const { upsTypeId: previousUpsTypeId = '' } = previous; + const isFirstRender = useIsFirstRender(); + const [inputUpsTypeIdValue, setInputUpsTypeIdValue] = useState(previousUpsTypeId); @@ -75,17 +93,19 @@ const AddUpsInputGroup: FC = ({ upsTemplate && ( { const newValue = String(rawNewValue); + setValidity(INPUT_ID_UPS_TYPE, true); setInputUpsTypeIdValue(newValue); }} required selectItems={upsTypeOptions} selectProps={{ onClearIndicatorClick: () => { + setValidity(INPUT_ID_UPS_TYPE, false); setInputUpsTypeIdValue(''); }, renderValue: (rawValue) => { @@ -98,8 +118,9 @@ const AddUpsInputGroup: FC = ({ value={inputUpsTypeIdValue} /> ), - [inputUpsTypeIdValue, upsTypeOptions, upsTemplate], + [upsTemplate, upsTypeOptions, inputUpsTypeIdValue, setValidity], ); + const content = useMemo( () => isExternalLoading ? ( @@ -107,15 +128,29 @@ const AddUpsInputGroup: FC = ({ ) : ( {pickUpsTypeElement} - {inputUpsTypeIdValue && } + {inputUpsTypeIdValue && ( + + )} ), - [inputUpsTypeIdValue, isExternalLoading, pickUpsTypeElement, previous], + [ + formUtils, + inputUpsTypeIdValue, + isExternalLoading, + pickUpsTypeElement, + previous, + ], ); + if (isFirstRender) { + buildInputFirstRenderFunction(INPUT_ID_UPS_TYPE)({ + isValid: Boolean(inputUpsTypeIdValue), + }); + } + return content; }; -export { INPUT_ID_UPS_TYPE_ID }; +export { INPUT_ID_UPS_TYPE, INPUT_LABEL_UPS_TYPE }; export default AddUpsInputGroup; diff --git a/striker-ui/types/AddUpsInputGroup.d.ts b/striker-ui/types/AddUpsInputGroup.d.ts index 76e43636..e053670f 100644 --- a/striker-ui/types/AddUpsInputGroup.d.ts +++ b/striker-ui/types/AddUpsInputGroup.d.ts @@ -1,7 +1,11 @@ type AddUpsInputGroupOptionalProps = { loading?: boolean; - previous?: CommonUpsInputGroupProps['previous'] & { upsTypeId?: string }; + previous?: CommonUpsInputGroupOptionalProps['previous'] & { + upsTypeId?: string; + }; upsTemplate?: APIUpsTemplate; }; -type AddUpsInputGroupProps = AddUpsInputGroupOptionalProps; +type AddUpsInputGroupProps = + AddUpsInputGroupOptionalProps & + Pick, 'formUtils'>; From 2f84f52090df6afa6f9b8c1a097af3c2374bda3e Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Fri, 3 Mar 2023 21:16:25 -0500 Subject: [PATCH 78/85] fix(striker-ui): passthrough input validation in EditUpsInputGroup --- .../ManageUps/EditUpsInputGroup.tsx | 25 ++++++++++++++----- striker-ui/types/EditUpsInputGroup.d.ts | 9 ++++--- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/striker-ui/components/ManageUps/EditUpsInputGroup.tsx b/striker-ui/components/ManageUps/EditUpsInputGroup.tsx index 963b89d9..47320378 100644 --- a/striker-ui/components/ManageUps/EditUpsInputGroup.tsx +++ b/striker-ui/components/ManageUps/EditUpsInputGroup.tsx @@ -1,27 +1,40 @@ -import { FC, ReactElement, useMemo } from 'react'; +import { ReactElement, useMemo } from 'react'; -import AddUpsInputGroup from './AddUpsInputGroup'; +import AddUpsInputGroup, { INPUT_ID_UPS_TYPE } from './AddUpsInputGroup'; +import { INPUT_ID_UPS_IP, INPUT_ID_UPS_NAME } from './CommonUpsInputGroup'; import Spinner from '../Spinner'; const INPUT_ID_UPS_UUID = 'edit-ups-input-ups-uuid'; -const EditUpsInputGroup: FC = ({ +const EditUpsInputGroup = < + M extends { + [K in + | typeof INPUT_ID_UPS_IP + | typeof INPUT_ID_UPS_NAME + | typeof INPUT_ID_UPS_TYPE]: string; + }, +>({ + formUtils, loading: isExternalLoading, previous, upsTemplate, upsUUID, -}) => { +}: EditUpsInputGroupProps): ReactElement => { const content = useMemo( () => isExternalLoading ? ( ) : ( <> - + ), - [isExternalLoading, previous, upsTemplate, upsUUID], + [formUtils, isExternalLoading, previous, upsTemplate, upsUUID], ); return content; diff --git a/striker-ui/types/EditUpsInputGroup.d.ts b/striker-ui/types/EditUpsInputGroup.d.ts index ebff2166..4c75a28b 100644 --- a/striker-ui/types/EditUpsInputGroup.d.ts +++ b/striker-ui/types/EditUpsInputGroup.d.ts @@ -2,7 +2,8 @@ type EditUpsInputGroupOptionalProps = { loading?: boolean; }; -type EditUpsInputGroupProps = EditUpsInputGroupOptionalProps & - Pick & { - upsUUID: string; - }; +type EditUpsInputGroupProps = + EditUpsInputGroupOptionalProps & + Pick, 'formUtils' | 'previous' | 'upsTemplate'> & { + upsUUID: string; + }; From 759cd6f58aafcac1238dcc7056415045a1cbd78f Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Fri, 3 Mar 2023 21:18:07 -0500 Subject: [PATCH 79/85] fix(striker-ui): add form validation and message in ManageUpsPanel --- .../components/ManageUps/ManageUpsPanel.tsx | 35 +++++++++++++++---- 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/striker-ui/components/ManageUps/ManageUpsPanel.tsx b/striker-ui/components/ManageUps/ManageUpsPanel.tsx index 7c4d3f04..e6d10305 100644 --- a/striker-ui/components/ManageUps/ManageUpsPanel.tsx +++ b/striker-ui/components/ManageUps/ManageUpsPanel.tsx @@ -9,7 +9,7 @@ import { import API_BASE_URL from '../../lib/consts/API_BASE_URL'; -import AddUpsInputGroup, { INPUT_ID_UPS_TYPE_ID } from './AddUpsInputGroup'; +import AddUpsInputGroup, { INPUT_ID_UPS_TYPE } from './AddUpsInputGroup'; import api from '../../lib/api'; import { INPUT_ID_UPS_IP, INPUT_ID_UPS_NAME } from './CommonUpsInputGroup'; import ConfirmDialog from '../ConfirmDialog'; @@ -18,11 +18,13 @@ import FlexBox from '../FlexBox'; import FormDialog from '../FormDialog'; import handleAPIError from '../../lib/handleAPIError'; import List from '../List'; +import MessageGroup, { MessageGroupForwardedRefContent } from '../MessageGroup'; import { Panel, PanelHeader } from '../Panels'; import periodicFetch from '../../lib/fetchers/periodicFetch'; import Spinner from '../Spinner'; import { BodyText, HeaderText, InlineMonoText, MonoText } from '../Text'; import useConfirmDialogProps from '../../hooks/useConfirmDialogProps'; +import useFormUtils from '../../hooks/useFormUtils'; import useIsFirstRender from '../../hooks/useIsFirstRender'; import useProtectedState from '../../hooks/useProtectedState'; @@ -48,7 +50,7 @@ const getUpsFormData = ( INPUT_ID_UPS_IP, ) as HTMLInputElement; - const inputUpsTypeId = elements.namedItem(INPUT_ID_UPS_TYPE_ID); + const inputUpsTypeId = elements.namedItem(INPUT_ID_UPS_TYPE); let upsAgent = ''; let upsBrand = ''; @@ -101,6 +103,7 @@ const ManageUpsPanel: FC = () => { const confirmDialogRef = useRef({}); const formDialogRef = useRef({}); + const messageGroupRef = useRef({}); const [confirmDialogProps, setConfirmDialogProps] = useConfirmDialogProps(); const [formDialogProps, setFormDialogProps] = useConfirmDialogProps(); @@ -116,6 +119,12 @@ const ManageUpsPanel: FC = () => { refreshInterval: 60000, }); + const formUtils = useFormUtils( + [INPUT_ID_UPS_IP, INPUT_ID_UPS_NAME, INPUT_ID_UPS_TYPE], + messageGroupRef, + ); + const { isFormInvalid } = formUtils; + const buildEditUpsFormDialogProps = useCallback< (args: APIUpsOverview[string]) => ConfirmDialogProps >( @@ -131,6 +140,7 @@ const ManageUpsPanel: FC = () => { actionProceedText: 'Update', content: ( { ), }; }, - [setConfirmDialogProps, upsTemplate], + [formUtils, setConfirmDialogProps, upsTemplate], ); const addUpsFormDialogProps = useMemo( () => ({ actionProceedText: 'Add', - content: , + content: ( + + ), onSubmitAppend: (event) => { if (!upsTemplate) { return; @@ -201,7 +213,7 @@ const ManageUpsPanel: FC = () => { }, titleText: 'Add a UPS', }), - [setConfirmDialogProps, upsTemplate], + [formUtils, setConfirmDialogProps, upsTemplate], ); const listElement = useMemo( @@ -269,7 +281,18 @@ const ManageUpsPanel: FC = () => { {panelContent} - + + } + proceedButtonProps={{ disabled: isFormInvalid }} + /> ); From 567abff9de188d996ab43735f8d66c67d5b03521 Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Fri, 3 Mar 2023 21:18:54 -0500 Subject: [PATCH 80/85] fix(striker-ui): add manage UPS tab --- striker-ui/pages/manage-element/index.tsx | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/striker-ui/pages/manage-element/index.tsx b/striker-ui/pages/manage-element/index.tsx index 18eceb99..54fdcb3a 100644 --- a/striker-ui/pages/manage-element/index.tsx +++ b/striker-ui/pages/manage-element/index.tsx @@ -8,6 +8,7 @@ import Grid from '../../components/Grid'; import handleAPIError from '../../lib/handleAPIError'; import Header from '../../components/Header'; import ManageFencePanel from '../../components/ManageFence'; +import ManageUpsPanel from '../../components/ManageUps'; import { Panel } from '../../components/Panels'; import PrepareHostForm from '../../components/PrepareHostForm'; import PrepareNetworkForm from '../../components/PrepareNetworkForm'; @@ -130,6 +131,19 @@ const ManageFenceTabContent: FC = () => ( /> ); +const ManageUpsTabContent: FC = () => ( + , + ...STEP_CONTENT_GRID_CENTER_COLUMN, + }, + }} + /> +); + const ManageElement: FC = () => { const { isReady, @@ -177,6 +191,7 @@ const ManageElement: FC = () => { + @@ -188,6 +203,9 @@ const ManageElement: FC = () => { + + + ); }; From cf73d8ed36cd94a99110defb1467178633e9d135 Mon Sep 17 00:00:00 2001 From: digimer Date: Wed, 5 Apr 2023 15:01:58 -0400 Subject: [PATCH 81/85] * Updated System->configure_ipmi() to auto-configure DR hosts once they've been assigned a BCN IP address. Signed-off-by: digimer --- Anvil/Tools/ScanCore.pm | 6 +- Anvil/Tools/System.pm | 363 ++++++++++++++++++++++++---------------- share/words.xml | 1 + 3 files changed, 227 insertions(+), 143 deletions(-) diff --git a/Anvil/Tools/ScanCore.pm b/Anvil/Tools/ScanCore.pm index d1b670a5..8af2b18f 100644 --- a/Anvil/Tools/ScanCore.pm +++ b/Anvil/Tools/ScanCore.pm @@ -1459,7 +1459,11 @@ sub post_scan_analysis_dr my $debug = defined $parameter->{debug} ? $parameter->{debug} : 3; $anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => $debug, key => "log_0125", variables => { method => "ScanCore->post_scan_analysis_dr()" }}); - + # Now that DR hosts are outside of specific Anvil! systems, auto-configuring their IPMI is more + # tricky. If there's no 'host_ipmi' set, and if there is a BCN1 IP, we'll configure using the BCN's + # third octet, plus 1. We'll use the password used for the database password. Later, if the IPMI + # is changed in the DB, we'll reconfigure to match. + $anvil->System->configure_ipmi({debug => $debug, dr => 1}); return(0); } diff --git a/Anvil/Tools/System.pm b/Anvil/Tools/System.pm index 41df7d65..601ca60f 100644 --- a/Anvil/Tools/System.pm +++ b/Anvil/Tools/System.pm @@ -1897,6 +1897,10 @@ B<< NOTE >>: The password used to set the IPMI BMC access is included both in th Parameters; +=head3 dr (Optional, default 0) + +This indicates that a DR host is being configured. If used, C<< manifest_uuid >> is ignored. + =head3 manifest_uuid (Optional, default sys::manifest_uuid) The C<< manifests >> -> C<< manifest_uuid >> used to pull out configuration data. This is required, but in most cases, it can be determined if not passed. @@ -1912,62 +1916,138 @@ sub configure_ipmi my $debug = defined $parameter->{debug} ? $parameter->{debug} : 3; $anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => $debug, key => "log_0125", variables => { method => "System->configure_ipmi()" }}); + my $dr = defined $parameter->{dr} ? $parameter->{dr} : ""; my $manifest_uuid = defined $parameter->{manifest_uuid} ? $parameter->{manifest_uuid} : ""; $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { + dr => $dr, manifest_uuid => $manifest_uuid, }}); - if ((not $manifest_uuid) && (exists $anvil->data->{sys}{manifest_uuid}) && ($anvil->data->{sys}{manifest_uuid})) - { - $manifest_uuid = $anvil->data->{sys}{manifest_uuid}; - $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { manifest_uuid => $manifest_uuid }}); - } - if (not $manifest_uuid) - { - # Nothing more we can do. - $anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 0, priority => "err", key => "log_0020", variables => { method => "Systeme->configure_ipmi()", parameter => "manifest_uuid" }}); - return(0); - } - - # Is this host in an Anvil!? $anvil->Database->get_hosts(); $anvil->Database->get_anvils(); - - my $anvil_uuid = ""; - my $host_uuid = $anvil->Get->host_uuid; - if ((exists $anvil->data->{hosts}{host_uuid}{$host_uuid}) && ($anvil->data->{hosts}{host_uuid}{$host_uuid}{anvil_uuid})) - { - # We're in an Anvil! - $anvil_uuid = $anvil->data->{hosts}{host_uuid}{$host_uuid}{anvil_uuid}; - $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { anvil_uuid => $anvil_uuid }}); + my $host_uuid = $anvil->Get->host_uuid; + my $ipmi_ip_address = ""; + my $ipmi_password = ""; + my $password_length = 0; + my $subnet_mask = ""; + my $gateway = ""; + my $in_network = ""; + if ($dr) + { + my $host_ipmi = $anvil->data->{hosts}{host_uuid}{$host_uuid}{host_ipmi}; + $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { host_ipmi => $host_ipmi }}); + if (not $host_ipmi) + { + # Do initial config. + $anvil->Network->load_ips({ + debug => $debug, + host_uuid => $host_uuid, + }); + foreach my $interface_name (sort {$a cmp $b} keys %{$anvil->data->{network}{$host_uuid}{interface}}) + { + $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { interface_name => $interface_name }}); + next if $interface_name !~ /^bcn1_/; + + my $ip_address_address = $anvil->data->{network}{$host_uuid}{interface}{$interface_name}{ip}; + my $ip_address_subnet_mask = $anvil->data->{network}{$host_uuid}{interface}{$interface_name}{subnet_mask}; + $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { + ip_address_address => $ip_address_address, + ip_address_subnet_mask => $ip_address_subnet_mask, + }}); + + # We need this to be a /16 + if ($ip_address_subnet_mask eq "255.255.0.0") + { + # Get the 3rd octet. + my ($first_octet, $second_octet, $third_octet, $fourth_octet) = ($ip_address_address =~ /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/); + $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { + first_octet => $first_octet, + second_octet => $second_octet, + third_octet => $third_octet, + fourth_octet => $fourth_octet, + }}); + + $ipmi_ip_address = $first_octet.".".$second_octet.".".($third_octet+1).".".$fourth_octet; + $subnet_mask = $ip_address_subnet_mask; + $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { + ipmi_ip_address => $ipmi_ip_address, + subnet_mask => $subnet_mask, + }}); + last; + } + } + + # Get the password using the Striker password. + my $db_uuid = $anvil->data->{sys}{database}{read_uuid}; + $ipmi_password = $anvil->data->{database}{$db_uuid}{password}; + $password_length = length(Encode::encode('UTF-8', $ipmi_password)); + $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { + db_uuid => $db_uuid, + ipmi_password => $anvil->Log->is_secure($ipmi_password), + password_length => $password_length, + }}); + + if ((not $anvil->Validate->ipv4({debug => $debug, ip => $ipmi_ip_address})) or (not $ipmi_password)) + { + # Can't proceed. + $anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 0, priority => "alert", key => "warning_0149"}); + return(0); + } + } + else + { + # TODO: Check if the local IPMI config matches the value in host_ipmi and, if not, + # reconfigure to match. + return(0); + } } else { - # Not in an Anvil!, return 0. - $anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 1, priority => "err", key => "log_0498"}); - return(0); - } - - # Look for a match in the anvils table for this host uuid. - my $machine = ""; - if ($anvil->data->{anvils}{anvil_uuid}{$anvil_uuid}{anvil_node1_host_uuid} eq $host_uuid) - { - $machine = "node1"; - $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { machine => $machine }}); - } - elsif ($anvil->data->{anvils}{anvil_uuid}{$anvil_uuid}{anvil_node2_host_uuid} eq $host_uuid) - { - $machine = "node2"; - $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { machine => $machine }}); - } - - # TODO: Make sure this works on DR hosts. - - if (not $machine) - { - # Look for a job for 'anvil-join-anvil' for this host. With it, we'll figure out the password - # and which machine we are. - my $query = " + if ((not $manifest_uuid) && (exists $anvil->data->{sys}{manifest_uuid}) && ($anvil->data->{sys}{manifest_uuid})) + { + $manifest_uuid = $anvil->data->{sys}{manifest_uuid}; + $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { manifest_uuid => $manifest_uuid }}); + } + if (not $manifest_uuid) + { + # Nothing more we can do. + $anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 0, priority => "err", key => "log_0020", variables => { method => "Systeme->configure_ipmi()", parameter => "manifest_uuid" }}); + return(0); + } + + # Is this host in an Anvil!? + my $anvil_uuid = ""; + if ((exists $anvil->data->{hosts}{host_uuid}{$host_uuid}) && ($anvil->data->{hosts}{host_uuid}{$host_uuid}{anvil_uuid})) + { + # We're in an Anvil! + $anvil_uuid = $anvil->data->{hosts}{host_uuid}{$host_uuid}{anvil_uuid}; + $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { anvil_uuid => $anvil_uuid }}); + } + else + { + # Not in an Anvil!, return 0. + $anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 1, priority => "err", key => "log_0498"}); + return(0); + } + + # Look for a match in the anvils table for this host uuid. + my $machine = ""; + if ($anvil->data->{anvils}{anvil_uuid}{$anvil_uuid}{anvil_node1_host_uuid} eq $host_uuid) + { + $machine = "node1"; + $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { machine => $machine }}); + } + elsif ($anvil->data->{anvils}{anvil_uuid}{$anvil_uuid}{anvil_node2_host_uuid} eq $host_uuid) + { + $machine = "node2"; + $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { machine => $machine }}); + } + + if (not $machine) + { + # Look for a job for 'anvil-join-anvil' for this host. With it, we'll figure out the password + # and which machine we are. + my $query = " SELECT job_uuid, job_data @@ -1981,56 +2061,109 @@ ORDER BY modified_date DESC LIMIT 1 ;"; - $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { query => $query }}); - my $results = $anvil->Database->query({query => $query, source => $THIS_FILE, line => __LINE__}); - my $count = @{$results}; - $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { - results => $results, - count => $count, - }}); - my $job_uuid = defined $results->[0]->[0] ? $results->[0]->[0] : ""; - my $job_data = defined $results->[0]->[1] ? $results->[0]->[1] : ""; - $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { - job_uuid => $job_uuid, - job_data => $anvil->Log->is_secure($job_data), - }}); - if (not $job_uuid) + $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { query => $query }}); + my $results = $anvil->Database->query({query => $query, source => $THIS_FILE, line => __LINE__}); + my $count = @{$results}; + $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { + results => $results, + count => $count, + }}); + my $job_uuid = defined $results->[0]->[0] ? $results->[0]->[0] : ""; + my $job_data = defined $results->[0]->[1] ? $results->[0]->[1] : ""; + $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { + job_uuid => $job_uuid, + job_data => $anvil->Log->is_secure($job_data), + }}); + if (not $job_uuid) + { + # Unable to proceed. + $anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 1, priority => "err", key => "log_0501"}); + return(0); + } + + ($machine, $manifest_uuid, $anvil_uuid) = ($job_data =~ /as_machine=(.*?),manifest_uuid=(.*?),anvil_uuid=(.*?)$/); + $machine = "" if not defined $machine; + $manifest_uuid = "" if not defined $manifest_uuid; + $anvil_uuid = "" if not defined $anvil_uuid; + $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { + machine => $machine, + manifest_uuid => $manifest_uuid, + anvil_uuid => $anvil_uuid, + }}); + } + + # Load the manifest. + my $problem = $anvil->Striker->load_manifest({debug => $debug, manifest_uuid => $manifest_uuid}); + $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { problem => $problem }}); + if ($problem) { - # Unable to proceed. - $anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 1, priority => "err", key => "log_0501"}); + # The load_manifest method would log the details. return(0); } - ($machine, $manifest_uuid, $anvil_uuid) = ($job_data =~ /as_machine=(.*?),manifest_uuid=(.*?),anvil_uuid=(.*?)$/); - $machine = "" if not defined $machine; - $manifest_uuid = "" if not defined $manifest_uuid; - $anvil_uuid = "" if not defined $anvil_uuid; + # Make sure the IPMI IP, subnet mask and password are available. + $ipmi_ip_address = $anvil->data->{manifests}{manifest_uuid}{$manifest_uuid}{parsed}{machine}{$machine}{ipmi_ip}; + $ipmi_password = $anvil->data->{anvils}{anvil_uuid}{$anvil_uuid}{anvil_password}; + $password_length = length(Encode::encode('UTF-8', $ipmi_password)); $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { - machine => $machine, - manifest_uuid => $manifest_uuid, - anvil_uuid => $anvil_uuid, + ipmi_ip_address => $ipmi_ip_address, + ipmi_password => $anvil->Log->is_secure($ipmi_password), + password_length => $password_length, }}); + + # Find the subnet the IPMI IP is in. + foreach my $network_type ("bcn", "ifn", "sn") + { + my $count = $anvil->data->{manifests}{manifest_uuid}{$manifest_uuid}{parsed}{networks}{count}{$network_type}; + $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { + network_type => $network_type, + count => $count, + }}); + foreach my $i (1..$count) + { + my $network_name = $network_type.$i; + my $network = $anvil->data->{manifests}{manifest_uuid}{$manifest_uuid}{parsed}{networks}{name}{$network_name}{network}; + my $this_subnet_mask = $anvil->data->{manifests}{manifest_uuid}{$manifest_uuid}{parsed}{networks}{name}{$network_name}{subnet}; + my $this_gateway = $anvil->data->{manifests}{manifest_uuid}{$manifest_uuid}{parsed}{networks}{name}{$network_name}{gateway}; + $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { + network_name => $network_name, + network => $network, + this_subnet_mask => $this_subnet_mask, + this_gateway => $this_gateway, + }}); + + my $match = $anvil->Network->is_ip_in_network({ + network => $network, + subnet_mask => $this_subnet_mask, + ip => $ipmi_ip_address, + }); + $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { network_name => $match}}); + if ($match) + { + $subnet_mask = $this_subnet_mask; + $gateway = $this_gateway; + $in_network = $network_name; + $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { + subnet_mask => $subnet_mask, + gateway => $gateway, + in_network => $in_network, + }}); + last; + } + } + } + + # If we didn't find a network, we're done. + if (not $subnet_mask) + { + $anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 1, priority => "err", key => "log_0502", variables => { + ip_address => $ipmi_ip_address, + manifest_uuid => $manifest_uuid, + }}); + return(0); + } } - # Load the manifest. - my $problem = $anvil->Striker->load_manifest({debug => $debug, manifest_uuid => $manifest_uuid}); - $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { problem => $problem }}); - if ($problem) - { - # The load_manifest method would log the details. - return(0); - } - - # Make sure the IPMI IP, subnet mask and password are available. - my $ipmi_ip_address = $anvil->data->{manifests}{manifest_uuid}{$manifest_uuid}{parsed}{machine}{$machine}{ipmi_ip}; - my $ipmi_password = $anvil->data->{anvils}{anvil_uuid}{$anvil_uuid}{anvil_password}; - my $password_length = length(Encode::encode('UTF-8', $ipmi_password)); - $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { - ipmi_ip_address => $ipmi_ip_address, - ipmi_password => $anvil->Log->is_secure($ipmi_password), - password_length => $password_length, - }}); - # If the password has spaces, some IPMI BMCs won't allow them. If we need to use it, we'll take out # the spaces and shrink the length. my $ipmi_no_space_password = ""; @@ -2041,61 +2174,7 @@ LIMIT 1 $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, secure => 1, list => { ipmi_no_space_password => $ipmi_no_space_password }}); } - my $subnet_mask = ""; - my $gateway = ""; - my $in_network = ""; - # Find the subnet the IPMI IP is in. - foreach my $network_type ("bcn", "ifn", "sn") - { - my $count = $anvil->data->{manifests}{manifest_uuid}{$manifest_uuid}{parsed}{networks}{count}{$network_type}; - $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { - network_type => $network_type, - count => $count, - }}); - foreach my $i (1..$count) - { - my $network_name = $network_type.$i; - my $network = $anvil->data->{manifests}{manifest_uuid}{$manifest_uuid}{parsed}{networks}{name}{$network_name}{network}; - my $this_subnet_mask = $anvil->data->{manifests}{manifest_uuid}{$manifest_uuid}{parsed}{networks}{name}{$network_name}{subnet}; - my $this_gateway = $anvil->data->{manifests}{manifest_uuid}{$manifest_uuid}{parsed}{networks}{name}{$network_name}{gateway}; - $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { - network_name => $network_name, - network => $network, - this_subnet_mask => $this_subnet_mask, - this_gateway => $this_gateway, - }}); - - my $match = $anvil->Network->is_ip_in_network({ - network => $network, - subnet_mask => $this_subnet_mask, - ip => $ipmi_ip_address, - }); - $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { network_name => $match}}); - if ($match) - { - $subnet_mask = $this_subnet_mask; - $gateway = $this_gateway; - $in_network = $network_name; - $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { - subnet_mask => $subnet_mask, - gateway => $gateway, - in_network => $in_network, - }}); - last; - } - } - } - - # If we didn't find a network, we're done. - if (not $subnet_mask) - { - $anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 1, priority => "err", key => "log_0502", variables => { - ip_address => $ipmi_ip_address, - manifest_uuid => $manifest_uuid, - }}); - return(0); - } # Call dmidecode to see if there even is an IPMI BMC on this host. my $host_ipmi = ""; diff --git a/share/words.xml b/share/words.xml index 86088c2a..c7783bb9 100644 --- a/share/words.xml +++ b/share/words.xml @@ -3582,6 +3582,7 @@ The error was: [ Warning ] - The interface: [#!variable!interface!#] appears to be down (state: [#!variable!state!#]). The system uptime is: [#!variable!uptime!#], so it might be a problem where the interface didn't start on boot as it should have. So we're going to bring the interface up. [ Warning ] - The IPMI stonith resource: [#!variable!resource!#] is in the role: [#!variable!role!#] (should be 'Started'). Will check the IPMI config now. + [ Warning ] - Failed to find a valid IP address or password to be used to setup the DR host's IPMI. From 9bf0f50084e951df6d7670fa38639f79e41bee1a Mon Sep 17 00:00:00 2001 From: digimer Date: Sun, 9 Apr 2023 23:38:39 -0400 Subject: [PATCH 82/85] Added a check to see if the server's UUID exists and looping if not to prevent unitialized variable warnings. Signed-off-by: digimer --- tools/anvil-provision-server | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tools/anvil-provision-server b/tools/anvil-provision-server index 55132889..fce6076f 100755 --- a/tools/anvil-provision-server +++ b/tools/anvil-provision-server @@ -394,6 +394,12 @@ sub write_definition $anvil->Database->get_servers(); $anvil->Database->get_server_definitions(); + if (not exists $anvil->data->{servers}{anvil_uuid}{$anvil_uuid}{server_name}{$server_name}{server_uuid}) + { + sleep 1; + next; + } + my $server_uuid = $anvil->data->{servers}{anvil_uuid}{$anvil_uuid}{server_name}{$server_name}{server_uuid}; my $server_definition = exists $anvil->data->{server_definitions}{server_definition_server_uuid}{$server_uuid} ? $anvil->data->{server_definitions}{server_definition_server_uuid}{$server_uuid}{server_definition_xml} : ""; $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { server_definition => $server_definition }}); From f9689a71067e1eefb1804fafe248fbec4b557ac8 Mon Sep 17 00:00:00 2001 From: digimer Date: Mon, 10 Apr 2023 17:40:46 -0400 Subject: [PATCH 83/85] Updated ocf:alteeve:server to look for /tmp/.fail' and, if that file exists, exits with rc:1. This is done to allow for testing. Signed-off-by: digimer --- Anvil/Tools/Cluster.pm | 5 +++-- notes | 2 ++ ocf/alteeve/server | 12 ++++++++++++ share/words.xml | 1 + 4 files changed, 18 insertions(+), 2 deletions(-) diff --git a/Anvil/Tools/Cluster.pm b/Anvil/Tools/Cluster.pm index 278962dd..26f0a055 100644 --- a/Anvil/Tools/Cluster.pm +++ b/Anvil/Tools/Cluster.pm @@ -863,9 +863,10 @@ sub check_node_status =head2 check_server_constraints -This method checks to see if the peer node is offline, and the local node is only. If this is the case, the location constraints for servers are checked to ensure that they favour the current host. If not, the location constraint is updated. +This checks to see if the constraints on a server are sane. Specifically; -This is meant to be used to prevent servers from automatically migrating back to a node after it was fenced. +* If the server is on a sub-node and the peer is offline, ensure that the location constraints prefer the current host. This prevents migrations back to the old host. +* Check to see if a DRBD resource constriant was applied against a node, and the node's DRBD resource is UpToDate. If so, remove the constraint. This method takes no parameters. diff --git a/notes b/notes index 745044cc..510ef61c 100644 --- a/notes +++ b/notes @@ -17,6 +17,8 @@ Common queries; * SELECT a.host_name, b.file_name, c.file_location_active FROM hosts a, files b, file_locations c WHERE a.host_uuid = c.file_location_host_uuid AND b.file_uuid = c.file_location_file_uuid ORDER BY b.file_name ASC, a.host_name ASC; * SELECT a.dr_link_uuid, b.host_name, c.anvil_name, a.dr_link_note FROM dr_links a, hosts b, anvils c WHERE a.dr_link_host_uuid = b.host_uuid AND a.dr_link_anvil_uuid = c.anvil_uuid ORDER BY c.anvil_name ASC, b.host_name ASC; +# Fail a resource for testing purposes. +crm_resource --fail --resource srv02-b -N vm-a01n01 uname -r; grubby --default-kernel; lsinitrd -m /boot/initramfs-4.18.0-448.el8.x86_64.img | grep lvm; systemctl is-enabled scancore.service; dnf -y update; systemctl disable --now anvil-daemon; systemctl disable --now scancore diff --git a/ocf/alteeve/server b/ocf/alteeve/server index 137a3e26..8e9b9842 100755 --- a/ocf/alteeve/server +++ b/ocf/alteeve/server @@ -256,6 +256,18 @@ if (not $anvil->data->{switches}{monitor}) show_environment($anvil, 3); } +# If there's a test fail file, return '1' to cause pacemaker to fail this resource. +if ($anvil->data->{environment}{OCF_RESKEY_name}) +{ + my $test_fail_file = "/tmp/".$anvil->data->{environment}{OCF_RESKEY_name}.".fail"; + $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { test_fail_file => $test_fail_file }}); + if (-e $test_fail_file) + { + $anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 1, priority => "alert", key => "warning_0150", variables => { fail_file => $test_fail_file }}); + $anvil->nice_exit({exit_code => 1}); + } +} + ### What are we being asked to do? # start  - Starts the resource. # stop  - Shuts down the resource. diff --git a/share/words.xml b/share/words.xml index c7783bb9..3b9d0fe5 100644 --- a/share/words.xml +++ b/share/words.xml @@ -3583,6 +3583,7 @@ The error was: [ Warning ] - The interface: [#!variable!interface!#] appears to be down (state: [#!variable!state!#]). The system uptime is: [#!variable!uptime!#], so it might be a problem where the interface didn't start on boot as it should have. So we're going to bring the interface up. [ Warning ] - The IPMI stonith resource: [#!variable!resource!#] is in the role: [#!variable!role!#] (should be 'Started'). Will check the IPMI config now. [ Warning ] - Failed to find a valid IP address or password to be used to setup the DR host's IPMI. + [ Warning ] - The test "fail file": [#!variable!fail_file!#] was found. So long as this file exists, the ocf:alteeve:server RA will return 'OCF_ERR_GENERIC' (exit code 1). Delete the file to resume normal operation. From 1afa7ce09e3fcdd738818b8a4ab7052d3a7fe415 Mon Sep 17 00:00:00 2001 From: digimer Date: Mon, 10 Apr 2023 23:04:15 -0400 Subject: [PATCH 84/85] * Created Cluster->recover_server() that uses crm_resource to try to recover a server that has entered a FAILED state. * Updated (not not yet completed) scan-cluster's check_resources() function to check if a FAILED server is ready to try to recover. Signed-off-by: digimer --- Anvil/Tools/Cluster.pm | 44 ++++++++++ notes | 4 + scancore-agents/scan-cluster/scan-cluster | 99 +++++++++++++++++++++++ 3 files changed, 147 insertions(+) diff --git a/Anvil/Tools/Cluster.pm b/Anvil/Tools/Cluster.pm index 26f0a055..16c39af1 100644 --- a/Anvil/Tools/Cluster.pm +++ b/Anvil/Tools/Cluster.pm @@ -35,6 +35,7 @@ my $THIS_FILE = "Cluster.pm"; # parse_cib # parse_crm_mon # parse_quorum +# recover_server # shutdown_server # start_cluster # which_node @@ -4268,6 +4269,49 @@ sub parse_quorum } +=head2 recover_server + +This tries to recover a C<< FAILED >> resource (server). + +Parameters; + +=head3 server_ (required) + +This is the server (resource) name to try to recover. + +=cut +sub recover_server +{ + my $self = shift; + my $parameter = shift; + my $anvil = $self->parent; + my $debug = defined $parameter->{debug} ? $parameter->{debug} : 3; + $anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => $debug, key => "log_0125", variables => { method => "Cluster->recover_server()" }}); + + my $server = defined $parameter->{server} ? $parameter->{server} : ""; + $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { + server => $server, + }}); + + if (not $server) + { + $anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => 0, priority => "err", key => "log_0020", variables => { method => "Cluster->recover_server()", parameter => "server" }}); + return("!!error!!"); + } + + my $shell_call = $anvil->data->{path}{exe}{crm_resource}." --resource ".$server." --refresh"; + $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { shell_call => $shell_call }}); + + my ($output, $return_code) = $anvil->System->call({debug => $debug, shell_call => $shell_call}); + $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { + output => $output, + return_code => $return_code, + }}); + + return(0); +} + + =head2 shutdown_server This shuts down a server that is running on the Anvil! system. If there is a problem, C<< !!error!! >> is returned. On success, C<< 0 >> is returned. diff --git a/notes b/notes index 510ef61c..e302d858 100644 --- a/notes +++ b/notes @@ -20,6 +20,10 @@ Common queries; # Fail a resource for testing purposes. crm_resource --fail --resource srv02-b -N vm-a01n01 +# Recover without reboot +crm_resource --resource srv01-a --refresh + + uname -r; grubby --default-kernel; lsinitrd -m /boot/initramfs-4.18.0-448.el8.x86_64.img | grep lvm; systemctl is-enabled scancore.service; dnf -y update; systemctl disable --now anvil-daemon; systemctl disable --now scancore diff --git a/scancore-agents/scan-cluster/scan-cluster b/scancore-agents/scan-cluster/scan-cluster index 9e13d4d6..125caa7f 100755 --- a/scancore-agents/scan-cluster/scan-cluster +++ b/scancore-agents/scan-cluster/scan-cluster @@ -99,6 +99,9 @@ check_config($anvil); # Check the fence delay check_fence_delay($anvil); +# Check for failed resources +check_resources($anvil); + # Shut down. $anvil->ScanCore->agent_shutdown({agent => $THIS_FILE}); @@ -107,6 +110,102 @@ $anvil->ScanCore->agent_shutdown({agent => $THIS_FILE}); # Functions # ############################################################################################################# +# This looks for failed resource and, if found, tries to recover them. +sub check_resources +{ + my ($anvil) = @_; + + foreach my $server (sort {$a cmp $b} keys %{$anvil->data->{crm_mon}{parsed}{'pacemaker-result'}{resources}{resource}}) + { + my $failed = exists $anvil->data->{crm_mon}{parsed}{'pacemaker-result'}{resources}{resource}{$server}{variables}{failed} ? $anvil->data->{crm_mon}{parsed}{'pacemaker-result'}{resources}{resource}{$server}{variables}{failed} : 0; + if ($failed eq "true") + { + $failed = 1; + } + elsif ($failed eq "false") + { + $failed = 0; + } + print "Server: [".$server."], failed? [".$failed."]\n"; + if ($failed) + { + # Who am I and who is my peer? See if the server is running on either host. + print "- Checking if it's safe to recover!\n"; + my $attempt_recovery = 0; + my $server_found = 0; + my $both_nodes_ready = 1; + foreach my $target ("local", "peer") + { + my $node_ready = $anvil->data->{cib}{parsed}{$target}{ready}; + my $node_name = $anvil->data->{cib}{parsed}{$target}{name}; + my $host_uuid = $anvil->Get->host_uuid_from_name({host_name => $node_name}); + print "- Searching node: [".$node_name." (".$host_uuid.")] which is in ready state: [".$node_ready."]\n"; + if (not $node_ready) + { + $both_nodes_ready = 1; + } + + if ($host_uuid eq $anvil->Get->host_uuid) + { + # Search for the server here + $anvil->Server->find({debug => 2}); + print "- Searching for the server on the local system.\n"; + } + else + { + # Search for the server on the peer. + my $target_ip = $anvil->Network->find_target_ip({host_uuid => $host_uuid}); + print "- Searching for the server on the peer using IP: [".$target_ip."]\n"; + $anvil->Server->find({ + debug => 2, + target => $target_ip, + }); + } + my $server_host = defined $anvil->data->{server}{location}{$server}{host_name} ? $anvil->data->{server}{location}{$server}{host_name} : ""; + my $server_status = defined $anvil->data->{server}{location}{$server}{status} ? $anvil->data->{server}{location}{$server}{status} : ""; + $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { + server_host => $server_host, + server_status => $server_status, + }}); + print "- Host: [".$server_host."], status: [".$server_status."]\n"; + if ($server_host) + { + $server_found = 1; + $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { server_found => $server_found }}); + if (($node_ready) && ($host_uuid eq $anvil->Get->host_uuid)) + { + # Go ahead with recovery + print "The server is running locally and we're a full cluster member. Will attempt recover.\n"; + $attempt_recovery = 1; + $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { attempt_recovery => $attempt_recovery }}); + } + } + } + + if ((not $server_found) && ($both_nodes_ready)) + { + print "Both nodes are up and the server wasn't found anywhere. Attempting recovery.\n"; + $attempt_recovery = 1; + $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { attempt_recovery => $attempt_recovery }}); + } + elsif (($server_found) && (not $attempt_recovery)) + { + print "The server was found to be running, but not here (or this node is not fully in the cluster). NOT attempting recovery yet.\n"; + } + elsif ($attempt_recovery) + { + print "Attempting recovery now...\n"; + $anvil->Cluster->recover_server({ + debug => 2, + server => $server, + }); + } + } + } + + return(0); +} + # Check to see if we need to move the fence delay. sub check_fence_delay { From 83aa4e6a5f37a4a3f5cacab787b2182589895297 Mon Sep 17 00:00:00 2001 From: digimer Date: Tue, 11 Apr 2023 16:32:31 -0400 Subject: [PATCH 85/85] Updated scan-cluster to check for FAILED resources (servers) and, if found, attempt to recover it. Signed-off-by: digimer --- scancore-agents/scan-cluster/scan-cluster | 129 +++++++++++++++--- scancore-agents/scan-cluster/scan-cluster.xml | 12 ++ 2 files changed, 121 insertions(+), 20 deletions(-) diff --git a/scancore-agents/scan-cluster/scan-cluster b/scancore-agents/scan-cluster/scan-cluster index 125caa7f..485dd070 100755 --- a/scancore-agents/scan-cluster/scan-cluster +++ b/scancore-agents/scan-cluster/scan-cluster @@ -117,20 +117,18 @@ sub check_resources foreach my $server (sort {$a cmp $b} keys %{$anvil->data->{crm_mon}{parsed}{'pacemaker-result'}{resources}{resource}}) { - my $failed = exists $anvil->data->{crm_mon}{parsed}{'pacemaker-result'}{resources}{resource}{$server}{variables}{failed} ? $anvil->data->{crm_mon}{parsed}{'pacemaker-result'}{resources}{resource}{$server}{variables}{failed} : 0; - if ($failed eq "true") - { - $failed = 1; - } - elsif ($failed eq "false") - { - $failed = 0; - } - print "Server: [".$server."], failed? [".$failed."]\n"; + # This is used for alerts, if needed below. + my $variables = { server => $server }; + + my $failed = check_if_server_failed($anvil, $server); + $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { + server => $server, + failed => $failed, + }}); if ($failed) { # Who am I and who is my peer? See if the server is running on either host. - print "- Checking if it's safe to recover!\n"; + $anvil->Log->entry({source => $THIS_FILE, line => __LINE__, 'print' => 1, level => 1, key => "scan_cluster_log_0003", variables => { server => $server }}); my $attempt_recovery = 0; my $server_found = 0; my $both_nodes_ready = 1; @@ -139,7 +137,11 @@ sub check_resources my $node_ready = $anvil->data->{cib}{parsed}{$target}{ready}; my $node_name = $anvil->data->{cib}{parsed}{$target}{name}; my $host_uuid = $anvil->Get->host_uuid_from_name({host_name => $node_name}); - print "- Searching node: [".$node_name." (".$host_uuid.")] which is in ready state: [".$node_ready."]\n"; + $anvil->Log->entry({source => $THIS_FILE, line => __LINE__, 'print' => 1, level => 1, key => "scan_cluster_log_0004", variables => { + node_name => $node_name, + host_uuid => $host_uuid, + node_ready => $node_ready, + }}); if (not $node_ready) { $both_nodes_ready = 1; @@ -149,13 +151,13 @@ sub check_resources { # Search for the server here $anvil->Server->find({debug => 2}); - print "- Searching for the server on the local system.\n"; + $anvil->Log->entry({source => $THIS_FILE, line => __LINE__, 'print' => 1, level => 1, key => "scan_cluster_log_0005"}); } else { # Search for the server on the peer. my $target_ip = $anvil->Network->find_target_ip({host_uuid => $host_uuid}); - print "- Searching for the server on the peer using IP: [".$target_ip."]\n"; + $anvil->Log->entry({source => $THIS_FILE, line => __LINE__, 'print' => 1, level => 1, key => "scan_cluster_log_0006", variables => { target_ip => $target_ip }}); $anvil->Server->find({ debug => 2, target => $target_ip, @@ -163,11 +165,10 @@ sub check_resources } my $server_host = defined $anvil->data->{server}{location}{$server}{host_name} ? $anvil->data->{server}{location}{$server}{host_name} : ""; my $server_status = defined $anvil->data->{server}{location}{$server}{status} ? $anvil->data->{server}{location}{$server}{status} : ""; - $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { + $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 1, list => { server_host => $server_host, server_status => $server_status, }}); - print "- Host: [".$server_host."], status: [".$server_status."]\n"; if ($server_host) { $server_found = 1; @@ -175,7 +176,7 @@ sub check_resources if (($node_ready) && ($host_uuid eq $anvil->Get->host_uuid)) { # Go ahead with recovery - print "The server is running locally and we're a full cluster member. Will attempt recover.\n"; + $anvil->Log->entry({source => $THIS_FILE, line => __LINE__, 'print' => 1, level => 1, key => "scan_cluster_log_0007"}); $attempt_recovery = 1; $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { attempt_recovery => $attempt_recovery }}); } @@ -184,21 +185,87 @@ sub check_resources if ((not $server_found) && ($both_nodes_ready)) { - print "Both nodes are up and the server wasn't found anywhere. Attempting recovery.\n"; + $anvil->Log->entry({source => $THIS_FILE, line => __LINE__, 'print' => 1, level => 1, key => "scan_cluster_log_0008"}); $attempt_recovery = 1; $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { attempt_recovery => $attempt_recovery }}); } elsif (($server_found) && (not $attempt_recovery)) { - print "The server was found to be running, but not here (or this node is not fully in the cluster). NOT attempting recovery yet.\n"; + # The server was found to be running, but not here (or this node is not fully in the cluster). NOT attempting recovery yet. + $anvil->Log->entry({source => $THIS_FILE, line => __LINE__, 'print' => 1, level => 1, key => "scan_cluster_log_0009"}); } elsif ($attempt_recovery) { - print "Attempting recovery now...\n"; + $anvil->Log->entry({source => $THIS_FILE, line => __LINE__, 'print' => 1, level => 1, key => "scan_cluster_log_0010"}); $anvil->Cluster->recover_server({ debug => 2, server => $server, }); + + # It'll leave 'failed state' for a bit, so we need to wait. + sleep 3; + my $wait_until = time + 10; + my $waiting = 1; + $anvil->Log->entry({source => $THIS_FILE, line => __LINE__, 'print' => 1, level => 1, key => "scan_cluster_log_0011"}); + while($waiting) + { + my $failed = check_if_server_failed($anvil, $server); + $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { failed => $failed }}); + if ($failed) + { + # No luck... + $waiting = 0; + my $changed = $anvil->Alert->check_alert_sent({ + record_locator => "scan_cluster::failed_server::".$server, + set_by => $THIS_FILE, + }); + $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { + waiting => $waiting, + changed => $changed, + }}); + $anvil->Log->entry({source => $THIS_FILE, line => __LINE__, 'print' => 1, level => 1, key => "scan_cluster_alert_0014", variables => $variables}); + if ($changed) + { + # Send an alert. + $anvil->Alert->register({debug => 2, alert_level => "notice", message => "scan_cluster_alert_0014", variables => $variables, set_by => $THIS_FILE}); + } + } + elsif (time > $wait_until) + { + # Success! + $anvil->Log->entry({source => $THIS_FILE, line => __LINE__, 'print' => 1, level => 1, key => "scan_cluster_alert_0013", variables => $variables}); + $anvil->Alert->register({debug => 2, alert_level => "notice", message => "scan_cluster_alert_0013", variables => $variables, set_by => $THIS_FILE}); + + # Clear the alert, if it existed before + $waiting = 0; + my $changed = $anvil->Alert->check_alert_sent({ + record_locator => "scan_cluster::failed_server::".$server, + set_by => $THIS_FILE, + clear => 1, + }); + } + else + { + # Wait a sec + sleep 2; + } + } + } + } + else + { + # Make sure that this server wasn't previously failed. + my $changed = $anvil->Alert->check_alert_sent({ + record_locator => "scan_cluster::failed_server::".$server, + set_by => $THIS_FILE, + clear => 1, + }); + $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { changed => $changed }}); + if ($changed) + { + # Send the All-good alert. + $anvil->Log->entry({source => $THIS_FILE, line => __LINE__, 'print' => 1, level => 1, key => "scan_cluster_alert_0015", variables => $variables}); + $anvil->Alert->register({debug => 2, alert_level => "notice", message => "scan_cluster_alert_0015", variables => $variables, set_by => $THIS_FILE}); } } } @@ -206,6 +273,28 @@ sub check_resources return(0); } +sub check_if_server_failed +{ + my ($anvil, $server) = @_; + $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 3, list => { server => $server }}); + + $anvil->Cluster->parse_crm_mon({debug => 3}); + my $failed = exists $anvil->data->{crm_mon}{parsed}{'pacemaker-result'}{resources}{resource}{$server}{variables}{failed} ? $anvil->data->{crm_mon}{parsed}{'pacemaker-result'}{resources}{resource}{$server}{variables}{failed} : 0; + $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 3, list => { failed => $failed }}); + if ($failed eq "true") + { + $failed = 1; + $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 3, list => { failed => $failed }}); + } + elsif ($failed eq "false") + { + $failed = 0; + $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 3, list => { failed => $failed }}); + } + + return($failed); +} + # Check to see if we need to move the fence delay. sub check_fence_delay { diff --git a/scancore-agents/scan-cluster/scan-cluster.xml b/scancore-agents/scan-cluster/scan-cluster.xml index 49c2b3e6..15063008 100644 --- a/scancore-agents/scan-cluster/scan-cluster.xml +++ b/scancore-agents/scan-cluster/scan-cluster.xml @@ -37,10 +37,22 @@ In Maintenance Mode: ..... [#!variable!maintenance_mode!#] #!variable!difference!# ==== + The server: [#!variable!server!#] was found to be failed in pacemaker, but it was successfully recovered. This does NOT mean the server rebooted, but it may have. Checking the server is advised. + The server: [#!variable!server!#] was found to be failed in pacemaker. The attempt to recover it appears to have failed. The server might well still be running ok, checking the server is advised. + The server: [#!variable!server!#] had been found to be failed in pacemaker. It's now recovered. This does NOT mean the server rebooted, but it may have. Checking the server is advised. Starting: [#!variable!program!#]. This host is a: [#!variable!host_type!#], this agent is only useful on nodes. Exiting. + [ Warning ] - The server: [#!variable!server!#] is in a FAILED state! Checking to see if it's safe to attempt recovery. + Searching node: [#!variable!node_name!# (#!variable!host_uuid!#] which is in ready state: [#!variable!node_ready!#]. + Searching for the server on the local system. + Searching for the server on the peer using IP: [#!variable!target_ip!#]. + The server is running locally and we're a full cluster member. Will attempt recover. + Both nodes are up and the server wasn't found anywhere. Attempting recovery. + The server was found to be running, but not here (or this node is not fully in the cluster). NOT attempting recovery yet. + Attempting recovery now... + Checking to see if the server has recovered yet...