Browse Source

Refactor distil api for distil v2

* Support displaying balance of credits
* Fewer API calls
* Returns data across all regions instead of just the current one
* Support viewing details for previous invoices
* Highlight unpaid invoices

Change-Id: Ic0f950948c83e76d760123046e8ffbd12dd632dd
changes/56/480356/17
Amelia Cordwell 2 years ago
parent
commit
d7282f9d15

+ 0
- 316
distil_ui/api/distil.py View File

@@ -1,316 +0,0 @@
1
-# Copyright (c) 2014 Catalyst IT Ltd.
2
-#
3
-# Licensed under the Apache License, Version 2.0 (the "License");
4
-# you may not use this file except in compliance with the License.
5
-# You may obtain a copy of the License at
6
-#
7
-# http://www.apache.org/licenses/LICENSE-2.0
8
-#
9
-# Unless required by applicable law or agreed to in writing, software
10
-# distributed under the License is distributed on an "AS IS" BASIS,
11
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
-# See the License for the specific language governing permissions and
13
-# limitations under the License.
14
-
15
-import collections
16
-import datetime
17
-import logging
18
-import math
19
-import time
20
-
21
-from django.conf import settings
22
-import eventlet
23
-
24
-from openstack_dashboard.api import base
25
-
26
-LOG = logging.getLogger(__name__)
27
-BILLITEM = collections.namedtuple('BillItem',
28
-                                  ['id', 'resource', 'count', 'cost'])
29
-
30
-EMPTY_BREAKDOWN = [BILLITEM(id=1, resource='N/A', count=0, cost=0)]
31
-
32
-RES_NAME_MAPPING = {'Virtual Machine': 'Compute',
33
-                    'Volume': 'Block Storage'}
34
-
35
-KNOWN_RESOURCE_TYPE = ['Compute', 'Block Storage', 'Network', 'Router',
36
-                       'Image', 'Floating IP', 'Object Storage', 'VPN',
37
-                       'Inbound International Traffic',
38
-                       'Outbound International Traffic',
39
-                       'Inbound National Traffic',
40
-                       'Outbound National Traffic']
41
-
42
-SRV_RES_MAPPING = {'m1.tiny': 'Compute',
43
-                   'm1.small': 'Compute',
44
-                   'm1.mini': 'Compute',
45
-                   'm1.medium': 'Compute',
46
-                   'c1.small': 'Compute',
47
-                   'm1.large': 'Compute',
48
-                   'm1.xlarge': 'Compute',
49
-                   'c1.large': 'Compute',
50
-                   'c1.xlarge': 'Compute',
51
-                   'c1.xxlarge': 'Compute',
52
-                   'm1.2xlarge': 'Compute',
53
-                   'c1.c1r1': 'Compute',
54
-                   'c1.c1r2': 'Compute',
55
-                   'c1.c1r4': 'Compute',
56
-                   'c1.c2r1': 'Compute',
57
-                   'c1.c2r2': 'Compute',
58
-                   'c1.c2r4': 'Compute',
59
-                   'c1.c2r8': 'Compute',
60
-                   'c1.c2r16': 'Compute',
61
-                   'c1.c4r2': 'Compute',
62
-                   'c1.c4r4': 'Compute',
63
-                   'c1.c4r8': 'Compute',
64
-                   'c1.c4r16': 'Compute',
65
-                   'c1.c4r32': 'Compute',
66
-                   'c1.c8r4': 'Compute',
67
-                   'c1.c8r8': 'Compute',
68
-                   'c1.c8r16': 'Compute',
69
-                   'c1.c8r32': 'Compute',
70
-                   'b1.standard': 'Block Storage',
71
-                   'o1.standard': 'Object Storage',
72
-                   'n1.ipv4': 'Floating IP',
73
-                   'n1.network': 'Network',
74
-                   'n1.router': 'Router',
75
-                   'n1.vpn': 'VPN',
76
-                   'n1.international-in': 'Inbound International Traffic',
77
-                   'n1.international-out': 'Outbound International Traffic',
78
-                   'n1.national-in': 'Inbound National Traffic',
79
-                   'n1.national-out': 'Outbound National Traffic'}
80
-
81
-TRAFFIC_MAPPING = {'n1.international-in': 'Inbound International Traffic',
82
-                   'n1.international-out': 'Outbound International Traffic',
83
-                   'n1.national-in': 'Inbound National Traffic',
84
-                   'n1.national-out': 'Outbound National Traffic'}
85
-
86
-CACHE = {}
87
-
88
-
89
-def distilclient(request):
90
-    try:
91
-        try:
92
-            from distilclient import client
93
-        except Exception:
94
-            from distil.client import client
95
-        auth_url = base.url_for(request, service_type='identity')
96
-        distil_url = base.url_for(request, service_type='rating')
97
-        insecure = getattr(settings, 'OPENSTACK_SSL_NO_VERIFY', False)
98
-        cacert = getattr(settings, 'OPENSTACK_SSL_CACERT', None)
99
-        distil = client.Client(distil_url=distil_url,
100
-                               os_auth_token=request.user.token.id,
101
-                               os_tenant_id=request.user.tenant_id,
102
-                               os_auth_url=auth_url,
103
-                               os_region_name=request.user.services_region,
104
-                               insecure=insecure,
105
-                               os_cacert=cacert)
106
-        distil.request = request
107
-    except Exception as e:
108
-        LOG.error(e)
109
-        return
110
-    return distil
111
-
112
-
113
-def _get_month_cost(distil_client, tenant_id, start_str, end_str,
114
-                    history_cost, i):
115
-    today = datetime.datetime.today()
116
-    start = datetime.datetime.strptime(start_str, '%Y-%m-%dT%H:%M:%S')
117
-    cache_key = (distil_client.endpoint + '_' + tenant_id + '_' +
118
-                 start_str + '_' + end_str)
119
-    if cache_key in CACHE:
120
-        history_cost[i] = CACHE[cache_key]
121
-        return
122
-
123
-    month_cost = distil_client.get_rated([tenant_id], start_str,
124
-                                         end_str)['usage']
125
-
126
-    resource_cost = collections.OrderedDict()
127
-    prices = {}
128
-    cost_details = collections.defaultdict(list)
129
-    for res in KNOWN_RESOURCE_TYPE:
130
-        cost_details[res] = []
131
-
132
-    for res_id, details in month_cost['resources'].items():
133
-        resource_type = details['type']
134
-        for s in details['services']:
135
-            if resource_type not in prices:
136
-                try:
137
-                    prices[resource_type] = float(s.get('rate', 0))
138
-                except Exception as e:
139
-                    LOG.error('Failed to get rate for %s since %s' % (s, e))
140
-            # Only collect service details for current month, we may support
141
-            # the details for history in the future.
142
-            if ((start.year == today.year and start.month == today.month) or
143
-                    s['name'] in TRAFFIC_MAPPING):
144
-                try:
145
-                    s_copy = s.copy()
146
-                    s_copy['volume'] = round(float(s_copy['volume']), 4)
147
-                    s_copy['resource_id'] = res_id
148
-                    cd_key = ('Image' if resource_type == 'Image' else
149
-                              SRV_RES_MAPPING.get(s['name'], resource_type))
150
-                    if cd_key in ('Image', 'Block Storage', 'Object Storage'):
151
-                        s_copy['unit'] = 'gigabyte * hour'
152
-
153
-                    # NOTE(flwang): Get the related resource info
154
-                    if resource_type == 'Floating IP':
155
-                        s_copy['resource'] = details['ip address']
156
-                    if resource_type in ('Image', 'Object Storage Container',
157
-                                         'Network', 'Virtual Machine',
158
-                                         'Router', 'VPN', 'Volume'):
159
-                        s_copy['resource'] = details['name']
160
-
161
-                    cost_details.get(cd_key).append(s_copy)
162
-                except Exception as e:
163
-                    LOG.error('Failed to save: %s, since %s' % (s, e))
164
-                    continue
165
-
166
-        res_type = (resource_type if resource_type not in
167
-                    RES_NAME_MAPPING else RES_NAME_MAPPING[resource_type])
168
-        count, cost = _calculate_count_cost(list(details['services']),
169
-                                            res_type)
170
-
171
-        if res_type in resource_cost:
172
-            tmp_count_cost = resource_cost[res_type]
173
-            tmp_count_cost = [tmp_count_cost[0] + count,
174
-                              tmp_count_cost[1] + cost]
175
-            resource_cost[res_type] = tmp_count_cost
176
-        else:
177
-            resource_cost[res_type] = [count, cost]
178
-
179
-    # NOTE(flwang): Based on current Distil API design, it's making the
180
-    # traffic data associate with floating ip and router. So we need to
181
-    # get them out and recalculate the cost of floating ip and router.
182
-    if ['admin'] in [r.values() for r in distil_client.request.user.roles]:
183
-        _calculate_traffic_cost(cost_details, resource_cost)
184
-
185
-    breakdown = []
186
-    total_cost = 0
187
-    for resource, count_cost in resource_cost.items():
188
-        rounded_cost = round(count_cost[1], 2)
189
-        breakdown.append(BILLITEM(id=len(breakdown) + 1,
190
-                                  resource=resource,
191
-                                  count=count_cost[0],
192
-                                  cost=rounded_cost))
193
-        total_cost += rounded_cost
194
-
195
-    if breakdown:
196
-        if start.year == today.year and start.month == today.month:
197
-            # Only apply/show the discount for current month
198
-            end_str = today.strftime('%Y-%m-%dT%H:00:00')
199
-            history_cost[i] = _apply_discount((round(total_cost, 2),
200
-                                               breakdown, cost_details),
201
-                                              start_str,
202
-                                              end_str,
203
-                                              prices)
204
-        else:
205
-            month_cost = (round(total_cost, 2), breakdown, [])
206
-            if month_cost:
207
-                CACHE[cache_key] = month_cost
208
-            history_cost[i] = month_cost
209
-
210
-
211
-def _calculate_count_cost(service_details, resource_type):
212
-    count = 0
213
-    cost = 0
214
-    for s in service_details:
215
-        if resource_type == 'Image' and s['name'] == 'b1.standard':
216
-            count += 1
217
-            cost += float(s['cost'])
218
-        if SRV_RES_MAPPING.get(s['name'], '') == resource_type:
219
-            count += 1
220
-            cost += float(s['cost'])
221
-    return count, cost
222
-
223
-
224
-def _calculate_traffic_cost(cost_details, resource_cost):
225
-    for resource_type in TRAFFIC_MAPPING.values():
226
-        if resource_type in cost_details:
227
-            (count, cost) = _calculate_count_cost(cost_details[resource_type],
228
-                                                  resource_type)
229
-            if cost > 0:
230
-                resource_cost[resource_type] = (count, cost)
231
-
232
-
233
-def _apply_discount(cost, start_str, end_str, prices):
234
-    """Appy discount for the usage costs
235
-
236
-    For now we only show the discount info for current month cost, because
237
-    the discount for history month has shown on customer's invoice.
238
-    """
239
-    total_cost, breakdown, cost_details = cost
240
-    start = time.mktime(time.strptime(start_str, '%Y-%m-%dT%H:%M:%S'))
241
-    end = time.mktime(time.strptime(end_str, '%Y-%m-%dT%H:%M:%S'))
242
-    # Get the integer part of the hours
243
-    free_hours = math.floor((end - start) / 3600)
244
-
245
-    free_network_cost = round(prices.get('Network', 0.0164) * free_hours, 2)
246
-    free_router_cost = round(prices.get('Router', 0.0170) * free_hours, 2)
247
-
248
-    for item in breakdown:
249
-        if item.resource == 'Network':
250
-            free_network_cost = (item.cost if item.cost <= free_network_cost
251
-                                 else free_network_cost)
252
-            breakdown[item.id - 1] = item._replace(cost=(item.cost -
253
-                                                         free_network_cost))
254
-            total_cost -= free_network_cost
255
-        if item.resource == 'Router':
256
-            free_router_cost = (item.cost if item.cost <= free_router_cost
257
-                                else free_router_cost)
258
-            breakdown[item.id - 1] = item._replace(cost=(item.cost -
259
-                                                         free_router_cost))
260
-            total_cost -= free_router_cost
261
-
262
-    return (total_cost, breakdown, cost_details)
263
-
264
-
265
-def _calculate_start_date(today):
266
-    last_year = today.year - 1 if today.month < 12 else today.year
267
-    month = ((today.month + 1) % 12 if today.month + 1 > 12
268
-             else today.month + 1)
269
-    return datetime.datetime(last_year, month, 1)
270
-
271
-
272
-def _calculate_end_date(start):
273
-    year = start.year + 1 if start.month + 1 > 12 else start.year
274
-    month = (start.month + 1) % 12 or 12
275
-    return datetime.datetime(year, month, 1)
276
-
277
-
278
-def get_cost(request, distil_client=None, enable_eventlet=True):
279
-    """Get cost for the last 1atest 12 months include current month
280
-
281
-    This function will return the latest 12 months cost and their breakdown
282
-    details, which includes current month.
283
-    """
284
-    if enable_eventlet:
285
-        eventlet.monkey_patch()
286
-    thread_pool = eventlet.GreenPool(size=12)
287
-    history_cost = [(0, EMPTY_BREAKDOWN, []) for _ in range(12)]
288
-
289
-    distil_client = distil_client or distilclient(request)
290
-
291
-    if not distil_client:
292
-        return history_cost
293
-
294
-    today = datetime.date.today()
295
-    start = _calculate_start_date(datetime.date.today())
296
-    end = _calculate_end_date(start)
297
-    final_end = datetime.datetime(today.year, today.month + 1, 1)
298
-
299
-    try:
300
-        for i in range(12):
301
-            start_str = start.strftime("%Y-%m-%dT00:00:00")
302
-            end_str = end.strftime("%Y-%m-%dT00:00:00")
303
-            thread_pool.spawn_n(_get_month_cost,
304
-                                distil_client, request.user.tenant_id,
305
-                                start_str, end_str,
306
-                                history_cost, i)
307
-            start = end
308
-            end = _calculate_end_date(start)
309
-            if end > final_end:
310
-                break
311
-
312
-        thread_pool.waitall()
313
-    except Exception as e:
314
-        LOG.exception('Failed to get the history cost data', e)
315
-
316
-    return history_cost

