add initial v2 api

this change implements the initial v2 experimental api. it is
implemented as an optional series of class that can be configured
through the paste deploy configuration file.

* add wsgi auth validator
* add middleware router
* add v2 endpoints
* add v2 flask blueprint object
* add optional paste filter and composite
* add developer docs for v2 api

Change-Id: I74627c0879851b354b5043f8a6ff91bae8438bb1
Partial-Implements: bp v2-api-experimental-impl
This commit is contained in:
Michael McCune 2016-01-22 09:46:54 -05:00
parent 493561692c
commit a9137addb4
17 changed files with 1019 additions and 7 deletions

View File

@ -0,0 +1,99 @@
API Version 2 Development
=========================
The sahara project is currently in the process of creating a new
RESTful application programming interface (API). This interface is
experimental and will not be enabled until it has achieved feature
parity with the current (version 1.1) API.
This document defines the steps necessary to enable and communicate
with the new API. This API has a few fundamental changes from the
previous APIs and they should be noted before proceeding with
development work.
.. warning::
This API is currently marked as experimental. It is not supported
by the sahara python client. These instructions are included purely
for developers who wish to help participate in the development
effort.
Enabling the experimental API
-----------------------------
There are a few changes to the WSGI pipeline that must be made to
enable the new v2 API. These changes will leave the 1.0 and 1.1 API
versions in place and will not adjust their communication parameters.
To begin, uncomment, or add, the following sections in your
api-paste.ini file:
.. sourcecode:: ini
[app:sahara_apiv2]
paste.app_factory = sahara.api.middleware.sahara_middleware:RouterV2.factory
[filter:auth_validator_v2]
paste.filter_factory = sahara.api.middleware.auth_valid:AuthValidatorV2.factory
These lines define a new authentication filter for the v2 API, and
define the application that will handle the new calls.
With these new entries in the paste configuration, we can now enable
them with the following changes to the api-paste.ini file:
.. sourcecode:: ini
[pipeline:sahara]
pipeline = cors request_id acl auth_validator_v2 sahara_api
[composite:sahara_api]
use = egg:Paste#urlmap
/: sahara_apiv2
There are 2 significant changes occurring here; changing the
authentication validator in the pipline, and changing the root "/"
application to the new v2 handler.
At this point the sahara API server should be configured to accept
requests on the new v2 endpoints.
Communicating with the v2 API
-----------------------------
The v2 API makes at least one major change from the previous versions,
removing the OpenStack project identifier from the URL. Instead of
adding this UUID to the URL, it is now required to be included as a
header named ``OpenStack-Project-ID``.
For example, in previous versions of the API, a call to get the list of
clusters for project "12345678-1234-1234-1234-123456789ABC" would have
been made as follows::
GET /v1.1/12345678-1234-1234-1234-123456789ABC/clusters
X-Auth-Token: {valid auth token}
This call would now be made to the following URL, while including the
project identifier in a header named ``OpenStack-Project-ID``::
GET /v2/clusters
X-Auth-Token: {valid auth token}
OpenStack-Project-ID: 12345678-1234-1234-1234-123456789ABC
Using a tool like `HTTPie <https://github.com/jkbrzt/httpie>`_, the
same request could be made like this::
$ httpie http://{sahara service ip:port}/v2/clusters \
X-Auth-Token:{valid auth token} \
OpenStack-Project-ID:12345678-1234-1234-1234-123456789ABC
Following the implementation progress
-------------------------------------
As the creation of this API will be under regular change until it moves
out of the experimental phase, a wiki page has been established to help
track the progress.
https://wiki.openstack.org/wiki/Sahara/api-v2
This page will help to coordinate the various reviews, specs, and work
items that are a continuing facet of this work.

View File

@ -95,6 +95,7 @@ Developer Guide
devref/adding_database_migrations
devref/testing
devref/log.guidelines
devref/apiv2
**Background Concepts for Sahara**

View File

@ -8,6 +8,10 @@ use = egg:Paste#urlmap
[app:sahara_apiv11]
paste.app_factory = sahara.api.middleware.sahara_middleware:Router.factory
# this app is only for use with the experimental v2 API
# [app:sahara_apiv2]
# paste.app_factory = sahara.api.middleware.sahara_middleware:RouterV2.factory
[filter:cors]
paste.filter_factory = oslo_middleware.cors:filter_factory
oslo_config_project = sahara
@ -24,5 +28,9 @@ paste.filter_factory = keystonemiddleware.auth_token:filter_factory
[filter:auth_validator]
paste.filter_factory = sahara.api.middleware.auth_valid:AuthValidator.factory
# this filter is only for use with the experimental v2 API
# [filter:auth_validator_v2]
# paste.filter_factory = sahara.api.middleware.auth_valid:AuthValidatorV2.factory
[filter:debug]
paste.filter_factory = oslo_middleware.debug:Debug.factory

