diff --git a/examples/conf.yaml b/examples/conf.yaml index d439942..f42a6d4 100644 --- a/examples/conf.yaml +++ b/examples/conf.yaml @@ -33,31 +33,13 @@ collection: max_windows_per_cycle: 4 # defines which meter is mapped to which transformer meter_mappings: - # - - # # meter name as seen in ceilometer - # meter: instance - # # type of resource it maps to (seen on sales order) - # type: Virtual Machine - # # which transformer to use - # transformer: InstanceUptime - # # what unit type is coming in via the meter - # unit: second - # metadata: - # name: - # sources: - # # which keys to search for in the ceilometer entry metadata - # # this can be more than one as metadata is inconsistent between - # # source types - # - display_name - # availability zone: - # sources: - # - OS-EXT-AZ:availability_zone - - meter: state - # type of resource it maps to (seen on sales order) + # meter name as seen in ceilometer + meter: instance + # type of resource it maps to (seen on sales order) type: Virtual Machine # which transformer to use - transformer: Uptime + transformer: InstanceUptime # what unit type is coming in via the meter unit: second metadata: @@ -70,6 +52,9 @@ collection: availability zone: sources: - OS-EXT-AZ:availability_zone + host: + sources: + - instance_host - meter: ip.floating service: n1.ipv4 diff --git a/odoo/odoo-glue.py b/odoo/odoo-glue.py index 0204584..993434d 100755 --- a/odoo/odoo-glue.py +++ b/odoo/odoo-glue.py @@ -415,7 +415,6 @@ def get_tenant_usage(shell, tenant, start, end): for region in REGION_MAPPING.keys(): distil_client = getattr(shell, 'distil' + region.replace('-', '_')) raw_usage = distil_client.get_usage(tenant, start, end) - if not raw_usage: return None @@ -428,6 +427,7 @@ def get_tenant_usage(shell, tenant, start, end): name = res.get('name', res.get('ip address', '')) or res_id type = res.get('type') is_windows_instance = res.get('os_distro') == 'windows' + instance_host = res.get('host') for service_usage in res['services']: if service_usage['volume'] == 'unknown unit conversion': @@ -442,20 +442,18 @@ def get_tenant_usage(shell, tenant, start, end): if service_usage['unit'] == 'byte': v = Decimal(service_usage['volume']) service_usage['unit'] = 'gigabyte' - service_usage['volume'] = str(v / - Decimal(1024 * 1024 * 1024)) + service_usage['volume'] = str(v / Decimal(1024 * 1024 * 1024)) if service_usage['unit'] == 'second': # convert seconds to hours, rounding up. v = Decimal(service_usage['volume']) service_usage['unit'] = 'hour' - service_usage['volume'] = str(math.ceil(v / - Decimal(60 * 60))) + service_usage['volume'] = str(math.ceil(v / Decimal(60 * 60))) # drop zero usages. if not Decimal(service_usage['volume']): - print('WARNING: Dropping 0-volume line: %s' % - (service_usage,)) + log(shell.debug,'WARNING: Dropping 0-volume line: %s' % + (service_usage,)) continue if Decimal(service_usage['volume']) <= 0.00001: @@ -474,7 +472,8 @@ def get_tenant_usage(shell, tenant, start, end): usage.append({'product': service_usage['name'], 'name': name, 'volume': float(service_usage['volume']), - 'region': region}) + 'region': region, + 'resource_id': res_id}) # NOTE(flwang): If this usage line is for VM(instance), # and the instance is windows image, then a new usage line # is added. @@ -482,12 +481,14 @@ def get_tenant_usage(shell, tenant, start, end): usage.append({'product': service_usage['name'] + '-windows', 'name': name, 'volume': float(service_usage['volume']), - 'region': region}) + 'region': region, + 'resource_id': res_id, + 'host': instance_host}) # Aggregate traffic data for type, volume in traffic.items(): - print('Region: %s, traffic type: %s, volume: %s' % - (region, type, str(volume))) + log(shell.debug,'Region: %s, traffic type: %s, volume: %s' % + (region, type, str(volume))) usage.append({'product': type, 'name': TRAFFIC_MAPPING[type], 'volume': math.floor(volume), @@ -682,6 +683,8 @@ def login_odoo(shell): shell.Partner = shell.oerp.env['res.partner'] shell.Pricelist = shell.oerp.env['product.pricelist'] shell.Product = shell.oerp.env['product.product'] + shell.PurchaseOrder = shell.oerp.env['purchase.order'] + shell.PurchaseOrderline = shell.oerp.env['purchase.order.line'] def check_duplicate(order): @@ -749,6 +752,111 @@ def do_update_quote(shell, args): print('Failed to update order: %s' % id) +@arg('--start', type=str, metavar='START', + dest='START', required=True, + help='Start date for quote.') +@arg('--end', type=str, metavar='END', + dest='END', required=True, + help='End date for quote.') +@arg('--create-purchase-order', type=bool, metavar='CREATE_PURCHASE_ORDER', + dest='CREATE_PURCHASE_ORDER', required=False, default=False, + help='If or not create a purchase order based on usage for given time' + ' range, the default value is False.') +def do_windows(shell, args): + """ + By default, this sub command will print the windows hosts list based on + the usage for given time range. If the 'create-purchase-order' is set to + 'True' then a purchase order based on the monthly Windows instances usage + will be created on Odoo. + """ + user_roles = shell.keystone.session.auth.auth_ref['user']['roles'] + if {u'name': u'admin'} not in user_roles: + print('Admin permission is required.') + return + + login_odoo(shell) + + end_timestamp = datetime.datetime.strptime( + args.END, + '%Y-%m-%dT%H:%M:%S' + ) + billing_date = str((end_timestamp - datetime.timedelta(days=1)).date()) + + windows_usage = [] + windows_hosts = set() + tenants = shell.keystone.tenants.list() + pricelist = None + for t in tenants: + if t.name == 'openstack': + # NOTE(flwang): Using a default tenant to find parter and + # pricelist of root parter + partner = find_oerp_partner_for_tenant(shell, t) + root_partner = find_root_partner(shell, partner) + pricelist, _ = root_partner['property_product_pricelist'] + usages = get_tenant_usage(shell, t.id, args.START, args.END) + for u in usages: + # TODO(flwang): Using regex to match 'c1.c%dr%d-windows' + if u['product'].endswith('-windows'): + windows_usage.append(u) + if u['host'] not in windows_hosts: + windows_hosts.add(u['host']) + print(u['host']) + + if args.CREATE_PURCHASE_ORDER: + generate_purchase_order(shell, args, windows_usage, + billing_date, pricelist) + +def generate_purchase_order(shell, args, usage, billing_date, pricelist): + """ + Call odoo API to create a new purchase order + """ + # TODO(flwang): Partner id is hardcoded for Windows license provider + # location id is hardcoded since we don't care the delivery location + partner_id = 5618 + localtion_id = 10 + try: + order_dict = { + 'partner_id': partner_id, + 'pricelist_id': pricelist, + 'partner_invoice_id': partner_id, + 'partner_shipping_id': partner_id, + 'date_order': billing_date, + 'location_id': localtion_id, + } + order = shell.PurchaseOrder.create(order_dict) + + for m in sorted(usage, key=lambda m: m['product']): + prod = find_oerp_product(shell, m['region'], m['product']) + usage_dict = { + 'order_id': order, + 'date_planned': billing_date, + 'account_analytic_id': '', + 'product_id': prod['id'], + 'product_uom': prod['uom_id'][0], + 'product_qty': math.fabs(m['volume']), + 'name': m['resource_id'], # Hide the instance name + 'price_unit': get_price(shell, pricelist, prod, m['volume']) + } + if usage_dict['product_qty'] < 0.005: + print( + '%s is too small for %s.' % + (str(usage_dict['product_qty']), m['name']) + ) + continue + + shell.PurchaseOrderline.create(usage_dict) + except odoorpc.error.RPCError as e: + exc_type, exc_value, exc_traceback = sys.exc_info() + traceback.print_exception(exc_type, exc_value, exc_traceback, + limit=2, file=sys.stdout) + raise e + except Exception as e: + exc_type, exc_value, exc_traceback = sys.exc_info() + traceback.print_exception(exc_type, exc_value, exc_traceback, + limit=2, file=sys.stdout) + raise e + + def print_dict(d, max_column_width=80): pt = prettytable.PrettyTable(['Property', 'Value'], caching=False) pt.align = 'l'