1# Licensed to the Apache Software Foundation (ASF) under one or more
2# contributor license agreements.  See the NOTICE file distributed with
3# this work for additional information regarding copyright ownership.
4# The ASF licenses this file to You under the Apache License, Version 2.0
5# (the "License"); you may not use this file except in compliance with
6# the License.  You may obtain a copy of the License at
7#
8#     http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15
16from libcloud.utils.py3 import ET
17from libcloud.backup.base import BackupDriver, BackupTarget, BackupTargetJob
18from libcloud.backup.types import BackupTargetType
19from libcloud.backup.types import Provider
20from libcloud.common.dimensiondata import dd_object_to_id
21from libcloud.common.dimensiondata import DimensionDataConnection
22from libcloud.common.dimensiondata import DimensionDataBackupClient
23from libcloud.common.dimensiondata import DimensionDataBackupClientAlert
24from libcloud.common.dimensiondata import DimensionDataBackupClientType
25from libcloud.common.dimensiondata import DimensionDataBackupDetails
26from libcloud.common.dimensiondata import DimensionDataBackupSchedulePolicy
27from libcloud.common.dimensiondata import DimensionDataBackupStoragePolicy
28from libcloud.common.dimensiondata import API_ENDPOINTS, DEFAULT_REGION
29from libcloud.common.dimensiondata import TYPES_URN
30from libcloud.common.dimensiondata import GENERAL_NS, BACKUP_NS
31from libcloud.utils.xml import fixxpath, findtext, findall
32
33# pylint: disable=no-member
34
35DEFAULT_BACKUP_PLAN = 'Advanced'
36
37
38class DimensionDataBackupDriver(BackupDriver):
39    """
40    DimensionData backup driver.
41    """
42
43    selected_region = None
44    connectionCls = DimensionDataConnection
45    name = 'Dimension Data Backup'
46    website = 'https://cloud.dimensiondata.com/'
47    type = Provider.DIMENSIONDATA
48    api_version = 1.0
49
50    network_domain_id = None
51
52    def __init__(self, key, secret=None, secure=True, host=None, port=None,
53                 api_version=None, region=DEFAULT_REGION, **kwargs):
54
55        if region not in API_ENDPOINTS and host is None:
56            raise ValueError(
57                'Invalid region: %s, no host specified' % (region))
58        if region is not None:
59            self.selected_region = API_ENDPOINTS[region]
60
61        super(DimensionDataBackupDriver, self).__init__(
62            key=key, secret=secret,
63            secure=secure, host=host,
64            port=port,
65            api_version=api_version,
66            region=region,
67            **kwargs)
68
69    def _ex_connection_class_kwargs(self):
70        """
71            Add the region to the kwargs before the connection is instantiated
72        """
73
74        kwargs = super(DimensionDataBackupDriver,
75                       self)._ex_connection_class_kwargs()
76        kwargs['region'] = self.selected_region
77        return kwargs
78
79    def get_supported_target_types(self):
80        """
81        Get a list of backup target types this driver supports
82
83        :return: ``list`` of :class:``BackupTargetType``
84        """
85        return [BackupTargetType.VIRTUAL]
86
87    def list_targets(self):
88        """
89        List all backuptargets
90
91        :rtype: ``list`` of :class:`BackupTarget`
92        """
93        targets = self._to_targets(
94            self.connection.request_with_orgId_api_2('server/server').object)
95        return targets
96
97    def create_target(self, name, address,
98                      type=BackupTargetType.VIRTUAL, extra=None):
99        """
100        Creates a new backup target
101
102        :param name: Name of the target (not used)
103        :type name: ``str``
104
105        :param address: The ID of the node in Dimension Data Cloud
106        :type address: ``str``
107
108        :param type: Backup target type, only Virtual supported
109        :type type: :class:`BackupTargetType`
110
111        :param extra: (optional) Extra attributes (driver specific).
112        :type extra: ``dict``
113
114        :rtype: Instance of :class:`BackupTarget`
115        """
116        if extra is not None:
117            service_plan = extra.get('servicePlan', DEFAULT_BACKUP_PLAN)
118        else:
119            service_plan = DEFAULT_BACKUP_PLAN
120            extra = {'servicePlan': service_plan}
121
122        create_node = ET.Element('NewBackup',
123                                 {'xmlns': BACKUP_NS})
124        create_node.set('servicePlan', service_plan)
125
126        response = self.connection.request_with_orgId_api_1(
127            'server/%s/backup' % (address),
128            method='POST',
129            data=ET.tostring(create_node)).object
130
131        asset_id = None
132        for info in findall(response,
133                            'additionalInformation',
134                            GENERAL_NS):
135            if info.get('name') == 'assetId':
136                asset_id = findtext(info, 'value', GENERAL_NS)
137
138        return BackupTarget(
139            id=asset_id,
140            name=name,
141            address=address,
142            type=type,
143            extra=extra,
144            driver=self
145        )
146
147    def create_target_from_node(self, node, type=BackupTargetType.VIRTUAL,
148                                extra=None):
149        """
150        Creates a new backup target from an existing node
151
152        :param node: The Node to backup
153        :type  node: ``Node``
154
155        :param type: Backup target type (Physical, Virtual, ...).
156        :type type: :class:`BackupTargetType`
157
158        :param extra: (optional) Extra attributes (driver specific).
159        :type extra: ``dict``
160
161        :rtype: Instance of :class:`BackupTarget`
162        """
163        return self.create_target(name=node.name,
164                                  address=node.id,
165                                  type=BackupTargetType.VIRTUAL,
166                                  extra=extra)
167
168    def create_target_from_container(self, container,
169                                     type=BackupTargetType.OBJECT,
170                                     extra=None):
171        """
172        Creates a new backup target from an existing storage container
173
174        :param node: The Container to backup
175        :type  node: ``Container``
176
177        :param type: Backup target type (Physical, Virtual, ...).
178        :type type: :class:`BackupTargetType`
179
180        :param extra: (optional) Extra attributes (driver specific).
181        :type extra: ``dict``
182
183        :rtype: Instance of :class:`BackupTarget`
184        """
185        return NotImplementedError(
186            'create_target_from_container not supported for this driver')
187
188    def update_target(self, target, name=None, address=None, extra=None):
189        """
190        Update the properties of a backup target, only changing the serviceplan
191        is supported.
192
193        :param target: Backup target to update
194        :type  target: Instance of :class:`BackupTarget` or ``str``
195
196        :param name: Name of the target
197        :type name: ``str``
198
199        :param address: Hostname, FQDN, IP, file path etc.
200        :type address: ``str``
201
202        :param extra: (optional) Extra attributes (driver specific).
203        :type extra: ``dict``
204
205        :rtype: Instance of :class:`BackupTarget`
206        """
207        if extra is not None:
208            service_plan = extra.get('servicePlan', DEFAULT_BACKUP_PLAN)
209        else:
210            service_plan = DEFAULT_BACKUP_PLAN
211        request = ET.Element('ModifyBackup',
212                             {'xmlns': BACKUP_NS})
213        request.set('servicePlan', service_plan)
214        server_id = self._target_to_target_address(target)
215        self.connection.request_with_orgId_api_1(
216            'server/%s/backup/modify' % (server_id),
217            method='POST',
218            data=ET.tostring(request)).object
219        if isinstance(target, BackupTarget):
220            target.extra = extra
221        else:
222            target = self.ex_get_target_by_id(server_id)
223        return target
224
225    def delete_target(self, target):
226        """
227        Delete a backup target
228
229        :param target: Backup target to delete
230        :type  target: Instance of :class:`BackupTarget` or ``str``
231
232        :rtype: ``bool``
233        """
234        server_id = self._target_to_target_address(target)
235        response = self.connection.request_with_orgId_api_1(
236            'server/%s/backup?disable' % (server_id),
237            method='GET').object
238        response_code = findtext(response, 'result', GENERAL_NS)
239        return response_code in ['IN_PROGRESS', 'SUCCESS']
240
241    def list_recovery_points(self, target, start_date=None, end_date=None):
242        """
243        List the recovery points available for a target
244
245        :param target: Backup target to delete
246        :type  target: Instance of :class:`BackupTarget`
247
248        :param start_date: The start date to show jobs between (optional)
249        :type  start_date: :class:`datetime.datetime`
250
251        :param end_date: The end date to show jobs between (optional)
252        :type  end_date: :class:`datetime.datetime``
253
254        :rtype: ``list`` of :class:`BackupTargetRecoveryPoint`
255        """
256        raise NotImplementedError(
257            'list_recovery_points not implemented for this driver')
258
259    def recover_target(self, target, recovery_point, path=None):
260        """
261        Recover a backup target to a recovery point
262
263        :param target: Backup target to delete
264        :type  target: Instance of :class:`BackupTarget`
265
266        :param recovery_point: Backup target with the backup data
267        :type  recovery_point: Instance of :class:`BackupTarget`
268
269        :param path: The part of the recovery point to recover (optional)
270        :type  path: ``str``
271
272        :rtype: Instance of :class:`BackupTargetJob`
273        """
274        raise NotImplementedError(
275            'recover_target not implemented for this driver')
276
277    def recover_target_out_of_place(self, target, recovery_point,
278                                    recovery_target, path=None):
279        """
280        Recover a backup target to a recovery point out-of-place
281
282        :param target: Backup target with the backup data
283        :type  target: Instance of :class:`BackupTarget`
284
285        :param recovery_point: Backup target with the backup data
286        :type  recovery_point: Instance of :class:`BackupTarget`
287
288        :param recovery_target: Backup target with to recover the data to
289        :type  recovery_target: Instance of :class:`BackupTarget`
290
291        :param path: The part of the recovery point to recover (optional)
292        :type  path: ``str``
293
294        :rtype: Instance of :class:`BackupTargetJob`
295        """
296        raise NotImplementedError(
297            'recover_target_out_of_place not implemented for this driver')
298
299    def get_target_job(self, target, id):
300        """
301        Get a specific backup job by ID
302
303        :param target: Backup target with the backup data
304        :type  target: Instance of :class:`BackupTarget`
305
306        :param id: Backup target with the backup data
307        :type  id: Instance of :class:`BackupTarget`
308
309        :rtype: :class:`BackupTargetJob`
310        """
311        jobs = self.list_target_jobs(target)
312        return list(filter(lambda x: x.id == id, jobs))[0]
313
314    def list_target_jobs(self, target):
315        """
316        List the backup jobs on a target
317
318        :param target: Backup target with the backup data
319        :type  target: Instance of :class:`BackupTarget`
320
321        :rtype: ``list`` of :class:`BackupTargetJob`
322        """
323        raise NotImplementedError(
324            'list_target_jobs not implemented for this driver')
325
326    def create_target_job(self, target, extra=None):
327        """
328        Create a new backup job on a target
329
330        :param target: Backup target with the backup data
331        :type  target: Instance of :class:`BackupTarget`
332
333        :param extra: (optional) Extra attributes (driver specific).
334        :type extra: ``dict``
335
336        :rtype: Instance of :class:`BackupTargetJob`
337        """
338        raise NotImplementedError(
339            'create_target_job not implemented for this driver')
340
341    def resume_target_job(self, target, job):
342        """
343        Resume a suspended backup job on a target
344
345        :param target: Backup target with the backup data
346        :type  target: Instance of :class:`BackupTarget`
347
348        :param job: Backup target job to resume
349        :type  job: Instance of :class:`BackupTargetJob`
350
351        :rtype: ``bool``
352        """
353        raise NotImplementedError(
354            'resume_target_job not implemented for this driver')
355
356    def suspend_target_job(self, target, job):
357        """
358        Suspend a running backup job on a target
359
360        :param target: Backup target with the backup data
361        :type  target: Instance of :class:`BackupTarget`
362
363        :param job: Backup target job to suspend
364        :type  job: Instance of :class:`BackupTargetJob`
365
366        :rtype: ``bool``
367        """
368        raise NotImplementedError(
369            'suspend_target_job not implemented for this driver')
370
371    def cancel_target_job(self, job, ex_client=None, ex_target=None):
372        """
373        Cancel a backup job on a target
374
375        :param job: Backup target job to cancel.  If it is ``None``
376                    ex_client and ex_target must be set
377        :type  job: Instance of :class:`BackupTargetJob` or ``None``
378
379        :param ex_client: Client of the job to cancel.
380                          Not necessary if job is specified.
381                          DimensionData only has 1 job per client
382        :type  ex_client: Instance of :class:`DimensionDataBackupClient`
383                          or ``str``
384
385        :param ex_target: Target to cancel a job from.
386                          Not necessary if job is specified.
387        :type  ex_target: Instance of :class:`BackupTarget` or ``str``
388
389        :rtype: ``bool``
390        """
391        if job is None:
392            if ex_client is None or ex_target is None:
393                raise ValueError("Either job or ex_client and "
394                                 "ex_target have to be set")
395            server_id = self._target_to_target_address(ex_target)
396            client_id = self._client_to_client_id(ex_client)
397        else:
398            server_id = job.target.address
399            client_id = job.extra['clientId']
400
401        response = self.connection.request_with_orgId_api_1(
402            'server/%s/backup/client/%s?cancelJob' % (server_id,
403                                                      client_id),
404            method='GET').object
405        response_code = findtext(response, 'result', GENERAL_NS)
406        return response_code in ['IN_PROGRESS', 'SUCCESS']
407
408    def ex_get_target_by_id(self, id):
409        """
410        Get a target by server id
411
412        :param id: The id of the target you want to get
413        :type  id: ``str``
414
415        :rtype: :class:`BackupTarget`
416        """
417        node = self.connection.request_with_orgId_api_2(
418            'server/server/%s' % id).object
419        return self._to_target(node)
420
421    def ex_add_client_to_target(self, target, client_type, storage_policy,
422                                schedule_policy, trigger, email):
423        """
424        Add a client to a target
425
426        :param target: Backup target with the backup data
427        :type  target: Instance of :class:`BackupTarget` or ``str``
428
429        :param client: Client to add to the target
430        :type  client: Instance of :class:`DimensionDataBackupClientType`
431                       or ``str``
432
433        :param storage_policy: The storage policy for the client
434        :type  storage_policy: Instance of
435                               :class:`DimensionDataBackupStoragePolicy`
436                               or ``str``
437
438        :param schedule_policy: The schedule policy for the client
439        :type  schedule_policy: Instance of
440                                :class:`DimensionDataBackupSchedulePolicy`
441                                or ``str``
442
443        :param trigger: The notify trigger for the client
444        :type  trigger: ``str``
445
446        :param email: The notify email for the client
447        :type  email: ``str``
448
449        :rtype: ``bool``
450        """
451        server_id = self._target_to_target_address(target)
452
453        backup_elm = ET.Element('NewBackupClient',
454                                {'xmlns': BACKUP_NS})
455        if isinstance(client_type, DimensionDataBackupClientType):
456            ET.SubElement(backup_elm, "type").text = client_type.type
457        else:
458            ET.SubElement(backup_elm, "type").text = client_type
459
460        if isinstance(storage_policy, DimensionDataBackupStoragePolicy):
461            ET.SubElement(backup_elm,
462                          "storagePolicyName").text = storage_policy.name
463        else:
464            ET.SubElement(backup_elm,
465                          "storagePolicyName").text = storage_policy
466
467        if isinstance(schedule_policy, DimensionDataBackupSchedulePolicy):
468            ET.SubElement(backup_elm,
469                          "schedulePolicyName").text = schedule_policy.name
470        else:
471            ET.SubElement(backup_elm,
472                          "schedulePolicyName").text = schedule_policy
473
474        alerting_elm = ET.SubElement(backup_elm, "alerting")
475        alerting_elm.set('trigger', trigger)
476        ET.SubElement(alerting_elm, "emailAddress").text = email
477
478        response = self.connection.request_with_orgId_api_1(
479            'server/%s/backup/client' % (server_id),
480            method='POST',
481            data=ET.tostring(backup_elm)).object
482        response_code = findtext(response, 'result', GENERAL_NS)
483        return response_code in ['IN_PROGRESS', 'SUCCESS']
484
485    def ex_remove_client_from_target(self, target, backup_client):
486        """
487        Removes a client from a backup target
488
489        :param  target: The backup target to remove the client from
490        :type   target: :class:`BackupTarget` or ``str``
491
492        :param  backup_client: The backup client to remove
493        :type   backup_client: :class:`DimensionDataBackupClient` or ``str``
494
495        :rtype: ``bool``
496        """
497        server_id = self._target_to_target_address(target)
498        client_id = self._client_to_client_id(backup_client)
499        response = self.connection.request_with_orgId_api_1(
500            'server/%s/backup/client/%s?disable' % (server_id, client_id),
501            method='GET').object
502        response_code = findtext(response, 'result', GENERAL_NS)
503        return response_code in ['IN_PROGRESS', 'SUCCESS']
504
505    def ex_get_backup_details_for_target(self, target):
506        """
507        Returns a backup details object for a target
508
509        :param  target: The backup target to get details for
510        :type   target: :class:`BackupTarget` or ``str``
511
512        :rtype: :class:`DimensionDataBackupDetails`
513        """
514        if not isinstance(target, BackupTarget):
515            target = self.ex_get_target_by_id(target)
516            if target is None:
517                return
518        response = self.connection.request_with_orgId_api_1(
519            'server/%s/backup' % (target.address),
520            method='GET').object
521        return self._to_backup_details(response, target)
522
523    def ex_list_available_client_types(self, target):
524        """
525        Returns a list of available backup client types
526
527        :param  target: The backup target to list available types for
528        :type   target: :class:`BackupTarget` or ``str``
529
530        :rtype: ``list`` of :class:`DimensionDataBackupClientType`
531        """
532        server_id = self._target_to_target_address(target)
533        response = self.connection.request_with_orgId_api_1(
534            'server/%s/backup/client/type' % (server_id),
535            method='GET').object
536        return self._to_client_types(response)
537
538    def ex_list_available_storage_policies(self, target):
539        """
540        Returns a list of available backup storage policies
541
542        :param  target: The backup target to list available policies for
543        :type   target: :class:`BackupTarget` or ``str``
544
545        :rtype: ``list`` of :class:`DimensionDataBackupStoragePolicy`
546        """
547        server_id = self._target_to_target_address(target)
548        response = self.connection.request_with_orgId_api_1(
549            'server/%s/backup/client/storagePolicy' % (server_id),
550            method='GET').object
551        return self._to_storage_policies(response)
552
553    def ex_list_available_schedule_policies(self, target):
554        """
555        Returns a list of available backup schedule policies
556
557        :param  target: The backup target to list available policies for
558        :type   target: :class:`BackupTarget` or ``str``
559
560        :rtype: ``list`` of :class:`DimensionDataBackupSchedulePolicy`
561        """
562        server_id = self._target_to_target_address(target)
563        response = self.connection.request_with_orgId_api_1(
564            'server/%s/backup/client/schedulePolicy' % (server_id),
565            method='GET').object
566        return self._to_schedule_policies(response)
567
568    def _to_storage_policies(self, object):
569        elements = object.findall(fixxpath('storagePolicy', BACKUP_NS))
570
571        return [self._to_storage_policy(el) for el in elements]
572
573    def _to_storage_policy(self, element):
574        return DimensionDataBackupStoragePolicy(
575            retention_period=int(element.get('retentionPeriodInDays')),
576            name=element.get('name'),
577            secondary_location=element.get('secondaryLocation')
578        )
579
580    def _to_schedule_policies(self, object):
581        elements = object.findall(fixxpath('schedulePolicy', BACKUP_NS))
582
583        return [self._to_schedule_policy(el) for el in elements]
584
585    def _to_schedule_policy(self, element):
586        return DimensionDataBackupSchedulePolicy(
587            name=element.get('name'),
588            description=element.get('description')
589        )
590
591    def _to_client_types(self, object):
592        elements = object.findall(fixxpath('backupClientType', BACKUP_NS))
593
594        return [self._to_client_type(el) for el in elements]
595
596    def _to_client_type(self, element):
597        description = element.get('description')
598        if description is None:
599            description = findtext(element, 'description', BACKUP_NS)
600        return DimensionDataBackupClientType(
601            type=element.get('type'),
602            description=description,
603            is_file_system=bool(element.get('isFileSystem') == 'true')
604        )
605
606    def _to_backup_details(self, object, target):
607        return DimensionDataBackupDetails(
608            asset_id=object.get('assetId'),
609            service_plan=object.get('servicePlan'),
610            status=object.get('state'),
611            clients=self._to_clients(object, target)
612        )
613
614    def _to_clients(self, object, target):
615        elements = object.findall(fixxpath('backupClient', BACKUP_NS))
616
617        return [self._to_client(el, target) for el in elements]
618
619    def _to_client(self, element, target):
620        client_id = element.get('id')
621        return DimensionDataBackupClient(
622            id=client_id,
623            type=self._to_client_type(element),
624            status=element.get('status'),
625            schedule_policy=findtext(element, 'schedulePolicyName', BACKUP_NS),
626            storage_policy=findtext(element, 'storagePolicyName', BACKUP_NS),
627            download_url=findtext(element, 'downloadUrl', BACKUP_NS),
628            running_job=self._to_backup_job(element, target, client_id),
629            alert=self._to_alert(element)
630        )
631
632    def _to_alert(self, element):
633        alert = element.find(fixxpath('alerting', BACKUP_NS))
634        if alert is not None:
635            notify_list = [
636                email_addr.text for email_addr
637                in alert.findall(fixxpath('emailAddress', BACKUP_NS))
638            ]
639            return DimensionDataBackupClientAlert(
640                trigger=element.get('trigger'),
641                notify_list=notify_list
642            )
643        return None
644
645    def _to_backup_job(self, element, target, client_id):
646        running_job = element.find(fixxpath('runningJob', BACKUP_NS))
647        if running_job is not None:
648            return BackupTargetJob(
649                id=running_job.get('id'),
650                status=running_job.get('status'),
651                progress=int(running_job.get('percentageComplete')),
652                driver=self.connection.driver,
653                target=target,
654                extra={'clientId': client_id}
655            )
656        return None
657
658    def _to_targets(self, object):
659        node_elements = object.findall(fixxpath('server', TYPES_URN))
660
661        return [self._to_target(el) for el in node_elements]
662
663    def _to_target(self, element):
664        backup = findall(element, 'backup', TYPES_URN)
665        if len(backup) == 0:
666            return
667        extra = {
668            'description': findtext(element, 'description', TYPES_URN),
669            'sourceImageId': findtext(element, 'sourceImageId', TYPES_URN),
670            'datacenterId': element.get('datacenterId'),
671            'deployedTime': findtext(element, 'createTime', TYPES_URN),
672            'servicePlan': backup[0].get('servicePlan')
673        }
674
675        n = BackupTarget(id=backup[0].get('assetId'),
676                         name=findtext(element, 'name', TYPES_URN),
677                         address=element.get('id'),
678                         driver=self.connection.driver,
679                         type=BackupTargetType.VIRTUAL,
680                         extra=extra)
681        return n
682
683    @staticmethod
684    def _client_to_client_id(backup_client):
685        return dd_object_to_id(backup_client, DimensionDataBackupClient)
686
687    @staticmethod
688    def _target_to_target_address(target):
689        return dd_object_to_id(target, BackupTarget, id_value='address')
690