Skip to content

Commit 14692fc

Browse files
authored
Adds bandwidth report via slcli report bandwidth (softlayer#745)
* Adds bandwidth report via `slcli report bandwidth` * Fixes lint issues * Adds a test for `slcli report bandwidth` * Adds tests for invalid dates with slcli report bandwidth * Resolves style issue * Resolves issue with formatting bytes. Adds sort option
1 parent 889b0f8 commit 14692fc

7 files changed

Lines changed: 440 additions & 4 deletions

File tree

SoftLayer/CLI/report/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Reports."""

SoftLayer/CLI/report/bandwidth.py

Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
"""Metric Utilities"""
2+
from __future__ import print_function
3+
import datetime
4+
import itertools
5+
import sys
6+
7+
import click
8+
9+
from SoftLayer.CLI import environment
10+
from SoftLayer.CLI import formatting
11+
from SoftLayer import utils
12+
13+
14+
# pylint: disable=unused-argument
15+
def _validate_datetime(ctx, param, value):
16+
try:
17+
return datetime.datetime.strptime(value, "%Y-%m-%d")
18+
except (ValueError, TypeError):
19+
pass
20+
21+
try:
22+
return datetime.datetime.strptime(value, "%Y-%m-%d %H:%M:%S")
23+
except (ValueError, TypeError):
24+
raise click.BadParameter(
25+
"not in the format 'YYYY-MM-DD' or 'YYYY-MM-DD HH:MM:SS'")
26+
27+
28+
def _get_pooled_bandwidth(env, start, end):
29+
call = env.client.call('Account', 'getVirtualDedicatedRacks',
30+
iter=True,
31+
mask='id,name,metricTrackingObjectId')
32+
types = [
33+
{'keyName': 'PUBLICIN',
34+
'name': 'publicIn',
35+
'summaryType': 'sum'},
36+
{'keyName': 'PUBLICOUT',
37+
'name': 'publicOut',
38+
'summaryType': 'sum'},
39+
{'keyName': 'PRIVATEIN',
40+
'name': 'privateIn',
41+
'summaryType': 'sum'},
42+
{'keyName': 'PRIVATEOUT',
43+
'name': 'privateOut',
44+
'summaryType': 'sum'},
45+
]
46+
47+
with click.progressbar(list(call),
48+
label='Calculating for bandwidth pools',
49+
file=sys.stderr) as pools:
50+
for pool in pools:
51+
if not pool.get('metricTrackingObjectId'):
52+
continue
53+
54+
yield {
55+
'id': pool['id'],
56+
'type': 'pool',
57+
'name': pool['name'],
58+
'data': env.client.call(
59+
'Metric_Tracking_Object',
60+
'getSummaryData',
61+
start.strftime('%Y-%m-%d %H:%M:%S %Z'),
62+
end.strftime('%Y-%m-%d %H:%M:%S %Z'),
63+
types,
64+
300,
65+
id=pool['metricTrackingObjectId'],
66+
),
67+
}
68+
69+
70+
def _get_hardware_bandwidth(env, start, end):
71+
hw_call = env.client.call(
72+
'Account', 'getHardware',
73+
iter=True,
74+
mask='id,hostname,metricTrackingObject.id,'
75+
'virtualRack[id,bandwidthAllotmentTypeId]')
76+
types = [
77+
{'keyName': 'PUBLICIN',
78+
'name': 'publicIn',
79+
'summaryType': 'counter'},
80+
{'keyName': 'PUBLICOUT',
81+
'name': 'publicOut',
82+
'summaryType': 'counter'},
83+
{'keyName': 'PRIVATEIN',
84+
'name': 'privateIn',
85+
'summaryType': 'counter'},
86+
{'keyName': 'PRIVATEOUT',
87+
'name': 'privateOut',
88+
'summaryType': 'counter'},
89+
]
90+
91+
with click.progressbar(list(hw_call),
92+
label='Calculating for hardware',
93+
file=sys.stderr) as hws:
94+
for instance in hws:
95+
if not utils.lookup(instance, 'metricTrackingObject', 'id'):
96+
continue
97+
98+
pool_name = None
99+
if utils.lookup(instance,
100+
'virtualRack',
101+
'bandwidthAllotmentTypeId') == 2:
102+
pool_name = utils.lookup(instance, 'virtualRack', 'name')
103+
104+
yield {
105+
'id': instance['id'],
106+
'type': 'hardware',
107+
'name': instance['hostname'],
108+
'pool': pool_name,
109+
'data': env.client.call(
110+
'Metric_Tracking_Object',
111+
'getSummaryData',
112+
start.strftime('%Y-%m-%d %H:%M:%S %Z'),
113+
end.strftime('%Y-%m-%d %H:%M:%S %Z'),
114+
types,
115+
3600,
116+
id=instance['metricTrackingObject']['id'],
117+
),
118+
}
119+
120+
121+
def _get_virtual_bandwidth(env, start, end):
122+
call = env.client.call(
123+
'Account', 'getVirtualGuests',
124+
iter=True,
125+
mask='id,hostname,metricTrackingObjectId,'
126+
'virtualRack[id,bandwidthAllotmentTypeId]')
127+
types = [
128+
{'keyName': 'PUBLICIN_NET_OCTET',
129+
'name': 'publicIn_net_octet',
130+
'summaryType': 'sum'},
131+
{'keyName': 'PUBLICOUT_NET_OCTET',
132+
'name': 'publicOut_net_octet',
133+
'summaryType': 'sum'},
134+
{'keyName': 'PRIVATEIN_NET_OCTET',
135+
'name': 'privateIn_net_octet',
136+
'summaryType': 'sum'},
137+
{'keyName': 'PRIVATEOUT_NET_OCTET',
138+
'name': 'privateOut_net_octet',
139+
'summaryType': 'sum'},
140+
]
141+
142+
with click.progressbar(list(call),
143+
label='Calculating for virtual',
144+
file=sys.stderr) as vms:
145+
for instance in vms:
146+
pool_name = None
147+
if utils.lookup(instance,
148+
'virtualRack',
149+
'bandwidthAllotmentTypeId') == 2:
150+
pool_name = utils.lookup(instance, 'virtualRack', 'id')
151+
152+
yield {
153+
'id': instance['id'],
154+
'type': 'virtual',
155+
'name': instance['hostname'],
156+
'pool': pool_name,
157+
'data': env.client.call(
158+
'Metric_Tracking_Object',
159+
'getSummaryData',
160+
start.strftime('%Y-%m-%d %H:%M:%S %Z'),
161+
end.strftime('%Y-%m-%d %H:%M:%S %Z'),
162+
types,
163+
3600,
164+
id=instance['metricTrackingObjectId'],
165+
),
166+
}
167+
168+
169+
@click.command(short_help="Bandwidth report for every pool/server")
170+
@click.option(
171+
'--start',
172+
callback=_validate_datetime,
173+
default=(
174+
datetime.datetime.now() - datetime.timedelta(days=30)
175+
).strftime('%Y-%m-%d'),
176+
help="datetime in the format 'YYYY-MM-DD' or 'YYYY-MM-DD HH:MM:SS'")
177+
@click.option(
178+
'--end',
179+
callback=_validate_datetime,
180+
default=datetime.datetime.now().strftime('%Y-%m-%d'),
181+
help="datetime in the format 'YYYY-MM-DD' or 'YYYY-MM-DD HH:MM:SS'")
182+
@click.option('--sortby', help='Column to sort by',
183+
default='hostname',
184+
show_default=True)
185+
@environment.pass_env
186+
def cli(env, start, end, sortby):
187+
"""Bandwidth report for every pool/server.
188+
189+
This reports on the total data transfered for each virtual sever, hardware
190+
server and bandwidth pool.
191+
"""
192+
193+
env.err('Generating bandwidth report for %s to %s' % (start, end))
194+
195+
table = formatting.Table([
196+
'type',
197+
'name',
198+
'public_in',
199+
'public_out',
200+
'private_in',
201+
'private_out',
202+
'pool',
203+
])
204+
table.sortby = sortby
205+
206+
def f_type(key, results):
207+
"Filter metric data by type"
208+
return (result['counter'] for result in results
209+
if result['type'] == key)
210+
211+
try:
212+
for item in itertools.chain(_get_pooled_bandwidth(env, start, end),
213+
_get_virtual_bandwidth(env, start, end),
214+
_get_hardware_bandwidth(env, start, end)):
215+
pub_in = int(sum(f_type('publicIn_net_octet', item['data'])))
216+
pub_out = int(sum(f_type('publicOut_net_octet', item['data'])))
217+
pri_in = int(sum(f_type('privateIn_net_octet', item['data'])))
218+
pri_out = int(sum(f_type('privateOut_net_octet', item['data'])))
219+
table.add_row([
220+
item['type'],
221+
item['name'],
222+
formatting.b_to_gb(pub_in),
223+
formatting.b_to_gb(pub_out),
224+
formatting.b_to_gb(pri_in),
225+
formatting.b_to_gb(pri_out),
226+
item.get('pool') or formatting.blank(),
227+
])
228+
except KeyboardInterrupt:
229+
env.err("Printing collected results and then aborting.")
230+
231+
env.out(env.fmt(table))

SoftLayer/CLI/routes.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,9 @@
237237
('vlan:list', 'SoftLayer.CLI.vlan.list:cli'),
238238

239239
('summary', 'SoftLayer.CLI.summary:cli'),
240+
241+
('report', 'SoftLayer.CLI.report'),
242+
('report:bandwidth', 'SoftLayer.CLI.report.bandwidth:cli'),
240243
]
241244

242245
ALL_ALIASES = {

SoftLayer/fixtures/SoftLayer_Account.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323

2424
getVirtualGuests = [{
2525
'id': 100,
26+
'metricTrackingObjectId': 1,
2627
'hostname': 'vs-test1',
2728
'domain': 'test.sftlyr.ws',
2829
'fullyQualifiedDomainName': 'vs-test1.test.sftlyr.ws',
@@ -50,6 +51,7 @@
5051
},
5152
}, {
5253
'id': 104,
54+
'metricTrackingObjectId': 2,
5355
'hostname': 'vs-test2',
5456
'domain': 'test.sftlyr.ws',
5557
'fullyQualifiedDomainName': 'vs-test2.test.sftlyr.ws',
@@ -74,6 +76,7 @@
7476
}
7577
}
7678
},
79+
'virtualRack': {'id': 1, 'bandwidthAllotmentTypeId': 2},
7780
}]
7881

7982
getMonthlyVirtualGuests = [vs for vs in getVirtualGuests
@@ -84,6 +87,7 @@
8487

8588
getHardware = [{
8689
'id': 1000,
90+
'metricTrackingObject': {'id': 3},
8791
'globalIdentifier': '1a2b3c-1701',
8892
'datacenter': {'id': 50, 'name': 'TEST00',
8993
'description': 'Test Data Center'},
@@ -144,6 +148,7 @@
144148
},
145149
}, {
146150
'id': 1001,
151+
'metricTrackingObject': {'id': 4},
147152
'globalIdentifier': '1a2b3c-1702',
148153
'datacenter': {'name': 'TEST00',
149154
'description': 'Test Data Center'},
@@ -188,6 +193,7 @@
188193
]
189194
}, {
190195
'id': 1002,
196+
'metricTrackingObject': {'id': 5},
191197
'datacenter': {'name': 'TEST00',
192198
'description': 'Test Data Center'},
193199
'billingItem': {
@@ -535,3 +541,9 @@
535541
'username': 'username',
536542
'storageType': {'keyName': 'ENDURANCE_STORAGE'},
537543
}]
544+
545+
getVirtualDedicatedRacks = [{
546+
'id': 1,
547+
'name': 'my first pool',
548+
'metricTrackingObjectId': 10,
549+
}]
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
getSummaryData = []

SoftLayer/testing/__init__.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"""
77
# Disable pylint import error and too many methods error
88
# pylint: disable=invalid-name
9+
from __future__ import print_function
910
import logging
1011
import os.path
1112

@@ -121,14 +122,16 @@ def tearDown(self): # NOQA
121122
self.tear_down()
122123
self.mocks.clear()
123124

124-
def calls(self, service=None, method=None):
125+
def calls(self, service=None, method=None, **props):
125126
"""Return all API calls made during the current test."""
126127

127128
conditions = []
128129
if service is not None:
129130
conditions.append(lambda call: call.service == service)
130131
if method is not None:
131132
conditions.append(lambda call: call.method == method)
133+
if props:
134+
conditions.append(lambda call: call_has_props(call, props))
132135

133136
return [call for call in self.mocks.calls
134137
if all(cond(call) for cond in conditions)]
@@ -139,16 +142,16 @@ def assert_called_with(self, service, method, **props):
139142
Props are properties of the given transport.Request object.
140143
"""
141144

142-
for call in self.calls(service, method):
143-
if call_has_props(call, props):
144-
return
145+
if self.calls(service, method, **props):
146+
return
145147

146148
raise AssertionError('%s::%s was not called with given properties: %s'
147149
% (service, method, props))
148150

149151
def assert_no_fail(self, result):
150152
"""Fail when a failing click result has an error"""
151153
if result.exception:
154+
print(result.output)
152155
raise result.exception
153156

154157
self.assertEqual(result.exit_code, 0)

0 commit comments

Comments
 (0)