Merge "VNF workflow implementation"

This commit is contained in:
Zuul
2020-01-28 05:25:41 +00:00
committed by Gerrit Code Review
17 changed files with 1753 additions and 18 deletions

View File

@@ -34,7 +34,7 @@ api_opts = [
help="API host IP"), help="API host IP"),
cfg.IntOpt('port', cfg.IntOpt('port',
default=5000, default=5000,
help="API port to use."), help="API port to use.")
] ]
CONF = cfg.CONF CONF = cfg.CONF
@@ -49,9 +49,8 @@ def setup_app(pecan_config=None, debug=False, argv=None):
hooks.DBHook(), hooks.DBHook(),
hooks.ContextHook(), hooks.ContextHook(),
hooks.RPCHook()] hooks.RPCHook()]
app = pecan.make_app(pecan_config.app.root, app = pecan.make_app(pecan_config.app.root,
debug=CONF.debug, debug=True,
hooks=app_hooks, hooks=app_hooks,
wrap_app=middleware.ParsableErrorMiddleware, wrap_app=middleware.ParsableErrorMiddleware,
guess_content_type_from_ext=False) guess_content_type_from_ext=False)

View File

@@ -36,6 +36,12 @@ class V1Controller(rest.RestController):
session = controller.SessionController() session = controller.SessionController()
# maps to v1/maintenance/{session_id}/{project_id} # maps to v1/maintenance/{session_id}/{project_id}
project = controller.ProjectController() project = controller.ProjectController()
# maps to v1/maintenance/{session_id}/{project_id}/{instance_id}
project_instance = controller.ProjectInstanceController()
# maps to v1/instance/{instance_id}
instance = controller.InstanceController()
# maps to v1/instance_group/{group_id}
instance_group = controller.InstanceGroupController()
@pecan.expose() @pecan.expose()
def _route(self, args): def _route(self, args):
@@ -44,7 +50,6 @@ class V1Controller(rest.RestController):
It allows to map controller URL with correct controller instance. It allows to map controller URL with correct controller instance.
By default, it maps with the same name. By default, it maps with the same name.
""" """
try: try:
route = self._routes.get(args[0], args[0]) route = self._routes.get(args[0], args[0])
depth = len(args) depth = len(args)
@@ -53,10 +58,17 @@ class V1Controller(rest.RestController):
args[0] = 'http404-nonexistingcontroller' args[0] = 'http404-nonexistingcontroller'
elif depth == 1: elif depth == 1:
args[0] = route args[0] = route
elif depth == 2 and route == "maintenance": elif depth == 2:
args[0] = "session" if route == "maintenance":
args[0] = "session"
elif route in ["instance", "instance_group"]:
args[0] = route
else:
args[0] = 'http404-nonexistingcontroller'
elif depth == 3 and route == "maintenance": elif depth == 3 and route == "maintenance":
args[0] = "project" args[0] = "project"
elif depth == 4 and route == "maintenance":
args[0] = "project_instance"
else: else:
args[0] = 'http404-nonexistingcontroller' args[0] = 'http404-nonexistingcontroller'
except IndexError: except IndexError:

View File

