1
|
|
|
#!/usr/bin/env python |
2
|
|
|
# -*- coding: UTF-8 -*- |
3
|
|
|
|
4
|
|
|
# Isomer - The distributed application framework |
5
|
|
|
# ============================================== |
6
|
|
|
# Copyright (C) 2011-2020 Heiko 'riot' Weinen <[email protected]> and others. |
7
|
|
|
# |
8
|
|
|
# This program is free software: you can redistribute it and/or modify |
9
|
|
|
# it under the terms of the GNU Affero General Public License as published by |
10
|
|
|
# the Free Software Foundation, either version 3 of the License, or |
11
|
|
|
# (at your option) any later version. |
12
|
|
|
# |
13
|
|
|
# This program is distributed in the hope that it will be useful, |
14
|
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
15
|
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
16
|
|
|
# GNU Affero General Public License for more details. |
17
|
|
|
# |
18
|
|
|
# You should have received a copy of the GNU Affero General Public License |
19
|
|
|
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
20
|
|
|
|
21
|
|
|
|
22
|
|
|
""" |
23
|
|
|
|
24
|
|
|
Module: CLI |
25
|
|
|
=========== |
26
|
|
|
|
27
|
|
|
Basic management tool functionality and plugin support. |
28
|
|
|
|
29
|
|
|
|
30
|
|
|
""" |
31
|
|
|
|
32
|
|
|
import os |
33
|
|
|
import sys |
34
|
|
|
|
35
|
|
|
import click |
36
|
|
|
from click_didyoumean import DYMGroup |
37
|
|
|
from click_plugins import with_plugins |
38
|
|
|
from pkg_resources import iter_entry_points |
39
|
|
|
from isomer.logger import set_logfile, set_color, set_verbosity, warn, verbose, \ |
40
|
|
|
critical, debug |
41
|
|
|
from isomer.misc.path import get_log_path, set_etc_path, set_instance, set_prefix_path |
42
|
|
|
from isomer.tool import log |
43
|
|
|
from isomer.tool.defaults import ( |
44
|
|
|
db_host_default, |
45
|
|
|
db_host_help, |
46
|
|
|
db_host_metavar, |
47
|
|
|
db_default, |
48
|
|
|
db_help, |
49
|
|
|
db_metavar, |
50
|
|
|
) |
51
|
|
|
from isomer.tool.etc import ( |
52
|
|
|
load_configuration, |
53
|
|
|
load_instances, |
54
|
|
|
instance_template, |
55
|
|
|
) |
56
|
|
|
from isomer.version import version_info |
57
|
|
|
|
58
|
|
|
RPI_GPIO_CHANNEL = 5 |
59
|
|
|
|
60
|
|
|
@click.group( |
61
|
|
|
context_settings={"help_option_names": ["-h", "--help"]}, |
62
|
|
|
cls=DYMGroup, |
63
|
|
|
short_help="Main Isomer CLI" |
64
|
|
|
) |
65
|
|
|
@click.option( |
66
|
|
|
"--instance", |
67
|
|
|
"-i", |
68
|
|
|
default="default", |
69
|
|
|
help="Name of instance to act on", |
70
|
|
|
metavar="<name>", |
71
|
|
|
) |
72
|
|
|
@click.option( |
73
|
|
|
"--env", |
74
|
|
|
"--environment", |
75
|
|
|
"-e", |
76
|
|
|
help="Override environment to act on (CAUTION!)", |
77
|
|
|
default=None, |
78
|
|
|
type=click.Choice(["blue", "green", "current", "other"]), |
79
|
|
|
) |
80
|
|
|
@click.option("--quiet", default=False, help="Suppress all output", is_flag=True) |
81
|
|
|
@click.option( |
82
|
|
|
"--no-colors", "-nc", default=False, help="Do not use colorful output", is_flag=True |
83
|
|
|
) |
84
|
|
|
@click.option( |
85
|
|
|
"--console-level", |
86
|
|
|
"--clog", |
87
|
|
|
default=None, |
88
|
|
|
help="Log level to use (0-100)", |
89
|
|
|
metavar="<level>", |
90
|
|
|
) |
91
|
|
|
@click.option( |
92
|
|
|
"--file-level", |
93
|
|
|
"--flog", |
94
|
|
|
default=None, |
95
|
|
|
help="Log level to use (0-100)", |
96
|
|
|
metavar="<level>", |
97
|
|
|
) |
98
|
|
|
@click.option("--no-log", default=False, is_flag=True, help="Do not log to file") |
99
|
|
|
@click.option("--log-path", default=None, help="Logfile path") |
100
|
|
|
@click.option("--log-file", default=None, help="Logfile name") |
101
|
|
|
@click.option("--dbhost", default=None, help=db_host_help, metavar=db_host_metavar) |
102
|
|
|
@click.option("--dbname", default=None, help=db_help, metavar=db_metavar) |
103
|
|
|
@click.option("--prefix-path", "-p", default=None, help="Use different system prefix") |
104
|
|
|
@click.option("--config-path", "-c", default="/etc/isomer", |
105
|
|
|
help="System configuration path") |
106
|
|
|
@click.option("--fat-logo", "--fat", hidden=True, is_flag=True, default=False) |
107
|
|
|
@click.pass_context |
108
|
|
|
def cli( |
109
|
|
|
ctx, |
110
|
|
|
instance, |
111
|
|
|
env, |
112
|
|
|
quiet, |
113
|
|
|
no_colors, |
114
|
|
|
console_level, |
115
|
|
|
file_level, |
116
|
|
|
no_log, |
117
|
|
|
log_path, |
118
|
|
|
log_file, |
119
|
|
|
dbhost, |
120
|
|
|
dbname, |
121
|
|
|
prefix_path, |
122
|
|
|
config_path, |
123
|
|
|
fat_logo, |
124
|
|
|
): |
125
|
|
|
"""Isomer Management Tool |
126
|
|
|
|
127
|
|
|
This tool supports various operations to manage Isomer instances. |
128
|
|
|
|
129
|
|
|
Most of the commands are grouped. To obtain more information about the |
130
|
|
|
groups' available sub commands/groups, try |
131
|
|
|
|
132
|
|
|
iso [group] |
133
|
|
|
|
134
|
|
|
To display details of a command or its subgroups, try |
135
|
|
|
|
136
|
|
|
iso [group] [subgroup] [..] [command] --help |
137
|
|
|
|
138
|
|
|
To get a map of all available commands, try |
139
|
|
|
|
140
|
|
|
iso cmdmap |
141
|
|
|
""" |
142
|
|
|
|
143
|
|
|
ctx.obj["quiet"] = quiet |
144
|
|
|
|
145
|
|
|
def _set_verbosity(): |
146
|
|
|
if quiet: |
147
|
|
|
console_setting = 100 |
148
|
|
|
else: |
149
|
|
|
console_setting = int( |
150
|
|
|
console_level if console_level is not None else 20 |
151
|
|
|
) |
152
|
|
|
|
153
|
|
|
if no_log: |
154
|
|
|
file_setting = 100 |
155
|
|
|
else: |
156
|
|
|
file_setting = int(file_level if file_level is not None else 20) |
157
|
|
|
|
158
|
|
|
global_setting = min(console_setting, file_setting) |
159
|
|
|
set_verbosity(global_setting, console_setting, file_setting) |
160
|
|
|
|
161
|
|
|
def _set_logger(): |
162
|
|
|
if log_path is not None or log_file is not None: |
163
|
|
|
set_logfile(log_path, instance, log_file) |
164
|
|
|
|
165
|
|
|
if no_colors is False: |
166
|
|
|
set_color() |
167
|
|
|
|
168
|
|
|
_set_verbosity() |
169
|
|
|
_set_logger() |
170
|
|
|
|
171
|
|
|
ctx.obj["instance"] = instance |
172
|
|
|
|
173
|
|
|
log("Running with Python", sys.version.replace("\n", ""), sys.platform, lvl=verbose) |
174
|
|
|
log("Interpreter executable:", sys.executable, lvl=verbose) |
175
|
|
|
|
176
|
|
|
set_etc_path(config_path) |
177
|
|
|
configuration = load_configuration() |
178
|
|
|
|
179
|
|
|
if configuration is not None: |
180
|
|
|
ctx.obj["config"] = configuration |
181
|
|
|
else: |
182
|
|
|
if ctx.invoked_subcommand not in ("version", "cmdmap"): |
183
|
|
|
log("No configuration found. Most commands won't work. " |
184
|
|
|
"Use 'iso system configure' to generate a configuration.", lvl=warn) |
185
|
|
|
return |
186
|
|
|
|
187
|
|
|
set_prefix_path(configuration['meta']['prefix']) |
188
|
|
|
|
189
|
|
|
instances = load_instances() |
190
|
|
|
|
191
|
|
|
ctx.obj["instances"] = instances |
192
|
|
|
|
193
|
|
|
if instance not in instances: |
194
|
|
|
log( |
195
|
|
|
"No instance configuration called %s found! Using fresh defaults." |
196
|
|
|
% instance, |
197
|
|
|
lvl=warn, |
198
|
|
|
) |
199
|
|
|
instance_configuration = instance_template |
200
|
|
|
else: |
201
|
|
|
instance_configuration = instances[instance] |
202
|
|
|
|
203
|
|
|
if file_level is None and console_level is None: |
204
|
|
|
instance_log_level = int(instance_configuration["loglevel"]) |
205
|
|
|
|
206
|
|
|
set_verbosity(instance_log_level, file_level=instance_log_level) |
207
|
|
|
log("Instance log level set to", instance_log_level, lvl=verbose) |
208
|
|
|
|
209
|
|
|
ctx.obj["instance_configuration"] = instance_configuration |
210
|
|
|
|
211
|
|
|
instance_environment = instance_configuration["environment"] |
212
|
|
|
|
213
|
|
|
if env is not None: |
214
|
|
|
if env == "current": |
215
|
|
|
ctx.obj["acting_environment"] = instance_environment |
216
|
|
|
elif env == "other": |
217
|
|
|
ctx.obj["acting_environment"] = ( |
218
|
|
|
"blue" if instance_environment == "green" else "blue" |
219
|
|
|
) |
220
|
|
|
else: |
221
|
|
|
ctx.obj["acting_environment"] = env |
222
|
|
|
env = ctx.obj["acting_environment"] |
223
|
|
|
else: |
224
|
|
|
env = instance_configuration["environment"] |
225
|
|
|
ctx.obj["acting_environment"] = None |
226
|
|
|
|
227
|
|
|
def get_environment_toggle(platform, toggles): |
228
|
|
|
"""Checks well known methods to determine if the other environment should be |
229
|
|
|
booted instead of the default environment.""" |
230
|
|
|
|
231
|
|
|
def temp_file_toggle(): |
232
|
|
|
"""Check by looking for a state file in /tmp""" |
233
|
|
|
|
234
|
|
|
state_filename = "/tmp/isomer_toggle_%s" % instance_configuration["name"] |
235
|
|
|
log("Checking for override state file ", state_filename, lvl=debug) |
236
|
|
|
|
237
|
|
|
if os.path.exists(state_filename): |
238
|
|
|
log("Environment override state file found!", lvl=warn) |
239
|
|
|
return True |
240
|
|
|
else: |
241
|
|
|
log("Environment override state file not found", lvl=debug) |
242
|
|
|
return False |
243
|
|
|
|
244
|
|
|
def gpio_switch_toggle(): |
245
|
|
|
"""Check by inspection of a GPIO pin for a closed switch""" |
246
|
|
|
|
247
|
|
|
log("Checking for override GPIO switch on channel ", RPI_GPIO_CHANNEL, |
248
|
|
|
lvl=debug) |
249
|
|
|
|
250
|
|
|
if platform != "rpi": |
251
|
|
|
log( |
252
|
|
|
"Environment toggle: " |
253
|
|
|
"GPIO switch can only be handled on Raspberry Pi!", |
254
|
|
|
lvl=critical |
255
|
|
|
) |
256
|
|
|
return False |
257
|
|
|
else: |
258
|
|
|
try: |
259
|
|
|
import RPi.GPIO as GPIO |
260
|
|
|
except ImportError: |
261
|
|
|
log("RPi Python module not found. " |
262
|
|
|
"This only works on a Raspberry Pi!", lvl=critical) |
263
|
|
|
return False |
264
|
|
|
|
265
|
|
|
GPIO.setmode(GPIO.BOARD) |
266
|
|
|
GPIO.setup(RPI_GPIO_CHANNEL, GPIO.IN) |
267
|
|
|
|
268
|
|
|
state = GPIO.input(RPI_GPIO_CHANNEL) is True |
269
|
|
|
|
270
|
|
|
if state: |
271
|
|
|
log("Environment override switch active!", lvl=warn) |
272
|
|
|
else: |
273
|
|
|
log("Environment override switch not active", lvl=debug) |
274
|
|
|
|
275
|
|
|
return state |
276
|
|
|
|
277
|
|
|
toggle = False |
278
|
|
|
if "temp_file" in toggles: |
279
|
|
|
toggle = toggle or temp_file_toggle() |
280
|
|
|
if "gpio_switch" in toggles: |
281
|
|
|
toggle = toggle or gpio_switch_toggle() |
282
|
|
|
|
283
|
|
|
if toggle: |
284
|
|
|
log("Booting other Environment per user request.") |
285
|
|
|
else: |
286
|
|
|
log("Booting active environment", lvl=debug) |
287
|
|
|
|
288
|
|
|
return toggle |
289
|
|
|
|
290
|
|
|
#log(configuration['meta'], pretty=True) |
291
|
|
|
#log(instance_configuration, pretty=True) |
292
|
|
|
|
293
|
|
|
if get_environment_toggle(configuration["meta"]["platform"], |
294
|
|
|
instance_configuration['environment_toggles'] |
295
|
|
|
): |
296
|
|
|
if env == 'blue': |
297
|
|
|
env = 'green' |
298
|
|
|
else: |
299
|
|
|
env = 'blue' |
300
|
|
|
|
301
|
|
|
ctx.obj["environment"] = env |
302
|
|
|
|
303
|
|
|
if not fat_logo: |
304
|
|
|
log("<> Isomer", version_info, " [%s|%s]" % (instance, env), lvl=99) |
305
|
|
|
else: |
306
|
|
|
from isomer.misc import logo |
307
|
|
|
|
308
|
|
|
pad = len(logo.split("\n", maxsplit=1)[0]) |
309
|
|
|
log(("Isomer %s" % version_info).center(pad), lvl=99) |
310
|
|
|
for line in logo.split("\n"): |
311
|
|
|
log(line, lvl=99) |
312
|
|
|
|
313
|
|
|
if dbname is None: |
314
|
|
|
dbname = instance_configuration["environments"][env]["database"] |
315
|
|
|
if dbname in ("", None) and ctx.invoked_subcommand in ( |
316
|
|
|
"config", |
317
|
|
|
"db", |
318
|
|
|
"environment", |
319
|
|
|
"plugin", |
320
|
|
|
): |
321
|
|
|
log( |
322
|
|
|
"Database for this instance environment is unset, " |
323
|
|
|
"you probably have to install the environment first.", |
324
|
|
|
lvl=warn, |
325
|
|
|
) |
326
|
|
|
|
327
|
|
|
if dbhost is None: |
328
|
|
|
dbhost = "%s:%i" % ( |
329
|
|
|
instance_configuration["database_host"], |
330
|
|
|
instance_configuration["database_port"], |
331
|
|
|
) |
332
|
|
|
|
333
|
|
|
ctx.obj["dbhost"] = dbhost |
334
|
|
|
ctx.obj["dbname"] = dbname |
335
|
|
|
|
336
|
|
|
set_instance(instance, env, prefix_path) |
337
|
|
|
|
338
|
|
|
if log_path is None and log_file is None: |
339
|
|
|
log_path = get_log_path() |
340
|
|
|
|
341
|
|
|
set_logfile(log_path, instance, log_file) |
342
|
|
|
|
343
|
|
|
|
344
|
|
|
@with_plugins(iter_entry_points("isomer.management")) |
345
|
|
|
@cli.group( |
346
|
|
|
cls=DYMGroup, |
347
|
|
|
short_help="Plugin module management commands" |
348
|
|
|
) |
349
|
|
|
@click.pass_context |
350
|
|
|
def module(ctx): |
351
|
|
|
"""[GROUP] Module commands""" |
352
|
|
|
|
353
|
|
|
from isomer import database |
354
|
|
|
|
355
|
|
|
database.initialize(ctx.obj["dbhost"], ctx.obj["dbname"]) |
356
|
|
|
ctx.obj["db"] = database |
357
|
|
|
|
358
|
|
|
|
359
|
|
|
cli.add_command(module) |
360
|
|
|
|