diff --git a/releasenotes/notes/email-notification-by-internal-tool-08910ab2247c3864.yaml b/releasenotes/notes/email-notification-by-internal-tool-08910ab2247c3864.yaml new file mode 100644 index 000000000..20efe51c4 --- /dev/null +++ b/releasenotes/notes/email-notification-by-internal-tool-08910ab2247c3864.yaml @@ -0,0 +1,8 @@ +--- +features: + - | + Currently the email subscription in Zaqar relay on the third part + tools, such as "sendmail". It means that deployer should install + it out of Zaqar. If he forgets, Zaqar will raise internal error. + This work let Zaqar support email subscription by itself using + the ``smtp`` python library. diff --git a/samples/zaqar/sendmail.py b/samples/zaqar/sendmail.py new file mode 100644 index 000000000..ff172189e --- /dev/null +++ b/samples/zaqar/sendmail.py @@ -0,0 +1,228 @@ +# Copyright (c) 2018 Ustack, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not +# use this file except in compliance with the License. You may obtain a copy +# of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations under +# the License. + +from email.mime.text import MIMEText +from email.parser import Parser +import json +import smtplib +import sys + +from keystoneauth1 import loading +from keystoneauth1 import session as ks_session +from oslo_config import cfg +import requests +import retrying + +KUNKA_SERVICE_TYPE = 'portal' + +"""KUNKA_CORP_INFO_PATH is an API for obtaining information from the database +(such as /api/email/corporation-info), and the returned information +is the value of the field in the mail template. It is connected after +the ip:port which is the database connection information. ip:port +like 127.0.0.1:3306, or other""" +KUNKA_CORP_INFO_PATH = 'Your API address' + +# The information is relatively sensitive, suggesting encrypted transmission, +# or stored in the database to return.When "use_ssl" is False, the "port" is +# 25, otherwise, the "port" is 465 in using SSL type. +mail_info = { + "from": "youremail@youremail.com", + "hostname": "yourSMTP_serverAddr", + "username": "yourSMTP_server-username", + "password": "Authorization_code", + "port": 25, + "use_ssl": False +} + +# It's a HTML mail template,and can be changed as needed +mail_body = u""" +
+
+
+
+
+
+
+
+
+
+
+

+ /////////////////// + Respected + {corp_name}user + /////////////////// +

+
+
+
+
+

{confirm_or_alarm}

+
+
+
+
+

{corp_name}— + {home_link}

