blob: aac6ffba4e3c474f201f430f081fce4aca084017 [file] [log] [blame]
Matteo Scandolod2044a42017-08-07 16:08:28 -07001# Copyright 2017-present Open Networking Foundation
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15
Matteo Scandolo56879722017-05-17 21:39:54 -070016import os
17import sys
18import yaml
Matteo Scandolo56879722017-05-17 21:39:54 -070019import default
20from pykwalify.core import Core as PyKwalify
Matteo Scandoloc59372a2018-06-11 15:17:40 -070021import pykwalify
22
23pykwalify.init_logging(1)
Matteo Scandolo56879722017-05-17 21:39:54 -070024
Matteo Scandolo6bc017c2017-05-25 18:37:42 -070025DEFAULT_CONFIG_FILE = "/opt/xos/xos_config.yaml"
Zack Williams045b63d2019-01-22 16:30:57 -070026DEFAULT_CONFIG_SCHEMA = "xos-config-schema.yaml"
Matteo Scandolo56879722017-05-17 21:39:54 -070027INITIALIZED = False
Matteo Scandoloe0fc6852017-06-07 16:01:54 -070028CONFIG_FILE = None
Matteo Scandolo56879722017-05-17 21:39:54 -070029CONFIG = {}
30
Matteo Scandoloc59372a2018-06-11 15:17:40 -070031OVERRIDE_CONFIG = {}
Zack Williams37845ca2017-07-18 15:39:20 -070032
Zack Williams045b63d2019-01-22 16:30:57 -070033
Matteo Scandolo56879722017-05-17 21:39:54 -070034class Config:
35 """
36 XOS Configuration APIs
37 """
38
39 @staticmethod
Zack Williams045b63d2019-01-22 16:30:57 -070040 def init(
41 config_file=DEFAULT_CONFIG_FILE,
42 config_schema=DEFAULT_CONFIG_SCHEMA,
43 override_config_file=None,
44 ):
Matteo Scandolo1879ce72017-05-30 15:45:26 -070045
46 # make schema relative to this directory
47 # TODO give the possibility to specify an absolute path
48 config_schema = Config.get_abs_path(config_schema)
49
Matteo Scandolo56879722017-05-17 21:39:54 -070050 global INITIALIZED
51 global CONFIG
Matteo Scandoloe0fc6852017-06-07 16:01:54 -070052 global CONFIG_FILE
Zack Williams37845ca2017-07-18 15:39:20 -070053
Matteo Scandoloc59372a2018-06-11 15:17:40 -070054 global OVERRIDE_CONFIG
55 global OVERRIDE_CONFIG_FILE
56 global OVERRIDE_CONFIG_SCHEMA
Zack Williams37845ca2017-07-18 15:39:20 -070057
58 # Use same schema for both provided and global config by default
Matteo Scandoloc59372a2018-06-11 15:17:40 -070059 OVERRIDE_CONFIG_SCHEMA = config_schema
60 OVERRIDE_CONFIG_FILE = override_config_file
Zack Williams37845ca2017-07-18 15:39:20 -070061
Matteo Scandolo56879722017-05-17 21:39:54 -070062 # the config module can be initialized only one
63 if INITIALIZED:
Zack Williams045b63d2019-01-22 16:30:57 -070064 raise Exception("[XOS-Config] Module already initialized")
Matteo Scandolo56879722017-05-17 21:39:54 -070065 INITIALIZED = True
66
Matteo Scandolo1879ce72017-05-30 15:45:26 -070067 # if XOS_CONFIG_FILE is defined override the config_file
68 # FIXME shouldn't this stay in whatever module call this one? and then just pass the file to the init method
Zack Williams045b63d2019-01-22 16:30:57 -070069 if os.environ.get("XOS_CONFIG_FILE"):
70 config_file = os.environ["XOS_CONFIG_FILE"]
Matteo Scandolo1879ce72017-05-30 15:45:26 -070071
72 # if XOS_CONFIG_SCHEMA is defined override the config_schema
73 # FIXME shouldn't this stay in whatever module call this one? and then just pass the file to the init method
Zack Williams045b63d2019-01-22 16:30:57 -070074 if os.environ.get("XOS_CONFIG_SCHEMA"):
75 config_schema = Config.get_abs_path(os.environ["XOS_CONFIG_SCHEMA"])
Matteo Scandolo56879722017-05-17 21:39:54 -070076
Matteo Scandoloc59372a2018-06-11 15:17:40 -070077 # allow OVERRIDE_CONFIG_* to be overridden by env vars
Zack Williams045b63d2019-01-22 16:30:57 -070078 if os.environ.get("XOS_OVERRIDE_CONFIG_FILE"):
79 OVERRIDE_CONFIG_FILE = os.environ["XOS_OVERRIDE_CONFIG_FILE"]
80 if os.environ.get("XOS_OVERRIDE_CONFIG_SCHEMA"):
81 OVERRIDE_CONFIG_SCHEMA = Config.get_abs_path(
82 os.environ["XOS_OVERRIDE_CONFIG_SCHEMA"]
83 )
Zack Williams37845ca2017-07-18 15:39:20 -070084
Matteo Scandolo56879722017-05-17 21:39:54 -070085 # if a -C parameter is set in the cli override the config_file
86 # FIXME shouldn't this stay in whatever module call this one? and then just pass the file to the init method
87 if Config.get_cli_param(sys.argv):
Matteo Scandolo1879ce72017-05-30 15:45:26 -070088 config_schema = Config.get_cli_param(sys.argv)
Matteo Scandolo56879722017-05-17 21:39:54 -070089
Matteo Scandoloe0fc6852017-06-07 16:01:54 -070090 CONFIG_FILE = config_file
Matteo Scandolo1879ce72017-05-30 15:45:26 -070091 CONFIG = Config.read_config(config_file, config_schema)
Matteo Scandolo56879722017-05-17 21:39:54 -070092
Matteo Scandoloc59372a2018-06-11 15:17:40 -070093 # if an override is set
94 if OVERRIDE_CONFIG_FILE is not None:
Zack Williams045b63d2019-01-22 16:30:57 -070095 OVERRIDE_CONFIG = Config.read_config(
96 OVERRIDE_CONFIG_FILE, OVERRIDE_CONFIG_SCHEMA, True
97 )
Zack Williams37845ca2017-07-18 15:39:20 -070098
Matteo Scandolo56879722017-05-17 21:39:54 -070099 @staticmethod
Matteo Scandoloe0fc6852017-06-07 16:01:54 -0700100 def get_config_file():
101 return CONFIG_FILE
102
103 @staticmethod
Matteo Scandolo56879722017-05-17 21:39:54 -0700104 def clear():
105 global INITIALIZED
106 INITIALIZED = False
107
108 @staticmethod
Matteo Scandolo1879ce72017-05-30 15:45:26 -0700109 def get_abs_path(path):
110 if os.path.isabs(path):
111 return path
Zack Williams045b63d2019-01-22 16:30:57 -0700112 return os.path.dirname(os.path.realpath(__file__)) + "/" + path
Matteo Scandolo1879ce72017-05-30 15:45:26 -0700113
114 @staticmethod
115 def validate_config_format(config_file, config_schema):
116 schema = os.path.abspath(config_schema)
Matteo Scandolo56879722017-05-17 21:39:54 -0700117 c = PyKwalify(source_file=config_file, schema_files=[schema])
118 c.validate(raise_exception=True)
119
120 @staticmethod
121 def get_cli_param(args):
122 last = None
123 for arg in args:
Zack Williams045b63d2019-01-22 16:30:57 -0700124 if last == "-C":
Matteo Scandolo56879722017-05-17 21:39:54 -0700125 return arg
126 last = arg
127
128 @staticmethod
Zack Williams37845ca2017-07-18 15:39:20 -0700129 def read_config(config_file, config_schema, ignore_if_not_found=False):
Matteo Scandolo56879722017-05-17 21:39:54 -0700130 """
131 Read the configuration file and return a dictionary
132 :param config_file: string
133 :return: dict
134 """
Zack Williams37845ca2017-07-18 15:39:20 -0700135
Zack Williams045b63d2019-01-22 16:30:57 -0700136 if not os.path.exists(config_file) and ignore_if_not_found:
Zack Williams37845ca2017-07-18 15:39:20 -0700137 return {}
138
Matteo Scandolo56879722017-05-17 21:39:54 -0700139 if not os.path.exists(config_file):
Zack Williams045b63d2019-01-22 16:30:57 -0700140 raise Exception("[XOS-Config] Config file not found at: %s" % config_file)
Matteo Scandolo56879722017-05-17 21:39:54 -0700141
Matteo Scandolo1879ce72017-05-30 15:45:26 -0700142 if not os.path.exists(config_schema):
Zack Williams045b63d2019-01-22 16:30:57 -0700143 raise Exception(
144 "[XOS-Config] Config schema not found at: %s" % config_schema
145 )
Matteo Scandolo1879ce72017-05-30 15:45:26 -0700146
Matteo Scandolo56879722017-05-17 21:39:54 -0700147 try:
Matteo Scandolo1879ce72017-05-30 15:45:26 -0700148 Config.validate_config_format(config_file, config_schema)
Zack Williams045b63d2019-01-22 16:30:57 -0700149 except Exception as e:
Scott Baker1f6a32c2017-11-22 11:24:01 -0800150 try:
151 error_msg = e.msg
152 except AttributeError:
153 error_msg = str(e)
Zack Williams045b63d2019-01-22 16:30:57 -0700154 raise Exception("[XOS-Config] The config format is wrong: %s" % error_msg)
Matteo Scandolo56879722017-05-17 21:39:54 -0700155
Zack Williams045b63d2019-01-22 16:30:57 -0700156 with open(config_file, "r") as stream:
Matteo Scandolo56879722017-05-17 21:39:54 -0700157 return yaml.safe_load(stream)
158
159 @staticmethod
160 def get(query):
161 """
162 Read a parameter from the config
163 :param query: a dot separated selector for configuration options (eg: database.username)
164 :return: the requested parameter in any format the parameter is specified
165 """
166 global INITIALIZED
167 global CONFIG
Matteo Scandoloc59372a2018-06-11 15:17:40 -0700168 global OVERRIDE_CONFIG
169 global OVERRIDE_CONFIG_FILE
Matteo Scandolo56879722017-05-17 21:39:54 -0700170
171 if not INITIALIZED:
Zack Williams045b63d2019-01-22 16:30:57 -0700172 raise Exception("[XOS-Config] Module has not been initialized")
Matteo Scandolo56879722017-05-17 21:39:54 -0700173
174 val = Config.get_param(query, CONFIG)
Matteo Scandoloc59372a2018-06-11 15:17:40 -0700175 if OVERRIDE_CONFIG_FILE or not val:
176 # if we specified an override configuration, we should override the value
177 # we also look for the value in case it's missing
178 over_val = Config.get_param(query, OVERRIDE_CONFIG)
179 if over_val is not None:
180 val = over_val
Zack Williams37845ca2017-07-18 15:39:20 -0700181 if not val:
Matteo Scandolo56879722017-05-17 21:39:54 -0700182 val = Config.get_param(query, default.DEFAULT_VALUES)
183 if not val:
Matteo Scandolo1879ce72017-05-30 15:45:26 -0700184 # TODO if no val return none
185 # raise Exception('[XOS-Config] Config does not have a value (or a default) parameter %s' % query)
186 return None
Matteo Scandolo56879722017-05-17 21:39:54 -0700187 return val
188
189 @staticmethod
190 def get_param(query, config):
191 """
192 Search for a parameter in config's first level, other call get_nested_param
193 :param query: a dot separated selector for configuration options (eg: database.username)
194 :param config: the config source to read from (can be the config file or the defaults)
195 :return: the requested parameter in any format the parameter is specified
196 """
Zack Williams045b63d2019-01-22 16:30:57 -0700197 keys = query.split(".")
Matteo Scandolo56879722017-05-17 21:39:54 -0700198 if len(keys) == 1:
199 key = keys[0]
Zack Williams045b63d2019-01-22 16:30:57 -0700200 if key not in config:
Matteo Scandolo56879722017-05-17 21:39:54 -0700201 return None
202 return config[key]
203 else:
204 return Config.get_nested_param(keys, config)
205
206 @staticmethod
207 def get_nested_param(keys, config):
208 """
Zack Williams37845ca2017-07-18 15:39:20 -0700209
Matteo Scandolo56879722017-05-17 21:39:54 -0700210 :param keys: a list of descending selector
211 :param config: the config source to read from (can be the config file or the defaults)
212 :return: the requested parameter in any format the parameter is specified
213 """
214 param = config
215 for k in keys:
Zack Williams045b63d2019-01-22 16:30:57 -0700216 if k not in param:
Matteo Scandolo56879722017-05-17 21:39:54 -0700217 return None
218 param = param[k]
219 return param
220
Zack Williams045b63d2019-01-22 16:30:57 -0700221
222if __name__ == "__main__":
Zack Williams37845ca2017-07-18 15:39:20 -0700223 Config.init()