001/**
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *      http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018
019package org.apache.oozie.service;
020
021import org.apache.hadoop.conf.Configuration;
022import org.apache.oozie.util.ConfigUtils;
023import org.apache.oozie.util.Instrumentable;
024import org.apache.oozie.util.Instrumentation;
025import org.apache.oozie.util.XLog;
026import org.apache.oozie.util.XConfiguration;
027import org.apache.oozie.ErrorCode;
028
029import java.io.File;
030import java.io.FileInputStream;
031import java.io.IOException;
032import java.io.InputStream;
033import java.io.StringWriter;
034import java.lang.reflect.InvocationTargetException;
035import java.lang.reflect.Method;
036import java.util.HashMap;
037import java.util.HashSet;
038import java.util.Map;
039import java.util.Set;
040import java.util.Arrays;
041
042import org.apache.oozie.util.ZKUtils;
043
044import com.google.common.annotations.VisibleForTesting;
045
046/**
047 * Built in service that initializes the services configuration.
048 * <p/>
049 * The configuration loading sequence is identical to Hadoop configuration loading sequence.
050 * <p/>
051 * Default values are loaded from the 'oozie-default.xml' file from the classpath, then site configured values
052 * are loaded from a site configuration file from the Oozie configuration directory.
053 * <p/>
054 * The Oozie configuration directory is resolved using the <code>OOZIE_HOME<code> environment variable as
055 * <code>${OOZIE_HOME}/conf</code>. If the <code>OOZIE_HOME<code> environment variable is not defined the
056 * initialization of the <code>ConfigurationService</code> fails.
057 * <p/>
058 * The site configuration is loaded from the <code>oozie-site.xml</code> file in the configuration directory.
059 * <p/>
060 * The site configuration file name to use can be changed by setting the <code>OOZIE_CONFIG_FILE</code> environment
061 * variable to an alternate file name. The alternate file must ber in the Oozie configuration directory.
062 * <p/>
063 * Configuration properties, prefixed with 'oozie.', passed as system properties overrides default and site values.
064 * <p/>
065 * The configuration service logs details on how the configuration was loaded as well as what properties were overrode
066 * via system properties settings.
067 */
068public class ConfigurationService implements Service, Instrumentable {
069    private static final String INSTRUMENTATION_GROUP = "configuration";
070
071    public static final String CONF_PREFIX = Service.CONF_PREFIX + "ConfigurationService.";
072
073    public static final String CONF_IGNORE_SYS_PROPS = CONF_PREFIX + "ignore.system.properties";
074
075    public static final String CONF_VERIFY_AVAILABLE_PROPS = CONF_PREFIX + "verify.available.properties";
076
077    /**
078     * System property that indicates the configuration directory.
079     */
080    public static final String OOZIE_CONFIG_DIR = "oozie.config.dir";
081
082
083    /**
084     * System property that indicates the data directory.
085     */
086    public static final String OOZIE_DATA_DIR = "oozie.data.dir";
087
088    /**
089     * System property that indicates the name of the site configuration file to load.
090     */
091    public static final String OOZIE_CONFIG_FILE = "oozie.config.file";
092
093    private static final Set<String> IGNORE_SYS_PROPS = new HashSet<String>();
094    private static final Set<String> CONF_SYS_PROPS = new HashSet<String>();
095
096    private static final String IGNORE_TEST_SYS_PROPS = "oozie.test.";
097    private static final Set<String> MASK_PROPS = new HashSet<String>();
098    private static Map<String,String> defaultConfigs = new HashMap<String,String>();
099
100    private static Method getPasswordMethod;
101
102    static {
103
104        //all this properties are seeded as system properties, no need to log changes
105        IGNORE_SYS_PROPS.add(CONF_IGNORE_SYS_PROPS);
106        IGNORE_SYS_PROPS.add(Services.OOZIE_HOME_DIR);
107        IGNORE_SYS_PROPS.add(OOZIE_CONFIG_DIR);
108        IGNORE_SYS_PROPS.add(OOZIE_CONFIG_FILE);
109        IGNORE_SYS_PROPS.add(OOZIE_DATA_DIR);
110        IGNORE_SYS_PROPS.add(XLogService.OOZIE_LOG_DIR);
111        IGNORE_SYS_PROPS.add(XLogService.LOG4J_FILE);
112        IGNORE_SYS_PROPS.add(XLogService.LOG4J_RELOAD);
113
114        CONF_SYS_PROPS.add("oozie.http.hostname");
115        CONF_SYS_PROPS.add("oozie.http.port");
116        CONF_SYS_PROPS.add(ZKUtils.OOZIE_INSTANCE_ID);
117
118        // These properties should be masked when displayed because they contain sensitive info (e.g. password)
119        MASK_PROPS.add(JPAService.CONF_PASSWORD);
120        MASK_PROPS.add("oozie.authentication.signature.secret");
121
122        try {
123            // Only supported in Hadoop 2.6.0+
124            getPasswordMethod = Configuration.class.getMethod("getPassword", String.class);
125        } catch (NoSuchMethodException e) {
126            // Not supported
127            getPasswordMethod = null;
128        }
129    }
130
131    public static final String DEFAULT_CONFIG_FILE = "oozie-default.xml";
132    public static final String SITE_CONFIG_FILE = "oozie-site.xml";
133
134    private static XLog log = XLog.getLog(ConfigurationService.class);
135
136    private String configDir;
137    private String configFile;
138
139    private LogChangesConfiguration configuration;
140
141    public ConfigurationService() {
142        log = XLog.getLog(ConfigurationService.class);
143    }
144
145    /**
146     * Initialize the log service.
147     *
148     * @param services services instance.
149     * @throws ServiceException thrown if the log service could not be initialized.
150     */
151    @Override
152    public void init(Services services) throws ServiceException {
153        configDir = getConfigurationDirectory();
154        configFile = System.getProperty(OOZIE_CONFIG_FILE, SITE_CONFIG_FILE);
155        if (configFile.contains("/")) {
156            throw new ServiceException(ErrorCode.E0022, configFile);
157        }
158        log.info("Oozie home dir  [{0}]", Services.getOozieHome());
159        log.info("Oozie conf dir  [{0}]", configDir);
160        log.info("Oozie conf file [{0}]", configFile);
161        configFile = new File(configDir, configFile).toString();
162        configuration = loadConf();
163        if (configuration.getBoolean(CONF_VERIFY_AVAILABLE_PROPS, false)) {
164            verifyConfigurationName();
165        }
166    }
167
168    public static String getConfigurationDirectory() throws ServiceException {
169        String oozieHome = Services.getOozieHome();
170        String configDir = System.getProperty(OOZIE_CONFIG_DIR, null);
171        File file = configDir == null
172                ? new File(oozieHome, "conf")
173                : new File(configDir);
174        if (!file.exists()) {
175            throw new ServiceException(ErrorCode.E0024, configDir);
176        }
177        return file.getPath();
178    }
179
180    /**
181     * Destroy the configuration service.
182     */
183    @Override
184    public void destroy() {
185        configuration = null;
186    }
187
188    /**
189     * Return the public interface for configuration service.
190     *
191     * @return {@link ConfigurationService}.
192     */
193    @Override
194    public Class<? extends Service> getInterface() {
195        return ConfigurationService.class;
196    }
197
198    /**
199     * Return the services configuration.
200     *
201     * @return the services configuration.
202     */
203    public Configuration getConf() {
204        if (configuration == null) {
205            throw new IllegalStateException("Not initialized");
206        }
207        return configuration;
208    }
209
210    /**
211     * Return Oozie configuration directory.
212     *
213     * @return Oozie configuration directory.
214     */
215    public String getConfigDir() {
216        return configDir;
217    }
218
219    private InputStream getDefaultConfiguration() throws ServiceException, IOException {
220        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
221        InputStream inputStream = classLoader.getResourceAsStream(DEFAULT_CONFIG_FILE);
222        if (inputStream == null) {
223            throw new ServiceException(ErrorCode.E0023, DEFAULT_CONFIG_FILE);
224        }
225        return inputStream;
226    }
227
228    private LogChangesConfiguration loadConf() throws ServiceException {
229        XConfiguration configuration;
230        try {
231            InputStream inputStream = getDefaultConfiguration();
232            configuration = loadConfig(inputStream, true);
233            File file = new File(configFile);
234            if (!file.exists()) {
235                log.info("Missing site configuration file [{0}]", configFile);
236            }
237            else {
238                inputStream = new FileInputStream(configFile);
239                XConfiguration siteConfiguration = loadConfig(inputStream, false);
240                XConfiguration.injectDefaults(configuration, siteConfiguration);
241                configuration = siteConfiguration;
242            }
243        }
244        catch (IOException ex) {
245            throw new ServiceException(ErrorCode.E0024, configFile, ex.getMessage(), ex);
246        }
247
248        if (log.isTraceEnabled()) {
249            try {
250                StringWriter writer = new StringWriter();
251                for (Map.Entry<String, String> entry : configuration) {
252                    String value = getValue(configuration, entry.getKey());
253                    writer.write(" " + entry.getKey() + " = " + value + "\n");
254                }
255                writer.close();
256                log.trace("Configuration:\n{0}---", writer.toString());
257            }
258            catch (IOException ex) {
259                throw new ServiceException(ErrorCode.E0025, ex.getMessage(), ex);
260            }
261        }
262
263        String[] ignoreSysProps = configuration.getStrings(CONF_IGNORE_SYS_PROPS);
264        if (ignoreSysProps != null) {
265            IGNORE_SYS_PROPS.addAll(Arrays.asList(ignoreSysProps));
266        }
267
268        for (Map.Entry<String, String> entry : configuration) {
269            String sysValue = System.getProperty(entry.getKey());
270            if (sysValue != null && !IGNORE_SYS_PROPS.contains(entry.getKey())) {
271                log.info("Configuration change via System Property, [{0}]=[{1}]", entry.getKey(), sysValue);
272                configuration.set(entry.getKey(), sysValue);
273            }
274        }
275        for (Map.Entry<Object, Object> entry : System.getProperties().entrySet()) {
276            String name = (String) entry.getKey();
277            if (!IGNORE_SYS_PROPS.contains(name)) {
278                if (name.startsWith("oozie.") && !name.startsWith(IGNORE_TEST_SYS_PROPS)) {
279                    if (configuration.get(name) == null) {
280                        log.warn("System property [{0}] no defined in Oozie configuration, ignored", name);
281                    }
282                }
283            }
284        }
285
286        //Backward compatible, we should still support -Dparam.
287        for (String key : CONF_SYS_PROPS) {
288            String sysValue = System.getProperty(key);
289            if (sysValue != null && !IGNORE_SYS_PROPS.contains(key)) {
290                log.info("Overriding configuration with system property. Key [{0}], Value [{1}] ", key, sysValue);
291                configuration.set(key, sysValue);
292            }
293        }
294
295        return new LogChangesConfiguration(configuration);
296    }
297
298    private XConfiguration loadConfig(InputStream inputStream, boolean defaultConfig) throws IOException, ServiceException {
299        XConfiguration configuration;
300        configuration = new XConfiguration(inputStream);
301        for(Map.Entry<String,String> entry: configuration) {
302            if (defaultConfig) {
303                defaultConfigs.put(entry.getKey(), entry.getValue());
304            }
305            else {
306                log.debug("Overriding configuration with oozie-site, [{0}]", entry.getKey());
307            }
308        }
309        return configuration;
310    }
311
312    private class LogChangesConfiguration extends XConfiguration {
313
314        public LogChangesConfiguration(Configuration conf) {
315            for (Map.Entry<String, String> entry : conf) {
316                if (get(entry.getKey()) == null) {
317                    setValue(entry.getKey(), entry.getValue());
318                }
319            }
320        }
321
322        @Override
323        public String[] getStrings(String name) {
324            String s = get(name);
325            return (s != null && s.trim().length() > 0) ? super.getStrings(name) : new String[0];
326        }
327
328        @Override
329        public String[] getStrings(String name, String[] defaultValue) {
330            String s = get(name);
331            if (s == null) {
332                log.debug(XLog.OPS, "Configuration property [{0}] not found, use given value [{1}]", name,
333                        Arrays.asList(defaultValue).toString());
334            }
335            return (s != null && s.trim().length() > 0) ? super.getStrings(name) : defaultValue;
336        }
337
338        @Override
339        public String get(String name, String defaultValue) {
340            String value = get(name);
341            if (value == null) {
342                boolean maskValue = MASK_PROPS.contains(name);
343                value = defaultValue;
344                String logValue = (maskValue) ? "**MASKED**" : defaultValue;
345                log.debug(XLog.OPS, "Configuration property [{0}] not found, use given value [{1}]", name, logValue);
346            }
347            return value;
348        }
349
350        @Override
351        public void set(String name, String value) {
352            setValue(name, value);
353            boolean maskValue = MASK_PROPS.contains(name);
354            value = (maskValue) ? "**MASKED**" : value;
355            log.info(XLog.OPS, "Programmatic configuration change, property[{0}]=[{1}]", name, value);
356        }
357
358        @Override
359        public boolean getBoolean(String name, boolean defaultValue) {
360            String value = get(name);
361            if (value == null) {
362                log.debug(XLog.OPS, "Configuration property [{0}] not found, use given value [{1}]", name, defaultValue);
363            }
364            return super.getBoolean(name, defaultValue);
365        }
366
367        @Override
368        public int getInt(String name, int defaultValue) {
369            String value = get(name);
370            if (value == null) {
371                log.debug(XLog.OPS, "Configuration property [{0}] not found, use given value [{1}]", name, defaultValue);
372            }
373            return super.getInt(name, defaultValue);
374        }
375
376        @Override
377        public long getLong(String name, long defaultValue) {
378            String value = get(name);
379            if (value == null) {
380                log.debug(XLog.OPS, "Configuration property [{0}] not found, use given value [{1}]", name, defaultValue);
381            }
382            return super.getLong(name, defaultValue);
383        }
384
385        @Override
386        public float getFloat(String name, float defaultValue) {
387            String value = get(name);
388            if (value == null) {
389                log.debug(XLog.OPS, "Configuration property [{0}] not found, use given value [{1}]", name, defaultValue);
390            }
391            return super.getFloat(name, defaultValue);
392        }
393
394        @Override
395        public Class<?>[] getClasses(String name, Class<?> ... defaultValue) {
396            String value = get(name);
397            if (value == null) {
398                log.debug(XLog.OPS, "Configuration property [{0}] not found, use given value [{1}]", name, defaultValue);
399            }
400            return super.getClasses(name, defaultValue);
401        }
402
403        @Override
404        public Class<?> getClass(String name, Class<?> defaultValue) {
405            String value = get(name);
406            if (value == null) {
407                log.debug(XLog.OPS, "Configuration property [{0}] not found, use given value [{1}]", name, defaultValue);
408                return defaultValue;
409            }
410            try {
411                return getClassByName(value);
412            } catch (ClassNotFoundException e) {
413                throw new RuntimeException(e);
414            }
415        }
416
417        private void setValue(String name, String value) {
418            super.set(name, value);
419        }
420
421    }
422
423    /**
424     * Instruments the configuration service. <p/> It sets instrumentation variables indicating the config dir and
425     * config file used.
426     *
427     * @param instr instrumentation to use.
428     */
429    @Override
430    public void instrument(Instrumentation instr) {
431        instr.addVariable(INSTRUMENTATION_GROUP, "config.dir", new Instrumentation.Variable<String>() {
432            @Override
433            public String getValue() {
434                return configDir;
435            }
436        });
437        instr.addVariable(INSTRUMENTATION_GROUP, "config.file", new Instrumentation.Variable<String>() {
438            @Override
439            public String getValue() {
440                return configFile;
441            }
442        });
443    }
444
445    /**
446     * Return a configuration with all sensitive values masked.
447     *
448     * @return masked configuration.
449     */
450    public Configuration getMaskedConfiguration() {
451        XConfiguration maskedConf = new XConfiguration();
452        Configuration conf = getConf();
453        for (Map.Entry<String, String> entry : conf) {
454            String name = entry.getKey();
455            String value = getValue(conf, name);
456            maskedConf.set(name, value);
457        }
458        return maskedConf;
459    }
460
461    private String getValue(Configuration config, String key) {
462        String value;
463        if (MASK_PROPS.contains(key)) {
464            value = "**MASKED**";
465        }
466        else {
467            value = config.get(key);
468        }
469        return value;
470    }
471
472
473    /**
474     * Gets the oozie configuration value in oozie-default.
475     * @param name
476     * @return the configuration value of the <code>name</code> otherwise null
477     */
478    private String getDefaultOozieConfig(String name) {
479        return defaultConfigs.get(name);
480    }
481
482    /**
483     * Verify the configuration is in oozie-default
484     */
485    public void verifyConfigurationName() {
486        for (Map.Entry<String, String> entry: configuration) {
487            if (getDefaultOozieConfig(entry.getKey()) == null) {
488                log.warn("Invalid configuration defined, [{0}] ", entry.getKey());
489            }
490        }
491    }
492
493    @VisibleForTesting
494    public static void set(String name, String value) {
495        Configuration conf = Services.get().getConf();
496        conf.set(name, value);
497    }
498
499    @VisibleForTesting
500    public static void setBoolean(String name, boolean value) {
501        Configuration conf = Services.get().getConf();
502        conf.setBoolean(name, value);
503    }
504
505    public static String get(String name) {
506        Configuration conf = Services.get().getConf();
507        return get(conf, name);
508    }
509
510    public static String get(Configuration conf, String name) {
511        return conf.get(name, ConfigUtils.STRING_DEFAULT);
512    }
513
514    public static String[] getStrings(String name) {
515        Configuration conf = Services.get().getConf();
516        return getStrings(conf, name);
517    }
518
519    public static String[] getStrings(Configuration conf, String name) {
520        return conf.getStrings(name, new String[0]);
521    }
522
523    public static boolean getBoolean(String name) {
524        Configuration conf = Services.get().getConf();
525        return getBoolean(conf, name);
526    }
527
528    public static boolean getBoolean(Configuration conf, String name) {
529        return conf.getBoolean(name, ConfigUtils.BOOLEAN_DEFAULT);
530    }
531
532    public static int getInt(String name) {
533        Configuration conf = Services.get().getConf();
534        return getInt(conf, name);
535    }
536
537    public static int getInt(Configuration conf, String name) {
538        return conf.getInt(name, ConfigUtils.INT_DEFAULT);
539    }
540
541    public static float getFloat(String name) {
542        Configuration conf = Services.get().getConf();
543        return conf.getFloat(name, ConfigUtils.FLOAT_DEFAULT);
544    }
545
546    public static long getLong(String name) {
547        Configuration conf = Services.get().getConf();
548        return getLong(conf, name);
549    }
550
551    public static long getLong(Configuration conf, String name) {
552        return conf.getLong(name, ConfigUtils.LONG_DEFAULT);
553    }
554
555    public static Class<?>[] getClasses(String name) {
556        Configuration conf = Services.get().getConf();
557        return getClasses(conf, name);
558    }
559
560    public static Class<?>[] getClasses(Configuration conf, String name) {
561        return conf.getClasses(name);
562    }
563
564    public static Class<?> getClass(Configuration conf, String name) {
565        return conf.getClass(name, Object.class);
566    }
567
568    public static String getPassword(Configuration conf, String name) {
569        return getPassword(conf, name, null);
570    }
571
572    public static String getPassword(Configuration conf, String name, String defaultValue) {
573        if (getPasswordMethod != null) {
574            try {
575                char[] pass = (char[]) getPasswordMethod.invoke(conf, name);
576                return pass == null ? defaultValue : new String(pass);
577            } catch (IllegalAccessException e) {
578                log.error(e);
579                throw new IllegalArgumentException("Could not load password for [" + name + "]", e);
580            } catch (InvocationTargetException e) {
581                log.error(e);
582                throw new IllegalArgumentException("Could not load password for [" + name + "]", e);
583            }
584        } else {
585            return conf.get(name);
586        }
587    }
588
589    public static String getPassword(String name, String defaultValue) {
590        Configuration conf = Services.get().getConf();
591        return getPassword(conf, name, defaultValue);
592    }
593}