[MySQL, Python, Service Desk] Перенос данных из VisionFlow в ServiceNow
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Одни из самых интересных задач в работе администратора приложений, на мой взгляд, это осуществление миграции данных при переходе на новую систему. Сегодня я хочу поделится собственным опытом переноса данных с не очень известной helpdesk системы VisionFlow в более известную систему ServiceNow.Что хотел заказчик
- Перенести все данные из VisionFlow в ServiceNow с сохранением даты регистрации / закрытия тикетов
- Перенести всю историю переписки по каждому тикету (достаточно было объединить все комментарии в один тред, но мы пошли чуть дальше)
- Перенести все прикреплённые к тикетам файлы
Что мы имели
- Серверную версию Helpdesk системы VisionFlow развёрнутую на виртуальной линукс машине с БД MySQL для хранения данных.
- ServiceNow инстанс, с подготовленной заранее таблицей для заказчика.На данном этапе были обговорены все нюансы, такие как:
- Статусная модель
- Требуемые поля
- Логика автоматического назначения тикетов на исполнителя
- Данные требующие переноса
Перенос данныхServiceNow позволяет использовать excel файлы в качестве ресурса для импорта данных. Не буду подробно расписывать процесс импорта данных в систему (процесс неплохо описан в документации к продукту), но в общих чертах он выглядит так:Импорт данныхTransform map позволяет нам задать ключевое поле, по которому система будет понимать, что запись с данными параметрами уже присутствует в таблице и требуется только обновление полейТак как было решено использовать xlsx файл в качестве источника данных, файл требовалось подготовить. Требуемые данные VisionFlow хранит в разных таблицах своей базы данных. Для получения данных был написан запрос к БД:Запрос данных по тикетам в VisionFlow
SELECT
projectissue.projectIssueId,
projectissue.ticketId as 'Number',
reporter.email as 'Reporter',
projectissue.name as 'Short Description',
projectissue.Description as 'Description',
projectissue.companycustomfield15 as 'Product',
projectissue.companycustomfield13 as 'Document',
issuestatus.name as 'Status',
assignee.name as 'Assignee',
ADDTIME(projectissue.CreateDate, '-01:00') as 'Created',
ADDTIME(projectissue.completionDate, '-01:00') as 'Closed',
issuehistory.EventText as 'Comment',
author.name as 'commentAuthor'
FROM
projectissue
INNER JOIN issuestatus
ON projectissue.IssueStatusId = issuestatus.IssueStatusId
INNER JOIN systemuser assignee
ON projectissue.ResponsibleSystemUserId = assignee.SystemUserId
INNER JOIN systemuser reporter
ON projectissue.CreatedBySystemUserId = reporter.SystemUserId
INNER JOIN issuehistory ON
issuehistory.ProjectIssueId = projectissue.ProjectIssueId
INNER JOIN systemuser author
ON issuehistory.SystemUserId = author.SystemUserId
WHERE
projectissue.ProjectId = 54 AND (issuehistory.IssueEventTypeId = 5 OR issuehistory.IssueEventTypeId = 10 OR issuehistory.IssueEventTypeId = 2)
#projectissue.ProjectId = 54
ORDER BY projectissue.TicketId ASC, issuehistory.EventDate ASC
Выполнение запроса позволило нам получить данные по всем тикетам из определённого проекта, включая историю комментариев по каждому отдельному элементу, с сохранением авторства и даты добавления. Вторым шагом данные были выгружены в JSON и залиты в Excel документ. После загрузки документа в ServiceNow в качестве Data Source была проведена обработка записей и создание / обновление тикетов в системе. Результат: В системе ServiceNow зарегистрированы все записи из VisionFlow, включая дату открытия и закрытия тикетов, комментариев (и их авторов) с соблюдением исходного порядка и всех ключевых полей. Т.к. таблица на момент переноса была пуста, проблем с подменой даты создания тикетов не возникло (ничего такого, чтобы могло повлиять на нумерацию).Перенос вложенийЕсли первая часть миграции (требующая переноса исключительно текстовой информации) не потребовала больших усилий, то над переносом вложений пришлось попотеть. Сначала требовалось выяснить, а как VisionFlow, собственно, эти аттачи хранит.Выполняем запрос к БД для получения всех данных по вложениям переносимого проекта:Запрос к БД VisionFlow
SELECT
document.documentId,
document.name,
document.FullPath,
SUBSTRING_INDEX(SUBSTRING_INDEX(document.FullPath, '/', -2), '/', 1) as 'projectIssueId',
projectissue.ticketId as 'Number'
FROM
visionflow.document
INNER JOIN projectissue
ON projectissue.ProjectIssueId = SUBSTRING_INDEX(SUBSTRING_INDEX(document.FullPath, '/', -2), '/', 1)
WHERE
document.FullPath like '%/54/issuedocuments/%'
ORDER BY projectissueid
Данный запрос позволил нам получить информацию о том, как и где VisionFlow хранит вложения. К нашему счастью, оказалось, что VF создаёт отдельную папку для каждого проекта, в которой создаёт набор папок для тикетов, в которых вложения присутствуют. Папки имеют в качестве названия issueId, позволяющее однозначно идентифицировать принадлежность к тикету. Собственно, запрос выше позволят нам получить наименование папки, в которой лежит вложение и TicketId (его мы использовали для переноса данных в ServiceNow).Выгрузив архив с вложениями, встал вопрос о сопоставлении файлов с тикетами в ServiceNow и их проливке в систему. Т.к. я недавно начал изучать Python, я подумал, что решение данной задачи будет неплохой практикой в языке. Для добавления вложений в ServiceNow было решено использовать API attachments. Для этого на стороне SN был создан endpoint для получения временного токена с доступами к нужной таблице. ServiceNow предоставляет code samples для их API. По документации мы видим, что нам потребуются следующие параметры для нашего запроса:
file_name (Required) - имя добавляемого файлаtable_name (Required) - имя таблицы, в которой запись хранится table_sys_id (Required) - ID записи, в которую требуется добавить вложение Content-Type (Header) - mime type передаваемого контента
Как мы видим, вложение имеет связку с sys_id записи, к которой он принадлежит ( как и в VisionFlow). Следовательно, нам достаточно переименовать папки, которые мы загрузили из VisionFlow в sys_id записей, к которым мы будем их крепить. Для этого был выгружен список sys_id + ticketId из ServiceNow + список issueId + ticketId из VisionFlow. С помощью VLOOKUP функции Excel списки были сопоставлены и создан новый список с полями:
- old_folder_name
- ticket_id
- new_folder_name
На Python был написан скрипт для переименования папок и удаления тех, в которых не было найдено файлов (прогрессбар в данном случае был добавлен только для тренировки): Переименование папок
import pandas as pd, os
from tqdm import tqdm
def renameFolders():
df = pd.read_csv('/Downloads/folder_rename.csv')
pbar = tqdm(total=len(df))
for _ , row in df.iterrows():
old_name = row['old_folder_name']
new_name = row['new_folder_name']
try:
os.rename(f'/Downloads/home/tomcat/vflowdocs/54/issuedocuments/{old_name}', f'/Downloads/home/tomcat/vflowdocs/54/issuedocuments/{new_name}')
pbar.update(1)
except:
pbar.update(1)
def removeEmptyFolders():
folder_list = os.listdir('/Downloads/home/tomcat/vflowdocs/54/issuedocuments/')
for folder in folder_list:
path = f'/Downloads/home/tomcat/vflowdocs/54/issuedocuments/{folder}'
try:
os.rmdir(path)
except:
if len(os.path.basename(path)) < 6 and os.path.basename(path) != 'nan':
print(f'ServiceNow SysId not found for item: {os.path.basename(path)}')
renameFolders()
removeEmptyFolders()
Не буду расписывать скрипт для заливки вложений, опишу лишь несколько моментов:
- В скрипт добавлена проверка размера вложений, для того, чтобы отсеять всё то, что имеет вес менее 3000 kb (различные иконки, картинки из подписей и другой мусор) def getSize()
- Добавлен метод для удаления дубликатов аттачей. В VisionFlow каждое повторно пересылаемое вложение создавало новый файл документа def removeDuplicates()
- Добавлена обработка файлов с mime типом None. По какой-то причине mimetypes не возвращает типы для формата *msg, *txt, *eml
- Реализован финальный лог по операциями на основе ответов от сервера
- Ну и последнее (но мне, как любителю всё смотреть визуально, не менее важное) - прогрессбар для отслеживания процесса загрузки
Финальный скрипт
import os, glob, filetype, requests, mimetypes
from tqdm import tqdm
import pandas as pd
def number_of_files():
files_number = 0
folder_list = os.listdir('/Downloads/home/tomcat/vflowdocs/54/issuedocuments/')
for folder in folder_list:
files_number += len(os.listdir(f'/Downloads/home/tomcat/vflowdocs/54/issuedocuments/{folder}/'))
return files_number
#Progress Bar
pbar = tqdm(total=1297)
log_messages_status = []
log_messages_filepath = []
log_messages_filename = []
log_messages_target = []
def uploadAllFiles(folder_name):
#Variables
entire_list = glob.glob(f'/Downloads/home/tomcat/vflowdocs/54/issuedocuments/{folder_name}/*')
my_list_updated = []
#Get Files Size
def getSize(fileobject):
fileobject.seek(0,2)
size = fileobject.tell()
return size
#Upload Files
def uploadFunc(filename, sys_id, path_to_file, content_type):
url = f'https://instance.service-now.com/api/now/attachment/file?file_name={filename}&table_name=table_name&table_sys_id={sys_id}'
payload=open(path_to_file, 'rb').read()
headers = {
'Accept': 'application/json',
'Authorization': 'Bearer ',
'Content-Type': content_type,
}
response = requests.request("POST", url, headers=headers, data=payload)
if response.status_code == 201:
#print(f'Success: {filename} was uploaded to the incident with sys_id {sys_id}')
pbar.update(1)
log_messages_status.append('Success')
log_messages_filename.append(filename)
log_messages_filepath.append(path_to_file)
log_messages_target.append(sys_id)
else:
pbar.update(1)
#print(f'Error: {filename} was not uploaded to the incident with sys_id {sys_id}')
log_messages_status.append('Error')
log_messages_filename.append(filename)
log_messages_filepath.append(path_to_file)
log_messages_target.append(sys_id)
#Remove Duplicates
def removeDuplicatesByName(list_of_elements):
list_of_elements.sort()
if len(list_of_elements) > 1:
for item in list_of_elements:
item_to_compare = item.split('.')[0]
for element in list_of_elements:
if item_to_compare in element:
entire_list.remove(element)
else:
pass
return list_of_elements
else:
return list_of_elements
my_list = removeDuplicatesByName(entire_list)
for item in my_list:
file_size = open(item, 'rb')
if getSize(file_size) > 3000:
my_list_updated.append(item)
else:
pass
for attach in my_list_updated:
kind = filetype.guess_mime(attach)
if kind != None:
uploadFunc(os.path.basename(attach), os.path.dirname(attach).split('/')[-1], attach, kind)
elif kind == None and attach.split('.')[-1] == 'txt':
uploadFunc(os.path.basename(attach), os.path.dirname(attach).split('/')[-1], attach, 'text/plain')
else:
uploadFunc(os.path.basename(attach), os.path.dirname(attach).split('/')[-1], attach, 'application/octet-stream')
def getFolders():
folder_list = os.listdir('/Downloads/home/tomcat/vflowdocs/54/issuedocuments/')
for folder in folder_list:
if folder != '.DS_Store':
uploadAllFiles(folder)
getFolders()
data_to_write = pd.DataFrame({
'status': log_messages_status,
'file_name' : log_messages_filename,
'file_path' : log_messages_filepath,
'target' : log_messages_target
})
data_to_write.to_csv('/Downloads/results_log.csv')
ЗаключениеУ нас было 2 пакетика….©. У нас было 6000 тысяч записей к переносу (не так много, старая система работала не долго), 2000 вложений и немного времени. Процесс подготовки занял у меня около 14 часов (изучение, попытки и т.д.) неспешной работы, а процесс переноса занимает около 30 минут суммарно. Конечно, можно было бы многое улучшить, полностью автоматизировать процесс (начиная с выгрузки данных, заканчивая их проливкой), но, к сожалению, данная задача одноразовая. Было интересно попробовать Python для реализации проекта, и могу сказать, что с такой задачей он помог справится на ура.В конечном счёте, основная задача переезда - сделать это максимально незаметно для заказчика, что и было сделано с моей стороны.
===========
Источник:
habr.com
===========
Похожие новости:
- [Python, HTML, Big Data, Визуализация данных, Веб-аналитика] Аналитика алкогольной продукции сети магазинов «Лента»
- [Nginx, Машинное обучение, DevOps, Микросервисы, Flask] Развертывание ML модели в Docker с использованием Flask (REST API) + масштабирование нагрузки через Nginx балансер
- [Python, Разработка робототехники, Разработка на Raspberry Pi, Робототехника] Сделать робота на raspberry pi, обновленный pi-tank. Часть 1. Железо
- [Python, GitHub, Flask] Делаем телеграм бота за 5 минут: быстрый старт с продвинутым шаблоном
- [Информационная безопасность, Криптография, Python, C++, ООП] Поддержка токенов PKCS#11 с ГОСТ-криптографией в Python. Часть II — Обёртка PyKCS11
- [Service Desk, Управление проектами] ITSM: актуальные вызовы и тренды ближайших лет
- [Python, Голосовые интерфейсы] Голосовой ассистент Виталий (школьный проект)
- [Open source, Программирование, Геоинформационные сервисы, Визуализация данных, Научно-популярное] Google Earth Engine (GEE) как общедоступный каталог больших геоданных
- [Python, Машинное обучение, Искусственный интеллект, Natural Language Processing] LIT – Инспектор для вашего NLP. Обзор, установка, тест
- [Python, Программирование, Игры и игровые приставки] Создание арт-объектов, игр и много чего ещё с использованием числа Пи и Python (перевод)
Теги для поиска: #_mysql, #_python, #_service_desk, #_perenos (перенос), #_servicenow, #_python, #_mysql, #_python, #_service_desk
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 14:41
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Одни из самых интересных задач в работе администратора приложений, на мой взгляд, это осуществление миграции данных при переходе на новую систему. Сегодня я хочу поделится собственным опытом переноса данных с не очень известной helpdesk системы VisionFlow в более известную систему ServiceNow.Что хотел заказчик
SELECT
projectissue.projectIssueId, projectissue.ticketId as 'Number', reporter.email as 'Reporter', projectissue.name as 'Short Description', projectissue.Description as 'Description', projectissue.companycustomfield15 as 'Product', projectissue.companycustomfield13 as 'Document', issuestatus.name as 'Status', assignee.name as 'Assignee', ADDTIME(projectissue.CreateDate, '-01:00') as 'Created', ADDTIME(projectissue.completionDate, '-01:00') as 'Closed', issuehistory.EventText as 'Comment', author.name as 'commentAuthor' FROM projectissue INNER JOIN issuestatus ON projectissue.IssueStatusId = issuestatus.IssueStatusId INNER JOIN systemuser assignee ON projectissue.ResponsibleSystemUserId = assignee.SystemUserId INNER JOIN systemuser reporter ON projectissue.CreatedBySystemUserId = reporter.SystemUserId INNER JOIN issuehistory ON issuehistory.ProjectIssueId = projectissue.ProjectIssueId INNER JOIN systemuser author ON issuehistory.SystemUserId = author.SystemUserId WHERE projectissue.ProjectId = 54 AND (issuehistory.IssueEventTypeId = 5 OR issuehistory.IssueEventTypeId = 10 OR issuehistory.IssueEventTypeId = 2) #projectissue.ProjectId = 54 ORDER BY projectissue.TicketId ASC, issuehistory.EventDate ASC SELECT
document.documentId, document.name, document.FullPath, SUBSTRING_INDEX(SUBSTRING_INDEX(document.FullPath, '/', -2), '/', 1) as 'projectIssueId', projectissue.ticketId as 'Number' FROM visionflow.document INNER JOIN projectissue ON projectissue.ProjectIssueId = SUBSTRING_INDEX(SUBSTRING_INDEX(document.FullPath, '/', -2), '/', 1) WHERE document.FullPath like '%/54/issuedocuments/%' ORDER BY projectissueid file_name (Required) - имя добавляемого файлаtable_name (Required) - имя таблицы, в которой запись хранится table_sys_id (Required) - ID записи, в которую требуется добавить вложение Content-Type (Header) - mime type передаваемого контента
import pandas as pd, os
from tqdm import tqdm def renameFolders(): df = pd.read_csv('/Downloads/folder_rename.csv') pbar = tqdm(total=len(df)) for _ , row in df.iterrows(): old_name = row['old_folder_name'] new_name = row['new_folder_name'] try: os.rename(f'/Downloads/home/tomcat/vflowdocs/54/issuedocuments/{old_name}', f'/Downloads/home/tomcat/vflowdocs/54/issuedocuments/{new_name}') pbar.update(1) except: pbar.update(1) def removeEmptyFolders(): folder_list = os.listdir('/Downloads/home/tomcat/vflowdocs/54/issuedocuments/') for folder in folder_list: path = f'/Downloads/home/tomcat/vflowdocs/54/issuedocuments/{folder}' try: os.rmdir(path) except: if len(os.path.basename(path)) < 6 and os.path.basename(path) != 'nan': print(f'ServiceNow SysId not found for item: {os.path.basename(path)}') renameFolders() removeEmptyFolders()
import os, glob, filetype, requests, mimetypes
from tqdm import tqdm import pandas as pd def number_of_files(): files_number = 0 folder_list = os.listdir('/Downloads/home/tomcat/vflowdocs/54/issuedocuments/') for folder in folder_list: files_number += len(os.listdir(f'/Downloads/home/tomcat/vflowdocs/54/issuedocuments/{folder}/')) return files_number #Progress Bar pbar = tqdm(total=1297) log_messages_status = [] log_messages_filepath = [] log_messages_filename = [] log_messages_target = [] def uploadAllFiles(folder_name): #Variables entire_list = glob.glob(f'/Downloads/home/tomcat/vflowdocs/54/issuedocuments/{folder_name}/*') my_list_updated = [] #Get Files Size def getSize(fileobject): fileobject.seek(0,2) size = fileobject.tell() return size #Upload Files def uploadFunc(filename, sys_id, path_to_file, content_type): url = f'https://instance.service-now.com/api/now/attachment/file?file_name={filename}&table_name=table_name&table_sys_id={sys_id}' payload=open(path_to_file, 'rb').read() headers = { 'Accept': 'application/json', 'Authorization': 'Bearer ', 'Content-Type': content_type, } response = requests.request("POST", url, headers=headers, data=payload) if response.status_code == 201: #print(f'Success: {filename} was uploaded to the incident with sys_id {sys_id}') pbar.update(1) log_messages_status.append('Success') log_messages_filename.append(filename) log_messages_filepath.append(path_to_file) log_messages_target.append(sys_id) else: pbar.update(1) #print(f'Error: {filename} was not uploaded to the incident with sys_id {sys_id}') log_messages_status.append('Error') log_messages_filename.append(filename) log_messages_filepath.append(path_to_file) log_messages_target.append(sys_id) #Remove Duplicates def removeDuplicatesByName(list_of_elements): list_of_elements.sort() if len(list_of_elements) > 1: for item in list_of_elements: item_to_compare = item.split('.')[0] for element in list_of_elements: if item_to_compare in element: entire_list.remove(element) else: pass return list_of_elements else: return list_of_elements my_list = removeDuplicatesByName(entire_list) for item in my_list: file_size = open(item, 'rb') if getSize(file_size) > 3000: my_list_updated.append(item) else: pass for attach in my_list_updated: kind = filetype.guess_mime(attach) if kind != None: uploadFunc(os.path.basename(attach), os.path.dirname(attach).split('/')[-1], attach, kind) elif kind == None and attach.split('.')[-1] == 'txt': uploadFunc(os.path.basename(attach), os.path.dirname(attach).split('/')[-1], attach, 'text/plain') else: uploadFunc(os.path.basename(attach), os.path.dirname(attach).split('/')[-1], attach, 'application/octet-stream') def getFolders(): folder_list = os.listdir('/Downloads/home/tomcat/vflowdocs/54/issuedocuments/') for folder in folder_list: if folder != '.DS_Store': uploadAllFiles(folder) getFolders() data_to_write = pd.DataFrame({ 'status': log_messages_status, 'file_name' : log_messages_filename, 'file_path' : log_messages_filepath, 'target' : log_messages_target }) data_to_write.to_csv('/Downloads/results_log.csv') =========== Источник: habr.com =========== Похожие новости:
|
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 14:41
Часовой пояс: UTC + 5