From 8d3bc73d3aad6bdc0bf9cf4495b85e19fa07ad3f Mon Sep 17 00:00:00 2001 From: Lunfan Zhang Date: Tue, 18 Mar 2025 09:16:26 +0000 Subject: [PATCH] CP-53477 Update host/pool datamodel to support SSH status query and configure Add new host object fields: - ssh_enabled - ssh_enabled_timeout - ssh_expiry - console_idle_timeout Add new host/pool API to enable to set a temporary enabled SSH service timeout - set_ssh_enable_timeout Add new host/pool API to enable to set console timeout - set_console_timeout Signed-off-by: Lunfan Zhang --- ocaml/idl/datamodel_common.ml | 2 +- ocaml/idl/datamodel_errors.ml | 3 + ocaml/idl/datamodel_host.ml | 39 +++++++ ocaml/idl/datamodel_pool.ml | 29 ++++++ ocaml/idl/schematest.ml | 2 +- ocaml/tests/common/test_common.ml | 3 +- ocaml/xapi-cli-server/records.ml | 65 ++++++++++++ ocaml/xapi-consts/api_errors.ml | 2 + ocaml/xapi/message_forwarding.ml | 24 +++++ ocaml/xapi/xapi_host.ml | 163 ++++++++++++++++++++++++++++-- ocaml/xapi/xapi_host.mli | 9 ++ ocaml/xapi/xapi_pool.ml | 22 ++++ ocaml/xapi/xapi_pool.mli | 6 ++ 13 files changed, 360 insertions(+), 9 deletions(-) diff --git a/ocaml/idl/datamodel_common.ml b/ocaml/idl/datamodel_common.ml index 50bc585b7ac..a044c9a0f2d 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 = 786 +let schema_minor_vsn = 787 (* 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 27bb8a7bf98..e914d73813c 100644 --- a/ocaml/idl/datamodel_errors.ml +++ b/ocaml/idl/datamodel_errors.ml @@ -2043,6 +2043,9 @@ let _ = error Api_errors.host_driver_no_hardware ["driver variant"] ~doc:"No hardware present for this host driver variant" () ; + error Api_errors.set_console_timeout_failed ["timeout"] + ~doc:"Failed to set SSH&VNC idle session timeout." () ; + error Api_errors.tls_verification_not_enabled_in_pool [] ~doc: "TLS verification has not been enabled in the pool successfully, please \ diff --git a/ocaml/idl/datamodel_host.ml b/ocaml/idl/datamodel_host.ml index 99f4ebcf316..d28f5a35b4c 100644 --- a/ocaml/idl/datamodel_host.ml +++ b/ocaml/idl/datamodel_host.ml @@ -2368,6 +2368,29 @@ let disable_ssh = ~params:[(Ref _host, "self", "The host")] ~allowed_roles:_R_POOL_ADMIN () +let set_ssh_enable_timeout = + call ~name:"set_ssh_enable_timeout " ~lifecycle:[] + ~doc:"Set the SSH service enabled timeout for the host" + ~params: + [ + (Ref _host, "self", "The host") + ; ( Int + , "timeout" + , "The SSH enabled timeout in minutes (0 means no timeout, max 2880)" + ) + ] + ~allowed_roles:_R_POOL_ADMIN () + +let set_console_timeout = + call ~name:"set_console_timeout" ~lifecycle:[] + ~doc:"Set the idle SSH/VNC session timeout for the host" + ~params: + [ + (Ref _host, "self", "The host") + ; (Int, "console_timeout", "The idle console timeout in seconds") + ] + ~allowed_roles:_R_POOL_ADMIN () + let latest_synced_updates_applied_state = Enum ( "latest_synced_updates_applied_state" @@ -2527,6 +2550,8 @@ let t = ; emergency_clear_mandatory_guidance ; enable_ssh ; disable_ssh + ; set_ssh_enable_timeout + ; set_console_timeout ] ~contents: ([ @@ -2964,6 +2989,20 @@ let t = ~default_value:(Some (VString "")) "last_update_hash" "The SHA256 checksum of updateinfo of the most recently applied \ update on the host" + ; field ~qualifier:RW ~lifecycle:[] ~ty:Bool + ~default_value:(Some (VBool true)) "ssh_enabled" + "True if SSH access is enabled for the host" + ; field ~qualifier:RW ~lifecycle:[] ~ty:Int + ~default_value:(Some (VInt 0L)) "ssh_enabled_timeout" + "The timeout in minutes after which SSH access will be \ + automatically disabled (0 means never)" + ; field ~qualifier:DynamicRO ~lifecycle:[] ~ty:DateTime + ~default_value:(Some (VDateTime Date.epoch)) "ssh_expiry" + "The time when SSH access will expire" + ; field ~qualifier:RW ~lifecycle:[] ~ty:Int + ~default_value:(Some (VInt 0L)) "console_idle_timeout" + "The timeout in seconds after which idle console will be \ + automatically terminated (0 means never)" ] ) () diff --git a/ocaml/idl/datamodel_pool.ml b/ocaml/idl/datamodel_pool.ml index c35c6789f7f..8126c09bd65 100644 --- a/ocaml/idl/datamodel_pool.ml +++ b/ocaml/idl/datamodel_pool.ml @@ -1571,6 +1571,33 @@ let disable_ssh = ~params:[(Ref _pool, "self", "The pool")] ~allowed_roles:_R_POOL_ADMIN () +let set_ssh_enable_timeout = + call ~name:"set_ssh_enable_timeout " ~lifecycle:[] + ~doc:"Set the SSH enabled timeout for the hosts in the pool" + ~params: + [ + (Ref _pool, "self", "The pool") + ; ( Int + , "timeout" + , "The SSH enabled timeout in minutes. (0 means no timeout, max 2880)" + ) + ] + ~allowed_roles:_R_POOL_ADMIN () + +let set_console_timeout = + call ~name:"set_console_timeout" ~lifecycle:[] + ~doc:"Set the idle SSH/VNC session timeout for the pool" + ~params: + [ + (Ref _pool, "self", "The pool") + ; ( Int + , "console_timeout" + , "The idle SSH/VNC session timeout in seconds. A value of 0 means no \ + timeout." + ) + ] + ~allowed_roles:_R_POOL_ADMIN () + (** A pool class *) let t = create_obj ~in_db:true @@ -1667,6 +1694,8 @@ let t = ; get_guest_secureboot_readiness ; enable_ssh ; disable_ssh + ; set_ssh_enable_timeout + ; set_console_timeout ] ~contents: ([ diff --git a/ocaml/idl/schematest.ml b/ocaml/idl/schematest.ml index 255e094e1dd..9692c3fd8c3 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 = "ad67a64cd47cdea32085518c1fb38d27" +let last_known_schema_hash = "42fd0bbdc613092390c32e04e35a24d2" 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 7b5484a02ba..feab7169f5a 100644 --- a/ocaml/tests/common/test_common.ml +++ b/ocaml/tests/common/test_common.ml @@ -215,7 +215,8 @@ let make_host2 ~__context ?(ref = Ref.make ()) ?(uuid = make_uuid ()) ~last_software_update:(Xapi_host.get_servertime ~__context ~host:ref) ~recommended_guidances:[] ~latest_synced_updates_applied:`unknown ~pending_guidances_recommended:[] ~pending_guidances_full:[] - ~last_update_hash:"" ; + ~last_update_hash:"" ~ssh_enabled:true ~ssh_enabled_timeout:0L + ~ssh_expiry:Date.epoch ~console_idle_timeout:0L ; ref let make_pif ~__context ~network ~host ?(device = "eth0") diff --git a/ocaml/xapi-cli-server/records.ml b/ocaml/xapi-cli-server/records.ml index 56e97fbda03..2e82b18b59e 100644 --- a/ocaml/xapi-cli-server/records.ml +++ b/ocaml/xapi-cli-server/records.ml @@ -204,6 +204,15 @@ let get_pbds_host rpc session_id pbds = let get_sr_host rpc session_id record = get_pbds_host rpc session_id record.API.sR_PBDs +let get_unified_field ~rpc ~session_id ~getter ~transform ~default = + Client.Host.get_all ~rpc ~session_id + |> List.map (fun h -> getter ~rpc ~session_id ~self:h |> transform) + |> fun values -> + if List.for_all (( = ) (List.hd values)) values then + List.hd values + else + default + let bond_record rpc session_id bond = let _ref = ref bond in let empty_record = @@ -1506,6 +1515,42 @@ let pool_record rpc session_id pool = ) ~get_map:(fun () -> (x ()).API.pool_license_server) () + ; make_field ~name:"ssh-enabled" + ~get:(fun () -> + get_unified_field ~rpc ~session_id + ~getter:Client.Host.get_ssh_enabled ~transform:string_of_bool + ~default:"" + ) + () + ; make_field ~name:"ssh-enabled-timeout" + ~get:(fun () -> + get_unified_field ~rpc ~session_id + ~getter:Client.Host.get_ssh_enabled_timeout + ~transform:Int64.to_string ~default:"" + ) + ~set:(fun value -> + Client.Pool.set_ssh_enable_timeout ~rpc ~session_id ~self:pool + ~timeout:(Int64.of_string value) + ) + () + ; make_field ~name:"ssh-expiry" + ~get:(fun () -> + get_unified_field ~rpc ~session_id + ~getter:Client.Host.get_ssh_expiry ~transform:Date.to_rfc3339 + ~default:"" + ) + () + ; make_field ~name:"console-idle-timeout" + ~get:(fun () -> + get_unified_field ~rpc ~session_id + ~getter:Client.Host.get_console_idle_timeout + ~transform:Int64.to_string ~default:"" + ) + ~set:(fun value -> + Client.Pool.set_console_timeout ~rpc ~session_id ~self:pool + ~console_timeout:(Int64.of_string value) + ) + () ] } @@ -3265,6 +3310,26 @@ let host_record rpc session_id host = ; make_field ~name:"last-update-hash" ~get:(fun () -> (x ()).API.host_last_update_hash) () + ; make_field ~name:"ssh-enabled" + ~get:(fun () -> string_of_bool (x ()).API.host_ssh_enabled) + () + ; make_field ~name:"ssh-enabled-timeout" + ~get:(fun () -> Int64.to_string (x ()).API.host_ssh_enabled_timeout) + ~set:(fun value -> + Client.Host.set_ssh_enable_timeout ~rpc ~session_id ~self:host + ~timeout:(safe_i64_of_string "ssh-enabled-timeout" value) + ) + () + ; make_field ~name:"ssh-expiry" + ~get:(fun () -> Date.to_rfc3339 (x ()).API.host_ssh_expiry) + () + ; make_field ~name:"console-idle-timeout" + ~get:(fun () -> Int64.to_string (x ()).API.host_console_idle_timeout) + ~set:(fun value -> + Client.Host.set_console_timeout ~rpc ~session_id ~self:host + ~console_timeout:(safe_i64_of_string "console-idle-timeout" value) + ) + () ] } diff --git a/ocaml/xapi-consts/api_errors.ml b/ocaml/xapi-consts/api_errors.ml index 906e22bf259..8e019ab062a 100644 --- a/ocaml/xapi-consts/api_errors.ml +++ b/ocaml/xapi-consts/api_errors.ml @@ -1424,3 +1424,5 @@ let host_driver_no_hardware = add_error "HOST_DRIVER_NO_HARDWARE" let tls_verification_not_enabled_in_pool = add_error "TLS_VERIFICATION_NOT_ENABLED_IN_POOL" + +let set_console_timeout_failed = add_error "SET_CONSOLE_TIMEOUT_FAILED" diff --git a/ocaml/xapi/message_forwarding.ml b/ocaml/xapi/message_forwarding.ml index dc77569e646..8b3bac63d85 100644 --- a/ocaml/xapi/message_forwarding.ml +++ b/ocaml/xapi/message_forwarding.ml @@ -1185,6 +1185,14 @@ functor let disable_ssh ~__context ~self = info "%s: pool = '%s'" __FUNCTION__ (pool_uuid ~__context self) ; Local.Pool.disable_ssh ~__context ~self + + let set_ssh_enable_timeout ~__context ~self ~timeout = + info "%s: pool = '%s'" __FUNCTION__ (pool_uuid ~__context self) ; + Local.Pool.set_ssh_enable_timeout ~__context ~self ~timeout + + let set_console_timeout ~__context ~self ~console_timeout = + info "%s: pool = '%s'" __FUNCTION__ (pool_uuid ~__context self) ; + Local.Pool.set_console_timeout ~__context ~self ~console_timeout end module VM = struct @@ -4035,6 +4043,22 @@ functor let local_fn = Local.Host.disable_ssh ~self in let remote_fn = Client.Host.disable_ssh ~self in do_op_on ~local_fn ~__context ~host:self ~remote_fn + + let set_ssh_enable_timeout ~__context ~self ~timeout = + let uuid = host_uuid ~__context self in + info "Host.set_ssh_enable_timeout : host = '%s'" uuid ; + let local_fn = Local.Host.set_ssh_enable_timeout ~self ~timeout in + let remote_fn = Client.Host.set_ssh_enable_timeout ~self ~timeout in + do_op_on ~local_fn ~__context ~host:self ~remote_fn + + let set_console_timeout ~__context ~self ~console_timeout = + let uuid = host_uuid ~__context self in + info "Host.set_console_timeout: host = '%s'" uuid ; + let local_fn = Local.Host.set_console_timeout ~self ~console_timeout in + let remote_fn = + Client.Host.set_console_timeout ~self ~console_timeout + in + do_op_on ~local_fn ~__context ~host:self ~remote_fn end module Host_crashdump = struct diff --git a/ocaml/xapi/xapi_host.ml b/ocaml/xapi/xapi_host.ml index acd8a10936a..a7a344a2b3b 100644 --- a/ocaml/xapi/xapi_host.ml +++ b/ocaml/xapi/xapi_host.ml @@ -1042,7 +1042,9 @@ let create ~__context ~uuid ~name_label ~name_description:_ ~hostname ~address ~multipathing:false ~uefi_certificates:"" ~editions:[] ~pending_guidances:[] ~tls_verification_enabled ~last_software_update ~last_update_hash ~recommended_guidances:[] ~latest_synced_updates_applied:`unknown - ~pending_guidances_recommended:[] ~pending_guidances_full:[] ; + ~pending_guidances_recommended:[] ~pending_guidances_full:[] + ~ssh_enabled:true ~ssh_enabled_timeout:0L ~ssh_expiry:Date.epoch + ~console_idle_timeout:0L ; (* 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,22 +3114,171 @@ let emergency_clear_mandatory_guidance ~__context = ) ; Db.Host.set_pending_guidances ~__context ~self ~value:[] +let validate_timeout timeout = + if timeout < -1L || timeout > 2880L then + raise + (Api_errors.Server_error + ( Api_errors.invalid_value + , ["timeout"; Int64.to_string timeout; "must be between -1 and 2880"] + ) + ) + else + timeout + +let remove_disable_job ~__context ~self = + let host_uuid = Db.Host.get_uuid ~__context ~self in + let task_name = Printf.sprintf "disable_ssh_for_host_%s" host_uuid in + Xapi_stdext_threads_scheduler.Scheduler.remove_from_queue task_name + +let schedule_disable_job ~__context ~self ~timeout = + let host_uuid = Db.Host.get_uuid ~__context ~self in + let task_name = Printf.sprintf "disable_ssh_for_host_%s" host_uuid in + let current_time = Date.now () in + + let expiry_time = + let current_secs = Date.to_unix_time current_time in + let timeout_secs = Int64.to_float timeout in + Date.of_unix_time (current_secs +. timeout_secs) + in + + debug "Scheduling SSH disable job for host %s with timeout %Ld seconds" + host_uuid timeout ; + + (* Remove any existing job first *) + remove_disable_job ~__context ~self ; + + let _ = + Xapi_stdext_threads_scheduler.Scheduler.add_to_queue task_name + Xapi_stdext_threads_scheduler.Scheduler.OneShot (Int64.to_float timeout) + (fun () -> + try + Xapi_systemctl.disable ~wait_until_success:false "sshd" ; + Xapi_systemctl.stop ~wait_until_success:false "sshd" ; + Db.Host.set_ssh_enabled ~__context ~self ~value:false ; + debug "Successfully disabled SSH for host %s" host_uuid + with e -> + error "Failed to disable SSH for host %s: %s" host_uuid + (Printexc.to_string e) + ) + in + + Db.Host.set_ssh_expiry ~__context ~self ~value:expiry_time + let enable_ssh ~__context ~self = try + debug "Enabling SSH for host %s" (Db.Host.get_uuid ~__context ~self) ; + Xapi_systemctl.enable ~wait_until_success:false "sshd" ; - Xapi_systemctl.start ~wait_until_success:false "sshd" - with _ -> + Xapi_systemctl.start ~wait_until_success:false "sshd" ; + + let timeout = Db.Host.get_ssh_enabled_timeout ~__context ~self in + ( match timeout with + | 0L -> + remove_disable_job ~__context ~self + | t when t > 0L -> + schedule_disable_job ~__context ~self ~timeout:(Int64.mul t 60L) + | _ -> + () + ) ; + + Db.Host.set_ssh_enabled ~__context ~self ~value:true + with e -> + error "Failed to enable SSH on host %s: %s" (Ref.string_of self) + (Printexc.to_string e) ; raise (Api_errors.Server_error - (Api_errors.enable_ssh_failed, [Ref.string_of self]) + ( Api_errors.enable_ssh_failed + , [Ref.string_of self; Printexc.to_string e] + ) ) let disable_ssh ~__context ~self = try + debug "Disabling SSH for host %s" (Db.Host.get_uuid ~__context ~self) ; + + remove_disable_job ~__context ~self ; + Xapi_systemctl.disable ~wait_until_success:false "sshd" ; - Xapi_systemctl.stop ~wait_until_success:false "sshd" - with _ -> + Xapi_systemctl.stop ~wait_until_success:false "sshd" ; + Db.Host.set_ssh_enabled ~__context ~self ~value:false ; + + let expiry_time = Date.now () |> Date.to_unix_time |> Date.of_unix_time in + Db.Host.set_ssh_expiry ~__context ~self ~value:expiry_time + with e -> + error "Failed to disable SSH on host %s: %s" (Ref.string_of self) + (Printexc.to_string e) ; raise (Api_errors.Server_error (Api_errors.disable_ssh_failed, [Ref.string_of self]) ) + +let set_ssh_enable_timeout ~__context ~self ~timeout = + let timeout = validate_timeout timeout in + debug "Setting SSH timeout for host %s to %Ld minutes" + (Db.Host.get_uuid ~__context ~self) + timeout ; + Db.Host.set_ssh_enabled_timeout ~__context ~self ~value:timeout ; + match Db.Host.get_ssh_enabled ~__context ~self with + | false -> + () + | true -> ( + match timeout with + | 0L -> + remove_disable_job ~__context ~self ; + Db.Host.set_ssh_expiry ~__context ~self ~value:Date.epoch + | t -> + schedule_disable_job ~__context ~self ~timeout:(Int64.mul t 60L) ; + Db.Host.set_ssh_enabled_timeout ~__context ~self ~value:timeout + ) + +let set_console_timeout ~__context ~self ~console_timeout = + let validate_timeout = function + | timeout when timeout >= 0L -> + timeout + | timeout -> + raise + (Api_errors.Server_error + ( Api_errors.invalid_value + , [ + "console_timeout" + ; Int64.to_string timeout + ; "must be a positive integer" + ] + ) + ) + in + + let console_timeout = validate_timeout console_timeout in + + let configure_timeout = function + | 0L -> + let script = + "sed -i '/^export TMOUT=/d' /root/.bashrc && source /root/.bashrc" + in + Helpers.call_script "/bin/bash" ["-c"; script] |> Result.ok + | timeout -> + let script = + Printf.sprintf + "sed -i '/^export TMOUT=/d' /root/.bashrc && echo 'export \ + TMOUT=%Ld' >> /root/.bashrc && source /root/.bashrc" + timeout + in + Helpers.call_script "/bin/bash" ["-c"; script] |> Result.ok + in + + configure_timeout console_timeout + |> Result.map (fun _ -> + Db.Host.set_console_idle_timeout ~__context ~self + ~value:console_timeout + ) + |> function + | Ok () -> + () + | Error e -> + error "Failed to configure console timeout: %s" (Printexc.to_string e) ; + raise + (Api_errors.Server_error + ( Api_errors.set_console_timeout_failed + , ["Failed to configure console timeout"; Printexc.to_string e] + ) + ) diff --git a/ocaml/xapi/xapi_host.mli b/ocaml/xapi/xapi_host.mli index e1dc46c91ac..a1ed665e660 100644 --- a/ocaml/xapi/xapi_host.mli +++ b/ocaml/xapi/xapi_host.mli @@ -567,3 +567,12 @@ val emergency_clear_mandatory_guidance : __context:Context.t -> unit val enable_ssh : __context:Context.t -> self:API.ref_host -> unit val disable_ssh : __context:Context.t -> self:API.ref_host -> unit + +val set_ssh_enable_timeout : + __context:Context.t -> self:API.ref_host -> timeout:int64 -> unit + +val set_console_timeout : + __context:Context.t -> self:API.ref_host -> console_timeout:int64 -> unit + +val schedule_disable_job : + __context:Context.t -> self:API.ref_host -> timeout:int64 -> unit diff --git a/ocaml/xapi/xapi_pool.ml b/ocaml/xapi/xapi_pool.ml index 68c41f91a42..4ee3be6d406 100644 --- a/ocaml/xapi/xapi_pool.ml +++ b/ocaml/xapi/xapi_pool.ml @@ -4008,3 +4008,25 @@ end let enable_ssh = Ssh.enable let disable_ssh = Ssh.disable + +let set_ssh_enable_timeout ~__context ~self:_ ~timeout = + let hosts = Db.Host.get_all ~__context in + List.iter + (fun host -> + Helpers.call_api_functions ~__context (fun rpc session_id -> + Client.Host.set_ssh_enable_timeout ~rpc ~session_id ~self:host + ~timeout + ) + ) + hosts + +let set_console_timeout ~__context ~self:_ ~console_timeout = + let hosts = Db.Host.get_all ~__context in + List.iter + (fun host -> + Helpers.call_api_functions ~__context (fun rpc session_id -> + Client.Host.set_console_timeout ~rpc ~session_id ~self:host + ~console_timeout + ) + ) + hosts diff --git a/ocaml/xapi/xapi_pool.mli b/ocaml/xapi/xapi_pool.mli index 7d00d339805..6b70087243a 100644 --- a/ocaml/xapi/xapi_pool.mli +++ b/ocaml/xapi/xapi_pool.mli @@ -437,3 +437,9 @@ val put_bundle_handler : Http.Request.t -> Unix.file_descr -> 'a -> unit val enable_ssh : __context:Context.t -> self:API.ref_pool -> unit val disable_ssh : __context:Context.t -> self:API.ref_pool -> unit + +val set_ssh_enable_timeout : + __context:Context.t -> self:API.ref_pool -> timeout:int64 -> unit + +val set_console_timeout : + __context:Context.t -> self:API.ref_pool -> console_timeout:int64 -> unit