diff --git a/ocaml/idl/datamodel_common.ml b/ocaml/idl/datamodel_common.ml index 8a87d7eb52..819b7c6114 100644 --- a/ocaml/idl/datamodel_common.ml +++ b/ocaml/idl/datamodel_common.ml @@ -10,7 +10,7 @@ open Datamodel_roles to leave a gap for potential hotfixes needing to increment the schema version.*) let schema_major_vsn = 5 -let schema_minor_vsn = 788 +let schema_minor_vsn = 789 (* Historical schema versions just in case this is useful later *) let rio_schema_major_vsn = 5 diff --git a/ocaml/idl/datamodel_errors.ml b/ocaml/idl/datamodel_errors.ml index d7d24c6d76..69286e11f1 100644 --- a/ocaml/idl/datamodel_errors.ml +++ b/ocaml/idl/datamodel_errors.ml @@ -2046,6 +2046,9 @@ let _ = error Api_errors.set_console_timeout_partially_failed ["hosts"] ~doc:"Some hosts failed to set console timeout." () ; + error Api_errors.set_ssh_auto_mode_partially_failed ["hosts"] + ~doc:"Some hosts failed to set SSH auto mode." () ; + error Api_errors.host_driver_no_hardware ["driver variant"] ~doc:"No hardware present for this host driver variant" () ; diff --git a/ocaml/idl/datamodel_host.ml b/ocaml/idl/datamodel_host.ml index e51b59eb57..1dcd39aad4 100644 --- a/ocaml/idl/datamodel_host.ml +++ b/ocaml/idl/datamodel_host.ml @@ -1335,6 +1335,13 @@ let create_params = ; param_release= numbered_release "25.14.0-next" ; param_default= Some (VInt Constants.default_console_idle_timeout) } + ; { + param_type= Bool + ; param_name= "ssh_auto_mode" + ; param_doc= "True if SSH auto mode is enabled for the host" + ; param_release= numbered_release "25.14.0-next" + ; param_default= Some (VBool Constants.default_ssh_auto_mode) + } ] let create = @@ -1350,8 +1357,8 @@ let create = ; ( Changed , "25.14.0-next" , "Added --ssh_enabled --ssh_enabled_timeout --ssh_expiry \ - --console_idle_timeout options to allow them to be configured for \ - new host" + --console_idle_timeout --ssh_auto_mode options to allow them to be \ + configured for new host" ) ] ~versioned_params:create_params ~doc:"Create a new host record" @@ -2440,6 +2447,21 @@ let set_console_idle_timeout = ] ~allowed_roles:_R_POOL_ADMIN () +let set_ssh_auto_mode = + call ~name:"set_ssh_auto_mode" ~lifecycle:[] + ~doc:"Set the SSH auto mode for the host" + ~params: + [ + (Ref _host, "self", "The host") + ; ( Bool + , "value" + , "The SSH auto mode for the host,when set to true, SSH to normally be \ + disabled and SSH to be enabled only in case of emergency e.g., xapi \ + is down" + ) + ] + ~allowed_roles:_R_POOL_ADMIN () + let latest_synced_updates_applied_state = Enum ( "latest_synced_updates_applied_state" @@ -2601,6 +2623,7 @@ let t = ; disable_ssh ; set_ssh_enabled_timeout ; set_console_idle_timeout + ; set_ssh_auto_mode ] ~contents: ([ @@ -3056,6 +3079,10 @@ let t = "console_idle_timeout" "The timeout in seconds after which idle console will be \ automatically terminated (0 means never)" + ; field ~qualifier:DynamicRO ~lifecycle:[] ~ty:Bool + ~default_value:(Some (VBool Constants.default_ssh_auto_mode)) + "ssh_auto_mode" + "Reflects whether SSH auto mode is enabled for the host" ] ) () diff --git a/ocaml/idl/datamodel_pool.ml b/ocaml/idl/datamodel_pool.ml index 97b42e1287..1874512c14 100644 --- a/ocaml/idl/datamodel_pool.ml +++ b/ocaml/idl/datamodel_pool.ml @@ -1606,6 +1606,21 @@ let set_console_idle_timeout = ] ~allowed_roles:_R_POOL_ADMIN () +let set_ssh_auto_mode = + call ~name:"set_ssh_auto_mode" ~lifecycle:[] + ~doc:"Set the SSH auto mode for all hosts in the pool" + ~params: + [ + (Ref _pool, "self", "The pool") + ; ( Bool + , "value" + , "The SSH auto mode for all hosts in the pool,when set to true, SSH \ + to normally be disabled and SSH to be enabled only in case of \ + emergency e.g., xapi is down" + ) + ] + ~allowed_roles:_R_POOL_ADMIN () + (** A pool class *) let t = create_obj ~in_db:true @@ -1704,6 +1719,7 @@ let t = ; disable_ssh ; set_ssh_enabled_timeout ; set_console_idle_timeout + ; set_ssh_auto_mode ] ~contents: ([ diff --git a/ocaml/idl/schematest.ml b/ocaml/idl/schematest.ml index 0938dd78e8..f94dd14ae7 100644 --- a/ocaml/idl/schematest.ml +++ b/ocaml/idl/schematest.ml @@ -3,7 +3,7 @@ let hash x = Digest.string x |> Digest.to_hex (* BEWARE: if this changes, check that schema has been bumped accordingly in ocaml/idl/datamodel_common.ml, usually schema_minor_vsn *) -let last_known_schema_hash = "8bf2b9ab509301baf138820cf34608d3" +let last_known_schema_hash = "7c52d11789dea3ab3167c5d0e3e7fa89" let current_schema_hash : string = let open Datamodel_types in diff --git a/ocaml/tests/common/test_common.ml b/ocaml/tests/common/test_common.ml index b61de22ad8..f5b8a27051 100644 --- a/ocaml/tests/common/test_common.ml +++ b/ocaml/tests/common/test_common.ml @@ -172,14 +172,14 @@ let make_host ~__context ?(uuid = make_uuid ()) ?(name_label = "host") ?(local_cache_sr = Ref.null) ?(chipset_info = []) ?(ssl_legacy = false) ?(last_software_update = Date.epoch) ?(last_update_hash = "") ?(ssh_enabled = true) ?(ssh_enabled_timeout = 0L) ?(ssh_expiry = Date.epoch) - ?(console_idle_timeout = 0L) () = + ?(console_idle_timeout = 0L) ?(ssh_auto_mode = false) () = let host = Xapi_host.create ~__context ~uuid ~name_label ~name_description ~hostname ~address ~external_auth_type ~external_auth_service_name ~external_auth_configuration ~license_params ~edition ~license_server ~local_cache_sr ~chipset_info ~ssl_legacy ~last_software_update ~last_update_hash ~ssh_enabled ~ssh_enabled_timeout ~ssh_expiry - ~console_idle_timeout + ~console_idle_timeout ~ssh_auto_mode in Db.Host.set_cpu_info ~__context ~self:host ~value:default_cpu_info ; host @@ -219,7 +219,7 @@ let make_host2 ~__context ?(ref = Ref.make ()) ?(uuid = make_uuid ()) ~recommended_guidances:[] ~latest_synced_updates_applied:`unknown ~pending_guidances_recommended:[] ~pending_guidances_full:[] ~last_update_hash:"" ~ssh_enabled:true ~ssh_enabled_timeout:0L - ~ssh_expiry:Date.epoch ~console_idle_timeout:0L ; + ~ssh_expiry:Date.epoch ~console_idle_timeout:0L ~ssh_auto_mode:false ; ref let make_pif ~__context ~network ~host ?(device = "eth0") diff --git a/ocaml/tests/test_host.ml b/ocaml/tests/test_host.ml index 03f526d08d..1f814b5adf 100644 --- a/ocaml/tests/test_host.ml +++ b/ocaml/tests/test_host.ml @@ -25,7 +25,7 @@ let add_host __context name = ~local_cache_sr:Ref.null ~chipset_info:[] ~ssl_legacy:false ~last_software_update:Clock.Date.epoch ~last_update_hash:"" ~ssh_enabled:true ~ssh_enabled_timeout:0L ~ssh_expiry:Clock.Date.epoch - ~console_idle_timeout:0L + ~console_idle_timeout:0L ~ssh_auto_mode:false ) (* Creates an unlicensed pool with the maximum number of hosts *) diff --git a/ocaml/xapi-cli-server/records.ml b/ocaml/xapi-cli-server/records.ml index ca5d04b95a..cc30548676 100644 --- a/ocaml/xapi-cli-server/records.ml +++ b/ocaml/xapi-cli-server/records.ml @@ -1584,6 +1584,17 @@ let pool_record rpc session_id pool = ~value:(safe_i64_of_string "console-idle-timeout" value) ) () + ; make_field ~name:"ssh-auto-mode" + ~get:(fun () -> + get_consistent_field_or_default ~rpc ~session_id + ~getter:Client.Host.get_ssh_auto_mode ~transform:string_of_bool + ~default:inconsistent + ) + ~set:(fun value -> + Client.Pool.set_ssh_auto_mode ~rpc ~session_id ~self:pool + ~value:(safe_bool_of_string "ssh-auto-mode" value) + ) + () ] } @@ -3375,6 +3386,13 @@ let host_record rpc session_id host = ~value:(safe_i64_of_string "console-idle-timeout" value) ) () + ; make_field ~name:"ssh-auto-mode" + ~get:(fun () -> string_of_bool (x ()).API.host_ssh_auto_mode) + ~set:(fun value -> + Client.Host.set_ssh_auto_mode ~rpc ~session_id ~self:host + ~value:(safe_bool_of_string "ssh-auto-mode" value) + ) + () ] } diff --git a/ocaml/xapi-consts/api_errors.ml b/ocaml/xapi-consts/api_errors.ml index adfb96e4b8..96f9352f44 100644 --- a/ocaml/xapi-consts/api_errors.ml +++ b/ocaml/xapi-consts/api_errors.ml @@ -1426,6 +1426,9 @@ let set_ssh_timeout_partially_failed = let set_console_timeout_partially_failed = add_error "SET_CONSOLE_TIMEOUT_PARTIALLY_FAILED" +let set_ssh_auto_mode_partially_failed = + add_error "SET_SSH_AUTO_MODE_PARTIALLY_FAILED" + let host_driver_no_hardware = add_error "HOST_DRIVER_NO_HARDWARE" let tls_verification_not_enabled_in_pool = diff --git a/ocaml/xapi-consts/constants.ml b/ocaml/xapi-consts/constants.ml index 185f9669a7..7af0ccf649 100644 --- a/ocaml/xapi-consts/constants.ml +++ b/ocaml/xapi-consts/constants.ml @@ -428,3 +428,5 @@ let default_ssh_enabled = true let default_ssh_enabled_timeout = 0L let default_console_idle_timeout = 0L + +let default_ssh_auto_mode = false diff --git a/ocaml/xapi/dbsync_slave.ml b/ocaml/xapi/dbsync_slave.ml index 51ef2665d1..900e8a1ac0 100644 --- a/ocaml/xapi/dbsync_slave.ml +++ b/ocaml/xapi/dbsync_slave.ml @@ -64,6 +64,7 @@ let create_localhost ~__context info = ~ssh_enabled_timeout:Constants.default_ssh_enabled_timeout ~ssh_expiry:Date.epoch ~console_idle_timeout:Constants.default_console_idle_timeout + ~ssh_auto_mode:Constants.default_ssh_auto_mode in () diff --git a/ocaml/xapi/message_forwarding.ml b/ocaml/xapi/message_forwarding.ml index c9268e82d3..924cb3f124 100644 --- a/ocaml/xapi/message_forwarding.ml +++ b/ocaml/xapi/message_forwarding.ml @@ -1197,6 +1197,12 @@ functor (pool_uuid ~__context self) value ; Local.Pool.set_console_idle_timeout ~__context ~self ~value + + let set_ssh_auto_mode ~__context ~self ~value = + info "Pool.set_ssh_auto_mode: pool='%s' value='%b'" + (pool_uuid ~__context self) + value ; + Local.Pool.set_ssh_auto_mode ~__context ~self ~value end module VM = struct @@ -4063,6 +4069,14 @@ functor let local_fn = Local.Host.set_console_idle_timeout ~self ~value in let remote_fn = Client.Host.set_console_idle_timeout ~self ~value in do_op_on ~local_fn ~__context ~host:self ~remote_fn + + let set_ssh_auto_mode ~__context ~self ~value = + info "Host.set_ssh_auto_mode: host='%s' value='%b'" + (host_uuid ~__context self) + value ; + let local_fn = Local.Host.set_ssh_auto_mode ~self ~value in + let remote_fn = Client.Host.set_ssh_auto_mode ~self ~value in + do_op_on ~local_fn ~__context ~host:self ~remote_fn end module Host_crashdump = struct diff --git a/ocaml/xapi/xapi_globs.ml b/ocaml/xapi/xapi_globs.ml index a5fed248e6..a8b2d8485c 100644 --- a/ocaml/xapi/xapi_globs.ml +++ b/ocaml/xapi/xapi_globs.ml @@ -1297,6 +1297,8 @@ let job_for_disable_ssh = ref "Disable SSH" let ssh_service = ref "sshd" +let ssh_monitor_service = ref "xapi-ssh-monitor" + (* Fingerprint of default patch key *) let citrix_patch_key = "NERDNTUzMDMwRUMwNDFFNDI4N0M4OEVCRUFEMzlGOTJEOEE5REUyNg==" diff --git a/ocaml/xapi/xapi_host.ml b/ocaml/xapi/xapi_host.ml index dfccd2ebc7..d7f793aa33 100644 --- a/ocaml/xapi/xapi_host.ml +++ b/ocaml/xapi/xapi_host.ml @@ -979,7 +979,7 @@ let create ~__context ~uuid ~name_label ~name_description:_ ~hostname ~address ~external_auth_type ~external_auth_service_name ~external_auth_configuration ~license_params ~edition ~license_server ~local_cache_sr ~chipset_info ~ssl_legacy:_ ~last_software_update ~last_update_hash ~ssh_enabled - ~ssh_enabled_timeout ~ssh_expiry ~console_idle_timeout = + ~ssh_enabled_timeout ~ssh_expiry ~console_idle_timeout ~ssh_auto_mode = (* fail-safe. We already test this on the joining host, but it's racy, so multiple concurrent pool-join might succeed. Note: we do it in this order to avoid a problem checking restrictions during the initial setup of the database *) @@ -1044,7 +1044,7 @@ let create ~__context ~uuid ~name_label ~name_description:_ ~hostname ~address ~tls_verification_enabled ~last_software_update ~last_update_hash ~recommended_guidances:[] ~latest_synced_updates_applied:`unknown ~pending_guidances_recommended:[] ~pending_guidances_full:[] ~ssh_enabled - ~ssh_enabled_timeout ~ssh_expiry ~console_idle_timeout ; + ~ssh_enabled_timeout ~ssh_expiry ~console_idle_timeout ~ssh_auto_mode ; (* If the host we're creating is us, make sure its set to live *) Db.Host_metrics.set_last_updated ~__context ~self:metrics ~value:(Date.now ()) ; Db.Host_metrics.set_live ~__context ~self:metrics ~value:host_is_us ; @@ -3112,10 +3112,39 @@ let emergency_clear_mandatory_guidance ~__context = ) ; Db.Host.set_pending_guidances ~__context ~self ~value:[] +let set_ssh_auto_mode ~__context ~self ~value = + debug "Setting SSH auto mode for host %s to %B" + (Helpers.get_localhost_uuid ()) + value ; + + Db.Host.set_ssh_auto_mode ~__context ~self ~value ; + + try + (* When enabled, the ssh_monitor_service regularly checks XAPI status to manage SSH availability. + During normal operation when XAPI is running properly, SSH is automatically disabled. + SSH is only enabled during emergency scenarios + (e.g., when XAPI is down) to allow administrative access for troubleshooting. *) + if value then ( + Xapi_systemctl.enable ~wait_until_success:false + !Xapi_globs.ssh_monitor_service ; + Xapi_systemctl.start ~wait_until_success:false + !Xapi_globs.ssh_monitor_service + ) else ( + Xapi_systemctl.stop ~wait_until_success:false + !Xapi_globs.ssh_monitor_service ; + Xapi_systemctl.disable ~wait_until_success:false + !Xapi_globs.ssh_monitor_service + ) + with e -> + error "Failed to configure SSH auto mode: %s" (Printexc.to_string e) ; + Helpers.internal_error "Failed to configure SSH auto mode: %s" + (Printexc.to_string e) + let disable_ssh_internal ~__context ~self = try debug "Disabling SSH for host %s" (Helpers.get_localhost_uuid ()) ; - Xapi_systemctl.disable ~wait_until_success:false !Xapi_globs.ssh_service ; + if not (Db.Host.get_ssh_auto_mode ~__context ~self) then + Xapi_systemctl.disable ~wait_until_success:false !Xapi_globs.ssh_service ; Xapi_systemctl.stop ~wait_until_success:false !Xapi_globs.ssh_service ; Db.Host.set_ssh_enabled ~__context ~self ~value:false with e -> @@ -3123,7 +3152,7 @@ let disable_ssh_internal ~__context ~self = (Printexc.to_string e) ; Helpers.internal_error "Failed to disable SSH: %s" (Printexc.to_string e) -let schedule_disable_ssh_job ~__context ~self ~timeout = +let schedule_disable_ssh_job ~__context ~self ~timeout ~auto_mode = let host_uuid = Helpers.get_localhost_uuid () in let expiry_time = match @@ -3152,7 +3181,11 @@ let schedule_disable_ssh_job ~__context ~self ~timeout = Xapi_stdext_threads_scheduler.Scheduler.add_to_queue !Xapi_globs.job_for_disable_ssh Xapi_stdext_threads_scheduler.Scheduler.OneShot (Int64.to_float timeout) - (fun () -> disable_ssh_internal ~__context ~self + (fun () -> + disable_ssh_internal ~__context ~self ; + (* re-enable SSH auto mode if it was enabled before calling host.enable_ssh *) + if auto_mode then + set_ssh_auto_mode ~__context ~self ~value:true ) ; Db.Host.set_ssh_expiry ~__context ~self ~value:expiry_time @@ -3161,6 +3194,10 @@ let enable_ssh ~__context ~self = try debug "Enabling SSH for host %s" (Helpers.get_localhost_uuid ()) ; + let cached_ssh_auto_mode = Db.Host.get_ssh_auto_mode ~__context ~self in + (* Disable SSH auto mode when SSH is enabled manually *) + set_ssh_auto_mode ~__context ~self ~value:false ; + Xapi_systemctl.enable ~wait_until_success:false !Xapi_globs.ssh_service ; Xapi_systemctl.start ~wait_until_success:false !Xapi_globs.ssh_service ; @@ -3171,6 +3208,7 @@ let enable_ssh ~__context ~self = !Xapi_globs.job_for_disable_ssh | t -> schedule_disable_ssh_job ~__context ~self ~timeout:t + ~auto_mode:cached_ssh_auto_mode ) ; Db.Host.set_ssh_enabled ~__context ~self ~value:true @@ -3208,7 +3246,7 @@ let set_ssh_enabled_timeout ~__context ~self ~value = !Xapi_globs.job_for_disable_ssh ; Db.Host.set_ssh_expiry ~__context ~self ~value:Date.epoch | t -> - schedule_disable_ssh_job ~__context ~self ~timeout:t + schedule_disable_ssh_job ~__context ~self ~timeout:t ~auto_mode:false let set_console_idle_timeout ~__context ~self ~value = let assert_timeout_valid timeout = diff --git a/ocaml/xapi/xapi_host.mli b/ocaml/xapi/xapi_host.mli index a3d7504b4a..481b4699d5 100644 --- a/ocaml/xapi/xapi_host.mli +++ b/ocaml/xapi/xapi_host.mli @@ -134,6 +134,7 @@ val create : -> ssh_enabled_timeout:int64 -> ssh_expiry:API.datetime -> console_idle_timeout:int64 + -> ssh_auto_mode:bool -> [`host] Ref.t val destroy : __context:Context.t -> self:API.ref_host -> unit @@ -579,4 +580,11 @@ val set_console_idle_timeout : __context:Context.t -> self:API.ref_host -> value:int64 -> unit val schedule_disable_ssh_job : - __context:Context.t -> self:API.ref_host -> timeout:int64 -> unit + __context:Context.t + -> self:API.ref_host + -> timeout:int64 + -> auto_mode:bool + -> unit + +val set_ssh_auto_mode : + __context:Context.t -> self:API.ref_host -> value:bool -> unit diff --git a/ocaml/xapi/xapi_periodic_scheduler_init.ml b/ocaml/xapi/xapi_periodic_scheduler_init.ml index f394a9ad99..ff7c3187c2 100644 --- a/ocaml/xapi/xapi_periodic_scheduler_init.ml +++ b/ocaml/xapi/xapi_periodic_scheduler_init.ml @@ -90,6 +90,7 @@ let register ~__context = if Int64.compare expiry_time current_time > 0 then let remaining = Int64.sub expiry_time current_time in Xapi_host.schedule_disable_ssh_job ~__context ~self ~timeout:remaining + ~auto_mode:true (* handle the case where XAPI is not active when the SSH timeout expires *) else if Fe_systemctl.is_active ~service:!Xapi_globs.ssh_service then Xapi_host.disable_ssh ~__context ~self diff --git a/ocaml/xapi/xapi_pool.ml b/ocaml/xapi/xapi_pool.ml index 96fff99e34..ac09ebca7f 100644 --- a/ocaml/xapi/xapi_pool.ml +++ b/ocaml/xapi/xapi_pool.ml @@ -967,6 +967,7 @@ let rec create_or_get_host_on_master __context rpc session_id (host_ref, host) : ~ssh_enabled_timeout:host.API.host_ssh_enabled_timeout ~ssh_expiry:host.API.host_ssh_expiry ~console_idle_timeout:host.API.host_console_idle_timeout + ~ssh_auto_mode:host.API.host_ssh_auto_mode in (* Copy other-config into newly created host record: *) no_exn @@ -4070,6 +4071,13 @@ module Ssh = struct Client.Host.set_console_idle_timeout ~rpc ~session_id ~self ~value ) ~error:Api_errors.set_console_timeout_partially_failed + + let set_ssh_auto_mode ~__context ~self:_ ~value = + operate ~__context + ~action:(fun ~rpc ~session_id ~self -> + Client.Host.set_ssh_auto_mode ~rpc ~session_id ~self ~value + ) + ~error:Api_errors.set_ssh_auto_mode_partially_failed end let enable_ssh = Ssh.enable @@ -4079,3 +4087,5 @@ let disable_ssh = Ssh.disable let set_ssh_enabled_timeout = Ssh.set_enabled_timeout let set_console_idle_timeout = Ssh.set_console_timeout + +let set_ssh_auto_mode = Ssh.set_ssh_auto_mode diff --git a/ocaml/xapi/xapi_pool.mli b/ocaml/xapi/xapi_pool.mli index b9c5b6fea3..dc87e90a18 100644 --- a/ocaml/xapi/xapi_pool.mli +++ b/ocaml/xapi/xapi_pool.mli @@ -443,3 +443,6 @@ val set_ssh_enabled_timeout : val set_console_idle_timeout : __context:Context.t -> self:API.ref_pool -> value:int64 -> unit + +val set_ssh_auto_mode : + __context:Context.t -> self:API.ref_pool -> value:bool -> unit