#!/usr/bin/env escript
%% -*- erlang-indent-level: 4;indent-tabs-mode: nil; fill-column: 92-*-
%% ex: ts=4 sw=4 et
%% @author Seth Falcon <seth@opscode.com>
%% @author Seth Chisamore <schisamo@opscode.com>
%% Copyright 2012 Opscode, Inc. All Rights Reserved.
%%
%% This file is provided to you 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.
%%

%% TODO: The cookie used by erchef should be part of config
%% TODO: Make database type configurable

-include_lib("public_key/include/public_key.hrl").

-define(SELF, 'me@127.0.0.1').
-define(ERCHEF, 'erchef@127.0.0.1').
-define(ERCHEF_COOKIE, 'erchef').
-define(DEFAULT_KEY_PATH, '/etc/chef-server').
%% File 00600 mode permission specified the Erlang way. See
%% file:write_file_info for details.
-define(OWNER_RW, 8#00400 + 8#00200).

main(_) ->
    init_code_path(),
    init_network(),
    %% if we generated keys, let's use them
    Keys = load_keys(?DEFAULT_KEY_PATH),
    create_client(<<"chef-validator">>, validator, get_public_key('chef-validator', Keys)),
    create_client(<<"chef-webui">>, admin, get_public_key('chef-webui', Keys)),
    UserName = get_user_name(),
    check_reserved_name(UserName),
    UserPassword = get_user_password(),
    create_user(UserName, UserPassword, admin, get_public_key(UserName, Keys)),
    create_default_environment().

get_user_name() ->
  case os:getenv("CHEF_ADMIN_USER") of
    false ->
        "admin";
    Name ->
        Name
    end.

get_user_password() ->
  case os:getenv("CHEF_ADMIN_PASS") of
    false ->
        "p@ssw0rd";
    Password ->
        Password
  end.

check_reserved_name(Name) when Name == "chef-validator"; Name == "chef-webui" ->
    io:format("Username ~p is reserved; please pick another ~n", [Name]),
    halt(2);
check_reserved_name(_Name) ->
    false.

init_code_path() ->
    AllTheEbins = filename:join(filename:dirname(escript:script_name()), "../lib/*/ebin"),
    [code:add_path(CodePath) || CodePath <- filelib:wildcard(AllTheEbins)].

init_network() ->
    net_kernel:start([?SELF, longnames]),
    erlang:set_cookie(node(), ?ERCHEF_COOKIE),
    pong = net_adm:ping(?ERCHEF).

create_client(Name, Type, PublicKey) ->
    Args = [Name, is_validator(Type), is_admin(Type), PublicKey],
    Result = rpc:call(?ERCHEF, chef_sked, create_client, Args),
    process_result(client, Name, Result).

create_user(Name, Password, Type, PublicKey) ->
    Args = [Name, Password, is_admin(Type), PublicKey],
    Result = rpc:call(?ERCHEF, chef_sked, create_user, Args),
    process_result(user, Name, Result).

process_result(CallType, Name, Result) ->
    case Result of
        ok ->
            io:format("~p ~p created.~n", [CallType, Name]),
            {ok, created};
        {ok, PrivateKey} ->
            PKPath = filename:join([?DEFAULT_KEY_PATH, iolist_to_binary([Name, ".pem"])]),
            ok = write_file(PKPath, PrivateKey, ?OWNER_RW),
            io:format("~p ~p created. Key written to ~p~n", [CallType, Name, PKPath]),
            {ok, created};
        {conflict, _} ->
            io:format("~p ~p already exists~n", [CallType, Name]),
            {ok, exists};
        Error ->
            io:format("error creating ~p ~p: ~p~n", [CallType, Name, Error]),
            halt(2)
    end.

get_public_key(Name, Keys) ->
    case dict:find(Name, Keys) of
        {ok, #'RSAPrivateKey'{modulus=Mod, publicExponent=Exp}} ->
            encode_public_pem(Mod, Exp);
        {ok, #'RSAPublicKey'{modulus=Mod, publicExponent=Exp}} ->
            encode_public_pem(Mod, Exp);
        {ok, {Format, _, _}} ->
            io:format("could not locate 'RSAPrivateKey' entry in pem ~p - "
                      "did find ~p entry.~n", [Name, Format]),
            halt(3);
        error ->
            create_key
    end.

create_default_environment() ->
    case rpc:call(?ERCHEF, chef_sked, create_default_environment, []) of
        ok ->
            io:format("environment '_default' created~n"),
            ok;
        {conflict, _} ->
            io:format("environment '_default' already exists~n"),
            ok;
        Error ->
            io:format("Error creating environment '_default': ~p~n", [Error]),
            halt(2)
    end.

is_validator(Type) ->
    Type =:= validator.

is_admin(Type) ->
    Type =:= admin.

encode_public_pem(Mod, Exp) ->
    EncodedEntry = public_key:pem_entry_encode('SubjectPublicKeyInfo',
                                                       #'RSAPublicKey'{
                                                            modulus=Mod,
                                                            publicExponent=Exp
                                                       }),
    public_key:pem_encode([EncodedEntry]).

load_keys(Path) ->
    %% temporarly disable log output while we load keys
    error_logger:tty(false),
    Keys = load_keyring_from_dir(Path, dict:new()),
    %% re-enable log output
    error_logger:tty(true),
    Keys.

%% Write data to a file and set permission modes. This is a non-atomic
%% write, making three separate calls into the file module. The approach
%% is: create or truncate the file, update mode, then write
%% contents. The idea being that we never write data into a file without
%% the mode being as desired.
%%
%% The mode is an integer representing the mode as the sum of octal
%% integers. So, e.g., to make a file rw for owner only: `8#00400 +
%% 8#00200'.
write_file(Name, Data, Mode) ->
    %% first create the file (or truncate it)
    ok = file:write_file(Name, <<>>),
    ok = file:change_mode(Name, Mode),
    %% now write the contents
    ok = file:write_file(Name, Data),
    ok.

%%-----------------------------------------------------------------------------
%% FIXME - export these cargo-culted functions in chef_keyring
%%-----------------------------------------------------------------------------

%%%
%%% Load all of the .pem files in the specified directory into the keys dictionary
%%%
load_keyring_from_dir(undef, Keys) -> Keys;
load_keyring_from_dir(Dir, Keys) ->
    case filelib:wildcard(filename:join([Dir,"*.pem"])) of
        [] ->
            error_logger:info_msg("Error reading keyring directory ~s: ~p~n", [Dir, "No *.pem files found"]),
            Keys;
        FileNames ->
            KeyPaths = [{list_to_atom(filename:rootname(filename:basename(F))), F} || F <- FileNames ],
            {ok, NewKeys} = load_keyring_files(KeyPaths, Keys),
            NewKeys
    end.

%%%
%%% Load a list of {keyname, filename} pairs into the keys dictionary
%%%
load_keyring_files([], Keys) ->
    {ok, Keys};
load_keyring_files([{Name, Path}|T], Keys) ->
    case key_from_file(Name, Path) of
        {ok, Key} -> load_keyring_files(T, dict:store(Name, Key, Keys));
        _ -> load_keyring_files(T, Keys)
    end.

%%%
%%% Load key from file
%%%
key_from_file(Name, File) ->
    case file:read_file(File) of
        {ok, RawKey} ->
            case chef_authn:extract_public_or_private_key(RawKey) of
                {error, bad_key} ->
                    error_logger:error_msg("Failed to decode PEM file ~s for ~p~n", [File, Name]),
                    {error, bad_key};
                PrivateKey when is_tuple(PrivateKey) ->
                    KeyType = element(1, PrivateKey),
                    error_logger:info_msg("Loaded key ~s of type ~s from file ~s ~n", [Name, KeyType, File]),
                    {ok, PrivateKey}
            end;
        Error ->
            error_logger:error_msg("Error reading file ~s for ~p: ~p~n", [File, Name, Error]),
            Error
    end.
