Skip to content

Commit 6e302cb

Browse files
authored
Support new pycue configuration paths. (#972)
1 parent 02456f5 commit 6e302cb

4 files changed

Lines changed: 332 additions & 20 deletions

File tree

pycue/opencue/config.py

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
# Copyright Contributors to the OpenCue Project
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+
"""OpenCue configuration."""
16+
17+
import logging
18+
import os
19+
import platform
20+
21+
import yaml
22+
23+
24+
logger = logging.getLogger("opencue")
25+
26+
27+
# Config file from which default settings are loaded. This file is distributed with the
28+
# opencue Python library.
29+
__DEFAULT_CONFIG_FILE = os.path.join(os.path.dirname(__file__), 'default.yaml')
30+
31+
# Environment variables which can be used to define a custom config file. Any settings
32+
# defined in this file will be used instead of the defaults.
33+
__CONFIG_FILE_ENV_VARS = [
34+
# OPENCUE_CONFIG_FILE is the preferred setting to use.
35+
'OPENCUE_CONFIG_FILE',
36+
# OPENCUE_CONF is deprecated, but kept for now for backwards compatibility.
37+
'OPENCUE_CONF',
38+
]
39+
40+
41+
def config_base_directory():
42+
"""Returns the OpenCue config base directory.
43+
44+
This platform-dependent directory, stored within your user profile, is used by
45+
OpenCue components as the default location for various configuration files. Typically
46+
if you store your config files in this location, there is no need to set environment
47+
variables to indicate where your config files are located -- OpenCue should recognize
48+
them automatically.
49+
50+
NOTE: This work is ongoing. Over time more OpenCue components will start using this
51+
base directory. See https://github.com/AcademySoftwareFoundation/OpenCue/issues/785.
52+
53+
:rtype: str
54+
:return: config file base directory
55+
"""
56+
if platform.system() == 'Windows':
57+
return os.path.join(os.path.expandvars('%APPDATA%'), 'opencue')
58+
return os.path.join(os.path.expanduser('~'), '.config', 'opencue')
59+
60+
61+
def load_config_from_file():
62+
"""Loads configuration settings from config file on the local system.
63+
64+
Default settings are read from default.yaml which is distributed with the opencue library.
65+
User-provided config is then read from disk, in order of preference:
66+
- Path defined by the OPENCUE_CONFIG_FILE environment variable.
67+
- Path defined by the OPENCUE_CONF environment variable.
68+
- Path within the config base directory (i.e. ~/.config/opencue/opencue.yaml)
69+
70+
:rtype: dict
71+
:return: config settings
72+
"""
73+
with open(__DEFAULT_CONFIG_FILE) as file_object:
74+
config = yaml.load(file_object, Loader=yaml.SafeLoader)
75+
76+
user_config_file = None
77+
78+
for config_file_env_var in __CONFIG_FILE_ENV_VARS:
79+
logger.debug('Checking for opencue config file path in %s', config_file_env_var)
80+
config_file_from_env = os.environ.get(config_file_env_var)
81+
if config_file_from_env and os.path.exists(config_file_from_env):
82+
user_config_file = config_file_from_env
83+
break
84+
85+
if not user_config_file:
86+
config_from_user_profile = os.path.join(config_base_directory(), 'opencue.yaml')
87+
logger.debug('Checking for opencue config at %s', config_from_user_profile)
88+
if os.path.exists(config_from_user_profile):
89+
user_config_file = config_from_user_profile
90+
91+
if user_config_file:
92+
logger.info('Loading opencue config from %s', user_config_file)
93+
with open(user_config_file) as file_object:
94+
config.update(yaml.load(file_object, Loader=yaml.SafeLoader))
95+
96+
return config

pycue/opencue/cuebot.py

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,10 @@
2626
import logging
2727
import os
2828
import platform
29-
import yaml
3029

3130
import grpc
3231

32+
import opencue.config
3333
from opencue.compiled_proto import comment_pb2
3434
from opencue.compiled_proto import comment_pb2_grpc
3535
from opencue.compiled_proto import criterion_pb2
@@ -67,15 +67,6 @@
6767

6868
logger = logging.getLogger("opencue")
6969

70-
default_config = os.path.join(os.path.dirname(__file__), 'default.yaml')
71-
with open(default_config) as file_object:
72-
config = yaml.load(file_object, Loader=yaml.SafeLoader)
73-
74-
# check for facility specific configurations.
75-
fcnf = os.environ.get('OPENCUE_CONF', '')
76-
if os.path.exists(fcnf):
77-
with open(fcnf) as file_object:
78-
config.update(yaml.load(file_object, Loader=yaml.SafeLoader))
7970

8071
DEFAULT_MAX_MESSAGE_BYTES = 1024 ** 2 * 10
8172
DEFAULT_GRPC_PORT = 8443
@@ -96,7 +87,8 @@ class Cuebot(object):
9687
RpcChannel = None
9788
Hosts = []
9889
Stubs = {}
99-
Timeout = config.get('cuebot.timeout', 10000)
90+
Config = opencue.config.load_config_from_file()
91+
Timeout = Config.get('cuebot.timeout', 10000)
10092

10193
PROTO_MAP = {
10294
'action': filter_pb2,
@@ -150,13 +142,20 @@ class Cuebot(object):
150142
}
151143

152144
@staticmethod
153-
def init():
145+
def init(config=None):
154146
"""Main init method for setting up the Cuebot object.
155-
Sets the communication channel and hosts."""
147+
Sets the communication channel and hosts.
148+
149+
:type config: dict
150+
:param config: config dictionary, this will override the config read from disk
151+
"""
152+
if config:
153+
Cuebot.Config = config
154+
Cuebot.Timeout = config.get('cuebot.timeout', Cuebot.Timeout)
156155
if os.getenv("CUEBOT_HOSTS"):
157156
Cuebot.setHosts(os.getenv("CUEBOT_HOSTS").split(","))
158157
else:
159-
facility_default = config.get("cuebot.facility_default")
158+
facility_default = Cuebot.Config.get("cuebot.facility_default")
160159
Cuebot.setFacility(facility_default)
161160
if Cuebot.Hosts is None:
162161
raise CueException('Cuebot host not set. Please ensure CUEBOT_HOSTS is set ' +
@@ -169,7 +168,7 @@ def setChannel():
169168
# gRPC must specify a single host. Randomize host list to balance load across cuebots.
170169
hosts = list(Cuebot.Hosts)
171170
shuffle(hosts)
172-
maxMessageBytes = config.get('cuebot.max_message_bytes', DEFAULT_MAX_MESSAGE_BYTES)
171+
maxMessageBytes = Cuebot.Config.get('cuebot.max_message_bytes', DEFAULT_MAX_MESSAGE_BYTES)
173172

174173
# create interceptors
175174
interceptors = (
@@ -186,7 +185,8 @@ def setChannel():
186185
if ':' in host:
187186
connectStr = host
188187
else:
189-
connectStr = '%s:%s' % (host, config.get('cuebot.grpc_port', DEFAULT_GRPC_PORT))
188+
connectStr = '%s:%s' % (
189+
host, Cuebot.Config.get('cuebot.grpc_port', DEFAULT_GRPC_PORT))
190190
logger.debug('connecting to gRPC at %s', connectStr)
191191
# TODO(bcipriano) Configure gRPC TLS. (Issue #150)
192192
try:
@@ -228,12 +228,12 @@ def setFacility(facility):
228228
229229
:type facility: str
230230
:param facility: a facility named in the config file"""
231-
if facility not in list(config.get("cuebot.facility").keys()):
232-
default = config.get("cuebot.facility_default")
231+
if facility not in list(Cuebot.Config.get("cuebot.facility").keys()):
232+
default = Cuebot.Config.get("cuebot.facility_default")
233233
logger.warning("The facility '%s' does not exist, defaulting to %s", facility, default)
234234
facility = default
235235
logger.debug("setting facility to: %s", facility)
236-
hosts = config.get("cuebot.facility")[facility]
236+
hosts = Cuebot.Config.get("cuebot.facility")[facility]
237237
Cuebot.setHosts(hosts)
238238

239239
@staticmethod
@@ -290,7 +290,7 @@ def getStub(cls, name):
290290
@staticmethod
291291
def getConfig():
292292
"""Gets the Cuebot config object, originally read in from the config file on disk."""
293-
return config
293+
return Cuebot.Config
294294

295295

296296
# Python 2/3 compatible implementation of ABC

pycue/tests/config_test.py

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
#!/usr/bin/env python
2+
3+
# Copyright Contributors to the OpenCue Project
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License");
6+
# you may not use this file except in compliance with the License.
7+
# You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
17+
"""Tests for `opencue.config`."""
18+
19+
import os
20+
import unittest
21+
22+
import mock
23+
import pyfakefs.fake_filesystem_unittest
24+
25+
import opencue.config
26+
27+
28+
EXPECTED_DEFAULT_CONFIG = {
29+
'logger.format': '%(levelname)-9s %(module)-10s %(message)s',
30+
'logger.level': 'WARNING',
31+
'cuebot.protocol': 'tcp',
32+
'cuebot.grpc_port': 8443,
33+
'cuebot.timeout': 10000,
34+
'cuebot.max_message_bytes': 104857600,
35+
'cuebot.exception_retries': 3,
36+
'cuebot.facility_default': 'local',
37+
'cuebot.facility': {
38+
'local': ['localhost:8443'],
39+
'dev': ['cuetest02-vm.example.com:8443'],
40+
'cloud': [
41+
'cuebot1.example.com:8443',
42+
'cuebot2.example.com:8443',
43+
'cuebot3.example.com:8443'
44+
],
45+
},
46+
}
47+
48+
USER_CONFIG = """
49+
cuebot.facility_default: fake-facility-01
50+
cuebot.facility:
51+
fake-facility-01:
52+
- fake-cuebot-01:1234
53+
fake-facility-02:
54+
- fake-cuebot-02:5678
55+
- fake-cuebot-03:9012
56+
"""
57+
58+
59+
class ConfigTests(pyfakefs.fake_filesystem_unittest.TestCase):
60+
def setUp(self):
61+
self.setUpPyfakefs()
62+
self.fs.add_real_file(
63+
os.path.join(os.path.dirname(opencue.__file__), 'default.yaml'), read_only=True)
64+
os.unsetenv('OPENCUE_CONFIG_FILE')
65+
os.unsetenv('OPENCUE_CONF')
66+
67+
@mock.patch('platform.system', new=mock.Mock(return_value='Linux'))
68+
@mock.patch('os.path.expanduser', new=mock.Mock(return_value='/home/username'))
69+
def test__should_return_config_dir_unix(self):
70+
self.assertEqual('/home/username/.config/opencue', opencue.config.config_base_directory())
71+
72+
@mock.patch('platform.system', new=mock.Mock(return_value='Windows'))
73+
@mock.patch(
74+
'os.path.expandvars', new=mock.Mock(return_value='C:/Users/username/AppData/Roaming'))
75+
def test__should_return_config_dir_windows(self):
76+
self.assertEqual(
77+
'C:/Users/username/AppData/Roaming/opencue', opencue.config.config_base_directory())
78+
79+
def test__should_load_default_config(self):
80+
self.assertIsNone(os.environ.get('OPENCUE_CONFIG_FILE'))
81+
self.assertIsNone(os.environ.get('OPENCUE_CONF'))
82+
83+
config = opencue.config.load_config_from_file()
84+
85+
self.assertEqual(EXPECTED_DEFAULT_CONFIG, config)
86+
87+
def test__should_load_user_config(self):
88+
config_file_path = '/path/to/config.yaml'
89+
self.fs.create_file(config_file_path, contents=USER_CONFIG)
90+
os.environ['OPENCUE_CONFIG_FILE'] = config_file_path
91+
# Define some invalid config using the old setting name, this ensures the old env var
92+
# will be ignored if the new one is set.
93+
config_file_path_legacy = '/path/to/legacy/config.yaml'
94+
self.fs.create_file(config_file_path_legacy, contents='invalid yaml')
95+
os.environ['OPENCUE_CONF'] = config_file_path_legacy
96+
97+
config = opencue.config.load_config_from_file()
98+
99+
self.assertEqual('fake-facility-01', config['cuebot.facility_default'])
100+
self.assertEqual(['fake-cuebot-01:1234'], config['cuebot.facility']['fake-facility-01'])
101+
self.assertEqual(
102+
['fake-cuebot-02:5678', 'fake-cuebot-03:9012'],
103+
config['cuebot.facility']['fake-facility-02'])
104+
# Settings not defined in user config should still have default values.
105+
self.assertEqual(10000, config['cuebot.timeout'])
106+
self.assertEqual(3, config['cuebot.exception_retries'])
107+
108+
def test__should_load_user_config_from_legacy_var(self):
109+
config_file_path = '/path/to/config.yaml'
110+
self.fs.create_file(config_file_path, contents=USER_CONFIG)
111+
os.environ['OPENCUE_CONF'] = config_file_path
112+
113+
config = opencue.config.load_config_from_file()
114+
115+
self.assertEqual('fake-facility-01', config['cuebot.facility_default'])
116+
self.assertEqual(['fake-cuebot-01:1234'], config['cuebot.facility']['fake-facility-01'])
117+
self.assertEqual(
118+
['fake-cuebot-02:5678', 'fake-cuebot-03:9012'],
119+
config['cuebot.facility']['fake-facility-02'])
120+
# Settings not defined in user config should still have default values.
121+
self.assertEqual(10000, config['cuebot.timeout'])
122+
self.assertEqual(3, config['cuebot.exception_retries'])
123+
124+
@mock.patch('platform.system', new=mock.Mock(return_value='Linux'))
125+
@mock.patch('os.path.expanduser', new=mock.Mock(return_value='/home/username'))
126+
def test__should_load_user_config_from_user_profile(self):
127+
config_file_path = '/home/username/.config/opencue/opencue.yaml'
128+
self.fs.create_file(config_file_path, contents=USER_CONFIG)
129+
130+
config = opencue.config.load_config_from_file()
131+
132+
self.assertEqual('fake-facility-01', config['cuebot.facility_default'])
133+
self.assertEqual(['fake-cuebot-01:1234'], config['cuebot.facility']['fake-facility-01'])
134+
self.assertEqual(
135+
['fake-cuebot-02:5678', 'fake-cuebot-03:9012'],
136+
config['cuebot.facility']['fake-facility-02'])
137+
# Settings not defined in user config should still have default values.
138+
self.assertEqual(10000, config['cuebot.timeout'])
139+
self.assertEqual(3, config['cuebot.exception_retries'])
140+
141+
142+
if __name__ == '__main__':
143+
unittest.main()

0 commit comments

Comments
 (0)