View File

@ -58,3 +58,45 @@ class AuthValidator(base.Middleware):
raise ex.HTTPUnauthorized(
_('Token tenant != requested tenant'))
return self.application
class AuthValidatorV2(base.Middleware):
"""Handles token auth results and tenants."""
@webob.dec.wsgify
def __call__(self, req):
"""Ensures that the requested and token tenants match
Handle incoming requests by checking tenant info from the
headers and url ({tenant_id} url attribute), if using v1 or v1.1
APIs. If using the v2 API, this function will check the token
tenant and the requested tenent in the headers.
Pass request downstream on success.
Reject request if tenant_id from headers is not equal to the
tenant_id from url or v2 project header.
"""
path = req.environ['PATH_INFO']
if path != '/':
token_tenant = req.environ.get("HTTP_X_TENANT_ID")
if not token_tenant:
LOG.warning(_LW("Can't get tenant_id from env"))
raise ex.HTTPServiceUnavailable()
if path.startswith('/v2'):
version, rest = commons.split_path(path, 2, 2, True)
requested_tenant = req.headers.get('OpenStack-Project-ID')
else:
version, requested_tenant, rest = commons.split_path(
path, 3, 3, True)
if not version or not requested_tenant or not rest:
LOG.warning(_LW("Incorrect path: {path}").format(path=path))
raise ex.HTTPNotFound(_("Incorrect path"))
if token_tenant != requested_tenant:
LOG.debug("Unauthorized: token tenant != requested tenant")
raise ex.HTTPUnauthorized(
_('Token tenant != requested tenant'))
return self.application

View File

@ -20,6 +20,7 @@ from werkzeug import exceptions as werkzeug_exceptions
from sahara.api import v10 as api_v10
from sahara.api import v11 as api_v11
from sahara.api import v2 as api_v2
from sahara import context
from sahara.utils import api as api_utils
@ -27,22 +28,25 @@ from sahara.utils import api as api_utils
CONF = cfg.CONF
def build_app():
def build_app(version_response=None):
"""App builder (wsgi).
Entry point for Sahara REST API server
"""
app = flask.Flask('sahara.api')
version_response = (version_response or
{
"versions": [
{"id": "v1.0", "status": "SUPPORTED"},
{"id": "v1.1", "status": "CURRENT"}
]
})
@app.route('/', methods=['GET'])
def version_list():
context.set_ctx(None)
return api_utils.render({
"versions": [
{"id": "v1.0", "status": "SUPPORTED"},
{"id": "v1.1", "status": "CURRENT"}
]
})
return api_utils.render(version_response)
@app.teardown_request
def teardown_request(_ex=None):
@ -69,6 +73,25 @@ def build_app():
return app
def build_v2_app():
"""App builder (wsgi).
Entry point for Experimental V2 Sahara REST API server
"""
version_response = {
"versions": [
{"id": "v1.0", "status": "SUPPORTED"},
{"id": "v1.1", "status": "CURRENT"},
{"id": "v2", "status": "EXPERIMENTAL"}
]
}
app = build_app(version_response)
api_v2.register_blueprints(app, url_prefix='/v2')
return app
class Router(object):
def __call__(self, environ, response):
return self.app(environ, response)
@ -77,3 +100,13 @@ class Router(object):
def factory(cls, global_config, **local_config):
cls.app = build_app()
return cls(**local_config)
class RouterV2(object):
def __call__(self, environ, response):
return self.app(environ, response)
@classmethod
def factory(cls, global_config, **local_config):
cls.app = build_v2_app()
return cls(**local_config)

66
sahara/api/v2/__init__.py Normal file
View File

