forked from luci/luci-py
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathconfig.py
189 lines (141 loc) · 5.57 KB
/
config.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
# Copyright 2013 The LUCI Authors. All rights reserved.
# Use of this source code is governed under the Apache License, Version 2.0
# that can be found in the LICENSE file.
"""Instance specific settings."""
import logging
import posixpath
import re
from google.appengine.api import app_identity
from google.appengine.api import modules
from google.appengine.ext import ndb
from components import config
from components import gitiles
from components import net
from components import utils
from components.config import validation
from components.datastore_utils import config as ds_config
from proto import config_pb2
ConfigApi = config.ConfigApi
### Public code.
class Config(object):
"""A join of Datastore and config_service backed configs."""
def __init__(self, ds_cfg, cfg):
self._ds_cfg = ds_cfg
self._cfg = cfg
def __getattr__(self, name):
if hasattr(self._cfg, name):
return getattr(self._cfg, name)
return getattr(self._ds_cfg, name)
class GlobalConfig(ds_config.GlobalConfig):
"""Application wide settings."""
# Secret key used to sign Google Storage URLs: base64 encoded *.der file.
gs_private_key = ndb.StringProperty(indexed=False, default='')
def settings(fresh=False):
"""Loads GlobalConfig or a default one if not present.
If fresh=True, a full fetch from NDB is done.
"""
if fresh:
GlobalConfig.clear_cache()
cfg = _get_settings_with_defaults()[1]
else:
cfg = _get_settings_cached()
ds_cfg = GlobalConfig.cached()
return Config(ds_cfg, cfg)
def settings_info():
"""Returns information about the settings file.
Returns a dict with keys:
'cfg': parsed SettingsCfg message
'rev': revision of cfg
'rev_url': URL of a human-consumable page that displays the config
'config_service_url': URL of the config_service.
"""
GlobalConfig.clear_cache()
ds_cfg = GlobalConfig.cached()
rev, cfg = _get_settings_with_defaults()
rev_url = _gitiles_url(_get_configs_url(), rev, _SETTINGS_CFG_FILENAME)
cfg_service_hostname = config.config_service_hostname()
return {
'cfg': ds_cfg,
'config_service_url':
'https://%s' % cfg_service_hostname if cfg_service_hostname else '',
'luci_cfg': cfg,
'rev': rev,
'rev_url': rev_url,
}
def get_local_dev_server_host():
"""Returns 'hostname:port' for a default module on a local dev server."""
assert utils.is_local_dev_server()
return modules.get_hostname(module='default')
def warmup():
"""Precaches configuration in local memory, to be called from warmup handler.
This call is optional. Everything works even if 'warmup' is never called.
"""
settings()
utils.get_task_queue_host()
utils.get_app_version()
### Private code.
_SETTINGS_CFG_FILENAME = 'settings.cfg'
_GS_BUCKET_RE = re.compile(r'^[a-z0-9A-Z\-]+$')
_EMAIL_RE = re.compile(r'^[a-z0-9A-Z\-\._+]+@[a-z0-9A-Z\-\._]+$')
@validation.self_rule(_SETTINGS_CFG_FILENAME, config_pb2.SettingsCfg)
def _validate_settings(cfg, ctx):
"""Validates settings.cfg file against proto message schema."""
with ctx.prefix('default_expiration '):
if cfg.default_expiration < 0:
ctx.error('cannot be negative')
with ctx.prefix('sharding_letters '):
if not (0 <= cfg.sharding_letters and cfg.sharding_letters <= 5):
ctx.error('must be within [0..5]')
with ctx.prefix('gs_bucket '):
if not _GS_BUCKET_RE.match(cfg.gs_bucket):
ctx.error('invalid value: %s', cfg.gs_bucket)
if cfg.HasField('gs_client_id_email'):
with ctx.prefix('gs_client_id_email '):
if not _EMAIL_RE.match(cfg.gs_client_id_email):
ctx.error('invalid value: %s', cfg.gs_client_id_email)
@utils.memcache('config:get_configs_url', time=60)
def _get_configs_url():
"""Returns URL where luci-config fetches configs from."""
try:
return config.get_config_set_location(config.self_config_set())
except net.Error:
logging.info(
'Could not get configs URL. Possibly config directory for this '
'instance of swarming does not exist')
def _gitiles_url(configs_url, rev, path):
"""URL to a directory in gitiles -> URL to a file at concrete revision."""
try:
loc = gitiles.Location.parse(configs_url or '')
return str(loc._replace(
treeish=rev or loc.treeish,
path=posixpath.join(loc.path, path)))
except ValueError:
# Not a gitiles URL, return as is.
return configs_url
def _get_settings():
"""Returns (rev, cfg) where cfg is a parsed SettingsCfg message.
If config does not exists, returns (None, None).
Mock this method in tests to inject changes to the defaults.
"""
# store_last_good=True tells config component to update the config file
# in a cron job. Here we just read from the datastore.
return config.get_self_config(
_SETTINGS_CFG_FILENAME, config_pb2.SettingsCfg, store_last_good=True)
def _get_settings_with_defaults():
"""Returns (rev, cfg) where cfg is a parsed SettingsCfg message.
If config does not exists, returns (None, <cfg with defaults>).
The config is cached in the datastore.
"""
rev, cfg = _get_settings()
cfg = cfg or config_pb2.SettingsCfg()
cfg.default_expiration = cfg.default_expiration or 30*24*60*60
cfg.sharding_letters = cfg.sharding_letters or 4
cfg.gs_bucket = cfg.gs_bucket or app_identity.get_application_id()
cfg.auth.full_access_group = cfg.auth.full_access_group or 'administrators'
cfg.auth.readonly_access_group = \
cfg.auth.readonly_access_group or 'administrators'
return rev, cfg
@utils.cache_with_expiration(60)
def _get_settings_cached():
"""Loads settings from an NDB-based cache or a default one if not present."""
return _get_settings_with_defaults()[1]