diff --git a/ocaml/forkexecd/lib/fe_systemctl.ml b/ocaml/forkexecd/lib/fe_systemctl.ml index 046396002ca..97af823be67 100644 --- a/ocaml/forkexecd/lib/fe_systemctl.ml +++ b/ocaml/forkexecd/lib/fe_systemctl.ml @@ -121,15 +121,19 @@ let stop ~service = Xapi_stdext_unix.Unixext.unlink_safe destination ; status -let is_active ~service = +let status ~command ~service = let status = Forkhelpers.safe_close_and_exec None None None [] systemctl - ["is-active"; "--quiet"; service] + [command; "--quiet"; service] |> Forkhelpers.waitpid |> snd in Unix.WEXITED 0 = status +let is_active ~service = status ~command:"is-active" ~service + +let is_enabled ~service = status ~command:"is-enabled" ~service + (** path to service file *) let path service = Filename.concat run_path (service ^ ".service") diff --git a/ocaml/forkexecd/lib/fe_systemctl.mli b/ocaml/forkexecd/lib/fe_systemctl.mli index 5ba44c4e290..908987f8432 100644 --- a/ocaml/forkexecd/lib/fe_systemctl.mli +++ b/ocaml/forkexecd/lib/fe_systemctl.mli @@ -45,6 +45,9 @@ val start_transient : val is_active : service:string -> bool (** [is_active ~service] checks whether the [service] is still running *) +val is_enabled : service:string -> bool +(** [is_enabled ~service] checks whether the [service] is enabled *) + val show : service:string -> status (** [shows ~service] retrieves the exitcodes and PIDs of the specified [service] *) diff --git a/ocaml/idl/datamodel_errors.ml b/ocaml/idl/datamodel_errors.ml index fed2f830db1..954c9d7452e 100644 --- a/ocaml/idl/datamodel_errors.ml +++ b/ocaml/idl/datamodel_errors.ml @@ -2010,6 +2010,18 @@ let _ = error Api_errors.too_many_groups [] ~doc:"VM can only belong to one group." () ; + error Api_errors.enable_ssh_failed ["host"] + ~doc:"Failed to enable SSH access." () ; + + error Api_errors.disable_ssh_failed ["host"] + ~doc:"Failed to disable SSH access." () ; + + error Api_errors.enable_ssh_partially_failed ["hosts"] + ~doc:"Some of hosts failed to enable SSH access." () ; + + error Api_errors.disable_ssh_partially_failed ["hosts"] + ~doc:"Some of hosts failed to disable SSH access." () ; + message (fst Api_messages.ha_pool_overcommitted) ~doc: diff --git a/ocaml/idl/datamodel_host.ml b/ocaml/idl/datamodel_host.ml index 78b68a35722..737cfd83680 100644 --- a/ocaml/idl/datamodel_host.ml +++ b/ocaml/idl/datamodel_host.ml @@ -2338,6 +2338,28 @@ let emergency_clear_mandatory_guidance = ~doc:"Clear the pending mandatory guidance on this host" ~allowed_roles:_R_LOCAL_ROOT_ONLY () +let enable_ssh = + call ~name:"enable_ssh" + ~doc: + "Enable SSH access on the host. It will start the service sshd only if \ + it is not running. It will also enable the service sshd only if it is \ + not enabled. A newly joined host in the pool or an ejected host from \ + the pool would keep the original status." + ~lifecycle:[] + ~params:[(Ref _host, "self", "The host")] + ~allowed_roles:_R_POOL_ADMIN () + +let disable_ssh = + call ~name:"disable_ssh" + ~doc: + "Disable SSH access on the host. It will stop the service sshd only if \ + it is running. It will also disable the service sshd only if it is \ + enabled. A newly joined host in the pool or an ejected host from the \ + pool would keep the original status." + ~lifecycle:[] + ~params:[(Ref _host, "self", "The host")] + ~allowed_roles:_R_POOL_ADMIN () + let latest_synced_updates_applied_state = Enum ( "latest_synced_updates_applied_state" @@ -2494,6 +2516,8 @@ let t = ; set_https_only ; apply_recommended_guidances ; emergency_clear_mandatory_guidance + ; enable_ssh + ; disable_ssh ] ~contents: ([ diff --git a/ocaml/idl/datamodel_pool.ml b/ocaml/idl/datamodel_pool.ml index ab0d1669788..6e468367132 100644 --- a/ocaml/idl/datamodel_pool.ml +++ b/ocaml/idl/datamodel_pool.ml @@ -1539,6 +1539,24 @@ let get_guest_secureboot_readiness = ~result:(pool_guest_secureboot_readiness, "The readiness of the pool") ~allowed_roles:_R_POOL_OP () +let enable_ssh = + call ~name:"enable_ssh" + ~doc: + "Enable SSH access on all hosts in the pool. It's a helper which calls \ + host.enable_ssh for all the hosts in the pool." + ~lifecycle:[] + ~params:[(Ref _pool, "self", "The pool")] + ~allowed_roles:_R_POOL_ADMIN () + +let disable_ssh = + call ~name:"disable_ssh" + ~doc: + "Disable SSH access on all hosts in the pool. It's a helper which calls \ + host.disable_ssh for all the hosts in the pool." + ~lifecycle:[] + ~params:[(Ref _pool, "self", "The pool")] + ~allowed_roles:_R_POOL_ADMIN () + (** A pool class *) let t = create_obj ~in_db:true @@ -1633,6 +1651,8 @@ let t = ; set_ext_auth_cache_size ; set_ext_auth_cache_expiry ; get_guest_secureboot_readiness + ; enable_ssh + ; disable_ssh ] ~contents: ([ diff --git a/ocaml/sdk-gen/go/gen_go_helper.ml b/ocaml/sdk-gen/go/gen_go_helper.ml index 47540f55ef7..84b91260ae2 100644 --- a/ocaml/sdk-gen/go/gen_go_helper.ml +++ b/ocaml/sdk-gen/go/gen_go_helper.ml @@ -38,6 +38,7 @@ let acronyms = ; "db" ; "xml" ; "eof" + ; "ssh" ] |> StringSet.of_list diff --git a/ocaml/xapi-cli-server/cli_frontend.ml b/ocaml/xapi-cli-server/cli_frontend.ml index 3de231f3cad..82c642da64c 100644 --- a/ocaml/xapi-cli-server/cli_frontend.ml +++ b/ocaml/xapi-cli-server/cli_frontend.ml @@ -1048,6 +1048,32 @@ let rec cmdtable_data : (string * cmd_spec) list = ; flags= [Host_selectors] } ) + ; ( "host-enable-ssh" + , { + reqd= [] + ; optn= [] + ; help= + "Enable SSH access on the host. It will start the service sshd only \ + if it is not running. It will also enable the service sshd only if \ + it is not enabled. A newly joined host in the pool or an ejected \ + host from the pool would keep the original status." + ; implementation= No_fd Cli_operations.host_enable_ssh + ; flags= [Host_selectors] + } + ) + ; ( "host-disable-ssh" + , { + reqd= [] + ; optn= [] + ; help= + "Disable SSH access on the host. It will stop the service sshd only \ + if it is running. It will also disable the service sshd only if it \ + is enabled. A newly joined host in the pool or an ejected host from \ + the pool would keep the original status." + ; implementation= No_fd Cli_operations.host_disable_ssh + ; flags= [Host_selectors] + } + ) ; ( "host-emergency-clear-mandatory-guidance" , { reqd= [] @@ -3105,6 +3131,28 @@ let rec cmdtable_data : (string * cmd_spec) list = ; flags= [] } ) + ; ( "pool-enable-ssh" + , { + reqd= [] + ; optn= [] + ; help= + "Enable SSH access on all hosts in the pool. It's a helper which \ + calls host.enable_ssh for all the hosts in the pool." + ; implementation= No_fd Cli_operations.pool_enable_ssh + ; flags= [] + } + ) + ; ( "pool-disable-ssh" + , { + reqd= [] + ; optn= [] + ; help= + "Disable SSH access on all hosts in the pool. It's a helper which \ + calls host.disable_ssh for all the hosts in the pool." + ; implementation= No_fd Cli_operations.pool_disable_ssh + ; flags= [] + } + ) ; ( "host-ha-xapi-healthcheck" , { reqd= [] diff --git a/ocaml/xapi-cli-server/cli_operations.ml b/ocaml/xapi-cli-server/cli_operations.ml index 1e8ba0f3b37..f8e1a0606ae 100644 --- a/ocaml/xapi-cli-server/cli_operations.ml +++ b/ocaml/xapi-cli-server/cli_operations.ml @@ -6779,6 +6779,14 @@ let pool_sync_bundle fd _printer rpc session_id params = | None -> failwith "Required parameter not found: filename" +let pool_enable_ssh _printer rpc session_id params = + let pool = get_pool_with_default rpc session_id params "uuid" in + Client.Pool.enable_ssh ~rpc ~session_id ~self:pool + +let pool_disable_ssh _printer rpc session_id params = + let pool = get_pool_with_default rpc session_id params "uuid" in + Client.Pool.disable_ssh ~rpc ~session_id ~self:pool + let host_restore fd _printer rpc session_id params = let filename = List.assoc "file-name" params in let op _ host = @@ -7729,6 +7737,26 @@ let host_apply_updates _printer rpc session_id params = params ["hash"] ) +let host_enable_ssh _printer rpc session_id params = + ignore + (do_host_op rpc session_id + (fun _ host -> + let host = host.getref () in + Client.Host.enable_ssh ~rpc ~session_id ~self:host + ) + params [] + ) + +let host_disable_ssh _printer rpc session_id params = + ignore + (do_host_op rpc session_id + (fun _ host -> + let host = host.getref () in + Client.Host.disable_ssh ~rpc ~session_id ~self:host + ) + params [] + ) + module SDN_controller = struct let introduce printer rpc session_id params = let port = diff --git a/ocaml/xapi-consts/api_errors.ml b/ocaml/xapi-consts/api_errors.ml index 54bdd6f6660..9bd3937a89d 100644 --- a/ocaml/xapi-consts/api_errors.ml +++ b/ocaml/xapi-consts/api_errors.ml @@ -1403,3 +1403,11 @@ let telemetry_next_collection_too_late = let illegal_in_fips_mode = add_error "ILLEGAL_IN_FIPS_MODE" let too_many_groups = add_error "TOO_MANY_GROUPS" + +let enable_ssh_failed = add_error "ENABLE_SSH_FAILED" + +let disable_ssh_failed = add_error "DISABLE_SSH_FAILED" + +let enable_ssh_partially_failed = add_error "ENABLE_SSH_PARTIALLY_FAILED" + +let disable_ssh_partially_failed = add_error "DISABLE_SSH_PARTIALLY_FAILED" diff --git a/ocaml/xapi/message_forwarding.ml b/ocaml/xapi/message_forwarding.ml index 63b27076a1a..186516dcae5 100644 --- a/ocaml/xapi/message_forwarding.ml +++ b/ocaml/xapi/message_forwarding.ml @@ -1185,6 +1185,14 @@ functor let get_guest_secureboot_readiness ~__context ~self = info "%s: pool='%s'" __FUNCTION__ (pool_uuid ~__context self) ; Local.Pool.get_guest_secureboot_readiness ~__context ~self + + let enable_ssh ~__context ~self = + info "%s: pool = '%s'" __FUNCTION__ (pool_uuid ~__context self) ; + Local.Pool.enable_ssh ~__context ~self + + let disable_ssh ~__context ~self = + info "%s: pool = '%s'" __FUNCTION__ (pool_uuid ~__context self) ; + Local.Pool.disable_ssh ~__context ~self end module VM = struct @@ -4154,6 +4162,20 @@ functor let emergency_clear_mandatory_guidance ~__context = info "Host.emergency_clear_mandatory_guidance" ; Local.Host.emergency_clear_mandatory_guidance ~__context + + let enable_ssh ~__context ~self = + info "%s: host = '%s'" __FUNCTION__ (host_uuid ~__context self) ; + let local_fn = Local.Host.enable_ssh ~self in + do_op_on ~local_fn ~__context ~host:self (fun session_id rpc -> + Client.Host.enable_ssh ~rpc ~session_id ~self + ) + + let disable_ssh ~__context ~self = + info "%s: host = '%s'" __FUNCTION__ (host_uuid ~__context self) ; + let local_fn = Local.Host.disable_ssh ~self in + do_op_on ~local_fn ~__context ~host:self (fun session_id rpc -> + Client.Host.disable_ssh ~rpc ~session_id ~self + ) end module Host_crashdump = struct diff --git a/ocaml/xapi/xapi_host.ml b/ocaml/xapi/xapi_host.ml index cd6ae3a7d35..b543cfa63dc 100644 --- a/ocaml/xapi/xapi_host.ml +++ b/ocaml/xapi/xapi_host.ml @@ -3127,3 +3127,23 @@ let emergency_clear_mandatory_guidance ~__context = info "%s: %s is cleared" __FUNCTION__ s ) ; Db.Host.set_pending_guidances ~__context ~self ~value:[] + +let enable_ssh ~__context ~self = + try + Xapi_systemctl.enable ~wait_until_success:false "sshd" ; + Xapi_systemctl.start ~wait_until_success:false "sshd" + with _ -> + raise + (Api_errors.Server_error + (Api_errors.enable_ssh_failed, [Ref.string_of self]) + ) + +let disable_ssh ~__context ~self = + try + Xapi_systemctl.disable ~wait_until_success:false "sshd" ; + Xapi_systemctl.stop ~wait_until_success:false "sshd" + with _ -> + raise + (Api_errors.Server_error + (Api_errors.disable_ssh_failed, [Ref.string_of self]) + ) diff --git a/ocaml/xapi/xapi_host.mli b/ocaml/xapi/xapi_host.mli index f8fe73f8379..a2ec3b31831 100644 --- a/ocaml/xapi/xapi_host.mli +++ b/ocaml/xapi/xapi_host.mli @@ -561,3 +561,7 @@ val set_https_only : __context:Context.t -> self:API.ref_host -> value:bool -> unit 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 diff --git a/ocaml/xapi/xapi_pool.ml b/ocaml/xapi/xapi_pool.ml index 2f471932c14..14a830712c6 100644 --- a/ocaml/xapi/xapi_pool.ml +++ b/ocaml/xapi/xapi_pool.ml @@ -3952,3 +3952,37 @@ let put_bundle_handler (req : Request.t) s _ = | None -> () ) + +module Ssh = struct + let operate ~__context ~action ~error = + let hosts = Db.Host.get_all ~__context in + Helpers.call_api_functions ~__context (fun rpc session_id -> + let failed_hosts = + List.fold_left + (fun failed_hosts host -> + try + action ~rpc ~session_id ~self:host ; + failed_hosts + with _ -> Ref.string_of host :: failed_hosts + ) + [] hosts + in + match failed_hosts with + | [] -> + () + | _ -> + raise (Api_errors.Server_error (error, failed_hosts)) + ) + + let enable ~__context ~self:_ = + operate ~__context ~action:Client.Host.enable_ssh + ~error:Api_errors.enable_ssh_partially_failed + + let disable ~__context ~self:_ = + operate ~__context ~action:Client.Host.disable_ssh + ~error:Api_errors.disable_ssh_partially_failed +end + +let enable_ssh = Ssh.enable + +let disable_ssh = Ssh.disable diff --git a/ocaml/xapi/xapi_pool.mli b/ocaml/xapi/xapi_pool.mli index 835a356f782..d17e667fbb1 100644 --- a/ocaml/xapi/xapi_pool.mli +++ b/ocaml/xapi/xapi_pool.mli @@ -434,3 +434,7 @@ val get_guest_secureboot_readiness : -> API.pool_guest_secureboot_readiness 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 diff --git a/ocaml/xapi/xapi_systemctl.ml b/ocaml/xapi/xapi_systemctl.ml index 1dbca594a0c..448d565a266 100644 --- a/ocaml/xapi/xapi_systemctl.ml +++ b/ocaml/xapi/xapi_systemctl.ml @@ -19,7 +19,7 @@ module D = Debug.Make (struct let name = "xapi_systemctl" end) open D -type t = Start | Stop | Restart +type t = Start | Stop | Restart | Enable | Disable exception Systemctl_fail of string @@ -30,6 +30,10 @@ let to_string = function "stop" | Restart -> "restart" + | Enable -> + "enable" + | Disable -> + "disable" let perform ~wait_until_success ~service ~timeout op = let op_str = op |> to_string in @@ -42,8 +46,17 @@ let perform ~wait_until_success ~service ~timeout op = if wait_until_success then ( if op = Restart then Thread.delay 0.1 ; let is_active = Fe_systemctl.is_active ~service in + let is_enabled = Fe_systemctl.is_enabled ~service in let success_cond () = - match op with Start | Restart -> is_active | Stop -> is_active |> not + match op with + | Start | Restart -> + is_active + | Stop -> + is_active |> not + | Enable -> + is_enabled + | Disable -> + is_enabled |> not in try Helpers.retry_until_timeout ~timeout @@ -62,7 +75,17 @@ let restart ?(timeout = 5.) ~wait_until_success service = perform ~wait_until_success ~service ~timeout Restart let stop ?(timeout = 5.) ~wait_until_success service = - perform ~wait_until_success ~service ~timeout Stop + if Fe_systemctl.is_active ~service then + perform ~wait_until_success ~service ~timeout Stop let start ?(timeout = 5.) ~wait_until_success service = - perform ~wait_until_success ~service ~timeout Start + if not (Fe_systemctl.is_active ~service) then + perform ~wait_until_success ~service ~timeout Start + +let disable ?(timeout = 5.) ~wait_until_success service = + if Fe_systemctl.is_enabled ~service then + perform ~wait_until_success ~service ~timeout Disable + +let enable ?(timeout = 5.) ~wait_until_success service = + if not (Fe_systemctl.is_enabled ~service) then + perform ~wait_until_success ~service ~timeout Enable diff --git a/ocaml/xapi/xapi_systemctl.mli b/ocaml/xapi/xapi_systemctl.mli index 2660839f9b7..b552afebbbc 100644 --- a/ocaml/xapi/xapi_systemctl.mli +++ b/ocaml/xapi/xapi_systemctl.mli @@ -15,6 +15,10 @@ (* Exception about systemctl operation like start/stop failed *) exception Systemctl_fail of string +type t = Start | Stop | Restart | Enable | Disable + +val to_string : t -> string + (* start a service with systemctl *) val start : ?timeout:float -> wait_until_success:bool -> string -> unit @@ -23,3 +27,9 @@ val stop : ?timeout:float -> wait_until_success:bool -> string -> unit (* restart a service with systemctl *) val restart : ?timeout:float -> wait_until_success:bool -> string -> unit + +(* enable a service with systemctl *) +val enable : ?timeout:float -> wait_until_success:bool -> string -> unit + +(* disable a service with systemctl *) +val disable : ?timeout:float -> wait_until_success:bool -> string -> unit