v3bwfile.py 6.95 KB
Newer Older
juga's avatar
juga committed
1
2
3
4
5
# -*- coding: utf-8 -*-
"""Classes and functions that create the bandwidth measurements document
(v3bw) used by bandwidth authorities."""

import logging
juga's avatar
juga committed
6

7
8
from sbws import __version__
from sbws.globals import SPEC_VERSION
juga's avatar
juga committed
9
from sbws.util.filelock import FileLock
10
from sbws.util.timestamp import now_isodt_str, unixts_to_isodt_str
juga's avatar
juga committed
11
12
13

log = logging.getLogger(__name__)

14
LINE_SEP = '\n'
15
16
17
18
19
20
KEYVALUE_SEP_V110 = '='
KEYVALUE_SEP_V200 = ' '
# List of the extra KeyValues accepted by the class
EXTRA_ARG_KEYVALUES = ['software', 'software_version', 'file_created',
                       'earliest_bandwidth', 'generator_started']
# List of all unordered KeyValues currently being used to generate the file
juga's avatar
juga committed
21
UNORDERED_KEYVALUES = EXTRA_ARG_KEYVALUES + ['latest_bandwidth']
22
23
# List of all the KeyValues currently being used to generate the file
ALL_KEYVALUES = ['version'] + UNORDERED_KEYVALUES
24
TERMINATOR = '===='
juga's avatar
juga committed
25
26
# Num header lines in v1.1.0 using all the KeyValues
NUM_LINES_HEADER_V110 = len(ALL_KEYVALUES) + 2
27
28
LINE_TERMINATOR = TERMINATOR + LINE_SEP

juga's avatar
juga committed
29

juga's avatar
juga committed
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
def read_started_ts(conf):
    """Read ISO formated timestamp which represents the date and time
    when scanner started.

    :param ConfigParser conf: configuration
    :returns: str, ISO formated timestamp
    """
    filepath = conf['paths']['started_filepath']
    try:
        with FileLock(filepath):
            with open(filepath, 'r') as fd:
                generator_started = fd.read()
    except FileNotFoundError as e:
        log.warn('File %s not found.%s', filepath, e)
        return ''
    return generator_started


juga's avatar
juga committed
48
49
50
51
52
class V3BwHeader(object):
    """
    Create a bandwidth measurements (V3bw) header
    following bandwidth measurements document spec version 1.1.0.

53
    :param str timestamp: timestamp in Unix Epoch seconds of the most recent
54
        generator result.
juga's avatar
juga committed
55
56
57
    :param str version: the spec version
    :param str software: the name of the software that generates this
    :param str software_version: the version of the software
58
    :param dict kwargs: extra headers. Currently supported:
59
60
61
62
        - earliest_bandwidth: str, ISO 8601 timestamp in UTC time zone
          when the first bandwidth was obtained
        - generator_started: str, ISO 8601 timestamp in UTC time zone
          when the generator started
juga's avatar
juga committed
63
    """
64
    def __init__(self, timestamp, **kwargs):
juga's avatar
juga committed
65
66
67
68
        assert isinstance(timestamp, str)
        for v in kwargs.values():
            assert isinstance(v, str)
        self.timestamp = timestamp
69
70
71
72
        # KeyValues with default value when not given by kwargs
        self.version = kwargs.get('version', SPEC_VERSION)
        self.software = kwargs.get('software', 'sbws')
        self.software_version = kwargs.get('software_version', __version__)
73
        self.file_created = kwargs.get('file_created', now_isodt_str())
juga's avatar
juga committed
74
        # latest_bandwidth should not be in kwargs, since it MUST be the
75
        # same as timestamp
juga's avatar
juga committed
76
        self.latest_bandwidth = unixts_to_isodt_str(timestamp)
77
78
        [setattr(self, k, v) for k, v in kwargs.items()
         if k in EXTRA_ARG_KEYVALUES]
juga's avatar
juga committed
79

80
    @property
81
82
83
84
85
86
87
    def keyvalue_unordered_tuple_ls(self):
        """Return list of KeyValue tuples that do not have specific order."""
        # sort the list to generate determinist headers
        keyvalue_tuple_ls = sorted([(k, v) for k, v in self.__dict__.items()
                                    if k in UNORDERED_KEYVALUES])
        log.debug('keyvalue_tuple_ls %s', keyvalue_tuple_ls)
        return keyvalue_tuple_ls
