Merge "VNF workflow implementation"
This commit is contained in:
@@ -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)
|
||||||
|
@@ -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:
|
||||||
|
@@ -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)
|
||||||
|
@@ -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)
|
||||||
|
@@ -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)
|
||||||
|
@@ -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')
|
||||||
|
@@ -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)
|
||||||
|
@@ -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()
|
||||||
|
@@ -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()
|
||||||
)
|
)
|
||||||
|
57
fenix/policies/instance.py
Normal file
57
fenix/policies/instance.py
Normal 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
|
57
fenix/policies/instance_group.py
Normal file
57
fenix/policies/instance_group.py
Normal 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
|
37
fenix/policies/project_instance.py
Normal file
37
fenix/policies/project_instance.py
Normal 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
|
@@ -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
27
fenix/utils/thread.py
Normal 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
|
@@ -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
|
||||||
|
1087
fenix/workflow/workflows/vnf.py
Normal file
1087
fenix/workflow/workflows/vnf.py
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user