Add api support for jobs

removes distinction between actions and configs in the api

The v1/jobs endpoint is the single point of access to the api
to store, search and retrieve operating instruction about
a single freezer run.

optional scheduling informations can be added to a job to
allow for future/recurrent job execution.

removes api endpoints
  - v1/actions
  - v1/configs

adds api endpoints:
  - v1/jobs

Implements blueprint: freezer-api-jobs

Change-Id: Ideeef14dfccd21ddd10b4faa438124c04d2e1ff8
This commit is contained in:
Fabrizio Vanni 2015-06-08 12:05:08 +01:00 committed by Fausto Marzi
parent 2f690e1ac5
commit 5d75e3c789
6 changed files with 132 additions and 235 deletions

View File

@ -1,101 +0,0 @@
"""
Copyright 2015 Hewlett-Packard
Licensed 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.
This product includes cryptographic software written by Eric Young
(eay@cryptsoft.com). This product includes software written by Tim
Hudson (tjh@cryptsoft.com).
========================================================================
"""
import json
import requests
from freezer.apiclient import exceptions
class ActionManager(object):
def __init__(self, client):
self.client = client
self.endpoint = self.client.endpoint + '/v1/actions/'
@property
def headers(self):
return {'X-Auth-Token': self.client.auth_token}
def create(self, doc):
r = requests.post(self.endpoint,
data=json.dumps(doc),
headers=self.headers)
if r.status_code != 201:
raise exceptions.MetadataCreationFailure(
"[*] Error {0}: {1}".format(r.status_code, r.text))
action_id = r.json()['action_id']
return action_id
def delete(self, action_id):
endpoint = self.endpoint + action_id
r = requests.delete(endpoint, headers=self.headers)
if r.status_code != 204:
raise exceptions.MetadataDeleteFailure(
"[*] Error {0}".format(r.status_code))
def list(self, limit=10, offset=0, search=None):
"""
Retrieves a list of action info structures
:param limit: number of result to return (optional, default 10)
:param offset: order of first document (optional, default 0)
:param search: structured query (optional)
can contain:
* "match": list of {field, value}
Example:
{ "match": [
{"description": "some search text here"},
{"backup_name": "mydata"},
...
],
}
"""
data = json.dumps(search) if search else None
query = {'limit': int(limit), 'offset': int(offset)}
r = requests.get(self.endpoint, headers=self.headers,
params=query, data=data)
if r.status_code != 200:
raise exceptions.MetadataGetFailure(
"[*] Error {0}: {1}".format(r.status_code, r.text))
return r.json()['actions']
def get(self, action_id):
endpoint = self.endpoint + action_id
r = requests.get(endpoint, headers=self.headers)
if r.status_code == 200:
return r.json()
if r.status_code == 404:
return None
raise exceptions.MetadataGetFailure(
"[*] Error {0}".format(r.status_code))
def update(self, action_id, update_doc):
endpoint = self.endpoint + action_id
r = requests.patch(endpoint,
headers=self.headers,
data=json.dumps(update_doc))
if r.status_code != 200:
raise exceptions.MetadataUpdateFailure(
"[*] Error {0}: {1}".format(r.status_code, r.text))
return r.json()['version']
def set_status(self, action_id, new_status):
return self.update(action_id, {'status': new_status})

View File