@@ -45,7 +45,10 @@ class ProjectController(rest.RestController):
abort(400) abort(400)
engine_data = self.engine_rpcapi.project_get_session(session_id, engine_data = self.engine_rpcapi.project_get_session(session_id,
project_id) project_id)
response.text = jsonutils.dumps(engine_data) try:
response.text = jsonutils.dumps(engine_data)
except TypeError:
response.body = jsonutils.dumps(engine_data)
# PUT /v1/maintenance/<session_id>/<project_id> # PUT /v1/maintenance/<session_id>/<project_id>
@policy.authorize('maintenance:session:project', 'put') @policy.authorize('maintenance:session:project', 'put')
@@ -55,7 +58,33 @@ class ProjectController(rest.RestController):
engine_data = self.engine_rpcapi.project_update_session(session_id, engine_data = self.engine_rpcapi.project_update_session(session_id,
project_id, project_id,
data) data)
response.text = jsonutils.dumps(engine_data) try:
response.text = jsonutils.dumps(engine_data)
except TypeError:
response.body = jsonutils.dumps(engine_data)
class ProjectInstanceController(rest.RestController):
name = 'project_instance'
def __init__(self):
self.engine_rpcapi = maintenance.EngineRPCAPI()
# PUT /v1/maintenance/<session_id>/<project_id>/<instance_id>
@policy.authorize('maintenance:session:project:instance', 'put')
@expose(content_type='application/json')
def put(self, session_id, project_id, instance_id):
data = json.loads(request.body.decode('utf8'))
engine_data = (
self.engine_rpcapi.project_update_session_instance(session_id,
project_id,
instance_id,
data))
try:
response.text = jsonutils.dumps(engine_data)
except TypeError:
response.body = jsonutils.dumps(engine_data)
class SessionController(rest.RestController): class SessionController(rest.RestController):
@@ -76,7 +105,10 @@ class SessionController(rest.RestController):
if session is None: if session is None:
response.status = 404 response.status = 404
return {"error": "Invalid session"} return {"error": "Invalid session"}
response.text = jsonutils.dumps(session) try:
response.text = jsonutils.dumps(session)
except TypeError:
response.body = jsonutils.dumps(session)
# PUT /v1/maintenance/<session_id> # PUT /v1/maintenance/<session_id>
@policy.authorize('maintenance:session', 'put') @policy.authorize('maintenance:session', 'put')
@@ -84,7 +116,10 @@ class SessionController(rest.RestController):
def put(self, session_id): def put(self, session_id):
data = json.loads(request.body.decode('utf8')) data = json.loads(request.body.decode('utf8'))
engine_data = self.engine_rpcapi.admin_update_session(session_id, data) engine_data = self.engine_rpcapi.admin_update_session(session_id, data)
response.text = jsonutils.dumps(engine_data) try:
response.text = jsonutils.dumps(engine_data)
except TypeError:
response.body = jsonutils.dumps(engine_data)
# DELETE /v1/maintenance/<session_id> # DELETE /v1/maintenance/<session_id>
@policy.authorize('maintenance:session', 'delete') @policy.authorize('maintenance:session', 'delete')
@@ -94,7 +129,10 @@ class SessionController(rest.RestController):
LOG.error("Unexpected data") LOG.error("Unexpected data")
abort(400) abort(400)
engine_data = self.engine_rpcapi.admin_delete_session(session_id) engine_data = self.engine_rpcapi.admin_delete_session(session_id)
response.text = jsonutils.dumps(engine_data) try:
response.text = jsonutils.dumps(engine_data)
except TypeError:
response.body = jsonutils.dumps(engine_data)
class MaintenanceController(rest.RestController): class MaintenanceController(rest.RestController):
@@ -112,16 +150,120 @@ class MaintenanceController(rest.RestController):
LOG.error("Unexpected data") LOG.error("Unexpected data")
abort(400) abort(400)
sessions = self.engine_rpcapi.admin_get() sessions = self.engine_rpcapi.admin_get()
response.text = jsonutils.dumps(sessions) try:
response.text = jsonutils.dumps(sessions)
except TypeError:
response.body = jsonutils.dumps(sessions)
# POST /v1/maintenance # POST /v1/maintenance
@policy.authorize('maintenance', 'post') @policy.authorize('maintenance', 'post')
@expose(content_type='application/json') @expose(content_type='application/json')
def post(self): def post(self):
LOG.debug("POST /v1/maintenance")
data = json.loads(request.body.decode('utf8')) data = json.loads(request.body.decode('utf8'))
session = self.engine_rpcapi.admin_create_session(data) session = self.engine_rpcapi.admin_create_session(data)
if session is None: if session is None:
response.status = 509 response.status = 509
return {"error": "Too many sessions"} return {"error": "Too many sessions"}
response.text = jsonutils.dumps(session) try:
response.text = jsonutils.dumps(session)
except TypeError:
response.body = jsonutils.dumps(session)
class InstanceController(rest.RestController):
name = 'instance'
def __init__(self):
self.engine_rpcapi = maintenance.EngineRPCAPI()
# GET /v1/instance/<instance_id>
@policy.authorize('instance', 'get')
@expose(content_type='application/json')
def get(self, instance_id):
if request.body:
LOG.error("Unexpected data")
abort(400)
session = self.engine_rpcapi.get_instance(instance_id)
if session is None:
response.status = 404
return {"error": "Invalid session"}
try:
response.text = jsonutils.dumps(session)
except TypeError:
response.body = jsonutils.dumps(session)
# PUT /v1/instance/<instance_id>
@policy.authorize('instance', 'put')
@expose(content_type='application/json')
def put(self, instance_id):
data = json.loads(request.body.decode('utf8'))
engine_data = self.engine_rpcapi.update_instance(instance_id,
data)
try:
response.text = jsonutils.dumps(engine_data)
except TypeError:
response.body = jsonutils.dumps(engine_data)
# DELETE /v1/instance/<instance_id>
@policy.authorize('instance', 'delete')
@expose(content_type='application/json')
def delete(self, instance_id):
if request.body:
LOG.error("Unexpected data")
abort(400)
engine_data = self.engine_rpcapi.delete_instance(instance_id)
try:
response.text = jsonutils.dumps(engine_data)
except TypeError:
response.body = jsonutils.dumps(engine_data)
class InstanceGroupController(rest.RestController):
name = 'instance_group'
def __init__(self):
self.engine_rpcapi = maintenance.EngineRPCAPI()
# GET /v1/instance_group/<group_id>
@policy.authorize('instance_group', 'get')
@expose(content_type='application/json')
def get(self, group_id):
if request.body:
LOG.error("Unexpected data")
abort(400)
session = self.engine_rpcapi.get_instance_group(group_id)
if session is None:
response.status = 404
return {"error": "Invalid session"}
try:
response.text = jsonutils.dumps(session)
except TypeError:
response.body = jsonutils.dumps(session)
# PUT /v1/instance_group/<group_id>
@policy.authorize('instance_group', 'put')
@expose(content_type='application/json')
def put(self, group_id):
data = json.loads(request.body.decode('utf8'))
engine_data = (
self.engine_rpcapi.update_instance_group(group_id, data))
try:
response.text = jsonutils.dumps(engine_data)
except TypeError:
response.body = jsonutils.dumps(engine_data)
# DELETE /v1/instance_group/<group_id>
@policy.authorize('instance_group', 'delete')
@expose(content_type='application/json')
def delete(self, group_id):
if request.body:
LOG.error("Unexpected data")
abort(400)
engine_data = (
self.engine_rpcapi.delete_instance_group(group_id))
try:
response.text = jsonutils.dumps(engine_data)
except TypeError:
response.body = jsonutils.dumps(engine_data)

