Templating: working implementation

- Includes a sample custom.jinja file.
- Supports any number of templates.

Change-Id: Iae199170ea9ab8c75258720070ea7e5f2200d8a2
This commit is contained in:
Ziad Sawalha 2014-03-11 14:20:33 -05:00
parent 4b77d11ffb
commit b80866bac5
7 changed files with 186 additions and 23 deletions

View File

@ -1,3 +1,4 @@
iso8601>=0.1.9
Jinja2
pbr>=0.5.21,<1.0
python-novaclient==2.15.0

View File

@ -16,12 +16,13 @@ import datetime
import logging
import socket
import dateutil.parser
import pythonwhois
from six.moves.urllib import parse as urlparse
import tldextract
from satori import errors
from satori import utils
LOG = logging.getLogger(__name__)
@ -64,9 +65,12 @@ def domain_info(domain):
if (isinstance(result['expiration_date'], list)
and len(result['expiration_date']) > 0):
expires = result['expiration_date'][0]
if not isinstance(expires, datetime.datetime):
expires = dateutil.parser.parse(expires)
days_until_expires = (expires - datetime.datetime.now()).days
if isinstance(expires, datetime.datetime):
days_until_expires = (expires - datetime.datetime.now()).days
expires = utils.get_time_string(time_obj=expires)
else:
days_until_expires = (utils.parse_time_string(expires) -
datetime.datetime.now()).days
return {
'name': domain,
'whois': result['raw'],

View File

@ -0,0 +1,23 @@
{#
This is a jinja template used to customize the output of satori. You can add
as many of those as you'd like to your setup. You can reference them using the
--format (or -F) argument. satori takes the format you asked for and appends
".jinja" to it to look up the file from this dirfectory.
You have some global variables available to you:
- target: that's the address or URL supplied at the command line
- data: all the discovery data
For example, to use this template:
$ satori openstack.org -F custo
Hi! localhost
Happy customizing :-)
#}
Hi! {{ target }}

View File

@ -23,6 +23,7 @@ findings back to the user.
from __future__ import print_function
import argparse
import json
import logging
import os
import sys
@ -108,10 +109,18 @@ def main():
)
# Output formatting
parser.add_argument(
'--format', '-F',
dest='format',
default='text',
help='Format for output (json or text)'
)
parser.add_argument(
"--logconfig",
help="Optional logging configuration file"
)
parser.add_argument(
"-d", "--debug",
action="store_true",
@ -156,9 +165,13 @@ def main():
"tenant. Either provide all of these settings or none of "
"them.")
if not (args.format == 'json' or check_format(args.format or "text")):
sys.exit("Output format file (%s) not found or accessible. Try "
"specifying raw JSON format using `--format json`" %
get_template_path(args.format))
try:
results = discovery.run(args.netloc, args)
print(format_output(args.netloc, results))
print(format_output(args.netloc, results, template_name=args.format))
except Exception as exc: # pylint: disable=W0703
if args.debug:
LOG.exception(exc)
@ -166,6 +179,18 @@ def main():
return 0
def get_template_path(name):
"""Get template path from name."""
root_dir = os.path.dirname(__file__)
return os.path.join(root_dir, "formats", "%s.jinja" % name)
def check_format(name):
"""Verify that we have the requested format template."""
template_path = get_template_path(name)
return os.path.exists(template_path)
def get_template(name):
"""Get template text fromtemplates directory by name."""
root_dir = os.path.dirname(__file__)
@ -175,10 +200,14 @@ def get_template(name):
return template
def format_output(discovered_target, results):
def format_output(discovered_target, results, template_name="text"):
"""Format results in CLI format."""
template = get_template("text")
return templating.parse(template, target=discovered_target, data=results)
if template_name == 'json':
return(json.dumps(results, indent=2))
else:
template = get_template(template_name)
return templating.parse(template, target=discovered_target,
data=results)
if __name__ == "__main__":

View File

