tool.cli.cli()   F
last analyzed

Complexity

Conditions 34

Size

Total Lines 282
Code Lines 185

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 34
eloc 185
nop 15
dl 0
loc 282
rs 0
c 0
b 0
f 0

How to fix   Long Method    Complexity    Many Parameters   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Complexity

Complex classes like tool.cli.cli() often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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