Merge pull request #11 from vexxhost/update-backup-service

Use openstack cloud api directly
diff --git a/staffeln/common/constants.py b/staffeln/common/constants.py
index 6de73f6..d0ec6fe 100644
--- a/staffeln/common/constants.py
+++ b/staffeln/common/constants.py
@@ -1,7 +1,5 @@
-BACKUP_COMPLETED=2

-BACKUP_WIP=1

-BACKUP_PLANNED=0

-

-BACKUP_ENABLED_KEY = 'true'

-

-DEFAULT_TIME_FORMAT = "%Y-%m-%d %H:%M:%S"
\ No newline at end of file
+BACKUP_COMPLETED=2
+BACKUP_WIP=1
+BACKUP_PLANNED=0
+
+BACKUP_ENABLED_KEY = 'true'
diff --git a/staffeln/common/notify.py b/staffeln/common/notify.py
deleted file mode 100644
index 19f8440..0000000
--- a/staffeln/common/notify.py
+++ /dev/null
@@ -1,49 +0,0 @@
-# Email notification package

-# This should be upgraded by integrating with mail server to send batch

-import smtplib

-from email.mime.text import MIMEText

-from email.mime.multipart import MIMEMultipart

-import staffeln.conf

-

-CONF = staffeln.conf.CONF

-

-

-def sendEmail(src_email, src_pwd, dest_email, subject, content, smtp_server_domain, smtp_server_port):

-    try:

-        message = MIMEMultipart("alternative")

-        message["Subject"] = subject

-        message["From"] = src_email

-        message["To"] = dest_email

-        part = MIMEText(content, "html")

-        message.attach(part)

-

-        s = smtplib.SMTP(host=smtp_server_domain, port=smtp_server_port)

-        s.ehlo()

-        s.starttls()

-        # we can comment this auth func when use the trusted ip without authentication against the smtp server

-        s.login(src_email, src_pwd)

-        s.sendmail(src_email, dest_email, message.as_string())

-        s.close()

-        return True

-    except Exception as e:

-        print(str(e))

-        return False

-

-def SendNotification(content, receiver=None):

-    subject = "Backup result"

-

-    html = "<h3>${CONTENT}</h3>"

-    html = html.replace("${CONTENT}", content)

-

-    if receiver == None:

-        return

-    if len(receiver) == 0:

-        return

-

-    res = sendEmail(src_email=CONF.notification.sender_email,

-                        src_pwd=CONF.notification.sender_pwd,

-                        dest_email=CONF.notification.receiver,

-                        subject=subject,

-                        content=html,

-                        smtp_server_domain=CONF.notification.smtp_server_domain,

-                        smtp_server_port=CONF.notification.smtp_server_port)

diff --git a/staffeln/common/time.py b/staffeln/common/time.py
index 6ebdee7..3059085 100644
--- a/staffeln/common/time.py
+++ b/staffeln/common/time.py
@@ -1,48 +1,55 @@
-import re

-from datetime import datetime

-from dateutil.relativedelta import relativedelta

-

-regex = re.compile(

-    r'((?P<years>\d+?)y)?((?P<months>\d+?)m)?((?P<weeks>\d+?)w)?((?P<days>\d+?)d)?'

-)

-

-

-# parse_time parses timedelta string to time dict

-# input: <string> 1y2m3w5d - all values should be integer

-# output: <dict> {year: 1, month: 2, week: 3, day: 5}

-def parse_timedelta_string(time_str):

-    empty_flag = True

-    try:

-        parts = regex.match(time_str)

-        if not parts:

-            return None

-        parts = parts.groupdict()

-        time_params = {}

-        for key in parts:

-            if parts[key]:

-                time_params[key] = int(parts[key])

-                empty_flag = False

-            else:

-                time_params[key] = 0

-        if empty_flag: return None

-        return time_params

-    except:

-        return None

-

-

-def timeago(years, months, weeks, days, from_date=None):

-    if from_date is None:

-        from_date = datetime.now()

-    return from_date - relativedelta(years=years, months=months, weeks=weeks, days=days)

-

-## yearsago using Standard library

-# def yearsago(years, from_date=None):

-#     if from_date is None:

-#         from_date = datetime.now()

-#     try:

-#         return from_date.replace(year=from_date.year - years)

-#     except ValueError:

-#         # Must be 2/29!

-#         assert from_date.month == 2 and from_date.day == 29 # can be removed

-#         return from_date.replace(month=2, day=28,

