tuskar-ui/tuskar_ui/api/tuskar.py

559 lines
18 KiB
Python

# 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_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(cls, request, plan):
roles_by_image = {}
for role in Role.list(request):
image = plan.parameter_value(role.image_parameter_name)
if image in roles_by_image:
roles_by_image[image].append(role)
else:
roles_by_image[image] = [role]
return roles_by_image
@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(request, plan)
try:
return roles[image.name]
except KeyError:
return []
@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_parameter_name(self):
return self.parameter_prefix + 'Image'
@property
def flavor_parameter_name(self):
return self.parameter_prefix + 'Flavor'
def image(self, plan):
image_name = plan.parameter_value(self.image_parameter_name)
if image_name:
try:
return glance.image_list_detailed(
self._request, filters={'name': image_name})[0][0]
except (glance_exceptions.HTTPNotFound, IndexError):
LOG.error("Couldn't obtain image with name %s" % image_name)
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