Source code for mss.swift_storage_backend

#!/usr/bin/env python
# coding:utf-8

# -- Standard lib ------------------------------------------------------------
from hashlib import sha1
from time import time
import threading
import commands
import logging
import hmac
import re
import os

# -- 3rd party ---------------------------------------------------------------
from VestaRestPackage.app_objects import APP
from flask import redirect
import requests

# -- Project specific --------------------------------------------------------
from .abstract_storage_backend import AbstractStorageBackend
from .exceptions import SwiftException

MSS_CONFIG = APP.config['MSS']
TOKEN_RENEWAL_FREQ = MSS_CONFIG['TOKEN_RENEWAL_FREQ']
TEMP_URL_DEFAULT_VALIDITY = MSS_CONFIG['TEMP_URL_DEFAULT_VALIDITY']
STORAGE_SERVICE_CONTAINER = MSS_CONFIG['STORAGE_SERVICE_CONTAINER']
SWIFT_CONFIG = MSS_CONFIG['SWIFT']

STORAGE_URL_IGNORE_PREFIX_FOR_TEMP_URL = None
if "STORAGE_URL_IGNORE_PREFIX_FOR_TEMP_URL" in MSS_CONFIG:
    STORAGE_URL_IGNORE_PREFIX_FOR_TEMP_URL =\
        MSS_CONFIG['STORAGE_URL_IGNORE_PREFIX_FOR_TEMP_URL']

# Swift credential options to obtain (AUTH_STORAGE and AUTH_TOKEN):
# For v2.
#    "V2_REMOTE", if we need to connect to a machine on the open stack network
#    and call python-swift client there.  "V2_LOCAL", we can call swift auth
#    api from local machine using python-swift client.
# For v1:
#   "V1_LOCAL", we can obtain them from from local machine using curl.

SWIFT_AUTHENTIFICATION_OPTIONS = "V2_REMOTE"
if "SWIFT_AUTHENTIFICATION_OPTIONS" in MSS_CONFIG:
    SWIFT_AUTHENTIFICATION_OPTIONS =\
        MSS_CONFIG["SWIFT_AUTHENTIFICATION_OPTIONS"]

SWIFT_REDIRECT_URL = MSS_CONFIG.get('SWIFT_REDIRECT_URL', None)


