blob: e568285824888b9f9ce95edd0384e0efd64323e8 [file] [log] [blame]
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import with_statement
"""
A class which handles loading the pricing files.
"""
import os.path
from os.path import join as pjoin
try:
import simplejson as json
try:
JSONDecodeError = json.JSONDecodeError
except AttributeError:
# simplejson < 2.1.0 does not have the JSONDecodeError exception class
JSONDecodeError = ValueError
except ImportError:
import json
JSONDecodeError = ValueError
from libcloud.utils.connection import get_response_object
__all__ = [
'get_pricing',
'get_size_price',
'set_pricing',
'clear_pricing_data',
'download_pricing_file'
]
# Default URL to the pricing file
DEFAULT_FILE_URL = 'https://git-wip-us.apache.org/repos/asf?p=libcloud.git;a=blob_plain;f=libcloud/data/pricing.json' # NOQA
CURRENT_DIRECTORY = os.path.dirname(os.path.abspath(__file__))
DEFAULT_PRICING_FILE_PATH = pjoin(CURRENT_DIRECTORY, 'data/pricing.json')
CUSTOM_PRICING_FILE_PATH = os.path.expanduser('~/.libcloud/pricing.json')
# Pricing data cache
PRICING_DATA = {
'compute': {},
'storage': {}
}
VALID_PRICING_DRIVER_TYPES = ['compute', 'storage']
def get_pricing_file_path(file_path=None):
if os.path.exists(CUSTOM_PRICING_FILE_PATH) and \
os.path.isfile(CUSTOM_PRICING_FILE_PATH):
# Custom pricing file is available, use it
return CUSTOM_PRICING_FILE_PATH
return DEFAULT_PRICING_FILE_PATH
def get_pricing(driver_type, driver_name, pricing_file_path=None):
"""
Return pricing for the provided driver.
:type driver_type: ``str``
:param driver_type: Driver type ('compute' or 'storage')
:type driver_name: ``str``
:param driver_name: Driver name
:type pricing_file_path: ``str``
:param pricing_file_path: Custom path to a price file. If not provided
it uses a default path.
:rtype: ``dict``
:return: Dictionary with pricing where a key name is size ID and
the value is a price.
"""
if driver_type not in VALID_PRICING_DRIVER_TYPES:
raise AttributeError('Invalid driver type: %s', driver_type)
if driver_name in PRICING_DATA[driver_type]:
return PRICING_DATA[driver_type][driver_name]
if not pricing_file_path:
pricing_file_path = get_pricing_file_path(file_path=pricing_file_path)
with open(pricing_file_path) as fp:
content = fp.read()
pricing_data = json.loads(content)
size_pricing = pricing_data[driver_type][driver_name]
for driver_type in VALID_PRICING_DRIVER_TYPES:
# pylint: disable=maybe-no-member
pricing = pricing_data.get(driver_type, None)
if pricing:
PRICING_DATA[driver_type] = pricing
return size_pricing
def set_pricing(driver_type, driver_name, pricing):
"""
Populate the driver pricing dictionary.
:type driver_type: ``str``
:param driver_type: Driver type ('compute' or 'storage')
:type driver_name: ``str``
:param driver_name: Driver name
:type pricing: ``dict``
:param pricing: Dictionary where a key is a size ID and a value is a price.
"""
PRICING_DATA[driver_type][driver_name] = pricing
def get_size_price(driver_type, driver_name, size_id):
"""
Return price for the provided size.
:type driver_type: ``str``
:param driver_type: Driver type ('compute' or 'storage')
:type driver_name: ``str``
:param driver_name: Driver name
:type size_id: ``str`` or ``int``
:param size_id: Unique size ID (can be an integer or a string - depends on
the driver)
:rtype: ``float``
:return: Size price.
"""
pricing = get_pricing(driver_type=driver_type, driver_name=driver_name)
try:
price = float(pricing[size_id])
except KeyError:
# Price not available
price = None
return price
def invalidate_pricing_cache():
"""
Invalidate pricing cache for all the drivers.
"""
PRICING_DATA['compute'] = {}
PRICING_DATA['storage'] = {}
def clear_pricing_data():
"""
Invalidate pricing cache for all the drivers.
Note: This method does the same thing as invalidate_pricing_cache and is
here for backward compatibility reasons.
"""
invalidate_pricing_cache()
def invalidate_module_pricing_cache(driver_type, driver_name):
"""
Invalidate the cache for the specified driver.
:type driver_type: ``str``
:param driver_type: Driver type ('compute' or 'storage')
:type driver_name: ``str``
:param driver_name: Driver name
"""
if driver_name in PRICING_DATA[driver_type]:
del PRICING_DATA[driver_type][driver_name]
def download_pricing_file(file_url=DEFAULT_FILE_URL,
file_path=CUSTOM_PRICING_FILE_PATH):
"""
Download pricing file from the file_url and save it to file_path.
:type file_url: ``str``
:param file_url: URL pointing to the pricing file.
:type file_path: ``str``
:param file_path: Path where a download pricing file will be saved.
"""
dir_name = os.path.dirname(file_path)
if not os.path.exists(dir_name):
# Verify a valid path is provided
msg = ('Can\'t write to %s, directory %s, doesn\'t exist' %
(file_path, dir_name))
raise ValueError(msg)
if os.path.exists(file_path) and os.path.isdir(file_path):
msg = ('Can\'t write to %s file path because it\'s a'
' directory' % (file_path))
raise ValueError(msg)
response = get_response_object(file_url)
body = response.body
# Verify pricing file is valid
try:
data = json.loads(body)
except JSONDecodeError:
msg = 'Provided URL doesn\'t contain valid pricing data'
raise Exception(msg)
# pylint: disable=maybe-no-member
if not data.get('updated', None):
msg = 'Provided URL doesn\'t contain valid pricing data'
raise Exception(msg)
# No need to stream it since file is small
with open(file_path, 'w') as file_handle:
file_handle.write(body)