Работа со скриптами приложений Qlik Sense с использованием Python и REST API

На связи Николаев Николай и Грищенко Константин.

В своей работе мы активно используем BI инструмент Qlik Sense (QS), в частности занимаемся мониторингом его серверов. А это значит, что за раз часто приходится анализировать состояние множества приложений (сейчас их более 1600 штук). В таких случаях спасает использование API (Application Programming Interface) QS (об этом подробнее в этом посте) и работа с БД QS напрямую. Однако БД не содержит детальную информацию, только общую по приложениям. Например, данных по скриптам загрузки (об этом пойдёт речь дальше) в ней нет. Поэтому, чтобы с ними работать, необходимо использовать REST API.

Что такое REST API? API – это набор инструментов, который позволяет одним программам взаимодействовать с другими. Representational State Transfer (REST) – архитектурный стиль взаимодействия компонентов распределённого приложения в сети, позволяющий получать и модифицировать данные и состояния удаленных приложений при помощи передачи HTTP-запросов.

В этом посте хотим рассказать об одном из вариантов использования API QS, возможно не совсем стандартном, для следующей ситуации.

QS для разграничения доступа к приложению предоставляет функционал Section Access. Section Access – это часть скрипта загрузки данных, в которую добавляется таблица безопасности, определяющая какую информацию может просматривать указанный пользователь. Qlik Sense использует эту таблицу для скрытия некоторых данных в приложении от пользователя на основе указанных прав. Выглядит этот раздел примерно так:

В предложении упомянута БД QS, дальше про нее речи больше не идет. У читателей может возникнуть вопрос, а зачем они используют API? Почему из БД информацию не получить.

Поэтому предлагаю пояснить, что в БД QS содержится не вся информация, и поэтому в тех случаях когда ее там нет, то используется API

Section Access; Load * INLINE [ ACCESS, USERID, REDUCTION USER, AD_DOMAIN\ADMIN, * USER, AD_DOMAIN\A, 1 USER, AD_DOMAIN\B, 2 USER, AD_DOMAIN\C, 3 ADMIN, INTERNAL\SA_SCHEDULER, ]; Section Application; T1: Load *, NUM AS REDUCTION; LOAD Chr(RecNo()+ord('A')-1) AS ALPHA, RecNo() AS NUM AUTOGENERATE 3;

Видно, что таблица доступов статична и создаётся разработчиком/владельцем приложения. Вот с этого и начинается проблема: сама эта таблица при изменении структуры систем безопасности/доменной структуры не поменяется. Следовательно, вообще все пользователи могут потерять доступ к приложениям, в которых был раздел Section Access. А такие изменения как раз и планировались в нашей организации.

Поэтому стояла задача поменять этот раздел во всех своих приложениях и предупредить владельцев приложений с таким разделом о необходимости корректировки. Понятно, что заходить в каждое приложение и глазами просматривать скрипт — не вариант. И готового метода в API, для того, чтобы это можно было сделать, нет. Как мы воспользовались API QS для работы со скриптами загрузки приложений QS расскажем дальше.

Как известно, у QS есть несколько видов API. Мы будем использовать два: для получения списка приложений и для работы с объектами приложений. Тот вид API, который мы использовали для манипуляций с объектами, работает по технологии websocket.

Итак, первым шагом на пути решения задачи будет подключение к серверу и аутентификация.

Аутентифицироваться на сервере QS можно двумя способами:

· по SSL сертификатам;

· при помощи NTLM.

Аутентификация NTLM отличается от сертификатов тем, что для подключения по этому протоколу необходимы только логин и пароль доменной учетной записи. Но так как логин и пароль передаются в коде в открытом виде, аутентификация при помощи NTLM может подойти не во всех случаях (например, если это не удовлетворяет требованиям кибербезопасности, принятым в организации).

Подключаться к серверу QS будем первым способом (по SSL сертификатам) — подробно о нём и о том, как экспортировать сертификаты можнопочитать в упомянутой выше статье. При подключении выполним шаги аналогичные описанным в том же посте, но с некоторыми отличиями: мы обойдемся без использования библиотеки qsAPI – вместо этого, получим ответ от сервера QS через requests.get() и сразу загрузим список приложений с их id в датафрейм pandas.

Для выполнения задачи понадобятся следующие библиотеки:

Import random import string import ssl import requests, json from requests import Request, Session import pandas as pd from pandas.io.json import json_normalize from websocket import create_connection

На всякий случай, пользовались мы такими версиями:

Python 3.9.7

requests == 2.26.0

pandas == 1.3.4

websocket_client == 1.6.4

И следует обратить внимание, что библиотека websocket и библиотека websocket_client —разные, и нам нужна именно websocket_client. При этом первую в нашем случае категорически рекомендуется не ставить, а если она уже есть – удалить.