View File

@@ -58,3 +58,40 @@ class EngineRPCAPI(service.RPCClient):
"""Update maintenance workflow session project state""" """Update maintenance workflow session project state"""
return self.call('project_update_session', session_id=session_id, return self.call('project_update_session', session_id=session_id,
project_id=project_id, data=data) project_id=project_id, data=data)
def project_update_session_instance(self, session_id, project_id,
instance_id, data):
"""Update maintenance workflow session project instance project_state
"""
return self.call('project_update_session_instance',
session_id=session_id,
project_id=project_id,
instance_id=instance_id,
data=data)
def get_instance(self, instance_id):
"""Get internal instance"""
return self.call('get_instance', instance_id=instance_id)
def update_instance(self, instance_id, data):
"""Update internal instance"""
return self.call('update_instance', instance_id=instance_id,
data=data)
def delete_instance(self, instance_id):
"""Delete internal instance"""
return self.call('delete_instance', instance_id=instance_id)
def get_instance_group(self, group_id):
"""Get internal instance group"""
return self.call('get_instance_group', group_id=group_id)
def update_instance_group(self, group_id, data):
"""Update internal instance group"""
return self.call('update_instance_group', group_id=group_id,
data=data)
def delete_instance_group(self, group_id):
"""Delete internal instance group"""
return self.call('delete_instance_group', group_id=group_id)

View File

