v3bwfile.py 7.54 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
from statistics import median
juga's avatar
juga committed
7

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

log = logging.getLogger(__name__)

15
LINE_SEP = '\n'
16
17
18
19
20
21
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
22
UNORDERED_KEYVALUES = EXTRA_ARG_KEYVALUES + ['latest_bandwidth']
23
24
# List of all the KeyValues currently being used to generate the file
ALL_KEYVALUES = ['version'] + UNORDERED_KEYVALUES
25
TERMINATOR = '===='
juga's avatar
juga committed
26
27
# Num header lines in v1.1.0 using all the KeyValues
NUM_LINES_HEADER_V110 = len(ALL_KEYVALUES) + 2
28
29
LINE_TERMINATOR = TERMINATOR + LINE_SEP

juga's avatar
juga committed
30

juga's avatar
juga committed
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
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
49
50
51
52
53
class V3BwHeader(object):
    """
    Create a bandwidth measurements (V3bw) header
    following bandwidth measurements document spec version 1.1.0.

54
    :param str timestamp: timestamp in Unix Epoch seconds of the most recent
55
        generator result.
juga's avatar
juga committed
56
57
58
    :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
59
    :param dict kwargs: extra headers. Currently supported:
60
61
62
63
        - 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
64
    """
65
    def __init__(self, timestamp, **kwargs):
juga's avatar
juga committed
66
67
68
69
        assert isinstance(timestamp, str)
        for v in kwargs.values():
            assert isinstance(v, str)
        self.timestamp = timestamp
70
71
72
73
        # 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__)
74
        self.file_created = kwargs.get('file_created', now_isodt_str())
juga's avatar
juga committed
75
        # latest_bandwidth should not be in kwargs, since it MUST be the
76
        # same as timestamp
juga's avatar
juga committed
77
        self.latest_bandwidth = unixts_to_isodt_str(timestamp)
78
79
        [setattr(self, k, v) for k, v in kwargs.items()
         if k in EXTRA_ARG_KEYVALUES]
juga's avatar
juga committed
80

81
    @property
82
83
84
85
86
87
88
    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
89
90

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

    @property
96
97
98
99
100
101
    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
102
103
104
105

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

    @property
112
113
114
115
116
117
    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
118
119
120
121

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

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

132
133
134
135
136
137
138
139
140
141
142
143
144
    @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
145
        ts = lines[0]
146
        # not checking order
147
        kwargs = dict([l.split(KEYVALUE_SEP_V110)
148
                       for l in lines[:index_terminator]
149
                       if l.split(KEYVALUE_SEP_V110)[0] in ALL_KEYVALUES])
150
151
152
153
154
155
        h = cls(ts, **kwargs)
        return h, lines[index_terminator + 1:]

    @classmethod
    def from_text_v110(self, text):
        """
156
        :param str text: text to parse
157
158
159
160
        :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
161
162
163
164
165
166
167
168
169
170

    @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
171
    def latest_bandwidth_from_results(results):
juga's avatar
juga committed
172
        return round(max([r.time for fp in results for r in results[fp]]))
juga's avatar
juga committed
173
174
175

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

    @classmethod
    def from_results(cls, conf, results):
        kwargs = dict()
juga's avatar
juga committed
181
182
        latest_bandwidth = cls.latest_bandwidth_from_results(results)
        earliest_bandwidth = cls.latest_bandwidth_from_results(results)
juga's avatar
juga committed
183
        generator_started = cls.generator_started_from_file(conf)
juga's avatar
juga committed
184
185
        timestamp = str(latest_bandwidth)
        kwargs['latest_bandwidth'] = unixts_to_isodt_str(latest_bandwidth)
juga's avatar
juga committed
186
187
188
189
        kwargs['earliest_bandwidth'] = unixts_to_isodt_str(earliest_bandwidth)
        kwargs['generator_started'] = generator_started
        h = cls(timestamp, **kwargs)
        return h
juga's avatar
juga committed
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206


class V3BWLine:
    def __init__(self, fp, bw, nick, rtts, last_time):
        self.fp = fp
        self.nick = nick
        # convert to KiB and make sure the answer is at least 1
        self.bw = max(round(bw / 1024), 1)
        # convert to ms
        rtts = [round(r * 1000) for r in rtts]
        self.rtt = round(median(rtts))
        self.time = last_time

    def __str__(self):
        frmt = 'node_id=${fp} bw={sp} nick={n} rtt={rtt} time={t}'
        return frmt.format(fp=self.fp, sp=self.bw, n=self.nick, rtt=self.rtt,
                           t=self.time)