@ -22,7 +22,7 @@ Hudson (tjh@cryptsoft.com).
import json
import requests
from freezer.apiclient import exceptions
import exceptions
class BackupsManager(object):
@ -40,8 +40,7 @@ class BackupsManager(object):
data=json.dumps(backup_metadata),
headers=self.headers)
if r.status_code != 201:
raise exceptions.MetadataCreationFailure(
"[*] Error {0}".format(r.status_code))
raise exceptions.ApiClientException(r)
backup_id = r.json()['backup_id']
return backup_id
@ -49,8 +48,7 @@ class BackupsManager(object):
endpoint = self.endpoint + backup_id
r = requests.delete(endpoint, headers=self.headers)
if r.status_code != 204:
raise exceptions.MetadataDeleteFailure(
"[*] Error {0}".format(r.status_code))
raise exceptions.ApiClientException(r)
def list(self, limit=10, offset=0, search=None):
"""
@ -70,8 +68,7 @@ class BackupsManager(object):
r = requests.get(self.endpoint, headers=self.headers,
params=query, data=data)
if r.status_code != 200:
raise exceptions.MetadataGetFailure(
"[*] Error {0}".format(r.status_code))
raise exceptions.ApiClientException(r)
return r.json()['backups']
@ -82,5 +79,4 @@ class BackupsManager(object):
return r.json()
if r.status_code == 404:
return None
raise exceptions.MetadataGetFailure(
"[*] Error {0}".format(r.status_code))
raise exceptions.ApiClientException(r)

View File

@ -1,5 +1,5 @@
"""
Copyright 2014 Hewlett-Packard
Copyright 2015 Hewlett-Packard
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -19,23 +19,30 @@ Hudson (tjh@cryptsoft.com).
========================================================================
"""
import os
import sys
import socket
from openstackclient.identity import client as os_client
possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]),
os.pardir, os.pardir, os.pardir))
if os.path.exists(os.path.join(possible_topdir, 'freezer', '__init__.py')):
sys.path.insert(0, possible_topdir)
from backups import BackupsManager
from registration import RegistrationManager
from jobs import JobManager
from freezer.apiclient.backups import BackupsManager
from freezer.apiclient.registration import RegistrationManager
from freezer.apiclient.actions import ActionManager
from freezer.apiclient.configs import ConfigsManager
import exceptions
class cached_property(object):
def __init__(self, func):
self.__doc__ = getattr(func, '__doc__')
self.func = func
def __get__(self, obj, cls):
if obj is None:
return self
value = obj.__dict__[self.func.__name__] = self.func(obj)
return value
class Client(object):
def __init__(self, version='1',
@ -54,18 +61,19 @@ class Client(object):
self.auth_url = auth_url
self._endpoint = endpoint
self.session = session
self._auth = None
self.backups = BackupsManager(self)
self.registration = RegistrationManager(self)
self.actions = ActionManager(self)
self.configs = ConfigsManager(self)
self.jobs = JobManager(self)
def _update_api_endpoint(self):
@cached_property
def endpoint(self):
if self._endpoint:
return self._endpoint
services = self.auth.services.list()
try:
freezer_service = next(x for x in services if x.name == 'freezer')
except:
raise exceptions.AuthFailure(
raise exceptions.ApiClientException(
'freezer service not found in services list')
endpoints = self.auth.endpoints.list()
try:
@ -73,40 +81,37 @@ class Client(object):
next(x for x in endpoints
if x.service_id == freezer_service.id)
except:
raise exceptions.AuthFailure(
raise exceptions.ApiClientException(
'freezer endpoint not found in endpoint list')
self._endpoint = freezer_endpoint.publicurl
return freezer_endpoint.publicurl
@property
@cached_property
def auth(self):
if self._auth is None:
if self.username and self.password:
self._auth = os_client.IdentityClientv2(
auth_url=self.auth_url,
username=self.username,
password=self.password,
tenant_name=self.tenant_name)
elif self.token:
self._auth = os_client.IdentityClientv2(
endpoint=self.auth_url,
token=self.token)
else:
raise exceptions.AuthFailure("Missing auth credentials")
return self._auth
if self.username and self.password:
_auth = os_client.IdentityClientv2(
auth_url=self.auth_url,
username=self.username,
password=self.password,
tenant_name=self.tenant_name)
elif self.token:
_auth = os_client.IdentityClientv2(
endpoint=self.auth_url,
token=self.token)
else:
raise exceptions.ApiClientException("Missing auth credentials")
return _auth
@property
def auth_token(self):
return self.auth.auth_token
@property
def endpoint(self):
if self._endpoint is None:
self._update_api_endpoint()
return self._endpoint
def api_exists(self):
try:
if self.endpoint is not None:
return True
except:
return False
@cached_property
def client_id(self):
return '{0}_{1}'.format(self.auth.project_id, socket.gethostname())

View File

@ -23,60 +23,48 @@ import json
class ApiClientException(Exception):
def __init__(self, r):
@staticmethod
def get_message_from_api_response(r):
"""
returns a message based on information from a api-formatted response
if available, otherwise None
api-formatted response should be:
{
title: "error title string",
description: "error description string"
}
:param r: response object
:return: string with error message or None if it fails
"""
try:
body = json.loads(r.text)
message = "[*] Error {0}: {1}".format(
r.status_code,
body['description'])
except:
message = r
message = None
return message
@staticmethod
def get_message_from_response(r):
"""
composes the error message using information eventually present
in the response (http error code and the http response body)
:param r: response object
:return: string with error message or None if it fails
"""
try:
message = "[*] Error {0}: {1}".format(
r.status_code,
r.text)
except:
message = None
return message
def __init__(self, r):
message = self.get_message_from_api_response(r) or \
self.get_message_from_response(r) or \
str(r)
super(ApiClientException, self).__init__(message)
def __str__(self):
return self.message
class MetadataCreationFailure(ApiClientException):
def __init__(self, r=''):
super(self.__class__, self).__init__(r)
class MetadataGetFailure(ApiClientException):
def __init__(self, r=''):
super(self.__class__, self).__init__(r)
class MetadataDeleteFailure(ApiClientException):
def __init__(self, r=''):
super(self.__class__, self).__init__(r)
class AuthFailure(ApiClientException):
def __init__(self, r=''):
super(self.__class__, self).__init__(r)
class MetadataUpdateFailure(ApiClientException):
def __init__(self, r=''):
super(self.__class__, self).__init__(r)
class ConfigCreationFailure(ApiClientException):
def __init__(self, r=''):
super(self.__class__, self).__init__(r)
class ConfigGetFailure(ApiClientException):
def __init__(self, r=''):
super(self.__class__, self).__init__(r)
class ConfigDeleteFailure(ApiClientException):
def __init__(self, r=''):
super(self.__class__, self).__init__(r)
class ConfigUpdateFailure(ApiClientException):
def __init__(self, r=''):
super(self.__class__, self).__init__(r)

View File

@ -1,5 +1,5 @@
"""
Copyright 2014 Hewlett-Packard
Copyright 2015 Hewlett-Packard
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -22,63 +22,76 @@ Hudson (tjh@cryptsoft.com).
import json
import requests
from freezer.apiclient import exceptions
import exceptions
class ConfigsManager(object):
class JobManager(object):
def __init__(self, client):
self.client = client
self.endpoint = self.client.endpoint + '/v1/configs/'
self.endpoint = self.client.endpoint + '/v1/jobs/'
@property
def headers(self):
return {'X-Auth-Token': self.client.auth_token}
def create(self, config_file):
r = requests.post(self.endpoint,
data=json.dumps(config_file),
def create(self, doc, job_id=''):
job_id = job_id or doc.get('job_id', '')
endpoint = self.endpoint + job_id
doc['client_id'] = doc.get('client_id', '') or self.client.client_id
r = requests.post(endpoint,
data=json.dumps(doc),
headers=self.headers)
if r.status_code != 201:
raise exceptions.ConfigCreationFailure(
"[*] Error {0}".format(r.status_code))
config_id = r.json()['config_id']
return config_id
raise exceptions.ApiClientException(r)
job_id = r.json()['job_id']
return job_id
def delete(self, config_id):
endpoint = self.endpoint + config_id
def delete(self, job_id):
endpoint = self.endpoint + job_id
r = requests.delete(endpoint, headers=self.headers)
if r.status_code != 204:
raise exceptions.ConfigDeleteFailure(
"[*] Error {0}".format(r.status_code))
raise exceptions.ApiClientException(r)
def list(self, limit=10, offset=0, search=None):
def list_all(self, limit=10, offset=0, search=None):
data = json.dumps(search) if search else None
query = {'limit': int(limit), 'offset': int(offset)}
r = requests.get(self.endpoint, headers=self.headers,
params=query, data=data)
if r.status_code != 200:
raise exceptions.ConfigGetFailure(
"[*] Error {0}".format(r.status_code))
raise exceptions.ApiClientException(r)
return r.json()['jobs']
return r.json()['configs']
def list(self, limit=10, offset=0, search={}, client_id=None):
client_id = client_id or self.client.client_id
new_search = search.copy()
new_search['match'] = search.get('match', [])
new_search['match'].append({'client_id': client_id})
return self.list_all(limit, offset, new_search)
def get(self, config_id):
endpoint = self.endpoint + config_id
def get(self, job_id):
endpoint = self.endpoint + job_id
r = requests.get(endpoint, headers=self.headers)
if r.status_code == 200:
return r.json()
if r.status_code == 404:
return None
raise exceptions.ConfigGetFailure(
"[*] Error {0}".format(r.status_code))
raise exceptions.ApiClientException(r)
def update(self, config_id, update_doc):
endpoint = self.endpoint + config_id
def update(self, job_id, update_doc):
endpoint = self.endpoint + job_id
r = requests.patch(endpoint,
headers=self.headers,
data=json.dumps(update_doc))
if r.status_code != 200:
raise exceptions.ConfigUpdateFailure(
"[*] Error {0}: {1}".format(r.status_code, r.text))
raise exceptions.ApiClientException(r)
return r.json()['version']
def start_job(self, job_id):
return self.update(job_id, {'job_schedule': {'event': 'start'}})
def stop_job(self, job_id):
return self.update(job_id, {'job_schedule': {'event': 'stop'}})
def abort_job(self, job_id):
return self.update(job_id, {'job_schedule': {'event': 'abort'}})

View File

@ -22,7 +22,7 @@ Hudson (tjh@cryptsoft.com).
import json
import requests
from freezer.apiclient import exceptions
import exceptions
class RegistrationManager(object):
@ -40,8 +40,7 @@ class RegistrationManager(object):
data=json.dumps(client_info),
headers=self.headers)
if r.status_code != 201:
raise exceptions.MetadataCreationFailure(
"[*] Error {0}: {1}".format(r.status_code, r.text))
raise exceptions.ApiClientException(r)
client_id = r.json()['client_id']
return client_id
@ -49,8 +48,7 @@ class RegistrationManager(object):
endpoint = self.endpoint + client_id
r = requests.delete(endpoint, headers=self.headers)
if r.status_code != 204:
raise exceptions.MetadataDeleteFailure(
"[*] Error {0}".format(r.status_code))
raise exceptions.ApiClientException(r)
def list(self, limit=10, offset=0, search=None):
"""
@ -74,8 +72,7 @@ class RegistrationManager(object):
r = requests.get(self.endpoint, headers=self.headers,
params=query, data=data)
if r.status_code != 200:
raise exceptions.MetadataGetFailure(
"[*] Error {0}: {1}".format(r.status_code, r.text))
raise exceptions.ApiClientException(r)
return r.json()['clients']
def get(self, client_id):
@ -85,5 +82,4 @@ class RegistrationManager(object):
return r.json()
if r.status_code == 404:
return None
raise exceptions.MetadataGetFailure(
"[*] Error {0}".format(r.status_code))
raise exceptions.ApiClientException(r)