summaryrefslogtreecommitdiff
path: root/tuskar_ui/api/tuskar.py
blob: 85d293eba17298833b3b555dd895a2c24be8ae3c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
#    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.

import logging
import random
import string

from django.conf import settings
from django.utils.translation import ugettext_lazy as _
from glanceclient import exc as glance_exceptions
from horizon.utils import memoized
from openstack_dashboard.api import base
from openstack_dashboard.api import glance
from openstack_dashboard.api import neutron
from os_cloud_config import keystone_pki
from tuskarclient import client as tuskar_client

from tuskar_ui.api import flavor
from tuskar_ui.cached_property import cached_property  # noqa
from tuskar_ui.handle_errors import handle_errors  # noqa

LOG = logging.getLogger(__name__)
MASTER_TEMPLATE_NAME = 'plan.yaml'
ENVIRONMENT_NAME = 'environment.yaml'
TUSKAR_SERVICE = 'management'

SSL_HIDDEN_PARAMS = ('SSLCertificate', 'SSLKey')
KEYSTONE_CERTIFICATE_PARAMS = (
    'KeystoneSigningCertificate', 'KeystoneCACertificate',
    'KeystoneSigningKey')


@memoized.memoized
def tuskarclient(request, password=None):
    api_version = "2"
    insecure = getattr(settings, 'OPENSTACK_SSL_NO_VERIFY', False)
    ca_file = getattr(settings, 'OPENSTACK_SSL_CACERT', None)
    endpoint = base.url_for(request, TUSKAR_SERVICE)

    LOG.debug('tuskarclient connection created using token "%s" and url "%s"' %
              (request.user.token.id, endpoint))

    client = tuskar_client.get_client(api_version,
                                      tuskar_url=endpoint,
                                      insecure=insecure,
                                      ca_file=ca_file,
                                      username=request.user.username,
                                      password=password,
                                      os_auth_token=request.user.token.id)
    return client


def password_generator(size=40, chars=(string.ascii_uppercase +
                                       string.ascii_lowercase +
                                       string.digits)):
    return ''.join(random.choice(chars) for _ in range(size))


def strip_prefix(parameter_name):
    return parameter_name.split('::', 1)[-1]


def _is_blank(parameter):
    return not parameter['value'] or parameter['value'] == 'unset'


def _should_generate_password(parameter):
    # TODO(lsmola) Filter out SSL params for now. Once it will be generated
    # in TripleO add it here too. Note: this will also affect how endpoints are
    # created
    key = parameter['name']
    return all([
        parameter['hidden'],
        _is_blank(parameter),
        strip_prefix(key) not in SSL_HIDDEN_PARAMS,
        strip_prefix(key) not in KEYSTONE_CERTIFICATE_PARAMS,
        key != 'SnmpdReadonlyUserPassword',
    ])


def _should_generate_keystone_cert(parameter):
    return all([
        strip_prefix(parameter['name']) in KEYSTONE_CERTIFICATE_PARAMS,
        _is_blank(parameter),
    ])


def _should_generate_neutron_control_plane(parameter):
    return all([
        strip_prefix(parameter['name']) == 'NeutronControlPlaneID',
        _is_blank(parameter),
    ])