Теперь создадим ключ xrfkey (необходимо, чтобы и в headers, и в url-адресе запроса к API он был один и тот же):

На всякий случай, пользовались мы такими версиями: Python 3.9.7 requests == 2.26.0 pandas == 1.3.4 websocket_client == 1.6.4 И следует обратить внимание, что библиотека websocket и библиотека websocket_client —разные, и нам нужна именно websocket_client. При этом первую в нашем случае категорически рекомендуется не ставить, а если она уже есть – удалить. Теперь создадим ключ xrfkey (необходимо, чтобы и в headers, и в url-адресе запроса к API он был один и тот же):

И зададим параметры для подключения к серверу: хост, имя домена в сети (userDirectory), учетная запись (УЗ) пользователя:

senseHost = 'tksds-mvp000060.delta.sbrf.ru' proxyprefix = 'sudirpy' userDirectory = 'INTERNAL' userName = 'sa_repository'

Также, при аутентификации на сервере QS потребуется задать заголовок http-запроса (в формате словаря):

Поясним немного о том, что описано выше. Заголовок включает в себя реквизиты УЗ пользователя QS (домен/УЗ), указание на формат получаемых данных (JSON), а также ключ пользовательской сессии Qlik (xrfkey).

Чтобы подключить сертификаты, нам необходимо указать путь к ним.

keyPath = r"." cert = keyPath + r"\clientandkey.pem" ssl.match_hostname = lambda cert, hostname:True

Примечание: Файл для сертификата clientandkey.pem состоит из содержимого файла client_key.pem и через строку – из содержимого файла client.pem.

Далее собираем url для подключения. Для целевого Endpoint указываем ранее сгенерированный xrfkeyи передаем url в запрос requests:

port = '4242' #для подключения через requests выбираем порт 4242, для websocket 4747 endpoint_app = 'qrs/app/full' endpoint_app_sh = 'qrs/app' url_app = 'https://{0}:{3}/{1}?xrfkey={2}'.format(senseHost, endpoint_app, xrf, port)

Примечание: каждый из API имеет свой набор конечных точек и портов, здесь показан запрос для получения списка всех приложений из QRS.

На этом настройки авторизации закончены. Посмотрим, что вернёт запрос на сервер:

response_app = requests.get(url_app, headers=headers, verify=False, cert=cert)

В ответе список приложений в JSON

resp_app = json.loads(response_app.content)
Работа со скриптами приложений Qlik Sense с использованием Python и REST API

Для каждого приложения свой отдельный словарь с его параметрами: его id, дата создания, дата изменения, id владельца и т.д.

Первый этап пройден. Список приложений загружаем в датафрейм pandas для дальнейшей обработки:

app_df = pd.DataFrame.from_dict(resp_app)

Теперь подключаемся к движку QIX Engine при помощи технологии websocket (по сертификатам). Для этого потребуется весь набор сертификатов, ранее выгруженных в QMC. Указываем место, где они сохранены и их список:

keyPath = "C:\Temp\Qlik\From_DEV_Server\Exported Certificates\cab-wsv-deldcrz" certs = ({"ca_certs": keyPath + r"\root.pem", "certfile": keyPath + r"\client.pem", "keyfile": keyPath + r"\client_key.pem", "cert_regs": ssl.CERT_REQUIRED, "server_side": False }) ssl.match_hostname = lambda cert, hostname:True

URL для подключения к Qlik Engine собирается с указанием другого порта (4747 для QIX, в отличие от 4242 для подключения к репозиторию QRS):

port = '4747' endpoint_wss_app = 'app' wss_url_app = 'wss://{0}:{3}/{1}/{2}'.format( senseHost, proxyprefix, endpoint_wss_app, port)

На следующем шаге мы как раз и будем искать приложения, в скрипте загрузки которых есть раздел Section Access.

Чтобы получить скрипты по всем приложениям, нужно пройти циклом по списку id приложений (который ранее был получен через requests) и для каждого из них получить его загрузочный скрипт. Откроем тело цикла и начнем с выделения из датафрейма приложений с нужным id:

appid, qScript, hyperlink, appid_err, script_err, text_err = [], [], [], [], [], [] script_df = pd.DataFrame() for a in range(len(app_df)): try: app_id = app_df.iloc[a]['id']

Разберём порядок работы с запросами. Сначала отправляем запрос на открытие приложения.

json_open_doc = { "jsonrpc": "2.0", "method": "OpenDoc", "handle": -1, "params": [ app_id ], "outKey": -1, "id": 1 } json_open_doc_send = json.dumps(json_open_doc)

После открытия объекта приложения, можно отправлять на сервер JSON-запросы с любыми методами API QS, которые можно применить к данному объекту.

Сейчас нам нужно получить загрузочный скрипт приложения, поэтому мы формируем JSON с указанием метода GetScript:

json_get_script = { "jsonrpc": "2.0", "handle": 1, "method": "GetScript", "params": {}, "outKey": -1, "id": 2 } json_get_script_send = json.dumps(json_get_script)

Для создания подключения будем использовать метод create_connection. Указываем собранный url, способ аутентификации (по сертификатам), заголовок запроса:

wss_conn = create_connection(wss_url_app, sslopt=certs, header=headers) wss_conn.getstatus() wss_conn.recv()

Сформировав JSON-запрос, отправляем его и получаем ответ, который содержит id открытого приложения:

wss_conn.send(json_open_doc_send) wss_conn.recv()

В ответе для запроса json_get_script_send, также в формате JSON, возвращается текст загрузочного скрипта приложения с идентификатором app_id:

wss_conn.send(json_get_script_send)
Работа со скриптами приложений Qlik Sense с использованием Python и REST API

После этого соединение websocket можно закрыть и сохранить полученный ответ в переменную:

script_a = wss_conn.recv() wss_conn.close() script_a_json = json.loads(script_a)

И закрыть цикл, обработав исключения и записав полученные тексты загрузочных скриптов в датафрейм

link = 'https://{0}/{1}/{2}'.format(senseHost, endpoint_app_sh, app_id) qScript.append(script_a_json['result']['qScript']) appid.append(app_id) hyperlink.append(link) if len(script_a_json['result']['qScript']) > 0: print( '\r\nСтрока №' + str(a) + ': ' + 'Обработано приложение id: ' + str( app_id) + ', длина строки скрипта ' + str(len(script_a_json['result']['qScript'])) +' символов.' ) else: print('Приложение ' + str(app_id) + ' не обработано, длина скрипта = 0') except Exception as error: name = app_df.iloc[a]['name'] appid_err.append(app_id) script_err.append(str(script_a_json)) text_err.append(str(error) + '&&' + link + '&&' + name) print( '\r\nСтрока №' + str(a) + ': ' + '\r\n Ошибка при обработке приложения id: ' + str(app_id) + '\r\n текст ошибки: ' + str(error) + ' ;' )

В общем весь цикл получится такой:

for a in range(len(app_df)): try: app_id = app_df.iloc[a]['id'] json_open_doc = {} json_open_doc = { "jsonrpc": "2.0", "method": "OpenDoc", "handle": -1, "params": [ app_id ], "outKey": -1, "id": 1 } json_open_doc_send = json.dumps(json_open_doc) json_get_script = {} json_get_script = { "jsonrpc": "2.0", "handle": 1, "method": "GetScript", "params": {}, "outKey": -1, "id": 2 } json_get_script_send = json.dumps(json_get_script) wss_conn = create_connection(wss_url_app, sslopt=certs, header=headers) wss_conn.getstatus() wss_conn.recv() wss_conn.send(json_open_doc_send) wss_conn.recv() wss_conn.send(json_get_script_send) script_a = wss_conn.recv() wss_conn.close() script_a_json = json.loads(script_a) link = 'https://{0}/{1}/{2}'.format(senseHost, endpoint_app_sh, app_id) qScript.append(script_a_json['result']['qScript']) appid.append(app_id) hyperlink.append(link) if len(script_a_json['result']['qScript']) > 0: print( '\r\nСтрока №' + str(a) + ': ' + 'Обработано приложение id: ' + str( app_id) + ', длина строки скрипта ' + str(len(script_a_json['result']['qScript'])) +' символов.' ) else: print('Приложение ' + str(app_id) + ' не обработано, длина скрипта = 0') except Exception as error: name = app_df.iloc[a]['name'] appid_err.append(app_id) script_err.append(str(script_a_json)) text_err.append(str(error) + '&&' + link + '&&' + name) print( '\r\nСтрока №' + str(a) + ': ' + '\r\n Ошибка при обработке приложения id: ' + str(app_id) + '\r\n текст ошибки: ' + str(error) + ' ;' )

И обработка ошибок:

error_df = pd.DataFrame([appid_err,text_err,script_err]).T cols_err = ['app_id','Error_text','qScript'] error_df.columns = cols_err if len(error_df) > 0: error_df.to_excel('error_script_text.xlsx', index=False) d = {'app_id': appid, 'qScript': qScript,'hyperlink': hyperlink} script_df = pd.DataFrame(d) print( '\r\nВсего обработано ' + str(len(script_df)) + ' приложений из ' + str(len(app_df)) + ' доступных на сервере под УЗ ' + str(userDirectory) + "\\" + str(userName)) if len(error_df)>0: print('\r\n При обработке возникло ' + str(len(error_df)) + ' ошибок, информация об ошибках сохранена в файл error_script_text.xlsx') else: print('\r\n Ошибок при обработке приложений не было!')

