Dmitry Tantsur 2d25fffd29 Replace ironic_discoverd.client with ironic_inspector_client
python-ironic-inspector-client works with both ironic-discoverd
and newly introduced ironic-inspector. It also pulls in less
dependencies (aka does not pull in the whole service).

Change-Id: Iaab480b4a2e4556dd6b96b42e4cecdbef878ff45
2015-08-27 13:39:42 +02:00

446 lines
15 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 time
from django.conf import settings
from django.utils.translation import ugettext_lazy as _
from horizon.utils import memoized
from ironic_inspector_client import client as inspector_client
from ironicclient import client as ironic_client
from openstack_dashboard.api import base
from openstack_dashboard.api import glance
from openstack_dashboard.api import nova
from tuskar_ui.cached_property import cached_property # noqa
from tuskar_ui.handle_errors import handle_errors # noqa
from tuskar_ui.utils import utils
# power states
ERROR_STATES = set(['deploy failed', 'error'])
POWER_ON_STATES = set(['on', 'power on'])
# provision_states of ironic aggregated to reasonable groups
PROVISION_STATE_FREE = ['available', 'deleted', None]
PROVISION_STATE_PROVISIONED = ['active']
PROVISION_STATE_PROVISIONING = [
'deploying', 'wait call-back', 'rebuild', 'deploy complete']
PROVISION_STATE_DELETING = ['deleting']
PROVISION_STATE_ERROR = ['error', 'deploy failed']
# names for states of ironic used in UI,
# provison_states + discovery states
DISCOVERING_STATE = 'discovering'
DISCOVERED_STATE = 'discovered'
DISCOVERY_FAILED_STATE = 'discovery failed'
MAINTENANCE_STATE = 'manageable'
PROVISIONED_STATE = 'provisioned'
PROVISIONING_FAILED_STATE = 'provisioning failed'
PROVISIONING_STATE = 'provisioning'
DELETING_STATE = 'deleting'
FREE_STATE = 'free'
IRONIC_DISCOVERD_URL = getattr(settings, 'IRONIC_DISCOVERD_URL', None)
LOG = logging.getLogger(__name__)
@memoized.memoized
def ironicclient(request):
api_version = 1
kwargs = {'os_auth_token': request.user.token.id,
'ironic_url': base.url_for(request, 'baremetal')}
return ironic_client.get_client(api_version, **kwargs)
# FIXME(lsmola) This should be done in Horizon, they don't have caching
@memoized.memoized
@handle_errors(_("Unable to retrieve image."))
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 image_id: list
:return: object
:rtype: glanceclient.v1.images.Image
"""
image = glance.image_get(request, image_id)
return image
class Node(base.APIResourceWrapper):
_attrs = ('id', 'uuid', 'instance_uuid', 'driver', 'driver_info',
'properties', 'power_state', 'target_power_state',
'provision_state', 'maintenance', 'extra')
def __init__(self, apiresource, request=None, instance=None):
"""Initialize a Node
:param apiresource: apiresource we want to wrap
:type apiresource: IronicNode
: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: tusar_ui.api.node.Node
"""
super(Node, self).__init__(apiresource)
self._request = request
self._instance = instance
@classmethod
def create(cls, request, ipmi_address=None, cpu_arch=None, cpus=None,
memory_mb=None, local_gb=None, mac_addresses=[],
ipmi_username=None, ipmi_password=None, ssh_address=None,
ssh_username=None, ssh_key_contents=None,
deployment_kernel=None, deployment_ramdisk=None,
driver=None):
"""Create a Node in Ironic."""
if driver == 'pxe_ssh':
driver_info = {
'ssh_address': ssh_address,
'ssh_username': ssh_username,
'ssh_key_contents': ssh_key_contents,
'ssh_virt_type': 'virsh',
}
else:
driver_info = {
'ipmi_address': ipmi_address,
'ipmi_username': ipmi_username,
'ipmi_password': ipmi_password
}
driver_info.update(
deploy_kernel=deployment_kernel,
deploy_ramdisk=deployment_ramdisk
)
properties = {'capabilities': 'boot_option:local', }
if cpus:
properties.update(cpus=cpus)
if memory_mb:
properties.update(memory_mb=memory_mb)
if local_gb:
properties.update(local_gb=local_gb)
if cpu_arch:
properties.update(cpu_arch=cpu_arch)
node = ironicclient(request).node.create(
driver=driver,
driver_info=driver_info,
properties=properties,
)
for mac_address in mac_addresses:
ironicclient(request).port.create(
node_uuid=node.uuid,
address=mac_address
)
return cls(node, request)
@classmethod
@memoized.memoized
@handle_errors(_("Unable to retrieve node"))
def get(cls, request, uuid):
"""Return the Node 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 IronicNode matches the ID
:rtype: tuskar_ui.api.node.Node
"""
node = ironicclient(request).node.get(uuid)
if node.instance_uuid is not None:
server = nova.server_get(request, node.instance_uuid)
else:
server = None
return cls(node, request, server)
@classmethod
@handle_errors(_("Unable to retrieve node"))
def get_by_instance_uuid(cls, request, instance_uuid):
"""Return the Node 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.Node
:raises: ironicclient.exc.HTTPNotFound if there is no Node with
the matching instance UUID
"""
node = ironicclient(request).node.get_by_instance_uuid(instance_uuid)
server = nova.server_get(request, instance_uuid)
return cls(node, request, server)
@classmethod
@memoized.memoized
@handle_errors(_("Unable to retrieve nodes"), [])
def list(cls, request, associated=None, maintenance=None):
"""Return a list of Nodes
: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
:param maintenance: should we also retrieve all Nodes, only those
in maintenance mode, or those which are not in
maintenance mode?
:type maintenance: bool
:return: list of Nodes, or an empty list if there are none
:rtype: list of tuskar_ui.api.node.Node
"""
nodes = ironicclient(request).node.list(associated=associated,
maintenance=maintenance)
if associated is None or associated:
servers = nova.server_list(request)[0]
servers_dict = utils.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 [cls.get(request, node.uuid)
for node in nodes_with_instance]
return [cls.get(request, node.uuid) for node in nodes]
@classmethod
def delete(cls, request, uuid):
"""Delete an Node
Remove the IronicNode matching the ID if it
exists; otherwise, does nothing.
:param request: request object
:type request: django.http.HttpRequest
:param uuid: ID of IronicNode to be removed
:type uuid: str
"""
return ironicclient(request).node.delete(uuid)
@classmethod
def discover(cls, request, uuids):
"""Set the maintenance status of node
:param request: request object
:type request: django.http.HttpRequest
:param uuids: IDs of IronicNodes
:type uuids: list of str
"""
if not IRONIC_DISCOVERD_URL:
return
for uuid in uuids:
inspector_client.introspect(
uuid,
base_url=IRONIC_DISCOVERD_URL,
auth_token=request.user.token.id)
# NOTE(dtantsur): PXE firmware on virtual machines misbehaves when
# a lot of nodes start DHCPing simultaneously: it ignores NACK from
# DHCP server, tries to get the same address, then times out. Work
# around it by using sleep, anyway introspection takes much longer.
time.sleep(5)
@classmethod
def set_maintenance(cls, request, uuid, maintenance):
"""Set the maintenance status of node
:param request: request object
:type request: django.http.HttpRequest
:param uuid: ID of Node to be removed
:type uuid: str
:param maintenance: desired maintenance state
:type maintenance: bool
"""
patch = {
'op': 'replace',
'value': 'True' if maintenance else 'False',
'path': '/maintenance'
}
node = ironicclient(request).node.update(uuid, [patch])
return cls(node, request)
@classmethod
def set_power_state(cls, request, uuid, power_state):
"""Set the power_state of node
:param request: request object
:type request: django.http.HttpRequest
:param uuid: ID of Node
:type uuid: str
:param power_state: desired power_state
:type power_state: str
"""
node = ironicclient(request).node.set_power_state(uuid, power_state)
return cls(node, request)
@classmethod
@memoized.memoized
def list_ports(cls, request, uuid):
"""Return a list of ports associated with this Node
:param request: request object
:type request: django.http.HttpRequest
:param uuid: ID of IronicNode
:type uuid: str
"""
return ironicclient(request).node.list_ports(uuid)
@cached_property
def addresses(self):
"""Return a list of port addresses associated with this IronicNode
:return: list of port addresses associated with this IronicNode, or
an empty list if no addresses are associated with
this IronicNode
:rtype: list of str
"""
ports = self.list_ports(self._request, self.uuid)
return [port.address for port in ports]
@cached_property
def cpus(self):
return self.properties.get('cpus', None)
@cached_property
def memory_mb(self):
return self.properties.get('memory_mb', None)
@cached_property
def local_gb(self):
return self.properties.get('local_gb', None)
@cached_property
def cpu_arch(self):
return self.properties.get('cpu_arch', None)
@cached_property
def state(self):
if self.maintenance:
if not IRONIC_DISCOVERD_URL:
return MAINTENANCE_STATE
try:
status = inspector_client.get_status(
uuid=self.uuid,
base_url=IRONIC_DISCOVERD_URL,
auth_token=self._request.user.token.id,
)
except inspector_client.ClientError as e:
if getattr(e.response, 'status_code', None) == 404:
return MAINTENANCE_STATE
raise
if status['error']:
return DISCOVERY_FAILED_STATE
elif status['finished']:
return DISCOVERED_STATE
else:
return DISCOVERING_STATE
else:
if self.provision_state in PROVISION_STATE_FREE:
return FREE_STATE
if self.provision_state in PROVISION_STATE_PROVISIONING:
return PROVISIONING_STATE
if self.provision_state in PROVISION_STATE_PROVISIONED:
return PROVISIONED_STATE
if self.provision_state in PROVISION_STATE_DELETING:
return DELETING_STATE
if self.provision_state in PROVISION_STATE_ERROR:
return PROVISIONING_FAILED_STATE
# Unknown state
return None
@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: Instance
"""
if self._instance is not None:
return self._instance
if self.instance_uuid:
servers, _has_more_data = nova.server_list(self._request)
for server in servers:
if server.id == self.instance_uuid:
return server
@cached_property
def ip_address(self):
try:
apiresource = self.instace._apiresource
except AttributeError:
LOG.error("Couldn't obtain IP address")
return None
return apiresource.addresses['ctlplane'][0]['addr']
@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
image = image_get(self._request, self.instance.image['id'])
return image.name
@cached_property
def instance_status(self):
return getattr(getattr(self, 'instance', None), 'status', None)
@cached_property
def provisioning_status(self):
if self.instance_uuid:
return _("Provisioned")
return _("Free")
@classmethod
def get_all_mac_addresses(cls, request):
macs = [node.addresses for node in cls.list(request)]
return set([mac.upper() for sublist in macs for mac in sublist])