-#                                  year=from_date.year-years)

+import re
+from datetime import datetime
+from dateutil.relativedelta import relativedelta
+
+DEFAULT_TIME_FORMAT = "%Y-%m-%d %H:%M:%S"
+
+regex = re.compile(
+    r'((?P<years>\d+?)y)?((?P<months>\d+?)m)?((?P<weeks>\d+?)w)?((?P<days>\d+?)d)?'
+)
+
+
+# parse_time parses timedelta string to time dict
+# input: <string> 1y2m3w5d - all values should be integer
+# output: <dict> {year: 1, month: 2, week: 3, day: 5}
+def parse_timedelta_string(time_str):
+    empty_flag = True
+    try:
+        parts = regex.match(time_str)
+        if not parts:
+            return None
+        parts = parts.groupdict()
+        time_params = {}
+        for key in parts:
+            if parts[key]:
+                time_params[key] = int(parts[key])
+                empty_flag = False
+            else:
+                time_params[key] = 0
+        if empty_flag: return None
+        return time_params
+    except:
+        return None
+
+
+def get_current_strtime():
+    now = datetime.now()
+    return now.strftime(DEFAULT_TIME_FORMAT)
+
+
+def timeago(years, months, weeks, days, from_date=None):
+    if from_date is None:
+        from_date = datetime.now()
+    return from_date - relativedelta(years=years, months=months, weeks=weeks, days=days)
+
+## yearsago using Standard library
+# def yearsago(years, from_date=None):
+#     if from_date is None:
+#         from_date = datetime.now()
+#     try:
+#         return from_date.replace(year=from_date.year - years)
+#     except ValueError:
+#         # Must be 2/29!
+#         assert from_date.month == 2 and from_date.day == 29 # can be removed
+#         return from_date.replace(month=2, day=28,
+#                                  year=from_date.year-years)
diff --git a/staffeln/conductor/backup.py b/staffeln/conductor/backup.py
index ada7faa..f358c8a 100755
--- a/staffeln/conductor/backup.py
+++ b/staffeln/conductor/backup.py
@@ -2,7 +2,8 @@
 import collections
 from staffeln.common import constants
 
-from openstack import exceptions
+from openstack.exceptions import ResourceNotFound as OpenstackResourceNotFound
+from openstack.exceptions import SDKException as OpenstackSDKException
 from oslo_log import log
 from staffeln.common import auth
 from staffeln.common import context
@@ -51,11 +52,19 @@
         queues = objects.Queue.list(self.ctx, filters=filters)
         return queues
 
-    def create_queue(self):
+    def create_queue(self, old_tasks):
         """Create the queue of all the volumes for backup"""
+        # 1. get the old task list, not finished in the last cycle
+        #  and keep till now
+        old_task_volume_list = []
+        for old_task in old_tasks:
+            old_task_volume_list.append(old_task.volume_id)
+
+        # 2. add new tasks in the queue which are not existing in the old task list
         queue_list = self.check_instance_volumes()
         for queue in queue_list:
-            self._volume_queue(queue)
+            if not queue.volume_id in old_task_volume_list:
+                self._volume_queue(queue)
 
     # Backup the volumes attached to which has a specific metadata
     def filter_server(self, metadata):
@@ -75,7 +84,7 @@
                 LOG.info(_("Volume %s is not backed because it is in %s status" % (volume_id, volume['status'])))
             return res
 
-        except exceptions.ResourceNotFound:
+        except OpenstackResourceNotFound:
             return False
 
     def remove_volume_backup(self, backup_object):
@@ -95,7 +104,7 @@
                 LOG.info(_("Rotation for the backup %s is skipped in this cycle "
                            "because it is in %s status") % (backup_object.backup_id, backup["status"]))
 
-        except exceptions.ResourceNotFound:
+        except OpenstackResourceNotFound:
             LOG.info(_("Backup %s is not existing in Openstack."
                        "Or cinder-backup is not existing in the cloud." % backup_object.backup_id))
             # remove from the backup table
@@ -151,14 +160,15 @@
         backup_id = queue.backup_id
         if backup_id == "NULL":
             try:
-                volume_backup = conn.block_storage.create_backup(
+                volume_backup = conn.create_volume_backup(
                     volume_id=queue.volume_id, force=True
                 )
                 queue.backup_id = volume_backup.id
                 queue.backup_status = constants.BACKUP_WIP
                 queue.save()
-            except exceptions as error:
-                print(error)
+            except OpenstackSDKException as error:
+                LOG.info(_("Backup creation for the volume %s failled. %s"
+                           % (queue.volume_id, str(error))))
         else:
             pass
             # TODO(Alex): remove this task from the task list
@@ -199,20 +209,27 @@
                  status checked.
         Call the backups api to see if the backup is successful.
         """
-        for backup_gen in conn.block_storage.backups(volume_id=queue.volume_id):
-            if backup_gen.id == queue.backup_id:
-                if backup_gen.status == "error":
-                    self.process_failed_backup(queue)
-                elif backup_gen.status == "available":
-                    self.process_available_backup(queue)
-                elif backup_gen.status == "creating":
-                    # TODO(Alex): Need to escalate discussion
-                    # How to proceed WIP bakcup generators?
-                    # To make things worse, the last backup generator is in progress till
-                    # the new backup cycle
-                    LOG.info("Waiting for backup of %s to be completed" % queue.volume_id)
-                else:  # "deleting", "restoring", "error_restoring" status
-                    self.process_using_backup(queue)
+        # for backup_gen in conn.block_storage.backups(volume_id=queue.volume_id):
+        try:
+            backup_gen = conn.get_volume_backup(queue.backup_id)
+            if backup_gen == None:
+                # TODO(Alex): need to check when it is none
+                LOG.info(_("Backup status of %s is returning none."%(queue.backup_id)))
+                return
+            if backup_gen.status == "error":
+                self.process_failed_backup(queue)
+            elif backup_gen.status == "available":
+                self.process_available_backup(queue)
+            elif backup_gen.status == "creating":
+                # TODO(Alex): Need to escalate discussion
+                # How to proceed WIP bakcup generators?
+                # To make things worse, the last backup generator is in progress till
+                # the new backup cycle
+                LOG.info("Waiting for backup of %s to be completed" % queue.volume_id)
+            else:  # "deleting", "restoring", "error_restoring" status
+                self.process_using_backup(queue)
+        except OpenstackResourceNotFound as e:
+            self.process_failed_backup(queue)
 
     def _volume_backup(self, task):
         # matching_backups = [
diff --git a/staffeln/conductor/manager.py b/staffeln/conductor/manager.py
index 4493001..6898348 100755
--- a/staffeln/conductor/manager.py
+++ b/staffeln/conductor/manager.py
@@ -1,164 +1,174 @@
-import cotyledon
-import datetime
-from futurist import periodics
-from oslo_log import log
-import staffeln.conf
-import threading
-import time
-
-from staffeln.common import constants
-from staffeln.conductor import backup
-from staffeln.common import context
-from staffeln.common import time as xtime
-from staffeln.i18n import _
-
-LOG = log.getLogger(__name__)
-CONF = staffeln.conf.CONF
-
-
-class BackupManager(cotyledon.Service):
-    name = "Staffeln conductor backup controller"
-
-    def __init__(self, worker_id, conf):
-        super(BackupManager, self).__init__(worker_id)
-        self._shutdown = threading.Event()
-        self.conf = conf
-        self.ctx = context.make_context()
-        LOG.info("%s init" % self.name)
-
-    def run(self):
-        LOG.info("%s run" % self.name)
-        periodic_callables = [
-            (self.backup_engine, (), {}),
-        ]
-        periodic_worker = periodics.PeriodicWorker(periodic_callables, schedule_strategy="last_finished")
-        periodic_thread = threading.Thread(target=periodic_worker.start)
-        periodic_thread.daemon = True
-        periodic_thread.start()
-
-    def terminate(self):
-        LOG.info("%s terminate" % self.name)
-        super(BackupManager, self).terminate()
-
-    def reload(self):
-        LOG.info("%s reload" % self.name)
-
-    # Check if the backup count is over the limit
-    # TODO(Alex): how to count the backup number
-    #  only available backups are calculated?
-    def _over_limitation(self):
-        LOG.info(_("Checking the backup limitation..."))
-        max_count = CONF.conductor.max_backup_count
-        current_count = len(backup.Backup().get_backups())
-        if max_count <= current_count:
-            # TODO(Alex): Send notification
-            LOG.info(_("The backup limit is over."))
-            return True
-        LOG.info(_("The max limit is %s, and current backup count is %s" % (max_count, current_count)))
-        return False
-
-    # Manage active backup generators
-    def _process_wip_tasks(self):
-        LOG.info(_("Processing WIP backup generators..."))
-        queues_started = backup.Backup().get_queues(
-            filters={"backup_status": constants.BACKUP_WIP}
-        )
-        if len(queues_started) != 0:
-            for queue in queues_started: backup.Backup().check_volume_backup_status(queue)
-
-    # Create backup generators
-    def _process_new_tasks(self):
-        LOG.info(_("Creating new backup generators..."))
-        queues_to_start = backup.Backup().get_queues(
-            filters={"backup_status": constants.BACKUP_PLANNED}
-        )
-        if len(queues_to_start) != 0:
-            for queue in queues_to_start:
-                backup.Backup().create_volume_backup(queue)
-
-    # Refresh the task queue
-    # TODO(Alex): need to escalate discussion
-    #  how to manage last backups not finished yet
-    def _update_task_queue(self):
-        LOG.info(_("Updating backup task queue..."))
-        all_tasks = backup.Backup().get_queues()
-        if len(all_tasks) == 0:
-            backup.Backup().create_queue()
-        else:
-            LOG.info(_("The last backup cycle is not finished yet."
-                       "So the new backup cycle is skipped."))
-
-    @periodics.periodic(spacing=CONF.conductor.backup_service_period, run_immediately=True)
-    def backup_engine(self):
-        LOG.info("backing... %s" % str(time.time()))
-        LOG.info("%s periodics" % self.name)
-
-        if self._over_limitation(): return
-        self._update_task_queue()
-        self._process_wip_tasks()
-        self._process_new_tasks()
-
-
-class RotationManager(cotyledon.Service):
-    name = "Staffeln conductor rotation controller"
-
-    def __init__(self, worker_id, conf):
-        super(RotationManager, self).__init__(worker_id)
-        self._shutdown = threading.Event()
-        self.conf = conf
-        LOG.info("%s init" % self.name)
-
-    def run(self):
-        LOG.info("%s run" % self.name)
-
-        periodic_callables = [
-            (self.rotation_engine, (), {}),
-        ]
-        periodic_worker = periodics.PeriodicWorker(periodic_callables, schedule_strategy="last_finished")
-        periodic_thread = threading.Thread(target=periodic_worker.start)
-        periodic_thread.daemon = True
-        periodic_thread.start()
-
-    def terminate(self):
-        LOG.info("%s terminate" % self.name)
-        super(RotationManager, self).terminate()
-
-    def reload(self):
-        LOG.info("%s reload" % self.name)
-
-    def get_backup_list(self):
-        threshold_strtime = self.get_threshold_strtime()
-        if threshold_strtime == None: return False
-        self.backup_list = backup.Backup().get_backups(filters={"created_at__lt": threshold_strtime})
-        return True
-
-    def remove_backups(self):
-        print(self.backup_list)
-        for retention_backup in self.backup_list:
-            # 1. check the backup status and delete only available backups
-            backup.Backup().remove_volume_backup(retention_backup)
-
-    @periodics.periodic(spacing=CONF.conductor.retention_service_period, run_immediately=True)
-    def rotation_engine(self):
-        LOG.info("%s rotation_engine" % self.name)
-        # 1. get the list of backups to remove based on the retention time
-        if not self.get_backup_list(): return
-
-        # 2. remove the backups
-        self.remove_backups()
-
-    # get the threshold time str
-    def get_threshold_strtime(self):
-        time_delta_dict = xtime.parse_timedelta_string(CONF.conductor.retention_time)
-        if time_delta_dict == None: return None
-
-        res = xtime.timeago(
-            years=time_delta_dict["years"],
-            months=time_delta_dict["months"],
-            weeks=time_delta_dict["weeks"],
-            days=time_delta_dict["days"],
-        )
-        if res == None: LOG.info(_("Retention time format is invalid. "
-                                   "Follow <YEARS>y<MONTHS>m<WEEKS>w<DAYS>d."))
-
-        return res.strftime(constants.DEFAULT_TIME_FORMAT)
+import cotyledon

+import datetime

+from futurist import periodics

+from oslo_log import log

+import staffeln.conf

+import threading

+import time

+

+from staffeln.common import constants

+from staffeln.common import context

+from staffeln.common import time as xtime

+from staffeln.conductor import backup

+from staffeln.conductor import notify

+from staffeln.i18n import _

+

+LOG = log.getLogger(__name__)

+CONF = staffeln.conf.CONF

+

+

+class BackupManager(cotyledon.Service):

+    name = "Staffeln conductor backup controller"

+

+    def __init__(self, worker_id, conf):

+        super(BackupManager, self).__init__(worker_id)

+        self._shutdown = threading.Event()

+        self.conf = conf

+        self.ctx = context.make_context()

+        LOG.info("%s init" % self.name)

+

+    def run(self):

+        LOG.info("%s run" % self.name)

+        periodic_callables = [

+            (self.backup_engine, (), {}),

+        ]

+        periodic_worker = periodics.PeriodicWorker(periodic_callables, schedule_strategy="last_finished")

+        periodic_thread = threading.Thread(target=periodic_worker.start)

+        periodic_thread.daemon = True

+        periodic_thread.start()

+

+    def terminate(self):

+        LOG.info("%s terminate" % self.name)

+        super(BackupManager, self).terminate()

+

+    def reload(self):

+        LOG.info("%s reload" % self.name)

+

+    # Check if the backup count is over the limit

+    # TODO(Alex): how to count the backup number

+    #  only available backups are calculated?

+    def _check_quota(self):

+        LOG.info(_("Checking the backup limitation..."))

+        max_count = CONF.conductor.max_backup_count

+        current_count = len(backup.Backup().get_backups())

+        if max_count <= current_count:

+            # TODO(Alex): Send notification

+            LOG.info(_("The backup limit is over."))

+            return True

+        LOG.info(_("The max limit is %s, and current backup count is %s" % (max_count, current_count)))

+        return False

+

+    # Manage active backup generators

+    # TODO(Alex): need to discuss

+    #  Need to wait until all backups are finished?

+    #  That is required to make the backup report

+    def _process_wip_tasks(self):

+        LOG.info(_("Processing WIP backup generators..."))

+        queues_started = backup.Backup().get_queues(

+            filters={"backup_status": constants.BACKUP_WIP}

+        )

+        if len(queues_started) != 0:

+            for queue in queues_started: backup.Backup().check_volume_backup_status(queue)

+

+    # Create backup generators

+    def _process_todo_tasks(self):

+        LOG.info(_("Creating new backup generators..."))

+        queues_to_start = backup.Backup().get_queues(

+            filters={"backup_status": constants.BACKUP_PLANNED}

+        )

+        if len(queues_to_start) != 0:

+            for queue in queues_to_start:

+                backup.Backup().create_volume_backup(queue)

+

+    # Refresh the task queue

+    def _update_task_queue(self):

+        LOG.info(_("Updating backup task queue..."))

+        current_tasks = backup.Backup().get_queues()

+        backup.Backup().create_queue(current_tasks)

+

+    def _report_backup_result(self):

+        # TODO(Alex): Need to update these list

+        self.success_backup_list = []

+        self.failed_backup_list = []

+        notify.SendBackupResultEmail(self.success_backup_list, self.failed_backup_list)

+

+

+    @periodics.periodic(spacing=CONF.conductor.backup_service_period, run_immediately=True)

+    def backup_engine(self):

+        LOG.info("backing... %s" % str(time.time()))

+        LOG.info("%s periodics" % self.name)

+

+        if self._check_quota(): return

+        # NOTE(Alex): If _process_wip_tasks() waits tiil no WIP tasks

+        # exist, no need to repeat this function before and after queue update.

+        self._process_wip_tasks()

+        self._update_task_queue()

+        self._process_todo_tasks()

+        self._process_wip_tasks()

+        self._report_backup_result()

+

+

+

+class RotationManager(cotyledon.Service):

+    name = "Staffeln conductor rotation controller"

+

+    def __init__(self, worker_id, conf):

+        super(RotationManager, self).__init__(worker_id)

+        self._shutdown = threading.Event()

+        self.conf = conf

+        LOG.info("%s init" % self.name)

+

+    def run(self):

+        LOG.info("%s run" % self.name)

+

+        periodic_callables = [

+            (self.rotation_engine, (), {}),

+        ]

+        periodic_worker = periodics.PeriodicWorker(periodic_callables, schedule_strategy="last_finished")

+        periodic_thread = threading.Thread(target=periodic_worker.start)

+        periodic_thread.daemon = True

+        periodic_thread.start()

+

+    def terminate(self):

+        LOG.info("%s terminate" % self.name)

+        super(RotationManager, self).terminate()

+

+    def reload(self):

+        LOG.info("%s reload" % self.name)

+

+    def get_backup_list(self):

+        threshold_strtime = self.get_threshold_strtime()

+        if threshold_strtime == None: return False

+        self.backup_list = backup.Backup().get_backups(filters={"created_at__lt": threshold_strtime})

+        return True

+

+    def remove_backups(self):

+        print(self.backup_list)

+        for retention_backup in self.backup_list:

+            # 1. check the backup status and delete only available backups

+            backup.Backup().remove_volume_backup(retention_backup)

+

+    @periodics.periodic(spacing=CONF.conductor.retention_service_period, run_immediately=True)

+    def rotation_engine(self):

+        LOG.info("%s rotation_engine" % self.name)

+        # 1. get the list of backups to remove based on the retention time

+        if not self.get_backup_list(): return

+

+        # 2. remove the backups

+        self.remove_backups()

+

+    # get the threshold time str

+    def get_threshold_strtime(self):

+        time_delta_dict = xtime.parse_timedelta_string(CONF.conductor.retention_time)

+        if time_delta_dict == None: return None

+

+        res = xtime.timeago(

+            years=time_delta_dict["years"],

+            months=time_delta_dict["months"],

+            weeks=time_delta_dict["weeks"],

+            days=time_delta_dict["days"],

+        )

+        if res == None: LOG.info(_("Retention time format is invalid. "

+                                   "Follow <YEARS>y<MONTHS>m<WEEKS>w<DAYS>d."))

+

+        return res.strftime(xtime.DEFAULT_TIME_FORMAT)

diff --git a/staffeln/conductor/notify.py b/staffeln/conductor/notify.py
new file mode 100644
index 0000000..d80da4e
--- /dev/null
+++ b/staffeln/conductor/notify.py
@@ -0,0 +1,57 @@
+# Email notification package

+# This should be upgraded by integrating with mail server to send batch

+import smtplib

+from email.mime.text import MIMEText

+from email.mime.multipart import MIMEMultipart

+from oslo_log import log

+import staffeln.conf

+from staffeln.common import time as xtime

+from staffeln.i18n import _

+

+CONF = staffeln.conf.CONF

+LOG = log.getLogger(__name__)

+

+

+def _sendEmail(src_email, src_pwd, dest_email, subject, content, smtp_server_domain, smtp_server_port):

+    message = MIMEMultipart("alternative")

+    message["Subject"] = subject

+    message["From"] = src_email

+    message["To"] = dest_email

+    part = MIMEText(content, "html")

+    message.attach(part)

+

+    s = smtplib.SMTP(host=smtp_server_domain, port=smtp_server_port)

+    s.ehlo()

+    s.starttls()

+    # we can comment this auth func when use the trusted ip without authentication against the smtp server

+    s.login(src_email, src_pwd)

+    s.sendmail(src_email, dest_email, message.as_string())

+    s.close()

+

+

+def SendBackupResultEmail(success_backup_list, failed_backup_list):

+    subject = "Backup result"

+

+    html = "<h3>${TIME}</h3>" \

+           "<h3>Success List</h3>" \

+           "<h4>${SUCCESS_VOLUME_LIST}</h4>" \

+           "<h3>Failed List</h3>" \

+           "<h4>${FAILED_VOLUME_LIST}</h4>"

+

+    success_volumes = '<br>'.join([str(elem) for elem in success_backup_list])

+    failed_volumes = '<br>'.join([str(elem) for elem in failed_backup_list])

+    html = html.replace("${TIME}", xtime.get_current_strtime())

+    html = html.replace("${SUCCESS_VOLUME_LIST}", success_volumes)

+    html = html.replace("${FAILED_VOLUME_LIST}", failed_volumes)

+    try:

+        _sendEmail(src_email=CONF.notification.sender_email,

+                   src_pwd=CONF.notification.sender_pwd,

+                   dest_email=CONF.notification.receiver,

+                   subject=subject,

+                   content=html,

+                   smtp_server_domain=CONF.notification.smtp_server_domain,

+                   smtp_server_port=CONF.notification.smtp_server_port)

+        LOG.info(_("Backup result email sent"))

+    except Exception as e:

+        LOG.error(_("Backup result email send failed. Please check email configuration. %s" % (str(e))))

+

diff --git a/staffeln/conf/notify.py b/staffeln/conf/notify.py
index 42daefe..c292e51 100644
--- a/staffeln/conf/notify.py
+++ b/staffeln/conf/notify.py
@@ -9,11 +9,6 @@
 )
 
 email_opts = [
-    cfg.StrOpt(
-        "template",
-        default="<h3>\${CONTENT}</h3>",
-        help=_("This html template is used to email the backup result."),
-    ),
     cfg.ListOpt(
         "receiver",
         default=[],