+ 293
- 0
distil_ui/api/distil_v2.py View File

@@ -0,0 +1,293 @@
1
+# Copyright (c) 2014 Catalyst IT Ltd.
2
+#
3
+# Licensed under the Apache License, Version 2.0 (the "License");
4
+# you may not use this file except in compliance with the License.
5
+# You may obtain a copy of the License at
6
+#
7
+# http://www.apache.org/licenses/LICENSE-2.0
8
+#
9
+# Unless required by applicable law or agreed to in writing, software
10
+# distributed under the License is distributed on an "AS IS" BASIS,
11
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+# See the License for the specific language governing permissions and
13
+# limitations under the License.
14
+
15
+import collections
16
+import datetime
17
+import logging
18
+import six
19
+
20
+from django.conf import settings
21
+
22
+from openstack_dashboard.api import base
23
+
24
+LOG = logging.getLogger(__name__)
25
+
26
+
27
+COMPUTE_CATEGORY = "Compute"
28
+NETWORK_CATEGORY = "Network"
29
+BLOCKSTORAGE_CATEGORY = "Block Storage"
30
+OBJECTSTORAGE_CATEGORY = "Object Storage"
31
+DISCOUNTS_CATEGORY = "Discounts"
32
+
33
+
34
+def distilclient(request, region_id=None):
35
+    try:
36
+        from distilclient import client
37
+        auth_url = base.url_for(request, service_type='identity')
38
+        distil_url = base.url_for(request, service_type='ratingv2',
39
+                                  region=region_id)
40
+        insecure = getattr(settings, 'OPENSTACK_SSL_NO_VERIFY', False)
41
+        cacert = getattr(settings, 'OPENSTACK_SSL_CACERT', None)
42
+        version = getattr(settings, 'DISTIL_VERSION', '2')
43
+        distil = client.Client(distil_url=distil_url,
44
+                               input_auth_token=request.user.token.id,
45
+                               tenant_id=request.user.tenant_id,
46
+                               auth_url=auth_url,
47
+                               region_name=request.user.services_region,
48
+                               insecure=insecure,
49
+                               os_cacert=cacert,
50
+                               version=version)
51
+        distil.request = request
52
+    except Exception as e:
53
+        LOG.error(e)
54
+        return
55
+    return distil
56
+
57
+
58
+def _calculate_start_date(today):
59
+    last_year = today.year - 1 if today.month < 12 else today.year
60
+    month = ((today.month + 1) % 12 if today.month + 1 > 12
61
+             else today.month + 1)
62
+    return datetime.datetime(last_year, month, 1)
63
+
64
+
65
+def _calculate_end_date(start):
66
+    year = start.year + 1 if start.month + 1 > 12 else start.year
67
+    month = (start.month + 1) % 12 or 12
68
+    return datetime.datetime(year, month, 1)
69
+
70
+
71
+def _wash_details(current_details):
72
+    """Apply the discount for current month quotation and merge object storage
73
+
74
+    Unfortunately, we have to put it here, though here is not the right place.
75
+    Most of the code grab from internal billing script to keep the max
76
+    consistency.
77
+    :param current_details: The original cost details merged from all regions
78
+    :return cost details after applying discount and merging object storage
79
+    """
80
+    end = datetime.datetime.utcnow()
81
+    start = datetime.datetime.strptime('%s-%s-01T00:00:00' %
82
+                                       (end.year, end.month),
83
+                                       '%Y-%m-%dT00:00:00')
84
+
85
+    free_hours = int((end - start).total_seconds() / 3600)
86
+
87
+    network_hours = collections.defaultdict(float)
88
+    router_hours = collections.defaultdict(float)
89
+    swift_usage = collections.defaultdict(list)
90
+    washed_details = []
91
+    rate_router = 0
92
+    rate_network = 0
93
+
94
+    for u in current_details["details"]:
95
+        # FIXME(flwang): 8 is the magic number here, we need a better way
96
+        # to get the region name.
97
+        region = u["product"].split(".")[0]
98
+        if u['product'].endswith('n1.network'):
99
+            network_hours[region] += u['quantity']
100
+            rate_network = u['rate']
101
+
102
+        if u['product'].endswith('n1.router'):
103
+            router_hours[region] += u['quantity']
104
+            rate_router = u['rate']
105
+
106
+        if u['product'].endswith('o1.standard'):
107
+            swift_usage[u['resource_id']].append(u)
108
+        else:
109
+            washed_details.append(u)
110
+
111
+    free_network_hours_left = free_hours
112
+    for region, hours in six.iteritems(network_hours):
113
+        free_network_hours = (hours if hours <= free_network_hours_left
114
+                              else free_network_hours_left)
115
+        if not free_network_hours:
116
+            break
117
+        line_name = 'Free Network Tier in %s' % region
118
+        washed_details.append({'product': region + '.n1.network',
119
+                               'resource_name': line_name,
120
+                               'quantity': free_network_hours,
121
+                               'resource_id': '',
122
+                               'unit': 'hour', 'rate': -rate_network,
123
+                               'cost': round(free_network_hours *
124
+                                             -rate_network, 2)})
125
+        free_network_hours_left -= free_network_hours
126
+
127
+    free_router_hours_left = free_hours
128
+    for region, hours in six.iteritems(router_hours):
129
+        free_router_hours = (hours if hours <= free_router_hours_left
130
+                             else free_router_hours_left)
131
+        if not free_router_hours:
132
+            break
133
+        line_name = 'Free Router Tier in %s' % region
134
+        washed_details.append({'product': region + '.n1.router',
135
+                               'resource_name': line_name,
136
+                               'quantity': free_router_hours,
137
+                               'resource_id': '',
138
+                               'unit': 'hour', 'rate': -rate_router,
139
+                               'cost': round(free_router_hours *
140
+                                             -rate_router, 2)})
141
+        free_router_hours_left -= free_router_hours
142
+
143
+    region_count = 0
144
+    for container, container_usage in swift_usage.items():
145
+        region_count = len(container_usage)
146
+        if (len(container_usage) > 0 and
147
+                container_usage[0]['product'].endswith('o1.standard')):
148
+            # NOTE(flwang): Find the biggest size
149
+            container_usage[0]['product'] = "NZ.o1.standard"
150
+            container_usage[0]['quantity'] = max([u['quantity']
151
+                                                  for u in container_usage])
152
+            washed_details.append(container_usage[0])
153
+
154
+    current_details["details"] = washed_details
155
+    # NOTE(flwang): Currently, the breakdown will accumulate all the object
156
+    # storage cost, so we need to deduce the duplicated part.
157
+    object_cost = current_details["breakdown"].get(OBJECTSTORAGE_CATEGORY, 0)
158
+    dup_object_cost = (region_count - 1) * (object_cost / region_count)
159
+    current_details["total_cost"] = (current_details["total_cost"] -
160
+                                     dup_object_cost)
161
+    return current_details
162
+
163
+
164
+def _parse_invoice(invoice):
165
+    LOG.debug("Start to get invoices.")
166
+    parsed = {"total_cost": 0, "breakdown": {}, "details": []}
167
+    parsed["total_cost"] += invoice["total_cost"]
168
+    breakdown = parsed["breakdown"]
169
+    details = parsed["details"]
170
+    for category, services in invoice['details'].items():
171
+        if category != DISCOUNTS_CATEGORY:
172
+            breakdown[category] = services["total_cost"]
173
+        for product in services["breakdown"]:
174
+            for order_line in services["breakdown"][product]:
175
+                order_line["product"] = product
176
+                details.append(order_line)
177
+    LOG.debug("Got quotations successfully.")
178
+    return parsed
179
+
180
+
181
+def _parse_quotation(quotation, merged_quotations, region=None):
182
+    parsed = merged_quotations
183
+    parsed["total_cost"] += quotation["total_cost"]
184
+    breakdown = parsed["breakdown"]
185
+    details = parsed["details"]
186
+    for category, services in quotation['details'].items():
187
+        if category in breakdown:
188
+            breakdown[category] += services["total_cost"]
189
+        else:
190
+            breakdown[category] = services["total_cost"]
191
+        for product in services["breakdown"]:
192
+            for order_line in services["breakdown"][product]:
193
+                order_line["product"] = product
194
+                details.append(order_line)
195
+
196
+    return parsed
197
+
198
+
199
+def _get_quotations(request):
200
+    LOG.debug("Start to get quotations from all regions.")
201
+    today_date = datetime.date.today().strftime("%Y-%m-%d")
202
+    regions = request.user.available_services_regions
203
+
204
+    merged_quotations = {"total_cost": 0, "breakdown": {}, "details": [],
205
+                         "date": today_date, "status": None}
206
+
207
+    for region in regions:
208
+        region_client = distilclient(request, region_id=region)
209
+        resp = region_client.quotations.list(detailed=True)
210
+        quotation = resp['quotations'][today_date]
211
+        merged_quotations = _parse_quotation(quotation, merged_quotations,
212
+                                             region)
213
+
214
+    merged_quotations = _wash_details(merged_quotations)
215
+    LOG.debug("Got quotations from all regions successfully.")
216
+    return merged_quotations
217
+
218
+
219
+def get_cost(request, distil_client=None):
220
+    """Get cost for the 1atest 12 months include current month
221
+
222
+    This function will return the latest 12 months cost and the breakdown
223
+    details for the each month.
224
+    :param request: Horizon request object
225
+    :param distil_client: Client object of Distilclient
226
+    :return list of cost for last 12 months
227
+    """
228
+    # 1. Process invoices
229
+    today = datetime.date.today()
230
+    start = _calculate_start_date(datetime.date.today())
231
+    # NOTE(flwang): It's OK to get invoice using the 1st day of curent month
232
+    # as the "end" date.
233
+    end = datetime.datetime(today.year, today.month, 1)
234
+
235
+    cost = [{"date": None, "total_cost": 0, "paid": False, "breakdown": {},
236
+             "details": {}}]
237
+
238
+    temp_end = end
239
+    for i in range(11):
240
+        last_day = temp_end - datetime.timedelta(seconds=1)
241
+        temp_end = datetime.datetime(last_day.year, last_day.month, 1)
242
+        cost.insert(0, {"date": last_day.strftime("%Y-%m-%d"), "total_cost": 0,
243
+                        "paid": False, "breakdown": {}, "details": {}})
244
+        if temp_end < start:
245
+            break
246
+
247
+    distil_client = distil_client or distilclient(request)
248
+    if not distil_client:
249
+        return cost
250
+    # FIXME(flwang): Get the last 11 invoices. If "today" is the early of month
251
+    # then it's possible that the invoice hasn't been created. And there is no
252
+    # way to see it based on current design of Distil API.
253
+    invoices = distil_client.invoices.list(start, end,
254
+                                           detailed=True)['invoices']
255
+
256
+    ordered_invoices = collections.OrderedDict(sorted(invoices.items(),
257
+                                               key=lambda t: t[0]))
258
+    # NOTE(flwang): The length of invoices dict could be less than 11 based on
259
+    # above comments.
260
+    for i in range(len(cost)):
261
+        month_cost = ordered_invoices.get(cost[i]['date'])
262
+        if not month_cost:
263
+            continue
264
+        cost[i]["total_cost"] = month_cost["total_cost"]
265
+        cost[i]["status"] = month_cost.get("status", None)
266
+        parsed = _parse_invoice(month_cost)
267
+        cost[i]["breakdown"] = parsed["breakdown"]
268
+        cost[i]["details"] = parsed["details"]
269
+
270
+    # 2. Process quotations from all regions
271
+    # NOTE(flwang): The quotations from all regions is always the last one of
272
+    # the cost list.
273
+    cost[-1] = _get_quotations(request)
274
+
275
+    return cost
276
+
277
+
278
+def get_credits(request, distil_client=None):
279
+    """Get balance of customer's credit
280
+
281
+    For now, it only supports credits like trail, development grant or
282
+    education grant. In the future, we will add supports for term discount if
283
+    it applys.
284
+    :param request: Horizon request object
285
+    :param distil_client: Client object of Distilclient
286
+    :return dict of credits
287
+    """
288
+    distil_client = distil_client or distilclient(request)
289
+
290
+    if not distil_client:
291
+        return {}
292
+
293
+    return distil_client.credits.list()

