
Currently error handling in Tuskar-UI involves a lot of repeated code, as the same pattern is used everywhere and the same API calls tend to be handled in the same way no matter where they appear. This patch introduces a decorator that can be used to add default error handling to all API calls that take a request as a parameter. That default error handling can be disabled or modified using the special parameters passed to the call, all of which start with "_error" to avoid conflicts. Change-Id: Ie441789c471deeb6d64267bf4424f5661ef073af
849 lines
29 KiB
Python
849 lines
29 KiB
Python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
|
|
|
# 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 django.conf
|
|
import heatclient
|
|
import logging
|
|
|
|
from django.utils.translation import ugettext_lazy as _
|
|
from horizon.utils import memoized
|
|
from novaclient.v1_1.contrib import baremetal
|
|
from openstack_dashboard.api import base
|
|
from openstack_dashboard.api import glance
|
|
from openstack_dashboard.api import heat
|
|
from openstack_dashboard.api import nova
|
|
from openstack_dashboard.test.test_data import utils
|
|
from tuskarclient.v1 import client as tuskar_client
|
|
|
|
from tuskar_ui.cached_property import cached_property # noqa
|
|
from tuskar_ui.handle_errors import handle_errors # noqa
|
|
from tuskar_ui.test.test_data import tuskar_data
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
TUSKAR_ENDPOINT_URL = getattr(django.conf.settings, 'TUSKAR_ENDPOINT_URL')
|
|
|
|
|
|
def baremetalclient(request):
|
|
nc = nova.novaclient(request)
|
|
return baremetal.BareMetalNodeManager(nc)
|
|
|
|
|
|
# TODO(Tzu-Mainn Chen): remove test data when possible
|
|
def test_data():
|
|
test_data = utils.TestDataContainer()
|
|
tuskar_data.data(test_data)
|
|
return test_data
|
|
|
|
|
|
# FIXME: request isn't used right in the tuskar client right now,
|
|
# but looking at other clients, it seems like it will be in the future
|
|
def tuskarclient(request):
|
|
c = tuskar_client.Client(TUSKAR_ENDPOINT_URL)
|
|
return c
|
|
|
|
|
|
def list_to_dict(object_list, key_attribute='id'):
|
|
"""Converts an object list to a dict
|
|
|
|
:param object_list: list of objects to be put into a dict
|
|
:type object_list: list
|
|
|
|
:param key_attribute: object attribute used as index by dict
|
|
:type key_attribute: str
|
|
|
|
:return: dict containing the objects in the list
|
|
:rtype: dict
|
|
"""
|
|
return dict((getattr(o, key_attribute), o) for o in object_list)
|
|
|
|
|
|
# FIXME(lsmola) This should be done in Horizon, they don't have caching
|
|
@memoized.memoized
|
|
def image_get(request, image_id):
|
|
"""Returns an Image object with metadata
|
|
|
|
Returns an Image object populated with metadata for image
|
|
with supplied identifier.
|
|
|
|
:param image_id: list of objects to be put into a dict
|
|
:type object_list: list
|
|
|
|
:return: object
|
|
:rtype: glanceclient.v1.images.Image
|
|
"""
|
|
image = glance.image_get(request, image_id)
|
|
return image
|
|
|
|
|
|
class NodeProfile(object):
|
|
|
|
def __init__(self, flavor):
|
|
"""Construct node profile by wrapping flavor
|
|
|
|
:param flavor: Nova flavor
|
|
:type flavor: novaclient.v1_1.flavors.Flavor
|
|
"""
|
|
self._flavor = flavor
|
|
|
|
def __getattr__(self, name):
|
|
return getattr(self._flavor, name)
|
|
|
|
@cached_property
|
|
def extras_dict(self):
|
|
"""Return extra parameters of node profile
|
|
|
|
:return: Nova flavor keys
|
|
:rtype: dict
|
|
"""
|
|
return self._flavor.get_keys()
|
|
|
|
@property
|
|
def cpu_arch(self):
|
|
return self.extras_dict.get('cpu_arch', '')
|
|
|
|
@property
|
|
def kernel_image_id(self):
|
|
return self.extras_dict.get('baremetal:deploy_kernel_id', '')
|
|
|
|
@property
|
|
def ramdisk_image_id(self):
|
|
return self.extras_dict.get('baremetal:deploy_ramdisk_id', '')
|
|
|
|
@classmethod
|
|
def create(cls, request, name, memory, vcpus, disk, cpu_arch,
|
|
kernel_image_id, ramdisk_image_id):
|
|
extras_dict = {'cpu_arch': cpu_arch,
|
|
'baremetal:deploy_kernel_id': kernel_image_id,
|
|
'baremetal:deploy_ramdisk_id': ramdisk_image_id}
|
|
return cls(nova.flavor_create(request, name, memory, vcpus, disk,
|
|
metadata=extras_dict))
|
|
|
|
@classmethod
|
|
def get(cls, request, node_profile_id):
|
|
return cls(nova.flavor_get(request, node_profile_id))
|
|
|
|
@classmethod
|
|
@handle_errors(_("Unable to retrieve node profile list."), [])
|
|
def list(cls, request):
|
|
return [cls(item) for item in nova.flavor_list(request)]
|
|
|
|
@classmethod
|
|
@memoized.memoized
|
|
@handle_errors(_("Unable to retrieve existing servers list."), [])
|
|
def list_deployed_ids(cls, request):
|
|
"""Get and memoize ID's of deployed node profiles."""
|
|
servers = nova.server_list(request)[0]
|
|
return set(server.flavor['id'] for server in servers)
|
|
|
|
|
|
class Overcloud(base.APIResourceWrapper):
|
|
_attrs = ('id', 'stack_id', 'name', 'description', 'counts', 'attributes')
|
|
|
|
def __init__(self, apiresource, request=None):
|
|
super(Overcloud, self).__init__(apiresource)
|
|
self._request = request
|
|
|
|
@classmethod
|
|
def create(cls, request, overcloud_sizing, overcloud_configuration):
|
|
"""Create an Overcloud in Tuskar
|
|
|
|
:param request: request object
|
|
:type request: django.http.HttpRequest
|
|
|
|
:param overcloud_sizing: overcloud sizing information with structure
|
|
{('overcloud_role_id',
|
|
'flavor_name'): count, ...}
|
|
:type overcloud_sizing: dict
|
|
|
|
:param overcloud_configuration: overcloud configuration with structure
|
|
{'key': 'value', ...}
|
|
:type overcloud_configuration: dict
|
|
|
|
:return: the created Overcloud object
|
|
:rtype: tuskar_ui.api.Overcloud
|
|
"""
|
|
# TODO(lsmola) for now we have to transform the sizing to simpler
|
|
# format, till API will accept the more complex with flavors,
|
|
# then we delete this
|
|
transformed_sizing = [{
|
|
'overcloud_role_id': role,
|
|
'num_nodes': sizing,
|
|
} for (role, flavor), sizing in overcloud_sizing.items()]
|
|
|
|
overcloud = tuskarclient(request).overclouds.create(
|
|
name='overcloud', description="Openstack cloud providing VMs",
|
|
counts=transformed_sizing, attributes=overcloud_configuration)
|
|
|
|
return cls(overcloud, request=request)
|
|
|
|
@classmethod
|
|
def list(cls, request):
|
|
"""Return a list of Overclouds in Tuskar
|
|
|
|
:param request: request object
|
|
:type request: django.http.HttpRequest
|
|
|
|
:return: list of Overclouds, or an empty list if there are none
|
|
:rtype: list of tuskar_ui.api.Overcloud
|
|
"""
|
|
ocs = tuskarclient(request).overclouds.list()
|
|
|
|
return [cls(oc, request=request) for oc in ocs]
|
|
|
|
@classmethod
|
|
@handle_errors(_("Unable to retrieve deployment"))
|
|
def get(cls, request, overcloud_id):
|
|
"""Return the Tuskar Overcloud that matches the ID
|
|
|
|
:param request: request object
|
|
:type request: django.http.HttpRequest
|
|
|
|
:param overcloud_id: ID of Overcloud to be retrieved
|
|
:type overcloud_id: int
|
|
|
|
:return: matching Overcloud, or None if no Overcloud matches
|
|
the ID
|
|
:rtype: tuskar_ui.api.Overcloud
|
|
"""
|
|
# FIXME(lsmola) hack for Icehouse, only one Overcloud is allowed
|
|
# TODO(lsmola) uncomment when possible
|
|
# overcloud = tuskarclient(request).overclouds.get(overcloud_id)
|
|
# return cls(overcloud, request=request)
|
|
return cls.get_the_overcloud(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_overcloud(cls, request):
|
|
overcloud_list = cls.list(request)
|
|
for overcloud in overcloud_list:
|
|
if overcloud.name == 'overcloud':
|
|
return overcloud
|
|
|
|
the_overcloud = cls(object(), request=request)
|
|
# I need to mock attributes of overcloud that is being deleted.
|
|
the_overcloud.id = "deleting_in_progress"
|
|
|
|
if the_overcloud.stack and the_overcloud.is_deleting:
|
|
return the_overcloud
|
|
else:
|
|
raise heatclient.exc.HTTPNotFound()
|
|
|
|
@classmethod
|
|
def delete(cls, request, overcloud_id):
|
|
"""Create an Overcloud in Tuskar
|
|
|
|
:param request: request object
|
|
:type request: django.http.HttpRequest
|
|
|
|
:param overcloud_id: overcloud id
|
|
:type overcloud_id: int
|
|
"""
|
|
tuskarclient(request).overclouds.delete(overcloud_id)
|
|
|
|
@cached_property
|
|
def stack(self):
|
|
"""Return the Heat Stack associated with this Overcloud
|
|
|
|
:return: Heat Stack associated with this Overcloud; or None
|
|
if no Stack is associated, or no Stack can be
|
|
found
|
|
:rtype: heatclient.v1.stacks.Stack or None
|
|
"""
|
|
# TODO(lsmola) load it properly, once the API has finished workflow
|
|
# and for example there can't be a situation when I delete Overcloud
|
|
# but Stack is still deleting. So the Overcloud will represent the
|
|
# state of all inner entities and operations correctly.
|
|
# Then also delete the try/except, it should not be caught on this
|
|
# level.
|
|
return heat.stack_get(self._request, 'overcloud')
|
|
|
|
@cached_property
|
|
def stack_events(self):
|
|
"""Return the Heat Events associated with this Overcloud
|
|
|
|
:return: list of Heat Events associated with this Overcloud;
|
|
or an empty list if there is no Stack associated with
|
|
this Overcloud, or there are no Events
|
|
:rtype: list of heatclient.v1.events.Event
|
|
"""
|
|
if self.stack:
|
|
return heat.events_list(self._request,
|
|
self.stack.stack_name)
|
|
return []
|
|
|
|
@cached_property
|
|
def is_deployed(self):
|
|
"""Check if this Overcloud is successfully deployed.
|
|
|
|
:return: True if this Overcloud is successfully deployed;
|
|
False otherwise
|
|
:rtype: bool
|
|
"""
|
|
return self.stack.stack_status in ('CREATE_COMPLETE',
|
|
'UPDATE_COMPLETE')
|
|
|
|
@cached_property
|
|
def is_deploying(self):
|
|
"""Check if this Overcloud is currently deploying or updating.
|
|
|
|
:return: True if deployment is in progress, False otherwise.
|
|
:rtype: bool
|
|
"""
|
|
return self.stack.stack_status in ('CREATE_IN_PROGRESS',
|
|
'UPDATE_IN_PROGRESS')
|
|
|
|
@cached_property
|
|
def is_failed(self):
|
|
"""Check if this Overcloud failed to update or deploy.
|
|
|
|
:return: True if deployment there was an error, False otherwise.
|
|
:rtype: bool
|
|
"""
|
|
return self.stack.stack_status in ('CREATE_FAILED',
|
|
'UPDATE_FAILED')
|
|
|
|
@cached_property
|
|
def is_deleting(self):
|
|
"""Check if this Overcloud is deleting.
|
|
|
|
:return: True if Overcloud is deleting, False otherwise.
|
|
:rtype: bool
|
|
"""
|
|
return self.stack.stack_status in ('DELETE_IN_PROGRESS', )
|
|
|
|
@memoized.memoized
|
|
def all_resources(self, with_joins=True):
|
|
"""Return a list of all Overcloud Resources
|
|
|
|
:param with_joins: should we also retrieve objects associated with each
|
|
retrieved Resource?
|
|
:type with_joins: bool
|
|
|
|
:return: list of all Overcloud Resources or an empty list if there
|
|
are none
|
|
:rtype: list of tuskar_ui.api.Resource
|
|
"""
|
|
# FIXME(lsmola) of this is a temporary hack. When I delete the stack
|
|
# there is a brief moment when list of resources throws an exception
|
|
# a second later, it does not. So the delete in progress page will
|
|
# need to be separated, because it is 'special'. Till then, this hack
|
|
# stays.
|
|
try:
|
|
resources = [r for r in heat.resources_list(self._request,
|
|
self.stack.stack_name)]
|
|
except heatclient.exc.HTTPNotFound:
|
|
resources = []
|
|
|
|
if not with_joins:
|
|
return [Resource(r, request=self._request) for r in resources]
|
|
|
|
nodes_dict = list_to_dict(Node.list(self._request, associated=True),
|
|
key_attribute='instance_uuid')
|
|
joined_resources = []
|
|
for r in resources:
|
|
node = nodes_dict.get(r.physical_resource_id, None)
|
|
joined_resources.append(Resource(r,
|
|
node=node,
|
|
request=self._request))
|
|
# TODO(lsmola) I want just resources with nova instance
|
|
# this could be probably filtered a better way, investigate
|
|
return [r for r in joined_resources if r.node is not None]
|
|
|
|
@memoized.memoized
|
|
def resources(self, overcloud_role, with_joins=True):
|
|
"""Return a list of Overcloud Resources that match an Overcloud Role
|
|
|
|
:param overcloud_role: role of resources to be returned
|
|
:type overcloud_role: tuskar_ui.api.OvercloudRole
|
|
|
|
:param with_joins: should we also retrieve objects associated with each
|
|
retrieved Resource?
|
|
:type with_joins: bool
|
|
|
|
:return: list of Overcloud Resources that match the Overcloud Role,
|
|
or an empty list if there are none
|
|
:rtype: list of tuskar_ui.api.Resource
|
|
"""
|
|
# FIXME(lsmola) with_joins is not necessary here, I need at least
|
|
# nova instance
|
|
all_resources = self.all_resources(with_joins)
|
|
filtered_resources = [resource for resource in all_resources if
|
|
(resource.node.is_overcloud_role(
|
|
overcloud_role))]
|
|
|
|
return filtered_resources
|
|
|
|
@cached_property
|
|
def dashboard_url(self):
|
|
# TODO(rdopieralski) Implement this.
|
|
return "http://horizon.example.com"
|
|
|
|
|
|
class Node(base.APIResourceWrapper):
|
|
# FIXME(lsmola) uncomment this and delete equivalent methods
|
|
#_attrs = ('uuid', 'instance_uuid', 'driver', 'driver_info',
|
|
# 'properties', 'power_state')
|
|
_attrs = ('id', 'uuid', 'instance_uuid')
|
|
|
|
def __init__(self, apiresource, request=None, **kwargs):
|
|
"""Initialize a node
|
|
|
|
:param apiresource: apiresource we want to wrap
|
|
:type apiresource: novaclient.v1_1.contrib.baremetal.BareMetalNode
|
|
|
|
:param request: request
|
|
:type request: django.core.handlers.wsgi.WSGIRequest
|
|
|
|
:param instance: instance relation we want to cache
|
|
:type instance: openstack_dashboard.api.nova.Server
|
|
|
|
:return: Node object
|
|
:rtype: Node
|
|
"""
|
|
super(Node, self).__init__(apiresource)
|
|
self._request = request
|
|
if 'instance' in kwargs:
|
|
self._instance = kwargs['instance']
|
|
|
|
@classmethod
|
|
def nova_baremetal_format(cls, ipmi_address, cpu, ram, local_disk,
|
|
mac_addresses, ipmi_username=None,
|
|
ipmi_password=None):
|
|
"""Converts Ironic parameters to Nova-baremetal format
|
|
"""
|
|
return {'service_host': 'undercloud',
|
|
'cpus': cpu,
|
|
'memory_mb': ram,
|
|
'local_gb': local_disk,
|
|
'prov_mac_address': mac_addresses,
|
|
'pm_address': ipmi_address,
|
|
'pm_user': ipmi_username,
|
|
'pm_password': ipmi_password,
|
|
'terminal_port': None}
|
|
|
|
@classmethod
|
|
def create(cls, request, ipmi_address, cpu, ram, local_disk,
|
|
mac_addresses, ipmi_username=None, ipmi_password=None):
|
|
"""Create a Node in Ironic
|
|
|
|
:param request: request object
|
|
:type request: django.http.HttpRequest
|
|
|
|
:param ipmi_address: IPMI address
|
|
:type ipmi_address: str
|
|
|
|
:param cpu: number of cores
|
|
:type cpu: int
|
|
|
|
:param ram: RAM in GB
|
|
:type ram: int
|
|
|
|
:param local_disk: local disk in TB
|
|
:type local_disk: int
|
|
|
|
:param mac_addresses: list of mac addresses
|
|
:type mac_addresses: list of str
|
|
|
|
:param ipmi_username: IPMI username
|
|
:type ipmi_username: str
|
|
|
|
:param ipmi_password: IPMI password
|
|
:type ipmi_password: str
|
|
|
|
:return: the created Node object
|
|
:rtype: tuskar_ui.api.Node
|
|
"""
|
|
# TODO(Tzu-Mainn Chen): remove test data when possible
|
|
# TODO(Tzu-Mainn Chen): transactionality?
|
|
# node = ironicclient(request).node.create(
|
|
# driver='pxe_ipmitool',
|
|
# driver_info={'ipmi_address': ipmi_address,
|
|
# 'ipmi_username': ipmi_username,
|
|
# 'password': ipmi_password},
|
|
# properties={'cpu': cpu,
|
|
# 'ram': ram,
|
|
# 'local_disk': local_disk})
|
|
# for mac_address in mac_addresses:
|
|
# ironicclient(request).port.create(
|
|
# node_uuid=node.uuid,
|
|
# address=mac_address
|
|
# )
|
|
node = baremetalclient(request).create(**cls.nova_baremetal_format(
|
|
ipmi_address, cpu, ram, local_disk, mac_addresses,
|
|
ipmi_username=None, ipmi_password=None))
|
|
|
|
return cls(node)
|
|
|
|
@classmethod
|
|
@handle_errors(_("Unable to retrieve node"))
|
|
def get(cls, request, uuid):
|
|
"""Return the Node in Ironic that matches the ID
|
|
|
|
:param request: request object
|
|
:type request: django.http.HttpRequest
|
|
|
|
:param uuid: ID of Node to be retrieved
|
|
:type uuid: str
|
|
|
|
:return: matching Node, or None if no Node matches the ID
|
|
:rtype: tuskar_ui.api.Node
|
|
"""
|
|
# TODO(Tzu-Mainn Chen): remove test data when possible
|
|
# node = ironicclient(request).nodes.get(uuid)
|
|
|
|
node = baremetalclient(request).get(uuid)
|
|
|
|
if node.instance_uuid is not None:
|
|
server = nova.server_get(request, node.instance_uuid)
|
|
return cls(node, instance=server, request=request)
|
|
|
|
return cls(node)
|
|
|
|
@classmethod
|
|
def get_by_instance_uuid(cls, request, instance_uuid):
|
|
"""Return the Node in Ironic associated with the instance ID
|
|
|
|
:param request: request object
|
|
:type request: django.http.HttpRequest
|
|
|
|
:param instance_uuid: ID of Instance that is deployed on the Node
|
|
to be retrieved
|
|
:type instance_uuid: str
|
|
|
|
:return: matching Node
|
|
:rtype: tuskar_ui.api.Node
|
|
|
|
:raises: ironicclient.exc.HTTPNotFound if there is no Node with the
|
|
matching instance UUID
|
|
"""
|
|
# TODO(Tzu-Mainn Chen): remove test data when possible
|
|
# node = ironicclient(request).nodes.get_by_instance_uuid(
|
|
# instance_uuid)
|
|
|
|
server = nova.server_get(request, instance_uuid)
|
|
nodes = baremetalclient(request).list()
|
|
|
|
node = next((n for n in nodes if instance_uuid == n.instance_uuid),
|
|
None)
|
|
|
|
return cls(node, instance=server, request=request)
|
|
|
|
@classmethod
|
|
@handle_errors(_("Unable to retrieve nodes"), [])
|
|
def list(cls, request, associated=None):
|
|
"""Return a list of Nodes in Ironic
|
|
|
|
:param request: request object
|
|
:type request: django.http.HttpRequest
|
|
|
|
:param associated: should we also retrieve all Nodes, only those
|
|
associated with an Instance, or only those not
|
|
associated with an Instance?
|
|
:type associated: bool
|
|
|
|
:return: list of Nodes, or an empty list if there are none
|
|
:rtype: list of tuskar_ui.api.Node
|
|
"""
|
|
# TODO(Tzu-Mainn Chen): remove test data when possible
|
|
# nodes = ironicclient(request).nodes.list(
|
|
# associated=associated)
|
|
|
|
# nodes = test_data().ironicclient_nodes.list()
|
|
nodes = baremetalclient(request).list()
|
|
|
|
if associated is not None:
|
|
if associated:
|
|
nodes = [node for node in nodes
|
|
if node.instance_uuid is not None]
|
|
else:
|
|
nodes = [node for node in nodes
|
|
if node.instance_uuid is None]
|
|
return [cls(node, request=request) for node in nodes]
|
|
|
|
servers, has_more_data = nova.server_list(request)
|
|
|
|
servers_dict = list_to_dict(servers)
|
|
nodes_with_instance = []
|
|
for n in nodes:
|
|
server = servers_dict.get(n.instance_uuid, None)
|
|
nodes_with_instance.append(cls(n, instance=server,
|
|
request=request))
|
|
|
|
return nodes_with_instance
|
|
|
|
@classmethod
|
|
def delete(cls, request, uuid):
|
|
"""Remove the Node matching the ID from Ironic if it
|
|
exists; otherwise, does nothing.
|
|
|
|
:param request: request object
|
|
:type request: django.http.HttpRequest
|
|
|
|
:param uuid: ID of Node to be removed
|
|
:type uuid: str
|
|
"""
|
|
# TODO(Tzu-Mainn Chen): uncomment when possible
|
|
# ironicclient(request).nodes.delete(uuid)
|
|
baremetalclient(request).delete(uuid)
|
|
return
|
|
|
|
@cached_property
|
|
def instance(self):
|
|
"""Return the Nova Instance associated with this Node
|
|
|
|
:return: Nova Instance associated with this Node; or
|
|
None if there is no Instance associated with this
|
|
Node, or no matching Instance is found
|
|
:rtype: tuskar_ui.api.Instance
|
|
"""
|
|
if hasattr(self, '_instance'):
|
|
return self._instance
|
|
|
|
if self.instance_uuid:
|
|
server = nova.server_get(self._request, self.instance_uuid)
|
|
return server
|
|
|
|
return None
|
|
|
|
@cached_property
|
|
def image_name(self):
|
|
"""Return image name of associated instance
|
|
|
|
Returns image name of instance associated with node
|
|
|
|
:return: Image name of instance
|
|
:rtype: string
|
|
"""
|
|
if self.instance is None:
|
|
return
|
|
return image_get(self._request, self.instance.image['id']).name
|
|
|
|
def is_overcloud_role(self, overcloud_role):
|
|
"""Determine whether a node matches an overcloud role
|
|
|
|
:param overcloud_role: overcloud role to check against
|
|
:type overcloud_role: tuskar_ui.api.OvercloudRole
|
|
|
|
:return: does this node match the overcloud_role?
|
|
:rtype: bool
|
|
"""
|
|
return self.image_name == overcloud_role.image_name
|
|
|
|
@cached_property
|
|
def overcloud_role(self):
|
|
"""Return overcloud role of associated instance
|
|
|
|
:return: OvercloudRole of associated instance, or None if
|
|
none exists
|
|
:rtype: tuskar_ui.api.OvercloudRole
|
|
"""
|
|
roles = OvercloudRole.list(self._request)
|
|
for role in roles:
|
|
if self.is_overcloud_role(role):
|
|
return role
|
|
|
|
@cached_property
|
|
def addresses(self):
|
|
# FIXME(lsmola) remove when Ironic is in
|
|
"""Return a list of port addresses associated with this Node
|
|
|
|
:return: list of port addresses associated with this Node, or
|
|
an empty list if no addresses are associated with
|
|
this Node
|
|
:rtype: list of str
|
|
"""
|
|
# TODO(Tzu-Mainn Chen): uncomment when possible
|
|
# ports = self.list_ports()
|
|
# ports = test_data().ironicclient_ports.list()[:2]
|
|
|
|
# return [port.address for port in ports]
|
|
return [interface["address"] for interface in
|
|
self._apiresource.interfaces]
|
|
|
|
@cached_property
|
|
def power_state(self):
|
|
# FIXME(lsmola) remove when Ironic is in
|
|
"""Return a power state of this Node
|
|
|
|
:return: power state of this node
|
|
:rtype: str
|
|
"""
|
|
task_state = self._apiresource.task_state
|
|
task_state_dict = {
|
|
'active': 'on',
|
|
'reboot': 'rebooting'
|
|
}
|
|
return task_state_dict.get(task_state, 'off')
|
|
|
|
@cached_property
|
|
def properties(self):
|
|
# FIXME(lsmola) remove when Ironic is in
|
|
"""Return properties of this Node
|
|
|
|
:return: return memory, cpus and local_disk properties
|
|
of this Node, ram and local_disk properties
|
|
are in bytes
|
|
:rtype: dict of str
|
|
"""
|
|
return {
|
|
'ram': self._apiresource.memory_mb * 1024.0 * 1024.0,
|
|
'cpu': self._apiresource.cpus,
|
|
'local_disk': self._apiresource.local_gb * 1024.0 * 1024.0 * 1024.0
|
|
}
|
|
|
|
@cached_property
|
|
def driver_info(self):
|
|
# FIXME(lsmola) remove when Ironic is in
|
|
"""Return driver_info for this Node
|
|
|
|
:return: return pm_address property of this Node
|
|
:rtype: dict of str
|
|
"""
|
|
# FIXME(lsmola) Ironic doc is missing, so I don't know
|
|
# whether this belongs here
|
|
try:
|
|
ip_address = (self.instance._apiresource.addresses['ctlplane'][0]
|
|
['addr'])
|
|
except Exception:
|
|
LOG.error("Couldn't obtain IP address")
|
|
ip_address = None
|
|
|
|
return {
|
|
'ipmi_address': self._apiresource.pm_address,
|
|
'ip_address': ip_address
|
|
}
|
|
|
|
@cached_property
|
|
def instance_status(self):
|
|
return getattr(getattr(self, 'instance', None),
|
|
'status', None)
|
|
|
|
|
|
class Resource(base.APIResourceWrapper):
|
|
_attrs = ('resource_name', 'resource_type', 'resource_status',
|
|
'physical_resource_id')
|
|
|
|
def __init__(self, apiresource, request=None, **kwargs):
|
|
"""Initialize a resource
|
|
|
|
:param apiresource: apiresource we want to wrap
|
|
:type apiresource: heatclient.v1.resources.Resource
|
|
|
|
:param request: request
|
|
:type request: django.core.handlers.wsgi.WSGIRequest
|
|
|
|
:param node: node relation we want to cache
|
|
:type node: tuskar_ui.api.Node
|
|
|
|
:return: Resource object
|
|
:rtype: Resource
|
|
"""
|
|
super(Resource, self).__init__(apiresource)
|
|
self._request = request
|
|
if 'node' in kwargs:
|
|
self._node = kwargs['node']
|
|
|
|
@classmethod
|
|
def get(cls, request, overcloud, resource_name):
|
|
"""Return the specified Heat Resource within an Overcloud
|
|
|
|
:param request: request object
|
|
:type request: django.http.HttpRequest
|
|
|
|
:param overcloud: the Overcloud from which to retrieve the resource
|
|
:type overcloud: tuskar_ui.api.Overcloud
|
|
|
|
:param resource_name: name of the Resource to retrieve
|
|
:type resource_name: str
|
|
|
|
:return: matching Resource, or None if no Resource in the Overcloud
|
|
stack matches the resource name
|
|
:rtype: tuskar_ui.api.Resource
|
|
"""
|
|
resource = heat.resource_get(overcloud.stack.id,
|
|
resource_name)
|
|
return cls(resource, request=request)
|
|
|
|
@cached_property
|
|
def node(self):
|
|
"""Return the Ironic Node associated with this Resource
|
|
|
|
:return: Ironic Node associated with this Resource, or None if no
|
|
Node is associated
|
|
:rtype: tuskar_ui.api.Node
|
|
|
|
:raises: ironicclient.exc.HTTPNotFound if there is no Node with the
|
|
matching instance UUID
|
|
"""
|
|
if hasattr(self, '_node'):
|
|
return self._node
|
|
if self.physical_resource_id:
|
|
return Node.get_by_instance_uuid(self._request,
|
|
self.physical_resource_id)
|
|
return None
|
|
|
|
|
|
class OvercloudRole(base.APIResourceWrapper):
|
|
_attrs = ('id', 'name', 'description', 'image_name', 'flavor_id')
|
|
|
|
@classmethod
|
|
@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.OvercloudRole
|
|
"""
|
|
roles = tuskarclient(request).overcloud_roles.list()
|
|
return [cls(role) for role in roles]
|
|
|
|
@classmethod
|
|
@handle_errors(_("Unable to retrieve overcloud role"))
|
|
def get(cls, request, role_id):
|
|
"""Return the Tuskar OvercloudRole that matches the ID
|
|
|
|
:param request: request object
|
|
:type request: django.http.HttpRequest
|
|
|
|
:param role_id: ID of OvercloudRole to be retrieved
|
|
:type role_id: int
|
|
|
|
:return: matching OvercloudRole, or None if no matching
|
|
OvercloudRole can be found
|
|
:rtype: tuskar_ui.api.OvercloudRole
|
|
"""
|
|
role = tuskarclient(request).overcloud_roles.get(role_id)
|
|
return cls(role)
|
|
|
|
def update(self, request, **kwargs):
|
|
"""Update the selected attributes of Tuskar OvercloudRole.
|
|
|
|
:param request: request object
|
|
:type request: django.http.HttpRequest
|
|
"""
|
|
for attr in kwargs:
|
|
if attr not in self._attrs:
|
|
raise TypeError('Invalid parameter %r' % attr)
|
|
tuskarclient(request).overcloud_roles.update(self.id, **kwargs)
|