@ -0,0 +1,66 @@
# Copyright (c) 2016 Red Hat, Inc.
#
# 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.
"""
v2 API interface package
This package contains the endpoint definitions for the version 2 API.
The modules in this package are named in accordance with the top-level
resource primitives they represent.
This module provides a convenience function to register all the
endpoint blueprints to the ``/v2`` root.
When creating new endpoint modules, the following steps should be taken
to ensure they are properly registered with the Flask application:
* create the module file with a name that indicates its endpoint
* add a sahara.utils.api.RestV2 blueprint object
* add an import to this module (__init__.py)
* add a registration line for the new endpoint to the
register_blueprint function
"""
from sahara.api.v2 import cluster_templates
from sahara.api.v2 import clusters
from sahara.api.v2 import data_sources
from sahara.api.v2 import images
from sahara.api.v2 import job_binaries
from sahara.api.v2 import job_executions
from sahara.api.v2 import job_types
from sahara.api.v2 import jobs
from sahara.api.v2 import node_group_templates
from sahara.api.v2 import plugins
def register_blueprints(app, url_prefix):
"""Register the v2 endpoints with a Flask application
This function will take a Flask application object and register all
the v2 endpoints. Register blueprints here when adding new endpoint
modules.
:param app: A Flask application object to register blueprints on
:param url_prefix: The url prefix for the blueprints
"""
app.register_blueprint(cluster_templates.rest, url_prefix=url_prefix)
app.register_blueprint(clusters.rest, url_prefix=url_prefix)
app.register_blueprint(data_sources.rest, url_prefix=url_prefix)
app.register_blueprint(images.rest, url_prefix=url_prefix)
app.register_blueprint(job_binaries.rest, url_prefix=url_prefix)
app.register_blueprint(job_executions.rest, url_prefix=url_prefix)
app.register_blueprint(job_types.rest, url_prefix=url_prefix)
app.register_blueprint(jobs.rest, url_prefix=url_prefix)
app.register_blueprint(node_group_templates.rest, url_prefix=url_prefix)
app.register_blueprint(plugins.rest, url_prefix=url_prefix)

View File

@ -0,0 +1,66 @@
# Copyright (c) 2016 Red Hat, Inc.
#
# 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.
from sahara.api import acl
from sahara.service import api
from sahara.service import validation as v
from sahara.service.validations import cluster_template_schema as ct_schema
from sahara.service.validations import cluster_templates as v_ct
import sahara.utils.api as u
rest = u.RestV2('cluster-templates', __name__)
@rest.get('/cluster-templates')
@acl.enforce("data-processing:cluster-templates:get_all")
def cluster_templates_list():
return u.render(
cluster_templates=[t.to_dict() for t in api.get_cluster_templates(
**u.get_request_args().to_dict())])
@rest.post('/cluster-templates')
@acl.enforce("data-processing:cluster-templates:create")
@v.validate(ct_schema.CLUSTER_TEMPLATE_SCHEMA,
v_ct.check_cluster_template_create)
def cluster_templates_create(data):
return u.render(api.create_cluster_template(data).to_wrapped_dict())
@rest.get('/cluster-templates/<cluster_template_id>')
@acl.enforce("data-processing:cluster-templates:get")
@v.check_exists(api.get_cluster_template, 'cluster_template_id')
def cluster_templates_get(cluster_template_id):
return u.to_wrapped_dict(api.get_cluster_template, cluster_template_id)
@rest.put('/cluster-templates/<cluster_template_id>')
@acl.enforce("data-processing:cluster-templates:modify")
@v.check_exists(api.get_cluster_template, 'cluster_template_id')
@v.validate(ct_schema.CLUSTER_TEMPLATE_UPDATE_SCHEMA,
v_ct.check_cluster_template_update)
def cluster_templates_update(cluster_template_id, data):
return u.to_wrapped_dict(
api.update_cluster_template, cluster_template_id, data)
@rest.delete('/cluster-templates/<cluster_template_id>')
@acl.enforce("data-processing:cluster-templates:delete")
@v.check_exists(api.get_cluster_template, 'cluster_template_id')
@v.validate(None, v_ct.check_cluster_template_usage)
def cluster_templates_delete(cluster_template_id):
api.terminate_cluster_template(cluster_template_id)
return u.render()

84
sahara/api/v2/clusters.py Normal file
View File

