1"""
2Copyright 2015 Rackspace
3
4Licensed under the Apache License, Version 2.0 (the "License");
5you may not use this file except in compliance with the License.
6You may obtain a copy of the License at
7
8    http://www.apache.org/licenses/LICENSE-2.0
9
10Unless required by applicable law or agreed to in writing, software
11distributed under the License is distributed on an "AS IS" BASIS,
12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13See the License for the specific language governing permissions and
14limitations under the License.
15"""
16import logging
17import os
18
19from tempest.lib.cli import base
20
21from designateclient.functionaltests.config import cfg
22from designateclient.functionaltests.models import FieldValueModel
23from designateclient.functionaltests.models import ListModel
24
25
26LOG = logging.getLogger(__name__)
27
28
29def build_option_string(options):
30    """Format a string of option flags (--key 'value').
31
32    This will quote the values, in case spaces are included.
33    Any values that are None are excluded entirely.
34
35    Usage::
36
37        build_option_string({
38            "--email": "me@example.com",
39            "--name": "example.com."
40            "--ttl": None,
41
42        })
43
44    Returns::
45
46        "--email 'me@example.com' --name 'example.com.'
47    """
48    return " ".join("{0} '{1}'".format(flag, value)
49                    for flag, value in options.items()
50                    if value is not None)
51
52
53def build_flags_string(flags):
54    """Format a string of value-less flags.
55
56    Pass in a dictionary mapping flags to booleans. Those flags set to true
57    are included in the returned string.
58
59    Usage::
60
61        build_flags_string({
62            '--no-ttl': True,
63            '--no-name': False,
64            '--verbose': True,
65        })
66
67    Returns::
68
69        '--no-ttl --verbose'
70    """
71    flags = {flag: is_set for flag, is_set in flags.items() if is_set}
72    return " ".join(flags.keys())
73
74
75class ZoneCommands(object):
76    """This is a mixin that provides zone commands to DesignateCLI"""
77
78    def zone_list(self, *args, **kwargs):
79        return self.parsed_cmd('zone list', ListModel, *args, **kwargs)
80
81    def zone_show(self, id, *args, **kwargs):
82        return self.parsed_cmd('zone show %s' % id, FieldValueModel, *args,
83                               **kwargs)
84
85    def zone_delete(self, id, *args, **kwargs):
86        return self.parsed_cmd('zone delete %s' % id, FieldValueModel, *args,
87                               **kwargs)
88
89    def zone_create(self, name, email=None, ttl=None, description=None,
90                    type=None, masters=None, *args, **kwargs):
91        options_str = build_option_string({
92            "--email": email,
93            "--ttl": ttl,
94            "--description": description,
95            "--masters": masters,
96            "--type": type,
97        })
98        cmd = 'zone create {0} {1}'.format(name, options_str)
99        return self.parsed_cmd(cmd, FieldValueModel, *args, **kwargs)
100
101    def zone_set(self, id, email=None, ttl=None, description=None,
102                 type=None, masters=None, *args, **kwargs):
103        options_str = build_option_string({
104            "--email": email,
105            "--ttl": ttl,
106            "--description": description,
107            "--masters": masters,
108            "--type": type,
109        })
110        cmd = 'zone set {0} {1}'.format(id, options_str)
111        return self.parsed_cmd(cmd, FieldValueModel, *args, **kwargs)
112
113
114class ZoneTransferCommands(object):
115    """A mixin for DesignateCLI to add zone transfer commands"""
116
117    def zone_transfer_request_list(self, *args, **kwargs):
118        cmd = 'zone transfer request list'
119        return self.parsed_cmd(cmd, ListModel, *args, **kwargs)
120
121    def zone_transfer_request_create(self, zone_id, target_project_id=None,
122                                     description=None, *args, **kwargs):
123        options_str = build_option_string({
124            "--target-project-id": target_project_id,
125            "--description": description,
126        })
127        cmd = 'zone transfer request create {0} {1}'.format(
128            zone_id, options_str)
129        return self.parsed_cmd(cmd, FieldValueModel, *args, **kwargs)
130
131    def zone_transfer_request_show(self, id, *args, **kwargs):
132        cmd = 'zone transfer request show {0}'.format(id)
133        return self.parsed_cmd(cmd, FieldValueModel, *args, **kwargs)
134
135    def zone_transfer_request_set(self, id, description=None, *args, **kwargs):
136        options_str = build_option_string({"--description": description})
137        cmd = 'zone transfer request set {0} {1}'.format(options_str, id)
138        return self.parsed_cmd(cmd, FieldValueModel, *args, **kwargs)
139
140    def zone_transfer_request_delete(self, id, *args, **kwargs):
141        cmd = 'zone transfer request delete {0}'.format(id)
142        return self.parsed_cmd(cmd, *args, **kwargs)
143
144    def zone_transfer_accept_request(self, id, key, *args, **kwargs):
145        options_str = build_option_string({
146            "--transfer-id": id,
147            "--key": key,
148        })
149        cmd = 'zone transfer accept request {0}'.format(options_str)
150        return self.parsed_cmd(cmd, *args, **kwargs)
151
152    def zone_transfer_accept_show(self, id, *args, **kwargs):
153        cmd = 'zone transfer accept show {0}'.format(id)
154        return self.parsed_cmd(cmd, FieldValueModel, *args, **kwargs)
155
156    def zone_transfer_accept_list(self, *args, **kwargs):
157        cmd = 'zone transfer accept list'
158        return self.parsed_cmd(cmd, ListModel, *args, **kwargs)
159
160
161class ZoneExportCommands(object):
162    """A mixin for DesignateCLI to add zone export commands"""
163
164    def zone_export_list(self, *args, **kwargs):
165        cmd = 'zone export list'
166        return self.parsed_cmd(cmd, ListModel, *args, **kwargs)
167
168    def zone_export_create(self, zone_id, *args, **kwargs):
169        cmd = 'zone export create {0}'.format(
170            zone_id)
171        return self.parsed_cmd(cmd, FieldValueModel, *args, **kwargs)
172
173    def zone_export_show(self, zone_export_id, *args, **kwargs):
174        cmd = 'zone export show {0}'.format(zone_export_id)
175        return self.parsed_cmd(cmd, FieldValueModel, *args, **kwargs)
176
177    def zone_export_delete(self, zone_export_id, *args, **kwargs):
178        cmd = 'zone export delete {0}'.format(zone_export_id)
179        return self.parsed_cmd(cmd, *args, **kwargs)
180
181    def zone_export_showfile(self, zone_export_id, *args, **kwargs):
182        cmd = 'zone export showfile {0}'.format(zone_export_id)
183        return self.parsed_cmd(cmd, FieldValueModel, *args, **kwargs)
184
185
186class ZoneImportCommands(object):
187    """A mixin for DesignateCLI to add zone import commands"""
188
189    def zone_import_list(self, *args, **kwargs):
190        cmd = 'zone import list'
191        return self.parsed_cmd(cmd, ListModel, *args, **kwargs)
192
193    def zone_import_create(self, zone_file_path, *args, **kwargs):
194        cmd = 'zone import create {0}'.format(zone_file_path)
195        return self.parsed_cmd(cmd, FieldValueModel, *args, **kwargs)
196
197    def zone_import_show(self, zone_import_id, *args, **kwargs):
198        cmd = 'zone import show {0}'.format(zone_import_id)
199        return self.parsed_cmd(cmd, FieldValueModel, *args, **kwargs)
200
201    def zone_import_delete(self, zone_import_id, *args, **kwargs):
202        cmd = 'zone import delete {0}'.format(zone_import_id)
203        return self.parsed_cmd(cmd, *args, **kwargs)
204
205
206class RecordsetCommands(object):
207
208    def recordset_show(self, zone_id, id, *args, **kwargs):
209        cmd = 'recordset show {0} {1}'.format(zone_id, id)
210        return self.parsed_cmd(cmd, FieldValueModel, *args, **kwargs)
211
212    def recordset_list(self, zone_id, *args, **kwargs):
213        cmd = 'recordset list {0}'.format(zone_id)
214        return self.parsed_cmd(cmd, ListModel, *args, **kwargs)
215
216    def recordset_create(self, zone_id, name, records=None, type=None,
217                         description=None, ttl=None, *args, **kwargs):
218        options_str = build_option_string({
219            '--records': records,
220            '--type': type,
221            '--description': description,
222            '--ttl': ttl,
223        })
224        cmd = 'recordset create {0} {1} {2}'.format(zone_id, name, options_str)
225        return self.parsed_cmd(cmd, FieldValueModel, *args, **kwargs)
226
227    def recordset_set(self, zone_id, id, records=None, type=None,
228                      description=None, ttl=None, no_description=False,
229                      no_ttl=False, *args, **kwargs):
230        options_str = build_option_string({
231            '--records': records,
232            '--type': type,
233            '--description': description,
234            '--ttl': ttl,
235        })
236        flags_str = build_flags_string({
237            '--no-description': no_description,
238            '--no-ttl': no_ttl,
239        })
240        cmd = 'recordset set {0} {1} {2} {3}'.format(
241            zone_id, id, flags_str, options_str)
242        return self.parsed_cmd(cmd, FieldValueModel, *args, **kwargs)
243
244    def recordset_delete(self, zone_id, id, *args, **kwargs):
245        cmd = 'recordset delete {0} {1}'.format(zone_id, id)
246        return self.parsed_cmd(cmd, FieldValueModel, *args, **kwargs)
247
248
249class TLDCommands(object):
250
251    def tld_list(self, *args, **kwargs):
252        return self.parsed_cmd('tld list', ListModel, *args, **kwargs)
253
254    def tld_show(self, id, *args, **kwargs):
255        return self.parsed_cmd('tld show {0}'.format(id), FieldValueModel,
256                               *args, **kwargs)
257
258    def tld_delete(self, id, *args, **kwargs):
259        return self.parsed_cmd('tld delete {0}'.format(id), *args, **kwargs)
260
261    def tld_create(self, name, description=None, *args, **kwargs):
262        options_str = build_option_string({
263            '--name': name,
264            '--description': description,
265        })
266        cmd = 'tld create {0}'.format(options_str)
267        return self.parsed_cmd(cmd, FieldValueModel, *args, **kwargs)
268
269    def tld_set(self, id, name=None, description=None, no_description=False,
270                *args, **kwargs):
271        options_str = build_option_string({
272            '--name': name,
273            '--description': description,
274        })
275        flags_str = build_flags_string({'--no-description': no_description})
276        cmd = 'tld set {0} {1} {2}'.format(id, options_str, flags_str)
277        return self.parsed_cmd(cmd, FieldValueModel, *args, **kwargs)
278
279
280class TSIGKeyCommands(object):
281    def tsigkey_list(self, *args, **kwargs):
282        return self.parsed_cmd('tsigkey list', ListModel, *args, **kwargs)
283
284    def tsigkey_show(self, id, *args, **kwargs):
285        return self.parsed_cmd('tsigkey show {0}'.format(id), FieldValueModel,
286                               *args, **kwargs)
287
288    def tsigkey_delete(self, id, *args, **kwargs):
289        return self.parsed_cmd('tsigkey delete {0}'.format(id), *args,
290                               **kwargs)
291
292    def tsigkey_create(self, name, algorithm, secret, scope, resource_id,
293                       *args, **kwargs):
294        options_str = build_option_string({
295            '--name': name,
296            '--algorithm': algorithm,
297            '--secret': secret,
298            '--scope': scope,
299            '--resource-id': resource_id,
300        })
301        cmd = 'tsigkey create {0}'.format(options_str)
302        return self.parsed_cmd(cmd, FieldValueModel, *args, **kwargs)
303
304    def tsigkey_set(self, id, name=None, algorithm=None, secret=None,
305                    scope=None,
306                    *args, **kwargs):
307        options_str = build_option_string({
308            '--name': name,
309            '--algorithm': algorithm,
310            '--secret': secret,
311            '--scope': scope,
312        })
313        cmd = 'tsigkey set {0} {1}'.format(id, options_str)
314        return self.parsed_cmd(cmd, FieldValueModel, *args, **kwargs)
315
316
317class BlacklistCommands(object):
318    def zone_blacklist_list(self, *args, **kwargs):
319        cmd = 'zone blacklist list'
320        return self.parsed_cmd(cmd, ListModel, *args, **kwargs)
321
322    def zone_blacklist_create(self, pattern, description=None, *args,
323                              **kwargs):
324        options_str = build_option_string({
325            '--pattern': pattern,
326            '--description': description,
327        })
328        cmd = 'zone blacklist create {0}'.format(options_str)
329        return self.parsed_cmd(cmd, FieldValueModel, *args, **kwargs)
330
331    def zone_blacklist_set(self, id, pattern=None, description=None,
332                           no_description=False, *args, **kwargs):
333        options_str = build_option_string({
334            '--pattern': pattern,
335            '--description': description,
336        })
337        flags_str = build_flags_string({'--no-description': no_description})
338        cmd = 'zone blacklist set {0} {1} {2}'.format(id, options_str,
339                                                      flags_str)
340        return self.parsed_cmd(cmd, FieldValueModel, *args, **kwargs)
341
342    def zone_blacklist_show(self, id, *args, **kwargs):
343        cmd = 'zone blacklist show {0}'.format(id)
344        return self.parsed_cmd(cmd, FieldValueModel, *args, **kwargs)
345
346    def zone_blacklist_delete(self, id, *args, **kwargs):
347        cmd = 'zone blacklist delete {0}'.format(id)
348        return self.parsed_cmd(cmd, FieldValueModel, *args, **kwargs)
349
350
351class DesignateCLI(base.CLIClient, ZoneCommands, ZoneTransferCommands,
352                   ZoneExportCommands, ZoneImportCommands, RecordsetCommands,
353                   TLDCommands, BlacklistCommands):
354
355    # instantiate this once to minimize requests to keystone
356    _CLIENTS = None
357
358    def __init__(self, *args, **kwargs):
359        super(DesignateCLI, self).__init__(*args, **kwargs)
360        # grab the project id. this is used for zone transfer requests
361        resp = FieldValueModel(self.openstack('token issue'))
362        self.project_id = resp.project_id
363
364    @property
365    def using_auth_override(self):
366        return bool(cfg.CONF.identity.override_endpoint)
367
368    @classmethod
369    def get_clients(cls):
370        if not cls._CLIENTS:
371            cls._init_clients()
372        return cls._CLIENTS
373
374    @classmethod
375    def _init_clients(cls):
376        cls._CLIENTS = {
377            'default': DesignateCLI(
378                cli_dir=cfg.CONF.designateclient.directory,
379                username=cfg.CONF.identity.username,
380                password=cfg.CONF.identity.password,
381                tenant_name=cfg.CONF.identity.tenant_name,
382                uri=cfg.CONF.identity.uri,
383            ),
384            'alt': DesignateCLI(
385                cli_dir=cfg.CONF.designateclient.directory,
386                username=cfg.CONF.identity.alt_username,
387                password=cfg.CONF.identity.alt_password,
388                tenant_name=cfg.CONF.identity.alt_tenant_name,
389                uri=cfg.CONF.identity.uri,
390            ),
391            'admin': DesignateCLI(
392                cli_dir=cfg.CONF.designateclient.directory,
393                username=cfg.CONF.identity.admin_username,
394                password=cfg.CONF.identity.admin_password,
395                tenant_name=cfg.CONF.identity.admin_tenant_name,
396                uri=cfg.CONF.identity.uri,
397            )
398        }
399
400    @classmethod
401    def as_user(self, user):
402        clients = self.get_clients()
403        if user in clients:
404            return clients[user]
405        raise Exception("User '{0}' does not exist".format(user))
406
407    def parsed_cmd(self, cmd, model=None, *args, **kwargs):
408        if self.using_auth_override:
409            # use --os-url and --os-token
410            func = self._openstack_noauth
411        else:
412            # use --os-username --os-tenant-name --os-password --os-auth-url
413            func = self.openstack
414
415        out = func(cmd, *args, **kwargs)
416        LOG.debug(out)
417        if model is not None:
418            return model(out)
419        return out
420
421    def _openstack_noauth(self, cmd, *args, **kwargs):
422        exe = os.path.join(cfg.CONF.designateclient.directory, 'openstack')
423        options = build_option_string({
424            '--os-url': cfg.CONF.identity.override_endpoint,
425            '--os-token': cfg.CONF.identity.override_token,
426        })
427        cmd = options + " " + cmd
428        return base.execute(exe, cmd, *args, **kwargs)
429