+ 0
- 104
distil_ui/content/billing/base.py View File

@@ -1,104 +0,0 @@
1
-# Licensed under the Apache License, Version 2.0 (the "License"); you may
2
-# not use this file except in compliance with the License. You may obtain
3
-# a copy of the License at
4
-#
5
-#      http://www.apache.org/licenses/LICENSE-2.0
6
-#
7
-# Unless required by applicable law or agreed to in writing, software
8
-# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9
-# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10
-# License for the specific language governing permissions and limitations
11
-# under the License.
12
-
13
-from __future__ import division
14
-
15
-import datetime
16
-
17
-from django.utils import timezone
18
-from django.utils.translation import ugettext_lazy as _
19
-
20
-from horizon import forms
21
-from horizon import messages
22
-
23
-
24
-class BaseBilling(object):
25
-
26
-    def __init__(self, request, project_id=None):
27
-        self.project_id = project_id or request.user.tenant_id
28
-        self.request = request
29
-        self.billing_list = []
30
-
31
-    @property
32
-    def today(self):
33
-        return timezone.now()
34
-
35
-    @staticmethod
36
-    def get_start(year, month, day):
37
-        start = datetime.datetime(year, month, day, 0, 0, 0)
38
-        return timezone.make_aware(start, timezone.utc)
39
-
40
-    @staticmethod
41
-    def get_end(year, month, day):
42
-        end = datetime.datetime(year, month, day, 23, 59, 59)
43
-        return timezone.make_aware(end, timezone.utc)
44
-
45
-    def get_date_range(self):
46
-        if not hasattr(self, "start") or not hasattr(self, "end"):
47
-            args_start = (self.today.year, self.today.month, 1)
48
-            args_end = (self.today.year, self.today.month, self.today.day)
49
-            form = self.get_form()
50
-            if form.is_valid():
51
-                start = form.cleaned_data['start']
52
-                end = form.cleaned_data['end']
53
-                args_start = (start.year,
54
-                              start.month,
55
-                              start.day)
56
-                args_end = (end.year,
57
-                            end.month,
58
-                            end.day)
59
-            elif form.is_bound:
60
-                messages.error(self.request,
61
-                               _("Invalid date format: "
62
-                                 "Using today as default."))
63
-        self.start = self.get_start(*args_start)
64
-        self.end = self.get_end(*args_end)
65
-        return self.start, self.end
66
-
67
-    def init_form(self):
68
-        today = datetime.date.today()
69
-        self.start = datetime.date(day=1, month=today.month, year=today.year)
70
-        self.end = today
71
-
72
-        return self.start, self.end
73
-
74
-    def get_form(self):
75
-        if not hasattr(self, 'form'):
76
-            req = self.request
77
-            start = req.GET.get('start', req.session.get('billing_start'))
78
-            end = req.GET.get('end', req.session.get('billing_end'))
79
-            if start and end:
80
-                # bound form
81
-                self.form = forms.DateForm({'start': start, 'end': end})
82
-            else:
83
-                # non-bound form
84
-                init = self.init_form()
85
-                start = init[0].isoformat()
86
-                end = init[1].isoformat()
87
-                self.form = forms.DateForm(initial={'start': start,
88
-                                                    'end': end})
89
-            req.session['billing_start'] = start
90
-            req.session['billing_end'] = end
91
-        return self.form
92
-
93
-    def get_billing_list(self, start, end):
94
-        return []
95
-
96
-    def csv_link(self):
97
-        form = self.get_form()
98
-        data = {}
99
-        if hasattr(form, "cleaned_data"):
100
-            data = form.cleaned_data
101
-        if not ('start' in data and 'end' in data):
102
-            data = {"start": self.today.date(), "end": self.today.date()}
103
-        return "?start=%s&end=%s&format=csv" % (data['start'],
104
-                                                data['end'])

+ 179
- 104
distil_ui/content/billing/templates/billing/index.html View File

@@ -18,103 +18,116 @@
18 18
 
19 19
 {% block main %}
20 20
 
21
-<p>Disclaimer: This is an estimate for your usage within the current region, not your final invoice. It includes the free router and network discount. All costs are in New Zealand dollars and are exclusive of GST.</p>
21
+<p>Disclaimer: This is an estimate for your usage cross <b>ALL regions</b>, not your final invoice. It includes the free router and network discount. All costs are in New Zealand dollars and are exclusive of GST.</p>
22 22
 
23 23
 <div class="row-fluid">
24
-    <div class="col-md-4">
25
-        <h3 class="quota-heading dot_line">{% trans "Month to date" %}</h3>
26
-        <div id="pie_chart">
27
-            <svg class="pie"></svg>
24
+    <div id="credits_div" class="list-group">
25
+       <h4 class="quota-heading dot_line">{% trans "Credits" %}</h4>
26
+       <ul id="credits_list" class="fa-ul" style="margin-left: 1.7em">
27
+       </ul>
28
+    </div>
29
+    <div>
30
+        <h4 class="quota-heading dot_line">{% trans "Usage Cost History" %}
31
+        </h4>
32
+        <div id="line_chart">
33
+            <svg class="line"></svg>
28 34
         </div>
29
-
30
-        {{table.render}}
31 35
     </div>
32 36
 
33
-    <div class="col-md-8">
34
-      <h3 class="quota-heading dot_line">{% trans "Usage cost history" %}</h3>
35
-      <!-- Remove the date range for now
36
-      <form action="?" method="get" id="date_form" class="form-horizontal">
37
-        <h3>{% trans "Select a period of time to query its cost" %}: </h3>
38
-        <div class="datepicker">
39
-            {% blocktrans with start=form.start %}
40
-            <label>From:</label>{{ start }}{% endblocktrans %}
41
-
42
-            {% blocktrans with end=form.end %}
43
-            <label>To:</label>{{ end }}{% endblocktrans %}
44
-            <button class="btn btn-primary" type="submit">{% trans "Submit" %}</button>
45
-            <small>{% trans "The date should be in YYYY-mm-dd format." %}</small>
37
+    <div>
38
+        <h4 id="monthly_title" class="quota-heading dot_line">{% trans "Monthly Cost Break Down" %}</h4>
39
+        <div class="col-md-4" style="padding:0px;">
40
+            <div id="pie_chart">
41
+               <select id="month_select" class="form-control" style="width:95%;"></select>
42
+               <svg class="pie"></svg>
43
+            </div>
44
+        </div>
45
+        <div class="col-md-8" style="padding:0px;">
46
+            <table id="month_details" class="table table-striped">
47
+                <thead>
48
+                    <tr>
49
+                        <th>Product Name</th>
50
+                        <th>Resource Name/ID</th>
51
+                        <th>Quantity</th>
52
+                        <th>Unit</th>
53
+                        <th>Rate</th>
54
+                        <th>Cost</th>
55
+                    </tr>
56
+                </thead>
57
+                <tfoot>
58
+                    <tr>
59
+                        <th>Product Name</th>
60
+                        <th>Resource Name/ID</th>
61
+                        <th>Quantity</th>
62
+                        <th>Unit</th>
63
+                        <th>Rate</th>
64
+                        <th>Cost</th>
65
+                    </tr>
66
+                </tfoot>
67
+                <tbody>
68
+
69
+                </tbody>
70
+            </table>
46 71
         </div>
47
-      </form>
48
-      -->
49
-
50
-      <div id="line_chart">
51
-        <svg class="line"></svg>
52
-      </div>
53
-      <table id="service_details" class="table table-striped datatable  tablesorter tablesorter-default">
54
-         <thead>
55
-           <tr class="table_caption">
56
-             <th data-column="0" class="table_header" colspan="6">
57
-               <h3 class="table_title">Usage Details</h3>
58
-             </th>
59
-           </tr>
60
-           <tr class="tablesorter-headerRow">
61
-            <th tabindex="0" data-column="0" class="sortable normal_column tablesorter-header"><div class="tablesorter-header-inner">Resource ID</div></th>
62
-            <th tabindex="0" data-column="1" class="sortable normal_column tablesorter-header"><div class="tablesorter-header-inner">Resource Name</div></th>
63
-            <th tabindex="0" data-column="2" class="sortable normal_column tablesorter-header"><div class="tablesorter-header-inner">Volume</div></th>
64
-            <th tabindex="0" data-column="3" class="sortable normal_column tablesorter-header"><div class="tablesorter-header-inner">Unit</div></th>
65
-            <th tabindex="0" data-column="4" class="sortable normal_column tablesorter-header"><div class="tablesorter-header-inner">Rate</div></th>
66
-            <th tabindex="0" data-column="5" class="sortable normal_column tablesorter-header"><div class="tablesorter-header-inner">Cost</div></th>
67
-          </tr>
68
-        </thead>
69
-        <tbody>
70
-        </tbody>
71
-      </table>
72 72
     </div>
73 73
 </div>
74 74
 
75 75
 <!-- Current d3 version is 3.4.1 -->
76 76
 <script type="text/javascript" src="{{ STATIC_URL }}catalystdashboard/d3.min.js" charset="utf-8"></script>
77 77
 <script type="text/javascript" src="{{ STATIC_URL }}catalystdashboard/nv.d3.min.js" charset="utf-8"></script>
78
+<script type="text/javascript" src="{{ STATIC_URL }}catalystdashboard/jquery.simplePagination.js" charset="utf-8"></script>
78 79
 <script type="text/javascript">
79
-    var CHART_DATA = {{chart_data | safe}};
80
-    var AMOUNT_COST = {{amount_cost | safe}};
81
-    var COST_DETAILS = {{cost_details | safe}};
80
+    var LINE_CHART_DATA = {{line_chart_data | safe}};
81
+    var PIE_CHART_DATA = {{pie_chart_data | safe}};
82
+    var MONTH_DETAILS = {{month_details | safe}};
83
+    var MONTHS = {{x_axis_line_chart | safe}};
84
+    var CREDITS = {{credits | safe}};
82 85
 
83 86
     function draw_pie(where, source){
87
+        var h = 500;
88
+        var r = h/2;
89
+        var arc = d3.svg.arc().outerRadius(r) /2 ;
84 90
         nv.addGraph(function() {
85 91
             var chart = nv.models.pieChart()
86 92
                 .x(function(d) { return d.key })
87 93
                 .y(function(d) { return d.value })
94
+                .margin({left: 0, right: 50})
88 95
                 .showLabels(true)
89 96
                 .labelType("percent")
90 97
                 .labelThreshold(.05)
91
-                .donut(true)
92
-                .donutRatio(0.35);
93
-
94
-            chart.tooltipContent(function(k, v, graph) {
95
-                return '<h3>' + k + '</h3>' + '<span style=\'padding:8px\'>$' + v + '</span>'
96
-            });
97
-
98
-            d3.select(where)
98
+                .donut(true).donutRatio(0.35);
99
+            d3.select("#pie_chart svg")
99 100
                 .datum(source)
100 101
                 .transition().duration(1200)
101 102
                 .call(chart);
102
-
103
-            nv.utils.windowResize(chart.update);
104
-
105 103
             return chart;
106 104
         });
107 105
     }
108 106
 
