Jira/jira_log_work.py
2025-11-28 09:46:59 +01:00

317 lines
9.5 KiB
Python

#!/usr/bin/env python3
'''
Script to log work to a jira ticket.
The MIT License (MIT)
Copyright © 2025 Peter Juerss (peter.juerss@gc-gruppe.de)
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.
'''
import os
import sys
import time
import shutil
import datetime
import argparse as ap
from getpass import getpass
from jira.client import JIRA
SCRIPT_VERSION = '1.0.4'
# --- Adjust as appropriate
JIRA_SERVER = 'https://jira.datpool.net'
JIRA_USER = 'de58076'
# --- Example
'''
./jira_log_work.py -t SF-6470 -w 30h -c "Jour Fix AL
CAB Meeting
Jour Fixes
IT Operation Lead
Mitarbeiter Jahresgespräche
Teamcall
Service Kalkulation
Jour Fix SP
Technische Entwicklung / Themen ohne Tickets"
'''
# --- Classes - DO NOT CHANGE
class ReturnCodes:
'''
Helper class for return codes
'''
rcOk = 0
rcNok = 1
rcUsage = 2
rcMissingInput = 3
rcMissingOutput = 4
rcErrorWriteFile = 5
rcErrorReadFile = 6
class ScriptLogger:
'''
Simple logging class for unified messaging throughout the script
It logs to stdout.
If initialized with a filename (log_file), the log output will be written to that file.
Default is to keep a history of 5 versions of the log file plus the actual log
Variables:
log_file = Name of the log file
log_dir = Path to the directory to store the log files
num_logs = number of old log files to keep
Example:
from lib.log import ScriptLogger as SL
logDir = os.path.abspath('log')
lg = SL(log_file='mico.log', log_dir=logDir)
lg.log(0, 'Success')
'''
def __init__(self, log_file=False, log_dir='log', num_logs=5, log_level=1):
self.keep_file = ['.gitkeep']
self.num_logs = int(num_logs)
# self.log_dir = os.path.abspath(log_dir)
self.log_dir = log_dir
self.log_file = log_file
self.log_level = int(log_level)
if not self.log_file is False:
self.log_file = os.path.join(self.log_dir, self.log_file)
self.initLogDir()
self.backupLogs()
self.initLogFile()
def log(self, level=int(0), key=str(''), value=str('')):
log_level = {
'0': '\033[1m\033[92m[I]\033[0m',
'1': '\033[1m\033[91m[E]\033[0m',
'2': '\033[1m\033[93m[W]\033[0m',
'3': '\033[1m\033[94m[T]\033[0m',
'4': '\033[1m\033[95m[D]\033[0m',
'99': '-'
}
if value:
key = key + ':'
msg = '{0:4} {1:36} {2}'.format(log_level[str(level)], key, value)
if level == 99:
msg_len = 80 - len(key) - 2
msg = log_level['99'] * 3 + ' ' + key + ' ' + log_level['99'] * msg_len
if self.log_level > 0:
self.log_console(msg)
if self.log_file:
msg = msg.replace('\033[0m', '').replace('\033[1m\033[9', '')[2:]
self.log_to_file(msg)
def log_console(self, data):
print(data)
def log_to_file(self, data):
try:
with open(self.log_file, 'a') as lf:
lf.write(f'{str(datetime.datetime.now())} {data} \n')
except Exception as e:
self.log_console(2, "Cannot access log file")
self.log_console(2, " Function", 'ScriptLogger - log_to_file')
self.log_console(2, ' Exception:', str(e))
sys.exit(1)
def initLogDir(self):
if not os.path.exists(self.log_dir):
try:
os.makedirs(self.log_dir)
except Exception as myError:
self.log_console(2, "Cannot create log directory")
self.log_console(2, " Function", 'ScriptLogger - initLogDir')
self.log_console(2, ' Exception:', str(myError))
sys.exit(1)
def initLogFile(self):
if not os.path.isfile(self.log_file):
with open(self.log_file, 'w'):
os.utime(self.log_file)
def backupLogs(self):
if os.path.isfile(self.log_file):
mod_time = datetime.datetime.fromtimestamp(os.path.getmtime(self.log_file)).strftime("%Y-%m-%d-%H%M%S")
shutil.move(self.log_file, self.log_file + '_' + str(mod_time))
for filename in sorted(os.listdir(self.log_dir))[:-self.num_logs]:
if filename not in self.keep_file:
os.remove(os.path.join(os.path.dirname(self.log_file), filename))
# --- Classes End
# --- Functions for the script - DO NOT CHANGE
def cliBaseParseCli():
'''
Parsing command line arguments. This is the generic function, copy / duplicate as appropriate
Parameters
----------
None
Returns
-------
cliArgs.logLevel : int
logging level, defaults to 0
'''
parser = ap.ArgumentParser()
### some examples
parser.add_argument('-t',
'--ticket',
dest='ticketId',
nargs=1,
type=str,
help='JIRA ticket number ',
required='True')
parser.add_argument('-w',
'--worklog',
dest='workLog',
nargs=1,
type=str,
help='Worklog amount - e.g. 30m, 1h, 4h, 2d, 1w',
required='True')
parser.add_argument('-c',
'--comment',
dest='workLogComment',
nargs=1,
type=str,
help='Worklog comment')
cliArgs = parser.parse_args()
return (utilBaseVerifySingleValue(cliArgs.ticketId),
utilBaseVerifySingleValue(cliArgs.workLog),
utilBaseVerifySingleValue(cliArgs.workLogComment))
def utilBaseVerifySingleValue(value):
'''
Test if value is a list.
If so, return first element, otherwise return the value as is
Parameters
----------
value : list, int, float, str
Returns
-------
value : int, float, str
'''
return value[0] if isinstance(value, list) else value
def utilBaseVerifyBoolean(value):
'''
Checks a value against a list with true values and returns True if it is in the list.
Otherwise it returns false
Parameters
----------
value : value to verify
Returns
-------
result : bool
True or False
'''
trueList = ['true', '0', 'yes']
return True if str(value).lower() in trueList else False
def utilBaseTimeStamp():
'''
Returns time in seconds since epoch
Parameters
----------
None
Returns
-------
result : float
'''
return time.time()
def getUserPass():
with open("/home/mag/jira-script/jira_token", "r") as f:
return f.read().strip()
return f.read().strip()
return f.read().strip()
def connectJira(sl, jiraServer, jiraUser, jiraPassword):
'''
Connect to JIRA and return the object. Exit with return code != 0 on error
Parameters
----------
sl : object
jiraServer : name of jira server incl. protocol - e.g. https://myjira.mydom.net
jiraUser : username to connect
jiraPassword : PAT token for authentication
Returns
-------
result : JIRA object
'''
try:
jiraOptions = {
'server': jiraServer,
'headers': {
'Authorization': f'Bearer {jiraPassword}'
}
}
jira = JIRA(options=jiraOptions)
sl.log(0, f"Connecting to JIRA {jiraServer}", "Success")
return jira
except Exception as e:
sl.log(1, "Failed to connect to JIRA", str(e))
sys.exit(ReturnCodes.rcNok)
def addWorkLogJira(sl, jc, ticketId, workLog, workLogComment):
'''
Add worklog to the JIRA ticket. Exit with return code != 0 on error
Parameters
----------
sl : object
jc : Jira object
ticketId : JIRA Ticket number
workLog : Time to log to ticket
workLogComment :
Returns
-------
jira : object
'''
try:
jc.add_worklog(ticketId, timeSpent=workLog, comment=workLogComment)
sl.log(0, f"Added worklog {workLog} to {ticketId}", "Success")
return True
except Exception as e:
sl.log(1, f"Failed to add worklog to {ticketId}", str(e))
sys.exit(ReturnCodes.rcNok)
def main():
t0 = utilBaseTimeStamp()
sl = ScriptLogger()
ticketId, workLog, workLogComment = cliBaseParseCli()
jc = connectJira(sl, JIRA_SERVER, JIRA_USER, getUserPass())
addWorkLogJira(sl, jc, ticketId, workLog, workLogComment)
sl.log(0, "Time spent (s)", utilBaseTimeStamp()-t0)
if __name__ == '__main__':
main()