Merge "initial commit for tripleo-critical-bugs"
This commit is contained in:
commit
9c9d467cbe
|
@ -0,0 +1,4 @@
|
||||||
|
Tool used to automated CIX escalations for Red Hat Openstack TripleO group.
|
||||||
|
|
||||||
|
|
||||||
|
./statusreport.py --trello_token $token --trello_api_key $key --trello_board_id $board_id
|
|
@ -0,0 +1,16 @@
|
||||||
|
# Each line is a launchpad project name, filter regex
|
||||||
|
[LaunchpadBugs]
|
||||||
|
TripleO=tripleo,.*
|
||||||
|
#Heat=heat,.*
|
||||||
|
|
||||||
|
[TrelloConfig]
|
||||||
|
# now in the cli
|
||||||
|
#token=
|
||||||
|
#api_key=
|
||||||
|
#board_id=
|
||||||
|
list_outtage=Critical PChain Outage
|
||||||
|
list_tech_debt=Release Blocker Technical Debt
|
||||||
|
list_new=New / Triage
|
||||||
|
|
||||||
|
[Bug]
|
||||||
|
delay: 5
|
|
@ -0,0 +1,12 @@
|
||||||
|
#
|
||||||
|
# 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.
|
|
@ -0,0 +1,11 @@
|
||||||
|
import requests
|
||||||
|
|
||||||
|
base_url = "https://opendev.org/openstack/tripleo-ci-health-queries/raw/branch/master/output/elastic-recheck/"
|
||||||
|
health_url = "http://health.sbarnea.com/"
|
||||||
|
|
||||||
|
|
||||||
|
def get_health_link(bug_id):
|
||||||
|
response = requests.get(base_url + str(bug_id) + ".yaml")
|
||||||
|
if response.status_code == 200:
|
||||||
|
return health_url + "#" + str(bug_id)
|
||||||
|
return ""
|
|
@ -0,0 +1,65 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
#
|
||||||
|
# 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 datetime
|
||||||
|
from datetime import timedelta
|
||||||
|
import pytz
|
||||||
|
import re
|
||||||
|
|
||||||
|
from launchpadlib.launchpad import Launchpad
|
||||||
|
|
||||||
|
""" this returns a list of launchpad bugs """
|
||||||
|
|
||||||
|
|
||||||
|
class LaunchpadReport(object):
|
||||||
|
def __init__(self, bugs, config):
|
||||||
|
self.bugs = bugs
|
||||||
|
self.config = config
|
||||||
|
|
||||||
|
def generate(self):
|
||||||
|
bugs_with_alerts_open = {}
|
||||||
|
bugs_with_alerts_closed = {}
|
||||||
|
launchpad = Launchpad.login_anonymously(
|
||||||
|
'Red Hat Status Bot', 'production', '.cache', version='devel'
|
||||||
|
)
|
||||||
|
bug_statuses_open = ['Confirmed', 'Triaged', 'In Progress', 'Fix Committed']
|
||||||
|
bug_statuses_closed = ['Fix Released']
|
||||||
|
for label, config_string in self.bugs.items():
|
||||||
|
|
||||||
|
c = config_string.split(',')
|
||||||
|
project = launchpad.projects[c[0]]
|
||||||
|
filter_re = c[1]
|
||||||
|
for milestone in project.all_milestones:
|
||||||
|
if re.match(filter_re, milestone.name):
|
||||||
|
for task in project.searchTasks(
|
||||||
|
milestone=milestone,
|
||||||
|
status=bug_statuses_open,
|
||||||
|
tags='promotion-blocker',
|
||||||
|
):
|
||||||
|
now = datetime.datetime.now(pytz.UTC)
|
||||||
|
delay = int(self.config.get('Bug', 'delay'))
|
||||||
|
delay_time = now - timedelta(hours=delay)
|
||||||
|
if delay_time > task.date_created:
|
||||||
|
bugs_with_alerts_open[task.bug.id] = task.bug
|
||||||
|
|
||||||
|
for task in project.searchTasks(
|
||||||
|
milestone=milestone,
|
||||||
|
status=bug_statuses_closed,
|
||||||
|
importance='Critical',
|
||||||
|
tags='alert',
|
||||||
|
):
|
||||||
|
|
||||||
|
bugs_with_alerts_closed[task.bug.id] = task.bug
|
||||||
|
return bugs_with_alerts_open, bugs_with_alerts_closed
|
|
@ -0,0 +1,265 @@
|
||||||
|
#!/usr/bin/python
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
import dateutil.parser
|
||||||
|
import pytz
|
||||||
|
import requests
|
||||||
|
from dateutil.relativedelta import relativedelta
|
||||||
|
|
||||||
|
BOARD_BLACKLIST = os.environ.get('TRELLO_BOARD_BLACKLIST')
|
||||||
|
|
||||||
|
#
|
||||||
|
# API CONTEXT OBJECT
|
||||||
|
#
|
||||||
|
|
||||||
|
|
||||||
|
class ApiContext(object):
|
||||||
|
def __init__(self, config):
|
||||||
|
self.config = config
|
||||||
|
self._apiVersion = 1
|
||||||
|
self.apiToken = config.get('TrelloConfig', 'token')
|
||||||
|
self.apiKey = config.get('TrelloConfig', 'api_key')
|
||||||
|
self._payload = {'key': self.apiKey, 'token': self.apiToken}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ApiRootUrl(self):
|
||||||
|
return "https://trello.com/%s" % self._apiVersion
|
||||||
|
|
||||||
|
@property
|
||||||
|
def Payload(self):
|
||||||
|
return self._payload
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# BOARDS
|
||||||
|
#
|
||||||
|
class Boards(object):
|
||||||
|
def __init__(self, context):
|
||||||
|
self._api = context
|
||||||
|
|
||||||
|
# note: it's not currently possible to nuke a board, only to close it
|
||||||
|
def create(self, name, description=None):
|
||||||
|
"create a new board."
|
||||||
|
response = requests.post(
|
||||||
|
"%s/boards" % self._api.ApiRootUrl,
|
||||||
|
params=self._api.Payload,
|
||||||
|
data=dict(name=name, desc=description),
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
jsonResponse = json.loads(response.text)
|
||||||
|
return jsonResponse
|
||||||
|
|
||||||
|
def get_all_by_member(self, memberNameOrId):
|
||||||
|
"Obtain data struct for a board by name."
|
||||||
|
|
||||||
|
memberBoardsUrl = '{0}/members/{1}/boards'.format(
|
||||||
|
self._api.ApiRootUrl, memberNameOrId
|
||||||
|
)
|
||||||
|
response = requests.get(memberBoardsUrl, params=self._api.Payload)
|
||||||
|
response.raise_for_status()
|
||||||
|
jsonResponse = json.loads(response.text)
|
||||||
|
return jsonResponse
|
||||||
|
|
||||||
|
def get_name(self, boardId):
|
||||||
|
"Obtain data struct for a board by name."
|
||||||
|
|
||||||
|
memberBoardsUrl = '{0}/boards/{1}'.format(self._api.ApiRootUrl, boardId)
|
||||||
|
response = requests.get(memberBoardsUrl, params=self._api.Payload)
|
||||||
|
response.raise_for_status()
|
||||||
|
jsonResponse = json.loads(response.text)
|
||||||
|
return jsonResponse['name']
|
||||||
|
|
||||||
|
def get_all_by_member_and_name(
|
||||||
|
self, memberNameOrId, boardName, raiseExceptionIfDuplicates=True
|
||||||
|
):
|
||||||
|
"Get all boards for a member, and return those matching a name (handles duplicate names)."
|
||||||
|
boards = self.get_all_by_member(memberNameOrId)
|
||||||
|
boardsToReturn = [b for b in boards if b['name'] == boardName]
|
||||||
|
|
||||||
|
if raiseExceptionIfDuplicates:
|
||||||
|
if len(boardsToReturn) != 1:
|
||||||
|
raise AssertionError(
|
||||||
|
"ERROR: get_all_by_member_and_name({0}, {1}) - NO DUPES ALLOWED".format(
|
||||||
|
memberNameOrId, boardName
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return boardsToReturn
|
||||||
|
|
||||||
|
def get_lists(self, boardId):
|
||||||
|
"Get all lists on a board."
|
||||||
|
|
||||||
|
boardListsUrl = '{0}/boards/{1}/lists'.format(self._api.ApiRootUrl, boardId)
|
||||||
|
resp = requests.get(boardListsUrl, params=self._api.Payload)
|
||||||
|
resp.raise_for_status()
|
||||||
|
jsonResp = json.loads(resp.text)
|
||||||
|
return jsonResp
|
||||||
|
|
||||||
|
def get_cards(self, boardId):
|
||||||
|
"Get all cards on a board."
|
||||||
|
|
||||||
|
boardListsUrl = '{0}/boards/{1}/cards'.format(self._api.ApiRootUrl, boardId)
|
||||||
|
resp = requests.get(boardListsUrl, params=self._api.Payload)
|
||||||
|
resp.raise_for_status()
|
||||||
|
jsonResp = json.loads(resp.text)
|
||||||
|
return jsonResp
|
||||||
|
|
||||||
|
def get_lists_by_name(self, boardId, listName, raiseExceptionIfDuplicates=True):
|
||||||
|
"Get all lists associated with a board by name."
|
||||||
|
lists = self.get_lists(boardId)
|
||||||
|
listsToReturn = [lst for lst in lists if lst['name'] == listName]
|
||||||
|
|
||||||
|
if raiseExceptionIfDuplicates:
|
||||||
|
if len(listsToReturn) != 1:
|
||||||
|
raise AssertionError(
|
||||||
|
"ERROR: get_lists_by_name({0}, {1}) - NO DUPES ALLOWED".format(
|
||||||
|
boardId, listName
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return listsToReturn
|
||||||
|
|
||||||
|
def get_lists_by_id(self, boardId, listId, raiseExceptionIfDuplicates=True):
|
||||||
|
"Get all lists associated with a board by id."
|
||||||
|
lists = self.get_lists(boardId)
|
||||||
|
listsToReturn = [lst for lst in lists if lst['id'] == listId]
|
||||||
|
|
||||||
|
# if raiseExceptionIfDuplicates == True:
|
||||||
|
# if len(listsToReturn) != 1:
|
||||||
|
# raise AssertionError("ERROR: get_lists_by_id({0}, {1}) - NO DUPES ALLOWED".format(boardId, listId))
|
||||||
|
|
||||||
|
return listsToReturn
|
||||||
|
|
||||||
|
# TODO: for now do this expensive way (getting everything and filtering) vs. a nice nuanced query
|
||||||
|
def get_single_by_member_and_name(self, memberNameOrId, boardName):
|
||||||
|
board = self.get_all_by_member_and_name(memberNameOrId, boardName)
|
||||||
|
id = board[0]['id']
|
||||||
|
return id
|
||||||
|
|
||||||
|
def get_single_list_by_name(self, boardId, listName):
|
||||||
|
lists = self.get_lists_by_name(boardId, listName)
|
||||||
|
id = lists[0]['id']
|
||||||
|
return id
|
||||||
|
|
||||||
|
def get_single_list_by_id(self, boardId, listId):
|
||||||
|
lists = self.get_lists_by_id(boardId, listId)
|
||||||
|
try:
|
||||||
|
name = lists[0]['name']
|
||||||
|
except IndexError:
|
||||||
|
name = "unknown"
|
||||||
|
return name
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# MEMBERS
|
||||||
|
#
|
||||||
|
class Members(object):
|
||||||
|
def __init__(self, context):
|
||||||
|
self._api = context
|
||||||
|
|
||||||
|
def get_member(self, memberName):
|
||||||
|
"Get member data based on name"
|
||||||
|
membersUrl = '{0}/members/{1}'.format(self._api.ApiRootUrl, memberName)
|
||||||
|
response = requests.get(membersUrl, params=self._api.Payload)
|
||||||
|
response.raise_for_status()
|
||||||
|
return json.loads(response.text)
|
||||||
|
|
||||||
|
def get_member_id(self, memberName):
|
||||||
|
"Get member id based on name"
|
||||||
|
return self.get_member(memberName)['id']
|
||||||
|
|
||||||
|
def get_member_name(self, memberId):
|
||||||
|
"Get member name based on id"
|
||||||
|
return self.get_member(memberId)['fullName'].encode('ascii', 'ignore')
|
||||||
|
|
||||||
|
def get_member_names_from_list(self, memberId):
|
||||||
|
"Get member name based on id"
|
||||||
|
if isinstance(memberId, list):
|
||||||
|
names = [self.get_member_name(member) for member in memberId]
|
||||||
|
return ', '.join(names)
|
||||||
|
else:
|
||||||
|
raise TypeError()
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# CARDS
|
||||||
|
#
|
||||||
|
class Cards(object):
|
||||||
|
def __init__(self, context):
|
||||||
|
self._api = context
|
||||||
|
|
||||||
|
def get_card(self, cardId):
|
||||||
|
cardUrl = '{0}/cards/{1}'.format(self._api.ApiRootUrl, cardId)
|
||||||
|
response = requests.get(cardUrl, params=self._api.Payload)
|
||||||
|
response.raise_for_status()
|
||||||
|
return json.loads(response.text)
|
||||||
|
|
||||||
|
def get_card_due_date(self, cardId):
|
||||||
|
"Get member id based on name"
|
||||||
|
return self.get_card(cardId)['due']
|
||||||
|
|
||||||
|
def get_card_labels(self, cardId):
|
||||||
|
return self.get_card(cardId)['labels']
|
||||||
|
|
||||||
|
def get_card_members(self, cardId):
|
||||||
|
return self.get_card(cardId)['idMembers']
|
||||||
|
|
||||||
|
def create(self, name, idList, due=None, desc=None):
|
||||||
|
"create a new card, optionally setting a due date and description."
|
||||||
|
response = requests.post(
|
||||||
|
"%s/cards" % self._api.ApiRootUrl,
|
||||||
|
params=self._api.Payload,
|
||||||
|
data=dict(name=name, idList=idList, due=due, desc=desc),
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
return json.loads(response.text)
|
||||||
|
|
||||||
|
def add_comment_to_card(self, cardId, comment):
|
||||||
|
"Add a member to a card"
|
||||||
|
postMemberToCardUrl = '{0}/cards/{1}/actions/comments'.format(
|
||||||
|
self._api.ApiRootUrl, cardId
|
||||||
|
)
|
||||||
|
response = requests.post(
|
||||||
|
postMemberToCardUrl, params=self._api.Payload, data={'text': comment}
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
return json.loads(response.text)
|
||||||
|
|
||||||
|
def add_due_date_to_card(self, card, date):
|
||||||
|
"Add a due date to a trello card"
|
||||||
|
putDueDateToCardUrl = '{0}/cards/{1}'.format(self._api.ApiRootUrl, card['id'])
|
||||||
|
response = requests.put(
|
||||||
|
putDueDateToCardUrl, params=self._api.Payload, data={'due': date}
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
return json.loads(response.text)
|
||||||
|
|
||||||
|
def check_card_overdue(self, cardId, blocking_labels, overdue_notice):
|
||||||
|
now = datetime.now(pytz.utc)
|
||||||
|
due = dateutil.parser.parse(self.get_card_due_date(cardId))
|
||||||
|
delta = relativedelta(now, due)
|
||||||
|
if delta.days > 0 or delta.months > 0:
|
||||||
|
if not self.check_card_blocked_label(cardId, blocking_labels):
|
||||||
|
self.add_comment_to_card(cardId, overdue_notice)
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def check_card_blocked_label(self, cardId, blocking_labels):
|
||||||
|
labels = self.get_card_labels(cardId)
|
||||||
|
if [label for label in labels if label['name'] in blocking_labels]:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_cards(self, listId, filterArg="all"):
|
||||||
|
"Get cards on a given list"
|
||||||
|
getCardsUrl = '{0}/lists/{1}/cards/{2}'.format(
|
||||||
|
self._api.ApiRootUrl, listId, filterArg
|
||||||
|
)
|
||||||
|
response = requests.get(getCardsUrl, params=self._api.Payload)
|
||||||
|
response.raise_for_status()
|
||||||
|
return json.loads(response.text)
|
|
@ -0,0 +1,7 @@
|
||||||
|
click
|
||||||
|
configparser
|
||||||
|
launchpadlib==1.10.5
|
||||||
|
python-dateutil
|
||||||
|
#pytz==2015.7
|
||||||
|
pytz
|
||||||
|
requests
|
|
@ -0,0 +1,194 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
# The MIT License (MIT)
|
||||||
|
# Copyright (c) 2017 Wes Hayutin <weshayutin@gmail.com>
|
||||||
|
#
|
||||||
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
# of this software and associated documentation files (the "Software"), to deal
|
||||||
|
# in the Software without restriction, including without limitation the rights
|
||||||
|
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
# copies of the Software, and to permit persons to whom the Software is
|
||||||
|
# furnished to do so, subject to the following conditions:
|
||||||
|
#
|
||||||
|
# The above copyright notice and this permission notice shall be included in all
|
||||||
|
# copies or substantial portions of the Software.
|
||||||
|
#
|
||||||
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
# SOFTWARE.
|
||||||
|
|
||||||
|
"""
|
||||||
|
takes a list of launchpad bugs that have the tag 'promotion-blocker'
|
||||||
|
then compares the list to cards on the cix trello board. If there
|
||||||
|
is a lp bug that does not have a card it will open a trello card
|
||||||
|
"""
|
||||||
|
|
||||||
|
import configparser
|
||||||
|
|
||||||
|
import click
|
||||||
|
|
||||||
|
import reports.trello as trello
|
||||||
|
from reports.erhealth import get_health_link
|
||||||
|
from reports.launchpad import LaunchpadReport
|
||||||
|
|
||||||
|
|
||||||
|
class StatusReport(object):
|
||||||
|
"""
|
||||||
|
compares a list of launchpad bugs to a list
|
||||||
|
of trello cards.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, config):
|
||||||
|
|
||||||
|
self.config = config
|
||||||
|
self.brief_status = {}
|
||||||
|
self.detailed_status = {}
|
||||||
|
|
||||||
|
def summarise_launchpad_bugs(self):
|
||||||
|
"""
|
||||||
|
a list of open bugs with promotion-blocker
|
||||||
|
also returns a list of closed bugs if action needs to
|
||||||
|
be taken
|
||||||
|
"""
|
||||||
|
if not self.config.has_section('LaunchpadBugs'):
|
||||||
|
return
|
||||||
|
|
||||||
|
bugs = self.config["LaunchpadBugs"]
|
||||||
|
report = LaunchpadReport(bugs, self.config)
|
||||||
|
bugs_with_alerts_open, bugs_with_alerts_closed = report.generate()
|
||||||
|
return bugs_with_alerts_open, bugs_with_alerts_closed
|
||||||
|
|
||||||
|
def print_report(self, bug_list):
|
||||||
|
"""print the bugs to the console"""
|
||||||
|
bug_number_list = []
|
||||||
|
for key, value in bug_list.items():
|
||||||
|
print(key)
|
||||||
|
bug_number_list.append(str(key))
|
||||||
|
return bug_number_list
|
||||||
|
|
||||||
|
def _get_config_items(self, section_name, prefix=None):
|
||||||
|
if not self.config.has_section(section_name):
|
||||||
|
return {}
|
||||||
|
|
||||||
|
items = {
|
||||||
|
k: v
|
||||||
|
for (k, v) in self.config.items(section_name)
|
||||||
|
if not k.startswith('_') and (prefix is None or k.startswith(prefix))
|
||||||
|
}
|
||||||
|
return items
|
||||||
|
|
||||||
|
def compare_bugs_with_cards(self, list_of_bugs, cards):
|
||||||
|
"""
|
||||||
|
compare a list of bugs to trello cards by checking for
|
||||||
|
the bug number in the title of the trello card
|
||||||
|
"""
|
||||||
|
open_bugs = list_of_bugs
|
||||||
|
cards_outtage_names = []
|
||||||
|
for card in cards:
|
||||||
|
cards_outtage_names.append(card['name'])
|
||||||
|
print(card['name'].encode('utf-8'))
|
||||||
|
|
||||||
|
match = []
|
||||||
|
for card in cards_outtage_names:
|
||||||
|
for key in open_bugs:
|
||||||
|
key = str(key)
|
||||||
|
if key in card:
|
||||||
|
match.append(int(key))
|
||||||
|
print("##########################################")
|
||||||
|
print("openbugs " + str(set(open_bugs)))
|
||||||
|
print("match " + str(set(match)))
|
||||||
|
critical_bugs_with_out_escalation_cards = list(set(open_bugs) - set(match))
|
||||||
|
return critical_bugs_with_out_escalation_cards
|
||||||
|
|
||||||
|
def create_escalation(
|
||||||
|
self, config, critical_bugs_with_out_escalation_cards, list_of_bugs, trello_list
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
create a trello card in the triage list of the cix board
|
||||||
|
"""
|
||||||
|
if not critical_bugs_with_out_escalation_cards:
|
||||||
|
print("There are no bugs that require a new escalation")
|
||||||
|
else:
|
||||||
|
# send email to list
|
||||||
|
for bug in critical_bugs_with_out_escalation_cards:
|
||||||
|
bug_title = list_of_bugs[bug].title
|
||||||
|
bug_link = list_of_bugs[bug].web_link
|
||||||
|
health_link = get_health_link(list_of_bugs[bug].id)
|
||||||
|
|
||||||
|
card_title = (
|
||||||
|
"[CIX][LP:" + str(bug) + "][tripleoci][proa] " + str(bug_title)
|
||||||
|
)
|
||||||
|
|
||||||
|
# create escalation card
|
||||||
|
trello_api_context = trello.ApiContext(config)
|
||||||
|
trello_cards = trello.Cards(trello_api_context)
|
||||||
|
trello_cards.create(
|
||||||
|
card_title, trello_list, desc=bug_link + "\n" + health_link
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@click.command()
|
||||||
|
@click.option(
|
||||||
|
"--config_file",
|
||||||
|
default="config/critical-alert-escalation.cfg",
|
||||||
|
help="Defaults to 'config/critical-alert-escalation.cfg'",
|
||||||
|
)
|
||||||
|
@click.option("--trello_token", required=True, help="Your Trello Token")
|
||||||
|
@click.option("--trello_api_key", required=True, help="Your Trello api key")
|
||||||
|
@click.option("--trello_board_id", required=True, help="The trello board id")
|
||||||
|
def main(config_file, trello_token, trello_api_key, trello_board_id):
|
||||||
|
"""
|
||||||
|
get the list of promotion-blocker bugs
|
||||||
|
compare the list to trello
|
||||||
|
create cards as needed
|
||||||
|
"""
|
||||||
|
|
||||||
|
config = configparser.ConfigParser()
|
||||||
|
config.read(config_file)
|
||||||
|
config['TrelloConfig']['token'] = trello_token
|
||||||
|
config['TrelloConfig']['api_key'] = trello_api_key
|
||||||
|
config['TrelloConfig']['board_id'] = trello_board_id
|
||||||
|
|
||||||
|
report = StatusReport(config)
|
||||||
|
|
||||||
|
bugs_with_alerts_open, bugs_with_alerts_closed = report.summarise_launchpad_bugs()
|
||||||
|
|
||||||
|
print("*** open critical bugs ***")
|
||||||
|
report.print_report(bugs_with_alerts_open)
|
||||||
|
print("*** closed critical bugs ***")
|
||||||
|
report.print_report(bugs_with_alerts_closed)
|
||||||
|
|
||||||
|
trello_api_context = trello.ApiContext(config)
|
||||||
|
trello_boards = trello.Boards(trello_api_context)
|
||||||
|
|
||||||
|
trello_new_list = trello_boards.get_lists_by_name(
|
||||||
|
config.get('TrelloConfig', 'board_id'), config.get('TrelloConfig', 'list_new')
|
||||||
|
)
|
||||||
|
trello_new_list_id = str(trello_new_list[0]['id'])
|
||||||
|
|
||||||
|
all_cards_on_board = trello_boards.get_cards(config.get('TrelloConfig', 'board_id'))
|
||||||
|
print("all cards " + str(len(all_cards_on_board)))
|
||||||
|
cards_outtage = all_cards_on_board
|
||||||
|
|
||||||
|
critical_bugs_with_out_escalation_cards = report.compare_bugs_with_cards(
|
||||||
|
bugs_with_alerts_open, cards_outtage
|
||||||
|
)
|
||||||
|
print(
|
||||||
|
"critical bugs not tracked on board "
|
||||||
|
+ str(critical_bugs_with_out_escalation_cards)
|
||||||
|
)
|
||||||
|
|
||||||
|
report.create_escalation(
|
||||||
|
config,
|
||||||
|
critical_bugs_with_out_escalation_cards,
|
||||||
|
bugs_with_alerts_open,
|
||||||
|
trello_new_list_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
|
@ -64,6 +64,7 @@
|
||||||
# tripleo-ansible
|
# tripleo-ansible
|
||||||
- ^_skeleton_role_/.*
|
- ^_skeleton_role_/.*
|
||||||
- ^scripts/.*
|
- ^scripts/.*
|
||||||
|
- ^scripts/tripleo-critical-bugs/.*
|
||||||
- ^tox.ini$
|
- ^tox.ini$
|
||||||
- ^tripleo_ansible/playbooks/docker-vfs-setup.yml$
|
- ^tripleo_ansible/playbooks/docker-vfs-setup.yml$
|
||||||
- ^tripleo_ansible/.*molecule.*
|
- ^tripleo_ansible/.*molecule.*
|
||||||
|
@ -173,6 +174,7 @@
|
||||||
- ^playbooks/tripleo-buildcontainers/.*$
|
- ^playbooks/tripleo-buildcontainers/.*$
|
||||||
- ^playbooks/tripleo-buildimages/.*$
|
- ^playbooks/tripleo-buildimages/.*$
|
||||||
- ^vars/sova-patterns.yml$
|
- ^vars/sova-patterns.yml$
|
||||||
|
- ^scripts/tripleo-critical-bugs/.*
|
||||||
dependencies:
|
dependencies:
|
||||||
- openstack-tox-linters
|
- openstack-tox-linters
|
||||||
- tripleo-ci-centos-8-content-provider-victoria:
|
- tripleo-ci-centos-8-content-provider-victoria:
|
||||||
|
|
|
@ -50,6 +50,7 @@
|
||||||
- ^test-requirements.txt$
|
- ^test-requirements.txt$
|
||||||
- ^vars/sova-patterns.yml$
|
- ^vars/sova-patterns.yml$
|
||||||
- tox.ini
|
- tox.ini
|
||||||
|
- ^scripts/.*
|
||||||
# like parent but with requirements.txt and setup.py removed
|
# like parent but with requirements.txt and setup.py removed
|
||||||
|
|
||||||
- job:
|
- job:
|
||||||
|
|
Loading…
Reference in New Issue