109 107
     function draw_line(where, source){
110 108
         nv.addGraph(function() {
111
-            var months = {{x_axis_line_chart | safe}};
109
+            d3.select(source).remove();
112 110
             var chart = nv.models.lineChart()
113
-                .margin({left: 75})
114
-                .size(70);
111
+                .margin({left: 75, right: 50})
112
+                .size(100);
115 113
 
116 114
             chart.tooltipContent(function(key, x, y, graph) {
117
-                return '<h3>' + key + '</h3>' + '<span style=\'padding:8px\'>$' + y + ' at ' + x + '</span>'
115
+                pay_status = "";
116
+                if (key == "Cost"){
117
+                    status = LINE_CHART_DATA[0].values[MONTHS.indexOf(x)].p;
118
+                    pay_status = "<br/>which is a quotation";
119
+                    switch(status) {
120
+                        case "paid":
121
+                            pay_status = "<br/>which has been paid";
122
+                            break;
123
+                        case "open":
124
+                            pay_status = '<br/>which has <strong style="color: #E6550D;">NOT</strong> been paid';
125
+                            break;
126
+                        default:
127
+                            text = "<br/>which is a quotation";
128
+                    }
129
+                }
130
+                return '<h3>' + key + '</h3>' + '<span style=\'padding:8px\'>$' + y + ' at ' + x + pay_status + '</span>'
118 131
             });
119 132
 
120 133
             chart.legend
@@ -122,15 +135,47 @@
122 135
 
123 136
             chart.xAxis.axisLabel("Cost per Month")
124 137
                 .tickValues([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11])
125
-                .tickFormat(function(m){return months[m]});
138
+                .tickFormat(function(m){return MONTHS[m]});
126 139
 
127 140
             chart.yAxis
128 141
                 .axisLabel("Cost (NZD) excl. GST")
129 142
                 .tickFormat(d3.format(',.2f'));
130
-
143
+            d3.select(where).select('.nv-interactive').selectAll("circle").remove();
131 144
             d3.select(where)
132 145
                 .datum(source)
133
-                .call(chart);
146
+                .transition()
147
+                .duration(500)
148
+                .call(chart)
149
+                .each("end", function(){
150
+                    var svg = d3.select(this);
151
+                    var data = LINE_CHART_DATA;
152
+                    var unpaid = svg.select('.nv-interactive').selectAll("circle")
153
+                        .data(data[0].values.filter(function (d) {
154
+                        return d.p == 'open';
155
+                    }))
156
+                        .enter().append("circle")
157
+                        .attr("class", "unpaid")
158
+                        .style("fill", "#E6550D")
159
+                        .attr("r", 6)
160
+                        .attr("cx", function(d) {
161
+                        return chart.xAxis.scale()(d.x);
162
+                    })
163
+                        .attr("cy", function(d) {
164
+                        return chart.yAxis.scale()(d.y);
165
+                    });
166
+                    //    .on("click", function(d) {
167
+                    //        showMonthlyCost(d["x"]);
168
+                    //});
169
+
170
+                    current_month = LINE_CHART_DATA[0].values[11];
171
+                    var current = svg.select('.nv-interactive')
172
+                        .append("circle")
173
+                        .attr("class", "mycircle")
174
+                        .style("fill", "#31A354")
175
+                        .attr("r", 6)
176
+                        .attr("cx", function(d) {return chart.xAxis.scale()(current_month.x);})
177
+                        .attr("cy", function(d) {return chart.yAxis.scale()(current_month.y);});
178
+                });
134 179
 
135 180
             nv.utils.windowResize(chart.update);
136 181
 
@@ -138,51 +183,81 @@
138 183
         });
139 184
     }
140 185
 
141
-    $(document).ready(function(){
186
+
187
+    function showMonthlyCost(monthIndex){
188
+        // Build the details table
189
+        var link_mapping = {"c1": "/project/instances/",
190
+                            "b1": "/project/volumes/"}
191
+        $('#month_details tbody').empty();
192
+        month_detail = MONTH_DETAILS[monthIndex]
193
+        for(i = 0; i < month_detail.length; i++) {
194
+            var resource_id = ""
195
+            var resource_url = "#";
196
+            if (month_detail[i]['resource_id'] != null && month_detail[i]['resource_id'] != "") {
197
+                resource_id = "(" + month_detail[i]['resource_id']+")"
198
+	            var resource_type = month_detail[i]["product"].split(".")[1];
199
+	            var product_name = month_detail[i]["product"].split(".")[2];
200
+	            if (resource_type in link_mapping){
201
+	                resource_url = link_mapping[resource_type] + month_detail[i]['resource_id'];
202
+	            }
203
+	            if (resource_type == 'n1'){
204
+	                if (product_name == 'network'){
205
+	                    resource_url = '/project/networks/'+ month_detail[i]['resource_id'] +'/detail';
206
+	                }
207
+	                if (product_name == 'router'){
208
+	                    resource_url = '/project/routers/'+ month_detail[i]['resource_id'];
209
+	                }
210
+	                if (product_name == 'vpn'){
211
+	                    resource_url = '/project/vpn/vpnservice/'+ month_detail[i]['resource_id'];
212
+	                }
213
+	            }
214
+            }
215
+            
216
+            resource = resource_id == ""? month_detail[i]['resource_name']+resource_id : "<a href="+ resource_url +">" + month_detail[i]['resource_name'] + resource_id + "</a>"
217
+            $('#month_details tbody').append('<tr><td>' + month_detail[i]['product'] + '</td><td>' + resource +'</td><td>'+month_detail[i]['quantity']+'</td><td>'+month_detail[i]['unit']+'</td><td>'+month_detail[i]['rate']+'</td><td>$'+month_detail[i]['cost']+'</td></tr>');
218
+        }
219
+
220
+        $("#month_details").simplePagination({
221
+            perPage: 10,
222
+        });
223
+
224
+        // Refresh the pie chart
142 225
         draw_pie("#pie_chart .pie", function(){
143
-            return CHART_DATA['pie'];
144
-        })
226
+            return PIE_CHART_DATA[monthIndex];
227
+        });
228
+    }
145 229
 
230
+    $(document).ready(function(){
146 231
         draw_line("#line_chart .line", function(){
147
-            return CHART_DATA['line'];
148
-        })
232
+            return LINE_CHART_DATA;
233
+        });
149 234
 
