From 5f8eca322571664254e9d5af380fd6a0382849ea Mon Sep 17 00:00:00 2001 From: Rico Lin Date: Fri, 14 Aug 2026 17:10:15 +0800 Subject: [PATCH] feat(ironic): harden graphical console input Add provider-neutral keyboard pacing, diagnostics, and timing controls to dynamic console Pods. Pass the controls to the backend selected through the centralized Atmosphere image catalog. Change-Id: I192c715235aff1f0ebcb36f5db4b9c7415b0d859 Signed-off-by: Rico Lin Assisted-By: Codex --- ...c-console-key-timing-9d58b6bce4a7f120.yaml | 14 ++++++ roles/ironic/README.md | 14 ++++-- roles/ironic/defaults/main.yml | 4 ++ roles/ironic/tasks/main.yml | 7 +++ roles/ironic/vars/main.yml | 15 +++++- roles/ironic/vars_test.go | 48 ++++++++++++++++--- 6 files changed, 90 insertions(+), 12 deletions(-) create mode 100644 releasenotes/notes/configure-ironic-console-key-timing-9d58b6bce4a7f120.yaml diff --git a/releasenotes/notes/configure-ironic-console-key-timing-9d58b6bce4a7f120.yaml b/releasenotes/notes/configure-ironic-console-key-timing-9d58b6bce4a7f120.yaml new file mode 100644 index 0000000000..69be6017ef --- /dev/null +++ b/releasenotes/notes/configure-ironic-console-key-timing-9d58b6bce4a7f120.yaml @@ -0,0 +1,14 @@ +--- +features: + - | + Adds configurable keyboard mode, diagnostics, printable-key hold, and + interval settings for Ironic graphical console containers. The selected + ``atmosphere_images.ironic_console`` image must support the + ``KEYBOARD_MODE``, ``KEYBOARD_DIAGNOSTICS``, ``PRINTABLE_KEY_HOLD``, and + ``PRINTABLE_KEY_INTERVAL`` environment variables. Operators can increase + the timing defaults for BMC KVM implementations that drop short synthetic + key taps. + + Optional diagnostics record only aggregate input counts at session close, + without key values, text, client addresses, console URLs, or tokens. They + are disabled by default. diff --git a/roles/ironic/README.md b/roles/ironic/README.md index 6468befd57..6a8a63cace 100644 --- a/roles/ironic/README.md +++ b/roles/ironic/README.md @@ -4,7 +4,9 @@ Graphical console support is disabled by default. Enable it and provide an immutable console backend image that implements Ironic's `APP`, `APP_INFO`, and -`READ_ONLY` environment contract and listens for RFB connections on port 5900: +`READ_ONLY` environment contract and listens for RFB connections on port 5900. +The selected image must also support `KEYBOARD_MODE`, `KEYBOARD_DIAGNOSTICS`, +`PRINTABLE_KEY_HOLD`, and `PRINTABLE_KEY_INTERVAL`: ```yaml ironic_vnc_enabled: true @@ -75,9 +77,13 @@ the corresponding explicit override. which authenticates against the Redfish system resource and probes the AMI KVM path without opening a graphical session. Connection, handshake, idle, session, console-token, global-client, and per-source limits are role -variables. `ironic_vnc_token_timeout` defaults to Ironic's -600-second lifetime; increase it only when the additional bearer-token exposure -is acceptable for the operator workflow. Disabling +variables. Set `ironic_vnc_keyboard_diagnostics: true` to log one +aggregate message, key-down, key-up, and pointer count when each writable +console session closes. The diagnostic does not record key values, text, +client addresses, console URLs, or tokens, and is disabled by default. +`ironic_vnc_token_timeout` defaults to Ironic's 600-second +lifetime; increase it only when the additional bearer-token exposure is +acceptable for the operator workflow. Disabling the feature or changing `ironic_vnc_namespace` first disables affected node consoles and waits for their Pods and Secrets to disappear. Feature disable also resets nodes from `redfish-graphical` to `no-console` diff --git a/roles/ironic/defaults/main.yml b/roles/ironic/defaults/main.yml index 9f6ce9e1c1..d4413fe881 100644 --- a/roles/ironic/defaults/main.yml +++ b/roles/ironic/defaults/main.yml @@ -54,6 +54,10 @@ ironic_vnc_session_timeout: 14400 ironic_vnc_readiness_interval: 30 ironic_vnc_max_clients: 8 ironic_vnc_max_clients_per_source: 2 +ironic_vnc_keyboard_mode: paced-tap +ironic_vnc_keyboard_diagnostics: false +ironic_vnc_printable_key_hold: 0.020 +ironic_vnc_printable_key_interval: 0.035 ironic_vnc_cloud: system # Optional driver/interface overrides. Keep hardware types unset to use diff --git a/roles/ironic/tasks/main.yml b/roles/ironic/tasks/main.yml index 89d17e0eac..4b54f97c74 100644 --- a/roles/ironic/tasks/main.yml +++ b/roles/ironic/tasks/main.yml @@ -45,6 +45,13 @@ - >- ironic_vnc_max_clients_per_source | int <= ironic_vnc_max_clients | int + - >- + ironic_vnc_keyboard_mode + in ['passthrough', 'paced-tap'] + - ironic_vnc_printable_key_hold | float > 0 + - ironic_vnc_printable_key_hold | float <= 1 + - ironic_vnc_printable_key_interval | float > 0 + - ironic_vnc_printable_key_interval | float <= 1 fail_msg: >- Ironic graphical console support requires a digest-pinned atmosphere_image_overrides.ironic_console image, an isolated Kubernetes diff --git a/roles/ironic/vars/main.yml b/roles/ironic/vars/main.yml index 591db3cefd..97a742fafd 100644 --- a/roles/ironic/vars/main.yml +++ b/roles/ironic/vars/main.yml @@ -66,7 +66,20 @@ _ironic_helm_values: {'name': 'SESSION_TIMEOUT', 'value': ironic_vnc_session_timeout | string}, {'name': 'READINESS_INTERVAL', 'value': ironic_vnc_readiness_interval | string}, {'name': 'MAX_CLIENTS', 'value': ironic_vnc_max_clients | string}, - {'name': 'MAX_CLIENTS_PER_SOURCE', 'value': ironic_vnc_max_clients_per_source | string} + {'name': 'MAX_CLIENTS_PER_SOURCE', 'value': ironic_vnc_max_clients_per_source | string}, + {'name': 'KEYBOARD_MODE', 'value': ironic_vnc_keyboard_mode | string}, + { + 'name': 'KEYBOARD_DIAGNOSTICS', + 'value': ironic_vnc_keyboard_diagnostics | bool | string | lower + }, + { + 'name': 'PRINTABLE_KEY_HOLD', + 'value': ironic_vnc_printable_key_hold | float | string + }, + { + 'name': 'PRINTABLE_KEY_INTERVAL', + 'value': ironic_vnc_printable_key_interval | float | string + } ] + ( [{'name': 'BMC_TLS_CIPHERS', 'value': ironic_vnc_tls_ciphers | string}] diff --git a/roles/ironic/vars_test.go b/roles/ironic/vars_test.go index 1428f58194..a086771c68 100644 --- a/roles/ironic/vars_test.go +++ b/roles/ironic/vars_test.go @@ -30,13 +30,17 @@ type Vars struct { } type Defaults struct { - BaremetalConsoleEnabled bool `yaml:"ironic_vnc_enabled"` - BaremetalConsoleTokenTimeout int `yaml:"ironic_vnc_token_timeout"` - BaremetalConsoleAllowInsecureTLS bool `yaml:"ironic_vnc_allow_insecure_tls"` - BaremetalConsoleCASecretName string `yaml:"ironic_vnc_ca_secret_name"` - BaremetalConsoleCASecretKey string `yaml:"ironic_vnc_ca_secret_key"` - BaremetalConsoleCAMountPath string `yaml:"ironic_vnc_ca_mount_path"` - BaremetalConsoleTLSMinimumVersion string `yaml:"ironic_vnc_tls_minimum_version"` + BaremetalConsoleEnabled bool `yaml:"ironic_vnc_enabled"` + BaremetalConsoleKeyboardMode string `yaml:"ironic_vnc_keyboard_mode"` + BaremetalConsoleKeyboardDiagnostics bool `yaml:"ironic_vnc_keyboard_diagnostics"` + BaremetalConsolePrintableKeyHold float64 `yaml:"ironic_vnc_printable_key_hold"` + BaremetalConsolePrintableKeyInterval float64 `yaml:"ironic_vnc_printable_key_interval"` + BaremetalConsoleTokenTimeout int `yaml:"ironic_vnc_token_timeout"` + BaremetalConsoleAllowInsecureTLS bool `yaml:"ironic_vnc_allow_insecure_tls"` + BaremetalConsoleCASecretName string `yaml:"ironic_vnc_ca_secret_name"` + BaremetalConsoleCASecretKey string `yaml:"ironic_vnc_ca_secret_key"` + BaremetalConsoleCAMountPath string `yaml:"ironic_vnc_ca_mount_path"` + BaremetalConsoleTLSMinimumVersion string `yaml:"ironic_vnc_tls_minimum_version"` } func TestMain(m *testing.M) { @@ -121,9 +125,39 @@ func TestBaremetalConsoleDefaults(t *testing.T) { require.NoError(t, err) require.False(t, defaults.BaremetalConsoleEnabled) + require.Equal(t, "paced-tap", defaults.BaremetalConsoleKeyboardMode) + require.False(t, defaults.BaremetalConsoleKeyboardDiagnostics) + require.Equal(t, 0.020, defaults.BaremetalConsolePrintableKeyHold) + require.Equal(t, 0.035, defaults.BaremetalConsolePrintableKeyInterval) require.Equal(t, 600, defaults.BaremetalConsoleTokenTimeout) } +func TestBaremetalConsoleKeyboardEnvironment(t *testing.T) { + var values struct { + HelmValues struct { + Console struct { + Container struct { + Env string `yaml:"env"` + } `yaml:"container"` + } `yaml:"console"` + } `yaml:"_ironic_helm_values"` + } + require.NoError(t, yaml.UnmarshalWithOptions(varsFile, &values)) + + environment := values.HelmValues.Console.Container.Env + for _, expected := range []string{ + "{'name': 'KEYBOARD_MODE', 'value': ironic_vnc_keyboard_mode | string}", + "'name': 'KEYBOARD_DIAGNOSTICS'", + "'value': ironic_vnc_keyboard_diagnostics | bool | string | lower", + "'name': 'PRINTABLE_KEY_HOLD'", + "'value': ironic_vnc_printable_key_hold | float | string", + "'name': 'PRINTABLE_KEY_INTERVAL'", + "'value': ironic_vnc_printable_key_interval | float | string", + } { + require.Contains(t, environment, expected) + } +} + func TestBaremetalConsoleNetworkPolicyManifestGate(t *testing.T) { const manifest = "network_policy_console: >-" require.Equal(t, 1, bytes.Count(varsFile, []byte(manifest)))