%% -------------------------------------------------------------------
%%
  riaknostic - automated diagnostic tools for Riak
%%
  Copyright ( c ) 2011 Basho Technologies , 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
%%
%%   -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 Diagnostic that checks permissions on data directories and
  whether  is set .   It will only check data directories of
%% known storage backends.
-module(riaknostic_check_disk).
-behaviour(riaknostic_check).

%% The file that we will attempt to create and read under each data directory.
-define(TEST_FILE, "riaknostic.tmp").

%% A dependent chain of permissions checking functions.
-define(CHECKPERMFUNS, [fun check_is_dir/1,
                        fun check_is_writeable/1,
                        fun check_is_readable/1,
                        fun check_is_file_readable/1,
                        fun check_atime/1]).

-include_lib("kernel/include/file.hrl").

-export([description/0,
         valid/0,
         check/0,
         format/1]).

-spec description() -> string().
description() ->
    "Data directory permissions and atime".

-spec valid() -> true.
valid() ->
    true.

-spec check() -> [{lager:log_level(), term()}].
check() ->
    DataDirs = riaknostic_config:data_directories(),
    %% Add additional disk checks in the function below
    lists:flatmap(fun(Dir) ->
                          check_directory_permissions(Dir)
                  end,
                  DataDirs).

-spec format(term()) -> {io:format(), [term()]}.
format({disk_full, DataDir}) ->
    {"Disk containing data directory ~s is full! "
     "Please check that it is set to the correct location and that there are not "
     "other files using up space intended for Riak.", [DataDir]};
format({no_data_dir, DataDir}) ->
    {"Data directory ~s does not exist. Please create it.", [DataDir]};
format({no_write, DataDir}) ->
    User = riaknostic_config:user(),
    {"No write access to data directory ~s. Please make it writeable by the '~s' user.", [DataDir, User]};
format({no_read, DataDir}) ->
    User = riaknostic_config:user(),
    {"No read access to data directory ~s. Please make it readable by the '~s' user.", [DataDir, User]};
format({write_check, File}) ->
    {"Write-test file ~s is a directory! Please remove it so this test can continue.", [File]};
format({atime, Dir}) ->
    {"Data directory ~s is not mounted with 'noatime'. "
     "Please remount its disk with the 'noatime' flag to improve performance.", [Dir]}.

%%% Private functions

check_directory_permissions(Directory) ->
    check_directory(Directory, ?CHECKPERMFUNS).

%% Run a list of check functions against the given directory,
  returning the first non - ok result .
check_directory(_, []) ->
    [];
check_directory(Directory, [Check|Checks]) ->
    case Check(Directory) of
        ok ->
            check_directory(Directory, Checks);
        Message ->
            [ Message ]
    end.

%% Check if the path is actually a directory
check_is_dir(Directory) ->
    case filelib:is_dir(Directory) of
        true ->
            ok;
        _ ->
            {error, {no_data_dir, Directory}}
    end.

%% Check if the directory is writeable
check_is_writeable(Directory) ->
    File = filename:join([Directory, ?TEST_FILE]),
    case file:write_file(File, <<"ok">>) of
        ok ->
            ok;
        {error, Error} when Error == enoent orelse Error == eacces ->
            {error, {no_write, Directory}};
        {error, enospc} ->
            {critical, {disk_full, Directory}};
        {error, eisdir} ->
            {error, {write_check, File}}
    end.