150
-        $('#id_start').attr('disabled', true);
151
-        $('#billing tbody tr').each(function(){
152
-            $(this).find("td").eq(2).html('$' + $(this).find("td").eq(2).html());
235
+        $(window).resize(function(){
236
+            draw_line("#line_chart .line", function(){
237
+                return LINE_CHART_DATA;
238
+            });
153 239
         });
154
-        $('#billing tfoot td').attr('colspan', 2);
155
-        $('#billing tfoot>tr').append('<td><span class="total">$'+AMOUNT_COST+'</span></td>');
156
-
157
-        var link_mapping = {"Compute": "/instances/",
158
-                            "Image": "/images/",
159
-                            "Network": "/networks/",
160
-                            "Router": "/routers/",
161
-                            "Block Storage": "/volumes/",
162
-                            "VPN": "/vpn/vpnservice/"}
163
-        $("#billing tbody>tr a")
164
-          .click(function() {
165
-            var resource_type = $(this).html();
166
-            res_cost_details = COST_DETAILS[resource_type]
167
-             $('#service_details .table_title').html(resource_type + ' - Usage Details')
168
-            $('#service_details').show();
169
-            $('#service_details tbody').html('');
170
-            if (res_cost_details.length>0){
171
-                for (i = 0; i < res_cost_details.length; i++) {
172
-                    var row_class = i%2==0? 'odd':'even';
173
-                    var link = "#";
174
-                    if (resource_type in link_mapping){
175
-                        link = "/project" + link_mapping[resource_type] + res_cost_details[i]['resource_id'];
176
-                    }
177
-                    if (resource_type == 'Network'){
178
-                        link += '/detail';
179
-                    }
180 240
 
181
-                    $('#service_details tbody').append('<tr class='+row_class+'><td><a href="' + link +'">' + res_cost_details[i]['resource_id'] + '</a></td><td>'+res_cost_details[i]['resource']+'</td><td>'+res_cost_details[i]['volume']+'</td><td>'+res_cost_details[i]['unit']+'</td><td>'+res_cost_details[i]['rate']+'</td><td>'+res_cost_details[i]['cost']+'</td></tr>');
182
-                }
241
+        for(i = MONTHS.length -1 ; i >= 0; i--) {
242
+            amount = LINE_CHART_DATA[0].values[i]["y"]
243
+            $("#month_select").append('<option value="'+ i.toString() +'">'+MONTHS[i] + ': $' + amount +'</option>');
244
+        }
245
+        $("#month_select").change(function (e) {
246
+            var optionSelected = $("option:selected", this);
247
+            var valueSelected = this.value;
248
+            showMonthlyCost(this.value);
249
+        });
250
+
251
+        showMonthlyCost(11);
252
+
253
+        if (CREDITS["credits"].length == 0) {
254
+            $("#credits_div").hide();
255
+        } else {
256
+             $("#credits_div").show();
257
+            for(i=0;i<CREDITS["credits"].length;i++){
258
+                $("#credits_list").append('<li><i class="fa-li fa fa-credit-card"></i> Balance of ' + CREDITS["credits"][i].type + ' is $' + CREDITS["credits"][i].balance + ' will expired at ' + CREDITS["credits"][i].expiry_date + '</li>');
183 259
             }
184
-          })
185
-        $('#service_details').hide();
260
+        }
186 261
     });
187 262
 
188 263
 </script>

+ 77
- 124
distil_ui/content/billing/tests.py View File

@@ -13,23 +13,26 @@
13 13
 # limitations under the License.
14 14
 
15 15
 import collections
16
-import datetime
16
+import json
17
+
18
+from freezegun import freeze_time
17 19
 import mock
18
-from mox3 import mox
19 20
 
20
-from distil_ui.content.billing import base
21 21
 from distil_ui.content.billing import views
22
-from django.utils import timezone
23
-from horizon import forms
24 22
 from openstack_dashboard.test import helpers as test
25 23
 
26 24
 BILLITEM = collections.namedtuple('BillItem',
27 25
                                   ['id', 'resource', 'count', 'cost'])
28 26
 
27
+FAKE_COST = [{'total_cost': 617.0, 'details': [{'quantity': 744.0, 'resource_name': '150.242.40.138', 'cost': 4.46, 'product': 'NZ-POR-1.n1.ipv4', 'rate': 0.006, 'unit': 'Hour(s)'}, {'quantity': 744.0, 'resource_name': '150.242.40.139', 'cost': 4.46, 'product': 'NZ-POR-1.n1.ipv4', 'rate': 0.006, 'unit': 'Hour(s)'}], 'breakdown': {'Network': 9.64}, 'paid': True, 'date': '2016-08-31', 'status': 'open'}, {'total_cost': 0, 'details': [], 'breakdown': {}, 'paid': True, 'date': '2016-09-30', 'status': None}, {'total_cost': 0, 'details': [], 'breakdown': {}, 'paid': True, 'date': '2016-10-31', 'status': None}, {'total_cost': 0, 'details': [], 'breakdown': {}, 'paid': True, 'date': '2016-11-30', 'status': None}, {'total_cost': 0, 'details': [], 'breakdown': {}, 'paid': True, 'date': '2016-12-31', 'status': None}, {'total_cost': 0, 'details': [], 'breakdown': {}, 'paid': True, 'date': '2017-01-31', 'status': None}, {'total_cost': 0, 'details': [], 'breakdown': {}, 'paid': True, 'date': '2017-02-28', 'status': None}, {'total_cost': 0, 'details': [], 'breakdown': {}, 'paid': True, 'date': '2017-03-31', 'status': None}, {'total_cost': 0, 'details': [], 'breakdown': {}, 'paid': True, 'date': '2017-04-30', 'status': None}, {'total_cost': 653.0, 'details': [{'quantity': 7440.0, 'resource_name': 'docker - root disk', 'cost': 3.72, 'product': 'NZ-POR-1.b1.standard', 'rate': 0.0005, 'unit': 'Gigabyte-hour(s)'}, {'quantity': 23808.0, 'resource_name': 'docker_tmp', 'cost': 11.9, 'product': 'NZ-POR-1.b1.standard', 'rate': 0.0005, 'unit': 'Gigabyte-hour(s)'}, {'quantity': 7440.0, 'resource_name': 'postgresql - root disk', 'cost': 3.72, 'product': 'NZ-POR-1.b1.standard', 'rate': 0.0005, 'unit': 'Gigabyte-hour(s)'}, {'quantity': 14880.0, 'resource_name': 'dbserver_dbvol', 'cost': 7.44, 'product': 'NZ-POR-1.b1.standard', 'rate': 0.0005, 'unit': 'Gigabyte-hour(s)'}, {'quantity': 37200.0, 'resource_name': 'server_dockervol', 'cost': 18.6, 'product': 'NZ-POR-1.b1.standard', 'rate': 0.0005, 'unit': 'Gigabyte-hour(s)'}, {'quantity': 37200.0, 'resource_name': 'docker_uservol', 'cost': 18.6, 'product': 'NZ-POR-1.b1.standard', 'rate': 0.0005, 'unit': 'Gigabyte-hour(s)'}, {'quantity': 23808.0, 'resource_name': 'docker_swap', 'cost': 11.9, 'product': 'NZ-POR-1.b1.standard', 'rate': 0.0005, 'unit': 'Gigabyte-hour(s)'}], 'breakdown': {'Block Storage': 75.88}, 'paid': True, 'date': '2017-05-31', 'status': 'paid'}, {'total_cost': 689.0, 'details': [{'quantity': 744.0, 'resource_name': 'postgresql', 'cost': 184.51, 'product': 'NZ-POR-1.c1.c4r8', 'rate': 0.248, 'unit': 'Hour(s)'}, {'quantity': 744.0, 'resource_name': 'docker', 'cost': 582.55, 'product': 'NZ-POR-1.c1.c8r32', 'rate': 0.783, 'unit': 'Hour(s)'}], 'breakdown': {'Compute': 767.06}, 'paid': True, 'date': '2017-06-30', 'status': 'paid'}, {'details': [{'quantity': 30000.0, 'resource_name': 'new_instance', 'cost': 15.0, 'product': 'REGIONTWO.b1.standard', 'rate': 0.0005, 'unit': 'second', 'resource_id': '22'}, {'quantity': 200, 'resource_name': 'my_block', 'cost': 2, 'product': 'REGIONONE.b1.standard', 'rate': 0.01, 'unit': 'hour', 'resource_id': '8'}, {'quantity': 30000.0, 'resource_name': 'my_instance', 'cost': 15.0, 'product': 'REGIONONE.b1.standard', 'rate': 0.0005, 'unit': 'second', 'resource_id': '2'}, {'quantity': 30000.0, 'resource_name': 'other_instance', 'cost': 15.0, 'product': 'REGIONONE.b1.standard', 'rate': 0.0005, 'unit': 'second', 'resource_id': '3'}, {'quantity': 50000.0, 'resource_name': 'my_container', 'cost': 13.5, 'product': 'NZ.o1.standard', 'rate': 0.00027, 'unit': 'gigabyte', 'resource_id': '1'}], 'status': None, 'date': '2017-07-10', 'breakdown': {'Virtual Machine': 30.0, 'Network': 2, 'Object Storage': 13.5}, 'total_cost': 60.5}]  # noqa
28
+
29
+FAKE_CREDITS = {'credits': [{'code': 'a9iberAn', 'type': 'Cloud Trial Credit', 'expiry_date': '2017-09-30', 'balance': 300.0, 'recurring': False, 'start_date': '2017-08-02 22:16:28'}]}  # noqa
30
+
29 31
 
30 32
 class FakeUser(object):
31 33
     roles = [{'name': 'admin'}]
32 34
     authorized_tenants = ["tenant_name"]
35
+    tenant_id = "fake_project_id"
33 36
 
34 37
     def is_authenticated(self):
35 38
         return True
@@ -50,127 +53,77 @@ class FakeRequest(object):
50 53
     GET.get = _get
51 54
 
52 55
 
53
-class BaseBillingTests(test.TestCase):
54
-    """FIXME(flwang): Move this test to rest_api_tests.py
55
-
56
-    Now we're putting the api test at here, since we don't want to hack
57
-    horizon too much. That means we don't want to put the api.py under /api
58
-    folder, at least for now.
59
-    """
60
-
61
-    def setUp(self):
62
-        super(BaseBillingTests, self).setUp()
63
-        self.mocker = mox.Mox()
64
-        self.billing = base.BaseBilling(FakeRequest(), 'my_project_id')
65
-        self.year = 2017
66
-        self.month = 1
67
-        self.day = 30
68
-
69
-    def test_today(self):
70
-        delta = datetime.timedelta(seconds=1)
71
-        self.assertTrue(self.billing.today - timezone.now() < delta)
72
-
73
-    def test_get_start(self):
74
-        start = datetime.datetime(self.year, self.month, self.day, 0, 0, 0)
75
-        self.assertEqual(self.billing.get_start(self.year, self.month,
76
-                                                self.day),
77
-                         timezone.make_aware(start, timezone.utc))
78
-
79
-    def test_get_end(self):
80
-        end = datetime.datetime(self.year, self.month, self.day, 23, 59, 59)
81
-        self.assertEqual(self.billing.get_end(self.year, self.month, self.day),
82
-                         timezone.make_aware(end, timezone.utc))
83
-
84
-    def test_get_date_range(self):
85
-        args_start = (self.billing.today.year, self.billing.today.month, 1)
86
-        args_end = (self.billing.today.year, self.billing.today.month,
87
-                    self.billing.today.day)
88
-        start = self.billing.get_start(*args_start)
89
-        end = self.billing.get_end(*args_end)
90
-        self.assertEqual(self.billing.get_date_range(),
91
-                         (start, end))
92
-
93
-    @mock.patch('distil_ui.content.billing.base.BaseBilling.get_form')
94
-    def test_get_date_range_valid_form(self, mock_get_form):
95
-        start = datetime.datetime(self.year, self.month, self.day, 0, 0, 0)
96
-        end = datetime.datetime(self.year, self.month, self.day, 23, 59, 59)
97
-        myform = forms.DateForm({'start': start, 'end': end})
98
-        myform.data = {'start': start, 'end': end}
99
-        myform.cleaned_data = {'start': start, 'end': end}
100
-        mock_get_form.return_value = myform
101
-        self.assertEqual(self.billing.get_date_range(),
102
-                         (timezone.make_aware(start, timezone.utc),
103
-                          timezone.make_aware(end, timezone.utc)))
104
-
105
-    def test_init_form(self):
106
-        start = datetime.date(self.billing.today.year,
107
-                              self.billing.today.month, 1)
108
-        end = datetime.date.today()
109
-        self.assertEqual(self.billing.init_form(), (start, end))
110
-
111
-    def test_get_form(self):
112
-        start = datetime.date(self.billing.today.year,
113
-                              self.billing.today.month, 1).strftime("%Y-%m-%d")
114
-        end = datetime.date.today().strftime("%Y-%m-%d")
115
-        self.assertEqual(self.billing.get_form().initial,
116
-                         {"start": start, "end": end})
117
-
118
-    def test_get_billing_list(self):
119
-        self.assertEqual(self.billing.get_billing_list(None, None), [])
120
-
121
-    def test_csv_link(self):
122
-        start = self.billing.today.strftime("%Y-%m-%d")
123
-        end = self.billing.today.strftime("%Y-%m-%d")
124
-        link = "?start={0}&end={1}&format=csv".format(start, end)
125
-        self.assertEqual(self.billing.csv_link(), link)
126
-
127
-
128
-class IndexCsvRendererTest(test.TestCase):
129
-    def setUp(self):
130
-        super(IndexCsvRendererTest, self).setUp()
131
-        request = FakeRequest()
132
-        template = ""
133
-        context = {"current_month": [BILLITEM(id=1, resource='N/A',
134
-                                              count=1, cost=2)]}
135
-        content_type = "text/csv"
136
-        self.csvRenderer = views.IndexCsvRenderer(request=request,
137
-                                                  template=template,
138
-                                                  context=context,
139
-                                                  content_type=content_type)
140
-
141
-    def test_get_row_data(self):
142
-        data = list(self.csvRenderer.get_row_data())
143
-        self.assertEqual(data, [('N/A', 1, u'2.00')])
144
-
145
-
146 56
 class ViewsTests(test.TestCase):
147 57
     def setUp(self):
148 58
         super(ViewsTests, self).setUp()
149
-        project_id = "fake_project_id"
59
+        self.project_id = "fake_project_id"
60
+        kwargs = {"project_id": self.project_id}
150 61
         self.view = views.IndexView()
62
+        self.view.kwargs = kwargs
151 63
         self.view.request = FakeRequest()
152
-        self.view.billing = base.BaseBilling(self.request, project_id)
153
-
154
-    def test_get_template_names(self):
155
-        self.assertEqual(self.view.get_template_names(),
156
-                         "management/billing/billing.csv")
157
-
158
-    def test_get_content_type(self):
159
-        self.assertEqual(self.view.get_content_type(), "text/csv")
160
-
161
-    def test_get_data(self):
162
-        # TODO(flwang): Will add in next patch
163
-        pass
164
-
165
-    @mock.patch('horizon.tables.DataTableView.get_context_data')
166
-    def test_get_context_data(self, mock_get_context_data):
167
-        # TODO(flwang): Will add in next patch
168
-        pass
169
-
170
-    def test_render_to_response(self):
171
-        self.view.start = datetime.datetime.now()
172
-        self.view.end = datetime.datetime.now()
173
-        context = {"current_month": [BILLITEM(id=1, resource='N/A',
174
-                                              count=1, cost=2)]}
175
-        self.assertIsInstance(self.view.render_to_response(context),
176
-                              views.IndexCsvRenderer)
64
+
65
+    @mock.patch('distil_ui.api.distil_v2.get_cost')
66
+    @mock.patch('distil_ui.api.distil_v2.get_credits')
67
+    @mock.patch('horizon.views.HorizonTemplateView.get_context_data')
68
+    @freeze_time("2017-08-10")
69
+    def test_get_context_data(self, mock_get_context_data,
70
+                              mock_get_credits, mock_get_cost):
71
+        mock_get_cost.return_value = FAKE_COST
72
+        mock_get_credits.return_value = FAKE_CREDITS
73
+        mock_get_context_data.return_value = {}
74
+        kwargs = {"project_id": self.project_id}
75
+        context = self.view.get_context_data(**kwargs)
76
+
77
+        expect_line_chart_data = [{"values": [{"p": "open",
78
+                                               "x": 0, "y": 617.0},
79
+                                              {"p": None, "x": 1, "y": 0},
80
+                                              {"p": None, "x": 2, "y": 0},
81
+                                              {"p": None, "x": 3, "y": 0},
82
+                                              {"p": None, "x": 4, "y": 0},
83
+                                              {"p": None, "x": 5, "y": 0},
84
+                                              {"p": None, "x": 6, "y": 0},
85
+                                              {"p": None, "x": 7, "y": 0},
86
+                                              {"p": None, "x": 8, "y": 0},
87
+                                              {"p": "paid",
88
+                                               "x": 9, "y": 653.0},
89
+                                              {"p": "paid",
90
+                                               "x": 10, "y": 689.0},
91
+                                              {"p": None,
92
+                                               "x": 11, "y": 60.5}],
93
+                                   "key": "Cost"},
94
+                                  {"values":
95
+                                   [{"x": 0, "y": 178.09},
96
+                                    {"x": 1, "y": 178.09},
97
+                                    {"x": 2, "y": 178.09},
98
+                                    {"x": 3, "y": 178.09},
99
+                                    {"x": 4, "y": 178.09},
100
+                                    {"x": 5, "y": 178.09},
101
+                                    {"x": 6, "y": 178.09},
102
+                                    {"x": 7, "y": 178.09},
103
+                                    {"x": 8, "y": 178.09},
104
+                                    {"x": 9, "y": 178.09},
105
+                                    {"x": 10, "y": 178.09},
106
+                                    {"x": 11, "y": 178.09}],
107
+                                   "key": "Avg Cost", "color": "#fdd0a2"}]
108
+        self.assertDictEqual(json.loads(context["line_chart_data"])[0],
109
+                             expect_line_chart_data[0])
110
+
111
+        expect_credits = {"credits": [{"balance": 300.0, "code": "a9iberAn",
112
+                                       "start_date": "2017-08-02 22:16:28",
113
+                                       "expiry_date": "2017-09-30",
114
+                                       "recurring": False,
115
+                                       "type": "Cloud Trial Credit"}]}
116
+        self.assertDictEqual(json.loads(context["credits"]), expect_credits)
117
+
118
+        expect_axis = ['Sep 2016', 'Oct 2016', 'Nov 2016', 'Dec 2016',
119
+                       'Jan 2017', 'Feb 2017', 'Mar 2017', 'Apr 2017',
120
+                       'May 2017', 'Jun 2017', 'Jul 2017', 'Aug 2017']
121
+        self.assertEqual(context["x_axis_line_chart"], expect_axis)
122
+
123
+    @freeze_time("2017-08-10")
124
+    def test_get_x_axis_for_line_chart(self):
125
+        x_axis = self.view._get_x_axis_for_line_chart()
126
+        expect = ['Sep 2016', 'Oct 2016', 'Nov 2016', 'Dec 2016',
127
+                  'Jan 2017', 'Feb 2017', 'Mar 2017', 'Apr 2017',
128
+                  'May 2017', 'Jun 2017', 'Jul 2017', 'Aug 2017']
129
+        self.assertEqual(x_axis, expect)

+ 32
- 82
distil_ui/content/billing/views.py View File

@@ -14,103 +14,53 @@
14 14
 
15 15
 import datetime
16 16
 import json
17
+import logging
17 18
 
18
-from django.template import defaultfilters
19
-from django.utils.translation import ugettext_lazy as _
20
-from horizon import exceptions
21
-from horizon import tables as horizon_tables
22
-from horizon.utils import csvbase
19
+from horizon import views
23 20
 
24
-from distil_ui.api import distil
25
-from distil_ui.content.billing import base
26
-from distil_ui.content.billing import tables
21
+from distil_ui.api import distil_v2 as distil
27 22
 
23
+LOG = logging.getLogger(__name__)
28 24
 
29
-class IndexCsvRenderer(csvbase.BaseCsvResponse):
30
-    columns = [_("Resource"), _("Count"), _("Cost")]
31 25
 
32
-    def get_row_data(self):
33
-        for b in self.context['current_month']:
34
-            yield (b.resource,
35
-                   b.count,
36
-                   defaultfilters.floatformat(b.cost, 2))
37
-
38
-
39
-class IndexView(horizon_tables.DataTableView):
40
-    table_class = tables.BillingTable
41
-    show_terminated = True
42
-    csv_template_name = 'management/billing/billing.csv'
26
+class IndexView(views.HorizonTemplateView):
43 27
     template_name = 'management/billing/index.html'
44
-    csv_response_class = IndexCsvRenderer
45 28
 
46 29
     def __init__(self, *args, **kwargs):
47 30
         super(IndexView, self).__init__(*args, **kwargs)
48 31
 
49
-    def get_template_names(self):
50
-        if self.request.GET.get('format', 'html') == 'csv':
51
-            return (self.csv_template_name or
52
-                    ".".join((self.template_name.rsplit('.', 1)[0], 'csv')))
53
-        return self.template_name
54
-
55
-    def get_content_type(self):
56
-        if self.request.GET.get('format', 'html') == 'csv':
57
-            return "text/csv"
58
-        return "text/html"
59
-
60
-    def get_data(self):
61
-        try:
62
-            project_id = self.kwargs.get('project_id',
63
-                                         self.request.user.tenant_id)
64
-            self.billing = base.BaseBilling(self.request, project_id)
65
-            self.start, self.end = self.billing.get_date_range()
66
-            distil_client = distil.distilclient(self.request)
67
-            self.history = (distil.get_cost(self.request, distil_client))
68
-
69
-            self.kwargs['billing'] = self.billing
70
-            self.kwargs['current_month'] = self.history[-1][1]
71
-            self.kwargs['history'] = self.history
72
-            return self.history[-1][1]
73
-        except Exception:
74
-            exceptions.handle(self.request, _('Unable to get usage cost.'))
75
-            return []
76
-
77 32
     def get_context_data(self, **kwargs):
78 33
         context = super(IndexView, self).get_context_data(**kwargs)
79
-        context['table'].kwargs['billing'] = self.billing
80
-        context['form'] = self.billing.form
81
-        context['billing'] = self.billing
82
-        context['current_month'] = self.history[-1][1]
83
-        pie_data = [{"value": b.cost, "key": b.resource}
84
-                    for b in self.history[-1][1] if b.cost >= 0]
85
-        line_data = [{"values": [{"y": m[0], "x": i}
86
-                                 for i, m in enumerate(self.history)],
87
-                      "method": "Square Root Choice", "key": "Cost"}]
88
-        chart_data = {'pie': pie_data, 'line': line_data}
89
-        context['chart_data'] = json.dumps(chart_data)
90
-        context['amount_cost'] = self.history[-1][0]
91
-        context['cost_details'] = json.dumps(self.history[-1][2])
34
+        distil_client = distil.distilclient(self.request)
35
+        self.cost = distil.get_cost(self.request, distil_client)
36
+        self.credits = distil.get_credits(self.request, distil_client)
37
+        pie_data = []
38
+        for i in range(len(self.cost)):
39
+            pie_data.append([{"value": value, "key": key} for (key, value)
40
+                             in self.cost[i]["breakdown"].items()])
41
+        # NOTE(flwang): The average cost is removed for now until we can get
42
+        # a better performance of the API.
43
+        # avg_cost = round(sum([m["total_cost"]
44
+        #                      for m in self.cost[:11]]) / 11.0, 2)
45
+        line_data = [{"values": [{"y": round(m["total_cost"], 2), "x": i,
46
+                                  "p": m.get("status")} for i, m
47
+                                 in enumerate(self.cost)], "key": "Cost"}]
48
+        #             {"values": [{"y": avg_cost, "x": i}
49
+        #                         for i in range(12)],
50
+        #              "key": "Avg Cost", "color": "#fdd0a2"}]
51
+
52
+        context['line_chart_data'] = json.dumps(line_data)
53
+        context['pie_chart_data'] = json.dumps(pie_data)
54
+        context['month_details'] = json.dumps([d["details"] for d
55
+                                               in self.cost])
92 56
         context['x_axis_line_chart'] = self._get_x_axis_for_line_chart()
57
+        context['credits'] = json.dumps(self.credits)
93 58
         return context
94 59
 
95 60
     def _get_x_axis_for_line_chart(self):
96 61
         today = datetime.date.today()
97
-        ordered_month = ['Jan ' + str(today.year), 'Feb', 'Mar', "Apr", 'May',
98
-                         'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
99
-
100
-        return ordered_month[today.month:] + ordered_month[:today.month]
62
+        ordered_month = ['Jan ', 'Feb ', 'Mar ', "Apr ", 'May ', 'Jun ',
63
+                         'Jul ', 'Aug ', 'Sep ', 'Oct ', 'Nov ', 'Dec ']
101 64
 
102
-    def render_to_response(self, context, **response_kwargs):
103
-        if self.request.GET.get('format', 'html') == 'csv':
104
-            render_class = self.csv_response_class
105
-            fn_template = "usage_cost_{0}_{1}.csv"
106
-            filename = fn_template.format(self.start.strftime('%Y-%m-%d'),
107
-                                          self.end.strftime('%Y-%m-%d'))
108
-            response_kwargs.setdefault("filename", filename)
109
-        else:
110
-            render_class = self.response_class
111
-        resp = render_class(request=self.request,
112
-                            template=self.get_template_names(),
113
-                            context=context,
114
-                            content_type=self.get_content_type(),
115
-                            **response_kwargs)
116
-        return resp
65
+        return ([m + str(today.year - 1) for m in ordered_month[today.month:]]
66
+                + [m + str(today.year) for m in ordered_month[:today.month]])

+ 8
- 0
distil_ui/enabled/_6100_management_billing_group.py View File

@@ -0,0 +1,8 @@
1
+from django.utils.translation import ugettext_lazy as _
2
+
3
+# The slug of the panel group to be added to HORIZON_CONFIG. Required.
4
+PANEL_GROUP = 'billing_group'
5
+# The display name of the PANEL_GROUP. Required.
6
+PANEL_GROUP_NAME = _('Billing')
7
+# The slug of the dashboard the PANEL_GROUP associated with. Required.
8
+PANEL_GROUP_DASHBOARD = 'management'

distil_ui/enabled/_6010_management_billing.py → distil_ui/enabled/_6110_management_billing.py View File

@@ -13,7 +13,7 @@
13 13
 # limitations under the License.
14 14
 
15 15
 PANEL = 'billing'
16
-PANEL_GROUP = 'default'
16
+PANEL_GROUP = 'billing_group'
17 17
 PANEL_DASHBOARD = 'management'
18 18
 
19 19
 ADD_PANEL = ('distil_ui.content.billing.panel.Billing')

+ 139
- 0
distil_ui/static/catalystdashboard/jquery.simplePagination.js View File

@@ -0,0 +1,139 @@
1
+/**
2
+The MIT License (MIT)
3
+
4
+Copyright (c) 2015 Sebastian Marulanda http://marulanda.me
5
+
6
+Permission is hereby granted, free of charge, to any person obtaining a copy
7
+of this software and associated documentation files (the "Software"), to deal
8
+in the Software without restriction, including without limitation the rights
9
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+copies of the Software, and to permit persons to whom the Software is
11
+furnished to do so, subject to the following conditions:
12
+
13
+The above copyright notice and this permission notice shall be included in all
14
+copies or substantial portions of the Software.
15
+
16
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+SOFTWARE.
23
+ */
24
+
25
+(function($) {
26
+
27
+	$.fn.simplePagination = function(options) {
28
+		var defaults = {
29
+			perPage: 5,
30
+			containerClass: '',
31
+			containerID: 'pager',
32
+			previousButtonClass: 'btn btn-default',
33
+			nextButtonClass: 'btn btn-default',
34
+			firstButtonClass: 'btn btn-default',
35
+			lastButtonClass: 'btn btn-default',
36
+			firstButtonText: 'First',
37
+		    lastButtonText: 'Last',
38
+			previousButtonText: 'Prev',
39
+			nextButtonText: 'Next',
40
+			currentPage: 1
41
+		};
42
+
43
+		var settings = $.extend({}, defaults, options);
44
+
45
+		return this.each(function() {
46
+		    $("#" + settings.containerID).remove();
47
+			var $rows = $('tbody tr', this);
48
+			var pages = Math.ceil($rows.length/settings.perPage);
49
+
50
+			var container = document.createElement('div');
51
+			container.id = settings.containerID;
52
+            var bFirst = document.createElement('button');
53
+			var bPrevious = document.createElement('button');
54
+			var bNext = document.createElement('button');
55
+            var bLast = document.createElement('button');
56
+			var of = document.createElement('span');
57
+
58
+			bPrevious.innerHTML = settings.previousButtonText;
59
+			bNext.innerHTML = settings.nextButtonText;
60
+			bFirst.innerHTML = settings.firstButtonText;
61
+			bLast.innerHTML = settings.lastButtonText;
62
+
63
+			container.className = settings.containerClass;
64
+			bPrevious.className = settings.previousButtonClass;
65
+			bNext.className = settings.nextButtonClass;
66
+			bFirst.className = settings.firstButtonClass;
67
+			bLast.className = settings.lastButtonClass;
68
+
69
+			bPrevious.style.marginRight = '8px';
70
+			bNext.style.marginLeft = '8px';
71
+			bFirst.style.marginRight = '8px';
72
+			bLast.style.marginLeft = '8px';
73
+			container.style.textAlign = "center";
74
+			container.style.marginBottom = '20px';
75
+
76
+            container.appendChild(bFirst);
77
+			container.appendChild(bPrevious);
78
+			container.appendChild(of);
79
+			container.appendChild(bNext);
80
+            container.appendChild(bLast);
81
+
82
+			$(this).after(container);
83
+
84
+			update();
85
+
86
+			$(bFirst).click(function() {
87
+                settings.currentPage = 1;
88
+				update();
89
+			});
90
+
91
+			$(bLast).click(function() {
92
+                settings.currentPage = pages;
93
+				update();
94
+			});
95
+
96
+			$(bNext).click(function() {
97
+				if (settings.currentPage + 1 > pages) {
98
+					settings.currentPage = pages;
99
+				} else {
100
+					settings.currentPage++;
101
+				}
102
+
103
+				update();
104
+			});
105
+
106
+			$(bPrevious).click(function() {
107
+				if (settings.currentPage - 1 < 1) {
108
+					settings.currentPage = 1;
109
+				} else {
110
+					settings.currentPage--;
111
+				}
112
+
113
+				update();
114
+			});
115
+
116
+			function update() {
117
+				var from = ((settings.currentPage - 1) * settings.perPage) + 1;
118
+				var to = from + settings.perPage - 1;
119
+
120
+				if (to > $rows.length) {
121
+					to = $rows.length;
122
+				}
123
+
124
+				$rows.hide();
125
+				$rows.slice((from-1), to).show();
126
+
127
+				of.innerHTML = from + ' to ' + to + ' of ' + $rows.length + ' entries';
128
+
129
+				if ($rows.length <= settings.perPage) {
130
+					$(container).hide();
131
+				} else {
132
+					$(container).show();
133
+				}
134
+			}
135
+		});
136
+
137
+	}
138
+
139
+}(jQuery));

+ 2
- 3
distil_ui/static/catalystdashboard/scss/style.css View File

@@ -14,13 +14,12 @@
14 14
 }
15 15
 
16 16
 .pie {
17
-    width:100%;
18
-    height:360px
17
+    height:500px;
19 18
 }
20 19
 
21 20
 .line {
22 21
     width:100%;
23
-    height:360px;
22
+    height:200px;
24 23
 }
25 24
 
26 25
 #billing .total {

+ 0
- 297
distil_ui/test/api_tests/rest_api_tests.py View File

@@ -1,297 +0,0 @@
1
-# Copyright (c) 2014 Catalyst IT Ltd.
2
-#
3
-# Licensed under the Apache License, Version 2.0 (the "License");
4
-# you may not use this file except in compliance with the License.
5
-# You may obtain a copy of the License at
6
-#
7
-# http://www.apache.org/licenses/LICENSE-2.0
8
-#
9
-# Unless required by applicable law or agreed to in writing, software
10
-# distributed under the License is distributed on an "AS IS" BASIS,
11
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
-# See the License for the specific language governing permissions and
13
-# limitations under the License.
14
-
15
-import datetime
16
-import math
17
-import mock
18
-import time
19
-
20
-from mox3 import mox
21
-
22
-from distil_ui.api import distil
23
-from openstack_dashboard.test import helpers as test
24
-
25
-
26
-class FakeUser(object):
27
-    roles = [{'name': 'admin'}]
28
-    token = mock.MagicMock()
29
-    tenant_id = "fake"
30
-    services_region = "fake"
31
-
32
-
33
-class FakeRequest(object):
34
-    user = FakeUser()
35
-
36
-
37
-class FakeDistilClient(object):
38
-    """A fake distil client for unit test."""
39
-    endpoint = 'http://localhost:8788'
40
-    request = FakeRequest()
41
-
42
-    def get_rated(self, tenant, start, end):
43
-        raise NotImplemented()
44
-
45
-
46
-class BillingTests(test.TestCase):
47
-    """FIXME(flwang): Move this test to rest_api_tests.py
48
-
49
-    Now we're putting the api test at here, since we don't want to hack
50
-    horizon too much. That means we don't want to put the api.py under /api
51
-    folder, at least for now.
52
-    """
53
-
54
-    def setUp(self):
55
-        super(BillingTests, self).setUp()
56
-        self.mocker = mox.Mox()
57
-
58
-    @mock.patch("openstack_dashboard.api.base.url_for")
59
-    def test_init_distilclient(self, mock_url_for):
60
-        request = FakeRequest()
61
-        distilclient = distil.distilclient(request)
62
-        self.assertIsNotNone(distilclient)
63
-
64
-    def test_calculate_end_date(self):
65
-        start = datetime.date(2015, 1, 1)
66
-        end = distil._calculate_end_date(start)
67
-        self.assertEqual((end.year, end.month, end.day), (2015, 2, 1))
68
-
69
-        start = datetime.date(2015, 6, 1)
70
-        end = distil._calculate_end_date(start)
71
-        self.assertEqual((end.year, end.month, end.day), (2015, 7, 1))
72
-
73
-        start = datetime.date(2015, 12, 1)
74
-        end = distil._calculate_end_date(start)
75
-        self.assertEqual((end.year, end.month, end.day), (2016, 1, 1))
76
-
77
-    def test_get_month_cost(self):
78
-        distilclient = self.mocker.CreateMock(FakeDistilClient)
79
-
80
-        resources = {"fake_uuid_1": {"services": [{
81
-                                     "volume": 2100,
82
-                                     "rate": 0.0005,
83
-                                     "cost": 1.05,
84
-                                     "name": "b1.standard",
85
-                                     "unit": "gigabyte"}],
86
-                                     "total_cost": 1.05,
87
-                                     "type": "Image",
88
-                                     "name": "cirros"},
89
-                     "fake_uuid_2": {"services": [{
90
-                                     "volume": 122,
91
-                                     "rate": 0.048,
92
-                                     "cost": 5.86,
93
-                                     "name": "m1.tiny",
94
-                                     "unit": "hour"}],
95
-                                     "total_cost": 5.86,
96
-                                     "type": "Virtual Machine",
97
-                                     "name": "dfgh"},
98
-                     "fake_uuid_3": {"services": [{
99
-                                     "volume": 200,
100
-                                     "rate": 0.048,
101
-                                     "cost": 9.60,
102
-                                     "name": "m1.tiny",
103
-                                     "unit": "hour"}],
104
-                                     "total_cost": 9.60,
105
-                                     "type": "Virtual Machine",
106
-                                     "name": "abcd"},
107
-                     "fake_uuid_4": {"services": [{"volume": 20.00,
108
-                                                   "rate": 0.016,
109
-                                                   "cost": 0.32,
110
-                                                   "name": "n1.network",
111
-                                                   "unit": "hour"},
112
-                                                  {"volume": 10.00,
113
-                                                   "rate": 0.016,
114
-                                                   "cost": 0.16,
115
-                                                   "name": "n1.network",
116
-                                                   "unit": "hour"}],
117
-                                     "total_cost": 0.48,
118
-                                     "type": "Network",
119
-                                     "name": "public"}
120
-                     }
121
-
122
-        result = {'usage': {"end": "2011-03-01 00:00:00", "name": "openstack",
123
-                            "total_cost": 7.23,
124
-                            "tenant_id": "7c3c506ad4b943f5bb12b9fb69478084",
125
-                            "start": "2011-02-01 00:00:00",
126
-                            "resources": resources
127
-                            }
128
-                  }
129
-
130
-        distilclient.get_rated([self.tenant.id],
131
-                               '2011-02-01T00:00:00',
132
-                               '2011-03-01T00:00:00').AndReturn(result)
133
-        self.mocker.ReplayAll()
134
-
135
-        cost = [()]
136
-        distil._get_month_cost(distilclient,
137
-                               self.tenant.id,
138
-                               '2011-02-01T00:00:00',
139
-                               '2011-03-01T00:00:00',
140
-                               cost, 0)
141
-        self.assertEqual(16.99, cost[0][0])
142
-        self.assertEqual(3, len(cost[0][1]))
143
-        bill_items = {}
144
-        for b in cost[0][1]:
145
-            # Convert cost to string make sure the floating number is right
146
-            bill_items[b.resource] = (b.count, str(b.cost))
147
-
148
-        self.assertEqual((2, '0.48'), bill_items['Network'])
149
-        self.assertEqual((2, '15.46'), bill_items['Compute'])
150
-        self.assertEqual((1, '1.05'), bill_items['Image'])
151
-
152
-    def test_calculate_history_date(self):
153
-        """Using the same algorithm to calculate the history date."""
154
-        today = datetime.date(2015, 2, 17)
155
-        start = distil._calculate_start_date(datetime.date(2015, 2, 17))
156
-        end = distil._calculate_end_date(start)
157
-        final_end = datetime.datetime(today.year, today.month + 1, 1)
158
-
159
-        history_date = [None for i in range(12)]
160
-        for i in range(12):
161
-            start_str = start.strftime("%Y-%m-%dT00:00:00")
162
-            end_str = end.strftime("%Y-%m-%dT00:00:00")
163
-            history_date[i] = (start_str, end_str)
164
-            start = end
165
-            end = distil._calculate_end_date(start)
166
-            if end > final_end:
167
-                break
168
-
169
-        self.assertEqual(('2014-03-01T00:00:00', '2014-04-01T00:00:00'),
170
-                         history_date[0])
171
-        self.assertEqual(('2014-04-01T00:00:00', '2014-05-01T00:00:00'),
172
-                         history_date[1])
173
-        self.assertEqual(('2014-05-01T00:00:00', '2014-06-01T00:00:00'),
174
-                         history_date[2])
175
-        self.assertEqual(('2014-06-01T00:00:00', '2014-07-01T00:00:00'),
176
-                         history_date[3])
177
-        self.assertEqual(('2014-07-01T00:00:00', '2014-08-01T00:00:00'),
178
-                         history_date[4])
179
-        self.assertEqual(('2014-08-01T00:00:00', '2014-09-01T00:00:00'),
180
-                         history_date[5])
181
-        self.assertEqual(('2014-09-01T00:00:00', '2014-10-01T00:00:00'),
182
-                         history_date[6])
183
-        self.assertEqual(('2014-10-01T00:00:00', '2014-11-01T00:00:00'),
184
-                         history_date[7])
185
-        self.assertEqual(('2014-11-01T00:00:00', '2014-12-01T00:00:00'),
186
-                         history_date[8])
187
-        self.assertEqual(('2014-12-01T00:00:00', '2015-01-01T00:00:00'),
188
-                         history_date[9])
189
-        self.assertEqual(('2015-01-01T00:00:00', '2015-02-01T00:00:00'),
190
-                         history_date[10])
191
-        self.assertEqual(('2015-02-01T00:00:00', '2015-03-01T00:00:00'),
192
-                         history_date[11])
193
-
194
-    def test_get_cost(self):
195
-        distilclient = self.mocker.CreateMock(FakeDistilClient)
196
-
197
-        today = datetime.date.today()
198
-        start = distil._calculate_start_date(datetime.date.today())
199
-        end = distil._calculate_end_date(start)
200
-        final_end = datetime.datetime(today.year, today.month + 1, 1)
201
-
202
-        for i in range(12):
203
-            result = {'usage': {'total_cost': (i + 1) * 100,
204
-                                'resources': {'uuid': {"services": [{
205
-                                                       "volume": 2100,
206
-                                                       "rate": 0.0005,
207
-                                                       "cost": 1.05,
208
-                                                       "name": "b1.standard",
209
-                                                       "unit": "gigabyte"}],
210
-                                                       "total_cost": 1.05,
211
-                                                       "type": "Image",
212
-                                                       "name": "cirros"}}}}
213
-            start_str = start.strftime("%Y-%m-%dT00:00:00")
214
-            end_str = end.strftime("%Y-%m-%dT00:00:00")
215
-            distilclient.get_rated([self.tenant.id],
216
-                                   start_str,
217
-                                   end_str).AndReturn(result)
218
-
219
-            start = end
220
-            end = distil._calculate_end_date(start)
221
-            if end > final_end:
222
-                break
223
-
224
-        self.mocker.ReplayAll()
225
-        setattr(self.request.user, 'tenant_id', self.tenant.id)
226
-        history_cost = distil.get_cost(self.request,
227
-                                       distil_client=distilclient,
228
-                                       enable_eventlet=False)
229
-        # 2 = math.ceil(1.05)
230
-        self.assertEqual([1.05 for i in range(12)],
231
-                         [c[0] for c in history_cost])
232
-
233
-    def test_apply_discount(self):
234
-        # There are 3 scenarios for current month.
235
-        cost = (47.54,
236
-                [distil.BILLITEM(id=1, resource='Compute', count=9,
237
-                                 cost=31.76),
238
-                 distil.BILLITEM(id=2, resource=u'Network', count=3, cost=1.5),
239
-                 distil.BILLITEM(id=3, resource=u'Image', count=35, cost=3.82),
240
-                 distil.BILLITEM(id=4, resource=u'Router', count=2, cost=0.96),
241
-                 distil.BILLITEM(id=5, resource=u'Floating IP', count=21,
242
-                                 cost=3.57),
243
-                 distil.BILLITEM(id=6, resource='Block Storage', count=22,
244
-                                 cost=6.08)
245
-                 ], [])
246
-        prices = {u'Virtual Machine': 0.044, u'Network': 0.016,
247
-                  u'Image': 0.0005, u'Volume': 0.0005,
248
-                  u'Router': 0.017, u'Floating IP': 0.006}
249
-        start_str = '2015-07-01T00:00:00'
250
-        end_str = '2015-07-02T04:00:00'
251
-
252
-        cost_after_discount = distil._apply_discount(cost, start_str, end_str,
253
-                                                     prices)
254
-        start = time.mktime(time.strptime(start_str, '%Y-%m-%dT%H:%M:%S'))
255
-        end = time.mktime(time.strptime(end_str, '%Y-%m-%dT%H:%M:%S'))
256
-        free_hours = math.floor((end - start) / 3600)
257
-
258
-        free_network_cost = round(0.016 * free_hours, 2)
259
-        free_router_cost = round(0.017 * free_hours, 2)
260
-
261
-        self.assertEqual(cost[0] - free_network_cost - free_router_cost,
262
-                         cost_after_discount[0])
263
-
264
-        self.assertIn(distil.BILLITEM(id=2, resource=u'Network', count=3,
265
-                                      cost=1.05),
266
-                      cost_after_discount[1])
267
-        self.assertIn(distil.BILLITEM(id=4, resource=u'Router', count=2,
268
-                                      cost=0.48),
269
-                      cost_after_discount[1])
270
-
271
-    def test_get_month_cost_with_cache(self):
272
-        distil.CACHE.clear()
273
-        distilclient = self.mocker.CreateMock(FakeDistilClient)
274
-        result = {'usage': {'total_cost': 5.05,
275
-                            'resources': {'uuid':
276
-                                          {"services": [{"volume": 2100,
277
-                                                         "rate": 0.0005,
278
-                                                         "cost": 5.05,
279
-                                                         "name": "b1.standard",
280
-                                                         "unit": "gigabyte"}],
281
-                                           "total_cost": 5.05,
282
-                                           "type": "Image",
283
-                                           "name": "cirros"}}}}
284
-        distilclient.get_rated([self.tenant.id],
285
-                               '2011-02-01T00:00:00',
286
-                               '2011-03-01T00:00:00').AndReturn(result)
287
-        self.mocker.ReplayAll()
288
-
289
-        cost = [()]
290
-        distil._get_month_cost(distilclient,
291
-                               self.tenant.id,
292
-                               '2011-02-01T00:00:00',
293
-                               '2011-03-01T00:00:00',
294
-                               cost, 0)
295
-        key = 'http://localhost:8788_1_2011-02-01T00:00:00_2011-03-01T00:00:00'
296
-        self.assertIn(key, distil.CACHE)
297
-        self.assertEqual(distil.CACHE[key][0], 5.05)

+ 232
- 0
distil_ui/test/api_tests/v2_api_tests.py View File

@@ -0,0 +1,232 @@
1
+# Copyright (c) 2017 Catalyst IT Ltd.
2
+#
3
+# Licensed under the Apache License, Version 2.0 (the "License");
4
+# you may not use this file except in compliance with the License.
5
+# You may obtain a copy of the License at
6
+#
7
+# http://www.apache.org/licenses/LICENSE-2.0
8
+#
9
+# Unless required by applicable law or agreed to in writing, software
10
+# distributed under the License is distributed on an "AS IS" BASIS,
11
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+# See the License for the specific language governing permissions and
13
+# limitations under the License.
14
+
15
+import datetime
16
+
17
+import mock
18
+
19
+from distil_ui.api import distil_v2
20
+from freezegun import freeze_time
21
+from openstack_dashboard.test import helpers as test
22
+
23
+regionOne = mock.Mock(id='RegionOne')
24
+regionTwo = mock.Mock(id='RegionTwo')
25
+region_list = [regionOne, regionTwo]
26
+fake_keystoneclient = mock.MagicMock()
27
+fake_keystoneclient.regions.list = mock.Mock(return_value=region_list)
28
+get_fake_keystoneclient = mock.Mock(return_value=fake_keystoneclient)
29
+
30
+
31
+class FakeUser(object):
32
+    roles = [{'name': 'admin'}]
33
+    token = mock.MagicMock()
34
+    tenant_id = "fake"
35
+    services_region = "fake"
36
+    available_services_regions = ["RegionOne", "RegionTwo"]
37
+
38
+
39
+class FakeRequest(object):
40
+    user = FakeUser()
41
+
42
+
43
+class FakeDistilClient(mock.MagicMock):
44
+    def __init__(self, *args, **kwargs):
45
+        super(FakeDistilClient, self).__init__(*args, **kwargs)
46
+        self.region_id = kwargs.get('region_id')
47
+        self.quotations = mock.MagicMock()
48
+        if self.region_id == 'RegionOne':
49
+            self.quotations.list = mock.Mock(return_value={
50
+                "quotations": {"2017-07-10": {"details": {
51
+                    "Object Storage": {
52
+                        "breakdown": {
53
+                            "REGIONONE.o1.standard": [
54
+                                {
55
+                                    "cost": 13.5,
56
+                                    "quantity": 50000.0,
57
+                                    "rate": 0.00027,
58
+                                    "resource_id": "1",
59
+                                    "resource_name": "my_container",
60
+                                    "unit": "gigabyte"
61
+                                }
62
+                            ]
63
+                        },
64
+                        "total_cost": 13.5
65
+                    },
66
+                    "Virtual Machine": {
67
+                        "breakdown": {
68
+                            "REGIONONE.c1.c2r2": [
69
+                                {
70
+                                    "cost": 15.0,
71
+                                    "quantity": 30000.0,
72
+                                    "rate": 0.0005,
73
+                                    "resource_id": "22",
74
+                                    "resource_name": "new_instance",
75
+                                    "unit": "second"
76
+                                }
77
+                            ]
78
+                        },
79
+                        "total_cost": 15.0
80
+                    }
81
+                }, "total_cost": 28.5}}})
82
+        elif self.region_id == 'RegionTwo':
83
+            self.quotations.list = mock.Mock(return_value={
84
+                "quotations": {"2017-07-10": {"details": {
85
+                    "Network": {"breakdown": {"REGIONTWO.b1.standard": [
86
+                        {"cost": 2,
87
+                         "quantity": 200,
88
+                         "rate": 0.01,
89
+                         "resource_id": "8",
90
+                         "resource_name": "my_block",
91
+                         "unit": "hour"}]},
92
+                        "total_cost": 2
93
+                    },
94
+                    "Object Storage": {"breakdown": {"REGIONTWO.o1.standard": [
95
+                        {"cost": 13.5,
96
+                         "quantity": 50000.0,
97
+                         "rate": 0.00027,
98
+                         "resource_id": "1",
99
+                         "resource_name": "my_container",
100
+                         "unit": "gigabyte"}]},
101
+                        "total_cost": 13.5},
102
+                    "Virtual Machine": {"breakdown": {
103
+                        "REGIONTWO.c1.c1r1": [
104
+                            {"cost": 15.0,
105
+                             "quantity": 30000.0,
106
+                             "rate": 0.0005,
107
+                             "resource_id": "2",
108
+                             "resource_name": "my_instance",
109
+                             "unit": "second"},
110
+                            {"cost": 15.0,
111
+                             "quantity": 30000.0,
112
+                             "rate": 0.0005,
113
+                             "resource_id": "3",
114
+                             "resource_name": "other_instance",
115
+                             "unit": "second"}]
116
+                        },
117
+                        "total_cost": 30.0
118
+                    }
119
+                }, "total_cost": 45.5
120
+                }}
121
+            })
122
+        self.invoices = mock.MagicMock()
123
+        self.invoices.list = mock.Mock(return_value={
124
+            "start": "2016-08-31 00:00:00",
125
+            "end": "2017-07-01 00:00:00",
126
+            "invoices": {
127
+                "2017-06-30": {
128
+                    "total_cost": 689.0,
129
+                    "status": "paid",
130
+                    "details": {'Compute': {'total_cost': 767.06, 'breakdown':
131
+                                            {'NZ-POR-1.c1.c4r8': [{'rate': 0.248, 'resource_name': 'postgresql', 'cost': 184.51, 'unit': 'Hour(s)', 'quantity': 744.0}],  # noqa
132
+                                             'NZ-POR-1.c1.c8r32': [{'rate': 0.783, 'resource_name': 'docker', 'cost': 582.55, 'unit': 'Hour(s)', 'quantity': 744.0}]}   # noqa
133
+                                            }
134
+                                }
135
+                },
136
+                "2017-05-31": {
137
+                    "total_cost": 653.0,
138
+                    "status": "paid",
139
+                    "details": {'Block Storage': {'total_cost': 75.88,
140
+                                                  'breakdown': {'NZ-POR-1.b1.standard': [{'rate': 0.0005, 'resource_name': 'docker - root disk', 'cost': 3.72, 'unit': 'Gigabyte-hour(s)', 'quantity': 7440.0}, {'rate': 0.0005, 'resource_name': 'docker_tmp', 'cost': 11.9, 'unit': 'Gigabyte-hour(s)', 'quantity': 23808.0},   # noqa
141
+                                                                                         {'rate': 0.0005, 'resource_name': 'postgresql - root disk', 'cost': 3.72, 'unit': 'Gigabyte-hour(s)', 'quantity': 7440.0}, {'rate': 0.0005, 'resource_name': 'dbserver_dbvol', 'cost': 7.44, 'unit': 'Gigabyte-hour(s)', 'quantity': 14880.0},   # noqa
142
+                                                                                         {'rate': 0.0005, 'resource_name': 'server_dockervol', 'cost': 18.6, 'unit': 'Gigabyte-hour(s)', 'quantity': 37200.0}, {'rate': 0.0005, 'resource_name': 'docker_uservol', 'cost': 18.6, 'unit': 'Gigabyte-hour(s)', 'quantity': 37200.0},   # noqa
143
+                                                                                         {'rate': 0.0005, 'resource_name': 'docker_swap', 'cost': 11.9, 'unit': 'Gigabyte-hour(s)', 'quantity': 23808.0}]}},   # noqa
144
+                                }
145
+                },
146
+                "2017-04-30": {"total_cost": 0, "details": {}},
147
+                "2017-03-31": {"total_cost": 0, "details": {}},
148
+                "2017-02-28": {"total_cost": 0, "details": {}},
149
+                "2017-01-31": {"total_cost": 0, "details": {}},
150
+                "2016-12-31": {"total_cost": 0, "details": {}},
151
+                "2016-11-30": {"total_cost": 0, "details": {}},
152
+                "2016-10-31": {"total_cost": 0, "details": {}},
153
+                "2016-09-30": {"total_cost": 0, "details": {}},
154
+                "2016-08-31": {
155
+                    "total_cost": 617.0,
156
+                    "status": "open",
157
+                    "details": {'Network': {'total_cost': 9.64,
158
+                                            'breakdown': {'NZ-POR-1.n1.ipv4': [{'rate': 0.006, 'resource_name': '150.242.40.138', 'cost': 4.46, 'unit': 'Hour(s)', 'quantity': 744.0}, {'rate': 0.006, 'resource_name': '150.242.40.139', 'cost': 4.46, 'unit': 'Hour(s)', 'quantity': 744.0}]}}   # noqa
159
+                                }
160
+                }
161
+            },
162
+            "project_id": "093551df28e545eba9ba676dbd56bfa7",
163
+            "project_name": "default_project",
164
+            })
165
+
166
+        self.credits = mock.MagicMock()
167
+        self.credits.list = mock.Mock(return_value={'credits': [{'code': 'abcdefg', 'type': 'Cloud Trial Credit', 'expiry_date': '2017-09-30', 'balance': 300.0, 'recurring': False, 'start_date': '2017-08-02 22:16:28'}]})  # noqa
168
+
169
+
170
+@mock.patch('distil_ui.api.distil_v2.distilclient', FakeDistilClient)
171
+class V2BillingTests(test.TestCase):
172
+    """Ensure the V2 api changes work. """
173
+    def setUp(self):
174
+        super(V2BillingTests, self).setUp()
175
+        region_list[:] = []
176
+        region_list.append(regionOne)
177
+        region_list.append(regionTwo)
178
+
179
+    @mock.patch("openstack_dashboard.api.base.url_for")
180
+    def test_init_distilclient(self, mock_url_for):
181
+        request = FakeRequest()
182
+        distilclient = distil_v2.distilclient(request)
183
+        self.assertIsNotNone(distilclient)
184
+
185
+    def test_calculate_start_date(self):
186
+        today = datetime.date(2017, 1, 1)
187
+        start = distil_v2._calculate_start_date(today)
188
+        self.assertEqual((start.year, start.month, start.day), (2016, 2, 1))
189
+
190
+        today = datetime.date(2017, 7, 1)
191
+        start = distil_v2._calculate_start_date(today)
192
+        self.assertEqual((start.year, start.month, start.day), (2016, 8, 1))
193
+
194
+        today = datetime.date(2017, 12, 31)
195
+        start = distil_v2._calculate_start_date(today)
196
+        self.assertEqual((start.year, start.month, start.day), (2017, 1, 1))
197
+
198
+    def test_calculate_end_date(self):
199
+        start = datetime.date(2015, 1, 1)
200
+        end = distil_v2._calculate_end_date(start)
201
+        self.assertEqual((end.year, end.month, end.day), (2015, 2, 1))
202
+
203
+        start = datetime.date(2015, 6, 6)
204
+        end = distil_v2._calculate_end_date(start)
205
+        self.assertEqual((end.year, end.month, end.day), (2015, 7, 1))
206
+
207
+        start = datetime.date(2015, 12, 31)
208
+        end = distil_v2._calculate_end_date(start)
209
+        self.assertEqual((end.year, end.month, end.day), (2016, 1, 1))
210
+
211
+    @freeze_time("2017-07-10")
212
+    @mock.patch('openstack_dashboard.api.keystone.keystoneclient',
213
+                get_fake_keystoneclient)
214
+    def test_get_cost(self):
215
+        cost = distil_v2.get_cost(FakeRequest())
216
+
217
+        self.assertEqual(cost[11]["total_cost"], 60.5)
218
+        self.assertEqual(cost[10]["total_cost"], 689.0)
219
+        self.assertEqual(cost[9]["total_cost"], 653.0)
220
+        self.assertEqual(cost[8]["total_cost"], 0)
221
+        self.assertEqual(cost[0]["total_cost"], 617)
222
+
223
+    def test_get_credit(self):
224
+        credits = distil_v2.get_credits(mock.MagicMock())
225
+        expect = {'credits': [{'code': 'abcdefg',
226
+                               'type': 'Cloud Trial Credit',
227
+                               'expiry_date': '2017-09-30',
228
+                               'balance': 300.0,
229
+                               'recurring': False,
230
+                               'start_date': '2017-08-02 22:16:28'}]}
231
+
232
+        self.assertDictEqual(credits, expect)

+ 1
- 1
test-requirements.txt View File

@@ -27,4 +27,4 @@ testtools>=1.4.0 # MIT
27 27
 xvfbwrapper>=0.1.3 #license: MIT
28 28
 # Include horizon as test requirement
29 29
 http://tarballs.openstack.org/horizon/horizon-master.tar.gz#egg=horizon
30
-
30
+freezegun>=0.3.6 # Apache-2.0

+ 7
- 0
tox.ini View File

@@ -49,6 +49,13 @@ commands =
49 49
     pip install git+https://github.com/openstack/python-distilclient.git
50 50
     python manage.py test {posargs}
51 51
 
52
+[testenv:py27debug]
53
+basepython = python2.7
54
+whitelist_externals = oslo_debug_helper
55
+commands =
56
+    pip install git+https://github.com/openstack/python-distilclient.git
57
+    oslo_debug_helper -t python manage.py test {posargs}
58
+
52 59
 [testenv:eslint]
53 60
 whitelist_externals = npm
54 61
 commands =

Loading…
Cancel
Save