@ -0,0 +1,84 @@
# Copyright (c) 2016 Red Hat, Inc.
#
# 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.
import six
from sahara.api import acl
from sahara.service import api
from sahara.service import validation as v
from sahara.service.validations import clusters as v_c
from sahara.service.validations import clusters_scaling as v_c_s
from sahara.service.validations import clusters_schema as v_c_schema
import sahara.utils.api as u
rest = u.RestV2('clusters', __name__)
@rest.get('/clusters')
@acl.enforce("data-processing:clusters:get_all")
def clusters_list():
return u.render(clusters=[c.to_dict() for c in api.get_clusters(
**u.get_request_args().to_dict())])
@rest.post('/clusters')
@acl.enforce("data-processing:clusters:create")
@v.validate(v_c_schema.CLUSTER_SCHEMA, v_c.check_cluster_create)
def clusters_create(data):
return u.render(api.create_cluster(data).to_wrapped_dict())
@rest.post('/clusters/multiple')
@acl.enforce("data-processing:clusters:create")
@v.validate(
v_c_schema.MULTIPLE_CLUSTER_SCHEMA, v_c.check_multiple_clusters_create)
def clusters_create_multiple(data):
return u.render(api.create_multiple_clusters(data))
@rest.put('/clusters/<cluster_id>')
@acl.enforce("data-processing:clusters:scale")
@v.check_exists(api.get_cluster, 'cluster_id')
@v.validate(v_c_schema.CLUSTER_SCALING_SCHEMA, v_c_s.check_cluster_scaling)
def clusters_scale(cluster_id, data):
return u.to_wrapped_dict(api.scale_cluster, cluster_id, data)
@rest.get('/clusters/<cluster_id>')
@acl.enforce("data-processing:clusters:get")
@v.check_exists(api.get_cluster, 'cluster_id')
def clusters_get(cluster_id):
data = u.get_request_args()
show_events = six.text_type(
data.get('show_progress', 'false')).lower() == 'true'
return u.to_wrapped_dict(api.get_cluster, cluster_id, show_events)
@rest.patch('/clusters/<cluster_id>')
@acl.enforce("data-processing:clusters:modify")
@v.check_exists(api.get_cluster, 'cluster_id')
@v.validate(v_c_schema.CLUSTER_UPDATE_SCHEMA, v_c.check_cluster_update)
def clusters_update(cluster_id, data):
return u.to_wrapped_dict(api.update_cluster, cluster_id, data)
@rest.delete('/clusters/<cluster_id>')
@acl.enforce("data-processing:clusters:delete")
@v.check_exists(api.get_cluster, 'cluster_id')
@v.validate(None, v_c.check_cluster_delete)
def clusters_delete(cluster_id):
api.terminate_cluster(cluster_id)
return u.render()

View File

@ -0,0 +1,62 @@
# Copyright (c) 2016 Red Hat, Inc.
#
# 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.
from sahara.api import acl
from sahara.service.edp import api
from sahara.service import validation as v
from sahara.service.validations.edp import data_source as v_d_s
from sahara.service.validations.edp import data_source_schema as v_d_s_schema
import sahara.utils.api as u
rest = u.RestV2('data-sources', __name__)
@rest.get('/data-sources')
@acl.enforce("data-processing:data-sources:get_all")
def data_sources_list():
return u.render(
data_sources=[ds.to_dict() for ds in api.get_data_sources(
**u.get_request_args().to_dict())])
@rest.post('/data-sources')
@acl.enforce("data-processing:data-sources:register")
@v.validate(v_d_s_schema.DATA_SOURCE_SCHEMA, v_d_s.check_data_source_create)
def data_source_register(data):
return u.render(api.register_data_source(data).to_wrapped_dict())
@rest.get('/data-sources/<data_source_id>')
@acl.enforce("data-processing:data-sources:get")
@v.check_exists(api.get_data_source, 'data_source_id')
def data_source_get(data_source_id):
return u.to_wrapped_dict(api.get_data_source, data_source_id)
@rest.delete('/data-sources/<data_source_id>')
@acl.enforce("data-processing:data-sources:delete")
@v.check_exists(api.get_data_source, 'data_source_id')
def data_source_delete(data_source_id):
api.delete_data_source(data_source_id)
return u.render()
@rest.put('/data-sources/<data_source_id>')
@acl.enforce("data-processing:data-sources:modify")
@v.check_exists(api.get_data_source, 'data_source_id')
@v.validate(v_d_s_schema.DATA_SOURCE_UPDATE_SCHEMA)
def data_source_update(data_source_id, data):
return u.to_wrapped_dict(api.data_source_update, data_source_id, data)

70
sahara/api/v2/images.py Normal file
View File

