mirror of
https://github.com/munin-monitoring/contrib.git
synced 2018-11-08 00:59:34 +01:00
fixes after @sumpfraller comments on pr #849
This commit is contained in:
parent
0d549a44e4
commit
8732576473
1 changed files with 111 additions and 68 deletions
179
plugins/tor/tor_
179
plugins/tor/tor_
|
@ -1,28 +1,78 @@
|
||||||
#!/usr/bin/python2
|
#!/usr/bin/python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
'''
|
||||||
|
=head1 NAME
|
||||||
|
tor_
|
||||||
|
|
||||||
|
=head1 DESCRIPTION
|
||||||
|
Wildcard plugin that gathers some metrics from the Tor deamon
|
||||||
|
https://github.com/daftaupe/munin-plugins-tor
|
||||||
|
|
||||||
|
Derived from https://github.com/mweinelt/munin-tor
|
||||||
|
|
||||||
|
This plugin requires the stem library : https://stem.torproject.org/
|
||||||
|
This plugin requires the GeoIP library : https://www.maxmind.com for the countries plugin
|
||||||
|
|
||||||
|
Available plugins :
|
||||||
|
tor_bandwidth # Graph the glabal bandwidth
|
||||||
|
tor_connections # Graph the number of connexions
|
||||||
|
tor_countries # Graph the countries represented our connexions
|
||||||
|
tor_dormant # Graph if tor is dormant or not
|
||||||
|
tor_flags # Graph the different flags of the relay
|
||||||
|
tor_routers # Graph the number of routers seen by the relay
|
||||||
|
tor_traffic # Graph the read/written traffic
|
||||||
|
|
||||||
|
=head2 CONFIGURATION
|
||||||
|
The default configuration is below
|
||||||
|
[tor_*]
|
||||||
|
user toranon # or any other user/group that is running tor
|
||||||
|
group toranon
|
||||||
|
env.torcachefile 'munin_tor_country_stats.json'
|
||||||
|
env.torconnectmethod 'port'
|
||||||
|
env.torgeoippath "/usr/share/GeoIP/GeoIP.dat"
|
||||||
|
env.tormaxcountries 15
|
||||||
|
env.torport 9051
|
||||||
|
env.torsocket '/var/run/tor/control'
|
||||||
|
|
||||||
|
To make it connect through a socket modify this way
|
||||||
|
[tor_*]
|
||||||
|
user toranon # or any other user/group that is running tor
|
||||||
|
group toranon
|
||||||
|
env.torcachefile 'munin_tor_country_stats.json'
|
||||||
|
env.torconnectmethod 'socket'
|
||||||
|
env.torgeoippath "/usr/share/GeoIP/GeoIP.dat"
|
||||||
|
env.tormaxcountries 15
|
||||||
|
env.torport 9051
|
||||||
|
env.torsocket '/var/run/tor/control'
|
||||||
|
|
||||||
|
=head1 COPYRIGHT
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
=head1 AUTHOR
|
||||||
|
daftaupe <daftaupe[at]protonmail.com>
|
||||||
|
'''
|
||||||
|
|
||||||
from __future__ import print_function
|
from __future__ import print_function
|
||||||
import collections
|
import collections
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import stem
|
|
||||||
import stem.control
|
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
default_torgeoippath = "/usr/share/GeoIP/GeoIP.dat"
|
import GeoIP
|
||||||
|
import stem
|
||||||
|
import stem.control
|
||||||
|
import stem.connection
|
||||||
|
|
||||||
default_torcachefile = 'munin_tor_country_stats.json'
|
default_torcachefile = 'munin_tor_country_stats.json'
|
||||||
|
default_torconnectmethod = 'port'
|
||||||
|
default_torgeoippath = "/usr/share/GeoIP/GeoIP.dat"
|
||||||
|
default_tormaxcountries = 15
|
||||||
default_torport = 9051
|
default_torport = 9051
|
||||||
default_torsocket = '/var/run/tor/control'
|
default_torsocket = '/var/run/tor/control'
|
||||||
default_torconnectmethod = 'port'
|
|
||||||
|
|
||||||
#%# family=auto
|
#%# family=auto
|
||||||
#%# capabilities=autoconf suggest
|
#%# capabilities=autoconf suggest
|
||||||
|
|
||||||
def simplify(cn):
|
|
||||||
"""Simplify country name"""
|
|
||||||
cn = cn.replace(' ', '_')
|
|
||||||
cn = cn.replace("'", '_')
|
|
||||||
cn = cn.split(',', 1)[0]
|
|
||||||
return cn
|
|
||||||
|
|
||||||
|
|
||||||
class ConnectionError(Exception):
|
class ConnectionError(Exception):
|
||||||
|
@ -49,18 +99,18 @@ def authenticate(controller):
|
||||||
try:
|
try:
|
||||||
controller.authenticate(password=password)
|
controller.authenticate(password=password)
|
||||||
except stem.connection.PasswordAuthFailed:
|
except stem.connection.PasswordAuthFailed:
|
||||||
print("Authentication failed (incorrect password)")
|
print("Authentication failed (incorrect password)", file=sys.stderr)
|
||||||
|
|
||||||
|
|
||||||
def gen_controller():
|
def gen_controller():
|
||||||
connect_method = os.environ.get('connectmethod', default_torconnectmethod)
|
connect_method = os.environ.get('torconnectmethod', default_torconnectmethod)
|
||||||
if connect_method == 'port':
|
if connect_method == 'port':
|
||||||
return stem.control.Controller.from_port(port=int(os.environ.get('port', default_torport)))
|
return stem.control.Controller.from_port(port=int(os.environ.get('torport', default_torport)))
|
||||||
elif connect_method == 'socket':
|
elif connect_method == 'socket':
|
||||||
return stem.control.Controller.from_socket_file(path=os.environ.get('socket', default_torsocket))
|
return stem.control.Controller.from_socket_file(path=os.environ.get('torsocket', default_torsocket))
|
||||||
else:
|
else:
|
||||||
print("env.connectmethod contains an invalid value. Please specify either 'port' or 'socket'.", file=sys.stderr)
|
print("env.torconnectmethod contains an invalid value. Please specify either 'port' or 'socket'.", file=sys.stderr)
|
||||||
sys.exit(-1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
#########################
|
#########################
|
||||||
|
@ -87,6 +137,18 @@ class TorPlugin(object):
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def autoconf():
|
def autoconf():
|
||||||
|
try:
|
||||||
|
import stem
|
||||||
|
|
||||||
|
except ImportError as e:
|
||||||
|
print('no ({})'.format(e))
|
||||||
|
|
||||||
|
try:
|
||||||
|
import GeoIP
|
||||||
|
|
||||||
|
except ImportError as e:
|
||||||
|
print('no ({})'.format(e))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with gen_controller() as controller:
|
with gen_controller() as controller:
|
||||||
try:
|
try:
|
||||||
|
@ -100,7 +162,7 @@ class TorPlugin(object):
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def suggest():
|
def suggest():
|
||||||
options = ['connections', 'traffic', 'dormant', 'bandwidth', 'flags', 'countries']
|
options = ['bandwidth', 'connections', 'countries', 'dormant', 'flags', 'routers', 'traffic']
|
||||||
|
|
||||||
for option in options:
|
for option in options:
|
||||||
print(option)
|
print(option)
|
||||||
|
@ -122,7 +184,7 @@ class TorBandwidth(TorPlugin):
|
||||||
graph = {'title': 'Observed bandwidth',
|
graph = {'title': 'Observed bandwidth',
|
||||||
'args': '-l 0 --base 1000',
|
'args': '-l 0 --base 1000',
|
||||||
'vlabel': 'bytes/s',
|
'vlabel': 'bytes/s',
|
||||||
'category': 'Tor',
|
'category': 'network',
|
||||||
'info': 'estimated capacity based on usage in bytes/s'}
|
'info': 'estimated capacity based on usage in bytes/s'}
|
||||||
labels = {'bandwidth': {'label': 'bandwidth', 'min': 0, 'type': 'GAUGE'}}
|
labels = {'bandwidth': {'label': 'bandwidth', 'min': 0, 'type': 'GAUGE'}}
|
||||||
|
|
||||||
|
@ -143,12 +205,12 @@ class TorBandwidth(TorPlugin):
|
||||||
fingerprint = controller.get_info('fingerprint', None)
|
fingerprint = controller.get_info('fingerprint', None)
|
||||||
if fingerprint is None:
|
if fingerprint is None:
|
||||||
print("Error while reading fingerprint from Tor daemon", file=sys.stderr)
|
print("Error while reading fingerprint from Tor daemon", file=sys.stderr)
|
||||||
sys.exit(-1)
|
sys.exit(1)
|
||||||
|
|
||||||
response = controller.get_server_descriptor(fingerprint, None)
|
response = controller.get_server_descriptor(fingerprint, None)
|
||||||
if response is None:
|
if response is None:
|
||||||
print("Error while getting server descriptor from Tor daemon", file=sys.stderr)
|
print("Error while getting server descriptor from Tor daemon", file=sys.stderr)
|
||||||
sys.exit(-1)
|
sys.exit(1)
|
||||||
print('bandwidth.value {}'.format(response.observed_bandwidth))
|
print('bandwidth.value {}'.format(response.observed_bandwidth))
|
||||||
|
|
||||||
|
|
||||||
|
@ -160,7 +222,7 @@ class TorConnections(TorPlugin):
|
||||||
graph = {'title': 'Connections',
|
graph = {'title': 'Connections',
|
||||||
'args': '-l 0 --base 1000',
|
'args': '-l 0 --base 1000',
|
||||||
'vlabel': 'connections',
|
'vlabel': 'connections',
|
||||||
'category': 'Tor',
|
'category': 'network',
|
||||||
'info': 'OR connections by state'}
|
'info': 'OR connections by state'}
|
||||||
labels = {'new': {'label': 'new', 'min': 0, 'max': 25000, 'type': 'GAUGE'},
|
labels = {'new': {'label': 'new', 'min': 0, 'max': 25000, 'type': 'GAUGE'},
|
||||||
'launched': {'label': 'launched', 'min': 0, 'max': 25000, 'type': 'GAUGE'},
|
'launched': {'label': 'launched', 'min': 0, 'max': 25000, 'type': 'GAUGE'},
|
||||||
|
@ -178,7 +240,7 @@ class TorConnections(TorPlugin):
|
||||||
response = controller.get_info('orconn-status', None)
|
response = controller.get_info('orconn-status', None)
|
||||||
if response is None:
|
if response is None:
|
||||||
print("No response from Tor daemon in TorConnection.fetch()", file=sys.stderr)
|
print("No response from Tor daemon in TorConnection.fetch()", file=sys.stderr)
|
||||||
sys.exit(-1)
|
sys.exit(1)
|
||||||
else:
|
else:
|
||||||
connections = response.split('\n')
|
connections = response.split('\n')
|
||||||
states = dict((state, 0) for state in stem.ORStatus)
|
states = dict((state, 0) for state in stem.ORStatus)
|
||||||
|
@ -198,26 +260,19 @@ class TorCountries(TorPlugin):
|
||||||
self.cache_dir_name = os.path.join(self.cache_dir_name,
|
self.cache_dir_name = os.path.join(self.cache_dir_name,
|
||||||
os.environ.get('torcachefile', default_torcachefile))
|
os.environ.get('torcachefile', default_torcachefile))
|
||||||
|
|
||||||
max_countries = os.environ.get('tormaxcountries', 15)
|
max_countries = os.environ.get('tormaxcountries', default_tormaxcountries)
|
||||||
self.max_countries = int(max_countries)
|
self.max_countries = int(max_countries)
|
||||||
|
|
||||||
geoip_path = os.environ.get('torgeoippath', default_torgeoippath)
|
geoip_path = os.environ.get('torgeoippath', default_torgeoippath)
|
||||||
try:
|
self.geodb = GeoIP.open(geoip_path, GeoIP.GEOIP_MEMORY_CACHE)
|
||||||
import GeoIP
|
|
||||||
self.geodb = GeoIP.open(geoip_path, GeoIP.GEOIP_MEMORY_CACHE)
|
|
||||||
self.available = True
|
|
||||||
except Exception:
|
|
||||||
self.available = False
|
|
||||||
|
|
||||||
def conf(self):
|
def conf(self):
|
||||||
"""Configure plugin"""
|
"""Configure plugin"""
|
||||||
if not self.available:
|
|
||||||
return
|
|
||||||
|
|
||||||
graph = {'title': 'Countries',
|
graph = {'title': 'Countries',
|
||||||
'args': '-l 0 --base 1000',
|
'args': '-l 0 --base 1000',
|
||||||
'vlabel': 'countries',
|
'vlabel': 'countries',
|
||||||
'category': 'Tor',
|
'category': 'network',
|
||||||
'info': 'OR connections by state'}
|
'info': 'OR connections by state'}
|
||||||
labels = {}
|
labels = {}
|
||||||
|
|
||||||
|
@ -237,9 +292,6 @@ class TorCountries(TorPlugin):
|
||||||
"""Generate metrics"""
|
"""Generate metrics"""
|
||||||
|
|
||||||
# If possible, read cached data instead of doing the processing twice
|
# If possible, read cached data instead of doing the processing twice
|
||||||
if not self.available:
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with open(self.cache_dir_name) as f:
|
with open(self.cache_dir_name) as f:
|
||||||
countries_num = json.load(f)
|
countries_num = json.load(f)
|
||||||
|
@ -251,23 +303,6 @@ class TorCountries(TorPlugin):
|
||||||
for c, v in countries_num:
|
for c, v in countries_num:
|
||||||
print("%s.value %d" % (c, v))
|
print("%s.value %d" % (c, v))
|
||||||
|
|
||||||
# Unused
|
|
||||||
#@staticmethod
|
|
||||||
#def _gen_ipaddrs_from_circuits(controller):
|
|
||||||
# """Generate a sequence of ipaddrs for every built circuit"""
|
|
||||||
# # Currently unused
|
|
||||||
# for circ in controller.get_circuits():
|
|
||||||
# if circ.status != CircStatus.BUILT:
|
|
||||||
# continue
|
|
||||||
#
|
|
||||||
# for entry in circ.path:
|
|
||||||
# fingerprint, nickname = entry
|
|
||||||
#
|
|
||||||
# desc = controller.get_network_status(fingerprint, None)
|
|
||||||
# if desc:
|
|
||||||
# ipaddr = desc.address
|
|
||||||
# yield ipaddr
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _gen_ipaddrs_from_statuses(controller):
|
def _gen_ipaddrs_from_statuses(controller):
|
||||||
"""Generate a sequence of ipaddrs for every network status"""
|
"""Generate a sequence of ipaddrs for every network status"""
|
||||||
|
@ -275,6 +310,14 @@ class TorCountries(TorPlugin):
|
||||||
ipaddr = desc.address
|
ipaddr = desc.address
|
||||||
yield ipaddr
|
yield ipaddr
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def simplify(cn):
|
||||||
|
"""Simplify country name"""
|
||||||
|
cn = cn.replace(' ', '_')
|
||||||
|
cn = cn.replace("'", '_')
|
||||||
|
cn = cn.split(',', 1)[0]
|
||||||
|
return cn
|
||||||
|
|
||||||
def _gen_countries(self, controller):
|
def _gen_countries(self, controller):
|
||||||
"""Generate a sequence of countries for every built circuit"""
|
"""Generate a sequence of countries for every built circuit"""
|
||||||
for ipaddr in self._gen_ipaddrs_from_statuses(controller):
|
for ipaddr in self._gen_ipaddrs_from_statuses(controller):
|
||||||
|
@ -283,7 +326,7 @@ class TorCountries(TorPlugin):
|
||||||
yield 'Unknown'
|
yield 'Unknown'
|
||||||
continue
|
continue
|
||||||
|
|
||||||
yield simplify(country)
|
yield self.simplify(country)
|
||||||
|
|
||||||
def top_countries(self):
|
def top_countries(self):
|
||||||
"""Build a list of top countries by number of circuits"""
|
"""Build a list of top countries by number of circuits"""
|
||||||
|
@ -305,7 +348,7 @@ class TorDormant(TorPlugin):
|
||||||
graph = {'title': 'Dormant',
|
graph = {'title': 'Dormant',
|
||||||
'args': '-l 0 --base 1000',
|
'args': '-l 0 --base 1000',
|
||||||
'vlabel': 'dormant',
|
'vlabel': 'dormant',
|
||||||
'category': 'Tor',
|
'category': 'network',
|
||||||
'info': 'Is Tor not building circuits because it is idle?'}
|
'info': 'Is Tor not building circuits because it is idle?'}
|
||||||
labels = {'dormant': {'label': 'dormant', 'min': 0, 'max': 1, 'type': 'GAUGE'}}
|
labels = {'dormant': {'label': 'dormant', 'min': 0, 'max': 1, 'type': 'GAUGE'}}
|
||||||
|
|
||||||
|
@ -314,13 +357,12 @@ class TorDormant(TorPlugin):
|
||||||
def fetch(self):
|
def fetch(self):
|
||||||
with gen_controller() as controller:
|
with gen_controller() as controller:
|
||||||
try:
|
try:
|
||||||
#controller.authenticate()
|
|
||||||
authenticate(controller)
|
authenticate(controller)
|
||||||
|
|
||||||
response = controller.get_info('dormant', None)
|
response = controller.get_info('dormant', None)
|
||||||
if response is None:
|
if response is None:
|
||||||
print("Error while reading dormant state from Tor daemon", file=sys.stderr)
|
print("Error while reading dormant state from Tor daemon", file=sys.stderr)
|
||||||
sys.exit(-1)
|
sys.exit(1)
|
||||||
print('dormant.value {}'.format(response))
|
print('dormant.value {}'.format(response))
|
||||||
except stem.connection.AuthenticationFailure as e:
|
except stem.connection.AuthenticationFailure as e:
|
||||||
print('Authentication failed ({})'.format(e))
|
print('Authentication failed ({})'.format(e))
|
||||||
|
@ -334,7 +376,7 @@ class TorFlags(TorPlugin):
|
||||||
graph = {'title': 'Relay flags',
|
graph = {'title': 'Relay flags',
|
||||||
'args': '-l 0 --base 1000',
|
'args': '-l 0 --base 1000',
|
||||||
'vlabel': 'flags',
|
'vlabel': 'flags',
|
||||||
'category': 'Tor',
|
'category': 'network',
|
||||||
'info': 'Flags active for relay'}
|
'info': 'Flags active for relay'}
|
||||||
labels = {flag: {'label': flag, 'min': 0, 'max': 1, 'type': 'GAUGE'} for flag in stem.Flag}
|
labels = {flag: {'label': flag, 'min': 0, 'max': 1, 'type': 'GAUGE'} for flag in stem.Flag}
|
||||||
|
|
||||||
|
@ -355,12 +397,12 @@ class TorFlags(TorPlugin):
|
||||||
fingerprint = controller.get_info('fingerprint', None)
|
fingerprint = controller.get_info('fingerprint', None)
|
||||||
if fingerprint is None:
|
if fingerprint is None:
|
||||||
print("Error while reading fingerprint from Tor daemon", file=sys.stderr)
|
print("Error while reading fingerprint from Tor daemon", file=sys.stderr)
|
||||||
sys.exit(-1)
|
sys.exit(1)
|
||||||
|
|
||||||
response = controller.get_network_status(fingerprint, None)
|
response = controller.get_network_status(fingerprint, None)
|
||||||
if response is None:
|
if response is None:
|
||||||
print("Error while getting server descriptor from Tor daemon", file=sys.stderr)
|
print("Error while getting server descriptor from Tor daemon", file=sys.stderr)
|
||||||
sys.exit(-1)
|
sys.exit(1)
|
||||||
for flag in stem.Flag:
|
for flag in stem.Flag:
|
||||||
if flag in response.flags:
|
if flag in response.flags:
|
||||||
print('{}.value 1'.format(flag))
|
print('{}.value 1'.format(flag))
|
||||||
|
@ -376,7 +418,7 @@ class TorRouters(TorPlugin):
|
||||||
graph = {'title': 'Routers',
|
graph = {'title': 'Routers',
|
||||||
'args': '-l 0',
|
'args': '-l 0',
|
||||||
'vlabel': 'routers',
|
'vlabel': 'routers',
|
||||||
'category': 'Tor',
|
'category': 'network',
|
||||||
'info': 'known Tor onion routers'}
|
'info': 'known Tor onion routers'}
|
||||||
labels = {'routers': {'label': 'routers', 'min': 0, 'type': 'GAUGE'} }
|
labels = {'routers': {'label': 'routers', 'min': 0, 'type': 'GAUGE'} }
|
||||||
|
|
||||||
|
@ -395,7 +437,7 @@ class TorRouters(TorPlugin):
|
||||||
response = controller.get_info('ns/all', None)
|
response = controller.get_info('ns/all', None)
|
||||||
if response is None:
|
if response is None:
|
||||||
print("Error while reading ns/all from Tor daemon", file=sys.stderr)
|
print("Error while reading ns/all from Tor daemon", file=sys.stderr)
|
||||||
sys.exit(-1)
|
sys.exit(1)
|
||||||
else:
|
else:
|
||||||
routers = response.split('\n')
|
routers = response.split('\n')
|
||||||
onr = 0
|
onr = 0
|
||||||
|
@ -414,7 +456,7 @@ class TorTraffic(TorPlugin):
|
||||||
graph = {'title': 'Traffic',
|
graph = {'title': 'Traffic',
|
||||||
'args': '-l 0 --base 1024',
|
'args': '-l 0 --base 1024',
|
||||||
'vlabel': 'data',
|
'vlabel': 'data',
|
||||||
'category': 'Tor',
|
'category': 'network',
|
||||||
'info': 'bytes read/written'}
|
'info': 'bytes read/written'}
|
||||||
labels = {'read': {'label': 'read', 'min': 0, 'type': 'DERIVE'},
|
labels = {'read': {'label': 'read', 'min': 0, 'type': 'DERIVE'},
|
||||||
'written': {'label': 'written', 'min': 0, 'type': 'DERIVE'}}
|
'written': {'label': 'written', 'min': 0, 'type': 'DERIVE'}}
|
||||||
|
@ -432,14 +474,14 @@ class TorTraffic(TorPlugin):
|
||||||
response = controller.get_info('traffic/read', None)
|
response = controller.get_info('traffic/read', None)
|
||||||
if response is None:
|
if response is None:
|
||||||
print("Error while reading traffic/read from Tor daemon", file=sys.stderr)
|
print("Error while reading traffic/read from Tor daemon", file=sys.stderr)
|
||||||
sys.exit(-1)
|
sys.exit(1)
|
||||||
|
|
||||||
print('read.value {}'.format(response))
|
print('read.value {}'.format(response))
|
||||||
|
|
||||||
response = controller.get_info('traffic/written', None)
|
response = controller.get_info('traffic/written', None)
|
||||||
if response is None:
|
if response is None:
|
||||||
print("Error while reading traffic/write from Tor daemon", file=sys.stderr)
|
print("Error while reading traffic/write from Tor daemon", file=sys.stderr)
|
||||||
sys.exit(-1)
|
sys.exit(1)
|
||||||
print('written.value {}'.format(response))
|
print('written.value {}'.format(response))
|
||||||
|
|
||||||
|
|
||||||
|
@ -477,15 +519,16 @@ def main():
|
||||||
elif __file__.endswith('_traffic'):
|
elif __file__.endswith('_traffic'):
|
||||||
provider = TorTraffic()
|
provider = TorTraffic()
|
||||||
else:
|
else:
|
||||||
print('Unknown plugin name, try "suggest" for a list of possible ones.')
|
print('Unknown plugin name, try "suggest" for a list of possible ones.', file=sys.stderr)
|
||||||
sys.exit()
|
sys.exit(1)
|
||||||
|
|
||||||
if param == 'config':
|
if param == 'config':
|
||||||
provider.conf()
|
provider.conf()
|
||||||
elif param == 'fetch':
|
elif param == 'fetch':
|
||||||
provider.fetch()
|
provider.fetch()
|
||||||
else:
|
else:
|
||||||
print('Unknown parameter "{}"'.format(param))
|
print('Unknown parameter "{}"'.format(param), file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
main()
|
main()
|
||||||
|
|
Loading…
Reference in a new issue