class Plan(base.APIResourceWrapper):
    _attrs = ('uuid', 'name', 'description', 'created_at', 'modified_at',
              'roles', 'parameters')

    def __init__(self, apiresource, request=None):
        super(Plan, self).__init__(apiresource)
        self._request = request

    @classmethod
    def create(cls, request, name, description):
        """Create a Plan in Tuskar

        :param request: request object
        :type  request: django.http.HttpRequest

        :param name: plan name
        :type  name: string

        :param description: plan description
        :type  description: string

        :return: the created Plan object
        :rtype:  tuskar_ui.api.tuskar.Plan
        """
        plan = tuskarclient(request).plans.create(name=name,
                                                  description=description)
        return cls(plan, request=request)

    @classmethod
    def patch(cls, request, plan_id, parameters):
        """Update a Plan in Tuskar

        :param request: request object
        :type  request: django.http.HttpRequest

        :param plan_id: id of the plan we want to update
        :type  plan_id: string

        :param parameters: new values for the plan's parameters
        :type  parameters: dict

        :return: the updated Plan object
        :rtype:  tuskar_ui.api.tuskar.Plan
        """
        parameter_list = [{
            'name': unicode(name),
            'value': unicode(value),
        } for (name, value) in parameters.items()]
        plan = tuskarclient(request).plans.patch(plan_id, parameter_list)
        return cls(plan, request=request)

    @classmethod
    @memoized.memoized
    def list(cls, request):
        """Return a list of Plans in Tuskar

        :param request: request object
        :type  request: django.http.HttpRequest

        :return: list of Plans, or an empty list if there are none
        :rtype:  list of tuskar_ui.api.tuskar.Plan
        """
        plans = tuskarclient(request).plans.list()
        return [cls(plan, request=request) for plan in plans]

    @classmethod
    @handle_errors(_("Unable to retrieve plan"))
    def get(cls, request, plan_id):
        """Return the Plan that matches the ID

        :param request: request object
        :type  request: django.http.HttpRequest

        :param plan_id: id of Plan to be retrieved
        :type  plan_id: int

        :return: matching Plan, or None if no Plan matches
                 the ID
        :rtype:  tuskar_ui.api.tuskar.Plan
        """
        plan = tuskarclient(request).plans.get(plan_uuid=plan_id)
        return cls(plan, request=request)

    # TODO(lsmola) before will will support multiple overclouds, we
    # can work only with overcloud that is named overcloud. Delete
    # this once we have more overclouds. Till then, this is the overcloud
    # that rules them all.
    # This is how API supports it now, so we have to have it this way.
    # Also till Overcloud workflow is done properly, we have to work
    # with situations that overcloud is deleted, but stack is still
    # there. So overcloud will pretend to exist when stack exist.
    @classmethod
    def get_the_plan(cls, request):
        plan_list = cls.list(request)
        for plan in plan_list:
            return plan
        # if plan doesn't exist, create it
        plan = cls.create(request, 'overcloud', 'overcloud')
        return plan

    @classmethod
    def delete(cls, request, plan_id):
        """Delete a Plan

        :param request: request object
        :type  request: django.http.HttpRequest

        :param plan_id: plan id
        :type  plan_id: int
        """
        tuskarclient(request).plans.delete(plan_uuid=plan_id)

    @cached_property
    def role_list(self):
        return [Role.get(self._request, role.uuid)
                for role in self.roles]

    @cached_property
    def _roles_by_name(self):
        return dict((role.name, role) for role in self.role_list)

    def get_role_by_name(self, role_name):
        """Get the role with the given name."""
        return self._roles_by_name[role_name]

    def get_role_node_count(self, role):
        """Get the node count for the given role."""
        return int(self.parameter_value(role.node_count_parameter_name,
                                        0) or 0)

    @cached_property
    def templates(self):
        return tuskarclient(self._request).plans.templates(self.uuid)

    @cached_property
    def master_template(self):
        return self.templates.get(MASTER_TEMPLATE_NAME, '')

    @cached_property
    def environment(self):
        return self.templates.get(ENVIRONMENT_NAME, '')

    @cached_property
    def provider_resource_templates(self):
        template_dict = dict(self.templates)
        del template_dict[MASTER_TEMPLATE_NAME]
        del template_dict[ENVIRONMENT_NAME]
        return template_dict

    def parameter_list(self, include_key_parameters=True):
        params = self.parameters
        if not include_key_parameters:
            key_params = []
            for role in self.role_list:
                key_params.extend([role.node_count_parameter_name,
                                   role.image_id_parameter_name,
                                   role.flavor_parameter_name])
            params = [p for p in params if p['name'] not in key_params]
        return [Parameter(p, plan=self) for p in params]

    def parameter(self, param_name):
        for parameter in self.parameters:
            if parameter['name'] == param_name:
                return Parameter(parameter, plan=self)

    def parameter_value(self, param_name, default=None):
        parameter = self.parameter(param_name)
        if parameter is not None:
            return parameter.value
        return default

    def list_generated_parameters(self, with_prefix=True):
        if with_prefix:
            key_format = lambda key: key
        else:
            key_format = strip_prefix

        # Get all password like parameters
        return dict(
            (key_format(parameter['name']), parameter)
            for parameter in self.parameter_list()
            if any([
                _should_generate_password(parameter),
                _should_generate_keystone_cert(parameter),
                _should_generate_neutron_control_plane(parameter),
            ])
        )

    def _make_keystone_certificates(self, wanted_generated_params):
        generated_params = {}
        for cert_param in KEYSTONE_CERTIFICATE_PARAMS:
            if cert_param in wanted_generated_params.keys():
                # If one of the keystone certificates is not set, we have
                # to generate all of them.
                generate_certificates = True
                break
        else:
            generate_certificates = False

        # Generate keystone certificates
        if generate_certificates:
            ca_key_pem, ca_cert_pem = keystone_pki.create_ca_pair()
            signing_key_pem, signing_cert_pem = (
                keystone_pki.create_signing_pair(ca_key_pem, ca_cert_pem))
            generated_params['KeystoneSigningCertificate'] = (
                signing_cert_pem)
            generated_params['KeystoneCACertificate'] = ca_cert_pem
            generated_params['KeystoneSigningKey'] = signing_key_pem
        return generated_params

    def make_generated_parameters(self):
        wanted_generated_params = self.list_generated_parameters(
            with_prefix=False)

        # Generate keystone certificates
        generated_params = self._make_keystone_certificates(
            wanted_generated_params)

        # Generate passwords and control plane id
        for (key, param) in wanted_generated_params.items():
            if _should_generate_password(param):
                generated_params[key] = password_generator()
            elif _should_generate_neutron_control_plane(param):
                generated_params[key] = neutron.network_list(
                    self._request, name='ctlplane')[0].id

        # Fill all the Tuskar parameters with generated content. There are
        # parameters that has just different prefix, such parameters should
        # have the same values.
        wanted_prefixed_params = self.list_generated_parameters(
            with_prefix=True)
        tuskar_params = {}

        for (key, param) in wanted_prefixed_params.items():
            tuskar_params[key] = generated_params[strip_prefix(key)]

        return tuskar_params

    @property
    def id(self):
        return self.uuid