@ -0,0 +1,70 @@
# Copyright (c) 2016 Red Hat, Inc.
#
# 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.
from sahara.api import acl
from sahara.service import api
from sahara.service import validation as v
from sahara.service.validations import images as v_images
import sahara.utils.api as u
rest = u.RestV2('images', __name__)
@rest.get('/images')
@acl.enforce("data-processing:images:get_all")
def images_list():
tags = u.get_request_args().getlist('tags')
name = u.get_request_args().get('name', None)
return u.render(images=[i.dict for i in api.get_images(name, tags)])
@rest.get('/images/<image_id>')
@acl.enforce("data-processing:images:get")
@v.check_exists(api.get_image, id='image_id')
def images_get(image_id):
return u.render(api.get_registered_image(id=image_id).wrapped_dict)
@rest.post('/images/<image_id>')
@acl.enforce("data-processing:images:register")
@v.check_exists(api.get_image, id='image_id')
@v.validate(v_images.image_register_schema, v_images.check_image_register)
def images_set(image_id, data):
return u.render(api.register_image(image_id, **data).wrapped_dict)
@rest.delete('/images/<image_id>')
@acl.enforce("data-processing:images:unregister")
@v.check_exists(api.get_image, id='image_id')
def images_unset(image_id):
api.unregister_image(image_id)
return u.render()
@rest.post('/images/<image_id>/tag')
@acl.enforce("data-processing:images:add_tags")
@v.check_exists(api.get_image, id='image_id')
@v.validate(v_images.image_tags_schema, v_images.check_tags)
def image_tags_add(image_id, data):
return u.render(api.add_image_tags(image_id, **data).wrapped_dict)
@rest.post('/images/<image_id>/untag')
@acl.enforce("data-processing:images:remove_tags")
@v.check_exists(api.get_image, id='image_id')
@v.validate(v_images.image_tags_schema)
def image_tags_delete(image_id, data):
return u.render(api.remove_image_tags(image_id, **data).wrapped_dict)

View File

@ -0,0 +1,123 @@
# Copyright (c) 2016 Red Hat, Inc.
#
# 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.
from sahara.api import acl
from sahara.service.edp import api
from sahara.service import validation as v
from sahara.service.validations.edp import job_binary as v_j_b
from sahara.service.validations.edp import job_binary_internal as v_j_b_i
from sahara.service.validations.edp import job_binary_internal_schema as vjbi_s
from sahara.service.validations.edp import job_binary_schema as v_j_b_schema
import sahara.utils.api as u
rest = u.RestV2('job-binaries', __name__)
@rest.post('/job-binaries')
@acl.enforce("data-processing:job-binaries:create")
@v.validate(v_j_b_schema.JOB_BINARY_SCHEMA, v_j_b.check_job_binary)
def job_binary_create(data):
return u.render(api.create_job_binary(data).to_wrapped_dict())
@rest.get('/job-binaries')
@acl.enforce("data-processing:job-binaries:get_all")
def job_binary_list():
return u.render(binaries=[j.to_dict() for j in api.get_job_binaries(
**u.get_request_args().to_dict())])
@rest.get('/job-binaries/<job_binary_id>')
@acl.enforce("data-processing:job-binaries:get")
@v.check_exists(api.get_job_binary, 'job_binary_id')
def job_binary_get(job_binary_id):
return u.to_wrapped_dict(api.get_job_binary, job_binary_id)
@rest.delete('/job-binaries/<job_binary_id>')
@acl.enforce("data-processing:job-binaries:delete")
@v.check_exists(api.get_job_binary, id='job_binary_id')
def job_binary_delete(job_binary_id):
api.delete_job_binary(job_binary_id)
return u.render()
@rest.get('/job-binaries/<job_binary_id>/data')
@acl.enforce("data-processing:job-binaries:get_data")
@v.check_exists(api.get_job_binary, 'job_binary_id')
def job_binary_data(job_binary_id):
data = api.get_job_binary_data(job_binary_id)
if type(data) == dict:
data = u.render(data)
return data
@rest.put('/job-binaries/<job_binary_id>')
@acl.enforce("data-processing:job-binaries:modify")
@v.validate(v_j_b_schema.JOB_BINARY_UPDATE_SCHEMA, v_j_b.check_job_binary)
def job_binary_update(job_binary_id, data):
return u.render(
api.update_job_binary(job_binary_id, data).to_wrapped_dict())
# Job binary internals ops
@rest.put_file('/job-binary-internals/<name>')
@acl.enforce("data-processing:job-binary-internals:create")
@v.validate(None, v_j_b_i.check_job_binary_internal)
def job_binary_internal_create(**values):
return u.render(
api.create_job_binary_internal(values).to_wrapped_dict())
@rest.get('/job-binary-internals')
@acl.enforce("data-processing:job-binary-internals:get_all")
def job_binary_internal_list():
return u.render(binaries=[j.to_dict() for j in
api.get_job_binary_internals(
**u.get_request_args().to_dict())])
@rest.get('/job-binary-internals/<job_binary_internal_id>')
@acl.enforce("data-processing:job-binary-internals:get")
@v.check_exists(api.get_job_binary_internal, 'job_binary_internal_id')
def job_binary_internal_get(job_binary_internal_id):
return u.to_wrapped_dict(
api.get_job_binary_internal, job_binary_internal_id)
@rest.delete('/job-binary-internals/<job_binary_internal_id>')
@acl.enforce("data-processing:job-binary-internals:delete")
@v.check_exists(api.get_job_binary_internal, 'job_binary_internal_id')
def job_binary_internal_delete(job_binary_internal_id):
api.delete_job_binary_internal(job_binary_internal_id)
return u.render()
@rest.get('/job-binary-internals/<job_binary_internal_id>/data')
@acl.enforce("data-processing:job-binary-internals:get_data")
@v.check_exists(api.get_job_binary_internal, 'job_binary_internal_id')
def job_binary_internal_data(job_binary_internal_id):
return api.get_job_binary_internal_data(job_binary_internal_id)
@rest.patch('/job-binary-internals/<job_binary_internal_id>')
@acl.enforce("data-processing:job-binaries:modify")
@v.check_exists(api.get_job_binary_internal, 'job_binary_internal_id')
@v.validate(vjbi_s.JOB_BINARY_UPDATE_SCHEMA)
def job_binary_internal_update(job_binary_internal_id, data):
return u.to_wrapped_dict(
api.update_job_binary_internal, job_binary_internal_id, data)

