Skip to content

Commit

Permalink
CP-50787 CP-51347: Support pool.sync_updates from remote_pool repo
Browse files Browse the repository at this point in the history
When a remote_pool type repository, which points to the enabled
repository in the remote pool coordinator, is set as the enabled
repository of the pool, updates can be synced from it with API
pool.sync_updates.

The username password of the remote pool coordinator is required as
parameters for pool.sync_updates to login the remote pool.

And the remote pool coordinator's host server certificate needs to be
configured in the remote_pool repository, it will be used to verify the
remote end when sending out username passwords.

A new yum/dnf plugin "xapitoken" is introduced to set xapi token as HTTP
cookie: "session_id" for each HTTP request which downloads files from the
remote_pool repository.

Signed-off-by: Gang Ji <[email protected]>
  • Loading branch information
gangj committed Nov 8, 2024
1 parent 7128d43 commit f3f21f1
Show file tree
Hide file tree
Showing 14 changed files with 293 additions and 46 deletions.
14 changes: 14 additions & 0 deletions ocaml/idl/datamodel_pool.ml
Original file line number Diff line number Diff line change
Expand Up @@ -1282,6 +1282,20 @@ let sync_updates =
; param_release= numbered_release "1.329.0"
; param_default= Some (VString "")
}
; {
param_type= String
; param_name= "username"
; param_doc= "The username of the remote pool"
; param_release= numbered_release "2.21.0-next"
; param_default= Some (VString "")
}
; {
param_type= String
; param_name= "password"
; param_doc= "The password of the remote pool"
; param_release= numbered_release "2.21.0-next"
; param_default= Some (VString "")
}
]
~result:(String, "The SHA256 hash of updateinfo.xml.gz")
~allowed_roles:(_R_POOL_OP ++ _R_CLIENT_CERT)
Expand Down
2 changes: 1 addition & 1 deletion ocaml/xapi-cli-server/cli_frontend.ml
Original file line number Diff line number Diff line change
Expand Up @@ -511,7 +511,7 @@ let rec cmdtable_data : (string * cmd_spec) list =
; ( "pool-sync-updates"
, {
reqd= []
; optn= ["force"; "token"; "token-id"]
; optn= ["force"; "token"; "token-id"; "username"; "password"]
; help= "Sync updates from remote YUM repository, pool-wide."
; implementation= No_fd Cli_operations.pool_sync_updates
; flags= []
Expand Down
3 changes: 3 additions & 0 deletions ocaml/xapi-cli-server/cli_operations.ml
Original file line number Diff line number Diff line change
Expand Up @@ -1833,8 +1833,11 @@ let pool_sync_updates printer rpc session_id params =
let force = get_bool_param params "force" in
let token = get_param params "token" ~default:"" in
let token_id = get_param params "token-id" ~default:"" in
let username = get_param params "username" ~default:"" in
let password = get_param params "password" ~default:"" in
let hash =
Client.Pool.sync_updates ~rpc ~session_id ~self:pool ~force ~token ~token_id
~username ~password
in
printer (Cli_printer.PList [hash])

Expand Down
14 changes: 10 additions & 4 deletions ocaml/xapi/helpers.ml
Original file line number Diff line number Diff line change
Expand Up @@ -2011,16 +2011,22 @@ let with_temp_file ?mode prefix suffix f =
let path, channel = Filename.open_temp_file ?mode prefix suffix in
finally (fun () -> f (path, channel)) (fun () -> Unix.unlink path)

let with_temp_file_of_content ?mode prefix suffix content f =
let@ temp_file, temp_out_ch = with_temp_file ?mode prefix suffix in
Xapi_stdext_pervasives.Pervasiveext.finally
(fun () -> output_string temp_out_ch content)
(fun () -> close_out temp_out_ch) ;
f temp_file

let with_temp_out_ch_of_temp_file ?mode prefix suffix f =
let@ path, channel = with_temp_file ?mode prefix suffix in
f (path, channel |> with_temp_out_ch)

let make_external_host_verified_rpc ~__context ext_host_address ext_host_cert
xml =
let@ temp_file, temp_out_ch = with_temp_file "external-host-cert" ".pem" in
Xapi_stdext_pervasives.Pervasiveext.finally
(fun () -> output_string temp_out_ch ext_host_cert)
(fun () -> close_out temp_out_ch) ;
let@ temp_file =
with_temp_file_of_content "external-host-cert-" ".pem" ext_host_cert
in
make_remote_rpc ~__context
~verify_cert:(Stunnel_client.external_host temp_file)
ext_host_address xml
Expand Down
3 changes: 2 additions & 1 deletion ocaml/xapi/pool_periodic_update_sync.ml
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,8 @@ let rec update_sync () =
ignore
(Client.Pool.sync_updates ~rpc ~session_id
~self:(Helpers.get_pool ~__context)
~force:false ~token:"" ~token_id:""
~force:false ~token:"" ~token_id:"" ~username:""
~password:""
)
with e ->
let exc = Printexc.to_string e in
Expand Down
149 changes: 116 additions & 33 deletions ocaml/xapi/repository.ml
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,15 @@ let get_proxy_params ~__context repo_name =
| _ ->
("", "", "")

let sync ~__context ~self ~token ~token_id =
let ext_host_verified_rpc ~__context ~cert host_address xml =
try Helpers.make_external_host_verified_rpc ~__context host_address cert xml
with Xmlrpc_client.Connection_reset ->
raise
(Api_errors.Server_error
(Api_errors.pool_joining_host_connection_failed, [])
)

let sync ~__context ~self ~token ~token_id ~remote_addr ~username ~password =
try
let repo_name = get_remote_repository_name ~__context ~self in
remove_repo_conf_file repo_name ;
Expand All @@ -163,10 +171,12 @@ let sync ~__context ~self ~token ~token_id =
in
(Uri.to_string uri, None)
| `remote_pool ->
(* TODO: sync with Stunnel.with_client_proxy as otherwise yum
reposync will fail when checking the self signed certificate on
the remote pool. *)
("", None)
let uri =
Uri.make ~scheme:"http" ~host:"127.0.0.1"
~port:!Xapi_globs.local_yum_repo_port
~path:"/repository/enabled/" ()
in
(Uri.to_string uri, None)
in
let gpgkey_path =
match Db.Repository.get_gpgkey_path ~__context ~self with
Expand All @@ -186,39 +196,112 @@ let sync ~__context ~self ~token ~token_id =
Xapi_stdext_unix.Unixext.rm_rec (get_repo_config repo_name "gpgdir") ;
Xapi_stdext_pervasives.Pervasiveext.finally
(fun () ->
with_access_token ~token ~token_id @@ fun token_path ->
(* Configure proxy and token *)
let token_param =
match token_path with
| Some p ->
Printf.sprintf "--setopt=%s.accesstoken=file://%s" repo_name p
| None ->
let cert =
match
Repository_helpers.is_remote_pool_repository ~__context ~repo:self
with
| true ->
Db.Repository.get_certificate ~__context ~self
| false ->
""
in
let proxy_url_param, proxy_username_param, proxy_password_param =
get_proxy_params ~__context repo_name
let make_cache () =
(* Import YUM repository GPG key to check metadata in reposync *)
let Pkg_mgr.{cmd; params} = Pkgs.make_cache ~repo_name in
ignore (Helpers.call_script cmd params)
in
let Pkg_mgr.{cmd; params} =
[
"--save"
; proxy_url_param
; proxy_username_param
; proxy_password_param
; token_param
]
|> fun config -> Pkgs.config_repo ~repo_name ~config
in
ignore (Helpers.call_script ~log_output:Helpers.On_failure cmd params) ;

(* Import YUM repository GPG key to check metadata in reposync *)
let Pkg_mgr.{cmd; params} = Pkgs.make_cache ~repo_name in
ignore (Helpers.call_script cmd params) ;

(* Sync with remote repository *)
let Pkg_mgr.{cmd; params} = Pkgs.sync_repo ~repo_name in
Unixext.mkdir_rec !Xapi_globs.local_pool_repo_dir 0o700 ;
clean_yum_cache repo_name ;
ignore (Helpers.call_script cmd params)
let sync_repo () =
let Pkg_mgr.{cmd; params} = Pkgs.sync_repo ~repo_name in
Unixext.mkdir_rec !Xapi_globs.local_pool_repo_dir 0o700 ;
clean_yum_cache repo_name ;
ignore (Helpers.call_script cmd params)
in
match Db.Repository.get_origin ~__context ~self with
| `remote ->
with_access_token ~token ~token_id @@ fun token_path ->
(* Configure proxy and token *)
let token_param =
match token_path with
| Some p ->
Printf.sprintf "--setopt=%s.accesstoken=file://%s" repo_name p
| None ->
""
in
let proxy_url_param, proxy_username_param, proxy_password_param =
get_proxy_params ~__context repo_name
in
let Pkg_mgr.{cmd; params} =
[
"--save"
; proxy_url_param
; proxy_username_param
; proxy_password_param
; token_param
]
|> fun config -> Pkgs.config_repo ~repo_name ~config
in
ignore
(Helpers.call_script ~log_output:Helpers.On_failure cmd params) ;
make_cache () ;
sync_repo ()
| `bundle ->
make_cache () ; sync_repo ()
| `remote_pool ->
let verified_rpc =
ext_host_verified_rpc ~__context ~cert remote_addr
in
let session_id =
try
Client.Client.Session.login_with_password ~rpc:verified_rpc
~uname:username ~pwd:password
~version:Datamodel_common.api_version_string
~originator:Xapi_version.xapi_user_agent
with
| Http_client.Http_request_rejected _ | Http_client.Http_error _
->
raise
(Api_errors.Server_error
(Api_errors.pool_joining_host_service_failed, [])
)
in
let xapi_token = session_id |> Ref.string_of in
with_xapi_token ~xapi_token @@ fun token_path ->
(* Configure xapi token *)
let token_param =
match token_path with
| Some p ->
Printf.sprintf "--setopt=%s.xapitoken=file://%s" repo_name p
| None ->
""
in
let proxy_url_param, proxy_username_param, proxy_password_param =
get_proxy_params ~__context repo_name
in
let Pkg_mgr.{cmd; params} =
[
"--save"
; proxy_url_param
; proxy_username_param
; proxy_password_param
; token_param
]
|> fun config -> Pkgs.config_repo ~repo_name ~config
in
ignore
(Helpers.call_script ~log_output:Helpers.On_failure cmd params) ;
let ( let@ ) f x = f x in
let@ temp_file =
Helpers.with_temp_file_of_content "external-host-cert-" ".pem"
cert
in
Stunnel.with_client_proxy
~verify_cert:(Stunnel_client.external_host temp_file)
~remote_host:remote_addr ~remote_port:Constants.default_ssl_port
~local_host:"127.0.0.1"
~local_port:!Xapi_globs.local_yum_repo_port
@@ fun () -> make_cache () ; sync_repo ()
)
(fun () ->
(* Rewrite repo conf file as initial content to remove credential related info,
Expand Down
3 changes: 3 additions & 0 deletions ocaml/xapi/repository.mli
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ val sync :
-> self:[`Repository] API.Ref.t
-> token:string
-> token_id:string
-> remote_addr:string
-> username:string
-> password:string
-> unit

val create_pool_repository :
Expand Down
28 changes: 26 additions & 2 deletions ocaml/xapi/repository_helpers.ml
Original file line number Diff line number Diff line change
Expand Up @@ -231,18 +231,22 @@ let assert_gpgkey_path_is_valid path =
raise Api_errors.(Server_error (invalid_gpgkey_path, [path]))
)

let assert_remote_pool_url_is_valid ~url =
let get_remote_pool_coordinator_ip url =
let uri = Uri.of_string url in
match (Uri.scheme uri, Uri.host uri, Uri.path uri) with
| Some "https", Some host, path
when path = Constants.get_enabled_repository_uri
&& Helpers.is_valid_ip `ipv4or6 host ->
()
host
| _ ->
error "Invalid url: %s, expected url format: %s" url
("https://<coordinator-ip>" ^ Constants.get_enabled_repository_uri) ;
raise Api_errors.(Server_error (invalid_base_url, [url]))

let assert_remote_pool_url_is_valid ~url =
get_remote_pool_coordinator_ip url
|> Xapi_stdext_pervasives.Pervasiveext.ignore_string

let with_pool_repositories f =
Xapi_stdext_pervasives.Pervasiveext.finally
(fun () ->
Expand Down Expand Up @@ -1268,6 +1272,9 @@ let append_by_key l k v =
in
(k, vals) :: others

let is_remote_pool_repository ~__context ~repo =
Db.Repository.get_origin ~__context ~self:repo = `remote_pool

let get_singleton = function
| [s] ->
s
Expand Down Expand Up @@ -1305,6 +1312,23 @@ let with_access_token ~token ~token_id f =
let msg = Printf.sprintf "%s: The token or token_id is empty" __LOC__ in
raise Api_errors.(Server_error (internal_error, [msg]))

let with_xapi_token ~xapi_token f =
match xapi_token with
| t when t <> "" ->
let json = `Assoc [("xapitoken", `String t)] in
let tmpfile, tmpch =
Filename.open_temp_file ~mode:[Open_text] "xapitoken" ".json"
in
Xapi_stdext_pervasives.Pervasiveext.finally
(fun () ->
output_string tmpch (Yojson.Basic.to_string json) ;
close_out tmpch ;
f (Some tmpfile)
)
(fun () -> Unixext.unlink_safe tmpfile)
| _ ->
f None

let prune_updateinfo_for_livepatches latest_lps updateinfo =
let livepatches =
let open LivePatch in
Expand Down
23 changes: 18 additions & 5 deletions ocaml/xapi/xapi_pool.ml
Original file line number Diff line number Diff line change
Expand Up @@ -3487,12 +3487,25 @@ let remove_repository ~__context ~self ~value =
if Db.Pool.get_repositories ~__context ~self = [] then
Db.Pool.set_last_update_sync ~__context ~self ~value:Date.epoch

let sync_repos ~__context ~self ~repos ~force ~token ~token_id =
let sync_repos ~__context ~self ~repos ~force ~token ~token_id ~username
~password =
let remote_addr =
let repo =
Repository_helpers.get_single_enabled_update_repository ~__context
in
match Repository_helpers.is_remote_pool_repository ~__context ~repo with
| true ->
Db.Repository.get_binary_url ~__context ~self:repo
|> Repository_helpers.get_remote_pool_coordinator_ip
| false ->
""
in
let open Repository in
repos
|> List.iter (fun repo ->
if force then cleanup_pool_repo ~__context ~self:repo ;
sync ~__context ~self:repo ~token ~token_id ;
sync ~__context ~self:repo ~token ~token_id ~remote_addr ~username
~password ;
(* Dnf sync all the metadata including updateinfo,
* Thus no need to re-create pool repository *)
if Pkgs.manager = Yum then
Expand All @@ -3502,14 +3515,14 @@ let sync_repos ~__context ~self ~repos ~force ~token ~token_id =
Db.Pool.set_last_update_sync ~__context ~self ~value:(Date.now ()) ;
checksum

let sync_updates ~__context ~self ~force ~token ~token_id =
let sync_updates ~__context ~self ~force ~token ~token_id ~username ~password =
Pool_features.assert_enabled ~__context ~f:Features.Updates ;
Xapi_pool_helpers.with_pool_operation ~__context ~self
~doc:"pool.sync_updates" ~op:`sync_updates
@@ fun () ->
let repos = Repository_helpers.get_enabled_repositories ~__context in
assert_not_bundle_repo ~__context ~repos ;
sync_repos ~__context ~self ~repos ~force ~token ~token_id
sync_repos ~__context ~self ~repos ~force ~token ~token_id ~username ~password

let check_update_readiness ~__context ~self:_ ~requires_reboot =
(* Pool license check *)
Expand Down Expand Up @@ -3883,7 +3896,7 @@ let put_bundle_handler (req : Request.t) s _ =
(fun () ->
try
sync_repos ~__context ~self:pool ~repos:[repo] ~force:true
~token:"" ~token_id:""
~token:"" ~token_id:"" ~username:"" ~password:""
|> ignore
with _ ->
raise Api_errors.(Server_error (bundle_sync_failed, []))
Expand Down
2 changes: 2 additions & 0 deletions ocaml/xapi/xapi_pool.mli
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,8 @@ val sync_updates :
-> force:bool
-> token:string
-> token_id:string
-> username:string
-> password:string
-> string

val check_update_readiness :
Expand Down
Loading

0 comments on commit f3f21f1

Please sign in to comment.