added ticman, a tool to manage ipr ticket details repository
fixes [https://bugzilla.ipr.univ-rennes.fr/show_bug.cgi?id=3882]
This commit is contained in:
parent
d3985db075
commit
ca9d7e4a47
|
@ -0,0 +1,173 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
from typing import List
|
||||||
|
import logging
|
||||||
|
import argparse
|
||||||
|
import re
|
||||||
|
import os
|
||||||
|
from textwrap import dedent
|
||||||
|
from enum import Enum
|
||||||
|
from pathlib import Path
|
||||||
|
# from shutil import rmtree
|
||||||
|
from subprocess import run
|
||||||
|
from datetime import datetime
|
||||||
|
import getpass
|
||||||
|
import socket
|
||||||
|
|
||||||
|
__version__ = '1.0.0'
|
||||||
|
|
||||||
|
TicketId = str # eg bug3879
|
||||||
|
HostFqdn = str # eg store.ipr.univ-rennes.fr
|
||||||
|
UserId = str # eg 'graffy'
|
||||||
|
SshLocation = str # eg graffy@store.ipr.univ-rennes.fr:/
|
||||||
|
|
||||||
|
|
||||||
|
class UserError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Action(Enum):
|
||||||
|
CHECK_IN = 'checkin'
|
||||||
|
CHECK_OUT = 'checkout'
|
||||||
|
PUSH = 'push'
|
||||||
|
PULL = 'pull'
|
||||||
|
LIST = 'list'
|
||||||
|
|
||||||
|
|
||||||
|
class TicketDbLocation():
|
||||||
|
"""the location of the ticket details database"""
|
||||||
|
ssh_user: UserId
|
||||||
|
ssh_host_fqdn: HostFqdn
|
||||||
|
db_root_path: Path
|
||||||
|
|
||||||
|
def __init__(self, ssh_user: UserId, ssh_host_fqdn: HostFqdn, db_root_path: Path):
|
||||||
|
self.ssh_user = ssh_user
|
||||||
|
self.ssh_host_fqdn = ssh_host_fqdn
|
||||||
|
self.db_root_path = db_root_path
|
||||||
|
|
||||||
|
def as_ssh_location(self) -> SshLocation:
|
||||||
|
return f'{self.ssh_user}@{self.ssh_host_fqdn}:{self.db_root_path}'
|
||||||
|
|
||||||
|
def as_ssh_target(self) -> str:
|
||||||
|
return f'{self.ssh_user}@{self.ssh_host_fqdn}'
|
||||||
|
|
||||||
|
|
||||||
|
def delete_tree(root_dir: Path):
|
||||||
|
_completed_process = run(['trash', str(root_dir)], check=True) # noqa: F841
|
||||||
|
# rmtree(root_dir)
|
||||||
|
|
||||||
|
|
||||||
|
class TicketManager():
|
||||||
|
db_location: TicketDbLocation
|
||||||
|
tickets_local_path: Path
|
||||||
|
|
||||||
|
def __init__(self, db_location: TicketDbLocation, tickets_local_path: Path):
|
||||||
|
self.db_location = db_location
|
||||||
|
self.tickets_local_path = tickets_local_path
|
||||||
|
|
||||||
|
def get_repos_tickets_ids(self) -> List[TicketId]:
|
||||||
|
_completed_process = run(['ssh', self.db_location.as_ssh_target(), f'ls -d {self.db_location.db_root_path}/*/'], check=True, capture_output=True) # noqa: F841
|
||||||
|
dirs = re.sub('\n$', '', _completed_process.stdout.decode('utf8')).split('\n')
|
||||||
|
ticket_ids = [TicketId(Path(re.sub('/$', '', dir)).name) for dir in dirs]
|
||||||
|
return ticket_ids
|
||||||
|
|
||||||
|
def list_repos_tickets(self):
|
||||||
|
repos_ticket_ids = self.get_repos_tickets_ids()
|
||||||
|
for ticket_id in repos_ticket_ids:
|
||||||
|
print(ticket_id)
|
||||||
|
|
||||||
|
def push(self, ticket_id: TicketId):
|
||||||
|
_completed_process = run(['rsync', '-va', self.get_ticket_local_path(ticket_id), self.db_location.as_ssh_location()], check=True, capture_output=True) # noqa: F841
|
||||||
|
|
||||||
|
def checkin(self, ticket_id: TicketId):
|
||||||
|
ticket_local_dir = self.get_ticket_local_path(ticket_id)
|
||||||
|
if not ticket_local_dir.exists():
|
||||||
|
raise UserError(f'impossible to to check-in {ticket_local_dir} as this directory doesn\'t exist')
|
||||||
|
# append the checkin time to a history file
|
||||||
|
time_stamp_file_path = ticket_local_dir / 'ticman-history.tsv'
|
||||||
|
if not time_stamp_file_path.exists():
|
||||||
|
with open(time_stamp_file_path, 'wt', encoding='utf8') as time_stamp_file:
|
||||||
|
time_stamp_file.write('#date\tcheckin-origin\n')
|
||||||
|
with open(time_stamp_file_path, 'at', encoding='utf8') as time_stamp_file:
|
||||||
|
checkin_origin = f'{getpass.getuser()}@{socket.getfqdn()}:{ticket_local_dir}'
|
||||||
|
time_stamp_file.write(f'{datetime.now().astimezone().isoformat()}\t{checkin_origin}\n')
|
||||||
|
self.push(ticket_id)
|
||||||
|
delete_tree(ticket_local_dir)
|
||||||
|
print(f'ticket {ticket_id} successfully checked in to {self.db_location.as_ssh_location()}, and {ticket_local_dir} has been deleted')
|
||||||
|
|
||||||
|
def checkout(self, ticket_id: TicketId):
|
||||||
|
repos_ticket_ids = self.get_repos_tickets_ids()
|
||||||
|
if ticket_id in repos_ticket_ids:
|
||||||
|
ticket_local_dir = self.get_ticket_local_path(ticket_id)
|
||||||
|
if ticket_local_dir.exists():
|
||||||
|
raise UserError(f'impossible to to checkout {ticket_local_dir} as it has already been checked out ({ticket_local_dir} already exists). Please make sure you delete this directory (possibly backing it up first) before checking it out.')
|
||||||
|
os.makedirs(ticket_local_dir, exist_ok=True)
|
||||||
|
_completed_process = run(['rsync', '-va', f'{self.db_location.as_ssh_location()}/{ticket_id}/', f'{ticket_local_dir}/'], check=True, capture_output=True) # noqa: F841
|
||||||
|
print(f'ticket {ticket_id} successfully checked out to {ticket_local_dir}')
|
||||||
|
else:
|
||||||
|
raise UserError(f'impossible to checkout ticket {ticket_id} as it doesn\'t exist in the ticket details repository')
|
||||||
|
|
||||||
|
def get_ticket_local_path(self, ticket_id: TicketId) -> Path:
|
||||||
|
return self.tickets_local_path / ticket_id
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
logging.basicConfig(level=logging.DEBUG) # , format='%(asctime)s - %(levelname)s - %(message)s')
|
||||||
|
description = dedent('''\
|
||||||
|
ipr ticket manager - a tool to help centralizing ipr tickets details (additional files that are not appropriate as attachment to ticketing system)
|
||||||
|
|
||||||
|
The repository of tickets details is stored on a shared disk, and the user can checkout a ticket detail when he wants to get a local copy in a workspace located on his workstation. Once the user has finished working on a ticket, he can checkin this ticket to the repository to save space on his workstation.
|
||||||
|
|
||||||
|
Ideally we would use a git repository as a ticket detail repository but the data would be too bog for a git repository, so ticman uses a simple file hierarchy.
|
||||||
|
|
||||||
|
''')
|
||||||
|
# parser = argparse.ArgumentParser(description='ipr ticket manager - a tool to help centralizing ipr tickets details (additional files that are not appropriate as attachment to ticketing system)', formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||||
|
parser = argparse.ArgumentParser(description=description, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||||
|
parser.add_argument('--workspace-dir', type=Path, default=Path('/home/graffy/work/tickets'), help='the local directory used as a workspace')
|
||||||
|
parser.add_argument('--repos-location', type=SshLocation, default='graffy@store.ipr.univ-rennes.fr:/mnt/store.ipr/InstallProgs/ipr/tickets', help='the tickets details repository location')
|
||||||
|
|
||||||
|
subparsers = parser.add_subparsers(dest='cmd', required=True, description='action to perform')
|
||||||
|
# parser.add_argument('--ticket-id', type=str, required=True, help='the identifier of the ticket on which the action is performed (eg bug3879)')
|
||||||
|
|
||||||
|
parser.add_argument('--version', action='version', help=f'shows {parser.prog}\'s version', version=f'{parser.prog} version {__version__}')
|
||||||
|
|
||||||
|
# parser.add_argument('action', type=Action, help=f'synchronizes the global tickets detail database from the local workspace')
|
||||||
|
epilog = dedent(f'''\
|
||||||
|
examples:
|
||||||
|
{parser.prog} checkout bug3879
|
||||||
|
|
||||||
|
''')
|
||||||
|
parser.epilog = epilog
|
||||||
|
|
||||||
|
_list_parser = subparsers.add_parser('list', description='lists the tickets in the tickets detail repository') # noqa: F841
|
||||||
|
|
||||||
|
_checkout_parser = subparsers.add_parser('checkout', description='create a local copy of the given ticket from the database') # noqa: F841
|
||||||
|
_checkout_parser.add_argument('ticketid', type=TicketId, help='the identifier of the ticket on which the action is performed (eg bug3879)')
|
||||||
|
|
||||||
|
_checkin_parser = subparsers.add_parser('checkin', description='updates the global tickets detail database from the local workspace') # noqa: F841
|
||||||
|
_checkin_parser.add_argument('ticketid', type=TicketId, help='the identifier of the ticket on which the action is performed (eg bug3879)')
|
||||||
|
|
||||||
|
_pull_parser = subparsers.add_parser('pull', description='updates the global tickets detail database from the local workspace ticket') # noqa: F841
|
||||||
|
|
||||||
|
_push_parser = subparsers.add_parser('push', description='updates the local workspace from the global tickets detail database') # noqa: F841
|
||||||
|
_push_parser.add_argument('ticketid', type=TicketId, help='the identifier of the ticket on which the action is performed (eg bug3879)')
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
db_location = TicketDbLocation(ssh_user='graffy', ssh_host_fqdn='store.ipr.univ-rennes.fr', db_root_path=Path('/mnt/store.ipr/InstallProgs/ipr/tickets'))
|
||||||
|
tic_man = TicketManager(db_location, tickets_local_path=args.workspace_dir)
|
||||||
|
|
||||||
|
if args.cmd == 'list':
|
||||||
|
tic_man.list_repos_tickets()
|
||||||
|
if args.cmd == 'checkin':
|
||||||
|
tic_man.checkin(args.ticketid)
|
||||||
|
elif args.cmd == 'checkout':
|
||||||
|
tic_man.checkout(args.ticketid)
|
||||||
|
elif args.cmd == 'push':
|
||||||
|
tic_man.push(args.ticketid)
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
main()
|
||||||
|
except UserError as err:
|
||||||
|
print(err)
|
||||||
|
exit(1)
|
Loading…
Reference in New Issue