Skip to content
This repository was archived by the owner on Jan 3, 2023. It is now read-only.

Commit a25c71f

Browse files
committed
Added a web configuration console, added support for promotion to 1-n sites, added support for auto-promotion on reloads, added an auditing dashboard, and more.
1 parent d315362 commit a25c71f

35 files changed

+9266
-880
lines changed

App Promotion Dashboard.qvf

320 KB
Binary file not shown.
File renamed without changes.

Modules/app_promote.py

Lines changed: 1030 additions & 423 deletions
Large diffs are not rendered by default.

Modules/app_publish_review.py

Lines changed: 138 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
'''
1+
"""
22
Contains the function that triggers whenever an app is published
33
to one of the streams that is set as an approval stream in
44
the QMC with a custom property='True' defined in the config.
55
66
Fires an email off to the associated addresses to any stream(s).
7-
'''
7+
"""
88

99
import logging
1010
from logging.handlers import RotatingFileHandler
@@ -13,34 +13,35 @@
1313
import json
1414
import csv
1515
from pathlib import Path
16+
import base64
1617
import Modules.qrs_functions as qrs
1718

1819
# configuration file
19-
with open("config.json") as f:
20+
with open("static/config.json") as f:
2021
CONFIG = json.load(f)
2122
f.close()
2223

2324
# logging
24-
LOG_LEVEL = CONFIG["log_level"].lower()
25+
LOG_LEVEL = CONFIG["logging"]["log_level"].lower()
2526

26-
LOG_LOCATION = os.path.expandvars("%ProgramData%\\Qlik\\Sense\\Log") + \
27-
"\\qs-event-driven-cross-site-app-promoter\\"
27+
# LOG_LOCATION = (
28+
# os.path.expandvars("%ProgramData%\\Qlik\\Sense\\Log")
29+
# + "\\qs-event-driven-cross-site-app-promoter\\"
30+
# )
31+
32+
LOG_LOCATION = str(Path(__file__).parent.parent).replace("\\", "/") + "/Log/"
2833

2934
if not os.path.exists(LOG_LOCATION):
3035
os.makedirs(LOG_LOCATION)
3136

3237
LOG_FILE = LOG_LOCATION + "app_publish_review.log"
33-
if not os.path.isfile(LOG_FILE):
34-
with open(LOG_FILE,"w") as file:
35-
file.write("{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}".format("Timestamp", "Module",
36-
"LogLevel","Process", "Thread", "Status", "LogID", "Message"))
37-
file.write('\n')
38-
file.close()
38+
LOG_BYTES = int(CONFIG["logging"]["other_logs_bytes"])
39+
LOG_ROLLING_BACKUP = int(CONFIG["logging"]["other_logs_rolling_backup_num"])
3940

4041
LOGGER = logging.getLogger(__name__)
41-
# rolling logs with max 2 MB files with 3 backups
4242
HANDLER = logging.handlers.RotatingFileHandler(
43-
LOG_FILE, maxBytes=2000000, backupCount=3)
43+
LOG_FILE, maxBytes=LOG_BYTES, backupCount=LOG_ROLLING_BACKUP
44+
)
4445

4546
if LOG_LEVEL == "info":
4647
logging.basicConfig(level=logging.INFO)
@@ -49,63 +50,85 @@
4950

5051
LOG_ID = str(uuid.uuid4())
5152
APP_CHANGE_STATUS = "Initializing"
52-
FORMATTER = logging.Formatter("%(asctime)s\t%(name)s\t%(levelname)s\t" +
53-
"%(process)d\t%(thread)d\t" + APP_CHANGE_STATUS + "\t%(message)s")
53+
FORMATTER = logging.Formatter(
54+
"%(asctime)s\t%(name)s\t%(levelname)s\t"
55+
+ "%(process)d\t%(thread)d\t"
56+
+ APP_CHANGE_STATUS
57+
+ "\t%(message)s"
58+
)
5459
HANDLER.setFormatter(FORMATTER)
5560
LOGGER.addHandler(HANDLER)
5661

5762
# additional config
5863
PROMOTION_EMAIL_ALERTS = CONFIG["promote_on_custom_property_change"]["email_config"][
59-
"promotion_email_alerts"]
60-
CUSTOM_PROPERTY_NAME_STREAM_ALERT_ON_PUBLISH = CONFIG["promote_on_custom_property_change"][
61-
"email_config"]["custom_property_name_stream_alert_on_publish"]
62-
LOCAL_SERVER_FQDN = CONFIG[
63-
'promote_on_custom_property_change']['local_server_FQDN']
64+
"promotion_email_alerts"
65+
]
66+
CUSTOM_PROPERTY_NAME_STREAM_ALERT_ON_PUBLISH = CONFIG[
67+
"promote_on_custom_property_change"
68+
]["email_config"]["custom_property_name_stream_alert_on_publish"]
69+
LOCAL_SERVER_FQDN = CONFIG["promote_on_custom_property_change"]["local_server_FQDN"]
6470
PROMOTION_SENDER_EMAIL = CONFIG["promote_on_custom_property_change"]["email_config"][
65-
"promotion_sender_email"]
71+
"promotion_sender_email"
72+
]
6673
PROMOTION_SENDER_PASS = CONFIG["promote_on_custom_property_change"]["email_config"][
67-
"promotion_sender_pass"]
74+
"promotion_sender_pass"
75+
]
76+
PROMOTION_SENDER_PASS_DECRYPTED = base64.b64decode(PROMOTION_SENDER_PASS).decode(
77+
"utf-8"
78+
)
6879
PROMOTION_SMTP = CONFIG["promote_on_custom_property_change"]["email_config"][
69-
"promotion_SMTP"]
70-
PROMOTION_SMTP_PORT = CONFIG["promote_on_custom_property_change"][
71-
"email_config"]["promotion_SMTP_port"]
80+
"promotion_SMTP"
81+
]
82+
PROMOTION_SMTP_PORT = CONFIG["promote_on_custom_property_change"]["email_config"][
83+
"promotion_SMTP_port"
84+
]
7285

7386
# email alert config for app promotion
7487
EMAIL_ALERTS = False
7588
if PROMOTION_EMAIL_ALERTS.lower() == "true":
7689
try:
7790
import smtplib
91+
7892
EMAIL_ALERTS = True
7993

8094
except Exception as error:
8195
LOGGER.error(
82-
"%s\tSomething went wrong while trying to setup email alerts: %s", LOG_ID, error)
96+
"%s\tSomething went wrong while trying to setup email alerts: %s",
97+
LOG_ID,
98+
error,
99+
)
83100
else:
84101
EMAIL_ALERTS = False
85102

86-
APP_CHANGE_STATUS = "app_published_gather_info"
87-
FORMATTER = logging.Formatter("%(asctime)s\t%(name)s\t%(levelname)s\t" +
88-
"%(process)d\t%(thread)d\t" + APP_CHANGE_STATUS + "\t%(message)s")
89-
HANDLER.setFormatter(FORMATTER)
90-
LOGGER.addHandler(HANDLER)
91-
92103

93104
def email_on_publish_to_review(app_id):
94-
'''
105+
"""
95106
Fires emails off to the addresses associated
96107
with the streams designated as approval streams
97108
once an app is published to one of them
98-
'''
99-
if EMAIL_ALERTS:
100-
log_id = str(uuid.uuid4())
109+
"""
110+
app_change_status = "app_published_gather_info"
111+
formatter = logging.Formatter(
112+
"%(asctime)s\t%(name)s\t%(levelname)s\t"
113+
+ "%(process)d\t%(thread)d\t"
114+
+ app_change_status
115+
+ "\t%(message)s"
116+
)
117+
HANDLER.setFormatter(formatter)
118+
LOGGER.addHandler(HANDLER)
119+
log_id = str(uuid.uuid4())
101120

121+
if EMAIL_ALERTS:
102122
s, base_url = qrs.establish_requests_session("local")
103123
LOGGER.info("%s\tRequesting app/full info on '%s'", log_id, app_id)
104124
app_full_status, app_full_response = qrs.app_full(s, base_url, app_id)
105125
qrs.close_requests_session(s)
106126
if app_full_status != 200:
107127
LOGGER.error(
108-
"%s\tSomething went wrong while trying to get app/full: %s", log_id, app_full_status)
128+
"%s\tSomething went wrong while trying to get app/full: %s",
129+
log_id,
130+
app_full_status,
131+
)
109132
else:
110133
LOGGER.debug("%s\tGot app/full data: %s", log_id, app_full_status)
111134

@@ -124,51 +147,71 @@ def email_on_publish_to_review(app_id):
124147
LOGGER.info("%s\tPublished to stream Name: '%s'", log_id, stream_name)
125148
LOGGER.info("%s\tPublished to stream ID: '%s'", log_id, stream_id)
126149

127-
LOGGER.info("%s\tChecking to see if the stream '%s' has the custom property '%s'",
128-
log_id, stream_id, CUSTOM_PROPERTY_NAME_STREAM_ALERT_ON_PUBLISH)
150+
LOGGER.info(
151+
"%s\tChecking to see if the stream '%s' has the custom property '%s'",
152+
log_id,
153+
stream_id,
154+
CUSTOM_PROPERTY_NAME_STREAM_ALERT_ON_PUBLISH,
155+
)
129156

130157
s, base_url = qrs.establish_requests_session("local")
131-
LOGGER.info("%s\tRequesting stream/full info on '%s'",
132-
log_id, stream_id)
158+
LOGGER.info("%s\tRequesting stream/full info on '%s'", log_id, stream_id)
133159
stream_full_status, stream_full_response = qrs.stream_full(
134-
s, base_url, stream_id)
160+
s, base_url, stream_id
161+
)
135162
qrs.close_requests_session(s)
136163
if stream_full_status != 200:
137-
LOGGER.error("%s\tSomething went wrong while trying to get stream/full: %s",
138-
log_id, stream_full_status)
164+
LOGGER.error(
165+
"%s\tSomething went wrong while trying to get stream/full: %s",
166+
log_id,
167+
stream_full_status,
168+
)
139169
else:
140-
LOGGER.debug("%s\tGot stream/full data: %s",
141-
log_id, stream_full_status)
170+
LOGGER.debug("%s\tGot stream/full data: %s", log_id, stream_full_status)
142171

143172
stream_custom_properties = stream_full_response["customProperties"]
144-
LOGGER.debug("%s\tStream custom properties: %s",
145-
log_id, stream_custom_properties)
173+
LOGGER.debug(
174+
"%s\tStream custom properties: %s", log_id, stream_custom_properties
175+
)
146176

147177
stream_marked_for_approval = False
148178
for stream_prop in stream_custom_properties:
149-
if stream_prop["definition"]["name"] == CUSTOM_PROPERTY_NAME_STREAM_ALERT_ON_PUBLISH:
179+
if (
180+
stream_prop["definition"]["name"]
181+
== CUSTOM_PROPERTY_NAME_STREAM_ALERT_ON_PUBLISH
182+
):
150183
stream_marked_for_approval = True
151184
break
152185

153186
if stream_marked_for_approval:
154187
app_change_status = "app_published_to_approval_stream"
155-
formatter = logging.Formatter("%(asctime)s\t%(name)s\t%(levelname)s\t" +
156-
"%(process)d\t%(thread)d\t" + app_change_status + "\t%(message)s")
188+
formatter = logging.Formatter(
189+
"%(asctime)s\t%(name)s\t%(levelname)s\t"
190+
+ "%(process)d\t%(thread)d\t"
191+
+ app_change_status
192+
+ "\t%(message)s"
193+
)
157194
HANDLER.setFormatter(formatter)
158195
LOGGER.addHandler(HANDLER)
159196
LOGGER.info(
160-
"%s\tApp published to approval stream: '%s', configuring email", log_id, stream_name)
197+
"%s\tApp published to approval stream: '%s', configuring email",
198+
log_id,
199+
stream_name,
200+
)
161201

162202
try:
163-
EMAIL_MAP_FILE_DIRECTORY = str(
164-
Path(__file__).parent.parent).replace("\\", "/")
165-
EMAIL_MAP_FILE = EMAIL_MAP_FILE_DIRECTORY + \
166-
'/ApprovalStreamsToEmailAddressMap.csv'
203+
EMAIL_MAP_FILE_DIRECTORY = str(Path(__file__).parent.parent).replace(
204+
"\\", "/"
205+
)
206+
EMAIL_MAP_FILE = (
207+
EMAIL_MAP_FILE_DIRECTORY
208+
+ "/static/ApprovalStreamsToEmailAddressMap.csv"
209+
)
167210
EMAIL_MAP_STREAM_LIST = []
168211
EMAIL_MAP_ADDRESS_LIST = []
169212

170213
with open(EMAIL_MAP_FILE) as email_map:
171-
reader = csv.reader(email_map, delimiter=',')
214+
reader = csv.reader(email_map, delimiter=",")
172215
next(reader, None) # skip the header
173216
for row in reader:
174217
EMAIL_MAP_STREAM_LIST.append(row[0])
@@ -182,35 +225,57 @@ def email_on_publish_to_review(app_id):
182225
recipient_list = list(set(recipient_list))
183226
LOGGER.info("%s\tRecipient list: '%s'", log_id, recipient_list)
184227

185-
subject = "'{}' published to: '{}'".format(
186-
app_name, stream_name)
228+
subject = "'{}' published to: '{}'".format(app_name, stream_name)
187229
body = """Application: '{}'\nOwned by: '{}'\nPublished by: '{}'\nPublished to: '{}'
188230
\n\nView the app here: https://{}/sense/app/{}""".format(
189-
app_name, app_owner, modified_by_user, stream_name, LOCAL_SERVER_FQDN, app_id)
190-
message = """From: %s\nTo: %s\nSubject: %s\n\n%s""" % (PROMOTION_SENDER_EMAIL, ", ".join(
191-
recipient_list), subject, body)
231+
app_name,
232+
app_owner,
233+
modified_by_user,
234+
stream_name,
235+
LOCAL_SERVER_FQDN,
236+
app_id,
237+
)
238+
message = """From: %s\nTo: %s\nSubject: %s\n\n%s""" % (
239+
PROMOTION_SENDER_EMAIL,
240+
", ".join(recipient_list),
241+
subject,
242+
body,
243+
)
192244

193245
try:
194246
server = smtplib.SMTP(PROMOTION_SMTP, PROMOTION_SMTP_PORT)
195247
server.ehlo()
196248
server.starttls()
197-
server.login(PROMOTION_SENDER_EMAIL, PROMOTION_SENDER_PASS)
198-
server.sendmail(PROMOTION_SENDER_EMAIL,
199-
recipient_list, message)
249+
server.login(
250+
PROMOTION_SENDER_EMAIL, PROMOTION_SENDER_PASS_DECRYPTED
251+
)
252+
server.sendmail(PROMOTION_SENDER_EMAIL, recipient_list, message)
200253
server.close()
201-
LOGGER.info("%s\tSuccessfully sent the email to %s",
202-
log_id, recipient_list)
254+
LOGGER.info(
255+
"%s\tSuccessfully sent the email to %s", log_id, recipient_list
256+
)
203257
except Exception as error:
204258
LOGGER.error(
205-
"%s\tThere was an error trying to send the email %s", log_id, error)
259+
"%s\tThere was an error trying to send the email %s",
260+
log_id,
261+
error,
262+
)
206263

207264
except Exception as error:
208265
LOGGER.error(
209-
"%s\tSomething went wrong with the email alerts: %s", log_id, error)
266+
"%s\tSomething went wrong with the email alerts: %s", log_id, error
267+
)
210268
LOGGER.error(
211-
"%s\tEnsure you have filled out 'ApprovalStreamsToEmailAddressMap.csv' properly", log_id)
269+
"%s\tEnsure you have filled out 'ApprovalStreamsToEmailAddressMap.csv' properly",
270+
log_id,
271+
)
212272
else:
213-
LOGGER.info("%s\tStream '%s' with id '%s' does not contain the custom property '%s'. Exiting.",
214-
log_id, stream_name, stream_id, CUSTOM_PROPERTY_NAME_STREAM_ALERT_ON_PUBLISH)
273+
LOGGER.info(
274+
"%s\tStream '%s' with id '%s' does not contain the custom property '%s'. Exiting.",
275+
log_id,
276+
stream_name,
277+
stream_id,
278+
CUSTOM_PROPERTY_NAME_STREAM_ALERT_ON_PUBLISH,
279+
)
215280

216281
return "Finished"

0 commit comments

Comments
 (0)