@@ -197,3 +197,38 @@ def create_instances(instances):
def remove_instance(session_id, instance_id): def remove_instance(session_id, instance_id):
return IMPL.remove_instance(session_id, instance_id) return IMPL.remove_instance(session_id, instance_id)
def update_project_instance(values):
return IMPL.update_project_instance(values)
def remove_project_instance(instance_id):
return IMPL.remove_project_instance(instance_id)
def project_instance_get(instance_id):
return IMPL.project_instance_get(instance_id)
def group_instances_get(group_id):
return IMPL.group_instances_get(group_id)
def instance_group_get(group_id):
return IMPL.instance_group_get(group_id)
def instance_group_get_detailed(group_id):
instances = group_instances_get(group_id)
group = instance_group_get(group_id)
group['instance_ids'] = instances
return group
def update_instance_group(values):
return IMPL.update_instance_group(values)
def remove_instance_group(instance_id):
return IMPL.remove_instance_group(instance_id)

View File

@@ -141,6 +141,37 @@ def upgrade():
name='_session_local_file_uc'), name='_session_local_file_uc'),
sa.PrimaryKeyConstraint('id')) sa.PrimaryKeyConstraint('id'))
op.create_table(
'project_instances',
sa.Column('created_at', sa.DateTime(), nullable=False),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.Column('instance_id', sa.String(36), primary_key=True,
default=_generate_unicode_uuid),
sa.Column('project_id', sa.String(36), nullable=True),
sa.Column('group_id', sa.String(36), nullable=False),
sa.Column('instance_name', sa.String(length=255),
nullable=False),
sa.Column('max_interruption_time', sa.Integer, nullable=False),
sa.Column('migration_type', sa.String(36), nullable=False),
sa.Column('resource_mitigation', sa.Boolean, default=False),
sa.Column('lead_time', sa.Integer, nullable=False),
sa.PrimaryKeyConstraint('instance_id'))
op.create_table(
'instance_groups',
sa.Column('created_at', sa.DateTime(), nullable=False),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.Column('group_id', sa.String(36), primary_key=True),
sa.Column('project_id', sa.String(36), nullable=True),
sa.Column('group_name', sa.String(length=255), nullable=False),
sa.Column('anti_affinity_group', sa.Boolean, default=False),
sa.Column('max_instances_per_host', sa.Integer,
nullable=False),
sa.Column('max_impacted_members', sa.Integer, nullable=False),
sa.Column('recovery_time', sa.Integer, nullable=False),
sa.Column('resource_mitigation', sa.Boolean, default=False),
sa.PrimaryKeyConstraint('group_id'))
def downgrade(): def downgrade():
op.drop_table('sessions') op.drop_table('sessions')
@@ -149,3 +180,5 @@ def downgrade():
op.drop_table('instances') op.drop_table('instances')
op.drop_table('action_plugins') op.drop_table('action_plugins')
op.drop_table('downloads') op.drop_table('downloads')
op.drop_table('project_instances')
op.drop_table('instance_groups')

View File