View File

@ -0,0 +1,75 @@
# Copyright (c) 2016 Red Hat, Inc.
#
# 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.
from sahara.api import acl
from sahara.service.edp import api
from sahara.service import validation as v
from sahara.service.validations.edp import job_execution as v_j_e
from sahara.service.validations.edp import job_execution_schema as v_j_e_schema
import sahara.utils.api as u
rest = u.RestV2('job-executions', __name__)
@rest.get('/job-executions')
@acl.enforce("data-processing:job-executions:get_all")
def job_executions_list():
job_executions = [je.to_dict() for je in api.job_execution_list(
**u.get_request_args().to_dict())]
return u.render(job_executions=job_executions)
@rest.get('/job-executions/<job_execution_id>')
@acl.enforce("data-processing:job-executions:get")
@v.check_exists(api.get_job_execution, id='job_execution_id')
def job_executions(job_execution_id):
return u.to_wrapped_dict(api.get_job_execution, job_execution_id)
@rest.get('/job-executions/<job_execution_id>/refresh-status')
@acl.enforce("data-processing:job-executions:refresh_status")
@v.check_exists(api.get_job_execution, id='job_execution_id')
def job_executions_status(job_execution_id):
return u.to_wrapped_dict(
api.get_job_execution_status, job_execution_id)
@rest.get('/job-executions/<job_execution_id>/cancel')
@acl.enforce("data-processing:job-executions:cancel")
@v.check_exists(api.get_job_execution, id='job_execution_id')
@v.validate(None, v_j_e.check_job_execution_cancel)
def job_executions_cancel(job_execution_id):
return u.to_wrapped_dict(api.cancel_job_execution, job_execution_id)
@rest.patch('/job-executions/<job_execution_id>')
@acl.enforce("data-processing:job-executions:modify")
@v.check_exists(api.get_job_execution, id='job_execution_id')
@v.validate(
v_j_e_schema.JOB_EXEC_UPDATE_SCHEMA, v_j_e.check_job_execution_update)
def job_executions_update(job_execution_id, data):
return u.to_wrapped_dict(
api.update_job_execution, job_execution_id, data)
@rest.delete('/job-executions/<job_execution_id>')
@acl.enforce("data-processing:job-executions:delete")
@v.check_exists(api.get_job_execution, id='job_execution_id')
@v.validate(None, v_j_e.check_job_execution_delete)
def job_executions_delete(job_execution_id):
api.delete_job_execution(job_execution_id)
return u.render()

