diff --git a/DEBIAN/Makefile b/DEBIAN/Makefile index da09ddae..6cf6f690 100644 --- a/DEBIAN/Makefile +++ b/DEBIAN/Makefile @@ -22,7 +22,7 @@ deb: build rm COPYING rm INSTALL rm USAGE - rm -rf samba-module/ docs/ includes/ web-app/ + rm -rf samba-module/ docs/ includes/ web-app/ scripts-examples/ build: mkdir -p $(DESTDIR)etc/init.d/ @@ -31,6 +31,7 @@ build: mkdir -p $(DESTDIR)usr/share/greyhole/ mkdir -p $(DESTDIR)etc/logrotate.d/ mkdir -p $(DESTDIR)usr/share/greyhole/web-app/ + mkdir -p $(DESTDIR)usr/share/greyhole/scripts-examples/ mkdir -p $(DESTDIR)etc/cron.d/ mkdir -p $(DESTDIR)etc/cron.daily/ mkdir -p $(DESTDIR)etc/cron.weekly/ @@ -57,6 +58,11 @@ build: install -m 0644 -D -p web-app/index.php $(DESTDIR)usr/share/greyhole/web-app/index.php install -m 0644 -D -p web-app/README $(DESTDIR)usr/share/greyhole/web-app/README + + install -m 0755 -D -p scripts-examples/greyhole_file_changed.sh $(DESTDIR)usr/share/greyhole/scripts-examples/ + install -m 0755 -D -p scripts-examples/greyhole_idle.sh $(DESTDIR)usr/share/greyhole/scripts-examples/ + install -m 0755 -D -p scripts-examples/greyhole_notify_error.sh $(DESTDIR)usr/share/greyhole/scripts-examples/ + install -m 0755 -D -p scripts-examples/greyhole_send_fsck_report.sh $(DESTDIR)usr/share/greyhole/scripts-examples/ install -m 0644 -D -p USAGE $(DESTDIR)usr/share/greyhole/ diff --git a/docs/greyhole.conf.5 b/docs/greyhole.conf.5 index a6a2d262..a4c3dbb2 100644 --- a/docs/greyhole.conf.5 +++ b/docs/greyhole.conf.5 @@ -424,6 +424,106 @@ Examples: \fIinclude = /etc/greyhole.d/storage_drives\fR \fIinclude = /etc/greyhole.d/greyhole_shares.sh\fR .RE .RE +.RE + +executed_tasks_retention +.PP +.RS 4 +How long should executed tasks be kept in the database, after having been executed. +.RS 0 +Those are strictly for debugging purposes; they serve no other purposes. +.RE +.RS 0 +Enter a number of days, or \fBforever\fR +.RE +.RS 0 +Default is \fB60\fR days. +.RE +.RE + + +ignored_files +.RS 0 +ignored_folders +.PP +.RS 4 +Files that match the patterns below will be ignored by Greyhole. +.RS 0 +They will stay in the landing zone indefinitely, so be careful on what you +define here. List here all files and folders that are temporary, to prevent +Greyhole from working for nothing. +.PP +Format is Regular Expressions (PCRE syntax) +.PP +\fBignored_files\fR is matched against the file name only. +.RE +.RS 0 +\fBignored_folders\fR is matched against the concatenation of the share name and +the full path to the file (without the filename), eg: \fBVideos/Movies/HD/\fR +.RE +.RE + +hook +.PP +.RS 4 +Call custom scripts on events. +.PP +.RS 0 +Available events type: \fBcreate\fR, \fBedit\fR, \fBrename\fR, \fBdelete\fR, \fBmkdir\fR, \fBrmdir\fR, \fBwarning\fR, \fBerror\fR, \fBcritical\fR, \fBfsck\fR, \fBidle\fR and \fBnot_idle\fR +.RE +.RS 2 +- For \fBcreate\fR, \fBedit\fR, \fBrename\fR, \fBdelete\fR, \fBmkdir\fR and \fBrmdir\fR: the hooks are called after Greyhole finished processing the operation; +.RE +.RS 2 +- for \fBwarning\fR, \fBerror\fR, \fBcritical\fR and \fBfsck\fR: the hooks are called after Greyhole created a log; +.RE +.RS 2 +- for \fBidle\fR and \fBnot_idle\fR: the hooks are called just before the daemon will sleep, or when it's about to start working again. +.RE +.PP +.RS 0 +The parameters sent to the custom scripts are: +.RE +.RS 2 +- \fBevent_type\fR (one of the above) +.RE +.RS 2 +If event is related to a file/folder on a share (\fBcreate\fR, \fBedit\fR, \fBrename\fR, \fBdelete\fR, \fBmkdir\fR and \fBrmdir\fR), other params will be: +.RE +.RS 4 +- \fBshare_name\fR +.RE +.RS 4 +- \fBpath_on_share\fR +.RE +.RS 4 +- \fBoriginal_path_on_share\fR (only for \fBrename\fR) +.RE +.RS 2 +If event is related to a log (\fBwarning\fR, \fBerror\fR, \fBcritical\fR, \fBfsck\fR, \fBidle\fR and \fBnot_idle\fR), other params will be: +.RE +.RS 4 +- \fBevent_code\fR: one of the predefined values that define the actual error/event. +.RE +.RS 6 +Look here for the list of possible codes: \fIhttps://github.com/gboudreau/Greyhole/blob/master/includes/Log.php#L62\fR +.RE +.RS 4 +- \fBlog\fR: user-readable log (might contain LF characters) +.RE +.PP +Examples: \fIhook[create|edit|rename|delete|mkdir|rmdir] = /usr/share/greyhole/scripts-examples/greyhole_file_changed.sh\fR +.RS 10 +\fIhook[warning|error|critical] = /usr/share/greyhole/scripts-examples/greyhole_notify_error.sh\fR +.RE +.RS 10 +\fIhook[fsck] = /usr/share/greyhole/scripts-examples/greyhole_send_fsck_report.sh\fR +.RE +.RS 10 +\fIhook[idle|not_idle] = /usr/share/greyhole/scripts-examples/greyhole_idle.sh\fR +.RE +.RE + .SH AUTHORS Guillaume Boudreau diff --git a/greyhole b/greyhole index 169d2225..e6ef23a0 100755 --- a/greyhole +++ b/greyhole @@ -1,7 +1,7 @@ #!/usr/bin/php -d open_basedir=/ emailAsRequired(); } + $log = "Nothing to do... Sleeping."; + Log::debug($log); + if (!$was_idle) { + LogHook::trigger(LogHook::EVENT_TYPE_IDLE, Log::EVENT_CODE_IDLE, $log); + $was_idle = TRUE; + } + $log_level = Log::getLevel(); sleep($log_level == Log::DEBUG ? 10 : ($log_level == Log::TEST || $log_level == Log::PERF ? 1 : 600)); $locked_files = array(); @@ -113,7 +118,12 @@ function execute_next_task() { if (array_contains($sleep_before_task, $task->id)) { Log::setAction(ACTION_SLEEP); - Log::debug("Only locked files operations pending... Sleeping."); + $log = "Only locked files operations pending... Sleeping."; + Log::debug($log); + if (!$was_idle) { + LogHook::trigger(LogHook::EVENT_TYPE_IDLE, Log::EVENT_CODE_IDLE, $log); + $was_idle = TRUE; + } $log_level = Log::getLevel(); sleep($log_level == Log::DEBUG ? 10 : ($log_level == Log::TEST ? 1 : 600)); $sleep_before_task = array(); @@ -121,7 +131,12 @@ function execute_next_task() { } Log::setAction($task->action); - Log::info("Now working on task ID $task->id: $task->action " . clean_dir("$task->share/$task->full_path") . ($task->action == 'rename' ? " -> $task->share/$task->additional_info" : '')); + $log = "Now working on task ID $task->id: $task->action " . clean_dir("$task->share/$task->full_path") . ($task->action == 'rename' ? " -> $task->share/$task->additional_info" : ''); + Log::info($log); + if ($was_idle) { + LogHook::trigger(LogHook::EVENT_TYPE_NOT_IDLE, Log::EVENT_CODE_IDLE_NOT, $log); + $was_idle = FALSE; + } if ($task->complete == 'written') { if (should_ignore_file($task->share, $task->full_path)) { @@ -172,9 +187,10 @@ function execute_next_task() { list($path, $filename) = explode_full_path($task->full_path); FSCKLogFile::loadFSCKReport('Missing files'); // Create or load the fsck_report from disk gh_fsck_file($path, $filename, $file_type, 'metastore', $task->share); - if (task_has_option($task, OPTION_EMAIL)) { + $send_email = task_has_option($task, OPTION_EMAIL); + if ($send_email || Hook::hasHookForEvent(LogHook::EVENT_TYPE_FSCK)) { // Save the report to disk to be able to email it when we're done with all fsck_file tasks - FSCKLogFile::saveFSCKReport(); + FSCKLogFile::saveFSCKReport($send_email); } break; case 'md5': @@ -281,7 +297,7 @@ function execute_next_task() { gh_fsck("$storage_volume/$share_name", $share_name, $storage_volume); } } else { - Log::error("Unknown folder to fsck. You should specify a storage pool folder, a metadata store folder, a shared folder, or a subdirectory of any of those."); + Log::error("Unknown folder to fsck. You should specify a storage pool folder, a metadata store folder, a shared folder, or a subdirectory of any of those.", Log::EVENT_CODE_FSCK_UNKNOWN_FOLDER); } } else { $share = $share_options['name']; @@ -341,12 +357,16 @@ function execute_next_task() { Settings::set('last_fsck_conf_md5', $new_conf_md5); - if (task_has_option($task, OPTION_EMAIL)) { + if (task_has_option($task, OPTION_EMAIL) || Hook::hasHookForEvent(LogHook::EVENT_TYPE_FSCK)) { // Email report for fsck + $subject = 'fsck of Greyhole shares on ' . exec('hostname'); $fsck_report_mail = get_fsck_report(); - $email_to = Config::get(CONFIG_EMAIL_TO); - Log::debug("Sending fsck report to $email_to"); - mail($email_to, 'fsck of Greyhole shares on ' . exec('hostname'), $fsck_report_mail); + if (task_has_option($task, OPTION_EMAIL)) { + $email_to = Config::get(CONFIG_EMAIL_TO); + Log::debug("Sending fsck report to $email_to"); + mail($email_to, $subject, $fsck_report_mail); + } + LogHook::trigger(LogHook::EVENT_TYPE_FSCK, Log::EVENT_CODE_FSCK_REPORT, $subject . "\n" . $fsck_report_mail); } break; case 'mkdir': @@ -417,6 +437,8 @@ function gh_rmdir($share, $full_path) { Log::debug(" Removed metadata files directory $metastore/$share/$full_path"); } } + + FileHook::trigger(FileHook::EVENT_TYPE_RMDIR, $share, $full_path); } function gh_unlink($share, $full_path, $task_id) { @@ -477,6 +499,8 @@ function gh_unlink($share, $full_path, $task_id) { } } remove_metafiles($share, $path, $filename); + + FileHook::trigger(FileHook::EVENT_TYPE_DELETE, $share, $full_path); } function is_a_metastore_dir($share, $full_path) { @@ -639,7 +663,7 @@ function gh_mv($share, $full_path, $target_full_path, $task_id) { $symlink_target = $metafile->path; } } else { - Log::warn(" Warning! An error occured while renaming file copy $old_path to $metafile->path."); + Log::warn(" Warning! An error occurred while renaming file copy $old_path to $metafile->path.", Log::EVENT_CODE_RENAME_FILE_COPY_FAILED); } $existing_metafiles[$key] = $metafile; } @@ -671,6 +695,9 @@ function gh_mv($share, $full_path, $target_full_path, $task_id) { } } $sleep_before_task = array(); + + FileHook::trigger(FileHook::EVENT_TYPE_RENAME, $share, $target_full_path, $full_path); + return TRUE; } @@ -735,7 +762,7 @@ function gh_file_exists($real_path, $log_message=null) { function get_num_copies($share) { $num_copies = SharesConfig::get($share, CONFIG_NUM_COPIES); if (!$num_copies) { - Log::warn("Found a task on a share ($share) that disappeared from " . ConfigHelper::$config_file . ". Skipping."); + Log::warn("Found a task on a share ($share) that disappeared from " . ConfigHelper::$config_file . ". Skipping.", Log::EVENT_CODE_TASK_FOR_UNKNOWN_SHARE); return -1; } if ($num_copies < 1) { @@ -942,6 +969,7 @@ function gh_write($share, $full_path, $task_id) { } } } + FileHook::trigger(FileHook::EVENT_TYPE_EDIT, $share, $full_path); } else { if (!isset($source_file)) { $source_file = clean_dir("$landing_zone/$full_path"); @@ -981,6 +1009,7 @@ function gh_write($share, $full_path, $task_id) { return FALSE; } } + FileHook::trigger(FileHook::EVENT_TYPE_CREATE, $share, $full_path); } return TRUE; } @@ -1012,7 +1041,7 @@ function gh_write_process_metafiles($num_copies_required, $existing_metafiles, $ $metafiles = create_metafiles($share, $full_path, $num_copies_required, $filesize, $existing_metafiles); if (count($metafiles) == 0) { - Log::warn(" No metadata files could be created. Will wait until metadata files can be created to work on this file."); + Log::error(" No metadata files could be created. Will wait until metadata files can be created to work on this file.", Log::EVENT_CODE_NO_METADATA_SAVED); postpone_task($task_id); return array(); } @@ -1027,7 +1056,9 @@ function gh_write_process_metafiles($num_copies_required, $existing_metafiles, $ save_metafiles($share, $path, $filename, $metafiles); - create_copies_from_metafiles($metafiles, $share, $full_path, $source_file); + if (!create_copies_from_metafiles($metafiles, $share, $full_path, $source_file)) { + return FALSE; + } return $metafiles; } @@ -1042,7 +1073,7 @@ function create_copies_from_metafiles($metafiles, $share, $full_path, $source_fi $link_next = FALSE; $file_infos = gh_get_file_infos("$landing_zone/$full_path"); foreach ($metafiles as $key => $metafile) { - if (!gh_file_exists("$landing_zone/$full_path", ' $real_path doesn\'t exist anymore. Aborting.')) { return; } + if (!gh_file_exists("$landing_zone/$full_path", ' $real_path doesn\'t exist anymore. Aborting.')) { return FALSE; } if ($metafile->path == $source_file && $metafile->state == 'OK' && gh_filesize($metafile->path) == gh_filesize($source_file)) { Log::debug(" File copy at $metafile->path is already up to date."); @@ -1056,7 +1087,7 @@ function create_copies_from_metafiles($metafiles, $share, $full_path, $source_fi $root_path = str_replace(clean_dir("/$share/$full_path"), '', $metafile->path); if (!StoragePool::is_pool_drive($root_path)) { - Log::warn(" Warning! It seems the partition UUID of $root_path changed. This probably means this mount is currently unmounted, or that you replaced this drive and didn't use 'greyhole --replace'. Because of that, Greyhole will NOT use this drive at this time."); + Log::warn(" Warning! It seems the partition UUID of $root_path changed. This probably means this mount is currently unmounted, or that you replaced this drive and didn't use 'greyhole --replace'. Because of that, Greyhole will NOT use this drive at this time.", Log::EVENT_CODE_STORAGE_POOL_DRIVE_UUID_CHANGED); $metafile->state = 'Gone'; $metafiles[$key] = $metafile; continue; @@ -1084,10 +1115,10 @@ function create_copies_from_metafiles($metafiles, $share, $full_path, $source_fi if (file_exists("$landing_zone/$full_path")) { global $current_task_id; if ($current_task_id === 0) { - Log::error(" Failed file copy (cont). We already retried this task. Aborting."); - return; + Log::error(" Failed file copy (cont). We already retried this task. Aborting.", Log::EVENT_CODE_FILE_COPY_FAILED); + return FALSE; } - Log::warn(" Failed file copy (cont). Will try to re-process this write task, since the source file seems intact."); + Log::warn(" Failed file copy (cont). Will try to re-process this write task, since the source file seems intact.", Log::EVENT_CODE_FILE_COPY_FAILED); // Queue a new write task, to replace the now gone copy. global $next_task; $next_task = (object) array( @@ -1097,7 +1128,7 @@ function create_copies_from_metafiles($metafiles, $share, $full_path, $source_fi 'full_path' => clean_dir($full_path), 'complete' => 'yes' ); - return; + return FALSE; } continue; } @@ -1123,6 +1154,7 @@ function create_copies_from_metafiles($metafiles, $share, $full_path, $source_fi } save_metafiles($share, $path, $filename, $metafiles); } + return TRUE; } function gh_copy_file($source_file, &$destination_file) { @@ -1180,7 +1212,7 @@ function gh_copy_file($source_file, &$destination_file) { gh_rename($temp_path, $destination_file); gh_chperm($destination_file, $original_file_infos); } else { - Log::warn(" Failed file copy. Will mark this metadata file 'Gone'."); + Log::warn(" Failed file copy. Will mark this metadata file 'Gone'.", Log::EVENT_CODE_FILE_COPY_FAILED); if ($renamed) { // Do NOT delete $temp_path if the file was renamed... Just move it back! gh_rename($temp_path, $source_file); @@ -1207,13 +1239,13 @@ function gh_mkdir($directory, $original_directory_or_dir_infos) { } if (is_dir($directory)) { if (!chown($directory, $dir_infos->fileowner)) { - Log::warn(" Failed to chown directory '$directory'"); + Log::warn(" Failed to chown directory '$directory'", Log::EVENT_CODE_MKDIR_CHOWN_FAILED); } if (!chgrp($directory, $dir_infos->filegroup)) { - Log::warn(" Failed to chgrp directory '$directory'"); + Log::warn(" Failed to chgrp directory '$directory'", Log::EVENT_CODE_MKDIR_CHGRP_FAILED); } if (!chmod($directory, $dir_infos->fileperms)) { - Log::warn(" Failed to chmod directory '$directory'"); + Log::warn(" Failed to chmod directory '$directory'", Log::EVENT_CODE_MKDIR_CHMOD_FAILED); } } else { // Need to mkdir & chown/chgrp all dirs that don't exists, up to the full path ($directory) @@ -1237,17 +1269,17 @@ function gh_mkdir($directory, $original_directory_or_dir_infos) { // Bingo! $parent_directory = normalize_utf8_characters($parent_directory); } else { - Log::warn(" Failed to create directory $parent_directory"); + Log::warn(" Failed to create directory $parent_directory", Log::EVENT_CODE_MKDIR_FAILED); return FALSE; } } } } if (!chown($parent_directory, $dir_infos->fileowner)) { - Log::warn(" Failed to chown directory '$parent_directory'"); + Log::warn(" Failed to chown directory '$parent_directory'", Log::EVENT_CODE_MKDIR_CHOWN_FAILED); } if (!chgrp($parent_directory, $dir_infos->filegroup)) { - Log::warn(" Failed to chgrp directory '$parent_directory'"); + Log::warn(" Failed to chgrp directory '$parent_directory'", Log::EVENT_CODE_MKDIR_CHGRP_FAILED); } if (!isset($dir_parts[$i])) { break; @@ -1434,7 +1466,7 @@ function get_metafiles_for_file($share, $path, $filename=null, $load_nok_metafil $ok_metafiles[$key] = $metafile; } else { if (!$valid_path) { - Log::warn("Found a metadata file pointing to a drive not defined in your storage pool: '$metafile->path'. Will mark it as Gone."); + Log::warn("Found a metadata file pointing to a drive not defined in your storage pool: '$metafile->path'. Will mark it as Gone.", Log::EVENT_CODE_METADATA_POINTS_TO_GONE_DRIVE); $metafile->state = 'Gone'; $metafiles[$key] = $metafile; save_metafiles($share, $path, $filename, $metafiles); @@ -1541,7 +1573,7 @@ function samba_restart() { } else if (is_file('/etc/systemd/system/multi-user.target.wants/smb.service')) { exec("systemctl restart smb.service"); } else { - Log::critical("Couldn't find how to restart Samba. Please restart the Samba daemon manually."); + Log::critical("Couldn't find how to restart Samba. Please restart the Samba daemon manually.", Log::EVENT_CODE_SAMBA_RESTART_FAILED); } } @@ -1584,7 +1616,7 @@ function samba_check_vfs() { } if (!$vfs_is_ok) { $vfs_target = "$source_libdir/greyhole/greyhole-samba$version.so"; - Log::warn(" Greyhole VFS module for Samba was missing, or the wrong version for your Samba. It will now be replaced with a symlink to $vfs_target"); + Log::warn(" Greyhole VFS module for Samba was missing, or the wrong version for your Samba. It will now be replaced with a symlink to $vfs_target", Log::EVENT_CODE_VFS_MODULE_WRONG); if (is_file($vfs_file)) { unlink($vfs_file); } @@ -1608,7 +1640,7 @@ function create_mem_spool() { } exec('mount -o size=4M -t tmpfs none /var/spool/greyhole/mem 2> /dev/null', $mount_result); if (!empty($mount_result)) { - Log::error("Error mounting tmpfs in /var/spool/greyhole/mem: $mount_result"); + Log::error("Error mounting tmpfs in /var/spool/greyhole/mem: $mount_result", Log::EVENT_CODE_SPOOL_MOUNT_FAILED); } return TRUE; } @@ -1698,6 +1730,7 @@ function parse_samba_spool() { gh_mkdir("$backup_drive/$share/$line[0]", $dir_fullpath); } } + FileHook::trigger(FileHook::EVENT_TYPE_MKDIR, $share, $line[0]); continue; } $result = array_pop($line); @@ -1907,25 +1940,25 @@ function order_target_drives($filesize_kb, $include_full_drives, $share, $path, foreach (Config::storagePoolDrives() as $sp_drive) { if (!isset($dfs[$sp_drive])) { if (!is_dir($sp_drive)) { - Log::error("The directory at $sp_drive doesn't exist. This drive will never be used!"); if (SystemHelper::is_amahi()) { - Log::error("You should de-select, then re-select this partition in your Amahi dashboard (http://hda), in the Shares > Storage Pool page, to fix this problem."); + $details = "You should de-select, then re-select this partition in your Amahi dashboard (http://hda), in the Shares > Storage Pool page, to fix this problem."; } else { - Log::error("See the INSTALL file for instructions on how to prepare partitions to include in your storage pool."); + $details = "See the INSTALL file for instructions on how to prepare partitions to include in your storage pool."; } + Log::error("The directory at $sp_drive doesn't exist. This drive will never be used! $details", Log::EVENT_CODE_STORAGE_POOL_FOLDER_NOT_FOUND); } else if (!file_exists("$sp_drive/.greyhole_used_this") && StoragePool::is_pool_drive($sp_drive)) { - Log::error("Can't find how much free space is left on $sp_drive. This partition will never be used!"); - Log::error("Please report this using the 'Issues' tab found on https://github.com/gboudreau/Greyhole. You should include the following information in your ticket:"); - Log::error("===== Error report starts here ====="); - Log::error("Unknown free space for partition: $sp_drive"); - Log::error("df_command: " . ConfigHelper::$df_command); - unset($responses); - exec(ConfigHelper::$df_command, $responses); - Log::error("Result of df_command: " . var_export($responses, TRUE)); - unset($responses); - exec('df -k 2>&1', $responses); - Log::error("Result of df -k: " . var_export($responses, TRUE)); - Log::error("===== Error report ends here ====="); + unset($df_command_responses); + exec(ConfigHelper::$df_command, $df_command_responses); + unset($df_k_responses); + exec('df -k 2>&1', $df_k_responses); + $details = "Please report this using the 'Issues' tab found on https://github.com/gboudreau/Greyhole. You should include the following information in your ticket:\n" + . "===== Error report starts here =====\n" + . "Unknown free space for partition: $sp_drive\n" + . "df_command: " . ConfigHelper::$df_command . "\n" + . "Result of df_command: " . var_export($df_command_responses, TRUE) . "\n" + . "Result of df -k: " . var_export($df_k_responses, TRUE) . "\n" + . "===== Error report ends here ====="; + Log::error("Can't find how much free space is left on $sp_drive. This partition will never be used! Details will follow.\n$details", Log::EVENT_CODE_STORAGE_POOL_DRIVE_DF_FAILED); } continue; } @@ -1983,11 +2016,11 @@ function order_target_drives($filesize_kb, $include_full_drives, $share, $path, // Email notification when all drives are over-capacity if (count($sorted_target_drives) == 0) { - Log::warn(" Warning! All storage pool drives are over-capacity!"); + Log::error(" Warning! All storage pool drives are over-capacity!", Log::EVENT_CODE_ALL_DRIVES_FULL); if (!isset($last_OOS_notification)) { $setting = Settings::get('last_OOS_notification'); if ($setting === FALSE) { - Log::warn("Received no rows when querying settings for 'last_OOS_notification'; expected one."); + Log::warn("Received no rows when querying settings for 'last_OOS_notification'; expected one.", Log::EVENT_CODE_SETTINGS_READ_ERROR); $setting = Settings::set('last_OOS_notification', 0); } $last_OOS_notification = $setting; @@ -2170,10 +2203,9 @@ function gh_fsck($path, $share, $storage_path = FALSE) { } - $list = array(); $handle = @opendir($path); if ($handle === FALSE) { - Log::error(" Couldn't open $path to list content. Skipping..."); + Log::error(" Couldn't open $path to list content. Skipping...", Log::EVENT_CODE_LIST_DIR_FAILED); return; } while (($filename = readdir($handle)) !== FALSE) { @@ -2368,7 +2400,7 @@ function gh_fsck_file($path, $filename, $file_type, $source, $share, $storage_pa // Try NFC form [http://en.wikipedia.org/wiki/Unicode_equivalence#Normalization] $root_path = str_replace(normalize_utf8_characters(clean_dir("/$share/$file_path/$filename")), '', normalize_utf8_characters($metafile->path)); if ($root_path == $metafile->path) { - Log::warn("Couldn't find root path for $metafile->path"); + Log::warn("Couldn't find root path for $metafile->path", Log::EVENT_CODE_FSCK_METAFILE_ROOT_PATH_NOT_FOUND); } if ($inode_number !== FALSE && $metafile->state == 'OK') { Log::debug("Found $metafile->path"); @@ -2391,7 +2423,7 @@ function gh_fsck_file($path, $filename, $file_type, $source, $share, $storage_pa $link_target = readlink($metafile->path); if (array_contains($file_copies_inodes, $link_target)) { // This link points to another file copy. Bad, bad! - Log::warn("Warning! Found a symlink in your storage pool: $metafile->path -> $link_target. Deleting."); + Log::warn("Warning! Found a symlink in your storage pool: $metafile->path -> $link_target. Deleting.", Log::EVENT_CODE_FSCK_SYMLINK_FOUND_IN_STORAGE_POOL); gh_recycle($metafile->path); } $inode_number = FALSE; @@ -2424,7 +2456,7 @@ function gh_fsck_file($path, $filename, $file_type, $source, $share, $storage_pa $num_copies_required = get_num_copies($share); if ($num_copies_required == -1) { - Log::warn("Tried to fsck a share that is missing from greyhole.conf. Skipping."); + Log::warn("Tried to fsck a share that is missing from greyhole.conf. Skipping.", Log::EVENT_CODE_FSCK_UNKNOWN_SHARE); return; } @@ -2522,10 +2554,10 @@ function gh_fsck_file($path, $filename, $file_type, $source, $share, $storage_pa if ($file_size === FALSE) { // Empty file; just delete it. - Log::warn(" An empty file copy was found: $real_full_path is 0 bytes. Original: $original_file_path is " . number_format($expected_file_size) . " bytes. This empty copy will be deleted."); + Log::warn(" An empty file copy was found: $real_full_path is 0 bytes. Original: $original_file_path is " . number_format($expected_file_size) . " bytes. This empty copy will be deleted.", Log::EVENT_CODE_FSCK_EMPTY_FILE_COPY_FOUND); unlink($real_full_path); } else { - Log::warn(" A file copy with a different file size than the original was found: $real_full_path is " . number_format($file_size) . " bytes. Original: $original_file_path is " . number_format($expected_file_size) . " bytes."); + Log::warn(" A file copy with a different file size than the original was found: $real_full_path is " . number_format($file_size) . " bytes. Original: $original_file_path is " . number_format($expected_file_size) . " bytes.", Log::EVENT_CODE_FSCK_SIZE_MISMATCH_FILE_COPY_FOUND); gh_recycle($real_full_path); $fsck_report['wrong_file_size'][clean_dir($real_full_path)] = array($file_size, $expected_file_size, $original_file_path); } @@ -2557,7 +2589,7 @@ function gh_fsck_file($path, $filename, $file_type, $source, $share, $storage_pa save_metafiles($share, $file_path, $filename, $file_metafiles); } } else if (count($file_copies_inodes) == 0 && !isset($original_file_path)) { - Log::warn(' WARNING! No copies of this file are available in the Greyhole storage pool. ' . (is_link("$landing_zone/$file_path/$filename") ? 'Deleting from share.' : (gh_is_file("$landing_zone/$file_path/$filename") ? 'Did you copy that file there without using your Samba shares? (If you did, don\'t do that in the future.)' : ''))); + Log::warn(' WARNING! No copies of this file are available in the Greyhole storage pool. ' . (is_link("$landing_zone/$file_path/$filename") ? 'Deleting from share.' : (gh_is_file("$landing_zone/$file_path/$filename") ? 'Did you copy that file there without using your Samba shares? (If you did, don\'t do that in the future.)' : '')), Log::EVENT_CODE_FSCK_NO_FILE_COPIES); if ($source == 'metastore' || get_metafile_data_filename($share, $file_path, $filename) !== FALSE) { $fsck_report['no_copies_found_files'][clean_dir("$share/$file_path/$filename")] = TRUE; } @@ -2958,7 +2990,7 @@ function gh_recycle($real_path, $file_was_modified=FALSE) { if ($should_move_to_trash) { // Move to trash if (!isset($trash_path)) { - Log::warn(" Warning! Can't find trash for $real_path. Won't delete this file!"); + Log::warn(" Warning! Can't find trash for $real_path. Won't delete this file!", Log::EVENT_CODE_TRASH_NOT_FOUND); return FALSE; } @@ -3254,7 +3286,7 @@ function gh_balance() { $pool_drives_avail_space[$sp_drive] -= $filesize; $pool_drives_avail_space[$source_drive] += $filesize; } else { - Log::warn(" Failed file copy. Skipping."); + Log::warn(" Failed file copy. Skipping.", Log::EVENT_CODE_FILE_COPY_FAILED); @unlink($temp_path); continue; } @@ -3298,7 +3330,7 @@ function gh_balance() { Log::debug("Some shares with sticky files were skipped. Balancing will now re-start to continue moving those sticky files as needed, and further balance. Recursion level = " . count($arr)); gh_balance(); } else { - Log::warn("Maximum number of consecutive balance reached. You'll need to re-execute --balance if you want to balance further."); + Log::info("Maximum number of consecutive balance reached. You'll need to re-execute --balance if you want to balance further."); } } } @@ -3369,7 +3401,7 @@ function process_config() { sleep(600); // 10 minutes } else { // Otherwise, die. - Log::critical("Config file parsing failed. Exiting."); + Log::critical("Config file parsing failed. Exiting.", Log::EVENT_CODE_CONFIG_FILE_PARSING_FAILED); } } // Config is OK; go on! @@ -3515,7 +3547,7 @@ function create_trash_share_symlink($filepath_in_trash, $trash_path) { if (@gh_symlink($filepath_in_trash, $filepath_in_trash_share)) { Log::debug(" Created symlink to deleted file in {$trash_share['name']} share ($filename)."); } else { - Log::warn(" Warning! Couldn't create symlink to deleted file in {$trash_share['name']} share ($filename)."); + Log::warn(" Warning! Couldn't create symlink to deleted file in {$trash_share['name']} share ($filename).", Log::EVENT_CODE_TRASH_SYMLINK_FAILED); } } } @@ -3663,8 +3695,7 @@ function gh_check_md5($task) { } } - Log::warn(" A file copy with a different checksum than the original was found: $latest_file_copy = $md5. Original: $original_file_path = $original_md5"); - Log::warn(" This copy will be deleted, and replaced with a new copy from $original_file_path"); + Log::warn(" A file copy with a different checksum than the original was found: $latest_file_copy = $md5. Original: $original_file_path = $original_md5. This copy will be deleted, and replaced with a new copy from $original_file_path", Log::EVENT_CODE_FSCK_MD5_MISMATCH); gh_recycle($latest_file_copy); $metafiles = array(); @@ -3697,13 +3728,13 @@ function gh_check_md5($task) { if (isset($logs)) { // Write to greyhole.log foreach ($logs as $log) { - Log::error($log); + Log::error($log, Log::EVENT_CODE_FSCK_MD5_MISMATCH); } // Write in fsck_checksums.log too $flog = fopen(FSCKLogFile::PATH . '/fsck_checksums.log', 'a'); if (!$flog) { - Log::critical("Couldn't open log file: " . FSCKLogFile::PATH . "/fsck_checksums.log"); + Log::critical("Couldn't open log file: " . FSCKLogFile::PATH . "/fsck_checksums.log", Log::EVENT_CODE_FSCK_MD5_LOG_FAILURE); } fwrite($flog, $date = date("M d H:i:s") . ' ' . implode("\n", $logs) . "\n\n"); fclose($flog); @@ -3766,7 +3797,7 @@ function get_file_inodes($share, $file_path, $filename, &$file_metafiles, $one_i $inode_number = @gh_fileinode($clean_full_path); if ($inode_number !== FALSE) { if (is_dir($clean_full_path)) { - Log::warn("Warning! Found a directory that should be a file! Will try to remove it, if it's empty."); + Log::info("Found a directory that should be a file! Will try to remove it, if it's empty."); @rmdir($clean_full_path); continue; } @@ -3786,7 +3817,7 @@ function get_file_inodes($share, $file_path, $filename, &$file_metafiles, $one_i } } if (is_string($file_metafiles)) { - Log::critical("Fatal error! \$file_metafiles is now a string: '$file_metafiles'."); + Log::critical("Fatal error! \$file_metafiles is now a string: '$file_metafiles'.", Log::EVENT_CODE_UNEXPECTED_VAR); } $file_metafiles[$clean_full_path] = (object) array('path' => $clean_full_path, 'is_linked' => FALSE, 'state' => $state); diff --git a/greyhole.example.conf b/greyhole.example.conf index 1b86c556..18458581 100644 --- a/greyhole.example.conf +++ b/greyhole.example.conf @@ -281,3 +281,27 @@ ignored_files = \.DS_Store # Windows thumbs database files #ignored_files = Thumb.db + +#### Hooks #### +# Call custom scripts on events. +# +# Available events type: create, edit, rename, delete, mkdir, rmdir, warning, error, critical, fsck, idle, not_idle +# - For create, edit, rename, delete, mkdir, and rmdir: the hooks are called after Greyhole finished processing the operation; +# - for warning, error, critical and fsck: the hooks are called after Greyhole created a log; +# - for idle and not_idle: the hooks are called just before the daemon will sleep, or when it's about to start working again. +# +# The parameters sent to the custom scripts are: +# - event_type (one of the above) +# If event is related to a file/folder on a share (create, edit, rename, delete, mkdir and rmdir), other params will be: +# - share_name +# - path_on_share +# - original_path_on_share (only for rename) +# If event is related to a log (warning, error, critical, fsck, idle and not_idle), other params will be: +# - event_code: one of the predefined values that define the actual error/event. +# Look here for the list of possible codes: https://github.com/gboudreau/Greyhole/blob/master/includes/Log.php#L62 +# - log: user-readable log (might contain LF characters) +# +# hook[create|edit|rename|delete|mkdir|rmdir] = /usr/share/greyhole/scripts-examples/greyhole_file_changed.sh +# hook[warning|error|critical] = /usr/share/greyhole/scripts-examples/greyhole_notify_error.sh +# hook[fsck] = /usr/share/greyhole/scripts-examples/greyhole_send_fsck_report.sh +# hook[idle|not_idle] = /usr/share/greyhole/scripts-examples/greyhole_idle.sh diff --git a/greyhole.spec b/greyhole.spec index b1ed0244..e0632df7 100644 --- a/greyhole.spec +++ b/greyhole.spec @@ -26,6 +26,7 @@ rm -rf $RPM_BUILD_ROOT mkdir -p $RPM_BUILD_ROOT/etc/rc.d/init.d mkdir -p $RPM_BUILD_ROOT%{_bindir} mkdir -p $RPM_BUILD_ROOT/usr/share/greyhole/web-app/ +mkdir -p $RPM_BUILD_ROOT/usr/share/greyhole/scripts-examples/ mkdir -p $RPM_BUILD_ROOT/usr/share/man/man1/ mkdir -p $RPM_BUILD_ROOT/usr/share/man/man5/ @@ -49,6 +50,11 @@ install -m 0755 -D -p greyhole.cron.daily ${RPM_BUILD_ROOT}%{_sysconfdir}/cron.d install -m 0644 -D -p web-app/index.php ${RPM_BUILD_ROOT}/usr/share/greyhole/web-app/ install -m 0644 -D -p web-app/README ${RPM_BUILD_ROOT}/usr/share/greyhole/web-app/ +install -m 0755 -D -p scripts-examples/greyhole_file_changed.sh ${RPM_BUILD_ROOT}/usr/share/greyhole/scripts-examples/ +install -m 0755 -D -p scripts-examples/greyhole_idle.sh ${RPM_BUILD_ROOT}/usr/share/greyhole/scripts-examples/ +install -m 0755 -D -p scripts-examples/greyhole_notify_error.sh ${RPM_BUILD_ROOT}/usr/share/greyhole/scripts-examples/ +install -m 0755 -D -p scripts-examples/greyhole_send_fsck_report.sh ${RPM_BUILD_ROOT}/usr/share/greyhole/scripts-examples/ + install -m 0644 -D -p USAGE ${RPM_BUILD_ROOT}/usr/share/greyhole/ install -m 0644 -D -p docs/greyhole.1.gz ${RPM_BUILD_ROOT}/usr/share/man/man1/ diff --git a/includes/CLI/GoneCliRunner.php b/includes/CLI/GoneCliRunner.php index 49498580..efda470f 100755 --- a/includes/CLI/GoneCliRunner.php +++ b/includes/CLI/GoneCliRunner.php @@ -109,7 +109,7 @@ public function run() { protected static function check_going_dir($path, $share, $going_drive) { $handle = @opendir($path); if ($handle === FALSE) { - Log::error("Couldn't open $path to list content. Skipping..."); + Log::error("Couldn't open $path to list content. Skipping...", Log::EVENT_CODE_LIST_DIR_FAILED); return; } Log::debug("Entering $path"); @@ -135,7 +135,7 @@ protected static function check_going_dir($path, $share, $going_drive) { $file_metafiles = array(); $file_copies_inodes = get_file_inodes($share, $file_path, $filename, $file_metafiles, TRUE); if (count($file_copies_inodes) == 0) { - Log::warn("Found a file, $full_path, that has no other copies on other drives. Removing $going_drive would make that file disappear! Will create extra copies now."); + Log::debug("Found a file, $full_path, that has no other copies on other drives. Removing $going_drive would make that file disappear! Will create extra copies now."); echo "."; gh_fsck_file($path, $filename, $file_type, 'landing_zone', $share, $going_drive); } diff --git a/includes/ConfigHelper.php b/includes/ConfigHelper.php index 349feb0e..bcf8e8e6 100644 --- a/includes/ConfigHelper.php +++ b/includes/ConfigHelper.php @@ -1,6 +1,6 @@ $new_name) { if (string_contains($name, $old_name)) { $fixed_name = str_replace($old_name, $new_name, $name); - Log::warn("Deprecated option found in greyhole.conf: $name. You should change that to: $fixed_name"); + Log::warn("Deprecated option found in greyhole.conf: $name. You should change that to: $fixed_name", Log::EVENT_CODE_CONFIG_DEPRECATED_OPTION); $name = $fixed_name; } } @@ -262,7 +266,7 @@ private static function parse_line_string($name, $value) { private static function parse_line_log($name, $value) { if ($name == CONFIG_LOG_LEVEL) { - self::assert(defined("Log::$value"), "Invalid value for log_level: '$value'"); + self::assert(defined("Log::$value"), "Invalid value for log_level: '$value'", Log::EVENT_CODE_CONFIG_INVALID_VALUE); Config::set(CONFIG_LOG_LEVEL, constant("Log::$value")); return TRUE; } @@ -285,7 +289,7 @@ private static function parse_line_pool_drive($name, $value) { } Config::add(CONFIG_MIN_FREE_SPACE_POOL_DRIVE, $value, $sp_drive); } else { - Log::warn("Warning! Unable to parse " . CONFIG_STORAGE_POOL_DRIVE . " line from config file. Value = $value"); + Log::warn("Warning! Unable to parse " . CONFIG_STORAGE_POOL_DRIVE . " line from config file. Value = $value", Log::EVENT_CODE_CONFIG_UNPARSEABLE_LINE); } return TRUE; } @@ -385,11 +389,30 @@ private static function parse_line_share_option($name, $value) { return TRUE; } + private static function parse_line_hook($name, $value) { + if (string_starts_with($name, CONFIG_HOOK)) { + if (!preg_match('/hook\[([^\]]+)\]/', $name, $re)) { + Log::warn("Can't parse the following config line: $name; ignoring.", Log::EVENT_CODE_CONFIG_UNPARSEABLE_LINE); + return TRUE; + } + if (!is_executable($value)) { + Log::warn("Hook script $value is not executable; ignoring.", Log::EVENT_CODE_CONFIG_HOOK_SCRIPT_NOT_EXECUTABLE); + return TRUE; + } + $events = explode('|', $re[1]); + foreach ($events as $event) { + Hook::add($event, $value); + } + return TRUE; + } + return FALSE; + } + private static function init() { Log::setLevel(Config::get(CONFIG_LOG_LEVEL)); if (count(Config::storagePoolDrives()) == 0) { - Log::error("You have no '" . CONFIG_STORAGE_POOL_DRIVE . "' defined. Greyhole can't run."); + Log::error("You have no '" . CONFIG_STORAGE_POOL_DRIVE . "' defined. Greyhole can't run.", Log::EVENT_CODE_CONFIG_NO_STORAGE_POOL); return FALSE; } @@ -441,7 +464,7 @@ private static function init() { SharesConfig::set($share_name, CONFIG_NUM_COPIES, count(Config::storagePoolDrives())); } if (!isset($share_options[CONFIG_LANDING_ZONE])) { - Log::warn("Found a share ($share_name) defined in " . self::$config_file . " with no path in " . self::$smb_config_file . ". Either add this share in " . self::$smb_config_file . ", or remove it from " . self::$config_file . ", then restart Greyhole."); + Log::warn("Found a share ($share_name) defined in " . self::$config_file . " with no path in " . self::$smb_config_file . ". Either add this share in " . self::$smb_config_file . ", or remove it from " . self::$config_file . ", then restart Greyhole.", Log::EVENT_CODE_CONFIG_SHARE_MISSING_FROM_SMB_CONF); return FALSE; } if (!isset($share_options[CONFIG_DELETE_MOVES_TO_TRASH])) { @@ -464,10 +487,10 @@ private static function init() { // Validate that the landing zone is NOT a subdirectory of a storage pool drive, and that storage pool drives are not subdirectories of the landing zone! foreach (Config::storagePoolDrives() as $sp_drive) { if (string_starts_with($share_options[CONFIG_LANDING_ZONE], $sp_drive)) { - Log::critical("Found a share ($share_name), with path " . $share_options[CONFIG_LANDING_ZONE] . ", which is INSIDE a storage pool drive ($sp_drive). Share directories should never be inside a directory that you have in your storage pool.\nFor your shares to use your storage pool, you just need them to have 'vfs objects = greyhole' in their (smb.conf) config; their location on your file system is irrelevant."); + Log::critical("Found a share ($share_name), with path " . $share_options[CONFIG_LANDING_ZONE] . ", which is INSIDE a storage pool drive ($sp_drive). Share directories should never be inside a directory that you have in your storage pool.\nFor your shares to use your storage pool, you just need them to have 'vfs objects = greyhole' in their (smb.conf) config; their location on your file system is irrelevant.", Log::EVENT_CODE_CONFIG_LZ_INSIDE_STORAGE_POOL); } if (string_starts_with($sp_drive, $share_options[CONFIG_LANDING_ZONE])) { - Log::critical("Found a storage pool drive ($sp_drive), which is INSIDE a share landing zone (" . $share_options[CONFIG_LANDING_ZONE] . "), for share $share_name. Storage pool drives should never be inside a directory that you use as a share landing zone ('path' in smb.conf).\nFor your shares to use your storage pool, you just need them to have 'vfs objects = greyhole' in their (smb.conf) config; their location on your file system is irrelevant."); + Log::critical("Found a storage pool drive ($sp_drive), which is INSIDE a share landing zone (" . $share_options[CONFIG_LANDING_ZONE] . "), for share $share_name. Storage pool drives should never be inside a directory that you use as a share landing zone ('path' in smb.conf).\nFor your shares to use your storage pool, you just need them to have 'vfs objects = greyhole' in their (smb.conf) config; their location on your file system is irrelevant.", Log::EVENT_CODE_CONFIG_STORAGE_POOL_INSIDE_LZ); } } } @@ -483,7 +506,7 @@ private static function init() { } } if (!$found) { - Log::warn("The storage pool drive '$sp_drive' is not part of any drive_selection_algorithm definition, and will thus never be used to receive any files."); + Log::warn("The storage pool drive '$sp_drive' is not part of any drive_selection_algorithm definition, and will thus never be used to receive any files.", Log::EVENT_CODE_CONFIG_STORAGE_POOL_DRIVE_NOT_IN_DRIVE_SELECTION_ALGO); } } @@ -525,9 +548,9 @@ private static function init() { return TRUE; } - private static function assert($check, $error_message) { + private static function assert($check, $error_message, $event_code) { if ($check === FALSE) { - Log::critical($error_message); + Log::critical($error_message, $event_code); } } } diff --git a/includes/DB.php b/includes/DB.php index aedfc5fd..1ff49180 100644 --- a/includes/DB.php +++ b/includes/DB.php @@ -43,7 +43,7 @@ public static function connect($retry_until_successful=FALSE) { return DB::connect(TRUE); } echo "ERROR: Can't connect to database: " . $ex->getMessage() . "\n"; - Log::critical("Can't connect to database: " . $ex->getMessage()); + Log::critical("Can't connect to database: " . $ex->getMessage(), Log::EVENT_CODE_DB_CONNECT_FAILED); } if (self::$handle) { @@ -375,7 +375,7 @@ private static function migrate_10_utf8() { try { DB::execute("ALTER TABLE `$table_name` CHARACTER SET utf8 COLLATE utf8_general_ci"); } catch (Exception $ex) { - Log::warn(" ALTER TABLE failed."); + Log::warn(" ALTER TABLE failed.", Log::EVENT_CODE_DB_MIGRATION_FAILED); } } } @@ -413,7 +413,7 @@ public static function deleteExecutedTasks() { return; } if (!is_int($executed_tasks_retention)) { - Log::critical("Error: Invalid value for 'executed_tasks_retention' in greyhole.conf: '$executed_tasks_retention'. You need to use either 'forever' (no quotes), or a number of days."); + Log::critical("Error: Invalid value for 'executed_tasks_retention' in greyhole.conf: '$executed_tasks_retention'. You need to use either 'forever' (no quotes), or a number of days.", Log::EVENT_CODE_CONFIG_INVALID_VALUE); } Log::info("Cleaning executed tasks: keeping the last $executed_tasks_retention days of logs."); $query = sprintf("DELETE FROM tasks_completed WHERE event_date < NOW() - INTERVAL %d DAY", (int) $executed_tasks_retention); diff --git a/includes/DaemonRunner.php b/includes/DaemonRunner.php index bf1974cf..eb6fc45d 100755 --- a/includes/DaemonRunner.php +++ b/includes/DaemonRunner.php @@ -26,9 +26,14 @@ public function run() { die("Found an already running Greyhole daemon with PID " . self::getPID() . ".\nCan't start multiple Greyhole daemons.\nQuitting.\n"); } - Log::info("Greyhole (version %VERSION%) daemon started."); + $log = "Greyhole (version %VERSION%) daemon started."; + Log::info($log); $this->initialize(); + global $was_idle; + $was_idle = TRUE; + LogHook::trigger(LogHook::EVENT_TYPE_IDLE, Log::EVENT_CODE_IDLE, $log); + // The daemon runs indefinitely, this the infinite loop here. while (TRUE) { // Process the spool directory, and insert each task found there into the database. diff --git a/includes/Hook.php b/includes/Hook.php new file mode 100644 index 00000000..df9acd9d --- /dev/null +++ b/includes/Hook.php @@ -0,0 +1,194 @@ +. +*/ + +abstract class Hook +{ + protected $event_type; + protected $script; + + protected static $hooks = array(); + + /** + * @param string $event_type + * @param string $script + */ + public function __construct($event_type, $script) { + $this->event_type = $event_type; + $this->script = $script; + } + + /** + * @param string $event_type + * @param string $script + */ + public static function add($event_type, $script) { + if (array_contains(FileHook::getEventTypes(), $event_type)) { + $hook = new FileHook($event_type, $script); + } elseif (array_contains(LogHook::getEventTypes(), $event_type)) { + $hook = new LogHook($event_type, $script); + } else { + Log::warn("Unknown hook event type '$event_type'; ignoring.", Log::EVENT_CODE_HOOK_NOT_EXECUTABLE); + return; + } + static::$hooks[$event_type][] = $hook; + } + + public static function hasHookForEvent($event_type) { + return !empty(static::$hooks[$event_type]); + } + + /** + * @param string $event_type + * @param HookContext $context + */ + protected static function _trigger($event_type, $context) { + $hooks = @static::$hooks[$event_type]; + if (!is_array($hooks)) { + return; + } + foreach ($hooks as $hook) { + Log::debug("Calling external hook $hook->script for event $hook->event_type ..."); + exec(escapeshellarg($hook->script) . " " . implode(' ', $hook->getArgs($context)) . " 2>&1", $output, $result_code); + if (!empty($output)) { + foreach($output as $line) { + Log::debug(" $line"); + } + } + if ($result_code === 0) { + Log::debug("External hook exited with status code $result_code."); + } else { + if ($hook->event_type == LogHook::EVENT_TYPE_WARNING) { + // Don't start an infinite loop! + } else { + Log::warn("External hook $hook->script exited with status code $result_code.", LogHook::EVENT_CODE_HOOK_NON_ZERO_EXIT_CODE_IN_WARN); + } + } + } + } + + /** + * @param HookContext $context + */ + abstract protected function getArgs($context); +} + +interface HookContext {} + +class FileHookContext implements HookContext +{ + public $share; + public $path_on_share; + public $from_path_on_share; + public function __construct($share, $path_on_share, $from_path_on_share = NULL) { + $this->share = $share; + $this->path_on_share = $path_on_share; + $this->from_path_on_share = $from_path_on_share; + } +} + +class FileHook extends Hook +{ + const EVENT_TYPE_CREATE = 'create'; + const EVENT_TYPE_EDIT = 'edit'; + const EVENT_TYPE_RENAME = 'rename'; + const EVENT_TYPE_DELETE = 'delete'; + const EVENT_TYPE_MKDIR = 'mkdir'; + const EVENT_TYPE_RMDIR = 'rmdir'; + + public static function trigger($event_type, $share, $path_on_share, $from_path_on_share = NULL) { + Hook::_trigger($event_type, new FileHookContext($share, $path_on_share, $from_path_on_share)); + } + + /** + * @param FileHookContext $context + * @return array + */ + protected function getArgs($context) { + $args = array( + escapeshellarg($this->event_type), + escapeshellarg($context->share), + escapeshellarg($context->path_on_share) + ); + if (!empty($context->from_path_on_share)) { + $args[] = escapeshellarg($context->from_path_on_share); + } + return $args; + } + + public static function getEventTypes() { + return array( + static::EVENT_TYPE_CREATE, + static::EVENT_TYPE_EDIT, + static::EVENT_TYPE_RENAME, + static::EVENT_TYPE_DELETE, + static::EVENT_TYPE_MKDIR, + static::EVENT_TYPE_RMDIR + ); + } +} + +class LogHookContext implements HookContext +{ + public $event_code; + public $log; + public function __construct($event_code, $log) { + $this->event_code = $event_code; + $this->log = $log; + } +} + +class LogHook extends Hook +{ + const EVENT_TYPE_WARNING = 'warning'; + const EVENT_TYPE_ERROR = 'error'; + const EVENT_TYPE_CRITICAL = 'critical'; + const EVENT_TYPE_IDLE = 'idle'; + const EVENT_TYPE_NOT_IDLE = 'not_idle'; + const EVENT_TYPE_FSCK = 'fsck'; + + const EVENT_CODE_HOOK_NON_ZERO_EXIT_CODE_IN_WARN = 1; // Used to prevent infinite loop, when logging a WARNING from a LogHook! + + public static function trigger($event_type, $event_code, $log) { + Hook::_trigger($event_type, new LogHookContext($event_code, $log)); + } + + /** + * @param LogHookContext $context + * @return array + */ + protected function getArgs($context) { + return array( + escapeshellarg($this->event_type), + escapeshellarg($context->event_code), + escapeshellarg($context->log) + ); + } + + public static function getEventTypes() { + return array( + static::EVENT_TYPE_WARNING, + static::EVENT_TYPE_ERROR, + static::EVENT_TYPE_CRITICAL, + static::EVENT_TYPE_IDLE, + static::EVENT_TYPE_NOT_IDLE, + static::EVENT_TYPE_FSCK + ); + } +} diff --git a/includes/Log.php b/includes/Log.php index 915640cc..2746eda5 100644 --- a/includes/Log.php +++ b/includes/Log.php @@ -59,6 +59,62 @@ final class Log { const ERROR = 3; const CRITICAL = 2; + const EVENT_CODE_ALL_DRIVES_FULL = 'all_drives_full'; + const EVENT_CODE_CONFIG_DEPRECATED_OPTION = 'config_deprecated_option'; + const EVENT_CODE_CONFIG_FILE_PARSING_FAILED = 'config_file_parsing_failed'; + const EVENT_CODE_CONFIG_HOOK_SCRIPT_NOT_EXECUTABLE = 'config_hook_script_not_executable'; + const EVENT_CODE_CONFIG_INCLUDE_INSECURE_PERMISSIONS = 'config_include_insecure_permissions'; + const EVENT_CODE_CONFIG_INVALID_VALUE = 'config_invalid_value'; + const EVENT_CODE_CONFIG_LZ_INSIDE_STORAGE_POOL = 'config_lz_inside_storage_pool'; + const EVENT_CODE_CONFIG_NO_STORAGE_POOL = 'config_no_storage_pool'; + const EVENT_CODE_CONFIG_SHARE_MISSING_FROM_SMB_CONF = 'config_share_missing_from_smb_conf'; + const EVENT_CODE_CONFIG_STORAGE_POOL_DRIVES_SAME_PARTITION = 'config_storage_pool_drives_same_partition'; + const EVENT_CODE_CONFIG_STORAGE_POOL_DRIVE_NOT_IN_DRIVE_SELECTION_ALGO = 'config_storage_pool_drive_not_in_drive_selection_algo'; + const EVENT_CODE_CONFIG_STORAGE_POOL_INSIDE_LZ = 'config_storage_pool_inside_lz'; + const EVENT_CODE_CONFIG_UNPARSEABLE_LINE = 'config_unparseable_line'; + const EVENT_CODE_DB_CONNECT_FAILED = 'db_connect_failed'; + const EVENT_CODE_DB_MIGRATION_FAILED = 'db_migration_failed'; + const EVENT_CODE_FILE_COPY_FAILED = 'file_copy_failed'; + const EVENT_CODE_FSCK_REPORT = 'fsck_report'; + const EVENT_CODE_FSCK_EMPTY_FILE_COPY_FOUND = 'fsck_empty_file_copy_found'; + const EVENT_CODE_FSCK_MD5_LOG_FAILURE = 'fsck_md5_log_failure'; + const EVENT_CODE_FSCK_MD5_MISMATCH = 'fsck_md5_mismatch'; + const EVENT_CODE_FSCK_METAFILE_ROOT_PATH_NOT_FOUND = 'fsck_metafile_root_path_not_found'; + const EVENT_CODE_FSCK_NO_FILE_COPIES = 'fsck_no_file_copies'; + const EVENT_CODE_FSCK_SIZE_MISMATCH_FILE_COPY_FOUND = 'fsck_size_mismatch_file_copy_found'; + const EVENT_CODE_FSCK_SYMLINK_FOUND_IN_STORAGE_POOL = 'fsck_symlink_found_in_storage_pool'; + const EVENT_CODE_FSCK_UNKNOWN_FOLDER = 'fsck_unknown_folder'; + const EVENT_CODE_FSCK_UNKNOWN_SHARE = 'fsck_unknown_share'; + const EVENT_CODE_HOOK_NON_ZERO_EXIT_CODE = 'hook_non_zero_exit_code'; + const EVENT_CODE_HOOK_NOT_EXECUTABLE = 'hook_not_executable'; + const EVENT_CODE_IDLE = "idle"; + const EVENT_CODE_IDLE_NOT = "busy"; + const EVENT_CODE_LIST_DIR_FAILED = 'list_dir_failed'; + const EVENT_CODE_MEMORY_LIMIT_REACHED = 'memory_limit_reached'; + const EVENT_CODE_METADATA_POINTS_TO_GONE_DRIVE = 'metadata_points_to_gone_drive'; + const EVENT_CODE_MKDIR_CHGRP_FAILED = 'mkdir_chgrp_failed'; + const EVENT_CODE_MKDIR_CHMOD_FAILED = 'mkdir_chmod_failed'; + const EVENT_CODE_MKDIR_CHOWN_FAILED = 'mkdir_chown_failed'; + const EVENT_CODE_MKDIR_FAILED = 'mkdir_failed'; + const EVENT_CODE_NO_METADATA_SAVED = 'no_metadata_saved'; + const EVENT_CODE_PHP_CRITICAL = 'php_critical'; + const EVENT_CODE_PHP_ERROR = 'php_error'; + const EVENT_CODE_PHP_WARNING = 'php_warning'; + const EVENT_CODE_RENAME_FILE_COPY_FAILED = 'rename_file_copy_failed'; + const EVENT_CODE_SAMBA_RESTART_FAILED = 'samba_restart_failed'; + const EVENT_CODE_SETTINGS_READ_ERROR = 'settings_read_error'; + const EVENT_CODE_SHARE_MISSING_FROM_GREYHOLE_CONF = 'share_missing_from_greyhole_conf'; + const EVENT_CODE_SPOOL_MOUNT_FAILED = 'spool_mount_failed'; + const EVENT_CODE_STORAGE_POOL_DRIVE_DF_FAILED = 'storage_pool_drive_df_failed'; + const EVENT_CODE_STORAGE_POOL_DRIVE_UUID_CHANGED = 'storage_pool_drive_uuid_changed'; + const EVENT_CODE_STORAGE_POOL_FOLDER_NOT_FOUND = 'storage_pool_folder_not_found'; + const EVENT_CODE_TASK_FOR_UNKNOWN_SHARE = 'task_for_unknown_share'; + const EVENT_CODE_TRASH_NOT_FOUND = 'trash_not_found'; + const EVENT_CODE_TRASH_SYMLINK_FAILED = 'trash_symlink_failed'; + const EVENT_CODE_UNEXPECTED_VAR = 'unexpected_var'; + const EVENT_CODE_VFS_MODULE_WRONG = 'vfs_module_wrong'; + const EVENT_CODE_ZFS_UNKNOWN_DEVICE = 'zfs_unknown_device'; + private static $log_level_names = array( 9 => 'PERF', 8 => 'TEST', @@ -94,27 +150,27 @@ public static function restorePreviousAction() { self::$action = self::$old_action; } - public static function debug($text) { - self::_log(self::DEBUG, $text); + public static function debug($text, $event_code = NULL) { + self::_log(self::DEBUG, $text, $event_code); } - public static function info($text) { - self::_log(self::INFO, $text); + public static function info($text, $event_code = NULL) { + self::_log(self::INFO, $text, $event_code); } - public static function warn($text) { - self::_log(self::WARN, $text); + public static function warn($text, $event_code) { + self::_log(self::WARN, $text, $event_code); } - public static function error($text) { - self::_log(self::ERROR, $text); + public static function error($text, $event_code) { + self::_log(self::ERROR, $text, $event_code); } - public static function critical($text) { - self::_log(self::CRITICAL, $text); + public static function critical($text, $event_code) { + self::_log(self::CRITICAL, $text, $event_code); } - private static function _log($local_log_level, $text) { + private static function _log($local_log_level, $text, $event_code) { if (self::$action == 'test-config') { $greyhole_log_file = NULL; $use_syslog = FALSE; @@ -157,6 +213,17 @@ private static function _log($local_log_level, $text) { error_log(trim($log_text)); } + if ($local_log_level == self::WARN) { + // Prevent infinite loop; this warning came from running a warning Hook, so... + if ($event_code != LogHook::EVENT_CODE_HOOK_NON_ZERO_EXIT_CODE_IN_WARN) { + LogHook::trigger(LogHook::EVENT_TYPE_WARNING, $event_code, $log_text); + } + } elseif ($local_log_level == self::ERROR) { + LogHook::trigger(LogHook::EVENT_TYPE_ERROR, $event_code, $log_text); + } elseif ($local_log_level == self::CRITICAL) { + LogHook::trigger(LogHook::EVENT_TYPE_CRITICAL, $event_code, $log_text); + } + if ($local_log_level === self::CRITICAL) { exit(1); } diff --git a/includes/MigrationHelper.php b/includes/MigrationHelper.php index 4ad796c2..e40a3ec4 100755 --- a/includes/MigrationHelper.php +++ b/includes/MigrationHelper.php @@ -74,7 +74,7 @@ public static function convertStoragePoolDrivesTagFiles() { unlink("$sp_drive/.greyhole_uses_this"); } if ($drives_definitions[$sp_drive] != $drive_uuid) { - Log::warn("Warning! It seems the partition UUID of $sp_drive changed. This probably means this mount is currently unmounted, or that you replaced this drive and didn't use 'greyhole --replace'. Because of that, Greyhole will NOT use this drive at this time."); + Log::warn("Warning! It seems the partition UUID of $sp_drive changed. This probably means this mount is currently unmounted, or that you replaced this drive and didn't use 'greyhole --replace'. Because of that, Greyhole will NOT use this drive at this time.", Log::EVENT_CODE_STORAGE_POOL_DRIVE_UUID_CHANGED); } } foreach ($drives_definitions as $sp_drive => $uuid) { @@ -93,7 +93,7 @@ public static function convertStoragePoolDrivesTagFiles() { if (Config::get(CONFIG_ALLOW_MULTIPLE_SP_PER_DRIVE)) { Log::info("The following storage pool drives are on the same partition: " . implode(", ", $sp_drives) . ", but per greyhole.conf '" . CONFIG_ALLOW_MULTIPLE_SP_PER_DRIVE . "' options, you chose to ignore this normally critical error."); } else { - Log::critical("ERROR: The following storage pool drives are on the same partition: " . implode(", ", $sp_drives) . ". The Greyhole daemon will now stop."); + Log::critical("ERROR: The following storage pool drives are on the same partition: " . implode(", ", $sp_drives) . ". The Greyhole daemon will now stop.", Log::EVENT_CODE_CONFIG_STORAGE_POOL_DRIVES_SAME_PARTITION); } } } diff --git a/includes/PoolDriveSelector.php b/includes/PoolDriveSelector.php index 45d0634a..8f66645d 100644 --- a/includes/PoolDriveSelector.php +++ b/includes/PoolDriveSelector.php @@ -51,7 +51,7 @@ function init(&$sorted_target_drives, &$last_resort_sorted_target_drives) { arsort($sorted_target_drives); arsort($last_resort_sorted_target_drives); } else { - Log::critical("Unknown '" . CONFIG_DRIVE_SELECTION_ALGORITHM . "' found: " . $this->selection_algorithm); + Log::critical("Unknown '" . CONFIG_DRIVE_SELECTION_ALGORITHM . "' found: " . $this->selection_algorithm, Log::EVENT_CODE_CONFIG_INVALID_VALUE); } // Only keep drives that are in $this->drives $this->sorted_target_drives = array(); @@ -101,7 +101,7 @@ static function parse($config_string, $drive_selection_groups) { return $ds; } if (!preg_match('/forced ?\((.+)\) ?(least_used_space|most_available_space)/i', $config_string, $regs)) { - Log::critical("Can't understand the '" . CONFIG_DRIVE_SELECTION_ALGORITHM . "' value: $config_string"); + Log::critical("Can't understand the '" . CONFIG_DRIVE_SELECTION_ALGORITHM . "' value: $config_string", Log::EVENT_CODE_CONFIG_INVALID_VALUE); } $selection_algorithm = $regs[2]; $groups = array_map('trim', explode(',', $regs[1])); diff --git a/includes/StoragePool.php b/includes/StoragePool.php index aaebb8d8..51e8a5fc 100644 --- a/includes/StoragePool.php +++ b/includes/StoragePool.php @@ -70,7 +70,7 @@ public static function check_drives($skip_fsck=FALSE) { } self::mark_gone_drive_fscked($sp_drive); $missing_drives[] = $sp_drive; - Log::warn("Warning! It seems the partition UUID of $sp_drive changed. This probably means this mount is currently unmounted, or that you replaced this drive and didn't use 'greyhole --replace'. Because of that, Greyhole will NOT use this drive at this time."); + Log::warn("Warning! It seems the partition UUID of $sp_drive changed. This probably means this mount is currently unmounted, or that you replaced this drive and didn't use 'greyhole --replace'. Because of that, Greyhole will NOT use this drive at this time.", Log::EVENT_CODE_STORAGE_POOL_DRIVE_UUID_CHANGED); Log::debug("Email sent for gone drive: $sp_drive"); self::$gone_ok_drives[$sp_drive] = TRUE; // The upcoming fsck should not recreate missing copies just yet } else if ((self::gone_ok($sp_drive, $j++ == 0) || self::gone_fscked($sp_drive, $i++ == 0)) && self::is_pool_drive($sp_drive) && !empty($drives_definitions[$sp_drive])) { diff --git a/includes/SystemHelper.php b/includes/SystemHelper.php index cd20151f..66ce9e0d 100644 --- a/includes/SystemHelper.php +++ b/includes/SystemHelper.php @@ -47,7 +47,7 @@ public static function directory_uuid($dir) { } } if (empty($dev)) { - Log::warn("Warning! Couldn't find the device used by your ZFS pool name $pool. That pool will never be used."); + Log::warn("Warning! Couldn't find the device used by your ZFS pool name $pool. That pool will never be used.", Log::EVENT_CODE_ZFS_UNKNOWN_DEVICE); return FALSE; } } diff --git a/includes/common.php b/includes/common.php index 0655e578..72b1f3a8 100644 --- a/includes/common.php +++ b/includes/common.php @@ -21,6 +21,7 @@ // Other helpers require_once('includes/ConfigHelper.php'); require_once('includes/DB.php'); +require_once('includes/Hook.php'); require_once('includes/Log.php'); require_once('includes/MigrationHelper.php'); require_once('includes/PoolDriveSelector.php'); @@ -76,7 +77,7 @@ function explode_full_path($full_path) { function gh_shutdown() { if ($err = error_get_last()) { - Log::error("PHP Fatal Error: " . $err['message'] . "; BT: " . basename($err['file']) . '[L' . $err['line'] . '] '); + Log::error("PHP Fatal Error: " . $err['message'] . "; BT: " . basename($err['file']) . '[L' . $err['line'] . '] ', Log::EVENT_CODE_PHP_CRITICAL); } } @@ -91,7 +92,7 @@ function gh_error_handler($errno, $errstr, $errfile, $errline, $errcontext) { case E_PARSE: case E_CORE_ERROR: case E_COMPILE_ERROR: - Log::critical("PHP Error [$errno]: $errstr in $errfile on line $errline"); + Log::critical("PHP Error [$errno]: $errstr in $errfile on line $errline", Log::EVENT_CODE_PHP_CRITICAL); break; case E_WARNING: @@ -104,11 +105,11 @@ function gh_error_handler($errno, $errstr, $errfile, $errline, $errcontext) { // What would have been logged will be echoed instead. return TRUE; } - Log::warn("PHP Warning [$errno]: $errstr in $errfile on line $errline; BT: " . get_debug_bt()); + Log::warn("PHP Warning [$errno]: $errstr in $errfile on line $errline; BT: " . get_debug_bt(), Log::EVENT_CODE_PHP_WARNING); break; default: - Log::warn("PHP Unknown Error [$errno]: $errstr in $errfile on line $errline"); + Log::error("PHP Unknown Error [$errno]: $errstr in $errfile on line $errline", Log::EVENT_CODE_PHP_ERROR); break; } @@ -192,7 +193,7 @@ function get_share_landing_zone($share) { } else if (array_contains(ConfigHelper::$trash_share_names, $share)) { return SharesConfig::get(CONFIG_TRASH_SHARE, CONFIG_LANDING_ZONE); } else { - Log::warn(" Found a share ($share) with no path in " . ConfigHelper::$smb_config_file . ", or missing it's num_copies[$share] config in " . ConfigHelper::$config_file . ". Skipping."); + Log::warn(" Found a share ($share) with no path in " . ConfigHelper::$smb_config_file . ", or missing it's num_copies[$share] config in " . ConfigHelper::$config_file . ". Skipping.", Log::EVENT_CODE_SHARE_MISSING_FROM_GREYHOLE_CONF); return FALSE; } } @@ -202,7 +203,7 @@ function memory_check() { $used = $usage / Config::get(CONFIG_MEMORY_LIMIT); $used = $used * 100; if ($used > 95) { - Log::critical("$used% memory usage, exiting. Please increase '" . CONFIG_MEMORY_LIMIT . "' in /etc/greyhole.conf"); + Log::critical("$used% memory usage, exiting. Please increase '" . CONFIG_MEMORY_LIMIT . "' in /etc/greyhole.conf", Log::EVENT_CODE_MEMORY_LIMIT_REACHED); } } @@ -313,15 +314,25 @@ public function emailAsRequired() { $last_mod_date = filemtime($logfile); if ($last_mod_date > $this->getLastEmailSentTime()) { - $email_to = Config::get(CONFIG_EMAIL_TO); - Log::warn("Sending $logfile by email to $email_to"); - mail($email_to, $this->getSubject(), $this->getBody()); + if ($this->shouldSendViaEmail()) { + $email_to = Config::get(CONFIG_EMAIL_TO); + Log::info("Sending $logfile by email to $email_to"); + mail($email_to, $this->getSubject(), $this->getBody()); + } + + LogHook::trigger(LogHook::EVENT_TYPE_FSCK, Log::EVENT_CODE_FSCK_REPORT, $this->getSubject() . "\n" . $this->getBody()); $this->lastEmailSentTime = $last_mod_date; Settings::set("last_email_$this->filename", $this->lastEmailSentTime); } } + private function shouldSendViaEmail() { + $this->getBody(); + global $fsck_report; + return (@$fsck_report['send_via_email'] === TRUE); + } + private function getBody() { $logfile = "$this->path/$this->filename"; if ($this->filename == 'fsck_checksums.log') { @@ -370,8 +381,9 @@ public static function loadFSCKReport($what) { initialize_fsck_report($what); } - public static function saveFSCKReport() { + public static function saveFSCKReport($send_via_email) { global $fsck_report; + $fsck_report['send_via_email'] = $send_via_email; $logfile = self::PATH . '/fsck_files.log'; file_put_contents($logfile, serialize($fsck_report)); } diff --git a/scripts-examples/greyhole_file_changed.sh b/scripts-examples/greyhole_file_changed.sh new file mode 100755 index 00000000..f93d32d6 --- /dev/null +++ b/scripts-examples/greyhole_file_changed.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash +# +# Copyright 2017 Guillaume Boudreau +# +# This file is part of Greyhole. +# +# Greyhole is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Greyhole is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Greyhole. If not, see . +# + +EVENT_TYPE="$1" +SHARE="$2" +PATH_ON_SHARE="$3" + +case "${EVENT_TYPE}" in + create|edit) + FILE_SIZE=`ls -l "/mnt/samba/${SHARE}/${PATH_ON_SHARE}" | awk '{print $5}'` + echo "The file on share ${SHARE} at ${PATH_ON_SHARE} was ${EVENT_TYPE}-ed. File size: ${FILE_SIZE}" >> /tmp/gh_files_changed.txt + ;; + rename) + OLD_PATH_ON_SHARE="$4" + FILE_SIZE=`ls -l "/mnt/samba/${SHARE}/${PATH_ON_SHARE}" | awk '{print $5}'` + echo "The file on share ${SHARE} at ${OLD_PATH_ON_SHARE} was renamed to ${PATH_ON_SHARE}. File size: ${FILE_SIZE}" >> /tmp/gh_files_changed.txt + ;; + delete) + echo "The file on share ${SHARE} at ${PATH_ON_SHARE} was ${EVENT_TYPE}d." >> /tmp/gh_files_changed.txt + ;; + mkdir) + echo "The folder on share ${SHARE} at ${PATH_ON_SHARE} was created." >> /tmp/gh_files_changed.txt + ;; + rmdir) + echo "The folder on share ${SHARE} at ${PATH_ON_SHARE} was deleted." >> /tmp/gh_files_changed.txt + ;; + *) + echo "Warning: Unknown event received: ${EVENT_TYPE}" >> /tmp/gh_files_changed.txt + exit 1 + ;; +esac diff --git a/scripts-examples/greyhole_idle.sh b/scripts-examples/greyhole_idle.sh new file mode 100755 index 00000000..947af7e5 --- /dev/null +++ b/scripts-examples/greyhole_idle.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +# +# Copyright 2017 Guillaume Boudreau +# +# This file is part of Greyhole. +# +# Greyhole is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Greyhole is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Greyhole. If not, see . +# + +EVENT_TYPE="$1" +EVENT_CODE="$2" +LOG="$3" + +echo "Received hook with event_type = ${EVENT_TYPE}, event_code = ${EVENT_CODE}" >> /tmp/gh_log.txt + +if [ "${EVENT_TYPE}" = "idle" ]; then + echo " Greyhole is idle: ${LOG}" >> /tmp/gh_log.txt +elif [ "${EVENT_TYPE}" = "not_idle" ]; then + echo " Greyhole is busy: ${LOG}" >> /tmp/gh_log.txt +else + echo "Warning: Unknown event received: ${EVENT_TYPE}" >> /tmp/gh_log.txt + exit 1 +fi diff --git a/scripts-examples/greyhole_notify_error.sh b/scripts-examples/greyhole_notify_error.sh new file mode 100755 index 00000000..6abe1357 --- /dev/null +++ b/scripts-examples/greyhole_notify_error.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +# +# Copyright 2017 Guillaume Boudreau +# +# This file is part of Greyhole. +# +# Greyhole is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Greyhole is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Greyhole. If not, see . +# + +EVENT_TYPE="$1" +EVENT_CODE="$2" +LOG="$3" + +echo "Received hook with event_type = ${EVENT_TYPE}, event_code = ${EVENT_CODE}" >> /tmp/gh_log.txt + +if [ "${EVENT_TYPE}" = "warning" -o "${EVENT_TYPE}" = "error" -o "${EVENT_TYPE}" = "critical" ]; then + # Expected log format: Jan 20 07:52:56 WARN initialize: ... + IFS=' ' + arrLOG=(${LOG}) + EVENT_DATE="${arrLOG[0]} ${arrLOG[1]} ${arrLOG[2]}" + LOG_LEVEL=${arrLOG[3]} + ACTION=${arrLOG[4]%?} # See ACTION_* defines in the includes/Log.php file: https://github.com/gboudreau/Greyhole/blob/master/includes/Log.php#L21 + MESSAGE=(${arrLOG[@]:5}) + MESSAGE="${MESSAGE[@]}" + echo " Tokenize'd log: EVENT_DATE=${EVENT_DATE} ; LOG_LEVEL=${LOG_LEVEL} ; ACTION=${ACTION} ; MESSAGE=${MESSAGE}" >> /tmp/gh_log.txt +else + echo "Warning: Unknown event received: ${EVENT_TYPE}" >> /tmp/gh_log.txt + exit 1 +fi diff --git a/scripts-examples/greyhole_send_fsck_report.sh b/scripts-examples/greyhole_send_fsck_report.sh new file mode 100755 index 00000000..ed14337a --- /dev/null +++ b/scripts-examples/greyhole_send_fsck_report.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash +# +# Copyright 2017 Guillaume Boudreau +# +# This file is part of Greyhole. +# +# Greyhole is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Greyhole is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Greyhole. If not, see . +# + +EVENT_TYPE="$1" +EVENT_CODE="$2" +LOG="$3" + +echo "Received hook with event_type = ${EVENT_TYPE}, event_code = ${EVENT_CODE}" >> /tmp/gh_log.txt + +if [ "${EVENT_TYPE}" = "fsck" ]; then + echo " fsck completed. Report:" >> /tmp/gh_log.txt + echo "${LOG}" >> /tmp/gh_log.txt +else + echo "Warning: Unknown event received: ${EVENT_TYPE}" >> /tmp/gh_log.txt + exit 1 +fi