
Move sample config to etc/drydock Update docs to generate a config with tox Update configuration for Keystone - Add config generation to tox.ini - Fix default in bootdata config - Add keystone dependencies - Add config generator config - Move sample config to a skeleton etc/drydock tree Use PasteDeploy for WSGI integration Using keystonemiddleware outside of a PasteDeploy pipeline is deprecated. Move Drydock to use PasteDeploy and integrate with keystonemiddleware Update Falcon context object Add keystone identity fields to context object Clean up context marker field Fix AuthMiddleware for keystone Update falcon middleware to harvest headers injected by keystonemiddleware Fix context middleware Update context middleware to enforce a UUID-formatted external context marker Lock keystonemiddleware version Lock keystonemiddleware version to the Newton release Sample drydock.conf with keystone This drydock.conf file is known to integrate successfully with Keystone via keystonemiddleware and the password plugin Add .dockerignore Stop adding .tox environment to docker images Integrate with oslo.policy Add oslo.policy 1.9.0 to requirements (Newton release) Add tox job to generate sample policy.yaml Create DrydockPolicy as facade for RBAC Inject policy engine into API init Create a DrydockPolicy instance and inject it into the Drydock API resources. Remove per-resource authorization Update Drydock context and auth middleware Update Drydock context to use keystone IDs instead of names as required by oslo.policy Update AuthMiddleware to capture headers when request provides a service token Add RBAC for /designs API Add RBAC enforcement for GET and POST of /api/v1.0/designs endpoint Refactor check_policy Refactor check_policy into the base class Enforce RBAC for /designs/id endpoint Enforce RBAC on /designs/id/parts endpoint Enforce RBAC on /designs/id/parts/kind Enforce RBAC on /designs/id/parts/kinds/ Enforce RBAC on /tasks/ endpoints Create unit tests - New unit tests for DrydockPolicy - New unit tests for AuthMiddleware w/ Keystone integration Address impacting keystonemiddleware bug Use v4.9.1 to address https://bugs.launchpad.net/keystonemiddleware/+bug/1653646 Add oslo_config fixtures for unit testing API base class fixes Fix an import error in API resource base class More graceful error handling in drydock_client Create shared function for checking API response status codes Create client errors for auth Create specific Exceptions for Unauthorized and Forbidden responses Ignore generated sample configs Lock iso8601 version oslo.versionedobjects appears to be impcompatible with iso8601 0.1.12 on Python 3.2+ Update docs for Keystone Note Keystone as a external depdendency and add notes on correctly configuring Drydock for Keystone integration Add keystoneauth1 to list_opts Explicitly pull keystoneauth password plugin options when generating a config template Update reference config for keystone Update the reference config template for Keystone integration Add keystoneauth1 to requirements Need to directly include keystoneauth1 so that oslo_config options can be pulled from it Update config doc for keystoneauth1 Use the keystoneauth1 generated configuration options for the configuration docs Remove auth options Force dependence on Keystone as the only authentication backend Clean up imports Fix how falcon modules are imported Default to empty role list Move param extraction Enforce RBAC before starting to parse parameters Implement DocumentedRuleDefault Use DocumentedRuleDefault for policy defaults at request of @tlam. Requires v 1.21.1 of oslo_policy, which is tied to the Pike openstack release. Change sample output filenames Update filenames to follow Openstack convention Fix tests to use hex formatted IDs Openstack resource IDs are not hyphenated, so update unit tests to reflect this Fix formating and whitespace Refactor a few small items for code review Update keystone integration to be more robust with Newton codebase Centralize policy_engine reference to support a decorator-based model RBAC enforcement decorator Add units tests for decorator-based RBAC and the tasks API Minor refactoring and format changes Change-Id: I35f90b0c88ec577fda1077814f5eac5c0ffb41e9
255 lines
11 KiB
Python
255 lines
11 KiB
Python
# Copyright 2017 AT&T Intellectual Property. All other rights reserved.
|
|
#
|
|
# 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 falcon
|
|
import json
|
|
import threading
|
|
import traceback
|
|
|
|
from drydock_provisioner import policy
|
|
from drydock_provisioner import error as errors
|
|
|
|
import drydock_provisioner.objects.task as obj_task
|
|
from .base import StatefulResource
|
|
|
|
class TasksResource(StatefulResource):
|
|
|
|
def __init__(self, orchestrator=None, **kwargs):
|
|
super(TasksResource, self).__init__(**kwargs)
|
|
self.orchestrator = orchestrator
|
|
|
|
@policy.ApiEnforcer('physical_provisioner:read_task')
|
|
def on_get(self, req, resp):
|
|
try:
|
|
task_id_list = [str(x.get_id()) for x in self.state_manager.tasks]
|
|
resp.body = json.dumps(task_id_list)
|
|
resp.status = falcon.HTTP_200
|
|
except Exception as ex:
|
|
self.error(req.context, "Unknown error: %s\n%s" % (str(ex), traceback.format_exc()))
|
|
self.return_error(resp, falcon.HTTP_500, message="Unknown error", retry=False)
|
|
|
|
@policy.ApiEnforcer('physical_provisioner:create_task')
|
|
def on_post(self, req, resp):
|
|
# A map of supported actions to the handlers for tasks for those actions
|
|
supported_actions = {
|
|
'validate_design': TasksResource.task_validate_design,
|
|
'verify_site': TasksResource.task_verify_site,
|
|
'prepare_site': TasksResource.task_prepare_site,
|
|
'verify_node': TasksResource.task_verify_node,
|
|
'prepare_node': TasksResource.task_prepare_node,
|
|
'deploy_node': TasksResource.task_deploy_node,
|
|
'destroy_node': TasksResource.task_destroy_node,
|
|
}
|
|
|
|
try:
|
|
ctx = req.context
|
|
json_data = self.req_json(req)
|
|
|
|
action = json_data.get('action', None)
|
|
if action not in supported_actions:
|
|
self.error(req,context, "Unsupported action %s" % action)
|
|
self.return_error(resp, falcon.HTTP_400, message="Unsupported action %s" % action, retry=False)
|
|
else:
|
|
supported_actions.get(action)(self, req, resp)
|
|
|
|
except Exception as ex:
|
|
self.error(req.context, "Unknown error: %s\n%s" % (str(ex), traceback.format_exc()))
|
|
self.return_error(resp, falcon.HTTP_500, message="Unknown error", retry=False)
|
|
|
|
@policy.ApiEnforcer('physical_provisioner:validate_design')
|
|
def task_validate_design(self, req, resp):
|
|
json_data = self.req_json(req)
|
|
action = json_data.get('action', None)
|
|
|
|
if action != 'validate_design':
|
|
self.error(req.context, "Task body ended up in wrong handler: action %s in task_validate_design" % action)
|
|
self.return_error(resp, falcon.HTTP_500, message="Error - misrouted request", retry=False)
|
|
|
|
try:
|
|
task = self.create_task(json_data)
|
|
resp.body = json.dumps(task.to_dict())
|
|
resp.append_header('Location', "/api/v1.0/tasks/%s" % str(task.task_id))
|
|
resp.status = falcon.HTTP_201
|
|
except errors.InvalidFormat as ex:
|
|
self.error(req.context, ex.msg)
|
|
self.return_error(resp, falcon.HTTP_400, message=ex.msg, retry=False)
|
|
|
|
@policy.ApiEnforcer('physical_provisioner:verify_site')
|
|
def task_verify_site(self, req, resp):
|
|
json_data = self.req_json(req)
|
|
action = json_data.get('action', None)
|
|
|
|
if action != 'verify_site':
|
|
self.error(req.context, "Task body ended up in wrong handler: action %s in task_verify_site" % action)
|
|
self.return_error(resp, falcon.HTTP_500, message="Error - misrouted request", retry=False)
|
|
|
|
try:
|
|
task = self.create_task(json_data)
|
|
resp.body = json.dumps(task.to_dict())
|
|
resp.append_header('Location', "/api/v1.0/tasks/%s" % str(task.task_id))
|
|
resp.status = falcon.HTTP_201
|
|
except errors.InvalidFormat as ex:
|
|
self.error(req.context, ex.msg)
|
|
self.return_error(resp, falcon.HTTP_400, message=ex.msg, retry=False)
|
|
|
|
@policy.ApiEnforcer('physical_provisioner:prepare_site')
|
|
def task_prepare_site(self, req, resp):
|
|
json_data = self.req_json(req)
|
|
action = json_data.get('action', None)
|
|
|
|
if action != 'prepare_site':
|
|
self.error(req.context, "Task body ended up in wrong handler: action %s in task_prepare_site" % action)
|
|
self.return_error(resp, falcon.HTTP_500, message="Error - misrouted request", retry=False)
|
|
|
|
try:
|
|
task = self.create_task(json_data)
|
|
resp.body = json.dumps(task.to_dict())
|
|
resp.append_header('Location', "/api/v1.0/tasks/%s" % str(task.task_id))
|
|
resp.status = falcon.HTTP_201
|
|
except errors.InvalidFormat as ex:
|
|
self.error(req.context, ex.msg)
|
|
self.return_error(resp, falcon.HTTP_400, message=ex.msg, retry=False)
|
|
|
|
@policy.ApiEnforcer('physical_provisioner:verify_node')
|
|
def task_verify_node(self, req, resp):
|
|
json_data = self.req_json(req)
|
|
action = json_data.get('action', None)
|
|
|
|
if action != 'verify_node':
|
|
self.error(req.context, "Task body ended up in wrong handler: action %s in task_verify_node" % action)
|
|
self.return_error(resp, falcon.HTTP_500, message="Error - misrouted request", retry=False)
|
|
|
|
try:
|
|
task = self.create_task(json_data)
|
|
resp.body = json.dumps(task.to_dict())
|
|
resp.append_header('Location', "/api/v1.0/tasks/%s" % str(task.task_id))
|
|
resp.status = falcon.HTTP_201
|
|
except errors.InvalidFormat as ex:
|
|
self.error(req.context, ex.msg)
|
|
self.return_error(resp, falcon.HTTP_400, message=ex.msg, retry=False)
|
|
|
|
@policy.ApiEnforcer('physical_provisioner:prepare_node')
|
|
def task_prepare_node(self, req, resp):
|
|
json_data = self.req_json(req)
|
|
action = json_data.get('action', None)
|
|
|
|
if action != 'prepare_node':
|
|
self.error(req.context, "Task body ended up in wrong handler: action %s in task_prepare_node" % action)
|
|
self.return_error(resp, falcon.HTTP_500, message="Error - misrouted request", retry=False)
|
|
|
|
try:
|
|
task = self.create_task(json_data)
|
|
resp.body = json.dumps(task.to_dict())
|
|
resp.append_header('Location', "/api/v1.0/tasks/%s" % str(task.task_id))
|
|
resp.status = falcon.HTTP_201
|
|
except errors.InvalidFormat as ex:
|
|
self.error(req.context, ex.msg)
|
|
self.return_error(resp, falcon.HTTP_400, message=ex.msg, retry=False)
|
|
|
|
@policy.ApiEnforcer('physical_provisioner:deploy_node')
|
|
def task_deploy_node(self, req, resp):
|
|
json_data = self.req_json(req)
|
|
action = json_data.get('action', None)
|
|
|
|
if action != 'deploy_node':
|
|
self.error(req.context, "Task body ended up in wrong handler: action %s in task_deploy_node" % action)
|
|
self.return_error(resp, falcon.HTTP_500, message="Error - misrouted request", retry=False)
|
|
|
|
try:
|
|
task = self.create_task(json_data)
|
|
resp.body = json.dumps(task.to_dict())
|
|
resp.append_header('Location', "/api/v1.0/tasks/%s" % str(task.task_id))
|
|
resp.status = falcon.HTTP_201
|
|
except errors.InvalidFormat as ex:
|
|
self.error(req.context, ex.msg)
|
|
self.return_error(resp, falcon.HTTP_400, message=ex.msg, retry=False)
|
|
|
|
@policy.ApiEnforcer('physical_provisioner:destroy_node')
|
|
def task_destroy_node(self, req, resp):
|
|
json_data = self.req_json(req)
|
|
action = json_data.get('action', None)
|
|
|
|
if action != 'destroy_node':
|
|
self.error(req.context, "Task body ended up in wrong handler: action %s in task_destroy_node" % action)
|
|
self.return_error(resp, falcon.HTTP_500, message="Error - misrouted request", retry=False)
|
|
|
|
try:
|
|
task = self.create_task(json_data)
|
|
resp.body = json.dumps(task.to_dict())
|
|
resp.append_header('Location', "/api/v1.0/tasks/%s" % str(task.task_id))
|
|
resp.status = falcon.HTTP_201
|
|
except errors.InvalidFormat as ex:
|
|
self.error(req.context, ex.msg)
|
|
self.return_error(resp, falcon.HTTP_400, message=ex.msg, retry=False)
|
|
|
|
def create_task(self, task_body):
|
|
"""
|
|
Given the parsed body of a create task request, create the task
|
|
and start it in a thread
|
|
|
|
:param dict task_body: Dict representing the JSON body of a create task request
|
|
action - The action the task will execute
|
|
design_id - The design context the task will execute in
|
|
node_filter - A filter on which nodes will be affected by the task. The result is
|
|
an intersection of
|
|
applying all filters
|
|
node_names - A list of node hostnames
|
|
rack_names - A list of rack names that contain the nodes
|
|
node_tags - A list of tags applied to the nodes
|
|
|
|
:return: The Task object created
|
|
"""
|
|
design_id = task_body.get('design_id', None)
|
|
node_filter = task_body.get('node_filter', None)
|
|
action = task_body.get('action', None)
|
|
|
|
if design_id is None or action is None:
|
|
raise errors.InvalidFormat('Task creation requires fields design_id, action')
|
|
|
|
task = self.orchestrator.create_task(obj_task.OrchestratorTask, design_id=design_id,
|
|
action=action, node_filter=node_filter)
|
|
|
|
task_thread = threading.Thread(target=self.orchestrator.execute_task, args=[task.get_id()])
|
|
task_thread.start()
|
|
|
|
return task
|
|
|
|
class TaskResource(StatefulResource):
|
|
|
|
def __init__(self, orchestrator=None, **kwargs):
|
|
super(TaskResource, self).__init__(**kwargs)
|
|
self.authorized_roles = ['user']
|
|
self.orchestrator = orchestrator
|
|
|
|
def on_get(self, req, resp, task_id):
|
|
ctx = req.context
|
|
policy_action = 'physical_provisioner:read_task'
|
|
|
|
try:
|
|
if not self.check_policy(policy_action, ctx):
|
|
self.access_denied(req, resp, policy_action)
|
|
return
|
|
|
|
task = self.state_manager.get_task(task_id)
|
|
|
|
if task is None:
|
|
self.info(req.context, "Task %s does not exist" % task_id )
|
|
self.return_error(resp, falcon.HTTP_404, message="Task %s does not exist" % task_id, retry=False)
|
|
return
|
|
|
|
resp.body = json.dumps(task.to_dict())
|
|
resp.status = falcon.HTTP_200
|
|
except Exception as ex:
|
|
self.error(req.context, "Unknown error: %s" % (str(ex)))
|
|
self.return_error(resp, falcon.HTTP_500, message="Unknown error", retry=False)
|