%% Check if the directory is readable
check_is_readable(Directory) ->
    case file:read_file_info(Directory) of
        {ok, #file_info{access=Access}} when Access == read orelse
                                             Access == read_write ->
            ok;
        {error, eacces} ->
            {error, {no_read, Directory}};
        {error, Error} when Error == enoent orelse
                            Error == enotdir ->
            {error, {no_data_dir, Directory}};
        _ ->
            {error, {no_read, Directory}}
    end.

%% Check if the file we created is readable
check_is_file_readable(Directory) ->
    File = filename:join([Directory, ?TEST_FILE]),
    case file:read_file(File) of
        {error, Error} when Error == eacces orelse
                            Error == enotdir ->
            {error, {no_read, Directory}};
        {error, enoent} ->
            {error, {write_check, File}};
        _ -> ok
    end.

%% Check if the directory is mounted with 'noatime'
check_atime(Directory) ->
    File = filename:join([Directory, ?TEST_FILE]),
    {ok, FileInfo1} = file:read_file_info(File),
    timer:sleep(1001),
    {ok, S} = file:open(File, [read]),
    io:get_line(S, ''),
    file:close(S),
    {ok, FileInfo2} = file:read_file_info(File),
    file:delete(File),
    case (FileInfo1#file_info.atime =/= FileInfo2#file_info.atime) of
        true ->
            {notice, {atime, Directory}};
        _ ->
            ok
    end.


 -------------------------------------------------------------------



 Version 2.0 (the "License"); you may not use this file
 a copy of the License at

   -2.0

 Unless required by applicable law or agreed to in writing,
 KIND, either express or implied.  See the License for the
 specific language governing permissions and limitations
 under the License.

 -------------------------------------------------------------------
 known storage backends.
 The file that we will attempt to create and read under each data directory.
 A dependent chain of permissions checking functions.
 Add additional disk checks in the function below
 Private functions
 Run a list of check functions against the given directory,
 Check if the path is actually a directory
 Check if the directory is writeable
 Check if the directory is readable
 Check if the file we created is readable
 Check if the directory is mounted with 'noatime'
  riaknostic - automated diagnostic tools for Riak
  Copyright ( c ) 2011 Basho Technologies , Inc.   All Rights Reserved .
  This file is provided to you under the Apache License ,
  except in compliance with the License .   You may obtain
  software distributed under the License is distributed on an
  " AS IS " BASIS , WITHOUT WARRANTIES OR CONDITIONS OF ANY

  @doc Diagnostic that checks permissions on data directories and
  whether  is set .   It will only check data directories of
-module(riaknostic_check_disk).
-behaviour(riaknostic_check).

-define(TEST_FILE, "riaknostic.tmp").

-define(CHECKPERMFUNS, [fun check_is_dir/1,
                        fun check_is_writeable/1,
                        fun check_is_readable/1,
                        fun check_is_file_readable/1,
                        fun check_atime/1]).

-include_lib("kernel/include/file.hrl").

-export([description/0,
         valid/0,
         check/0,
         format/1]).

-spec description() -> string().
description() ->
    "Data directory permissions and atime".

-spec valid() -> true.
valid() ->
    true.

-spec check() -> [{lager:log_level(), term()}].
check() ->
    DataDirs = riaknostic_config:data_directories(),
    lists:flatmap(fun(Dir) ->
                          check_directory_permissions(Dir)
                  end,
                  DataDirs).

-spec format(term()) -> {io:format(), [term()]}.
format({disk_full, DataDir}) ->
    {"Disk containing data directory ~s is full! "
     "Please check that it is set to the correct location and that there are not "
     "other files using up space intended for Riak.", [DataDir]};
format({no_data_dir, DataDir}) ->
    {"Data directory ~s does not exist. Please create it.", [DataDir]};
format({no_write, DataDir}) ->
    User = riaknostic_config:user(),
    {"No write access to data directory ~s. Please make it writeable by the '~s' user.", [DataDir, User]};
format({no_read, DataDir}) ->
    User = riaknostic_config:user(),
    {"No read access to data directory ~s. Please make it readable by the '~s' user.", [DataDir, User]};
format({write_check, File}) ->
    {"Write-test file ~s is a directory! Please remove it so this test can continue.", [File]};
format({atime, Dir}) ->
    {"Data directory ~s is not mounted with 'noatime'. "
     "Please remount its disk with the 'noatime' flag to improve performance.", [Dir]}.


check_directory_permissions(Directory) ->
    check_directory(Directory, ?CHECKPERMFUNS).

  returning the first non - ok result .
check_directory(_, []) ->
    [];
check_directory(Directory, [Check|Checks]) ->
    case Check(Directory) of
        ok ->
            check_directory(Directory, Checks);
        Message ->
            [ Message ]
    end.

check_is_dir(Directory) ->
    case filelib:is_dir(Directory) of
        true ->
            ok;
        _ ->
            {error, {no_data_dir, Directory}}
    end.

check_is_writeable(Directory) ->
    File = filename:join([Directory, ?TEST_FILE]),
    case file:write_file(File, <<"ok">>) of
        ok ->
            ok;
        {error, Error} when Error == enoent orelse Error == eacces ->
            {error, {no_write, Directory}};
        {error, enospc} ->
            {critical, {disk_full, Directory}};
        {error, eisdir} ->
            {error, {write_check, File}}
    end.

check_is_readable(Directory) ->
    case file:read_file_info(Directory) of
        {ok, #file_info{access=Access}} when Access == read orelse
                                             Access == read_write ->
            ok;
        {error, eacces} ->
            {error, {no_read, Directory}};
        {error, Error} when Error == enoent orelse
                            Error == enotdir ->
            {error, {no_data_dir, Directory}};
        _ ->
            {error, {no_read, Directory}}
    end.

check_is_file_readable(Directory) ->
    File = filename:join([Directory, ?TEST_FILE]),
    case file:read_file(File) of
        {error, Error} when Error == eacces orelse
                            Error == enotdir ->
            {error, {no_read, Directory}};
        {error, enoent} ->
            {error, {write_check, File}};
        _ -> ok
    end.

check_atime(Directory) ->
    File = filename:join([Directory, ?TEST_FILE]),
    {ok, FileInfo1} = file:read_file_info(File),
    timer:sleep(1001),
    {ok, S} = file:open(File, [read]),
    io:get_line(S, ''),
    file:close(S),
    {ok, FileInfo2} = file:read_file_info(File),
    file:delete(File),
    case (FileInfo1#file_info.atime =/= FileInfo2#file_info.atime) of
        true ->
            {notice, {atime, Directory}};
        _ ->
            ok
    end.

