Packages

Synchronize a release on a remote node

Current section

Files

Jump to
relsync src relsync.erl
Raw

src/relsync.erl

%% Copyright 2014 Frank Hunleth
%% Copyright 2020 João Henrique Ferreira de Freitas
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%% @doc
%%% This is the script that runs the relsync logic on the host.
%%% @end
-module(relsync).
-export([
main/1,
init/1,
update_nodes/2,
setup_local_node/1,
format_error/1
]).
-include("relsync.hrl").
-spec main([string()]) -> no_return().
main(CmdLine) ->
Opts = relsync_cli:opts(),
case getopt:parse(Opts, CmdLine) of
{ok, {Options, []}} ->
relsync_cli:do("relsync", Options, []);
{error, Error} ->
io:put_chars(standard_error, [getopt:format_error(Opts, Error), "\n\n"]),
getopt:usage(relsync_cli:opts(), "relsync")
end.
%% rebar3 plugin entry point
-spec init(rebar_state:t()) -> {ok, rebar_state:t()}.
init(State) ->
rebar3_relsync_prv:init(State).
%% Use the command line parameters to setup the local
%% Erlang instance to be able to talk to remote nodes.
-spec setup_local_node(config()) -> ok | {error, set_cookie} | {error, net_kernel, term()}.
setup_local_node(Config) ->
% First, make sure that Epmd is running
case net_adm:names() of
%% Epmd is running
{ok, _} ->
ok;
{error, address} ->
Epmd = os:find_executable("epmd"),
os:cmd(Epmd ++ " -daemon")
end,
% Next, start up net_kernel
case net_kernel:start(options_to_netkernel(Config)) of
{ok, _} ->
case erlang:is_alive() of
true ->
Cookie = Config#config.cookie,
erlang:set_cookie(node(), list_to_atom(Cookie)),
ok;
false ->
{error, set_cookie}
end;
{error, {already_started, _Pid}} ->
ok;
{error, Reason} ->
{error, net_kernel, Reason}
end.
-spec options_to_netkernel(config()) -> [atom()] | no_return().
options_to_netkernel(Config) ->
case Config#config.sname of
undefined ->
case Config#config.name of
undefined ->
exit({badargs, "Specify --sname or --name"});
Name ->
[list_to_atom(Name), longnames]
end;
SName ->
[list_to_atom(SName), shortnames]
end.
update_nodes(ssh, Config) ->
ok = ssh:start(),
DestNode = Config#config.destnode,
update_node(ssh, [DestNode], Config);
update_nodes(erl, Config) ->
% Only support one node for now. In theory, we could update
% a swarm of devices. That would be wild.
DestNode = Config#config.destnode,
update_node(erl, [list_to_atom(DestNode)], Config).
update_node(ssh, [], _Config) ->
ok;
update_node(ssh, [Node | T], Config) ->
io:format("Updating ~p...~n", [Node]),
% Gather our options
{ok, UserDir} = file:get_cwd(),
DestPath = normalize_path(Config#config.destpath),
DestRwPath = normalize_path(Config#config.destrwpath),
LocalPath = normalize_path(Config#config.localpath),
Hooks = Config#config.hooks,
Port = Config#config.port,
{ok, ConRef} =
case relsync_ssh_client:connect(Node, Port, UserDir) of
{ok, _ConRef} ->
{ok, _ConRef};
{error, econnrefused} ->
error(?RELSYNC_ERROR({ssh, econnrefused, Node, Port}))
end,
{ok, Client} = relsync_ssh_client:start_link(ConRef),
% Start syncing
ok = relsync_ssh_client:set_hooks(Client, Hooks),
ok = relsync_ssh_client:notify_presync(Client),
DestPathToUse =
case DestRwPath of
"" ->
DestPath;
_ ->
%ok = target_syncer:create_symlink_mirror(Node, DestPath, DestRwPath),
DestRwPath
end,
{ok, DestFileInfos0} = relsync_ssh_client:get_file_listing(Client, DestPathToUse),
LocalFileInfos0 = relsync_lib:get_file_listing(LocalPath),
DestFileInfos = maybe_exclude_system_libs(DestFileInfos0, Config),
LocalFileInfos = maybe_exclude_system_libs(LocalFileInfos0, Config),
{ok, FileList} = synchronize_node(
{relsync_ssh_client, Client},
LocalPath,
LocalFileInfos,
DestPathToUse,
DestFileInfos
),
{ok, FilesReloaded} = relsync_ssh_client:reload(Client, FileList),
print_files_reloaded(FilesReloaded),
ok = relsync_ssh_client:notify_postsync(Client),
ok = relsync_ssh_client:stop(Client),
ok = relsync_ssh_client:close(ConRef),
% Do the next node.
update_node(erl, T, Config);
update_node(erl, [], _Config) ->
ok;
update_node(erl, [Node | T], Config) ->
% Ping the node to make sure that it is connected
io:format("Updating ~p...~n", [Node]),
pong = net_adm:ping(Node),
% Start up the remote syncer
case target_syncer_sup:start_child(Node) of
{ok, _} -> ok;
{error, {already_started, _}} -> ok
end,
% Gather our options
DestPath = normalize_path(Config#config.destpath),
DestRwPath = normalize_path(Config#config.destrwpath),
LocalPath = normalize_path(Config#config.localpath),
Hooks = Config#config.hooks,
% Start syncing
ok = target_syncer:set_hooks(Node, Hooks),
ok = target_syncer:notify_presync(Node),
DestPathToUse =
case DestRwPath of
"" ->
DestPath;
_ ->
ok = target_syncer:create_symlink_mirror(Node, DestPath, DestRwPath),
DestRwPath
end,
DestFileInfos0 = target_syncer:get_file_listing(Node, DestPathToUse),
LocalFileInfos0 = relsync_lib:get_file_listing(LocalPath),
DestFileInfos = maybe_exclude_system_libs(DestFileInfos0, Config),
LocalFileInfos = maybe_exclude_system_libs(LocalFileInfos0, Config),
{ok, FileList} = synchronize_node(
{target_syncer, Node},
LocalPath,
LocalFileInfos,
DestPathToUse,
DestFileInfos
),
{ok, FilesReloaded} = target_syncer:reload(Node, FileList),
print_files_reloaded(FilesReloaded),
ok = target_syncer:notify_postsync(Node),
% Do the next node.
update_node(erl, T, Config).
-spec normalize_path(string()) -> string().
normalize_path("") ->
"";
normalize_path(Path) ->
case lists:last(Path) of
$/ -> Path;
_ -> Path ++ "/"
end.
% Return true if this is a safe file to synchronize
-spec safe_file({string(), {integer(), binary()}}) -> true | false.
safe_file({Filename, _Info}) ->
filename:extension(Filename) =/= ".so".
% Synchronize the nodes by taking the local and remote file
% lists, filtering and sorting them, and then comparing them
% one by one to make sure that both sides are in sync.
-spec synchronize_node(Node, LocalPath, LocalFileInfos, DestPath, DestFileInfos) ->
{ok, list()}
when
Node :: {module(), pid() | atom()},
LocalPath :: string(),
LocalFileInfos :: [{string(), {integer(), binary()}}],
DestPath :: string(),
DestFileInfos :: [{string(), {integer(), binary()}}].
synchronize_node(Node, LocalPath, LocalFileInfos, DestPath, DestFileInfos) ->
FilteredLocalInfos = lists:filter(fun safe_file/1, LocalFileInfos),
SortedLocalInfos = lists:sort(FilteredLocalInfos),
FilteredDestInfos = lists:filter(fun safe_file/1, DestFileInfos),
SortedDestInfos = lists:sort(FilteredDestInfos),
sync_files(
Node,
normalize_path(LocalPath),
SortedLocalInfos,
normalize_path(DestPath),
SortedDestInfos,
[]
).
make_system_libs_regexp() ->
Version = "-[[:digit:]{0,*}]?\.",
SystemLibs = [
asn1,
common_test,
compiler,
crypto,
debugger,
dialyzer,
diameter,
edoc,
eldap,
erl_docgen,
erl_interface,
et,
eunit,
ftp,
hipe,
inets,
jinterface,
kernel,
megaco,
mnesia,
observer,
odbc,
os_mon,
parsetools,
public_key,
reltool,
runtime_tools,
sasl,
snmp,
ssh,
ssl,
stdlib,
syntax_tools,
tftp,
tools,
wx,
xmerl
],
[
begin
Regexp = [atom_to_list(Lib), Version],
{ok, MP} = re:compile(Regexp),
MP
end
|| Lib <- SystemLibs
].
maybe_exclude_system_libs(FileInfos, #config{exclude_system_libs = true}) ->
SystemLibsRegexp = make_system_libs_regexp(),
Fun = fun({Path, _}) ->
Match = lists:any(
fun(Regexp) ->
case re:run(Path, Regexp, [{capture, none}]) of
match ->
true;
nomatch ->
false
end
end,
SystemLibsRegexp
),
case Match of
true ->
% Path is a system_lib, so remove it from FileInfos
false;
false ->
true
end
end,
lists:filter(Fun, FileInfos);
maybe_exclude_system_libs(LocalFileInfos, #config{exclude_system_libs = false}) ->
LocalFileInfos.
-spec sync_files(Node, LocalPath, LocalFiles, DestPath, DestFiles, AccFiles) -> {ok, list()} when
Node :: {module(), pid() | atom()},
LocalPath :: string(),
LocalFiles :: [{string(), {integer(), binary()}}],
DestPath :: string(),
DestFiles :: [{string(), {integer(), binary()}}],
AccFiles :: [string()].
sync_files(_Node, _LocalPath, [], _DestPath, [], AccFiles) ->
{ok, AccFiles};
sync_files(Node, LocalPath, [{LocalFile, LocalInfo} | LTail], DestPath, [], AccFiles) ->
io:format("Creating ~p on ~p...~n", [LocalFile, Node]),
{ok, Contents} = file:read_file(LocalPath ++ LocalFile),
{Mode, _} = LocalInfo,
Path = DestPath ++ LocalFile,
ok = copy_file(Node, Path, Mode, Contents),
sync_files(Node, LocalPath, LTail, DestPath, [], [Path | AccFiles]);
sync_files(Node, LocalPath, [], DestPath, [{DestFile, _DestInfo} | DTail], AccFiles) ->
io:format("Deleting ~p from ~p...~n", [DestFile, Node]),
Path = DestPath ++ DestFile,
rm_file(Node, Path),
sync_files(Node, LocalPath, [], DestPath, DTail, [Path | AccFiles]);
sync_files(
Node,
LocalPath,
[{LocalFile, LocalInfo} | LTail],
DestPath,
[{DestFile, DestInfo} | DTail],
AccFiles
) when LocalFile =:= DestFile, LocalInfo =:= DestInfo ->
sync_files(Node, LocalPath, LTail, DestPath, DTail, AccFiles);
sync_files(
Node,
LocalPath,
[{LocalFile, LocalInfo} | LTail],
DestPath,
[{DestFile, DestInfo} | DTail],
AccFiles
) when LocalFile =:= DestFile, LocalInfo =/= DestInfo ->
io:format("Updating ~p on ~p...~n", [LocalFile, Node]),
Path = LocalPath ++ LocalFile,
{ok, Contents} = file:read_file(Path),
{Mode, _} = LocalInfo,
ok = copy_file(Node, DestPath ++ LocalFile, Mode, Contents),
sync_files(Node, LocalPath, LTail, DestPath, DTail, [Path | AccFiles]);
sync_files(
Node,
LocalPath,
[{LocalFile, LocalInfo} | LTail],
DestPath,
[{DestFile, _DestInfo} | DTail],
AccFiles
) when LocalFile > DestFile ->
io:format("Deleting ~p from ~p...~n", [DestFile, Node]),
Path = DestPath ++ DestFile,
rm_file(Node, Path),
sync_files(Node, LocalPath, [{LocalFile, LocalInfo} | LTail], DestPath, DTail, [Path | AccFiles]);
sync_files(
Node,
LocalPath,
[{LocalFile, LocalInfo} | LTail],
DestPath,
[{DestFile, DestInfo} | DTail],
AccFiles
) when LocalFile < DestFile ->
io:format("Creating ~p on ~p...~n", [LocalFile, Node]),
Path = LocalPath ++ LocalFile,
{ok, Contents} = file:read_file(Path),
{Mode, _} = LocalInfo,
ok = copy_file(Node, DestPath ++ LocalFile, Mode, Contents),
sync_files(Node, LocalPath, LTail, DestPath, [{DestFile, DestInfo} | DTail], [Path | AccFiles]).
copy_file({Module, Node}, FilePath, Mode, Contents) ->
Module:copy_file(Node, FilePath, Mode, Contents).
rm_file({Module, Node}, FilePath) ->
Module:rm_file(Node, FilePath).
print_files_reloaded(FilesReloaded) ->
[io:format("~-10s ~s~n", [Status, Path]) || {Path, Status} <- FilesReloaded].
format_error({ssh, econnrefused, Host, Port}) ->
io_lib:format("No connection to host ~s port ~w", [Host, Port]).