[docs]class SwiftToken(object): """ Handler class for tokens to be used with swift. """ def __init__(self, storage_url=None, auth_token=None): self.logger = logging.getLogger(__name__ + ".SwiftToken") self.storage_url = storage_url self.auth_token = auth_token self.expire = int(time() + TOKEN_RENEWAL_FREQ) self.logger.debug(u"Creating token at {0}, expires: {1}". format(storage_url, self.expire))
[docs] def is_valid(self): """ Certify token validity. """ self.logger.debug(u"Checking token validity") return (self.storage_url is not None and self.auth_token is not None and self.expire > time())
class SwiftStorageBackend(AbstractStorageBackend): def __init__(self): self.logger = logging.getLogger(__name__ + ".SwiftStorageBackend") self.logger.info(u"Instantiating swift storage backend") self.token = None self.temp_key = None AbstractStorageBackend.__init__(self) try: self.__renew_swift_token() except SwiftException as exc: # Cannot do anything else apart logging the error # (No request have been done) self.logger.error(unicode(exc)) def __set_temp_key(self, key=STORAGE_SERVICE_CONTAINER): self.temp_key = key headers = {'X-Auth-Token': self.__get_token().auth_token, 'X-Account-Meta-Temp-URL-Key': self.temp_key} response = requests.post(self.__get_token().storage_url, headers=headers, verify=False) if response.status_code != requests.codes.ok: response.raise_for_status() @staticmethod def __async_renew_swift_token(out, cmd): out['cmd_output'] = commands.getstatusoutput(cmd) def __get_cmd_for_swift_credentials(self, swiftAuthOptions): auth_url = SWIFT_CONFIG['os-auth-url'] tenant = SWIFT_CONFIG['os-tenant-name'] user = SWIFT_CONFIG['os-username'] passwd = SWIFT_CONFIG['os-password'] if "V2" in swiftAuthOptions: region = SWIFT_CONFIG['os-region-name'] ssh_cmd = ("swift " "--os-auth-url '{auth_url}' " "--os-tenant-name '{tenant}' " "--os-username '{user}' " "--os-password '{pw}' " "--os-region-name {region} {swift_cmd}". format(auth_url=auth_url, tenant=tenant, user=user, pw=passwd, region=region, swift_cmd='stat -v')) if swiftAuthOptions == "V2_REMOTE": cert = os.path.abspath(SWIFT_CONFIG['certificate_filename']) token_user = SWIFT_CONFIG['token_server_user'] token_server = SWIFT_CONFIG['token_server'] cmd = ('ssh -oStrictHostKeyChecking=no ' '-i {cert} {user}@{server} \"{cmd}\"'. format(cert=cert, user=token_user, server=token_server, cmd=ssh_cmd)) else: cmd = ssh_cmd else: cmd = ("swift -A '{auth_url}' -U '{tenant}':'{user}' -K '{pw}' " "{swift_cmd}". format(auth_url=auth_url, tenant=tenant, user=user, pw=passwd, swift_cmd='stat -v')) self.logger.debug(cmd) out = dict() args = (out, cmd) thr = threading.Thread(target=self.__async_renew_swift_token, args=args) thr.start() thr.join(timeout=5) if thr.is_alive(): msg = ('Timeout occurred renewing swift token\n{cmd}' .format(cmd='Command:\n{cmd}'.format(cmd=cmd))) raise SwiftException(msg) return out['cmd_output'] def __renew_swift_token(self): self.logger.info(u"Renewing swift token") cmd_output = self.__get_cmd_for_swift_credentials( SWIFT_AUTHENTIFICATION_OPTIONS) if "Unauthorized" in cmd_output[1]: raise SwiftException(cmd_output[1]) lines = cmd_output[1].split('\n') self.token = SwiftToken() for line in lines: match = re.search('StorageURL: *(.*)$', line) if match: self.token.storage_url = match.group(1) else: match = re.search('Auth Token: *(.*)$', line) if match: self.token.auth_token = match.group(1) if not self.token.is_valid(): out = cmd_output[1].replace('\\r\\n', '\r\n') t_cmd = 'Command:\n{cmd}'.format(cmd=cmd_output[0]) t_out = '\nOutput:\n{out}'.format(out=out) msg = ('Cannot obtain a valid swift token\n{cmd}\n{out}' .format(cmd=t_cmd, out=t_out)) raise SwiftException(msg) # Make sure the temp key is OK each time we renew the token self.__set_temp_key() def __get_token(self): """ Renew Swift token. """ if self.token is None or not self.token.is_valid(): self.__renew_swift_token() return self.token def file_exists(self, filename): """ Check for the existence of a file :param filename: The unique document URL :type filename: str :returns: True if the file exists in the backend """ headers = {'X-Auth-Token': self.__get_token().auth_token} url = ('{url}/{container}/{fn}'. format(url=self.__get_token().storage_url, container=STORAGE_SERVICE_CONTAINER, fn=os.path.basename(filename))) response = requests.head(url, headers=headers, verify=False) if response.status_code != requests.codes.ok: return False return True # TODO: Upload as a stream rather than a local copy : # http://docs.python-requests.org/en/latest/user/quickstart/#make-a-request # http://toolbelt.readthedocs.org/en/latest/ def upload(self, filename): """ Upload the given file to the backend storage :param filename: The unique document URL :type filename: str """ headers = {'X-Auth-Token': self.__get_token().auth_token, 'Content-Type': 'application/octet-stream'} url = ('{url}/{container}/{fn}'. format(url=self.__get_token().storage_url, container=STORAGE_SERVICE_CONTAINER, fn=os.path.basename(filename))) self.logger.info(u"Uploading file {fn} to backend storage at {u}". format(fn=filename, u=url)) response = requests.put(url, headers=headers, data=open(filename, 'rb'), verify=False) if response.status_code != requests.codes.ok: response.raise_for_status() def download(self, filename): """ Make a flask response containing a redirect to a swift temp url :param filename: (String) The unique document URL :return: A flask response with the proper redirection """ self.logger.info(u"Getting file {fn}".format(fn=filename)) return redirect(self.get_temp_url(filename, method='GET')) def delete(self, filename): """ Delete a file from swift :param filename: (String) The unique document URL """ headers = {'X-Auth-Token': self.__get_token().auth_token} url = ('{url}/{container}/{fn}'. format(url=self.__get_token().storage_url, container=STORAGE_SERVICE_CONTAINER, fn=os.path.basename(filename))) self.logger.info(u"Deleting file {fn}".format(fn=filename)) response = requests.delete(url, headers=headers, verify=False) if response.status_code != requests.codes.ok: response.raise_for_status() def purge_container(self): """ Remove all objects from the container """ headers = {'X-Auth-Token': self.__get_token().auth_token, 'Accept': 'application/json'} url = ('{url}/{container}'. format(url=self.__get_token().storage_url, container=STORAGE_SERVICE_CONTAINER)) self.logger.info(u"Removing all objects from {u}".format(u=url)) response = requests.get(url, headers=headers, verify=False) if response.status_code != requests.codes.ok: response.raise_for_status() file_list = response.json() deleted_files = list() for file_desc in file_list: deleted_files.append(file_desc['name']) self.delete(file_desc['name']) return deleted_files def get_temp_url(self, filename, method='GET', validity_in_secs=TEMP_URL_DEFAULT_VALIDITY, ignore_prefix=STORAGE_URL_IGNORE_PREFIX_FOR_TEMP_URL, redirect_url=SWIFT_REDIRECT_URL): expires = int(time() + validity_in_secs) storage_url_parts = self.__get_token().storage_url.split('/', 3) self.logger.debug(storage_url_parts) path = ('/{url}/{container}/{fn}'. format(url=storage_url_parts[3], container=STORAGE_SERVICE_CONTAINER, fn=filename)) self.logger.debug("Temp url path {path}".format(path=path)) # hmac_body = '%s\n%s\n%s' % (method, expires, path) encode_path = path self.logger.debug("Encode {ignore_prefix}".format( ignore_prefix=ignore_prefix)) if ignore_prefix is not None and encode_path.startswith(ignore_prefix): encode_path = encode_path[len(ignore_prefix):] self.logger.debug("Encode {encode_path}".format( encode_path=encode_path)) hmac_body = ('{method}\n{exp}\n{encode_path}'.format( method=method, exp=expires, encode_path=encode_path)) sig = hmac.new(self.temp_key, hmac_body, sha1).hexdigest() args = 'temp_url_sig={0}&temp_url_expires={1}'.format(sig, expires) storageUrl = self.__get_token().storage_url if redirect_url: storageUrl = ('{redirect_url}/{extra}'.format( redirect_url=redirect_url, extra=storage_url_parts[3])) base_url = ('{url}/{container}/{fn}'. format(url=storageUrl, container=STORAGE_SERVICE_CONTAINER, fn=filename)) temp_url = '{base_url}?{args}'.format(base_url=base_url, args=args) self.logger.debug("Temp url path {temp_url}".format(temp_url=temp_url)) return temp_url