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