/************************************************************************* * Copyright 2009-2014 Eucalyptus Systems, Inc. * * This program 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; version 3 of the License. * * This program 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 this program. If not, see http://www.gnu.org/licenses/. * * Please contact Eucalyptus Systems, Inc., 6755 Hollister Ave., Goleta * CA 93117, USA or visit http://www.eucalyptus.com/licenses/ if you need * additional information or have any questions. ************************************************************************/ package com.eucalyptus.stats.emitters; import com.eucalyptus.configurable.ConfigurableClass; import com.eucalyptus.configurable.ConfigurableField; import com.eucalyptus.stats.SystemMetric; import com.eucalyptus.system.SubDirectory; import com.eucalyptus.util.Exceptions; import com.google.common.base.Supplier; import com.google.common.base.Suppliers; import org.apache.log4j.Logger; import javax.annotation.Nonnull; import java.io.BufferedWriter; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.FileSystems; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardCopyOption; import java.nio.file.Files; import java.nio.file.attribute.GroupPrincipal; import java.nio.file.attribute.PosixFileAttributeView; import java.nio.file.attribute.PosixFilePermission; import java.nio.file.attribute.PosixFilePermissions; import java.util.HashSet; import java.util.Set; import java.util.concurrent.TimeUnit; /** * An event emitter backed by local-host filesystem. Events are written to a filesystem with * a specific layout/format. * <p/> * Events are written in json with service names forming a directory tree using '.' replaced with '/' delimiters * in the name. * <p/> * Each output file is a json document owned by the eucalyptus user and @link{com.eucalyptus.monitoring.config.MonitoringConfiguration.statusGroupName} group has read permissions. */ public class FileSystemEmitter implements EventEmitter { private static final Logger LOG = Logger.getLogger(FileSystemEmitter.class); private static SubDirectory defaultFsRoot = FileSystemEmitterConfiguration.dataOutputFSRoot; private Path fsRoot; private boolean isEnabled = false; private static HashSet<Path> pathCache = new HashSet<Path>(); public FileSystemEmitter() throws IOException { this(defaultFsRoot.toString()); } public FileSystemEmitter(String rootPath) throws IOException { try { this.fsRoot = Paths.get(rootPath); FileSystemEmitterConfiguration.getGroup(); isEnabled = true; } catch (Throwable e) { LOG.error("Eucalyptus stats event emitter cannot initialize because the user group " + FileSystemEmitterConfiguration.getStatusGroupName() + " is not found on the host. Please create the user group and restart the jvm process"); isEnabled = false; throw e; } } public void check() throws Exception { try { FileSystemEmitterConfiguration.getGroup(); isEnabled = true; } catch (Throwable e) { LOG.error("Eucalyptus stats event emitter cannot initialize because the user group " + FileSystemEmitterConfiguration.getStatusGroupName() + " is not found on the host. Please create the user group and restart the jvm process"); isEnabled = false; throw e; } } @Override public boolean emit(SystemMetric event) { if (!isEnabled || event == null) { return false; } try { //The resulting file name String path = fsRoot + "/" + event.getSensor().replace('.', '/'); Path filePath = FileSystems.getDefault().getPath(path); //Temp file to do the write, then a swap. Path tmpPath = FileSystems.getDefault().getPath(path + ".new"); if ( !pathCache.contains(tmpPath) ) { try { LOG.debug("Creating directories tree up to " + filePath.getParent()); Files.createDirectories(filePath.getParent()); } catch (Exception e) { //cannot create parent paths of entry name throw new RuntimeException("Could not create file path for sensor output. Path='" + path + "'"); } // Set group permissions to directories Path c = filePath.getParent(); while(!c.equals(fsRoot)) { try { LOG.debug("Checking dir " + c); Files.getFileAttributeView(c, PosixFileAttributeView.class).setGroup(FileSystemEmitterConfiguration.getGroup()); } catch (Exception ex) { LOG.error("Can't set group permission for " + c); } c = c.getParent(); } pathCache.add(tmpPath); } tmpPath = Files.createFile(tmpPath, PosixFilePermissions.asFileAttribute(FileSystemEmitterConfiguration.getDataFilePermissions())); BufferedWriter fileOut = Files.newBufferedWriter(tmpPath, StandardCharsets.UTF_8); fileOut.write(event.toString()); fileOut.close(); try { Files.getFileAttributeView(tmpPath, PosixFileAttributeView.class).setGroup(FileSystemEmitterConfiguration.getGroup()); } catch (Exception ex) { LOG.error("Can't set group permission for " + tmpPath + " Please make sure that " + System.getProperty( "euca.user" ) + " user is part of " + FileSystemEmitterConfiguration.getGroup() + " group."); } Files.move(tmpPath, filePath, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE); return true; } catch (Exception e) { LOG.error("Failed emitting event to file", e); } return false; } @Override public boolean doesBatching() { return false; } @ConfigurableClass(root="stats.file_system_emitter",description="Configuration for the stats emitter that sends data to local filesystem on host") static class FileSystemEmitterConfiguration { private static final String STATUS_USER_GROUP_PROPERTY_NAME = "euca.stats_group"; private static final String STATUS_DATA_FILE_PERMS_PROPERTY_NAME = "euca.stats_data_perms"; private static final String STATUS_USER_GROUP_PROPERTY_DEFAULT = "eucalyptus-status"; private static final String DEFAULT_GROUP_PERM_STRING = "rw-r-----"; @ConfigurableField(displayName = "stats group name", description="group name that owns stats data files", initial=STATUS_USER_GROUP_PROPERTY_DEFAULT) public static String stats_group_name = STATUS_USER_GROUP_PROPERTY_DEFAULT; @ConfigurableField(displayName = "stats_data_group_permissions", description = "group permissions to place on stats data files in string form. eg. rwxr-x--x", initial=DEFAULT_GROUP_PERM_STRING) public static String stats_data_permissions = DEFAULT_GROUP_PERM_STRING; public static SubDirectory dataOutputFSRoot = SubDirectory.STATUS; //Output directory public static String getStatusGroupName() { return System.getProperty(STATUS_USER_GROUP_PROPERTY_NAME, stats_group_name); } public static Set<PosixFilePermission> getDataFilePermissions() { return OUTPUT_FILE_PERMS_SUPPLIER.get(); } public static GroupPrincipal getGroup() { return GROUP_SUPPLIER.get(); } /** * Reloads new supplier instances to forcibly update to the latest value if needed. In-lieu of a cache-flush mechanism * for the memoized values */ public static void forceConfigRefresh() { OUTPUT_FILE_PERMS_SUPPLIER = getOutputFilePermSupplier(); GROUP_SUPPLIER = getGroupSupplier(); } private static final Integer memoizationExpirationSeconds = 10; //Duration of memoization, max frequency of updates to make property changes effective for these configs. Ok because we don't expect them to change private static Supplier<Set<PosixFilePermission>> OUTPUT_FILE_PERMS_SUPPLIER = getOutputFilePermSupplier(); private static Supplier<GroupPrincipal> GROUP_SUPPLIER = getGroupSupplier(); private static Supplier<Set<PosixFilePermission>> getOutputFilePermSupplier() { return Suppliers.memoizeWithExpiration(new Supplier<Set<PosixFilePermission>>() { public Set<PosixFilePermission> get() { //Always honor the local setting over global return PosixFilePermissions.fromString(System.getProperty(STATUS_DATA_FILE_PERMS_PROPERTY_NAME, stats_data_permissions)); } }, memoizationExpirationSeconds, TimeUnit.SECONDS); } private static Supplier<GroupPrincipal> getGroupSupplier() { return Suppliers.memoizeWithExpiration(new Supplier<GroupPrincipal>() { public GroupPrincipal get() { //Always honor the local setting over global try { return verifyUserGroup(getStatusGroupName(), Paths.get(dataOutputFSRoot.toString())); } catch (IOException e) { LOG.error("Cannot use group name " + getStatusGroupName() + " for stats output. Invalid name or group not found", e); throw Exceptions.toUndeclared("Invalid group name. Not found", e); } } }, memoizationExpirationSeconds, TimeUnit.SECONDS); } private static GroupPrincipal verifyUserGroup(@Nonnull String groupName, @Nonnull Path fileSystemRoot) throws IOException { try { return fileSystemRoot.getFileSystem().getUserPrincipalLookupService().lookupPrincipalByGroupName(groupName); } catch (IOException e) { LOG.error("Could not get group information for user group " + FileSystemEmitterConfiguration.getStatusGroupName() + " from filesystem " + fileSystemRoot.toString(), e); throw e; } } } }