@ -19,6 +19,7 @@ import unittest
from freezegun import freeze_time
import mock
import pythonwhois
import six
from satori import dns
from satori import errors
@ -234,7 +235,7 @@ class TestDNS(utils.TestCase):
data = dns.domain_info(self.domain)
self.assertIsInstance(data['whois'][0], str)
def test_domain_info_returns_datetime_for_expiry_string(self):
def test_domain_info_returns_string_date_for_expiry(self):
small_whois = ["""
Domain : example.io
Status : Live
@ -245,17 +246,11 @@ class TestDNS(utils.TestCase):
"""]
self.mynet.get_whois_raw.return_value = small_whois
data = dns.domain_info(self.domain)
self.assertIsInstance(
data['expiration_date'],
datetime.datetime
)
self.assertIsInstance(data['expiration_date'], six.string_types)
def test_domain_info_returns_datetime_for_expiration_date_string(self):
def test_domain_info_returns_string_for_expiration_date_string(self):
data = dns.domain_info(self.domain)
self.assertIsInstance(
data['expiration_date'],
datetime.datetime
)
self.assertIsInstance(data['expiration_date'], six.string_types)
def test_domain_info_returns_none_for_missing_expiration_date(self):
small_whois = ["""
@ -267,10 +262,7 @@ class TestDNS(utils.TestCase):
"""]
self.mynet.get_whois_raw.return_value = small_whois
data = dns.domain_info(self.domain)
self.assertEqual(
data['expiration_date'],
None
)
self.assertIsNone(data['expiration_date'])
if __name__ == "__main__":
unittest.main()

View File

@ -0,0 +1,73 @@
# 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.
"""Tests for utils module."""
import datetime
import time
import unittest
import mock
from satori import utils
class SomeTZ(datetime.tzinfo):
"""A random timezone."""
def utcoffset(self, dt):
return datetime.timedelta(minutes=45)
def tzname(self, dt):
return "STZ"
def dst(self, dt):
return datetime.timedelta(0)
class TestTimeUtils(unittest.TestCase):
"""Test time formatting functions."""
def test_get_formatted_time_string(self):
some_time = time.gmtime(0)
with mock.patch.object(utils.time, 'gmtime') as mock_gmt:
mock_gmt.return_value = some_time
result = utils.get_time_string()
self.assertEqual(result, "1970-01-01 00:00:00 +0000")
def test_get_formatted_time_string_time_struct(self):
result = utils.get_time_string(time_obj=time.gmtime(0))
self.assertEqual(result, "1970-01-01 00:00:00 +0000")
def test_get_formatted_time_string_datetime(self):
result = utils.get_time_string(
time_obj=datetime.datetime(1970, 2, 1, 1, 2, 3, 0))
self.assertEqual(result, "1970-02-01 01:02:03 +0000")
def test_get_formatted_time_string_datetime_tz(self):
result = utils.get_time_string(
time_obj=datetime.datetime(1970, 2, 1, 1, 2, 3, 0, SomeTZ()))
self.assertEqual(result, "1970-02-01 01:47:03 +0000")
def test_parse_time_string(self):
result = utils.parse_time_string("1970-02-01 01:02:03 +0000")
self.assertEqual(result, datetime.datetime(1970, 2, 1, 1, 2, 3, 0))
def test_parse_time_string_with_tz(self):
result = utils.parse_time_string("1970-02-01 01:02:03 +1000")
self.assertEqual(result, datetime.datetime(1970, 2, 1, 11, 2, 3, 0))
if __name__ == '__main__':
unittest.main()

View File

@ -10,12 +10,21 @@
# License for the specific language governing permissions and limitations
# under the License.
#
"""General utilities."""
"""General utilities.
- Class and module import/export
- Time utilities (we standardize on UTC)
"""
import datetime
import logging
import sys
import time
import iso8601
LOG = logging.getLogger(__name__)
STRING_FORMAT = "%Y-%m-%d %H:%M:%S +0000"
def import_class(import_str):
@ -37,3 +46,35 @@ def import_object(import_str, *args, **kw):
except ImportError:
cls = import_class(import_str)
return cls(*args, **kw)
def get_time_string(time_obj=None):
"""The canonical time string format (in UTC).
:param time_obj: an optional datetime.datetime or timestruct (defaults to
gm_time)
Note: Changing this function will change all times that this project uses
in the returned data.
"""
if isinstance(time_obj, datetime.datetime):
if time_obj.tzinfo:
offset = time_obj.tzinfo.utcoffset(time_obj)
utc_dt = time_obj + offset
return datetime.datetime.strftime(utc_dt, STRING_FORMAT)
return datetime.datetime.strftime(time_obj, STRING_FORMAT)
elif isinstance(time_obj, time.struct_time):
return time.strftime(STRING_FORMAT, time_obj)
elif time_obj is not None:
raise TypeError("get_time_string takes only a time_struct, none, or a "
"datetime. It was given a %s" % type(time_obj))
return time.strftime(STRING_FORMAT, time.gmtime())
def parse_time_string(time_string):
"""Return naive datetime object from string in standard time format."""
parsed = time_string.replace(" +", "+").replace(" -", "-")
dt_with_tz = iso8601.parse_date(parsed)
offset = dt_with_tz.tzinfo.utcoffset(dt_with_tz)
result = dt_with_tz + offset
return result.replace(tzinfo=None)