View File

@ -0,0 +1,31 @@
# Copyright (c) 2016 Red Hat, Inc.
#
# 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.
from sahara.api import acl
from sahara.service.edp import api
import sahara.utils.api as u
rest = u.RestV2('job-types', __name__)
@rest.get('/job-types')
@acl.enforce("data-processing:job-types:get_all")
def job_types_get():
# We want to use flat=False with to_dict() so that
# the value of each arg is given as a list. This supports
# filters of the form ?type=Pig&type=Java, etc.
return u.render(job_types=api.get_job_types(
**u.get_request_args().to_dict(flat=False)))

78
sahara/api/v2/jobs.py Normal file
View File

@ -0,0 +1,78 @@
# Copyright (c) 2016 Red Hat, Inc.
#
# 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.
from sahara.api import acl
from sahara.service.edp import api
from sahara.service import validation as v
from sahara.service.validations.edp import job as v_j
from sahara.service.validations.edp import job_execution as v_j_e
from sahara.service.validations.edp import job_execution_schema as v_j_e_schema
from sahara.service.validations.edp import job_schema as v_j_schema
import sahara.utils.api as u
rest = u.RestV2('jobs', __name__)
@rest.get('/jobs')
@acl.enforce("data-processing:jobs:get_all")
def job_list():
return u.render(jobs=[j.to_dict() for j in api.get_jobs(
**u.get_request_args().to_dict())])
@rest.post('/jobs')
@acl.enforce("data-processing:jobs:create")
@v.validate(v_j_schema.JOB_SCHEMA, v_j.check_mains_libs, v_j.check_interface)
def job_create(data):
return u.render(api.create_job(data).to_wrapped_dict())
@rest.get('/jobs/<job_id>')
@acl.enforce("data-processing:jobs:get")
@v.check_exists(api.get_job, id='job_id')
def job_get(job_id):
return u.to_wrapped_dict(api.get_job, job_id)
@rest.patch('/jobs/<job_id>')
@acl.enforce("data-processing:jobs:modify")
@v.check_exists(api.get_job, id='job_id')
@v.validate(v_j_schema.JOB_UPDATE_SCHEMA)
def job_update(job_id, data):
return u.to_wrapped_dict(api.update_job, job_id, data)
@rest.delete('/jobs/<job_id>')
@acl.enforce("data-processing:jobs:delete")
@v.check_exists(api.get_job, id='job_id')
def job_delete(job_id):
api.delete_job(job_id)
return u.render()
@rest.post('/jobs/<job_id>/execute')
@acl.enforce("data-processing:jobs:execute")
@v.check_exists(api.get_job, id='job_id')
@v.validate(v_j_e_schema.JOB_EXEC_SCHEMA, v_j_e.check_job_execution)
def job_execute(job_id, data):
return u.render(job_execution=api.execute_job(job_id, data).to_dict())
@rest.get('/jobs/config-hints/<job_type>')
@acl.enforce("data-processing:jobs:get_config_hints")
@v.check_exists(api.get_job_config_hints, job_type='job_type')
def job_config_hints_get(job_type):
return u.render(api.get_job_config_hints(job_type))

View File

@ -0,0 +1,69 @@
# Copyright (c) 2016 Red Hat, Inc.
#
# 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.
from sahara.api import acl
from sahara.service import api
from sahara.service import validation as v
from sahara.service.validations import node_group_template_schema as ngt_schema
from sahara.service.validations import node_group_templates as v_ngt
import sahara.utils.api as u
rest = u.RestV2('node-group-templates', __name__)
@rest.get('/node-group-templates')
@acl.enforce("data-processing:node-group-templates:get_all")
def node_group_templates_list():
return u.render(
node_group_templates=[t.to_dict()
for t in api.get_node_group_templates(
**u.get_request_args().to_dict())])
@rest.post('/node-group-templates')
@acl.enforce("data-processing:node-group-templates:create")
@v.validate(ngt_schema.NODE_GROUP_TEMPLATE_SCHEMA,
v_ngt.check_node_group_template_create)
def node_group_templates_create(data):
return u.render(api.create_node_group_template(data).to_wrapped_dict())
@rest.get('/node-group-templates/<node_group_template_id>')
@acl.enforce("data-processing:node-group-templates:get")
@v.check_exists(api.get_node_group_template, 'node_group_template_id')
def node_group_templates_get(node_group_template_id):
return u.to_wrapped_dict(
api.get_node_group_template, node_group_template_id)
@rest.put('/node-group-templates/<node_group_template_id>')
@acl.enforce("data-processing:node-group-templates:modify")
@v.check_exists(api.get_node_group_template, 'node_group_template_id')
@v.validate(ngt_schema.NODE_GROUP_TEMPLATE_UPDATE_SCHEMA,
v_ngt.check_node_group_template_update)
def node_group_templates_update(node_group_template_id, data):
return u.to_wrapped_dict(
api.update_node_group_template, node_group_template_id, data)
@rest.delete('/node-group-templates/<node_group_template_id>')
@acl.enforce("data-processing:node-group-templates:delete")
@v.check_exists(api.get_node_group_template, 'node_group_template_id')
@v.validate(None, v_ngt.check_node_group_template_usage)
def node_group_templates_delete(node_group_template_id):
api.terminate_node_group_template(node_group_template_id)
return u.render()