class Role(base.APIResourceWrapper):
    _attrs = ('uuid', 'name', 'version', 'description', 'created')

    def __init__(self, apiresource, request=None):
        super(Role, self).__init__(apiresource)
        self._request = request

    @classmethod
    @memoized.memoized
    @handle_errors(_("Unable to retrieve overcloud roles"), [])
    def list(cls, request):
        """Return a list of Overcloud Roles in Tuskar

        :param request: request object
        :type  request: django.http.HttpRequest

        :return: list of Overcloud Roles, or an empty list if there
                 are none
        :rtype:  list of tuskar_ui.api.tuskar.Role
        """
        roles = tuskarclient(request).roles.list()
        return [cls(role, request=request) for role in roles]

    @classmethod
    @memoized.memoized
    @handle_errors(_("Unable to retrieve overcloud role"))
    def get(cls, request, role_id):
        """Return the Tuskar Role that matches the ID

        :param request: request object
        :type  request: django.http.HttpRequest

        :param role_id: ID of Role to be retrieved
        :type  role_id: int

        :return: matching Role, or None if no matching
                 Role can be found
        :rtype:  tuskar_ui.api.tuskar.Role
        """
        for role in Role.list(request):
            if role.uuid == role_id:
                return role

    @classmethod
    @memoized.memoized
    def _roles_by_image_id(cls, request, plan):
        return {plan.parameter_value(role.image_id_parameter_name): role
                for role in Role.list(request)}

    @classmethod
    @handle_errors(_("Unable to retrieve overcloud role"))
    def get_by_image(cls, request, plan, image):
        """Return the Role whose ImageID parameter matches the image.

        :param request: request object
        :type  request: django.http.HttpRequest

        :param plan: associated plan to check against
        :type  plan: Plan

        :param image: image to be matched
        :type  image: Image

        :return: matching Role, or None if no matching
                 Role can be found
        :rtype:  tuskar_ui.api.tuskar.Role
        """
        roles = cls._roles_by_image_id(request, plan)
        try:
            return roles[image.id]
        except KeyError:
            return None

    @classmethod
    @memoized.memoized
    def _roles_by_resource_type(cls, request):
        return {role.provider_resource_type: role
                for role in Role.list(request)}

    @classmethod
    @handle_errors(_("Unable to retrieve overcloud role"))
    def get_by_resource_type(cls, request, resource_type):
        roles = cls._roles_by_resource_type(request)
        try:
            return roles[resource_type]
        except KeyError:
            return None

    @property
    def provider_resource_type(self):
        return "Tuskar::{0}-{1}".format(self.name, self.version)

    @property
    def parameter_prefix(self):
        return "{0}-{1}::".format(self.name, self.version)

    @property
    def node_count_parameter_name(self):
        return self.parameter_prefix + 'count'

    @property
    def image_id_parameter_name(self):
        return self.parameter_prefix + 'Image'

    @property
    def flavor_parameter_name(self):
        return self.parameter_prefix + 'Flavor'

    def image(self, plan):
        image_id = plan.parameter_value(self.image_id_parameter_name)
        if image_id:
            try:
                return glance.image_get(self._request, image_id)
            except glance_exceptions.HTTPNotFound:
                LOG.error("Couldn't obtain image with id %s" % image_id)
                return None

    def flavor(self, plan):
        flavor_name = plan.parameter_value(
            self.flavor_parameter_name)
        if flavor_name:
            return flavor.Flavor.get_by_name(self._request, flavor_name)

    def parameter_list(self, plan):
        return [p for p in plan.parameter_list() if self == p.role]

    def is_valid_for_deployment(self, plan):
        node_count = plan.get_role_node_count(self)
        pending_required_params = list(Parameter.pending_parameters(
            Parameter.required_parameters(self.parameter_list(plan))))
        return not (
            self.image(plan) is None or
            (node_count and self.flavor(plan) is None) or
            pending_required_params
        )

    @property
    def id(self):
        return self.uuid