Работа выполняется под учетной записи sa_sheduler, так как её обычно добавляют в таблицу section access, чтобы выполнялись автоматически tasks. У приложений же без section access для этой УЗ доступ есть. Поэтому, в случае ошибок доступа по каким-либо приложениям, мы точно будем знать, что в их скрипте загрузки есть раздел section access. Поэтому такие предложения нужно будет опросить под той УЗ, у которой есть доступ к ним (например, под УЗ владельца приложения). Либо элементарно записать всех таких владельцев приложений и сделать почтовую рассылку.

Получив датафрейм со списком всех приложений на сервере и текстами их загрузочных скриптов, отфильтруем его – оставим только те записи, по которым в тексте скрипта (поле qScript в примере) присутствует текст ‘Section Access;’:

section_access_df = script_df[script_df['qScript'].str.lower().str.contains('section access') == True]

Каждый элемент отфильтрованного датафрейма содержит в себе id, скрипт и гиперссылку приложения с section access. Например:

Работа со скриптами приложений Qlik Sense с использованием Python и REST API

Чтобы получить более общую информацию о каждом приложении, мы можем добавить дополнительную информацию о приложениях и их владельцах:

app_df_new = json_normalize(app_df.to_dict(orient='records'))

У каждого из 189 доступных приложений, помимо их id, скриптов и гиперссылок, теперь есть дополнительная информация:

Работа со скриптами приложений Qlik Sense с использованием Python и REST API

Датафрейм script_df объединяем с полученным ранее списком приложений (app_df), удаляя ненужные поля:

section_access_df_new = section_access_df.merge(app_df_new, how='left', left_on='app_id', right_on='id') columns_to_drop = ['id', 'customProperties', 'modifiedByUserName', 'appId','sourceAppId', 'targetAppId', 'tags', 'description', 'fileSize', 'thumbnail', 'savedInProductVersion', 'migrationHash', 'dynamicColor', 'availabilityStatus', 'privileges', 'schemaPath', 'owner.userDirectoryConnectorName', 'owner.privileges', 'stream.privileges' #,'qScript' #Если не нужно поле с текстом скрипта, можно раскомментировать эту строку ] section_access_df_new = section_access_df_new.drop(columns_to_drop, axis=1) cols = [ 'app_id', 'name', 'createdDate', 'modifiedDate', 'lastReloadTime', 'published', 'publishTime', 'stream.id', 'stream.name', 'stream', 'owner.id', 'owner.userId', 'owner.userDirectory', 'owner.name', 'hyperlink', 'qScript' #Если выше удалили поле с текстом скрипта, эту строку закомментировать ] section_access_df_new = section_access_df_new[cols]

Оставим гиперссылку только для опубликованных приложений :

section_access_df_new.loc[(section_access_df_new.published == False), 'hyperlink'] = ''

У опубликованных приложений гиперссылка останется:

Работа со скриптами приложений Qlik Sense с использованием Python и REST API

У неопубликованных сотрется:

Работа со скриптами приложений Qlik Sense с использованием Python и REST API

В результате полученные данные (включая детальную информацию по приложению и его владельцу) можно сохранить в CSV или Excel формате:

section_access_df_new.to_excel( 'Apps with section Access in Load Scripts.xlsx', index=False )
Работа со скриптами приложений Qlik Sense с использованием Python и REST API

Изменение скрипта приложения

После получения скриптов можем сделать любое преобразование их текстов, например, при помощи string.replace() и загрузить их обратно в Qlik Sense аналогично тому, как мы выгрузили их до этого.

Для загрузки скрипта непосредственно в приложение в JSON-запросе вместо метода GetScript

json_get_script = { "jsonrpc":"2.0", "handle":1, "method":"GetScript", "params":{}, "outKey":-1, "id": 2,

нужно проставить метод SetScript и отправить следующий запрос:

json_set_script = { "jsonrpc":"2.0", "handle":1, "method":"SetScript", "params":["string_with_script"], "outKey":-1, "id": 2, }

Отправив соответствующий запрос на сервер, мы можем изменить раздел section access, например, добавить новых пользователей.

Раздел section access до:

Работа со скриптами приложений Qlik Sense с использованием Python и REST API

Раздел section access после:

Работа со скриптами приложений Qlik Sense с использованием Python и REST API

Итак, подведем итоги. Использование API QS помогает найти приложения с определённым составом скрипта, изменить этот скрипт, выполнить однообразную рутинную работу со множеством приложений довольно быстро. Данный подход может быть применим и к другим задачам с небольшими изменениями в коде. К минусам этого способа можно отнести тот факт, что оно эффективно только при массовых однотипных запросах. Если же необходим уникальный подход к каждому приложению, то способ скорее всего будет неуместным. С помощью API Qlik Sense вы сможете упростить работу с приложениями, оптимизировав типовые операции.

11
Начать дискуссию