88
89

    @property
90
91
92
    def keyvalue_tuple_ls(self):
        """Return list of all KeyValue tuples"""
        return [('version', self.version)] + self.keyvalue_unordered_tuple_ls
93
94

    @property
95
96
97
98
99
100
    def keyvalue_v110str_ls(self):
        """Return KeyValue list of strings following spec v1.1.0."""
        keyvalues = [self.timestamp] + [KEYVALUE_SEP_V110.join([k, v])
                                        for k, v in self.keyvalue_tuple_ls]
        log.debug('keyvalue %s', keyvalues)
        return keyvalues
101
102
103
104

    @property
    def strv110(self):
        """Return header string following spec v1.1.0."""
105
        header_str = LINE_SEP.join(self.keyvalue_v110str_ls) + LINE_SEP + \
106
107
108
109
110
            LINE_TERMINATOR
        log.debug('header_str %s', header_str)
        return header_str

    @property
111
112
113
114
115
116
    def keyvalue_v200_ls(self):
        """Return KeyValue list of strings following spec v2.0.0."""
        keyvalue = [self.timestamp] + [KEYVALUE_SEP_V200.join([k, v])
                                       for k, v in self.keyvalue_tuple_ls]
        log.debug('keyvalue %s', keyvalue)
        return keyvalue
117
118
119
120

    @property
    def strv200(self):
        """Return header string following spec v2.0.0."""
121
        header_str = LINE_SEP.join(self.keyvalue_v200_ls) + LINE_SEP + \
122
123
124
125
            LINE_TERMINATOR
        log.debug('header_str %s', header_str)
        return header_str

126
127
128
129
130
    def __str__(self):
        if self.version == '1.1.0':
            return self.strv110
        return self.strv200

131
132
133
134
135
136
137
138
139
140
141
142
143
    @classmethod
    def from_lines_v110(cls, lines):
        """
        :param list lines: list of lines to parse
        :returns: tuple of V3BwHeader object and non-header lines
        """
        assert isinstance(lines, list)
        try:
            index_terminator = lines.index(TERMINATOR)
        except ValueError as e:
            # is not a bw file or is v100
            log.warn('Terminator is not in lines')
            return None
144
        ts = lines[0]
145
        # not checking order
146
        kwargs = dict([l.split(KEYVALUE_SEP_V110)
147
                       for l in lines[:index_terminator]
148
                       if l.split(KEYVALUE_SEP_V110)[0] in ALL_KEYVALUES])
149
150
151
152
153
154
        h = cls(ts, **kwargs)
        return h, lines[index_terminator + 1:]

    @classmethod
    def from_text_v110(self, text):
        """
155
        :param str text: text to parse
156
157
158
159
        :returns: tuple of V3BwHeader object and non-header lines
        """
        assert isinstance(text, str)
        return self.from_lines_v110(text.split(LINE_SEP))
juga's avatar
juga committed
160
161
162
163
164
165
166
167
168
169

    @property
    def num_lines(self):
        return len(self.__str__().split(LINE_SEP))

    @staticmethod
    def generator_started_from_file(conf):
        return read_started_ts(conf)

    @staticmethod
juga's avatar
juga committed
170
    def latest_bandwidth_from_results(results):
juga's avatar
juga committed
171
        return round(max([r.time for fp in results for r in results[fp]]))
juga's avatar
juga committed
172
173
174

    @staticmethod
    def earliest_bandwidth_from_results(results):
juga's avatar
juga committed
175
        return round(min([r.time for fp in results for r in results[fp]]))
juga's avatar
juga committed
176
177
178
179

    @classmethod
    def from_results(cls, conf, results):
        kwargs = dict()
juga's avatar
juga committed
180
181
        latest_bandwidth = cls.latest_bandwidth_from_results(results)
        earliest_bandwidth = cls.latest_bandwidth_from_results(results)
juga's avatar
juga committed
182
        generator_started = cls.generator_started_from_file(conf)
juga's avatar
juga committed
183
184
        timestamp = str(latest_bandwidth)
        kwargs['latest_bandwidth'] = unixts_to_isodt_str(latest_bandwidth)
juga's avatar
juga committed
185
186
187
188
        kwargs['earliest_bandwidth'] = unixts_to_isodt_str(earliest_bandwidth)
        kwargs['generator_started'] = generator_started
        h = cls(timestamp, **kwargs)
        return h