241 lines
9.1 KiB
Python
241 lines
9.1 KiB
Python
# -*- coding: utf-8 -*-
|
|
|
|
# Copyright 2016 Mirantis, 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 datetime import datetime
|
|
import six
|
|
|
|
from sqlalchemy.orm import undefer
|
|
|
|
from nailgun.consts import HISTORY_TASK_STATUSES
|
|
from nailgun.db import db
|
|
from nailgun.db.sqlalchemy import models
|
|
|
|
|
|
from nailgun.objects import NailgunCollection
|
|
from nailgun.objects import NailgunObject
|
|
from nailgun.objects.serializers.deployment_history \
|
|
import DeploymentHistorySerializer
|
|
from nailgun.objects import Transaction
|
|
|
|
from nailgun.logger import logger
|
|
|
|
|
|
class DeploymentHistory(NailgunObject):
|
|
|
|
model = models.DeploymentHistory
|
|
serializer = DeploymentHistorySerializer
|
|
|
|
@classmethod
|
|
def update_if_exist(cls, task_id, node_id, deployment_graph_task_name,
|
|
status, summary, custom):
|
|
deployment_history = cls.find_history(task_id, node_id,
|
|
deployment_graph_task_name)
|
|
|
|
if not deployment_history:
|
|
logger.warn("Failed to find task in history for transaction id %s"
|
|
", node_id %s and deployment_graph_task_name %s",
|
|
task_id, node_id, deployment_graph_task_name)
|
|
return
|
|
|
|
getattr(cls, 'to_{0}'.format(status))(deployment_history)
|
|
|
|
deployment_history.custom.update(custom or {})
|
|
deployment_history.summary.update(summary or {})
|
|
|
|
@classmethod
|
|
def find_history(cls, task_id, node_id, deployment_graph_task_name):
|
|
return db().query(cls.model)\
|
|
.filter_by(task_id=task_id,
|
|
node_id=node_id,
|
|
deployment_graph_task_name=deployment_graph_task_name)\
|
|
.first()
|
|
|
|
@classmethod
|
|
def to_ready(cls, deployment_history):
|
|
if deployment_history.status == HISTORY_TASK_STATUSES.running:
|
|
deployment_history.status = HISTORY_TASK_STATUSES.ready
|
|
cls._set_time_end(deployment_history)
|
|
else:
|
|
cls._logging_wrong_status_change(deployment_history.status,
|
|
HISTORY_TASK_STATUSES.ready)
|
|
|
|
@classmethod
|
|
def to_error(cls, deployment_history):
|
|
if deployment_history.status == HISTORY_TASK_STATUSES.running:
|
|
deployment_history.status = HISTORY_TASK_STATUSES.error
|
|
cls._set_time_end(deployment_history)
|
|
else:
|
|
cls._logging_wrong_status_change(deployment_history.status,
|
|
HISTORY_TASK_STATUSES.error)
|
|
|
|
@classmethod
|
|
def to_skipped(cls, deployment_history):
|
|
if deployment_history.status == HISTORY_TASK_STATUSES.pending:
|
|
deployment_history.status = HISTORY_TASK_STATUSES.skipped
|
|
cls._set_time_start(deployment_history)
|
|
cls._set_time_end(deployment_history)
|
|
else:
|
|
cls._logging_wrong_status_change(deployment_history.status,
|
|
HISTORY_TASK_STATUSES.skipped)
|
|
|
|
@classmethod
|
|
def to_running(cls, deployment_history):
|
|
if deployment_history.status == HISTORY_TASK_STATUSES.pending:
|
|
deployment_history.status = HISTORY_TASK_STATUSES.running
|
|
cls._set_time_start(deployment_history)
|
|
else:
|
|
cls._logging_wrong_status_change(deployment_history.status,
|
|
HISTORY_TASK_STATUSES.running)
|
|
|
|
@classmethod
|
|
def to_pending(cls, deployment_history):
|
|
cls._logging_wrong_status_change(deployment_history.status,
|
|
HISTORY_TASK_STATUSES.pending)
|
|
|
|
@classmethod
|
|
def _set_time_end(cls, deployment_history):
|
|
if not deployment_history.time_end:
|
|
deployment_history.time_end = datetime.utcnow()
|
|
|
|
@classmethod
|
|
def _set_time_start(cls, deployment_history):
|
|
if not deployment_history.time_start:
|
|
deployment_history.time_start = datetime.utcnow()
|
|
|
|
@classmethod
|
|
def _logging_wrong_status_change(cls, from_status, to_status):
|
|
logger.warn("Error status transition from %s to %s",
|
|
from_status, to_status)
|
|
|
|
|
|
class DeploymentHistoryCollection(NailgunCollection):
|
|
|
|
single = DeploymentHistory
|
|
|
|
@classmethod
|
|
def create(cls, task, tasks_graph):
|
|
entries = []
|
|
for node_id in tasks_graph:
|
|
for graph_task in tasks_graph[node_id]:
|
|
if not graph_task.get('id'):
|
|
logger.warn("Task name missing. Ignoring %s", graph_task)
|
|
continue
|
|
entries.append(cls.single.model(
|
|
task_id=task.id,
|
|
node_id=node_id,
|
|
deployment_graph_task_name=graph_task['id']))
|
|
|
|
db().bulk_save_objects(entries)
|
|
|
|
@classmethod
|
|
def get_history(cls, transaction, nodes_ids=None, statuses=None,
|
|
tasks_names=None, include_summary=False):
|
|
"""Get deployment tasks history.
|
|
|
|
:param transaction: task SQLAlchemy object
|
|
:type transaction: models.Task
|
|
:param nodes_ids: filter by node IDs
|
|
:type nodes_ids: list[int]|None
|
|
:param statuses: filter by statuses
|
|
:type statuses: list[basestring]|None
|
|
:param tasks_names: filter by deployment graph task names
|
|
:param include_summary: bool flag to include summary
|
|
:type tasks_names: list[basestring]|None
|
|
:returns: tasks history
|
|
:rtype: list[dict]
|
|
"""
|
|
nodes_ids = nodes_ids and frozenset(nodes_ids)
|
|
statuses = statuses and frozenset(statuses)
|
|
tasks_names = tasks_names and frozenset(tasks_names)
|
|
|
|
task_parameters_by_name = {}
|
|
visited_tasks = set()
|
|
tasks_snapshot = Transaction.get_tasks_snapshot(transaction)
|
|
history = []
|
|
|
|
if tasks_snapshot:
|
|
# make a copy for each task to avoid modification
|
|
for task in six.moves.map(dict, tasks_snapshot):
|
|
# remove ambiguous id field
|
|
task.pop('id', None)
|
|
task_parameters_by_name[task['task_name']] = task
|
|
else:
|
|
logger.warning('No tasks snapshot is defined in given '
|
|
'transaction, probably it is a legacy '
|
|
'(Fuel<10.0) or malformed.')
|
|
query = None
|
|
if include_summary:
|
|
query = cls.options(query, undefer('summary'))
|
|
history_records = cls.filter_by(query, task_id=transaction.id)
|
|
if tasks_names:
|
|
history_records = cls.filter_by_list(
|
|
history_records, 'deployment_graph_task_name', tasks_names
|
|
)
|
|
if nodes_ids:
|
|
history_records = cls.filter_by_list(
|
|
history_records, 'node_id', nodes_ids
|
|
)
|
|
if statuses and HISTORY_TASK_STATUSES.skipped not in statuses:
|
|
history_records = cls.filter_by_list(
|
|
history_records, 'status', statuses
|
|
)
|
|
|
|
for history_record in history_records:
|
|
task_name = history_record.deployment_graph_task_name
|
|
visited_tasks.add(task_name)
|
|
|
|
# the visited tasks should be calculated, it is
|
|
# reason why the query filter cannot be used here
|
|
if statuses and history_record.status not in statuses:
|
|
continue
|
|
|
|
fields = list(DeploymentHistorySerializer.fields)
|
|
if include_summary:
|
|
fields.append('summary')
|
|
record = cls.single.to_dict(history_record, fields=fields)
|
|
history.append(record)
|
|
# remove ambiguous field
|
|
record['task_name'] = record.pop('deployment_graph_task_name')
|
|
|
|
if task_parameters_by_name:
|
|
try:
|
|
record.update(task_parameters_by_name[task_name])
|
|
except KeyError:
|
|
logger.warning(
|
|
'Definition of "{0}" task is not found'
|
|
.format(task_name)
|
|
)
|
|
|
|
# calculates absent tasks respecting filter
|
|
if (not nodes_ids and (
|
|
not statuses or HISTORY_TASK_STATUSES.skipped in statuses)):
|
|
|
|
for task_name in task_parameters_by_name:
|
|
if tasks_names and task_name not in tasks_names:
|
|
continue
|
|
if task_name in visited_tasks:
|
|
continue
|
|
|
|
history.append(dict(
|
|
task_parameters_by_name[task_name],
|
|
task_name=task_name,
|
|
node_id='-',
|
|
status=HISTORY_TASK_STATUSES.skipped,
|
|
time_start=None,
|
|
time_end=None,
|
|
))
|
|
return history
|