summaryrefslogtreecommitdiff
path: root/python/vyos
diff options
context:
space:
mode:
authorChristian Poessinger <christian@poessinger.com>2020-04-22 07:38:23 +0200
committerGitHub <noreply@github.com>2020-04-22 07:38:23 +0200
commit675f400bacb03ae93be928e7270f89205d1036b9 (patch)
treeda3ba7ff0148fec1ad912f517ee6d97ca7604287 /python/vyos
parent43e606d88fd88f4d82720cb74b721903a4ebe9f3 (diff)
parent1169b4298bb8bce5cb36f8fa8601d63c30abe67b (diff)
downloadvyos-1x-675f400bacb03ae93be928e7270f89205d1036b9.tar.gz
vyos-1x-675f400bacb03ae93be928e7270f89205d1036b9.zip
Merge pull request #360 from thomas-mangin/T2186-syslog
airbag: T2186: generic syslog and better text
Diffstat (limited to 'python/vyos')
-rw-r--r--python/vyos/airbag.py60
-rw-r--r--python/vyos/logger.py143
-rw-r--r--python/vyos/util.py35
-rw-r--r--python/vyos/version.py66
4 files changed, 261 insertions, 43 deletions
diff --git a/python/vyos/airbag.py b/python/vyos/airbag.py
index b0565192d..6698aa404 100644
--- a/python/vyos/airbag.py
+++ b/python/vyos/airbag.py
@@ -13,17 +13,14 @@
# You should have received a copy of the GNU Lesser General Public
# License along with this library. If not, see <http://www.gnu.org/licenses/>.
-import os
import sys
-import logging
-import logging.handlers
from datetime import datetime
from vyos import debug
from vyos.config import Config
+from vyos.logger import syslog
from vyos.version import get_version
-from vyos.util import run
-
+from vyos.version import get_full_version_data
# we allow to disable the extra logging
DISABLE = False
@@ -59,12 +56,14 @@ def bug_report(dtype, value, trace):
sys.stdout.flush()
sys.stderr.flush()
- information = {
+ information = get_full_version_data()
+ trace = '\n'.join(format_exception(dtype, value, trace)).replace('\n\n','\n')
+
+ information.update({
'date': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
- 'version': get_version(),
- 'trace': format_exception(dtype, value, trace),
+ 'trace': trace,
'instructions': COMMUNITY if 'rolling' in get_version() else SUPPORTED,
- }
+ })
sys.stdout.write(INTRO.format(**information))
sys.stdout.flush()
@@ -82,19 +81,14 @@ def intercepter(dtype, value, trace):
pdb.pm()
-def InterceptingLogger(address, _singleton=[False]):
+def InterceptingLogger(_singleton=[False]):
skip = _singleton.pop()
_singleton.append(True)
if skip:
return
- logger = logging.getLogger('VyOS')
- logger.setLevel(logging.DEBUG)
- handler = logging.handlers.SysLogHandler(address='/dev/log', facility='syslog')
- logger.addHandler(handler)
-
# log to syslog any message sent to stderr
- sys.stderr = _IO(sys.stderr, logger.critical)
+ sys.stderr = _IO(sys.stderr, syslog.critical)
# lists as default arguments in function is normally dangerous
@@ -124,15 +118,31 @@ except:
# running testing so we are checking that we are on the router
# as otherwise it prevents dpkg-buildpackage to work
if get_version() and insession:
- InterceptingLogger('/run/systemd/journal/dev-log')
+ InterceptingLogger()
InterceptingException(intercepter)
# Messages to print
+# if the key before the value has not time, syslog takes that as the source of the message
FAULT = """\
-Date: {date}
-VyOS image: {version}
+Report Time: {date}
+Image Version: VyOS {version}
+Release Train: {release_train}
+
+Built by: {built_by}
+Built on: {built_on}
+Build UUID: {build_uuid}
+Build Commit ID: {build_git}
+
+Architecture: {system_arch}
+Boot via: {boot_via}
+System type: {system_type}
+
+Hardware vendor: {hardware_vendor}
+Hardware model: {hardware_model}
+Hardware S/N: {hardware_serial}
+Hardware UUID: {hardware_uuid}
{trace}
"""
@@ -140,13 +150,13 @@ VyOS image: {version}
INTRO = """\
VyOS had an issue completing a command.
-We are sorry that you encountered a problem with VyOS.
+We are sorry that you encountered a problem while using VyOS.
There are a few things you can do to help us (and yourself):
{instructions}
-PLEASE, when reporting, do include as much information as you can:
-- do not obfuscate any data (feel free to send us a private communication with
- the extra information if your business policy is strict on information sharing)
+When reporting problems, please include as much information as possible:
+- do not obfuscate any data (feel free to contact us privately if your
+ business policy requires it)
- and include all the information presented below
"""
@@ -163,6 +173,8 @@ COMMUNITY = """\
SUPPORTED = """\
- Make sure you are running the latest stable version of VyOS
the code is available at https://downloads.vyos.io/?dir=release/current
-- Contact us on our online help desk
+- Contact us using the online help desk
https://support.vyos.io/
+- Join our community on slack where our users exchange help and advice
+ https://vyos.slack.com
""".strip()
diff --git a/python/vyos/logger.py b/python/vyos/logger.py
new file mode 100644
index 000000000..f7cc964d5
--- /dev/null
+++ b/python/vyos/logger.py
@@ -0,0 +1,143 @@
+# Copyright 2020 VyOS maintainers and contributors <maintainers@vyos.io>
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library 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
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this library. If not, see <http://www.gnu.org/licenses/>.
+
+# A wrapper class around logging to make it easier to use
+
+# for a syslog logger:
+# from vyos.logger import syslog
+# syslog.critical('message')
+
+# for a stderr logger:
+# from vyos.logger import stderr
+# stderr.critical('message')
+
+# for a custom logger (syslog and file):
+# from vyos.logger import getLogger
+# combined = getLogger(__name__, syslog=True, stream=sys.stdout, filename='/tmp/test')
+# combined.critical('message')
+
+import sys
+import logging
+import logging.handlers as handlers
+
+TIMED = '%(asctime)s: %(message)s'
+SHORT = '%(filename)s: %(message)s'
+CLEAR = '%(levelname) %(asctime)s %(filename)s: %(message)s'
+
+_levels = {
+ 'CRITICAL': logging.CRITICAL,
+ 'ERROR': logging.CRITICAL,
+ 'WARNING': logging.WARNING,
+ 'INFO': logging.INFO,
+ 'DEBUG': logging.DEBUG,
+ 'NOTSET': logging.NOTSET,
+}
+
+# prevent recreation of already created logger
+_created = {}
+
+def getLogger(name=None, **kwargs):
+ if name in _created:
+ if len(kwargs) == 0:
+ return _created[name]
+ raise ValueError('a logger with the name "{name} already exists')
+
+ logger = logging.getLogger(name)
+ logger.setLevel(_levels[kwargs.get('level', 'DEBUG')])
+
+ if 'address' in kwargs or kwargs.get('syslog', False):
+ logger.addHandler(_syslog(**kwargs))
+ if 'stream' in kwargs:
+ logger.addHandler(_stream(**kwargs))
+ if 'filename' in kwargs:
+ logger.addHandler(_file(**kwargs))
+
+ _created[name] = logger
+ return logger
+
+
+def _syslog(**kwargs):
+ formating = kwargs.get('format', SHORT)
+ handler = handlers.SysLogHandler(
+ address=kwargs.get('address', '/dev/log'),
+ facility=kwargs.get('facility', 'syslog'),
+ )
+ handler.setFormatter(logging.Formatter(formating))
+ return handler
+
+
+def _stream(**kwargs):
+ formating = kwargs.get('format', CLEAR)
+ handler = logging.StreamHandler(
+ stream=kwargs.get('stream', sys.stderr),
+ )
+ handler.setFormatter(logging.Formatter(formating))
+ return handler
+
+
+def _file(**kwargs):
+ formating = kwargs.get('format', CLEAR)
+ handler = handlers.RotatingFileHandler(
+ filename=kwargs.get('filename', 1048576),
+ maxBytes=kwargs.get('maxBytes', 1048576),
+ backupCount=kwargs.get('backupCount', 3),
+ )
+ handler.setFormatter(logging.Formatter(formating))
+ return handler
+
+
+# exported pre-built logger, please keep in mind that the names
+# must be unique otherwise the logger are shared
+
+# a logger for stderr
+stderr = getLogger(
+ 'VyOS Syslog',
+ format=SHORT,
+ stream=sys.stderr,
+ address='/dev/log'
+)
+
+# a logger to syslog
+syslog = getLogger(
+ 'VyOS StdErr',
+ format='%(message)s',
+ address='/dev/log'
+)
+
+
+# testing
+if __name__ == '__main__':
+ # from vyos.logger import getLogger
+ formating = '%(asctime)s (%(filename)s) %(levelname)s: %(message)s'
+
+ # syslog logger
+ # syslog=True if no 'address' field is provided
+ syslog = getLogger(__name__ + '.1', syslog=True, format=formating)
+ syslog.info('syslog test')
+
+ # steam logger
+ stream = getLogger(__name__ + '.2', stream=sys.stdout, level='ERROR')
+ stream.info('steam test')
+
+ # file logger
+ filelog = getLogger(__name__ + '.3', filename='/tmp/test')
+ filelog.info('file test')
+
+ # create a combined logger
+ getLogger('VyOS', syslog=True, stream=sys.stdout, filename='/tmp/test')
+
+ # recover the created logger from name
+ combined = getLogger('VyOS')
+ combined.info('combined test')
diff --git a/python/vyos/util.py b/python/vyos/util.py
index 3d4f1c42f..4340332d3 100644
--- a/python/vyos/util.py
+++ b/python/vyos/util.py
@@ -160,11 +160,36 @@ def call(command, flag='', shell=None, input=None, timeout=None, env=None,
return code
-def read_file(path):
- """ Read a file to string """
- with open(path, 'r') as f:
- data = f.read().strip()
- return data
+def read_file(fname, defaultonfailure=None):
+ """
+ read the content of a file, stripping any end characters (space, newlines)
+ should defaultonfailure be not None, it is returned on failure to read
+ """
+ try:
+ """ Read a file to string """
+ with open(fname, 'r') as f:
+ data = f.read().strip()
+ return data
+ except Exception as e:
+ if defaultonfailure is not None:
+ return defaultonfailure
+ raise e
+
+
+def read_json(fname, defaultonfailure=None):
+ """
+ read and json decode the content of a file
+ should defaultonfailure be not None, it is returned on failure to read
+ """
+ import json
+ try:
+ with open(fname, 'r') as f:
+ data = json.load(f)
+ return data
+ except Exception as e:
+ if defaultonfailure is not None:
+ return defaultonfailure
+ raise e
def chown(path, user, group):
diff --git a/python/vyos/version.py b/python/vyos/version.py
index d51a940d6..a524b36ea 100644
--- a/python/vyos/version.py
+++ b/python/vyos/version.py
@@ -34,9 +34,17 @@ import json
import vyos.defaults
+from vyos.util import read_file
+from vyos.util import read_json
+from vyos.util import popen
+from vyos.util import run
+from vyos.util import DEVNULL
+
+
version_file = os.path.join(vyos.defaults.directories['data'], 'version.json')
-def get_version_data(file=version_file):
+
+def get_version_data(fname=version_file):
"""
Get complete version data
@@ -52,20 +60,50 @@ def get_version_data(file=version_file):
is an implementation detail and may change in the future, while the interface
of this module will stay the same.
"""
- try:
- with open(file, 'r') as f:
- version_data = json.load(f)
- return version_data
- except FileNotFoundError:
- return {}
+ return read_json(fname, {})
-def get_version(file=None):
+
+def get_version(fname=version_file):
"""
Get the version number, or an empty string if it could not be determined
"""
- version_data = None
- if file:
- version_data = get_version_data(file=file)
- else:
- version_data = get_version_data()
- return version_data.get('version','')
+ return get_version_data(fname=fname).get('version', '')
+
+
+def get_full_version_data(fname=version_file):
+ version_data = get_version_data(fname)
+
+ # Get system architecture (well, kernel architecture rather)
+ version_data['system_arch'], _ = popen('uname -m', stderr=DEVNULL)
+
+ # Get hypervisor name, if any
+ try:
+ hypervisor, _ = popen('hvinfo', stderr=DEVNULL)
+ version_data['system_type'] = f"{hypervisor} guest"
+ except OSError:
+ # hvinfo returns 1 if it cannot detect any hypervisor
+ version_data['system_type'] = 'bare metal'
+
+ # Get boot type, it can be livecd, installed image, or, possible, a system installed
+ # via legacy "install system" mechanism
+ # In installed images, the squashfs image file is named after its image version,
+ # while on livecd it's just "filesystem.squashfs", that's how we tell a livecd boot
+ # from an installed image
+ boot_via = "installed image"
+ if run(""" grep -e '^overlay.*/filesystem.squashfs' /proc/mounts >/dev/null""") == 0:
+ boot_via = "livecd"
+ elif run(""" grep '^overlay /' /proc/mounts >/dev/null """) != 0:
+ boot_via = "legacy non-image installation"
+ version_data['boot_via'] = boot_via
+
+ # Get hardware details from DMI
+ dmi = '/sys/class/dmi/id'
+ version_data['hardware_vendor'] = read_file(dmi + '/sys_vendor', 'Unknown')
+ version_data['hardware_model'] = read_file(dmi +'/product_name','Unknown')
+
+ # These two assume script is run as root, normal users can't access those files
+ subsystem = '/sys/class/dmi/id/subsystem/id'
+ version_data['hardware_serial'] = read_file(subsystem + '/product_serial','Unknown')
+ version_data['hardware_uuid'] = read_file(subsystem + '/product_uuid', 'Unknown')
+
+ return version_data