52
sahara/api/v2/plugins.py Normal file
View File

@ -0,0 +1,52 @@
# Copyright (c) 2016 Red Hat, Inc.
#
# 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.
from sahara.api import acl
from sahara.service import api
from sahara.service import validation as v
from sahara.service.validations import plugins as v_p
import sahara.utils.api as u
rest = u.RestV2('plugins', __name__)
@rest.get('/plugins')
@acl.enforce("data-processing:plugins:get_all")
def plugins_list():
return u.render(plugins=[p.dict for p in api.get_plugins()])
@rest.get('/plugins/<plugin_name>')
@acl.enforce("data-processing:plugins:get")
@v.check_exists(api.get_plugin, plugin_name='plugin_name')
def plugins_get(plugin_name):
return u.render(api.get_plugin(plugin_name).wrapped_dict)
@rest.get('/plugins/<plugin_name>/<version>')
@acl.enforce("data-processing:plugins:get_version")
@v.check_exists(api.get_plugin, plugin_name='plugin_name', version='version')
def plugins_get_version(plugin_name, version):
return u.render(api.get_plugin(plugin_name, version).wrapped_dict)
@rest.post_file('/plugins/<plugin_name>/<version>/convert-config/<name>')
@acl.enforce("data-processing:plugins:convert_config")
@v.check_exists(api.get_plugin, plugin_name='plugin_name', version='version')
@v.validate(v_p.CONVERT_TO_TEMPLATE_SCHEMA, v_p.check_convert_to_template)
def plugins_convert_to_cluster_template(plugin_name, version, name, data):
return u.to_wrapped_dict(
api.convert_to_cluster_template, plugin_name, version, name, data)

View File

@ -111,6 +111,59 @@ class Rest(flask.Blueprint):
return decorator
class RestV2(Rest):
def route(self, rule, **options):
status = options.pop('status_code', None)
file_upload = options.pop('file_upload', False)
def decorator(func):
endpoint = options.pop('endpoint', func.__name__)
def handler(**kwargs):
context.set_ctx(None)
LOG.debug("Rest.route.decorator.handler, kwargs={kwargs}"
.format(kwargs=kwargs))
_init_resp_type(file_upload)
# update status code
if status:
flask.request.status_code = status
req_id = flask.request.environ.get(oslo_req_id.ENV_REQUEST_ID)
auth_plugin = flask.request.environ.get('keystone.token_auth')
ctx = context.Context(
flask.request.headers['X-User-Id'],
flask.request.headers['X-Tenant-Id'],
flask.request.headers['X-Auth-Token'],
flask.request.headers['X-Service-Catalog'],
flask.request.headers['X-User-Name'],
flask.request.headers['X-Tenant-Name'],
flask.request.headers['X-Roles'].split(','),
auth_plugin=auth_plugin,
request_id=req_id)
context.set_ctx(ctx)
if flask.request.method in ['POST', 'PUT', 'PATCH']:
kwargs['data'] = request_data()
try:
return func(**kwargs)
except ex.Forbidden as e:
return access_denied(e)
except ex.SaharaException as e:
return bad_request(e)
except Exception as e:
return internal_error(500, 'Internal Server Error', e)
self.add_url_rule(rule, endpoint, handler, **options)
self.add_url_rule(rule + '.json', endpoint, handler, **options)
return func
return decorator
RT_JSON = datastructures.MIMEAccept([("application/json", 1)])