diff --git a/.gitignore b/.gitignore index 5d74752..aa42336 100644 --- a/.gitignore +++ b/.gitignore @@ -7,10 +7,19 @@ *.cnf # test files -*.input +*.csv *.ids -output +*.input *.sample -*.csv *.tsv +*.txt +NOTES +output + +# log files *.log + +# working files +*.swp +*.tar +attachments/ diff --git a/Dockerfile b/Dockerfile index 86abb89..5a3410f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,7 +11,7 @@ COPY requirements.txt ./ COPY jira_cleanup jira_cleanup/ COPY wiki_cleanup wiki_cleanup/ RUN python -m pip install -r /srv/requirements.txt -RUN ln -s /home/.netrc /root/.netrc -RUN ln -s /home/.atlassian-tools-config.ini /root/.atlassian-tools-config.ini +#RUN ln -s /home/.netrc /root/.netrc +#RUN ln -s /home/.atlassian-tools-config.ini /root/.atlassian-tools-config.ini CMD ["bash"] diff --git a/bin/fix_app_config.sh b/bin/fix_app_config.sh index 792f88f..6712bf5 100644 --- a/bin/fix_app_config.sh +++ b/bin/fix_app_config.sh @@ -67,6 +67,7 @@ fi # See also: # https://confluence.atlassian.com/confkb/how-to-adjust-the-session-timeout-for-confluence-126910597.html # https://confluence.atlassian.com/jirakb/change-the-default-session-timeout-to-avoid-user-logout-in-jira-server-604209887.html +# TODO - Is this needed anymore after change to SSO via CiLogon? WEB_XML='' if [[ $APP_NAME == 'jira' ]] ; then WEB_XML="$APP_INSTALL_DIR"/atlassian-jira/WEB-INF/web.xml diff --git a/bin/fix_vgs.sh b/bin/fix_vgs.sh index e78e6b8..2c895a4 100755 --- a/bin/fix_vgs.sh +++ b/bin/fix_vgs.sh @@ -6,8 +6,9 @@ YES=0 NO=1 VERBOSE=$YES DEBUG=$YES +APP_NAME=jira -VGS=( vg_pgsql vg_backups vg_confluence ) +VGS=( vg_pgsql vg_backups vg_$APP_NAME ) ECHO= [[ $DEBUG -eq $YES ]] && ECHO=echo diff --git a/bin/go_log_rotate.sh b/bin/go_log_rotate.sh new file mode 100644 index 0000000..39df404 --- /dev/null +++ b/bin/go_log_rotate.sh @@ -0,0 +1,88 @@ +#!/usr/bin/bash + +# See also: https://jira.ncsa.illinois.edu/browse/SVC-24653 + + +### +# Global Variables +### +YES=0 +NO=1 +LOG_DIR=/var/log +ARCHIVE_DIR=/backups/old_logs +MAX_ARCHIVE_AGE=30 #days +DATE=$(date +%Y%m%d_%H%M%S) +VERBOSE=$YES +DEBUG=$NO +#DEBUG=$YES + + +### +# Functions +### + +die() { + echo "ERROR: ${*}" 1>&2 + exit 99 +} + + +force_rotate_logs() { + echo "${FUNCNAME[0]} ..." + local _vopts _dopts + [[ $VERBOSE -eq $YES ]] && { + set -x + _vopts+=( '-v' ) + } + [[ $DEBUG -eq $YES ]] && _dopts+=( '-d' ) + /usr/sbin/logrotate -f ${_vopts[@]} ${_dopts[@]} /etc/logrotate.conf + echo "${FUNCNAME[0]} OK" +} + + +archive_logs() { + echo "${FUNCNAME[0]} ..." + local _vopts + [[ $VERBOSE -eq $YES ]] && { + set -x + _vopts+=( '-v' ) + } + [[ $DEBUG -eq $YES ]] && _dopts+=( '-d' ) + local _tgz="${ARCHIVE_DIR}"/"${DATE}".tgz + local _tmp=$( mktemp -p "${ARCHIVE_DIR}" ) + >"${_tmp}" find "${LOG_DIR}" -type f \ + -name '*.[0-9]' \ + -o -name '*.gz' \ + -o -regextype egrep -regex '.*-[0-9]{8}' + if [[ $DEBUG -eq $YES ]] ; then + echo "DEBUG - files that would have been archived:" + cat "${_tmp}" + elif [[ -s "${_tmp}" ]] ; then + tar -zcf "${_tgz}" ${_vopts[@]} -T "${_tmp}" --remove-files + fi + rm "${_tmp}" + echo "${FUNCNAME[0]} OK" +} + + +clean_old_logs() { + echo "${FUNCNAME[0]} ..." + [[ $VERBOSE -eq $YES ]] && set -x + local _action='-delete' + [[ $DEBUG -eq $YES ]] && _action='-print' + find "${ARCHIVE_DIR}" -type f -mtime +${MAX_ARCHIVE_AGE} $_action + echo "${FUNCNAME[0]} OK" +} + + +### +# MAIN +### + +[[ $VERBOSE -eq $YES ]] && set -x + +force_rotate_logs + +archive_logs + +clean_old_logs diff --git a/bin/go_mk_atlassian_supportzip.sh b/bin/go_mk_atlassian_supportzip.sh new file mode 100644 index 0000000..89f4b5d --- /dev/null +++ b/bin/go_mk_atlassian_supportzip.sh @@ -0,0 +1,89 @@ +#!/usr/bin/bash + +# See also: https://jira.ncsa.illinois.edu/browse/SVCPLAN-5454 + + +### +# Global Variables +### +YES=0 +NO=1 +CHOME=/srv/confluence/home +CAPP=/srv/confluence/app +DATE=$(date +%Y%m%d_%H%M%S) +SUPPORTZIP=/root/${DATE}.support.zip +CPUZIP=/root/${DATE}.cpu_usage.zip +VERBOSE=$NO + + +### +# Functions +### + +die() { + echo "ERROR: ${*}" 1>&2 + exit 99 +} + + +get_confluence_pid() { + [[ $VERBOSE -eq $YES ]] && set -x + local _pid=$( systemctl show -p MainPID --value confluence ) + local _cmd=$( ps -p $_pid -o comm= ) + local _usr=$( ps -p $_pid -o user= ) + [[ "$_cmd" != "java" ]] && die "Unknown command '$_cmd' for pid '$_pid' ... expected 'java'" + [[ "$_usr" != "confluence" ]] && die "Unknown user '$_usr' for pid '$_pid' ... expected 'confluence'" + echo "$_pid" +} + + +dump_cpu_threads() { + # Get Thread dumps and CPU usage information + [[ $VERBOSE -eq $YES ]] && set -x + echo "Dump CPU Threads (this will take a few minutes) ..." + local _tempdir=$( mktemp -d ) + pushd "$_tempdir" + for i in $(seq 6); do + top -b -H -p $CONF_PID -n 1 > conf_cpu_usage.$(date +%s).txt + kill -3 $CONF_PID + sleep 10 + done + echo "... Dump CPU Threads OK" + + echo "Make CPU Threads zip ..." + zip -q $CPUZIP conf_cpu_usage.*.txt + echo "... Make CPU Threads zip OK" + + popd + rm -rf "$_tempdir" +} + + +mk_support_zip() { + [[ $VERBOSE -eq $YES ]] && set -x + echo "Make support zip (this will also take a minute) ..." + zip -q $SUPPORTZIP \ + ${CHOME}/confluence.cfg.xml \ + ${CHOME}/logs/* \ + ${CAPP}/logs/* \ + ${CAPP}/conf/server.xml \ + ${CAPP}/bin/setenv.sh + echo "... Make support zip OK" +} + +### +# MAIN +### + +[[ $VERBOSE -eq $YES ]] && set -x + +CONF_PID=$( get_confluence_pid ) + +dump_cpu_threads + +mk_support_zip + +echo +echo "Atlassian support files:" +ls -l /root/${DATE}*.zip +echo diff --git a/bin/go_upgrade_app.sh b/bin/go_upgrade_app.sh index a3c1cca..b4a1c42 100755 --- a/bin/go_upgrade_app.sh +++ b/bin/go_upgrade_app.sh @@ -22,13 +22,18 @@ ABLEMENT='' ### -run_installer() { +assert_app_installed() { [[ -d "${APP_INSTALL_DIR}"/bin ]] || die "app install dir '$APP_INSTALL_DIR/bin' not found" +} +assert_installer_exists() { [[ -z "$INSTALLER" ]] && INSTALLER=$( get_installer ) [[ -f "$INSTALLER" ]] || die "Installer file not found; '$INSTALLER' " +} + +run_installer() { /usr/bin/chmod +x "${INSTALLER}" "${INSTALLER}" } @@ -40,11 +45,14 @@ print_usage() { ${_prg} Upgrade the Atlassian app specified in config file '$CFG' SYNOPSYS - ${_prg} [OPTIONS] <--start|--finish> [installer_file.bin] -OPTIONS - -h --help Print this help and exit + ${_prg} [OPTIONS] <--start | --finish> + +USAGE --start Start an upgrade (stop services, install upgrade, etc.) --finish Finish an upgrade (re-enable services, re-enable notifications, etc.) + +OPTIONS + -h --help Print this help and exit ENDHERE } @@ -73,10 +81,12 @@ while [[ $# -gt 0 ]] && [[ $ENDWHILE -eq 0 ]] ; do shift done -INSTALLER="$1" - if [[ $ACTION == 'start' ]] ; then + INSTALLER=$( get_installer ) + assert_app_installed + assert_installer_exists + "${BIN}"/backup_config_files.sh "${BIN}"/set_services.sh $ABLEMENT @@ -99,6 +109,10 @@ elif [[ $ACTION == 'finish' ]] ; then "${BIN}"/set_services.sh $ABLEMENT + "${BIN}"/set_web_access.sh $ABLEMENT + +else + die "Missing one of '--start' | '--finish'" fi echo "Elapsed time: '$SECONDS' seconds." diff --git a/bin/mk_systemd.sh b/bin/mk_systemd.sh index ae3b9b3..0a47fa9 100755 --- a/bin/mk_systemd.sh +++ b/bin/mk_systemd.sh @@ -47,8 +47,15 @@ ENDHERE } +enable_service() { + systemctl enable ${APP_NAME}.service +} + + ### # MAIN ### mk_service_file + +enable_service diff --git a/bin/pg_run_sql.sh b/bin/pg_run_sql.sh index 4ffad2f..b8448fe 100755 --- a/bin/pg_run_sql.sh +++ b/bin/pg_run_sql.sh @@ -39,8 +39,8 @@ get_files() { prep() { CONF=~/.pgpass.cnf.${HOST} [[ -f "${CONF}" ]] || die "config file not found '${CONF}'" - rsync "${CONF}" ${HOST}:.pgpass - ssh ${HOST} chmod 600 .pgpass + rsync "${CONF}" ${HOST}:.pgpass || die 'failed to rsync pgpass file' + ssh ${HOST} chmod 600 .pgpass || die 'failed to chmod remote pgpass file' } diff --git a/bin/restore_config_files.sh b/bin/restore_config_files.sh index ba8bd3a..04b3a67 100644 --- a/bin/restore_config_files.sh +++ b/bin/restore_config_files.sh @@ -16,7 +16,7 @@ FILES=( for f in "${FILES[@]}" ; do tgt_dir=$( dirname "$f" ) reference=$( find "$tgt_dir" -type f -print -quit ) - cp "${BACKUP_DIR}/${f}" "${f}" + cp "${BACKUP_DIR}/${f}" "${f}" #BACKUP_DIR defined in config.sh chmod --reference="$reference" "$f" chown --reference="$reference" "$f" done diff --git a/bin/set_mail_transport.sh b/bin/set_mail_transport.sh new file mode 100644 index 0000000..97bd7f0 --- /dev/null +++ b/bin/set_mail_transport.sh @@ -0,0 +1,103 @@ +#!/bin/bash + +set -e + +BASE=${HOME}/atlassian-tools + +. ${BASE}/conf/config.sh +. ${BASE}/lib/utils.sh + +### +# VARS +### +MAIL_CONFIG=/etc/postfix/main.cf +TRANSPORT='' +ECHO='' +ACTION='' + + +### +# FUNCTIONS +### +ensure_transport_cf_line() { + grep -q '^default_transport = ' "${MAIL_CONFIG}" || \ + echo "default_transport = none" >> "${MAIL_CONFIG}" +} + + +set_transport() { + local _val="$1" + ensure_transport_cf_line + sed -i "s/^default_transport = .*/default_transport = $_val/" "${MAIL_CONFIG}" +} + + +assert_transport_match() { + local _val="$1" + grep -q "^default_transport = ${_val}" "${MAIL_CONFIG}" \ + && success "${param}d mail transport" \ + || err "${param}ing mail transport" +} + + +status_check() { + local _cf=$( grep '^default_transport = ' "${MAIL_CONFIG}" ) + local _live=$( postconf | grep '^default_transport = ' ) + cat << ENDHERE + Config = "$_cf" + Live = "$_live" +ENDHERE +} + + +print_help() { + cat << ENDHERE + Synopsis: ${0} ACTION + where ACTION is one of: + enable + disable + check status st ls + +ENDHERE +} + + +### +# MAIN +### +[[ $VERBOSE -eq $YES ]] && set -x + +[[ $DEBUG -eq $YES ]] && ECHO="echo" + +param=$1 +case $param in + (enable) + TRANSPORT=smtp + ACTION=set + ;; + (disable) + TRANSPORT=hold + ACTION=set + ;; + (check|status|st|ls) + ACTION=check + ;; + (-h) + print_help + exit + ;; + (*) + die "missing or unknown param '$param'" + ;; +esac + +case $ACTION in + (set) + $ECHO set_transport "$TRANSPORT" + $ECHO assert_transport_match "$TRANSPORT" + $ECHO status_check + ;; + (check) + $ECHO status_check + ;; +esac diff --git a/conf/config.sh-confluence-sample b/conf/config.sh-confluence-sample index b78851a..a833988 100644 --- a/conf/config.sh-confluence-sample +++ b/conf/config.sh-confluence-sample @@ -4,6 +4,8 @@ NO=1 ### START OF USER CONFIGURABLE SECTION # # +VERBOSE=$YES +DEBUG=$YES # These must be set or scripts will refuse to run # If no changes needed, set them the same @@ -15,8 +17,12 @@ HOSTNAME_NEW=$HOSTNAME_OLD # path to jira / confluence home & install dirs APP_NAME=confluence -APP_HOME_DIR=/srv/confluence/home -APP_INSTALL_DIR=/srv/confluence/app +APP_HOME_DIR=/srv/$APP_NAME/home +APP_INSTALL_DIR=/srv/$APP_NAME/app + +# arrays of service names +SYSTEM_SERVICES_TO_STOP=( puppet telegraf xcatpostinit1 $APP_NAME ) +PUPPET_SERVICES_TO_STOP=( telegraf ) # Jira/Confluence DB names # If no changes needed, set them the same @@ -25,13 +31,6 @@ APP_INSTALL_DIR=/srv/confluence/app DB_NAME_OLD= DB_NAME_NEW=$DB_NAME_OLD -# arrays of service names -SYSTEM_SERVICES_TO_STOP=( puppet telegraf $APP_NAME ) -PUPPET_SERVICES_TO_STOP=( telegraf ) - -VERBOSE=$YES -DEBUG=$YES - # # ### END OF USER CONFIGURABLE SECTION diff --git a/conf/config.sh-jira-sample b/conf/config.sh-jira-sample index 603de77..fa424f5 100644 --- a/conf/config.sh-jira-sample +++ b/conf/config.sh-jira-sample @@ -4,6 +4,8 @@ NO=1 ### START OF USER CONFIGURABLE SECTION # # +VERBOSE=$YES +DEBUG=$YES # These must be set or scripts will refuse to run # If no changes needed, set them the same @@ -15,24 +17,25 @@ HOSTNAME_NEW=$HOSTNAME_OLD # path to jira / confluence home & install dirs APP_NAME=jira -APP_HOME_DIR=/usr/services/jirahome -APP_INSTALL_DIR=/usr/services/jira-standalone +#APP_HOME_DIR=/usr/services/jirahome +#APP_INSTALL_DIR=/usr/services/jira-standalone +APP_HOME_DIR=/srv/${APP_NAME}/home +APP_INSTALL_DIR=/srv/${APP_NAME}/app # Jira/Confluence DB names # If no changes needed, set them the same # (if both are the same, no updates will be attempted) # TODO - can be removed once all databases are local -DB_NAME_OLD= +DB_NAME_OLD=jira5 DB_NAME_NEW=$DB_NAME_OLD +#DB_NAME_NEW=jira5test # arrays of service names # TODO - remove CRASHPLAN when no longer appropriate SYSTEM_SERVICES_TO_STOP=( puppet telegraf crashplan $APP_NAME ) +#SYSTEM_SERVICES_TO_STOP=( puppet telegraf xcatpostinit1 $APP_NAME ) PUPPET_SERVICES_TO_STOP=( telegraf ) -VERBOSE=$YES -DEBUG=$YES - # # ### END OF USER CONFIGURABLE SECTION diff --git a/jira_cleanup/IssueSecurity_MissingGroupsOrUsers.sql b/jira_cleanup/IssueSecurity_MissingGroupsOrUsers.sql new file mode 100644 index 0000000..effe3ab --- /dev/null +++ b/jira_cleanup/IssueSecurity_MissingGroupsOrUsers.sql @@ -0,0 +1,92 @@ +-- Find users and groups in issue security that don't exist in +-- ldap or jira databases +/* Find tickets that reference non-existant users/groups. */ +/* Useful when an org_* LDAP group goes out of existance but some tickets have a reference */ +/* to the group. Those references need to be cleaned up before migration. */ +/* Clean up looks like: */ +/* 1. add the group manually */ +/* 2. open each ticket and delete the reference (usually in "groups who can view" field) */ +/* 3. delete the group manually */ +SELECT + grps_and_users.cust_field + , grps_and_users.cust_value + , grps_and_users.proj_key + , grps_and_users.proj_name + , grps_and_users.issue_key +FROM ( + SELECT + cust_grps.cust_field + , cust_grps.cust_value + , p.pkey proj_key + , p.pname proj_name + , CONCAT(p.pkey,'-',ji.issuenum) issue_key + FROM + ( + SELECT + cf.cfname cust_field + , cfv.ISSUE issue + , LOWER(cfv.STRINGVALUE) cust_value + FROM customfield cf + JOIN customfieldvalue cfv ON + cf.ID = cfv.CUSTOMFIELD + WHERE cf.cfname = 'Groups who can view' + ) cust_grps + LEFT OUTER JOIN cwd_group cg ON + cust_grps.cust_value = cg.lower_group_name + JOIN jiraissue ji ON + cust_grps.issue = ji.ID AND + COALESCE(cg.group_name,'') = '' + JOIN project p ON + ji.PROJECT = p.ID + UNION + SELECT + cust_users.cust_field + , cust_users.cust_value + , p.pkey proj_key + , p.pname proj_name + , CONCAT(p.pkey,'-',ji.issuenum) AS issue_key + FROM + ( + SELECT + cf.cfname cust_field + , cfv.ISSUE issue + , LOWER(cfv.STRINGVALUE) cust_value + FROM customfield cf + JOIN customfieldvalue cfv ON + cf.ID = cfv.CUSTOMFIELD + WHERE cf.cfname = 'Users who can view' + ) cust_users + LEFT OUTER JOIN cwd_user cu ON + cust_users.cust_value = cu.lower_user_name + LEFT OUTER JOIN app_user au ON + cust_users.cust_value = LOWER(au.user_key) + JOIN jiraissue ji ON + cust_users.issue = ji.ID AND + COALESCE(cu.lower_user_name,au.lower_user_name,'') = '' + JOIN project p ON + ji.PROJECT = p.ID + ) grps_and_users +WHERE grps_and_users.proj_key IN -- projects staying in (new) Jira + ( + 'DELTA' + , 'HYDRO' + , 'IRC' + , 'NUS' + , 'CTA' + , 'PFR' + , 'MNIP' + , 'NCSACC' + , 'SECVAR' + , 'C3AID' + , 'CIL' + , 'CTSC' + , 'SECOPS' + , 'SVCPLAN' + , 'HELMAG' + , 'TGI' + , 'IDDS' + , 'CFAI' + , 'SVNA' + , 'SVNRW' + , 'IADDA' + ); diff --git a/jira_cleanup/attachment_migration_map.sql b/jira_cleanup/attachment_migration_map.sql new file mode 100644 index 0000000..18ef3bc --- /dev/null +++ b/jira_cleanup/attachment_migration_map.sql @@ -0,0 +1,17 @@ +/* \pset fieldsep '\t' */ +/* \pset recordsep '\n' */ +\pset tuples_only on + +SELECT + cfv.stringvalue AS oldIssueKey, + CONCAT('SUP-',ji.issuenum) AS issue_key +FROM jiraissue ji +JOIN project p ON + ji.project = p.id +JOIN customfield cf ON + 'oldIssueKey' = cf.cfname +JOIN customfieldvalue cfv ON + cfv.issue = ji.id AND + cf.id = cfv.customfield +WHERE p.pkey = 'SUP' +ORDER BY ji.issuenum; diff --git a/jira_cleanup/go_get_attachments.sh b/jira_cleanup/go_get_attachments.sh new file mode 100644 index 0000000..c3e65d1 --- /dev/null +++ b/jira_cleanup/go_get_attachments.sh @@ -0,0 +1,45 @@ +#!/usr/bin/bash + +set -x + +### +# VARIABLES +### +WORK_DIR=~/working/atlassian-tools/jira_cleanup +TAR_INPUT=attachments_dirlist.txt +# REMOTE_HOST=jira-test +REMOTE_HOST=jira + + +### +# FUNCTIONS +### + +push_file() { + local _fn="$1" + scp "$_fn" ${REMOTE_HOST}:. +} + + +get_remote_attachments() { + TGT_DIR="${WORK_DIR}"/attachments + rm -rf "${TGT_DIR}" + mkdir -p "${TGT_DIR}" + ssh $REMOTE_HOST "sudo tar vcf - -T /home/aloftus/$TAR_INPUT --exclude=thumbs" \ + | tar -x -C "${TGT_DIR}" --strip-components=7 -f - +} + + +cleanup() { + : pass +} + +### +# MAIN +### + +push_file ${WORK_DIR}/${TAR_INPUT} + +get_remote_attachments + +cleanup diff --git a/jira_cleanup/go_jcl_docker.sh b/jira_cleanup/go_jcl_docker.sh new file mode 100644 index 0000000..706bd02 --- /dev/null +++ b/jira_cleanup/go_jcl_docker.sh @@ -0,0 +1,15 @@ +#!/usr/bin/bash + +DEBUG=1 +REGISTRY=ghcr.io +REPO=ncsa/jiracmdline + +[[ "$DEBUG" -eq 1 ]] && set -x + +tag=production +tag=latest + +docker run -it --pull always \ +--mount type=bind,src=$HOME,dst=/home \ +--entrypoint "/bin/bash" \ +$REGISTRY/$REPO:$tag diff --git a/jira_cleanup/go_jira-cleanup.sh b/jira_cleanup/go_jira-cleanup.sh new file mode 100644 index 0000000..c3cc04d --- /dev/null +++ b/jira_cleanup/go_jira-cleanup.sh @@ -0,0 +1,28 @@ +DEBUG=1 +REGISTRY=ghcr.io +OWNER=ncsa +REPO=atlassian-tools + + +function is_windows { + rv=1 + [[ -n "$USERPROFILE" ]] && rv=0 + return $rv +} + + +[[ "$DEBUG" -eq 1 ]] && set -x + +tag=latest + +action='' +src_home="$HOME" +if is_windows ; then + action=winpty + src_home="$USERPROFILE" +fi + +$action docker run -it --pull always \ +--mount type=bind,src="${src_home}",dst=/home \ +$REGISTRY/$OWNER/$REPO:$tag + diff --git a/jira_cleanup/jcl_migrate_attachments.py b/jira_cleanup/jcl_migrate_attachments.py new file mode 100644 index 0000000..4e715bf --- /dev/null +++ b/jira_cleanup/jcl_migrate_attachments.py @@ -0,0 +1,206 @@ +import argparse +import json +import logging +import pprint +import sys +import csv +import pathlib +import textwrap +import jira + +# Add /app to import path +sys.path.append( '/app' ) + +# Local imports +import libjira + +# Setup logging +logfmt = '%(levelname)s:%(funcName)s[%(lineno)d] %(message)s' +loglvl = logging.INFO +loglvl = logging.DEBUG +logging.basicConfig( level=loglvl, format=logfmt ) +logging.getLogger( 'libjira' ).setLevel( loglvl ) + + +resources = {} #module level resources + +def get_jira( servername ): + key = f'jira_connection_{servername}' + try: + j = resources[key] + except KeyError: + j = libjira.jira_login( jira_server=f'{servername}.ncsa.illinois.edu' ) + return j + + +def get_old_jira(): + return get_jira( 'jira' ) + + +def get_jsm(): + return get_jira( 'jira-dev-m1' ) + + +def get_args( params=None ): + key = 'args' + if key not in resources: + constructor_args = { + 'formatter_class': argparse.ArgumentDefaultsHelpFormatter, + 'description': textwrap.dedent( '''\ + Create list of attachment directories for tickets provided. + Add attachments from old Jira tickets to new JSM tickets. + '''), + 'epilog': textwrap.dedent( '''\ + NETRC: + Jira login credentials should be stored in ~/.netrc. + Machine name should be hostname only. + '''), + } + parser = argparse.ArgumentParser( **constructor_args ) + # parser.add_argument( '-d', '--debug', action='store_true' ) + # parser.add_argument( '-v', '--verbose', action='store_true' ) + parser.add_argument( '--mk_paths', + dest='action', action='store_const', const='mk_paths' ) + parser.add_argument( '--migrate_attachments', + dest='action', action='store_const', const='migrate_attachments' ) + parser.add_argument( '--attachments_dir', default='./attachments', + help='Path to attachments dir' ) + args = parser.parse_args( params ) + if not args.action: + raise UserWarning( "Exacly one of --mk_paths or --migrate_attachments must be specified." ) + resources[key] = args + return resources[key] + + +def get_issuemap(): + key = 'issuemap' + if key not in resources: + infile = pathlib.Path( 'attachment_migration_map.csv' ) + with infile.open() as fh: + reader = csv.reader( fh, delimiter='|' ) + resources[key] = { row[0].strip():row[1].strip() for row in reader } + return resources[key] + + +def debug( msg ): + logging.info( msg ) + + +def info( msg ): + logging.info( msg ) + + +def error( msg ): + logging.error( msg ) + + +def dump_issue( issue ): + pprint.pprint( issue.raw ) + + +def slug_to_filepath( slug ): + base = pathlib.Path( get_args().attachments_dir ) + prj, ident = slug.split('-') + # convert ident into subdir name + scalar = int(ident) // 10000 + subdir = 10000 + (scalar * 10000) + return base / prj / str(subdir) / slug + + +def mk_paths(): + for k,v in get_issuemap().items(): + at_dir = slug_to_filepath( k ) + print( f"{at_dir}" ) + + + +def migrate_attachments(): + oldjira = get_old_jira() + newjira = get_jsm() + # Walk filesystem for issues that have attachments + # ... dir structure looks like: + # attachments_dir/TICKET-KEY/file + attachments_dir = pathlib.Path( get_args().attachments_dir ) + for root, dirs, files in attachments_dir.walk(): + for d in dirs: + # directory name will be the TICKET-KEY + old_key = str(d) + new_key = get_issuemap()[ old_key ] + try: + old_issue = oldjira.issue( old_key ) + except jira.exceptions.JIRAError as e: + if 'Issue Does Not Exist' in e.text: + error( f'source issue not found: {old_key} -> {new_key}' ) + continue + try: + new_issue = newjira.issue( new_key ) + except jira.exceptions.JIRAError as e: + if 'Issue Does Not Exist' in e.text: + error( f'target issue not found: {old_key} -> {new_key}' ) + continue + + # get filenames of any existing attachments in new issue + existing_filenames = [ a.filename for a in new_issue.fields.attachment ] + + # get attachments from old_issue; add to new_issue + for at in old_issue.fields.attachment: + if at.filename in existing_filenames: + # don't re-add attachments with same filename + info( f"SKIP attachment '{at.filename}' already exists for ticket '{new_key}'" ) + continue + local_file = root / d / at.id + if not local_file.exists(): + error( f'file not found: {local_file}' ) + continue + info( f"ADD attachment '{at.filename}' to ticket '{new_key}'" ) + newjira.add_attachment( + issue = new_issue, + attachment = str(local_file), + filename = at.filename + ) + + +if __name__ == '__main__': + + action = get_args().action + if action == 'mk_paths': + mk_paths() + elif action == 'migrate_attachments': + migrate_attachments() + + + + + +# TEST CONNECTION +# issue = oldjira.issue('SVC-5118') +# print( json.dumps( issue.raw ) ) + +# VIEW ATTACHMENT INFO +# for at in issue.fields.attachment: +# pprint.pprint( at ) +# + +# ATTACHMENT JSON +# {'author': {'active': True, +# 'avatarUrls': {'16x16': 'https://jira.ncsa.illinois.edu/secure/useravatar?size=xsmall&avatarId=10122', +# '24x24': 'https://jira.ncsa.illinois.edu/secure/useravatar?size=small&avatarId=10122', +# '32x32': 'https://jira.ncsa.illinois.edu/secure/useravatar?size=medium&avatarId=10122', +# '48x48': 'https://jira.ncsa.illinois.edu/secure/useravatar?avatarId=10122'}, +# 'displayName': 'Pallavi Jain', +# 'emailAddress': 'pjain15@illinois.edu', +# 'key': 'JIRAUSER36800', +# 'name': 'pjain15', +# 'self': 'https://jira.ncsa.illinois.edu/rest/api/2/user?username=pjain15', +# 'timeZone': 'America/Chicago'}, +# 'content': 'https://jira.ncsa.illinois.edu/secure/attachment/62174/image-2023-03-03-14-50-55-959.png', +# 'created': '2023-03-03T14:50:56.000-0600', +# 'filename': 'image-2023-03-03-14-50-55-959.png', +# 'id': '62174', +# 'mimeType': 'image/png', +# 'self': 'https://jira.ncsa.illinois.edu/rest/api/2/attachment/62174', +# 'size': 105502, +# 'thumbnail': 'https://jira.ncsa.illinois.edu/secure/thumbnail/62174/_thumb_62174.png'} diff --git a/jira_cleanup/jira_set_basic_settings.py b/jira_cleanup/jira_set_basic_settings.py index 51d1800..52e4b5d 100644 --- a/jira_cleanup/jira_set_basic_settings.py +++ b/jira_cleanup/jira_set_basic_settings.py @@ -1,4 +1,5 @@ import configparser +import csv import json import logging import netrc @@ -12,10 +13,18 @@ from functools import wraps +# from http.client import HTTPConnection +# HTTPConnection.debuglevel = 1 + logfmt = '%(levelname)s:%(funcName)s[%(lineno)d] %(message)s' loglvl = logging.INFO -loglvl = logging.DEBUG +#loglvl = logging.DEBUG logging.basicConfig( level=loglvl, format=logfmt ) + +# requests_log = logging.getLogger("urllib3") +# requests_log.setLevel(loglvl) +# requests_log.propagate = True + # logging.getLogger( 'libjira' ).setLevel( loglvl ) # logging.getLogger( 'jira.JIRA' ).setLevel( loglvl ) @@ -58,15 +67,30 @@ def get_netrc(): key = 'netrc' if key not in resources: n = netrc.netrc() + # n = netrc.netrc('/root/netrcfile') server = get_server() (login, account, password) = n.authenticators( server ) resources['login'] = login - # resources['account'] = account + resources['account'] = account resources['password'] = password resources[key] = n return resources[key] +def get_login(): + key = 'login' + if key not in resources: + get_netrc() + return resources[key] + + +def get_account(): + key = 'account' + if key not in resources: + get_netrc() + return resources[key] + + def get_password(): key = 'password' if key not in resources: @@ -110,6 +134,16 @@ def err( msg ): logging.error( msg ) +def get_role_id( name ): + key = 'roles' + if key not in resources: + path = f'role' + r = api_get( path ) + rawdata = r.json() + resources[key] = { d['name']: d['id'] for d in rawdata } + return resources[key][name] + + # https://stackoverflow.com/questions/1622943/timeit-versus-timing-decorator#27737385 def timing( f ): @wraps( f ) @@ -173,8 +207,18 @@ def post_sudo_path( path, data ): def api_go( method, path, version='latest', **kw ): url = f'https://{get_server()}/rest/api/{version}/{path}' logging.debug( f'{method} {path}, {pprint.pformat(kw)}' ) - r = get_session().request( method, url, **kw ) + s = get_session() + # to use personal access token, must disable netrc function in requests + # s.trust_env = False + # token = get_account() + s.headers = { + "Accept": "application/json", + "Content-Type": "application/json", + # "Authorization": f"Bearer {token}", + } + r = s.request( method, url, **kw ) logging.debug( f'RETURN CODE .. {r}' ) + # logging.debug( f'RETURN HEADERS .. {r.headers}' ) r.raise_for_status() return r @@ -186,21 +230,16 @@ def api_get( path ): def api_delete( path, data=None ): kwargs = { 'timeout': 1800 } if data: - kwargs.update ( { - 'headers': { "Content-Type": "application/json" }, - 'json': data, - } ) + kwargs.update ( { 'json': data } ) return api_go( 'DELETE', path, **kwargs ) def api_post( path, data): - headers = { "Content-Type": "application/json" } - return api_go( 'POST', path, json=data, headers=headers ) + return api_go( 'POST', path, json=data ) def api_put( path, data ): - headers = { "Content-Type": "application/json" } - return api_go( 'PUT', path, json=data, headers=headers ) + return api_go( 'PUT', path, json=data ) def web_delete_by_id( id, path, addl_form_data={} ): @@ -240,13 +279,85 @@ def set_general_config(): r = post_sudo_path( path, data ) +def add_application_access_groups(): + jira_groups = get_config().options( 'Application Access Jira' ) + path = 'applicationrole' + # r = api_get( path ) + data = { + 'key': 'jira-software', + 'groups': jira_groups[:2], + } + r = api_put( path, data ) + print( r.text ) + + +def get_project_roles( pid ): + r = api_get( f'project/{pid}/role' ) + data = r.json() + role_names = list( data.keys() ) + roles = {} + for role in role_names: + rid = get_role_id( role ) + role_data = get_project_role_details( pid, rid ) + # print( f"Project:{pid} Role:'{role}'" ) + # pprint.pprint( role_data ) + actors = [ f"{r['name']} ({r['displayName']})" for r in role_data['actors'] ] + roles[role] = actors + return roles + + +def get_project_role_details( pid, role_id ): + path = f'project/{pid}/role/{role_id}' + r = api_get( path ) + data = r.json() + return data + + +def project_roles_as_csv(): + r = api_get( 'project' ) + data = r.json() + project_keys = { p['key'] : p['name'] for p in data } + # projects = {} + csv_rows = [ ['Project', 'Role', 'Members'] ] + for pid,p_name in project_keys.items(): + roles = get_project_roles( pid ) + # projects[pid] = { + # 'name': p_name, + # 'roles': roles, + # } + for role, members in roles.items(): + csv_rows.append( [ pid, role] + members ) + # pprint.pprint( projects ) + output = pathlib.Path( 'perms.csv' ) + with output.open(mode='w', newline='') as f: + writer = csv.writer(f) + writer.writerows( csv_rows ) + # return projects + + + + +def test_auth(): + path = 'issue/SVCPLAN-2741' + r = api_get( path ) + # print( r.text ) + + + def run(): # starttime = time.time() + # test_auth() + set_banner() set_general_config() + # add_application_access_groups() #returns error 400 + + # project_roles_as_csv() + # get_project_roles( 'SVCPLAN', 10002 ) + # elapsed = time.time() - starttime # logging.info( f'Finished in {elapsed} seconds!' ) diff --git a/jira_cleanup/mk_attachment_migration_map.sh b/jira_cleanup/mk_attachment_migration_map.sh new file mode 100644 index 0000000..691b6d0 --- /dev/null +++ b/jira_cleanup/mk_attachment_migration_map.sh @@ -0,0 +1,9 @@ +BASE=~/working/atlassian-tools +FN_SQL=attachment_migration_map.sql +FN_OUTPUT=attachment_migration_map.csv +SERVER=jira-dev-m1 + +# run SQL +$BASE/bin/pg_run_sql.sh "$SERVER" "$FN_SQL" \ +| grep -v '^$' \ +> "$FN_OUTPUT" diff --git a/jira_cleanup/mysql_fixes.sh b/jira_cleanup/mysql_fixes.sh old mode 100755 new mode 100644 index d23d033..193de9d --- a/jira_cleanup/mysql_fixes.sh +++ b/jira_cleanup/mysql_fixes.sh @@ -12,15 +12,10 @@ die() { exit 99 } -prep() { - [[ -f "${MY_CNF}" ]] || die "File not found '${MY_CNF}'" - rsync "${MY_CNF}" ${HOST}:.my.cnf -} - run_sql() { fn="$1" - cat "$fn" | ssh $HOST 'mysql' + "${BASE}"/my_run_sql.sh "${HOST}" "$fn" } @@ -73,8 +68,6 @@ fix_bad_sprints() { ### MAIN -prep - set -x # Not needed anymore diff --git a/jira_cleanup/pg_fixes.sh b/jira_cleanup/pg_fixes.sh old mode 100755 new mode 100644 diff --git a/jira_cleanup/send_migration_file.sh b/jira_cleanup/send_migration_file.sh index c7daca8..a2b7770 100644 --- a/jira_cleanup/send_migration_file.sh +++ b/jira_cleanup/send_migration_file.sh @@ -40,7 +40,8 @@ copy_files() { echo "Copy files from $SRC_HOST to $TGT_HOST:" ssh $TGT_HOST "sudo mkdir -p $TGT_PATH" ssh $SRC_HOST "sudo tar vczf - -C ${SRC_PATH} ." | ssh $TGT_HOST "sudo tar xzf - -C $TGT_PATH" - ssh $TGT_HOST "sudo find $TGT_PATH -type f -exec chown jira:jira {} \;" + ssh $TGT_HOST "sudo chown -R jira:jira $TGT_PATH" + # ssh $TGT_HOST "sudo find $TGT_PATH -type f -exec chown jira:jira {} \;" # confirm file exists on target, exit if transfer failed echo } diff --git a/test-server-setup/wiki-make-test.sh b/test-server-setup/wiki-make-test.sh deleted file mode 100644 index 1b19d3a..0000000 --- a/test-server-setup/wiki-make-test.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash -sed -ine 's/141.142.192.52/141.142.192.248/g' /etc/sysconfig/network-scripts/ifcfg-ens192 -sed -ine 's/UUID/#UUID/g' /etc/sysconfig/network-scripts/ifcfg-ens192 -hostnamectl set-hostname --static wiki-test.ncsa.illinois.edu -systemctl disable --now confluence puppet crashplan -puppet resource service telegraf ensure=stopped enable=false -puppet agent --disable "puppet disable while testing and upgrading" -yes 'yes' | /root/crashplan-install/uninstall.sh -i /usr/local/crashplan -sed -ine 's/#CATALINA_OPTS="-Datlassian.mail.senddisabled=true -Datlassian.mail.fetchdisabled=true ${CATALINA_OPTS}"/CATALINA_OPTS="-Datlassian.mail.senddisabled=true -Datlassian.mail.fetchdisabled=true ${CATALINA_OPTS}"/g' /usr/services/confluence/bin/setenv.sh -# Change to HTTPD files in /etc/httpd/conf.d/ files needed ?!?!?! -rm -f /etc/krb5.keytab -sed -ine 's/wiki.ncsa/wiki-test.ncsa/g' /usr/services/confluence/conf/server.xml -shutdown -h now