@@ -60,6 +60,9 @@ def setup_db():
models.MaintenanceHost.metadata.create_all(engine) models.MaintenanceHost.metadata.create_all(engine)
models.MaintenanceProject.metadata.create_all(engine) models.MaintenanceProject.metadata.create_all(engine)
models.MaintenanceInstance.metadata.create_all(engine) models.MaintenanceInstance.metadata.create_all(engine)
models.ProjectInstance.metadata.create_all(engine)
models.MaintenanceProject.metadata.create_all(engine)
models.InstanceGroup.metadata.create_all(engine)
except sa.exc.OperationalError as e: except sa.exc.OperationalError as e:
LOG.error("Database registration exception: %s", e) LOG.error("Database registration exception: %s", e)
return False return False
@@ -499,3 +502,90 @@ def remove_instance(session_id, instance_id):
model='instances') model='instances')
session.delete(minstance) session.delete(minstance)
# Project instances
def _project_instance_get(session, instance_id):
query = model_query(models.ProjectInstance, session)
return query.filter_by(instance_id=instance_id).first()
def project_instance_get(instance_id):
return _project_instance_get(get_session(), instance_id)
def update_project_instance(values):
values = values.copy()
minstance = models.ProjectInstance()
minstance.update(values)
session = get_session()
with session.begin():
try:
minstance.save(session=session)
except common_db_exc.DBDuplicateEntry as e:
# raise exception about duplicated columns (e.columns)
raise db_exc.FenixDBDuplicateEntry(
model=minstance.__class__.__name__, columns=e.columns)
return project_instance_get(minstance.instance_id)
def remove_project_instance(instance_id):
session = get_session()
with session.begin():
minstance = _project_instance_get(session, instance_id)
if not minstance:
# raise not found error
raise db_exc.FenixDBNotFound(session, instance_id=instance_id,
model='project_instances')
session.delete(minstance)
# Instances groups
def _instance_group_get(session, group_id):
query = model_query(models.InstanceGroup, session)
return query.filter_by(group_id=group_id).first()
def instance_group_get(group_id):
return _instance_group_get(get_session(), group_id)
def _group_instances_get(session, group_id):
query = model_query(models.ProjectInstance, session)
return query.filter_by(group_id=group_id).all()
def group_instances_get(group_id):
return _group_instances_get(get_session(), group_id)
def update_instance_group(values):
values = values.copy()
minstance = models.InstanceGroup()
minstance.update(values)
session = get_session()
with session.begin():
try:
minstance.save(session=session)
except common_db_exc.DBDuplicateEntry as e:
# raise exception about duplicated columns (e.columns)
raise db_exc.FenixDBDuplicateEntry(
model=minstance.__class__.__name__, columns=e.columns)
return instance_group_get(minstance.group_id)
def remove_instance_group(group_id):
session = get_session()
with session.begin():
minstance = _instance_group_get(session, group_id)
if not minstance:
# raise not found error
raise db_exc.FenixDBNotFound(session, group_id=group_id,
model='instance_groups')
session.delete(minstance)

View File

@@ -154,3 +154,39 @@ class MaintenanceDownload(mb.FenixBase):
def to_dict(self): def to_dict(self):
return super(MaintenanceDownload, self).to_dict() return super(MaintenanceDownload, self).to_dict()
class ProjectInstance(mb.FenixBase):
"""Project instances"""
__tablename__ = 'project_instances'
instance_id = sa.Column(sa.String(36), primary_key=True)
project_id = sa.Column(sa.String(36), nullable=True)
group_id = sa.Column(sa.String(36), nullable=False)
instance_name = sa.Column(sa.String(length=255), nullable=False)
max_interruption_time = sa.Column(sa.Integer, nullable=False)
migration_type = sa.Column(sa.String(36), nullable=False)
resource_mitigation = sa.Column(sa.Boolean, default=False)
lead_time = sa.Column(sa.Integer, nullable=False)
def to_dict(self):
return super(ProjectInstance, self).to_dict()
class InstanceGroup(mb.FenixBase):
"""Instances groups"""
__tablename__ = 'instance_groups'
group_id = sa.Column(sa.String(36), primary_key=True)
project_id = sa.Column(sa.String(36), nullable=True)
group_name = sa.Column(sa.String(length=255), nullable=False)
anti_affinity_group = sa.Column(sa.Boolean, default=False)
max_instances_per_host = sa.Column(sa.Integer, nullable=True)
max_impacted_members = sa.Column(sa.Integer, nullable=False)
recovery_time = sa.Column(sa.Integer, nullable=False)
resource_mitigation = sa.Column(sa.Boolean, default=False)
def to_dict(self):
return super(InstanceGroup, self).to_dict()

View File

@@ -13,15 +13,21 @@
import itertools import itertools
from fenix.policies import base from fenix.policies import base
from fenix.policies import instance
from fenix.policies import instance_group
from fenix.policies import maintenance from fenix.policies import maintenance
from fenix.policies import project from fenix.policies import project
from fenix.policies import project_instance
from fenix.policies import session from fenix.policies import session
def list_rules(): def list_rules():
return itertools.chain( return itertools.chain(
base.list_rules(), base.list_rules(),
instance.list_rules(),
instance_group.list_rules(),
maintenance.list_rules(), maintenance.list_rules(),
session.list_rules(), session.list_rules(),
project.list_rules(), project.list_rules(),
project_instance.list_rules()
) )

View File

