#! /usr/bin/python # -*- coding: UTF-8 -*- #------------------------------------------------------------------------------- # Name: check_providers.py # Purpose: enable/disable shoewall providers based on ICMP reachability # update openvpn configuration # Author: htouvet # # Created: 03/03/2014 # Copyright: (c) htouvet 2014 # Licence: GPL V2 #------------------------------------------------------------------------------- import os import sys import subprocess import logging import re import time import datetime import json import signal import sqlite3 from iniparse import RawConfigParser from optparse import OptionParser usage="""\ %prog -c configfile action Check reachability of multiple providers managed by Shorewall enable or disable the providers based on maximum packets loss or RTT action is either : monitor : monitor in background all providers and enable/disable them check [all,] : check all or one provider and display reachability check-json [all,] : check providers and output state as json data status : display current state from state file """ version = "0.0.2" parser=OptionParser(usage=usage,version="%prog " + version) parser.add_option("-i","--check-interval", dest="check_interval", type=int, default=60, help="Config file full path (default: %default)") parser.add_option("-p","--ping-count", dest="ping_count", type=int, default=0, help="Override ping count (default: %default)") parser.add_option("-c","--config", dest="config", default='/etc/check-providers.ini', help="Config file full path (default: %default)") parser.add_option("-d","--dry-run", dest="dry_run", default=False, action='store_true', help="Dry run (default: %default)") parser.add_option("-v","--verbose", dest="verbose", default=False, action='store_true', help="More information (default: %default)") parser.add_option("-o","--log", dest="logfile", default=None, help="Path to log file (default: %default)") parser.add_option("-l","--loglevel", dest="loglevel", default='info', type='choice', choices=['debug','warning','info','error','critical'], metavar='LOGLEVEL',help="Loglevel (default: %default)") REPORT = re.compile(r'\n(?P\d+)\s+packets transmitted,\s+(?P\d+) received,\s+(?P\d+)%\s+packet loss') RTT = re.compile(r'rtt min/avg/max/mdev = (?P[0-9.]+)/(?P[0-9.]+)/(?P[0-9.]+)/(?P[0-9.]+) ms') # --------------------------------------------------------------------------- # Paths # --------------------------------------------------------------------------- BASE_DIR = '/opt/check_providers' DB_PATH = os.path.join(BASE_DIR, 'check-providers.db') STATE_FILE = os.path.join(BASE_DIR, 'check-providers-state.json') MONITOR_PID_FILE = os.path.join(BASE_DIR, 'check-providers.pid') # --------------------------------------------------------------------------- # Database # --------------------------------------------------------------------------- def init_db(): """Create the SQLite database and events table if not present.""" os.makedirs(BASE_DIR, exist_ok=True) with sqlite3.connect(DB_PATH) as conn: conn.execute(''' CREATE TABLE IF NOT EXISTS events ( id INTEGER PRIMARY KEY AUTOINCREMENT, ts TEXT NOT NULL, provider TEXT NOT NULL, available INTEGER, rtt REAL, loss INTEGER, status TEXT, transition INTEGER DEFAULT 0 ) ''') conn.execute('CREATE INDEX IF NOT EXISTS idx_events_provider ON events(provider)') conn.execute('CREATE INDEX IF NOT EXISTS idx_events_ts ON events(ts)') conn.commit() def purge_old_events(days=30): """Remove events older than `days` days.""" with sqlite3.connect(DB_PATH) as conn: conn.execute( "DELETE FROM events WHERE ts < datetime('now', '-{} days')".format(days) ) conn.commit() def write_state_file(providers): """Write the current state of all providers to the JSON state file.""" tmp = STATE_FILE + '.tmp' with open(tmp, 'w') as f: f.write(jsondumps([p.as_dict() for p in providers], indent=True)) os.replace(tmp, STATE_FILE) # atomic replace def record_providers(providers): """Insert one row per provider into the events table.""" with sqlite3.connect(DB_PATH) as conn: for provider in providers: provider.record(conn) conn.commit() # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- def run(cmd, dry_run=False): try: logger.debug(' running {}'.format(cmd)) if not dry_run: p = subprocess.check_output(cmd, shell=True, stderr=subprocess.STDOUT) logger.debug(' output : {}'.format(p)) return (0, p) else: print("DRYRUN : {}".format(cmd)) return (0, "#### DRYRUN ### no output for {}".format(cmd)) except subprocess.CalledProcessError as e: return (e.returncode, e.output) def default_json(o): if hasattr(o, 'as_dict'): return o.as_dict() elif hasattr(o, 'as_json'): return o.as_json() elif isinstance(o, datetime.datetime): return o.isoformat() else: return u"{}".format(o) def jsondumps(o, **kwargs): """Extended json dump of o.""" return json.dumps(o, default=default_json, **kwargs) def arping(device, target_ip, ping_count=3): ARPING1 = re.compile(r'bytes from (?P\S+).*time=(?P[0-9.]*) (?P.*)') ARPING2 = re.compile(r'reply from.*\[(?P\S+)\]\s+(?P[0-9.]*)(?P.*)') ARPING_PATH = "/usr/sbin/arping" if ARPING_PATH is None: raise Exception('No arping command found') elif "/usr/bin/arping" in ARPING_PATH: (returncode, output) = run('arping -c{ping_count} -I{device} {target_ip}'.format( ping_count=ping_count, device=device, target_ip=target_ip)) packets = [p.groupdict() for p in ARPING2.finditer(output.decode('utf-8'))] elif "/usr/sbin/arping" in ARPING_PATH: (returncode, output) = run('arping -c{ping_count} -i{device} {target_ip}'.format( ping_count=ping_count, device=device, target_ip=target_ip)) packets = [p.groupdict() for p in ARPING1.finditer(output.decode('utf-8'))] result = {} if packets: result['mac'] = packets[-1]['mac'] result['rtt'] = packets[-1]['rtt'] + packets[-1]['unit'] result['alive'] = len(packets) > 0 else: result['mac'] = None result['rtt'] = None result['alive'] = False return result def openvpn_local_sockets(): (retcode, output) = run("/bin/netstat -lupnw | grep -E '(udp|tcp) .*/openvpn'") result = [] listening = output.splitlines() for conn in listening: args = conn.split() proto = args[0] (local_ip, local_port) = args[3].rsplit(':', 1) result.append((proto, local_ip, local_port)) return result def delete_conntrack(conn): for (proto, ip, port) in conn: if ip != '0.0.0.0': run('/usr/sbin/conntrack -D -p {proto} -s {src} --sport={port}'.format(src=ip, proto=proto, port=port)) else: run('/usr/sbin/conntrack -D -p {proto} --sport={port}'.format(src=ip, proto=proto, port=port)) def restart_openvpn(): conn = openvpn_local_sockets() print(run('/etc/init.d/openvpn stop')) print(run('ip route flush cache')) delete_conntrack(conn) print(run('/etc/init.d/openvpn start')) # --------------------------------------------------------------------------- # Provider # --------------------------------------------------------------------------- class Provider(object): def __init__(self, provider_name, device=None, gateway=None, target_ip=None, max_rtt=2000.0, max_loss=30, ping_count=10, ping_interval=0.5, timeout=1.5, led=None): self.target_ip = target_ip self.provider_name = provider_name self.device = device self.device_type = None self.device_mac = None self.last_ip = None self._gateway = gateway self.gateway_alive = None self.gateway_rtt = None self.gateway_mac = None self.max_rtt = max_rtt self.max_loss = max_loss self.ping_count = ping_count self.ping_interval = ping_interval self.timeout = timeout self.openvpn_master = 0 self.fallback = 0 self.last_rtt = None self.last_loss = None self._available = None self._previous_available = None # for transition detection self._state_since = None # datetime of last state change self._link_states = [] self._link_status = 'UNKNOWN' self.led = led self.status = '' self.last_check_time = None self.last_enabled = None self.dry_run = False def record(self, conn): """Insert current state into the events table. Marks up<->down transitions.""" transition = int( self._previous_available != self._available and self._previous_available is not None ) if transition: self._state_since = datetime.datetime.now() logger.info('Transition detected for {}: {} -> {}'.format( self.provider_name, self._previous_available, self._available)) conn.execute( '''INSERT INTO events (ts, provider, available, rtt, loss, status, transition) VALUES (?, ?, ?, ?, ?, ?, ?)''', ( datetime.datetime.now().isoformat(), self.provider_name, int(self._available) if self._available is not None else None, self.last_rtt, self.last_loss, self.status, transition, ) ) self._previous_available = self._available def used_by_openvpn(self, proto='udp', port=1194): (retcode, output) = run('conntrack -L -p {proto} --dport {port} -o extended | grep "={src}"'.format( proto=proto, src=self.last_ip, port=port)) conn = output.splitlines() for c in conn: if "={src} ".format(src=self.last_ip) in c.decode('utf-8'): return True return False def read_config(self, config_file): for attrib in ['target_ip', 'device', 'gateway']: if config_file.has_option(self.provider_name, attrib): if attrib == 'gateway': setattr(self, '_gateway', config_file.get(self.provider_name, attrib)) else: setattr(self, attrib, config_file.get(self.provider_name, attrib)) for attrib in ['max_rtt', 'timeout', 'ping_interval']: if config_file.has_option(self.provider_name, attrib): setattr(self, attrib, config_file.getfloat(self.provider_name, attrib)) for attrib in ['max_loss', 'ping_count', 'led', 'openvpn_master', 'fallback']: if config_file.has_option(self.provider_name, attrib): setattr(self, attrib, config_file.getint(self.provider_name, attrib)) @property def device_up(self): (retcode, output) = run('ip link show dev {device}'.format(device=self.device)) LINK = re.compile(r':\s+<(?P.+)>.* state (?P.+?)\s') link = LINK.search(output.decode('utf-8')) if link: self._link_states = link.groupdict()['link_states'].split(',') self._link_status = link.groupdict()['link_status'] return (self._link_status == 'UP') or ('LOWER_UP' in self._link_states) else: return None def check_test_route(self): if self.target_ip: (retcode, route) = run('/sbin/ip route show {target_ip}'.format(target_ip=self.target_ip)) if self.gateway: if not "{target_ip} via {gateway}".format(target_ip=self.target_ip, gateway=self.gateway) in route.decode('utf-8'): logger.debug(run('/sbin/ip route del {target_ip}'.format(target_ip=self.target_ip), dry_run=self.dry_run)[1]) logger.warning('No route for {target_ip} via {gateway}, adding one'.format( target_ip=self.target_ip, gateway=self.gateway)) logger.debug(run('/sbin/ip route add {target_ip} via {gateway}'.format( target_ip=self.target_ip, gateway=self.gateway), dry_run=self.dry_run)[1]) elif self.device: if not " {} ".format(self.device) in route.decode('utf-8'): logger.warning('No route for {target_ip} through {device}, adding one'.format( target_ip=self.target_ip, device=self.device)) logger.debug(run('/sbin/ip route add {target_ip} dev {device}'.format( target_ip=self.target_ip, device=self.device), dry_run=self.dry_run)[1]) else: logger.critical('No gateway for {target_ip}'.format(target_ip=self.target_ip)) def check_gateway(self): if self.gateway: result = arping(device=self.device, target_ip=self.gateway) self.gateway_mac = result['mac'] self.gateway_rtt = result['rtt'] self.gateway_alive = result['alive'] else: self.gateway_mac = None self.gateway_rtt = None self.gateway_alive = None return self.gateway_alive def check_available(self): self._available = None self.last_check_time = datetime.datetime.now() if self.device_up: self.check_local_ip() if self.gateway and not self.check_gateway(): self.status = 'Gateway {} not reachable'.format(self.gateway) logger.critical('Gateway {} not reachable'.format(self.gateway)) ping_ip = self.target_ip if ping_ip: self.check_test_route() (returncode, output) = run('/bin/ping -q -n -c{ping_count:n} -W{timeout:n} -i{ping_interval} -I{device} {target_ip}'.format( ping_count=self.ping_count, timeout=self.timeout, device=self.device, target_ip=ping_ip, ping_interval=self.ping_interval, )) if returncode == 0: report = REPORT.search(output.decode('utf-8')) rtt = RTT.search(output.decode('utf-8')) if report: self.last_loss = int(report.groupdict()['loss']) else: self.last_loss = None if rtt: self.last_rtt = float(rtt.groupdict()['avg']) else: self.last_rtt = None self._available = report and rtt and \ self.last_loss <= self.max_loss and \ self.last_rtt <= self.max_rtt if self._available: self.status = 'OK' elif self.last_loss > self.max_loss: self.status = 'Too much loss {}%'.format(self.last_loss) elif self.last_rtt > self.max_rtt: self.status = 'Too long RTT {}ms'.format(self.last_rtt) else: self.status = 'ping test failed : {}'.format(output.decode('utf-8')) else: self._available = True else: self.status = 'Device {} is down or link state is unknown'.format(str(self.device)) self._available = False self.update_leds() return self._available def check_local_ip(self): (retcode, output) = run('ip addr show dev {device}'.format(device=self.device)) IPV4ADDR = re.compile(r'\sinet\s+(?P\d+.\d+.\d+.\d+)[/\s]') MACADDR = re.compile(r'link/(?P\S+)(\s(?P\S+))?') ipaddr = IPV4ADDR.search(output.decode('utf-8')) if ipaddr: self.last_ip = ipaddr.groupdict()['ipv4'] else: self.last_ip = None macaddr = MACADDR.search(str(output)) if macaddr: self.device_mac = macaddr.groupdict()['mac'] self.device_type = macaddr.groupdict()['type'] else: self.device_mac = None self.device_type = None return self.last_ip @property def gateway(self): if self._gateway: if self._gateway == '-': return None else: return self._gateway else: (retcode, output) = run('ip route list table {}'.format(self.provider_name)) GW = re.compile(r'default via (?P\d+.\d+.\d+.\d+)\s+') gw = GW.search(str(output)) if gw: logger.debug('Gateway : {}'.format(gw.groupdict()['gateway'])) return gw.groupdict()['gateway'] else: logger.debug('No gateway') return None @gateway.setter def gateway_set(self, value): self._gateway = value @property def enabled(self): try: (retcode, routes) = run('ip route list table {}'.format(self.provider_name)) if retcode == 0: routes = str(routes).splitlines() self.last_enabled = len(routes) > 0 else: self.last_enabled = False return self.last_enabled except Exception as e: logger.critical("Unable to get enabled status from routing table: {}".format(e)) return self.last_enabled def led_off(self): led_path = r'/sys/class/leds/apu:green:{}'.format(self.led) if os.path.isdir(led_path): with open(os.path.join(led_path, 'brightness'), 'wb') as f: f.write(bytes('0', encoding='utf-8')) with open(os.path.join(led_path, 'trigger'), 'wb') as f: f.write(bytes('none', encoding='utf-8')) def led_on(self): led_path = r'/sys/class/leds/apu:green:{}'.format(self.led) if os.path.isdir(led_path): with open(os.path.join(led_path, 'trigger'), 'wb') as f: f.write(bytes('none', encoding='utf-8')) with open(os.path.join(led_path, 'brightness'), 'wb') as f: f.write(bytes('1', encoding='utf-8')) def led_blink(self): led_path = r'/sys/class/leds/apu:green:{}'.format(self.led) if os.path.isdir(led_path): with open(os.path.join(led_path, 'brightness'), 'wb') as f: f.write(bytes('1', encoding='utf-8')) with open(os.path.join(led_path, 'trigger'), 'wb') as f: f.write(bytes('heartbeat', encoding='utf-8')) def update_leds(self): if self.enabled: if self._available: self.led_on() else: self.led_off() else: self.led_off() def enable(self): if not self.enabled: logger.debug('Enable {}'.format(self.provider_name)) try: print(run('/var/lib/shorewall/firewall enable {}'.format(self.provider_name), dry_run=self.dry_run)) except Exception as e: logger.info('Retrying to disable/enable provider because %s' % e) print(run('/var/lib/shorewall/firewall restart', dry_run=self.dry_run)) self.update_leds() print('Routes after enabling provider %s\n%s' % (self.provider_name, run('/sbin/shorewall show routing'))) else: logger.debug('{} already enabled'.format(self.device)) def disable(self): if self.enabled: openvpn = self.used_by_openvpn() logger.debug('Disable {}'.format(self.provider_name)) if openvpn: logger.info('openvpn was running here, stopping openvpn') print(run('/etc/init.d/openvpn stop', dry_run=self.dry_run)) print(run('/var/lib/shorewall/firewall disable {}'.format(self.provider_name), dry_run=self.dry_run)) if self.last_ip: logger.info('removing conntrack entries') logger.info(run('/usr/sbin/conntrack -D -s {src}'.format(src=self.last_ip), dry_run=self.dry_run)[1]) logger.info(run('/usr/sbin/conntrack -D -q {src}'.format(src=self.last_ip), dry_run=self.dry_run)[1]) self.remove_default_gw() if openvpn: logger.info('openvpn was running here, restarting openvpn') print(run('/etc/init.d/openvpn start', dry_run=self.dry_run)) self.update_leds() print('Routes after provider %s disabling\n%s' % (self.provider_name, run('/sbin/shorewall show routing'))) def remove_default_gw(self): (retcode, routes) = run('ip route list table main dev {}'.format(self.device)) if retcode == 0: if 'default ' in str(routes): print(run('ip route del default table main dev {}'.format(self.device), dry_run=self.dry_run)) def __str__(self): def get_available(en): if en is None: return "UNKNOWN" elif en: return "AVAILABLE" else: return "UNUSABLE" return "Provider {provider} on {device} ip:{local_ip} nh:{gw} (testing IP:{target_ip}) loss:{loss}%,rtt:{rtt}ms {available} ({status})".format( available=get_available(self._available), provider=self.provider_name, device=self.device, target_ip=self.target_ip, loss=self.last_loss, rtt=self.last_rtt, local_ip=self.last_ip, gw=self.gateway or "-", status=self.status, ) def as_dict(self): return dict( target_ip=self.target_ip, provider_name=self.provider_name, device=self.device, gateway=self._gateway, max_rtt=self.max_rtt, max_loss=self.max_loss, ping_count=self.ping_count, ping_interval=self.ping_interval, ping_timeout=self.timeout, last_rtt=self.last_rtt, last_loss=self.last_loss, available=self._available, link_states=self._link_states, link_status=self._link_status, led=self.led, status=self.status, last_check_time=self.last_check_time, last_ip=self.last_ip, device_mac=self.device_mac, device_type=self.device_type, gateway_alive=self.gateway_alive, gateway_mac=self.gateway_mac, gateway_rtt=self.gateway_rtt, enabled=self.last_enabled, state_since=self._state_since, ) # --------------------------------------------------------------------------- # Config / pid helpers # --------------------------------------------------------------------------- def read_config(filename, providers): cp = RawConfigParser() cp.read(filename) while providers: providers.pop() for provider_name in cp.sections(): provider = Provider(provider_name) provider.read_config(cp) providers.append(provider) def is_pid_running(pidfile): if os.path.isfile(pidfile): with open(pidfile, 'rb') as f: pid = f.read().strip() if pid and os.path.isdir("/proc/{}".format(pid.decode())): return int(pid) else: os.unlink(pidfile) return None else: return None def write_pidfile(pidfile, pid=None): if pid is None: pid = os.getpid() oldpid = is_pid_running(pidfile) if oldpid and oldpid != pid: raise Exception('There is already a running process {} for the pid file {}'.format(oldpid, pidfile)) os.makedirs(os.path.dirname(pidfile), exist_ok=True) with open(pidfile, "wb") as f: f.write(bytes(str(pid), 'utf-8')) def remove_pidfile(pidfile): if os.path.isfile(pidfile): os.unlink(pidfile) # --------------------------------------------------------------------------- # Main # --------------------------------------------------------------------------- if __name__ == '__main__': (options, args) = parser.parse_args() if len(args) < 1: print("ERROR : You must provide one action to perform") parser.print_usage() sys.exit(2) action = args[0] config_file = options.config dry_run = options.dry_run verbose = options.verbose loglevel = options.loglevel monitor_pid_file = MONITOR_PID_FILE current_pid = os.getpid() # setup Logger logger = logging.getLogger() if options.logfile: hdlr = logging.FileHandler(filename=options.logfile, encoding='utf8') hdlr.setFormatter(logging.Formatter('%(asctime)s %(levelname)s %(message)s')) logger.addHandler(hdlr) else: hdlr = logging.StreamHandler() logger.addHandler(hdlr) if loglevel in ('debug', 'warning', 'info', 'error', 'critical'): numeric_level = getattr(logging, loglevel.upper(), None) if not isinstance(numeric_level, int): raise ValueError('Invalid log level: %s' % loglevel) logger.setLevel(numeric_level) if not os.path.isfile(config_file): logger.error("Error : could not find file : " + config_file + ", please check the path") logger.debug("Using " + config_file + " config file") providers = [] read_config(config_file, providers) if options.ping_count: for provider in providers: provider.ping_count = options.ping_count if options.dry_run: logger.warning('### DRY RUN ### no change to routing or interface state will be performed') for provider in providers: provider.dry_run = options.dry_run # ----------------------------------------------------------------------- # Actions # ----------------------------------------------------------------------- if action == 'stop': monitor_pid = is_pid_running(monitor_pid_file) if monitor_pid: logger.info('Sending a TERM signal to running monitor process {}'.format(monitor_pid)) os.kill(monitor_pid, signal.SIGTERM) sys.exit(0) else: logger.warning('No running monitoring found') sys.exit(0) elif action == 'trigger': monitor_pid = is_pid_running(monitor_pid_file) if monitor_pid: logger.info('Sending a wakeup signal to running monitor process {}'.format(monitor_pid)) os.kill(monitor_pid, signal.SIGHUP) sys.exit(0) else: logger.critical('No running monitoring found') sys.exit(1) elif action == 'status': try: with open(STATE_FILE) as f: print(f.read()) except FileNotFoundError: print(jsondumps({'error': 'No state file found, is monitor running?'})) sys.exit(0) elif action == 'monitor': monitor_pid = is_pid_running(monitor_pid_file) if monitor_pid: logger.info('Sending a wakeup signal to running monitor process {}'.format(monitor_pid)) os.kill(monitor_pid, signal.SIGHUP) sys.exit(0) else: init_db() try: write_pidfile(monitor_pid_file) cycle_count = 0 def handler(signum, frame): global providers logger.info('Wake up by signal {}'.format(signum)) if signum == signal.SIGHUP: logger.info(jsondumps(providers, indent=True)) elif signum == signal.SIGUSR1: write_state_file(providers) logger.info('State file updated on SIGUSR1') elif signum == signal.SIGTERM: logger.info('Received kill, closing') remove_pidfile(monitor_pid_file) sys.exit(0) signal.signal(signal.SIGALRM, handler) signal.signal(signal.SIGHUP, handler) signal.signal(signal.SIGTERM, handler) signal.signal(signal.SIGUSR1, handler) while True: try: cycle_count += 1 logger.info('Checking providers {}:'.format( ','.join([provider.provider_name for provider in providers]))) current_ok = [provider for provider in providers if provider.check_available()] openvpn_prov = [provider for provider in providers if provider.used_by_openvpn()] shorewall_restart_needed = False for provider in providers: if provider._available: if not provider.enabled: logger.warning("Enabling the available provider {}".format(provider.provider_name)) provider.enable() run('/usr/sbin/conntrack -F') if provider.openvpn_master: restart_openvpn() if not shorewall_restart_needed and not provider.fallback: (retcode, output) = run('ip route show table balance') balance = str(output).splitlines() in_balance = False for l in balance: if provider.gateway in l.split(' ') or provider.device in l.split(' '): in_balance = True break if not in_balance: shorewall_restart_needed = True logger.critical("Shorewall restart needed because provider {} is not in default balance route".format( provider.provider_name)) run('/usr/sbin/shorewall restart && /usr/sbin/conntrack -F') else: if provider.enabled: if current_ok and not provider.fallback: logger.critical("Disabling the provider {} because {}".format( provider.provider_name, provider.status)) provider.disable() else: if not current_ok: logger.critical("About to disable provider {} but will not because there are no other one".format( provider.provider_name)) else: logger.critical("Not disabling fallback provider {}".format(provider.provider_name)) logger.info(' {}'.format(provider)) # Persist state and history write_state_file(providers) record_providers(providers) # Purge old events once every 100 cycles (~every 100 min with default interval) if cycle_count % 100 == 0: purge_old_events(days=30) signal.alarm(options.check_interval) signal.pause() except Exception as e: logger.critical(e) finally: remove_pidfile(monitor_pid_file) elif action == 'check': if len(args) >= 2: selproviders = [provider for provider in providers if provider.provider_name in args[1:]] else: selproviders = providers for provider in selproviders: print("Checking {}".format(provider.provider_name)) provider.check_available() print(provider) if provider.used_by_openvpn(): print("This provider is used by Openvpn") elif action == 'check-json': result = [] if len(args) >= 2: selproviders = [provider for provider in providers if provider.provider_name in args[1:]] else: selproviders = providers for provider in selproviders: provider.check_available() result.append(provider.as_dict()) print(jsondumps(result, indent=True))