-
Notifications
You must be signed in to change notification settings - Fork 38
/
kdcustomize
297 lines (253 loc) · 9.2 KB
/
kdcustomize
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
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
#!/usr/bin/env python
# KuberDock - is a platform that allows users to run applications using Docker
# container images and create SaaS / PaaS based on these applications.
# Copyright (C) 2017 Cloud Linux INC
#
# This file is part of KuberDock.
#
# KuberDock is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# KuberDock is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with KuberDock; if not, see <http://www.gnu.org/licenses/>.
#
import argparse
import gzip
import json
import os
import pwd
import shutil
import sys
import tempfile
from collections import namedtuple
from itertools import chain
from operator import attrgetter
from PIL import Image
COMMON_PREFIX = '/var/opt/kuberdock/kubedock/frontend'
TARGETS = [os.path.join(COMMON_PREFIX, i) for i in (
'static/errors/static_page404.html',
'static/js/app_data/menu/templates/list.tpl',
'static/js/app_data/pa/templates/header_for_anon.tpl',
)]
PREPARED_TARGETS = [os.path.join(COMMON_PREFIX, i) for i in (
'static/prepared-isv.bundle.js',
'static/prepared-full.bundle.js',
'static/prepared-init.js',
'static/prepared-full.css',
'static/prepared-isv.css',
)]
GZIP_PREPARED_TARGETS = [target + '.gz' for target in PREPARED_TARGETS]
STYLES_PATH = os.path.join(COMMON_PREFIX, 'static/css')
IMG_PATH = os.path.join(COMMON_PREFIX, 'static/img')
WEB_SERVER_USER = 'nginx'
LOGO_SIZE = (162, 48)
LOGIN_LOGO_SIZE = (227, 67)
ASSETS_PATH = '/var/lib/kuberdock/custom-assets'
ASSETS_MAP = os.path.join(ASSETS_PATH, 'assets.json')
ASSETS_TYPES = 'logo', 'login_logo', 'styles'
def parse_args():
parser = argparse.ArgumentParser('KD customizer')
parser.add_argument('-l', '--logo',
help='Logo image to subsitute with (162x48 px)')
parser.add_argument('-L', '--login-logo',
help='Login logo image to subsitute with (227x67 px)')
parser.add_argument('-s', '--styles', help='Styles file to customize')
parser.add_argument('-r', '--reapply', help='Reapply last customizations',
action='store_true')
return parser.parse_args()
def add_style(styles):
fmt = ("""{0}<link rel="stylesheet" type="text/css" """
"""href="{{{{ url_for('static', filename='css/{1}') }}}}">\n""")
contents = []
for target in TARGETS:
with open(target) as f:
for l in f:
if '</head>' in l:
pos = l.find('<')
s = fmt.format(' ' * (pos + 4), styles)
contents.append(s)
contents.append(l)
return contents
def change_image(logo, target, name='logo.png', gz=False):
if not os.path.exists(target):
return
contents = []
op = gzip.open if gz else open
with op(target) as f:
for l in f:
contents.append(l.replace('/{}'.format(name), '/{}'.format(logo)))
return contents
def process_logo(logo, user, reapply=False):
"""
Copies provided logo file to a img folder, chowns it and modifies contens
to use supplied file
:param logo: string -> file path
:param user: namedtuple object -> (user.uid, user.gid) of the web-server
user
"""
if logo is None:
return
check_filepath(logo)
check_image_size(logo, LOGO_SIZE)
fullpath = os.path.join(IMG_PATH, os.path.basename(logo))
shutil.copyfile(logo, fullpath)
for target in TARGETS:
save(change_image(os.path.basename(logo), target), target)
for target in PREPARED_TARGETS:
save(change_image(os.path.basename(logo), target), target)
for target in GZIP_PREPARED_TARGETS:
save(change_image(os.path.basename(logo), target, gz=True),
target, gz=True)
for path in chain([fullpath], TARGETS, PREPARED_TARGETS,
GZIP_PREPARED_TARGETS):
os.chown(path, user.uid, user.gid)
if not reapply:
save_assets(logo, 'logo')
def process_login_logo(logo, user, reapply=False):
"""
Copies provided logo file to a img folder, chowns it and modifies contens
to use supplied file
:param logo: string -> file path
:param user: namedtuple object -> (user.uid, user.gid) of the web-server
user
"""
if logo is None:
return
check_filepath(logo)
check_image_size(logo, LOGIN_LOGO_SIZE)
fullpath = os.path.join(IMG_PATH, os.path.basename(logo))
shutil.copyfile(logo, fullpath)
name = 'logo-login.png'
for target in TARGETS:
save(change_image(os.path.basename(logo), target, name), target)
for target in PREPARED_TARGETS:
save(change_image(os.path.basename(logo), target, name), target)
for target in GZIP_PREPARED_TARGETS:
save(change_image(os.path.basename(logo), target, name, gz=True),
target, gz=True)
for path in chain([fullpath], TARGETS, PREPARED_TARGETS,
GZIP_PREPARED_TARGETS):
os.chown(path, user.uid, user.gid)
if not reapply:
save_assets(logo, 'login_logo')
def process_styles(styles, user, reapply=False):
"""
Copies provided styles file to a styles folder, chowns it and modifies
contens to use supplied file
@param styles: string -> file path
@param user: namedtuple object -> (user.uid, user.gid) of the web-server
user
"""
if styles is None:
return
check_filepath(styles)
fullpath = os.path.join(STYLES_PATH, os.path.basename(styles))
shutil.copyfile(styles, fullpath)
os.chown(fullpath, user.uid, user.gid)
for target in TARGETS:
save(add_style(os.path.basename(styles)), target)
os.chown(target, user.uid, user.gid)
if not reapply:
save_assets(styles, 'styles')
def save(contents, target, gz=False):
if contents is None:
return
fh, path = tempfile.mkstemp(dir=os.path.dirname(target))
with os.fdopen(fh, 'w') as f:
for l in contents:
f.write(l)
if gz:
path0 = path
path = path + '.gz'
with open(path0, 'rb') as i, gzip.open(path, 'wb') as o:
shutil.copyfileobj(i, o)
os.unlink(path0)
os.rename(path, target)
if os.path.exists(path):
os.unlink(path)
def check_euid():
if os.geteuid() != 0:
raise SystemExit("Superuser privileges expected!")
def check_filepath(path):
if not os.path.exists(path):
raise SystemExit('File not found: {0}'.format(path,))
def check_image_size(path, size):
im = Image.open(path)
if im.size != size:
raise SystemExit('{0}x{1} pixels are expected for image'.format(*size))
def get_server_user():
try:
data = pwd.getpwnam(WEB_SERVER_USER)
User = namedtuple('User', 'uid gid')
return User._make((data.pw_uid, data.pw_gid))
except KeyError:
raise SystemExit('No such user: {0}'.format(WEB_SERVER_USER,))
def save_assets(asset_path, asset_type):
"""
Saving a customized asset for posible reapply
:param asset_path: str -> asset file path
:param asset_type: str -> one of the supported asset types
"""
if asset_type not in ASSETS_TYPES:
return
shutil.copy(asset_path, ASSETS_PATH)
asset_basename = os.path.basename(asset_path)
assets_map = get_assets_map()
assets_map[asset_type] = asset_basename
save_accets_map(assets_map)
def save_accets_map(data):
"""
Saves info about customized assets
:param data: dict -> map of files and its destination
"""
with open(ASSETS_MAP, 'w') as f:
json.dump(data, f)
def get_assets_map():
"""
Gets info about customized assets. If no map found returns empty dict
:return: dict -> map of files and its destination
"""
try:
with open(ASSETS_MAP) as f:
return json.load(f)
except (IOError, ValueError):
return dict()
def reapply(user):
"""
Reapplies customized assets from saved copies
:param user: namedtuple object -> (user.uid, user.gid) of the web-server
user
"""
assets = get_assets_map()
for asset in assets:
if not isinstance(assets[asset], basestring):
continue
if asset not in ASSETS_TYPES:
continue
asset_path = os.path.join(ASSETS_PATH, assets[asset])
if not os.path.exists(asset_path):
continue
globals()['process_' + asset](asset_path, user, reapply=True)
if __name__ == '__main__':
check_euid()
if not os.path.exists(ASSETS_PATH):
os.mkdir(ASSETS_PATH)
user = get_server_user()
args = parse_args()
if not any(attrgetter('logo', 'login_logo', 'styles', 'reapply')(args)):
print("At least one argument is expected. Use --help to learn about "
"options")
sys.exit(0)
if args.reapply:
reapply(user)
else:
process_logo(args.logo, user)
process_login_logo(args.login_logo, user)
process_styles(args.styles, user)