+
+
+
+""" + +mail_confirm_link = u""" +Your mailbox will be used for receiving system notifications. +If you confirm, click the following link: +Activation link +""" + +mail_alarm_info = u""" + Your alarm information is as follows:{reason}
+ Alarm level:{severity}
+ Alarm name:{alarm_name}
+ Alarm ID :{alarm_id} +""" + + +def prepare_conf(): + cfg.CONF(project='zaqar') + loading.register_auth_conf_options(cfg.CONF, 'keystone_authtoken') + + +def get_admin_session(): + auth_plugin = \ + loading.load_auth_from_conf_options(cfg.CONF, 'keystone_authtoken') + return ks_session.Session(auth=auth_plugin) + + +def get_endpoint(session, service_type, interface='internal'): + return session.get_endpoint(service_type=service_type, + interface=interface) + + +@retrying.retry(stop_max_attempt_number=3) +def get_corp_info(session): + kunka_endpoint = get_endpoint(session, KUNKA_SERVICE_TYPE) + kunka_url = kunka_endpoint + KUNKA_CORP_INFO_PATH + + res = None + res = requests.get(kunka_url) + + corp_info = res.json() + return {"corp_name": corp_info['corporationName'], + "logo_url": corp_info['emailLogoUrl'], + "home_link": corp_info['homeUrl'], + "from": corp_info['from']} + + +def generate_msg(subbody, to, from_, subject, **kwargs): + payload = mail_body.format(confirm_or_alarm=subbody, **kwargs) + msg = MIMEText(payload.encode('utf-8'), 'html', 'utf-8') + msg['subject'] = subject + msg['from'] = from_ + msg['to'] = to + + return msg + + +def generate_subbody(subbody, **kwargs): + return subbody.format(**kwargs) + + +def get_confirm_link(str_): + return str_.split('below: ')[-1] + + +def prepare_msg(msg_str): + headers = Parser().parsestr(msg_str) + payload = headers.get_payload() + + msg_subject = headers['subject'] + if not headers['subject']: + alarm_info = json.loads(payload)['body'] + subject = msg_subject + alarm_info['alarm_name'] + template = generate_subbody(mail_alarm_info, + reason=alarm_info['reason'], + severity=alarm_info['severity'], + alarm_name=alarm_info['alarm_name'], + alarm_id=alarm_info['alarm_id']) + else: + subject = msg_subject + template = generate_subbody(mail_confirm_link, + confirm_link=get_confirm_link(payload)) + + session = get_admin_session() + corp_info = get_corp_info(session) + + msg = generate_msg( + template, headers['to'], + corp_info['from'], subject, logo_url=corp_info['logo_url'], + corp_name=corp_info['corp_name'], home_link=corp_info['home_link']) + + return msg + + +@retrying.retry(stop_max_attempt_number=3) +def send_it(msg): + # if "use_ssl" is True, the "port" is 465 in using SSL type, + # or other SSL port. + if mail_info['use_ssl']: + sender = smtplib.SMTP_SSL(mail_info["hostname"], mail_info['port']) + else: + sender = smtplib.SMTP(mail_info["hostname"], mail_info['port']) + sender.set_debuglevel(1) + + sender.ehlo(mail_info["hostname"]) + try: + sender.login(mail_info["username"], mail_info["password"]) + except smtplib.SMTPException: + print("Error: Failed to connect to the SMTP service") + sender.sendmail(msg['from'], msg['to'], msg.as_string()) + + +def send_email(msg_str): + prepare_conf() + send_it(prepare_msg(msg_str)) + + +if __name__ == '__main__': + send_email(''.join(sys.stdin.readlines())) diff --git a/zaqar/conf/notification.py b/zaqar/conf/notification.py index 38653ce1b..e1a7e8f7a 100644 --- a/zaqar/conf/notification.py +++ b/zaqar/conf/notification.py @@ -14,6 +14,39 @@ from oslo_config import cfg +smtp_mode = cfg.StrOpt( + 'smtp_mode', default='third_part', + choices=('third_part', 'self_local'), + help='There are two values can be chosen: third_part or ' + 'self_local. third_part means Zaqar will use the tools ' + 'from config option smtp_commnd. self_local means the ' + 'smtp python library will be used.') + + +smtp_host = cfg.HostAddressOpt( + 'smtp_host', + help='The host IP for the email system. It should be ' + 'set when smtp_mode is set to self_local.') + + +smtp_port = cfg.PortOpt( + 'smtp_port', + help='The port for the email system. It should be set when ' + 'smtp_mode is set to self_local.') + + +smtp_user_name = cfg.StrOpt( + 'smtp_user_name', + help='The user name for the email system to login. It should ' + 'be set when smtp_mode is set to self_local.') + + +smtp_user_password = cfg.StrOpt( + 'smtp_user_password', + help='The user password for the email system to login. It ' + 'should be set when smtp_mode is set to self_local.') + + smtp_command = cfg.StrOpt( 'smtp_command', default='/usr/sbin/sendmail -t -oi', help=( @@ -76,6 +109,11 @@ unsubscribe_confirmation_email_template = cfg.DictOpt( GROUP_NAME = 'notification' ALL_OPTS = [ + smtp_mode, + smtp_host, + smtp_port, + smtp_user_name, + smtp_user_password, smtp_command, max_notifier_workers, require_confirmation, diff --git a/zaqar/notification/tasks/mailto.py b/zaqar/notification/tasks/mailto.py index 1691b6b70..3df4000e0 100644 --- a/zaqar/notification/tasks/mailto.py +++ b/zaqar/notification/tasks/mailto.py @@ -16,6 +16,7 @@ from email.mime import text import json from six.moves import urllib_parse +import smtplib import subprocess from oslo_log import log as logging @@ -67,8 +68,6 @@ class MailtoTask(object): conf_n = kwargs.get('conf').notification try: for message in messages: - p = subprocess.Popen(conf_n.smtp_command.split(' '), - stdin=subprocess.PIPE) # Send confirmation email to subscriber. if (message.get('Message_Type') == MessageType.SubscriptionConfirmation.name): @@ -98,7 +97,23 @@ class MailtoTask(object): msg["from"] = subscription['options'].get('from', '') subject_opt = subscription['options'].get('subject', '') msg["subject"] = params.get('subject', subject_opt) - p.communicate(msg.as_string()) + if conf_n.smtp_mode == 'third_part': + p = subprocess.Popen(conf_n.smtp_command.split(' '), + stdin=subprocess.PIPE) + p.communicate(msg.as_string()) + elif conf_n.smtp_mode == 'self_local': + sender = smtplib.SMTP_SSL(conf_n.smtp_host, + conf_n.smtp_port) + sender.set_debuglevel(1) + + sender.ehlo(conf_n.smtp_host) + try: + sender.login(conf_n.smtp_user_name, + conf_n.smtp_user_password) + except smtplib.SMTPException: + LOG.error("Failed to connect to the SMTP service") + continue + sender.sendmail(msg['from'], msg['to'], msg.as_string()) LOG.debug("Send mail successfully: %s", msg.as_string()) except OSError as err: LOG.exception('Failed to create process for sendmail, ' diff --git a/zaqar/tests/unit/notification/test_notifier.py b/zaqar/tests/unit/notification/test_notifier.py index 9c161e73f..b2ced9656 100644 --- a/zaqar/tests/unit/notification/test_notifier.py +++ b/zaqar/tests/unit/notification/test_notifier.py @@ -209,6 +209,7 @@ class NotifierTest(testing.TestBase): queue_ctlr.get = mock.Mock(return_value={}) driver = notifier.NotifierDriver(subscription_controller=ctlr, queue_controller=queue_ctlr) + ctlr.driver.conf.notification.smtp_mode = 'third_part' called = set() msg = ('Content-Type: text/plain; charset="us-ascii"\n' 'MIME-Version: 1.0\nContent-Transfer-Encoding: 7bit\nto:'