blob: 5b46710fa745882a08c1b988f2385eada5e27f94 [file] [log] [blame]
#!/usr/bin/python
# Copyright 2011 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# TR-069 has mandatory attribute names that don't comply with policy
# pylint:disable=invalid-name
#
"""Implement logging for cwmpd."""
import os
import xml.etree.cElementTree as ET
SUPPRESSLIST = frozenset(['ParameterNames', 'string', 'ParameterList',
'ParameterValueStruct', 'DeviceId', 'Event',
'EventStruct', 'detail', 'Header', 'Body',
'SetParameterAttributesStruct'])
PRUNELIST = frozenset(['MaxEnvelopes', 'ParameterKey', 'CommandKey',
'Manufacturer', 'OUI', 'ProductClass',
'HoldRequests'])
TRUNCATELIST = frozenset(['GetParameterValues', 'GetParameterValuesResponse',
'GetParameterNamesResponse', 'SetParameterAttributes',
'X_CATAWAMPUS_ORG_AddObjects',
'X_CATAWAMPUS_ORG_AddObjectsResponse'])
def _Shorten(s, prefixofs, suffixofs, maxlen):
"""Shorten the given string if its length is >= maxlen.
Note: maxlen should generally be considerably bigger than
prefixofs + suffixofs. It's disconcerting to a reader when
you have a "..." to replace 10 bytes, but it feels fine when the
"..." replaces 500 bytes.
Args:
s: the string to shorten.
prefixofs: the number of chars to keep at the beginning of s.
suffixofs: the number of chars to keep at the end of s.
maxlen: if the string is longer than this, shorten it.
Returns:
A shortened version of the string.
"""
s = str(s)
if len(s) >= maxlen:
# When the string exceeds the limit, we deliberately shorten it to
# considerably less than the limit, because it's disconcerting when
# you have a "..." to replace 10 bytes, but it feels right when the
# "..." replaces 500 bytes.
s = s[0:prefixofs] + '.....' + s[-suffixofs:]
return s
def _StripNamespace(tag):
"""Remove Namespace from XML Tag.
ElementTree retains namespaces in the tag, like:
{urn:dslforum-org:cwmp-1-2}GetParameterNames
The namespace is not useful; suppress it.
Args:
tag: the tag name
Returns:
the tag with namespace removed, if there was one.
"""
if tag[0] == '{':
e = tag.find('}')
return tag[e + 1:]
else:
return tag
def _SuppressSensitiveParams(name, value):
"""Don't log passwords, and other sensitive information."""
if 'KeyPassphrase' in name or 'WEPKey' in name or 'Password' in name:
value = 'XXXXXXXX'
return (name, value)
def _LogSoapETree(et, prefix=''):
"""Tersely log an ElementTree CWMP message.
Example output:
ID: google.acs.1370212049608.571675
SetParameterValues:
InternetGatewayDevice.ManagementServer.PeriodicInformEnable = true
InternetGatewayDevice.ManagementServer.PeriodicInformInterval = 60
Args:
et: an ElementTree node
prefix: a string to prepend to each line (generally indentation)
Returns:
the string to log.
"""
if len(et) == 2 and et[0].tag == 'Name' and et[1].tag == 'Value':
# change:
# Name: InternetGatewayDevice.Foo.Bar
# Value: 60
# to:
# InternetGatewayDevice.Foo.Bar = 60
text0 = et[0].text and et[0].text.strip()
name = _Shorten(text0, 8, 32, 64)
text1 = et[1].text and et[1].text.strip()
value = _Shorten(text1, 16, 64, 192)
(name, value) = _SuppressSensitiveParams(name, value)
return '%s%s = %s\n' % (prefix, name, value)
if et.text and et.text.strip():
return '%s%s\n' % (prefix, et.text.strip())
out = ''
for child in et:
tag = _StripNamespace(child.tag)
if tag in PRUNELIST:
et.remove(child)
continue
if tag in SUPPRESSLIST:
# Don't include the boring keyword, just its children.
out += '%s' % _LogSoapETree(child, prefix)
elif child.text and child.text.strip():
# Child is a leaf node; all on one line
out += '%s%s: %s\n' % (prefix, tag, child.text.strip())
else:
body = _LogSoapETree(child, prefix + ' ')
if tag in TRUNCATELIST:
lines = body.splitlines()
numlines = len(lines)
if numlines > 8:
lines = lines[0:3]
lines.append('%s...%d more lines...' % (prefix + ' ', numlines - 3))
body = '\n'.join(lines)
out += '%s%s:\n%s' % (prefix, tag, body)
return out
class Logger(object):
"""CWMP-specific logging support.
This class logs XML logs for the first few sessions after
reboot, trying to capture the initial GetParameterValues
exchanges and an AddObject or two. Then it switches to a
much more condensed logging format.
"""
def __init__(self, full_logs=10):
self.num_full_logs = full_logs
def LogSoapXML(self, xml):
if not xml or not xml.strip():
# An empty message is valid, means connection is closing.
return ''
# Developers can ask for full XML logs.
if os.environ.get('DONT_SHORTEN'):
return str(xml)
if self.num_full_logs > 0:
self.num_full_logs -= 1
return self.LogXML(xml)
else:
return self.LogTerse(xml)
def LogXML(self, xml):
return _Shorten(str(xml), 1024, 512, 3072)
def LogTerse(self, xml):
try:
return _LogSoapETree(ET.fromstring(xml))
except: # Never, ever kill catawampus for this. pylint:disable=bare-except
print 'Unable to parse XML for logging.'
return str(xml)