@@ -0,0 +1,57 @@
# 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 oslo_policy import policy
from fenix.policies import base
POLICY_ROOT = 'fenix:instance:%s'
instance_policies = [
policy.DocumentedRuleDefault(
name=POLICY_ROOT % 'get',
check_str=base.RULE_ADMIN_OR_OWNER,
description='Policy rule for showing instance API.',
operations=[
{
'path': '/{api_version}/instance/{instance_id}/',
'method': 'GET'
}
]
),
policy.DocumentedRuleDefault(
name=POLICY_ROOT % 'put',
check_str=base.RULE_ADMIN_OR_OWNER,
description='Policy rule for updating instance API.',
operations=[
{
'path': '/{api_version}/instance/{instance_id}/',
'method': 'PUT'
}
]
),
policy.DocumentedRuleDefault(
name=POLICY_ROOT % 'delete',
check_str=base.RULE_ADMIN_OR_OWNER,
description='Policy rule for deleting instance API',
operations=[
{
'path': '/{api_version}/instance/{instance_id}',
'method': 'DELETE'
}
]
)
]
def list_rules():
return instance_policies

View File

@@ -0,0 +1,57 @@
# 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 oslo_policy import policy
from fenix.policies import base
POLICY_ROOT = 'fenix:instance_group:%s'
instance_group_policies = [
policy.DocumentedRuleDefault(
name=POLICY_ROOT % 'get',
check_str=base.RULE_ADMIN_OR_OWNER,
description='Policy rule for showing instance_group API.',
operations=[
{
'path': '/{api_version}/instance_group/{group_id}/',
'method': 'GET'
}
]
),
policy.DocumentedRuleDefault(
name=POLICY_ROOT % 'put',
check_str=base.RULE_ADMIN_OR_OWNER,
description='Policy rule for updating instance_group API.',
operations=[
{
'path': '/{api_version}/instance_group/{group_id}/',
'method': 'PUT'
}
]
),
policy.DocumentedRuleDefault(
name=POLICY_ROOT % 'delete',
check_str=base.RULE_ADMIN_OR_OWNER,
description='Policy rule for deleting instance_group API',
operations=[
{
'path': '/{api_version}/instance_group/{group_id}',
'method': 'DELETE'
}
]
)
]
def list_rules():
return instance_group_policies

View File

@@ -0,0 +1,37 @@
# 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 oslo_policy import policy
from fenix.policies import base
POLICY_ROOT = 'fenix:maintenance:session:project:instance:%s'
project_policies = [
policy.DocumentedRuleDefault(
name=POLICY_ROOT % 'put',
check_str=base.RULE_ADMIN_OR_OWNER,
description='Policy rule for setting project session affected '
'instance action and state API.',
operations=[
{
'path': '/{api_version}/maintenance/{seission_id}/'
'{project_id}/{instance_id}',
'method': 'PUT'
}
]
)
]
def list_rules():
return project_policies

View File

@@ -36,6 +36,7 @@ from shutil import rmtree
from uuid import uuid1 as generate_uuid from uuid import uuid1 as generate_uuid
from fenix import context from fenix import context
from fenix.db import api as db_api
from fenix.utils.download import download_url from fenix.utils.download import download_url
import fenix.utils.identity_auth import fenix.utils.identity_auth
@@ -191,6 +192,47 @@ class EngineEndpoint(object):
data["instance_actions"].copy()) data["instance_actions"].copy())
return data return data
def project_update_session_instance(self, ctx, session_id, project_id,
instance_id, data):
"""Update maintenance workflow session project instance state"""
LOG.info("EngineEndpoint: project_update_session_instance")
session_obj = self.workflow_sessions[session_id]
instance = session_obj.instance_by_id(instance_id)
instance.project_state = data["state"]
if "instance_action" in data:
instance.action = data["instance_action"]
return data
def get_instance(self, ctx, instance_id):
LOG.info("EngineEndpoint: get_instance")
instance = db_api.project_instance_get(instance_id)
return instance
def update_instance(self, ctx, instance_id, data):
LOG.info("EngineEndpoint: update_instance")
instance = db_api.update_project_instance(data)
return instance
def delete_instance(self, ctx, instance_id):
LOG.info("EngineEndpoint: delete_instance")
db_api.remove_project_instance(instance_id)
return {}
def get_instance_group(self, ctx, group_id):
LOG.info("EngineEndpoint: get_instance_group")
instance_group = db_api.instance_group_get_detailed(group_id)
return instance_group
def update_instance_group(self, ctx, group_id, data):
LOG.info("EngineEndpoint: update_instance_group")
instance_group = db_api.update_instance_group(data)
return instance_group
def delete_instance_group(self, ctx, group_id):
LOG.info("EngineEndpoint: delete_instance_group")
db_api.remove_instance_group(group_id)
return {}
class RPCServer(service.Service): class RPCServer(service.Service):

