def is_user_data_expired(self, user_id: str) -> (bool, None): """ Returns True or False depending if seconds passed between last fetch of user data and now is greater then user_data_cache_timeout Parameters ---------- user_id: str user id to return data for Returns ------- bool, None: True if cache expired otherwise False """ if user_id is None: logging.error("%: user_id not provided.", my_own_function_name()) return this_user = self.get(user_id) if this_user is not None and \ this_user.data_last_updated + self.user_data_cache_timeout >= datetime.now().timestamp(): return False logging.debug("User data cache for user '%s' expired." % user_id) return True
async def fetch_slack_user_info(self, user_id: str) -> None: """ Fetch user data for user_id from Slack Parameters ---------- user_id: str user id to return data for """ if self.web_handle is None: logging.error( "%: function called before attribute web_handle set.", my_own_function_name()) return if user_id is None: logging.error("%: user_id not provided.", my_own_function_name()) return if self.is_user_data_expired(user_id) is False: return logging.debug("No cached user data found. Fetching from Slack.") slack_user_data = await self.web_handle.users_info(user=user_id) if slack_user_data is not None and slack_user_data.get("user"): logging.debug("Successfully fetched user data.") user = self.get(user_id) user.data = slack_user_data.get("user") user.data_last_updated = datetime.now().timestamp() else: logging.error("Unable to fetched user data.")
def set_web_handle(self, web_handle: WebClient) -> None: """ Set web handle to use for user data requests Parameters ---------- web_handle: WebClient slack web handle object which is part of a slack message """ if web_handle is None: logging.error("%: web_handle not provided.", my_own_function_name()) return self.web_handle = web_handle
def post_slack_message(handle=None, channel=None, slack_response=None): """ Post a message to Slack Parameters ---------- handle: object the Slack client handle to use channel: str Slack channel to post message to slack_response: BotResponse Slack response object Returns ------- RequestResponse: slack response from posting a message """ def __do_post(text, blocks, attachments): this_response = RequestResponse() # try to send of message try: logging.debug("Posting Slack message to channel '%s'" % channel) # noinspection PyUnresolvedReferences this_response.text = handle.chat_postMessage( channel=channel, text=text[:slack_max_message_text_length], blocks=blocks, attachments=attachments) except slack.errors.SlackApiError as e: this_response.text = e.response this_response.error = this_response.text.get("error") except Exception as e: this_response.error = str(e) return this_response response = RequestResponse() if handle is None: return RequestResponse( error="Error in function '%s': no client handle defined" % (my_own_function_name())) if channel is None: return RequestResponse( error="Error in function '%s': no channel defined" % (my_own_function_name())) if slack_response is None: return RequestResponse( error="Error in function '%s': no slack_response defined" % (my_own_function_name())) # split post into multiple posts if slack_response.blocks is not None and len(slack_response.blocks) > 50: # use lambda function to split message_blocks to chunks of 'slack_max_message_blocks' blocks split_blocks = lambda a, n=slack_max_message_blocks: [ a[i:i + n] for i in range(0, len(a), n) ] splitted_blocks = split_blocks(slack_response.blocks) logging.debug( "Sending multiple Slack messages as the number of blocks %d exceeds the maximum of %d" % (len(slack_response.blocks), slack_max_message_blocks)) post_iteration = 1 for message_blocks in splitted_blocks: last_message_attachments = None # get attachments and send them only with the last message if post_iteration == len(splitted_blocks): last_message_attachments = slack_response.dump_attachments() response = __do_post(slack_response.text, message_blocks, last_message_attachments) if response.error: break post_iteration += 1 else: response = __do_post(slack_response.text, slack_response.blocks, slack_response.dump_attachments()) """ if isinstance(slack_response, BotResponse): else: message can be sent like this with message builder classes unfortunately it causes to many log messages response.text = handle.chat_postMessage(channel=channel, **slack_response.to_dict()) """ if response.error: logging.error("Posting Slack message to channel '%s' failed: " % response.error) # only the response of the last message will be returned return response
def enable_disable_action(config=None, bot_commands=None, slack_message=None, slack_user=None, *args, **kwargs): """ Have a conversation with the user about the attribute the user wants to enable/disable Parameters ---------- config : dict dictionary with items parsed from config file bot_commands: BotCommands class with bot commands to avoid circular imports slack_message : string slack message to parse slack_user : SlackUser SlackUser object args, kwargs: None used to hold additional args which are just ignored Returns ------- BotResponse: questions about the action, confirmations or errors """ if slack_message is None: logging.error("Parameter '%s' missing while calling function '%s'" % ("slack_message", my_own_function_name())) if slack_user is None: logging.error("Parameter '%s' missing while calling function '%s'" % ("slack_user", my_own_function_name())) if slack_message is None or slack_user is None: return slack_error_response() # New conversation if slack_user.conversation is None: slack_user.start_conversation() this_conversation = slack_user.conversation # check or command if this_conversation.command is None: logging.debug("Command not set, parsing: %s" % slack_message) this_conversation.command = bot_commands.get_command_called( slack_message) # see if checking for sub_commands works better here if this_conversation.command.name not in ["enable", "disable"]: this_conversation.command = None return None logging.debug("Command parsed: %s" % this_conversation.command.name) slack_message = this_conversation.command.strip_command(slack_message) # check for sub command if this_conversation.sub_command is None: if len(slack_message) != 0: # we got a filter logging.debug("Sub command not set, parsing: %s" % slack_message) if this_conversation.command.has_sub_commands(): this_conversation.sub_command = \ this_conversation.command.sub_commands.get_command_called(slack_message) if this_conversation.sub_command: slack_message = this_conversation.sub_command.strip_command( slack_message) logging.debug("Sub command parsed: %s" % this_conversation.sub_command.name) # check for filter if this_conversation.sub_command is not None and \ this_conversation.sub_command.object_type != "global" and this_conversation.filter is None: if len(slack_message) != 0: filter_list = quoted_split(string_to_split=slack_message, preserve_quotations=True) filter_list = slack_user.get_last_user_filter_if_requested( filter_list) logging.debug("Filter parsed: %s" % filter_list) this_conversation.filter = filter_list slack_user.add_last_filter(this_conversation.filter) # try to find objects based on filter if this_conversation.filter and this_conversation.filter_result is None: logging.debug("Filter result list empty. Query Icinga for objects.") # query hosts and services i2_result = get_i2_object(config, this_conversation.sub_command.object_type, None, this_conversation.filter) # encountered Icinga request issue if i2_result.error: logging.debug("No icinga objects found for filter: %s" % this_conversation.filter) return slack_error_response( header= "Icinga request error while trying to find matching hosts/services", fallback_text="Icinga Error", error_message=i2_result.error) # save current conversation state if filter returned any objects if i2_result.data and len(i2_result.data) > 0: logging.debug("Found %d objects to %s %s for" % (len(i2_result.data), this_conversation.command.name, this_conversation.sub_command.name)) this_conversation.filter_result = i2_result.data this_conversation.filter_used = i2_result.filter # ask for sub command if this_conversation.sub_command is None: logging.debug("Sub command not set, asking for it") response_text = \ "%sSorry, I wasn't able to parse your sub command. Check `help %s` to get available sub commands" % \ (this_conversation.get_path(), this_conversation.command.name) return BotResponse(text=response_text) # ask for missing info if this_conversation.sub_command.object_type != "global" and this_conversation.filter is None: logging.debug("Filter not set, asking for it") response_text = "%sFor which object do you want to %s %s?" % ( this_conversation.get_path(), this_conversation.command.name, this_conversation.sub_command.name) return BotResponse(text=response_text) # no objects found based on filter if this_conversation.sub_command.object_type != "global" and this_conversation.filter_result is None: logging.debug( "Icinga2 object request returned empty, asking for a different filter" ) response_text = "%sSorry, I was not able to find any hosts or services for your search '%s'. Try again." \ % (this_conversation.get_path(), " ".join(this_conversation.filter)) this_conversation.filter = None return BotResponse(text=response_text) # now we seem to have all information and ask user if that's what the user wants if not this_conversation.confirmed: if this_conversation.confirmation_sent: if slack_message.startswith("y") or slack_message.startswith("Y"): this_conversation.confirmed = True elif slack_message.startswith("n") or slack_message.startswith( "N"): this_conversation.canceled = True else: this_conversation.confirmation_sent = False if not this_conversation.confirmation_sent: confirmation = { "Command": "%s %s" % (this_conversation.command.name, this_conversation.sub_command.name) } if this_conversation.sub_command.object_type != "global" and this_conversation.filter_result is not None: confirmation.update({"Objects": ""}) response = BotResponse(text="Confirm your action") confirmation_fields = list() for title, value in confirmation.items(): confirmation_fields.append(">*%s*: %s" % (title, value)) if this_conversation.sub_command.object_type != "global" and this_conversation.filter_result is not None: for i2_object in this_conversation.filter_result[0:10]: if this_conversation.sub_command.object_type == "Host": name = i2_object.get("name") else: name = '%s - %s' % (i2_object.get("host_name"), i2_object.get("name")) confirmation_fields.append(u">\t• %s" % name) if len(this_conversation.filter_result) > 10: confirmation_fields.append( ">\t... and %d more" % (len(this_conversation.filter_result) - 10)) response.add_block("\n".join(confirmation_fields)) response.add_block("Do you want to confirm this action?:") this_conversation.confirmation_sent = True return response if this_conversation.canceled: slack_user.reset_conversation() return BotResponse(text="%sOk, action has been canceled!" % this_conversation.get_path()) if this_conversation.confirmed: # delete conversation history slack_user.reset_conversation() i2_handle: object i2_handle, i2_error = setup_icinga_connection(config) if not i2_handle: if i2_error is not None: error_message = i2_error else: error_message = "Unknown error while setting up Icinga2 connection" return slack_error_response(header="Icinga request error", error_message=error_message) success_message = None i2_error = None enable = True if this_conversation.command.name == "disable": enable = False logging.debug("Sending command '%s %s' to Icinga2" % (this_conversation.command.name, this_conversation.sub_command.name)) try: if this_conversation.sub_command.object_type == "global": success_message = "Successfully %sd %s!" % \ (this_conversation.command.name, this_conversation.sub_command.name) i2_response = i2_handle.objects.update( object_type="IcingaApplication", name="app", attrs={ "attrs": { this_conversation.sub_command.icinga_attr_name: enable } }) else: success_message = "Successfully %sd %s for %s!" % \ (this_conversation.command.name, this_conversation.sub_command.name, " ".join(this_conversation.filter)) # noinspection PyProtectedMember url_path = '{}/{}'.format( i2_handle.objects.base_url_path, i2_handle.objects._convert_object_type( this_conversation.sub_command.object_type)) logging.debug(url_path) payload = { 'attrs': { this_conversation.sub_command.icinga_attr_name: enable }, 'filter': this_conversation.filter_used } # noinspection PyProtectedMember i2_response = i2_handle.objects._request( 'POST', url_path, payload) except Exception as e: i2_error = str(e) logging.error("Unable to perform Icinga2 object update: %s" % i2_error) pass if i2_error: return slack_error_response(header="Icinga request error", error_message=i2_error) return BotResponse(text=success_message) return None
def chat_with_user( config=None, bot_commands=None, slack_message=None, slack_user=None, *args, **kwargs): """ Have a conversation with the user about the action the user wants to perform Parameters ---------- config : dict dictionary with items parsed from config file bot_commands: BotCommands class with bot commands to avoid circular imports slack_message : string slack message to parse slack_user : SlackUser SlackUser object args, kwargs: None used to hold additional args which are just ignored Returns ------- BotResponse: questions about the action, confirmations or errors """ # set properties for available action commands action_commands = { "acknowledge": { "filter_end_marker": "until", "need_start_date": False, "need_end_date": True, "need_comment": True, "filter_question": "What do you want acknowledge?" }, "downtime": { "filter_end_marker": "from", "need_start_date": True, "need_end_date": True, "need_comment": True, "filter_question": "What do you want to set a downtime for?" }, "comment": { "filter_end_marker": "with", "need_start_date": False, "need_end_date": False, "need_comment": True, "filter_question": "What do you want to add a comment to?" }, "reschedule": { "filter_end_marker": None, "need_start_date": False, "need_end_date": False, "need_comment": False, "filter_question": "What do you want to reschedule?" }, "send notification": { "filter_end_marker": "with", "need_start_date": False, "need_end_date": False, "need_comment": True, "filter_question": "What do you want to send notifications for?" }, "delay notification": { "filter_end_marker": "until", "need_start_date": False, "need_end_date": True, "need_comment": False, "filter_question": "What do you want to delay notifications for?" }, "remove": { "filter_end_marker": None, "need_start_date": False, "need_end_date": False, "need_comment": False, "has_sub_commands": True, "filter_question": "For which object do you want to remove {}s?" } } # set defaults control vars filter_end_marker = None need_start_date = False need_end_date = False need_comment = False has_sub_commands = False filter_question = None if slack_message is None: logging.error("Parameter '%s' missing while calling function '%s'" % ("slack_message", my_own_function_name())) if slack_user is None: logging.error("Parameter '%s' missing while calling function '%s'" % ("slack_user_id", my_own_function_name())) if slack_message is None or slack_user is None: return slack_error_response() # New conversation if slack_user.conversation is None: slack_user.start_conversation() conversation = slack_user.conversation # check or command if conversation.command is None: logging.debug("Command not set, parsing: %s" % slack_message) conversation.command = bot_commands.get_command_called(slack_message) if conversation.command.name not in action_commands.keys(): conversation.command = None return None logging.debug("Command parsed: %s" % conversation.command.name) slack_message = conversation.command.strip_command(slack_message) if conversation.command is not None: command_data = action_commands.get(conversation.command.name) if command_data is not None: # update control vars filter_end_marker = command_data.get("filter_end_marker", None) need_start_date = command_data.get("need_start_date", False) need_end_date = command_data.get("need_end_date", False) need_comment = command_data.get("need_comment", False) has_sub_commands = command_data.get("has_sub_commands", False) filter_question = command_data.get("filter_question", None) if has_sub_commands is True and conversation.sub_command is None: if len(slack_message) != 0: # we got a filter logging.debug("Sub command not set, parsing: %s" % slack_message) if conversation.command.has_sub_commands(): conversation.sub_command = \ conversation.command.sub_commands.get_command_called(slack_message) if conversation.sub_command: slack_message = conversation.sub_command.strip_command(slack_message) logging.debug("Sub command parsed: %s" % conversation.sub_command.name) # check for filter if conversation.filter is None: if len(quoted_split(string_to_split=slack_message)) != 0: # we got a filter logging.debug("Filter not set, parsing: %s" % slack_message) split_slack_message = quoted_split(string_to_split=slack_message, preserve_quotations=True) # everything left of the index string will be parsed as filter if filter_end_marker is not None and filter_end_marker in [s.lower() for s in split_slack_message]: index = [s.lower() for s in split_slack_message].index(filter_end_marker) # get end of filter list filter_list = split_slack_message[0:index] # strip index string from slack message on comments if filter_end_marker == "with": index += 1 # strip the filter from the supplied string slack_message = " ".join(split_slack_message[index:]) else: # index string not found # assuming the whole message is meant to be a filter filter_list = split_slack_message slack_message = "" filter_list = slack_user.get_last_user_filter_if_requested(filter_list) logging.debug("Filter parsed: %s" % filter_list) if len(filter_list) > 0: conversation.filter = filter_list slack_user.add_last_filter(conversation.filter) # split slack_message into an array (chat message array) cma = quoted_split(string_to_split=slack_message, preserve_quotations=True) # try to find objects based on filter if conversation.filter and conversation.filter_result is None: logging.debug("Filter result list empty. Query Icinga for objects.") host_filter = list() service_filter = list() if conversation.command.name == "acknowledge": host_filter = ["host.state != 0"] service_filter = ["service.state != 0"] # query hosts and services if len(conversation.filter) == 1: object_type = "Host" if conversation.sub_command is not None: if conversation.sub_command.name == "downtime": object_type = "HostDowntime" else: object_type = "HostComment" i2_result = get_i2_object(config, object_type, host_filter, conversation.filter) if i2_result.error is None and len(i2_result.data) == 0: object_type = "Service" if conversation.sub_command is not None: if conversation.sub_command.name == "downtime": object_type = "ServiceDowntime" else: object_type = "ServiceComment" i2_result = get_i2_object(config, object_type, service_filter, conversation.filter) # just query services else: object_type = "Service" if conversation.sub_command is not None: if conversation.sub_command.name == "downtime": object_type = "ServiceDowntime" else: object_type = "ServiceComment" i2_result = get_i2_object(config, object_type, service_filter, conversation.filter) # encountered Icinga request issue if i2_result.error: logging.debug("No icinga objects found for filter: %s" % conversation.filter) return slack_error_response( header="Icinga request error while trying to find matching hosts/services", fallback_text="Icinga Error", error_message=i2_result.error ) # we can set a downtime for all objects no matter their state if conversation.sub_command is not None: if conversation.sub_command.name == "downtime" and len(i2_result.data) > 0: conversation.filter_result = i2_result.data else: # filter results based on sub command name ack_filter_result = list() for result in i2_result.data: if conversation.sub_command.name == "comment" and result.get("entry_type") == 1: ack_filter_result.append(result) if conversation.sub_command.name == "acknowledgement" and result.get("entry_type") == 4: ack_filter_result.append(result) if len(ack_filter_result) > 0: conversation.filter_result = ack_filter_result elif conversation.command.name == "acknowledge" and len(i2_result.data) > 0: # only objects which are not acknowledged can be acknowledged ack_filter_result = list() for result in i2_result.data: # only add results which are not acknowledged if result.get("acknowledgement") == 0: ack_filter_result.append(result) if len(ack_filter_result) > 0: conversation.filter_result = ack_filter_result else: conversation.filter_result = i2_result.data # save current conversation state if filter returned any objects if conversation.filter_result and len(conversation.filter_result) > 0: sub_command_name = "" if conversation.sub_command is not None: sub_command_name = f" {conversation.sub_command.name}" logging.debug("Found %d objects for command %s%s" % (len(conversation.filter_result), conversation.command.name, sub_command_name)) conversation.object_type = object_type else: conversation.filter_result = None # parse start time information for downtime if need_start_date is True and conversation.start_date is None and conversation.filter_result is not None: if len(cma) != 0: logging.debug("Start date not set, parsing: %s" % " ".join(cma)) date_string_parse = " ".join(cma) cma_lower = [s.lower() for s in cma] from_index = None until_index = None if "from" in cma_lower: from_index = cma_lower.index("from") if "until" in cma_lower: until_index = cma_lower.index("until") if from_index is not None and len(cma) > from_index + 1: cma = cma[from_index + 1:] if until_index is not None: until_index -= 1 date_string_parse = " ".join(cma[0:until_index]) cma = cma[until_index:] start_date_data = parse_relative_date(date_string_parse) if start_date_data: logging.debug("Start date successfully parsed") # get timestamp from returned datetime object if start_date_data.get("dt"): conversation.start_date = start_date_data.get("dt").timestamp() if len(cma) >= 1 and cma[0].lower() != "until": cma = date_string_parse[start_date_data.get("mend"):].strip().split(" ") else: conversation.start_date_parsing_failed = date_string_parse # parse end time information if need_end_date is True and conversation.end_date is None and conversation.filter_result is not None: if len(cma) != 0: logging.debug("End date not set, parsing: %s" % " ".join(cma)) cma_lower = [s.lower() for s in cma] until_index = None if "until" in cma_lower: until_index = cma_lower.index("until") if until_index is not None and len(cma) > until_index + 1: cma = cma[until_index + 1:] if len(cma) >= 1 and cma[0].lower() in ["never", "infinite"]: # add rest of message as description conversation.end_date = -1 del cma[0] else: string_parse = " ".join(cma) end_date_data = parse_relative_date(string_parse) if end_date_data: # get timestamp from returned datetime object if end_date_data.get("dt"): conversation.end_date = end_date_data.get("dt").timestamp() # add rest of string back to cma cma = string_parse[end_date_data.get("mend"):].strip().split(" ") else: conversation.end_date_parsing_failed = string_parse if need_comment is True and conversation.description is None and conversation.filter_result is not None: if len(cma) != 0 and len("".join(cma).strip()) != 0: logging.debug("Description not set, parsing: %s" % " ".join(cma)) conversation.description = " ".join(cma) cma = list() # ask for sub command if has_sub_commands is True and conversation.sub_command is None: logging.debug("Sub command not set, asking for it") response_text = \ "%sSorry, I wasn't able to parse your sub command. Check `help %s` to get available sub commands" % \ (conversation.get_path(), conversation.command.name) return BotResponse(text=response_text) # ask for missing info if conversation.filter is None: logging.debug("Filter not set, asking for it") if has_sub_commands is True: filter_question = filter_question.format(conversation.sub_command.name) filter_question = "%s%s" % (conversation.get_path(), filter_question) return BotResponse(text=filter_question) # no objects found based on filter if conversation.filter_result is None: problematic = "" logging.debug("Icinga2 object request returned empty, asking for a different filter") if conversation.command.name == "acknowledge": problematic = " problematic" object_text = "hosts or services" if conversation.sub_command is not None: object_text = conversation.sub_command.name response_text = "%sSorry, I was not able to find any%s %s for your search '%s'. Try again." \ % (conversation.get_path(), problematic, object_text, " ".join(conversation.filter)) conversation.filter = None return BotResponse(text=response_text) # ask for not parsed start time if need_start_date is True and conversation.start_date is None: if not conversation.start_date_parsing_failed: logging.debug("Start date not set, asking for it") response_text = "When should the downtime start?" else: logging.debug("Failed to parse start date, asking again for it") response_text = "Sorry, I was not able to understand the start date '%s'. Try again please." \ % conversation.start_date_parsing_failed response_text = "%s%s" % (conversation.get_path(), response_text) return BotResponse(text=response_text) # ask for not parsed end date if need_end_date is True and conversation.end_date is None: if not conversation.end_date_parsing_failed: logging.debug("End date not set, asking for it") if conversation.command.name == "acknowledge": response_text = "When should the acknowledgement expire? Or never?" else: response_text = "When should the downtime end?" else: logging.debug("Failed to parse end date, asking again for it") response_text = "Sorry, I was not able to understand the end date '%s'. Try again please." \ % conversation.end_date_parsing_failed response_text = "%s%s" % (conversation.get_path(), response_text) return BotResponse(text=response_text) if conversation.end_date and conversation.end_date != -1 and \ conversation.end_date - 60 < datetime.now().timestamp(): logging.debug("End date is already in the past. Ask user again for end date") response_text = "Sorry, end date '%s' lies (almost) in the past. Please define a valid end/expire date." % \ ts_to_date(conversation.end_date) response_text = "%s%s" % (conversation.get_path(), response_text) conversation.end_date = None return BotResponse(text=response_text) if need_start_date is True and conversation.start_date > conversation.end_date: logging.debug("Start date is after end date for downtime. Ask user again for start date.") response_text = "Sorry, start date '%s' can't be after and date '%s'. When should the downtime start?" % \ (ts_to_date(conversation.start_date), ts_to_date(conversation.end_date)) response_text = "%s%s" % (conversation.get_path(), response_text) conversation.start_date = None return BotResponse(text=response_text) if need_comment is True and conversation.description is None: logging.debug("Description not set, asking for it") response_text = "%sPlease add a comment." % conversation.get_path() return BotResponse(text=response_text) # now we seem to have all information and ask user if that's what the user wants if not conversation.confirmed: if conversation.confirmation_sent: if cma[0].startswith("y") or cma[0].startswith("Y"): conversation.confirmed = True elif cma[0].startswith("n") or cma[0].startswith("N"): conversation.canceled = True else: # see if user tried to filter the selection (i.e.: 1,2) if conversation.sub_command is not None: selection_list = [x.strip() for x in " ".join(cma).split(",")] if len(selection_list) > 0: objects_to_keep = list() for selection in selection_list: try: objects_to_keep.append(conversation.filter_result[int(selection) - 1]) except Exception: pass if len(objects_to_keep) > 0: conversation.filter_result = objects_to_keep conversation.confirmation_sent = False if not conversation.confirmation_sent: # get object type if conversation.command.name == "acknowledge": command = "Acknowledgement" else: command = conversation.command.name.capitalize() confirmation_type = conversation.object_type if conversation.sub_command is not None: confirmation_type = conversation.sub_command.name confirmation = { "Command": command, "Type": confirmation_type } if need_start_date is True: confirmation["Start"] = ts_to_date(conversation.start_date) confirmation["End"] = ts_to_date(conversation.end_date) elif conversation.command.name == "acknowledge": confirmation["Expire"] = "Never" if conversation.end_date == -1 else ts_to_date( conversation.end_date) elif conversation.command.name == "delay notification": confirmation["Delayed until"] = "Never" if conversation.end_date == -1 else ts_to_date( conversation.end_date) if need_comment is True: confirmation["Comment"] = conversation.description confirmation["Objects"] = "" response = BotResponse(text="Confirm your action") confirmation_fields = list() for title, value in confirmation.items(): confirmation_fields.append(">*%s*: %s" % (title, value)) object_num = 0 for i2_object in conversation.filter_result[0:10]: object_num += 1 host_name = service_name = comment_text = author = None bullet_text = "•" if conversation.object_type == "Service": host_name = i2_object.get("host_name") service_name = i2_object.get("name") elif "Comment" in conversation.object_type or "Downtime" in conversation.object_type: bullet_text = f"{object_num}." host_name = i2_object.get("host_name") if len(i2_object.get("service_name", "")) > 0: service_name = i2_object.get("service_name") if i2_object.get("comment") is not None: comment_text = i2_object.get("comment") else: comment_text = i2_object.get("text") author = i2_object.get("author") else: # host host_name = i2_object.get("name") host_url = get_web2_slack_url(host_name, web2_url=config["icinga.web2_url"]) service_url = get_web2_slack_url(host_name, service_name, web2_url=config["icinga.web2_url"]) if service_name is not None: object_text = "%s | %s" % (host_url, service_url) else: object_text = host_url if comment_text is not None: object_text += f" - {comment_text}" if comment_text is not None: object_text += f" (by: {author})" confirmation_fields.append(u">\t%s %s" % (bullet_text, object_text)) if len(conversation.filter_result) > 10: confirmation_fields.append(">\t... and %d more" % (len(conversation.filter_result) - 10)) response.add_block("\n".join(confirmation_fields)) if conversation.sub_command is not None: response.add_block("Do you want to confirm this action (yes|no)\n" "or do you want to select single/multiple %s (i.e.: 1,2)?:" % conversation.sub_command.name) else: response.add_block("Do you want to confirm this action?:") conversation.confirmation_sent = True return response if conversation.canceled: slack_user.reset_conversation() return BotResponse(text="%sOk, action has been canceled!" % conversation.get_path()) if conversation.confirmed: # delete conversation history slack_user.reset_conversation() i2_handle, i2_error = setup_icinga_connection(config) if not i2_handle: if i2_error is not None: error_message = i2_error else: error_message = "Unknown error while setting up Icinga2 connection" return slack_error_response(header="Icinga request error", error_message=error_message) # define filters filter_list = list() if conversation.object_type == "Host": for i2_object in conversation.filter_result: filter_list.append('host.name=="%s"' % i2_object.get("name")) else: for i2_object in conversation.filter_result: filter_list.append('( host.name=="%s" && service.name=="%s" )' % (i2_object.get("host_name"), i2_object.get("name"))) success_message = None i2_error = None # get username to add as comment this_user_info = slack_user.data author_name = "Anonymous Slack user" if this_user_info is not None and this_user_info.get("real_name"): author_name = this_user_info.get("real_name") icinga2_filters = '(' + ' || '.join(filter_list) + ')' try: if conversation.command.name == "downtime": logging.debug("Sending Downtime to Icinga2") success_message = "Successfully scheduled downtime!" i2_response = i2_handle.actions.schedule_downtime( object_type=conversation.object_type, filters=icinga2_filters, author=author_name, comment=conversation.description, start_time=conversation.start_date, end_time=conversation.end_date, duration=conversation.end_date - conversation.start_date, all_services=True ) elif conversation.command.name == "acknowledge": logging.debug("Sending Acknowledgement to Icinga2") success_message = "Successfully acknowledged %s problem%s!" % \ (conversation.object_type, plural(len(filter_list))) i2_response = i2_handle.actions.acknowledge_problem( object_type=conversation.object_type, filters=icinga2_filters, author=author_name, comment=conversation.description, expiry=None if conversation.end_date == -1 else conversation.end_date, sticky=True ) elif conversation.command.name == "comment": logging.debug("Sending Comment to Icinga2") success_message = "Successfully added %s comment%s!" % \ (conversation.object_type, plural(len(filter_list))) i2_response = i2_handle.actions.add_comment( object_type=conversation.object_type, filters=icinga2_filters, author=author_name, comment=conversation.description ) elif conversation.command.name == "reschedule": logging.debug("Sending reschedule check to Icinga2") success_message = "Successfully rescheduled %s check%s!" % \ (conversation.object_type, plural(len(filter_list))) i2_response = i2_handle.actions.reschedule_check( object_type=conversation.object_type, filters=icinga2_filters, ) elif conversation.command.name == "send notification": logging.debug("Sending custom notification to Icinga2") success_message = "Successfully sent %s notification%s!" % \ (conversation.object_type, plural(len(filter_list))) i2_response = i2_handle.actions.send_custom_notification( object_type=conversation.object_type, filters=icinga2_filters, author=author_name, comment=conversation.description ) elif conversation.command.name == "delay notification": logging.debug("Sending delay notification to Icinga2") success_message = "Successfully delayed %s notification%s!" % \ (conversation.object_type, plural(len(filter_list))) i2_response = i2_handle.actions.delay_notification( object_type=conversation.object_type, filters=icinga2_filters, timestamp=conversation.end_date ) elif conversation.command.name == "remove": logging.debug(f"Sending remove {conversation.sub_command.name} to Icinga2") success_message = f"Successfully removed {conversation.sub_command.name}!" if conversation.sub_command.name == "acknowledgement": for acknowledgement in conversation.filter_result: this_object_type = "Host" this_filter = 'host.name=="%s"' % acknowledgement.get("host_name") if len(acknowledgement.get("service_name", "")) > 0: this_object_type = "Service" this_filter += ' && service.name=="%s"' % acknowledgement.get("service_name") i2_response = i2_handle.actions.remove_acknowledgement( object_type=this_object_type, filters=this_filter, ) if conversation.sub_command.name == "comment": for comment in conversation.filter_result: name = "!".join( [comment.get("host_name"), comment.get("service_name"), comment.get("name")] ).replace("!!", "!") i2_response = i2_handle.actions.remove_comment( object_type="Comment", name=name, filters=None # bug in icinga2apic ) if conversation.sub_command.name == "downtime": for downtime in conversation.filter_result: name = "!".join( [downtime.get("host_name"), downtime.get("service_name"), downtime.get("name")] ).replace("!!", "!") i2_response = i2_handle.actions.remove_downtime( object_type="Downtime", name=name, filters=None ) except Exception as e: i2_error = str(e) logging.error("Unable to perform Icinga2 action: %s" % i2_error) pass if i2_error: return slack_error_response(header="Icinga request error", error_message=i2_error) return BotResponse(text=success_message) return None
def show_command(config=None, bot_commands=None, slack_message=None, slack_user=None, *args, **kwargs): """ Have a conversation with the user about the action the user wants to perform Parameters ---------- config : dict dictionary with items parsed from config file bot_commands: BotCommands class with bot commands to avoid circular imports slack_message : string slack message to parse slack_user : SlackUser SlackUser object args, kwargs: None used to hold additional args which are just ignored Returns ------- BotResponse: questions about the action, confirmations or errors """ called_sub_command = None called_command = None if slack_message is None: logging.error("Parameter '%s' missing while calling function '%s'" % ("slack_message", my_own_function_name())) return slack_error_response() # lowercase makes parsing easier slack_message = slack_message.lower() called_command = bot_commands.get_command_called(slack_message) slack_message = called_command.strip_command(slack_message) if called_command.name != "show": logging.error("This function (%s) only supports command 'show'", my_own_function_name()) return slack_error_response() if len(slack_message) != 0: # we got a filter logging.debug("Sub command not set, parsing: %s" % slack_message) if called_command.has_sub_commands(): called_sub_command = called_command.sub_commands.get_command_called( slack_message) if called_sub_command: slack_message = called_sub_command.strip_command(slack_message) logging.debug("Sub command parsed: %s" % called_sub_command.name) if called_sub_command is None: response_text = "Missing sub command com, dt or ack. Use `help show` for further details." return BotResponse(text=response_text) split_slack_message = quoted_split(string_to_split=slack_message, preserve_quotations=True) split_slack_message = slack_user.get_last_user_filter_if_requested( split_slack_message) logging.debug("Filter parsed: %s" % split_slack_message) if called_sub_command.name == "downtime": object_type = "Downtime" else: object_type = "Comment" object_filter = None if called_sub_command.name == "comment": object_filter = ["comment.entry_type == 1"] elif called_sub_command.name == "acknowledgement": object_filter = ["comment.entry_type == 4"] i2_result = None result_list = list() if len(split_slack_message) <= 1: i2_result = get_i2_object(config, f"Host{object_type}", object_filter, split_slack_message) result_list.extend(i2_result.data) if i2_result is None or (i2_result.error is None and len(i2_result.data) == 0): i2_result = get_i2_object(config, f"Service{object_type}", object_filter, split_slack_message) result_list.extend(i2_result.data) # encountered Icinga request issue if i2_result.error: logging.debug("No icinga objects found for filter: %s" % split_slack_message) return slack_error_response( header= "Icinga request error while trying to find matching hosts/services %s" % called_sub_command.name, fallback_text="Icinga Error", error_message=i2_result.error) if len(result_list) == 0: response_text = f"Sorry. No {called_sub_command.name}s found" if len(split_slack_message) > 0: response_text += " for " + " and ".join(split_slack_message) return BotResponse(text=response_text) slack_user.add_last_filter(split_slack_message) result_list = sorted(result_list, key=lambda k: (k['host_name'], k['service_name'], k['entry_time'])) block_text_list = list() for result in result_list: host_url = get_web2_slack_url(result.get("host_name"), web2_url=config["icinga.web2_url"]) service_url = get_web2_slack_url(result.get("host_name"), result.get("service_name"), web2_url=config["icinga.web2_url"]) if result.get("service_name"): object_text = "*%s | %s*" % (host_url, service_url) else: object_text = "*%s*" % host_url if called_sub_command.name == "downtime": # add text and info about expiration this_text = result.get("comment") if result.get("fixed") is True: this_text += " (fixed from {} until {})".format( ts_to_date(result.get("start_time")), ts_to_date(result.get("end_time"))) else: this_text += " (flexible for {} minutes between {} and {})".format( result.get("duration") / 60, ts_to_date(result.get("start_time")), ts_to_date(result.get("end_time"))) else: # add text and info about expiration this_text = result.get("text") if result.get("expire_time" ) is not None and result.get("expire_time") > 0: this_text += " (expires: {})".format( ts_to_date(result.get("expire_time"))) this_title = "{} by {} ({})\n>`{}`".format( object_text, result.get("author"), ts_to_date(result.get("entry_time")), this_text) block_text_list.append(this_title) response = BotResponse() response.text = "Icinga %s %ss response" % (called_command.name, called_sub_command.name) block_text = "Found %d matching %s%s" % \ (len(result_list), called_sub_command.name, plural(len(result_list))) response.add_block(block_text) # fill blocks with formatted response block_text = "" for response_object in block_text_list: if len(block_text) + len( response_object) + 2 > slack_max_block_text_length: response.add_block(block_text) block_text = "" block_text += "%s\n\n" % response_object else: response.add_block(block_text) return response