class Parameter(base.APIDictWrapper):

    _attrs = ['name', 'value', 'default', 'description', 'hidden', 'label',
              'parameter_type', 'constraints']

    def __init__(self, apidict, plan=None):
        super(Parameter, self).__init__(apidict)
        self._plan = plan

    @property
    def stripped_name(self):
        return strip_prefix(self.name)

    @property
    def plan(self):
        return self._plan

    @property
    def role(self):
        if self.plan:
            for role in self.plan.role_list:
                if self.name.startswith(role.parameter_prefix):
                    return role

    def is_required(self):
        """Boolean: True if parameter is required, False otherwise."""
        return self.default is None

    def get_constraint_by_type(self, constraint_type):
        """Returns parameter constraint by it's type.

        For available constraint types see HOT Spec:
        http://docs.openstack.org/developer/heat/template_guide/hot_spec.html
        """

        constraints_of_type = [c for c in self.constraints
                               if c['constraint_type'] == constraint_type]
        if constraints_of_type:
            return constraints_of_type[0]
        else:
            return None

    @staticmethod
    def required_parameters(parameters):
        """Yields parameters which are required."""
        for parameter in parameters:
            if parameter.is_required():
                yield parameter

    @staticmethod
    def pending_parameters(parameters):
        """Yields parameters which don't have value set."""
        for parameter in parameters:
            if not parameter.value:
                yield parameter

    @staticmethod
    def global_parameters(parameters):
        """Yields parameters with name without role prefix."""
        for parameter in parameters:
            if '::' not in parameter.name:
                yield parameter