27
fenix/utils/thread.py Normal file
View File

@@ -0,0 +1,27 @@
# Copyright (c) 2019 OpenStack Foundation.
# All 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.
def run_async(func):
from functools import wraps
from threading import Thread
@wraps(func)
def async_func(*args, **kwargs):
thread = Thread(target=func, args=args, kwargs=kwargs)
thread.start()
return thread
return async_func

View File

@@ -13,6 +13,7 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
import aodhclient.client as aodhclient import aodhclient.client as aodhclient
from aodhclient.exceptions import BadRequest
from ast import literal_eval from ast import literal_eval
import collections import collections
from oslo_log import log as logging from oslo_log import log as logging
@@ -293,6 +294,10 @@ class BaseWorkflow(Thread):
if instance.host == host and if instance.host == host and
instance.project_id == project] instance.project_id == project]
def instances_by_host(self, host):
return [instance for instance in self.instances if
instance.host == host]
def instance_action_by_project_reply(self, project, instance_id): def instance_action_by_project_reply(self, project, instance_id):
return self.proj_instance_actions[project][instance_id] return self.proj_instance_actions[project][instance_id]
@@ -416,9 +421,13 @@ class BaseWorkflow(Thread):
LOG.info("%s: done" % self.session_id) LOG.info("%s: done" % self.session_id)
def projects_listen_alarm(self, match_event): def projects_listen_alarm(self, match_event):
match_projects = ([str(alarm['project_id']) for alarm in try:
self.aodh.alarm.list() if alarms = self.aodh.alarm.list({'all_projects': True})
str(alarm['event_rule']['event_type']) == except BadRequest:
# Train or earlier
alarms = self.aodh.alarm.list()
match_projects = ([str(alarm['project_id']) for alarm in alarms
if str(alarm['event_rule']['event_type']) ==
match_event]) match_event])
all_projects_match = True all_projects_match = True
for project in self.project_names(): for project in self.project_names():
@@ -435,6 +444,10 @@ class BaseWorkflow(Thread):
self.session_id, self.session_id,
project_id) project_id)
if "/maintenance/" not in instance_ids:
# Single instance
reply_url += "/%s" % instance_ids[0]
payload = dict(project_id=project_id, payload = dict(project_id=project_id,
instance_ids=instance_ids, instance_ids=instance_ids,
allowed_actions=allowed_actions, allowed_actions=allowed_actions,
@@ -513,3 +526,28 @@ class BaseWorkflow(Thread):
pnames = self._project_names_in_state(projects, state) pnames = self._project_names_in_state(projects, state)
LOG.error('%s: projects not answered: %s' % (self.session_id, pnames)) LOG.error('%s: projects not answered: %s' % (self.session_id, pnames))
return False return False
def wait_instance_reply_state(self, state, instance, timer_name):
state_ack = 'ACK_%s' % state
state_nack = 'NACK_%s' % state
LOG.info('wait_instance_reply_state: %s' % instance.instance_id)
while not self.is_timer_expired(timer_name):
answer = instance.project_state
if answer == state:
pass
else:
self.stop_timer(timer_name)
if answer == state_ack:
LOG.info('%s in: %s' % (instance.instance_id, answer))
return True
elif answer == state_nack:
LOG.info('%s in: %s' % (instance.instance_id, answer))
return False
else:
LOG.error('%s in invalid state: %s' %
(instance.instance_id, answer))
return False
time.sleep(1)
LOG.error('%s: timer %s expired' %
(self.session_id, timer_name))
return False

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[tox] [tox]
minversion = 2.0 minversion = 2.0
envlist = py36,py35,py27,pep8,docs envlist = py36,py35,pep8,docs
skipsdist = True skipsdist = True
[testenv] [testenv]