Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

add readme

  • Loading branch information...
commit 991d5fb74e903c50a93cf905d7dc614cc541b5ef 1 parent b1a999b
authored August 08, 2012
397  README
... ...
@@ -0,0 +1,397 @@
  1
+Overview
  2
+===========================
  3
+
  4
+dough是一个openstack的计费系统。它可以:
  5
+    对租户进行持续的计费
  6
+    拥有灵活可定制的付款方式
  7
+    可按照价格和周期进行付款单位设定
  8
+    设定预付费或者即时付费
  9
+    扣除租户费用
  10
+    代金券管理
  11
+
  12
+====== Database Schema ======
  13
+
  14
+
  15
+# 地区
  16
+CREATE TABLE `regions` (
  17
+  `created_at` datetime DEFAULT NULL,
  18
+  `updated_at` datetime DEFAULT NULL,
  19
+  `deleted_at` datetime DEFAULT NULL,
  20
+  `deleted` tinyint(1) DEFAULT NULL,
  21
+  `id` int(11) NOT NULL AUTO_INCREMENT,
  22
+  `name` varchar(255) NOT NULL,
  23
+  PRIMARY KEY (`id`)
  24
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
  25
+
  26
+# 扣费项目表
  27
+CREATE TABLE `items` (
  28
+  `created_at` datetime DEFAULT NULL,
  29
+  `updated_at` datetime DEFAULT NULL,
  30
+  `deleted_at` datetime DEFAULT NULL,
  31
+  `deleted` tinyint(1) DEFAULT NULL,
  32
+  `id` int(11) NOT NULL AUTO_INCREMENT,
  33
+  `name` varchar(255) NOT NULL,
  34
+  PRIMARY KEY (`id`)
  35
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
  36
+
  37
+# 虚拟机类型
  38
+CREATE TABLE `item_types` (
  39
+  `created_at` datetime DEFAULT NULL,
  40
+  `updated_at` datetime DEFAULT NULL,
  41
+  `deleted_at` datetime DEFAULT NULL,
  42
+  `deleted` tinyint(1) DEFAULT NULL,
  43
+  `id` int(11) NOT NULL AUTO_INCREMENT,
  44
+  `name` varchar(255) NOT NULL,
  45
+  PRIMARY KEY (`id`)
  46
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
  47
+
  48
+# 计费周期类型
  49
+# is_prepaid是预付费还是后付费
  50
+# interval_*是计费的周期
  51
+CREATE TABLE `payment_types` (
  52
+  `created_at` datetime DEFAULT NULL,
  53
+  `updated_at` datetime DEFAULT NULL,
  54
+  `deleted_at` datetime DEFAULT NULL,
  55
+  `deleted` tinyint(1) DEFAULT NULL,
  56
+  `id` int(11) NOT NULL AUTO_INCREMENT,
  57
+  `name` varchar(255) NOT NULL,
  58
+  `interval_unit` varchar(255) NOT NULL,
  59
+  `interval_size` int(11) NOT NULL,
  60
+  `is_prepaid` tinyint(1) NOT NULL,
  61
+  PRIMARY KEY (`id`)
  62
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
  63
+
  64
+# 产品表
  65
+# order_*是费用单位, payment_types.interval*不能大于products.order*
  66
+# 目前payment_types.interval_* 和 products.order* 的时间周期必须全部一样
  67
+# 举例:
  68
+# payment_types.interval_*= 1 day , products.order*= 1 month, 则
  69
+# 举例:
  70
+# payment_types.interval_*= 1 hour , products.order*= 10Mb Bytes, 则一个小时之后产生一条 
  71
+#           purchases.quantity/products.order_size*products.price
  72
+# 的费用记录
  73
+# currency是货币单位,暂时没用
  74
+CREATE TABLE `products` (
  75
+  `created_at` datetime DEFAULT NULL,
  76
+  `updated_at` datetime DEFAULT NULL,
  77
+  `deleted_at` datetime DEFAULT NULL,
  78
+  `deleted` tinyint(1) DEFAULT NULL,
  79
+  `id` int(11) NOT NULL AUTO_INCREMENT,
  80
+  `region_id` int(11) NOT NULL,
  81
+  `item_id` int(11) NOT NULL,
  82
+  `item_type_id` int(11) NOT NULL,
  83
+  `payment_type_id` int(11) NOT NULL,
  84
+  `order_unit` varchar(255) NOT NULL,  # e.g) hours / days / months / KBytes / requests
  85
+  `order_size` int(11) NOT NULL,
  86
+  `price` float NOT NULL,
  87
+  `currency` varchar(255) NOT NULL,
  88
+  PRIMARY KEY (`id`),
  89
+  KEY `region_id` (`region_id`),
  90
+  KEY `item_id` (`item_id`),
  91
+  KEY `item_type_id` (`item_type_id`),
  92
+  KEY `payment_type_id` (`payment_type_id`)
  93
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
  94
+
  95
+# 订单表
  96
+# 创建resource如虚拟机的时候会生成一条记录
  97
+# project_id是用户的tenant_id
  98
+# resource_uuid 虚拟机的uuid或者loadblance的uuid等
  99
+# resource_name是用户起的名字
  100
+# expires_at是下一个计费的时间
  101
+# status是状态,对应内部的处理函数名称
  102
+# 资源被回收如虚拟机关闭、floatingip取消的时候会删除对应的subscriptions记录(仅标记deleted字段,不实际删除)
  103
+CREATE TABLE `subscriptions` (
  104
+  `created_at` datetime DEFAULT NULL,
  105
+  `updated_at` datetime DEFAULT NULL,
  106
+  `deleted_at` datetime DEFAULT NULL,
  107
+  `deleted` tinyint(1) DEFAULT NULL,
  108
+  `id` int(11) NOT NULL AUTO_INCREMENT,
  109
+  `project_id` varchar(64) NOT NULL,
  110
+  `product_id` int(11) NOT NULL,
  111
+  `resource_uuid` varchar(36) NOT NULL,
  112
+  `resource_name` varchar(255) NOT NULL,
  113
+  `expires_at` datetime DEFAULT NULL,
  114
+  `status` varchar(255) DEFAULT NULL,  # comfirmed, creating, deleting
  115
+  PRIMARY KEY (`id`),
  116
+  KEY `project_id` (`project_id`),
  117
+  KEY `product_id` (`product_id`),
  118
+  KEY `resource_uuid` (`resource_uuid`),
  119
+  KEY `expires_at` (`expires_at`)
  120
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
  121
+
  122
+# 扣费记录表
  123
+# 在每个收费点上都会生成一条记录
  124
+# 预付费的subscriptions会在creating改变为verify的时候生成一条记录
  125
+# 后付费的subscriptions会在从deleting变为terminated的时候生成一条记录
  126
+# 所有verify的subscriptions的expires_at大于当前时间就会生成一条记录
  127
+# quantity是当前计费周期内的数据量(流量单位目前为byte,floatingip等为天)
  128
+# line_total是本次费用
  129
+# 已经实际扣过费的记录,flag字段为1。未扣费的为0;扣费失败的为-1
  130
+CREATE TABLE `purchases` (
  131
+  `created_at` datetime DEFAULT NULL,
  132
+  `updated_at` datetime DEFAULT NULL,
  133
+  `deleted_at` datetime DEFAULT NULL,
  134
+  `deleted` tinyint(1) DEFAULT NULL,
  135
+  `id` int(11) NOT NULL AUTO_INCREMENT,
  136
+  `subscription_id` int(11) NOT NULL,
  137
+  `quantity` float NOT NULL,
  138
+  `line_total` float NOT NULL,
  139
+  `flag` tinyint(4) DEFAULT '0',
  140
+  PRIMARY KEY (`id`),
  141
+  KEY `subscription_id` (`subscription_id`)
  142
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
  143
+
  144
+
  145
+====== Communication Protocol ======
  146
+
  147
+
  148
+msg_type = 'dough'
  149
+msg_uuid = utils.gen_uuid()
  150
+
  151
+
  152
+===== Protocol =====
  153
+
  154
+==== horizon -> billing_server ====
  155
+
  156
+# ``payment_type`` is str, eg. hourly for hour, daily for day and monthly for month
  157
+# ``resource_uuid`` contains ``instance_uuid`` for instance, ``floating_ip_id``, ``load_balancer_id`` for floating_ip_id, load_balancer_id
  158
+# ``item_type`` is flavor for instance, 'default' for floating_ip_id, load_balancer_id
  159
+
  160
+# 'subscribe_item'
  161
+message = {
  162
+    'method': 'subscribe_item',
  163
+    'args': {
  164
+        'user_id': '864bbc5d23ea47799ae2a702927920e9',
  165
+        'tenant_id': '864bbc5d23ea47799ae2a702927920e9',
  166
+        'region': 'deafult',                        # always 'default' for now
  167
+        'item': 'instance',                         # 'floating_ip','load_balancer'
  168
+        'item_type': 'm1.tiny',                     # 'default' for floating_ip / load_balance
  169
+        'payment_type': 'hourly',
  170
+        'resource_uuid': 'uuidofinstance',          # 'xxx-id' value for floating_ip / load_balancer
  171
+        'resource_name': 'nameofinstance',          # 'display name' value for instance / floating_ip / load_balancer
  172
+        }
  173
+    }
  174
+
  175
+# 'unsubscribe_item'
  176
+message = {
  177
+    'method': 'unsubscribe_item',
  178
+    'args': {
  179
+        'user_id': '864bbc5d23ea47799ae2a702927920e9',
  180
+        'tenant_id': '864bbc5d23ea47799ae2a702927920e9',
  181
+        'region': 'deafult',                        # always 'default' for now
  182
+        'item': 'instance',                         # 'floating_ip','load_balancer'
  183
+        'resource_uuid': 'uuidofinstance',          # 'id' value for floating_ip / load_balancer
  184
+        }
  185
+    }
  186
+
  187
+# 'query_item_products'
  188
+message = {
  189
+    'method': 'query_item_products',
  190
+    'args': {
  191
+        'region': 'deafult',     # always 'default' for now
  192
+        'item': 'instance',      # 'floating_ip','load_balancer'
  193
+        }
  194
+    }
  195
+
  196
+# 'query_usage_report'
  197
+message = {
  198
+    'method': 'query_usage_report',
  199
+    'args': {
  200
+        'user_id': '864bbc5d23ea47799ae2a702927920e9',
  201
+        'tenant_id': '864bbc5d23ea47799ae2a702927920e9',
  202
+        'timestamp_from': '2012-03-06T11:05:54.747585',
  203
+        'timestamp_to': '2012-03-26T11:05:54.747585',
  204
+        }
  205
+    }
  206
+
  207
+# 'query_monthly_report'
  208
+message = {
  209
+    'method': 'query_monthly_report',
  210
+    'args': {
  211
+        'user_id': '864bbc5d23ea47799ae2a702927920e9',
  212
+        'tenant_id': '864bbc5d23ea47799ae2a702927920e9',
  213
+        'timestamp_from': '2012-03-01T00:00:00',
  214
+        'timestamp_to': '2012-05-01T00:00:00',
  215
+        }
  216
+    }
  217
+
  218
+#query_detail_report
  219
+message = {
  220
+    'method' : 'query_detail_report'
  221
+    'args' : {
  222
+            'user_id': '864bbc5d23ea47799ae2a702927920e9',
  223
+            'tenant_id': '864bbc5d23ea47799ae2a702927920e9',
  224
+            'timestamp_from': '2012-03-01T00:00:00',
  225
+            'timestamp_to': '2012-05-01T00:00:00',
  226
+        }
  227
+    }
  228
+
  229
+# 'query_report'
  230
+查询指定时间间隔的统计结果明细
  231
+period取值:days / hours / months
  232
+item_nam取值(dough.items表name字段的内容):instance / network / floating_ip / load_balancer / cdn_network
  233
+message = {
  234
+    'method': 'query_report',
  235
+    'args': {
  236
+        'user_id': '864bbc5d23ea47799ae2a702927920e9',
  237
+        'tenant_id': '864bbc5d23ea47799ae2a702927920e9',
  238
+        'timestamp_from': '2012-03-01T00:00:00',
  239
+        'timestamp_to': '2012-03-02T00:00:00',
  240
+        'period': 'days',
  241
+        'item_name': 'instance'
  242
+        'resource_name': 'myvm'
  243
+        }
  244
+    }
  245
+
  246
+
  247
+==== billing_server -> horizon ====
  248
+
  249
+# 'subscribe_item', 'unsubscribe_item'
  250
+message = {
  251
+    'message':'message_string',
  252
+    'code':200 # or 500,
  253
+    }
  254
+
  255
+# 'query_item_products'
  256
+message = {
  257
+    'message':'message_string',
  258
+    'code': 200, # or 500
  259
+    'data': {
  260
+        'default': {
  261
+            'daily_as_you_go': {
  262
+                'order_unit': 'Bytes',
  263
+                'order_size': 10240,
  264
+                'price': 0.0091,
  265
+                'currency': 'CNY',
  266
+                },
  267
+            },
  268
+        'm1.large': {
  269
+            'hourly': {
  270
+                'order_unit': 'hours',
  271
+                'order_size': 1,
  272
+                'price': 0.5,
  273
+                'currency': 'CNY',
  274
+                },
  275
+            'monthly': {
  276
+                'order_unit': 'months',
  277
+                'order_size': 1,
  278
+                'price': 300,
  279
+                'currency': 'CNY',
  280
+                },
  281
+            },
  282
+        },
  283
+    }
  284
+
  285
+# 'query_usage_report'
  286
+message = {
  287
+    'message':'message_string',
  288
+    'code':200, # or 500
  289
+    'data': {
  290
+        'default': {
  291
+            'instance': [
  292
+                #  (resource_uuid, resource_name, item_type, order_unit, order_size, price, currency, quantity_sum, line_total_sum ,timestamp_from, timestamp_to)
  293
+                ('uuid1', 'some instance', 'm1.tiny', 'hours', 1, 2.40, 'CNY', 16, 38.4, '2012-03-06T11:05:54.747585', '2012-03-26T11:05:54.747585',),
  294
+                ('uuid1', 'some instance2', 'm1.tiny', 'months', 1, 2100.00, 'CNY', 1, 2100.00, '2012-03-06T11:05:54.747585', '2012-03-26T11:05:54.747585',),
  295
+                ],
  296
+            'load_balancer': [
  297
+                ('1111', '10.211.23.45', 'default', 'days', 1, 1.1, 'CNY', 19, 20.9, '2012-03-06T11:05:54.747585', '2012-03-26T11:05:54.747585',),
  298
+                ('222', '170.1.223.5', 'default', 'days', 1, 1.1, 'CNY', 11, 12.1, '2012-03-06T11:05:54.747585', '2012-03-26T11:05:54.747585',),
  299
+                ],
  300
+            'floating_ip': [
  301
+                ('lb_id1', 'some load balancer', 'default', 'days', 1, 2.7, 'CNY', 13, 35.1, '2012-03-06T11:05:54.747585', '2012-03-26T11:05:54.747585',),
  302
+                ('lb_id2', 'some load balancer2', 'default', 'days', 1, 2.7, 'CNY', 27, 73.9, '2012-03-06T11:05:54.747585', '2012-03-26T11:05:54.747585',),
  303
+                ],
  304
+            'network': [ # ``uuid``, ``timestamp_from``,``timestamp_to`` and ``resource_name`` are the same with that of instance 
  305
+                ('uuid1', 'some instance', 'default', 'KBytes', 1, 0.7, 'CNY', 1852, 1296.4, '2012-03-06T11:05:54.747585', '2012-03-26T11:05:54.747585',),
  306
+                ('uuid2', 'some instance2', 'default', 'KBytes', 1, 0.7, 'CNY', 9853, 6897.1, '2012-03-06T11:05:54.747585', '2012-03-26T11:05:54.747585',),
  307
+                ],
  308
+            },
  309
+        },
  310
+    }
  311
+
  312
+# 'query_monthly_report'
  313
+message = {
  314
+    'message':'message_string',
  315
+    'code':200, # or 500
  316
+    'data': {
  317
+        'default': {
  318
+            '2012-03-01T00:00:00': {
  319
+                'instance': 12345.67,
  320
+                'floating_ip': 12345.67,
  321
+                'load_balancer': 12345.67,
  322
+                'network': 12345.67,
  323
+                },
  324
+            '2012-04-01T00:00:00': {
  325
+                'instance': 12345.67,
  326
+                'floating_ip': 12345.67,
  327
+                'load_balancer': 12345.67,
  328
+                'network': 12345.67,
  329
+                },
  330
+            },
  331
+        },
  332
+    }
  333
+
  334
+#'query_detail_report'
  335
+message = {
  336
+    'message':'message_string',
  337
+    'code':200, # or 500,
  338
+    'date':{
  339
+        {u'default':
  340
+             {
  341
+                  '2012-06-12T00:00:00+00:00:00':{
  342
+                     total_cost:246
  343
+                     sourse_type:'instance'
  344
+                     sourse_name:'my instance'
  345
+                     time_from:'2012-06-12T00:00:00+00:00:00'
  346
+                     time_to:'2012-06-13T00:00:00+00:00:00'
  347
+                     coupon_cost:123
  348
+                     coupon_remain:123
  349
+                     money_cost:123
  350
+                     money_remain:123
  351
+                  }
  352
+             },
  353
+             ...
  354
+        }
  355
+    }
  356
+}
  357
+
  358
+# 'query_report'
  359
+message = {
  360
+    'message':'message_string',
  361
+    'code':200, # or 500
  362
+    'data': {
  363
+        {u'default': 
  364
+             {
  365
+             '2012-06-12T00:00:00+00:00': {
  366
+             'line_total': 2.0, 
  367
+             'quantity': 1.0,
  368
+             'floating_ip': (u'276', u'119.167.136.72', u'default', u'days', 1L, 2.0, u'CNY', 1.0, 2.0, '2012-06-12T01:19:18', '2012-06-16T01:19:18')
  369
+             }
  370
+        },
  371
+    }
  372
+
  373
+
  374
+====== Use Case ======
  375
+
  376
+===== 用户开始使用收费条目 =====
  377
+
  378
+当用户开始使用收费服务以及停止收费服务,通知billing server。这里的收费服务指的是
  379
+  - 创建/停止 虚拟机
  380
+  - 分配/释放 IP地址
  381
+  - 创建/删除 负载均衡
  382
+
  383
+创建收费服务时,horizon确认用户的账户是否余额足够,如果足够则创建服务并且通知billing server。
  384
+
  385
+用户取消服务时,horizon通知billing server。
  386
+
  387
+
  388
+===== Billing Server 健康检查===== 
  389
+Billing Server通过nova api或者nova 数据库对收费条目进行检查,防止创建不成功但是依然扣费的情况出现。如果有创建失败的问题出现,那么Billing Server通知管理员(Administrator)并且停止扣费。
  390
+
  391
+
  392
+===== Billing Server 扣费 =====
  393
+Billing Server周期性(每个小时)在用户的账号上,根据账单进行扣费。
  394
+
  395
+
  396
+===== Billing Server与SSO交互 =====
  397
+Billing Server通过SSO读写用户的余额信息。
4  bin/dough-farmer
@@ -46,6 +46,10 @@ if __name__ == '__main__':
46 46
             expires_at = sub['expires_at']
47 47
             if expires_at > current_time:
48 48
                 continue
  49
+            product = sub['product']
  50
+            if product is None:
  51
+                app.warning("product is None, subid=" + str(sub.id))
  52
+                continue
49 53
 
50 54
             order_unit = sub['product']['order_unit']
51 55
             order_size = sub['product']['order_size']
4  dough/api.py
@@ -260,7 +260,6 @@ def query_report(context, timestamp_from=None, timestamp_to=None,
260 260
                   period=None, item_name=None, resource_name=None, **kwargs):
261 261
     """period='days' or 'hours'"""
262 262
     print "query_report", timestamp_from, timestamp_to, item_name, resource_name
263  
-#    period = int(period)
264 263
 
265 264
     if not period in ['days', 'hours', 'months']:
266 265
         return {'data': None}
@@ -280,7 +279,6 @@ def find_timeframe(start_time, end_time, target):
280 279
         return current_frame.isoformat()
281 280
 
282 281
     monthly_report = dict()
283  
-    #usage_report = dict()
284 282
     datetime_from = iso8601.parse_date(timestamp_from)
285 283
     datetime_to = iso8601.parse_date(timestamp_to)
286 284
     subscriptions = list()
@@ -290,9 +288,7 @@ def find_timeframe(start_time, end_time, target):
290 288
                                                         context.project_id)
291 289
     if not __subscriptions:
292 290
         return {'data': None}
293  
-#    print "context.project_id", context.project_id
294 291
     for subscription in __subscriptions:
295  
-#        print subscription['id'], subscription['resource_name'], subscription['product']['item']['name']
296 292
         if subscription['resource_name'] != resource_name:
297 293
             continue
298 294
         elif subscription['product']['item']['name'] != item_name:
3  dough/billing/api.py
@@ -106,7 +106,8 @@ def verified(context, subscription_id, tenant_id, item_name, resource_uuid,
106 106
                               expires_at, order_size)
107 107
     print "verified", tenant_id, subscription_id, \
108 108
                     quantity, order_size, "\033[1;33m", price, "\033[0m"
109  
-    app.info("verified %s:subid=%s,tid=%s,price=%s" % (item_name, subscription_id, tenant_id, str(price)))
  109
+    app.info("verified %s(%s/%s/%s)" \
  110
+             % (subscription_id, item_name, str(price), str(expires_at)))
110 111
     charge(context, tenant_id, subscription_id, quantity, order_size, price)
111 112
     db.subscription_extend(context, subscription_id,
112 113
                            expires_at + relativedelta(**interval_info))
2  dough/context.py
@@ -20,6 +20,7 @@
20 20
 
21 21
 from nova import context as nova_context
22 22
 
  23
+
23 24
 def get_admin_context(read_deleted="no"):
24 25
     return nova_context.RequestContext(user_id=None,
25 26
                                        project_id=None,
@@ -27,6 +28,7 @@ def get_admin_context(read_deleted="no"):
27 28
                                        read_deleted=read_deleted,
28 29
                                        overwrite=False)
29 30
 
  31
+
30 32
 def get_context(tenant_id=None, **kwargs):
31 33
     return nova_context.RequestContext(user_id=None,
32 34
                                        project_id=tenant_id,
1  dough/exception.py
@@ -26,6 +26,7 @@
26 26
 
27 27
 from nova import exception
28 28
 
  29
+
29 30
 class RegionNotFound(exception.NotFound):
30 31
     message = _("Region %(region_id)s could not be found.")
31 32
 

0 notes on commit 991d5fb

Please sign in to comment.
Something went wrong with that request. Please try again.