Victor Sergeyev e85b19e619 Enable E128 check for line indentation
E127/E128 checks helps us to keep code more readable, so these rules
should be allowed, when pep8 runs.
Enabled check E128 - continuation line under-indented for visual indent.

Change-Id: Ic17a7ba47d555183ff26a5dc5f6ec27c0461725a
2013-10-28 14:57:25 +02:00

845 lines
28 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 collections
import copy
import datetime
import logging
import random
import django.conf
import django.db.models
from django.utils.translation import ugettext_lazy as _ # noqa
import requests
from novaclient.v1_1.contrib import baremetal
from tuskarclient.v1 import client as tuskar_client
from openstack_dashboard.api import base
from openstack_dashboard.api import nova
LOG = logging.getLogger(__name__)
TUSKAR_ENDPOINT_URL = getattr(django.conf.settings, 'TUSKAR_ENDPOINT_URL')
REMOTE_NOVA_BAREMETAL_CREDS = getattr(django.conf.settings,
'REMOTE_NOVA_BAREMETAL_CREDS',
False)
OVERCLOUD_CREDS = getattr(django.conf.settings, 'OVERCLOUD_CREDS', False)
# 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 baremetalclient(request):
def create_remote_nova_client_baremetal():
nc = nova.nova_client.Client(
REMOTE_NOVA_BAREMETAL_CREDS['user'],
REMOTE_NOVA_BAREMETAL_CREDS['password'],
REMOTE_NOVA_BAREMETAL_CREDS['tenant'],
auth_url=REMOTE_NOVA_BAREMETAL_CREDS['auth_url'],
bypass_url=REMOTE_NOVA_BAREMETAL_CREDS['bypass_url'],
)
return nc
def create_nova_client_baremetal():
insecure = getattr(django.conf.settings, 'OPENSTACK_SSL_NO_VERIFY',
False)
nc = nova.nova_client.Client(
request.user.username,
request.user.token.id,
project_id=request.user.tenant_id,
auth_url=base.url_for(request, 'compute'),
insecure=insecure,
http_log_debug=django.conf.settings.DEBUG)
nc.client.auth_token = request.user.token.id
nc.client.management_url = base.url_for(request, 'compute')
return nc
if REMOTE_NOVA_BAREMETAL_CREDS:
LOG.debug('remote nova baremetal client connection created')
nc = create_remote_nova_client_baremetal()
else:
LOG.debug('nova baremetal client connection created using token "%s" '
'and url "%s"' %
(request.user.token.id, base.url_for(request, 'compute')))
nc = create_nova_client_baremetal()
return baremetal.BareMetalNodeManager(nc)
def overcloudclient(request):
c = nova.nova_client.Client(OVERCLOUD_CREDS['user'],
OVERCLOUD_CREDS['password'],
OVERCLOUD_CREDS['tenant'],
auth_url=OVERCLOUD_CREDS['auth_url'])
return c
class StringIdAPIResourceWrapper(base.APIResourceWrapper):
# horizon DataTable class expects ids to be string,
# if it's not string, then comparison in
# horizon/tables/base.py:get_object_by_id fails.
# Because of this, ids returned from dummy api are converted to string
# (luckily django autoconverts strings to integers when passing string to
# django model id)
def __init__(self, apiresource, request=None):
self.request = request
self._apiresource = apiresource
# FIXME
# this is redefined from base.APIResourceWrapper,
# remove this when tuskarclient returns object instead of dict
def __getattr__(self, attr):
if attr in self._attrs:
if issubclass(self._apiresource.__class__, dict):
return self._apiresource.get(attr)
else:
try:
return self._apiresource.__getattribute__(attr)
except AttributeError:
return None
else:
raise AttributeError(attr)
@property
def id(self):
return str(self._apiresource.id)
# FIXME: self.request is required when calling some instance
# methods (e.g. list_flavors), once we really start using this request
# param (if ever), a proper request value should be set
@property
def request(self):
return getattr(self, '_request', None)
@request.setter
def request(self, value):
setattr(self, '_request', value)
class Alert(StringIdAPIResourceWrapper):
"""Wrapper for the Alert object returned by the
dummy model.
"""
_attrs = ['message', 'time']
class Capacity(StringIdAPIResourceWrapper):
"""Wrapper for the Capacity object returned by the
dummy model.
"""
_attrs = ['name', 'value', 'unit']
# defines a random usage of capacity - API should probably be able to
# determine usage of capacity based on capacity value and obejct_id
@property
def usage(self):
if not hasattr(self, '_usage'):
self._usage = random.randint(0, int(self.value))
return self._usage
# defines a random average of capacity - API should probably be able to
# determine average of capacity based on capacity value and obejct_id
@property
def average(self):
if not hasattr(self, '_average'):
self._average = random.randint(0, int(self.value))
return self._average
class BaremetalNode(StringIdAPIResourceWrapper):
_attrs = ['id', 'pm_address', 'cpus', 'memory_mb', 'service_host',
'local_gb', 'pm_user', 'instance_uuid']
@classmethod
def create(cls, request, **kwargs):
# The pm_address, pm_user and terminal_port need to be None when
# empty for the baremetal vm to work.
# terminal_port needs separate handling because 0 is a valid value.
terminal_port = kwargs['terminal_port']
if terminal_port == '':
terminal_port = None
node = baremetalclient(request).create(kwargs['service_host'],
kwargs['cpus'],
kwargs['memory_mb'],
kwargs['local_gb'],
kwargs['prov_mac_address'],
kwargs['pm_address'] or None,
kwargs['pm_user'] or None,
kwargs['pm_password'],
terminal_port)
return cls(node)
@classmethod
def get(cls, request, node_id):
node = cls(baremetalclient(request).get(node_id))
node.request = request
try:
# Nova instance info will be added to baremetal node attributes
nova_instance = nova.server_get(request,
node.instance_uuid)
except Exception:
nova_instance = None
LOG.debug("Couldn't obtain nova.server_get instance for "
"baremetal node %s" % node_id)
if nova_instance:
# If baremetal is provisioned, there is a nova instance.
addresses = nova_instance._apiresource.addresses.get('ctlplane')
if addresses:
node.ip_address_other = (", "
.join([addr['addr'] for addr in addresses]))
node.status = (nova_instance._apiresource.
_info['OS-EXT-STS:vm_state'])
node.power_management = ""
if node.pm_user:
node.power_management = node.pm_user + "/********"
else:
# If baremetal is unprovisioned, there is no nova instance.
node.status = 'unprovisioned'
# Returning baremetal node containing nova instance info
return node
@classmethod
def list(cls, request):
return [cls(n, request) for n in
baremetalclient(request).list()]
@classmethod
def list_unracked(cls, request):
try:
racked_node_ids = [node.nova_baremetal_node_id
for node in Node.list(request)]
return [bn for bn in BaremetalNode.list(request)
if bn.id not in racked_node_ids]
except requests.ConnectionError:
return []
@property
def mac_address(self):
try:
return self._apiresource.interfaces[0]['address']
except Exception:
return None
@property
# FIXME: just mock implementation, add proper one
def running_instances(self):
return 4
@property
# FIXME: just mock implementation, add proper one
def remaining_capacity(self):
return 100 - self.running_instances
@property
def running_virtual_machines(self):
if not hasattr(self, '_running_virtual_machines'):
if OVERCLOUD_CREDS:
search_opts = {}
search_opts['all_tenants'] = True
self._running_virtual_machines = [
s for s in overcloudclient(
self.request).servers.list(True, search_opts)
if s.hostId == self.id]
else:
LOG.debug('OVERCLOUD_CREDS is not set. '
'Can\'t connect to Overcloud')
self._running_virtual_machines = []
return self._running_virtual_machines
class Node(StringIdAPIResourceWrapper):
"""
Wrapper for the Node object returned by the
dummy model.
"""
_attrs = ['id', 'nova_baremetal_node_id']
@classmethod
def get(cls, request, node_id):
node = cls(tuskarclient(request).nodes.get(node_id))
node.request = request
return node
@classmethod
def list(cls, request):
return [cls(n, request) for n in (tuskarclient(request).nodes.list())]
@property
def rack(self):
if not hasattr(self, '_rack'):
if self.rack_id:
self._rack = Rack.get(self.request, self.rack_id)
else:
self._rack = None
return self._rack
@property
def rack_id(self):
try:
return unicode(self._apiresource.rack['id'])
except AttributeError:
return None
@property
def nova_baremetal_node(self):
if not hasattr(self, '_nova_baremetal_node'):
if self.nova_baremetal_node_id:
self._nova_baremetal_node = BaremetalNode.get(
self.request,
self.nova_baremetal_node_id)
else:
self._nova_baremetal_node = None
return self._nova_baremetal_node
def nova_baremetal_node_attribute(self, attr_name):
key = "_%s" % attr_name
if not hasattr(self, key):
if self.nova_baremetal_node:
value = getattr(self.nova_baremetal_node, attr_name, None)
else:
value = None
setattr(self, key, value)
return getattr(self, key)
@property
def service_host(self):
return self.nova_baremetal_node_attribute('service_host')
@property
def mac_address(self):
return self.nova_baremetal_node_attribute('mac_address')
@property
def ip_address_other(self):
return self.nova_baremetal_node_attribute('ip_address_other')
@property
def pm_address(self):
return self.nova_baremetal_node_attribute('pm_address')
@property
def status(self):
return self.nova_baremetal_node_attribute('status')
@property
def running_virtual_machines(self):
return self.nova_baremetal_node_attribute('running_virtual_machines')
@property
def list_flavors(self):
if not hasattr(self, '_flavors'):
# FIXME: just a mock of used instances, add real values
used_instances = 0
if not self.rack or not self.rack.resource_class:
return []
resource_class = self.rack.resource_class
added_flavors = tuskarclient(self.request).flavors\
.list(resource_class.id)
self._flavors = []
if added_flavors:
for f in added_flavors:
flavor_obj = Flavor(f)
#flavor_obj.max_vms = f.max_vms
# FIXME just a mock of used instances, add real values
used_instances += 5
flavor_obj.used_instances = used_instances
self._flavors.append(flavor_obj)
return self._flavors
@property
# FIXME: just mock implementation, add proper one
def is_provisioned(self):
return (self.status != "unprovisioned" and self.rack)
@property
def alerts(self):
if not hasattr(self, '_alerts'):
self._alerts = []
return self._alerts
class Rack(StringIdAPIResourceWrapper):
"""Wrapper for the Rack object returned by the
dummy model.
"""
_attrs = ['id', 'name', 'location', 'subnet', 'nodes', 'state',
'capacities']
@classmethod
def create(cls, request, **kwargs):
nodes = kwargs.get('nodes', [])
## FIXME: set nodes here
rack = tuskarclient(request).racks.create(
name=kwargs['name'],
location=kwargs['location'],
subnet=kwargs['subnet'],
nodes=nodes,
resource_class={'id': kwargs['resource_class_id']},
slots=0)
return cls(rack)
@classmethod
def update(cls, request, rack_id, rack_kwargs):
## FIXME: set nodes here
rack_args = copy.copy(rack_kwargs)
# remove rack_id from kwargs (othervise it is duplicated)
rack_args.pop('rack_id', None)
# correct data mapping for resource_class
if 'resource_class_id' in rack_args:
rack_args['resource_class'] = {
'id': rack_args.pop('resource_class_id', None)}
rack = tuskarclient(request).racks.update(rack_id, **rack_args)
return cls(rack)
@classmethod
def list(cls, request, only_free_racks=False):
if only_free_racks:
return [Rack(r, request) for r in
tuskarclient(request).racks.list() if (
getattr(r, 'resource_class', None) is None)]
else:
return [Rack(r, request) for r in
tuskarclient(request).racks.list()]
@classmethod
def get(cls, request, rack_id):
rack = cls(tuskarclient(request).racks.get(rack_id))
rack.request = request
return rack
@classmethod
def delete(cls, request, rack_id):
tuskarclient(request).racks.delete(rack_id)
@property
def node_ids(self):
""" List of unicode ids of nodes added to rack"""
return [
unicode(node['id']) for node in (
self.nodes)]
@property
def list_nodes(self):
if not hasattr(self, '_nodes'):
self._nodes = [Node.get(self.request, node['id']) for node in
self.nodes]
return self._nodes
@property
def list_baremetal_nodes(self):
if not hasattr(self, '_baremetal_nodes'):
self._baremetal_nodes = [node.nova_baremetal_node
for node in self.list_nodes]
return self._baremetal_nodes
@property
def nodes_count(self):
return len(self.nodes)
@property
def resource_class_id(self):
try:
return self._apiresource.resource_class['id']
except AttributeError:
return None
@property
def resource_class(self):
if not hasattr(self, '_resource_class'):
if self.resource_class_id:
self._resource_class = ResourceClass.get(
self.request,
self.resource_class_id)
else:
self._resource_class = None
return self._resource_class
@property
def list_capacities(self):
if not hasattr(self, '_capacities'):
self._capacities = [Capacity(c) for c in self.capacities]
return self._capacities
@property
def vm_capacity(self):
""" Rack VM Capacity is maximum value from its Resource Class's
Flavors max_vms (considering flavor sizes are multiples).
"""
if not hasattr(self, '_vm_capacity'):
try:
value = max([flavor.max_vms for flavor in
self.resource_class.list_flavors])
except Exception:
value = None
self._vm_capacity = Capacity({'name': "VM Capacity",
'value': value,
'unit': 'VMs'})
return self._vm_capacity
@property
def alerts(self):
if not hasattr(self, '_alerts'):
self._alerts = []
return self._alerts
@property
def aggregated_alerts(self):
# FIXME: for now return only list of nodes (particular alerts are not
# used)
return [node for node in self.list_nodes if node.alerts]
@property
def list_flavors(self):
if not hasattr(self, '_flavors'):
# FIXME just a mock of used instances, add real values
used_instances = 0
if not self.resource_class:
return []
added_flavors = (tuskarclient(self.request)
.flavors
.list(self.resource_class_id))
self._flavors = []
if added_flavors:
for f in added_flavors:
flavor_obj = Flavor(f)
#flavor_obj.max_vms = f.max_vms
# FIXME just a mock of used instances, add real values
used_instances += 2
flavor_obj.used_instances = used_instances
self._flavors.append(flavor_obj)
return self._flavors
@property
def all_used_instances(self):
return [flavor.used_instances for flavor in self.list_flavors]
@property
def total_instances(self):
# FIXME just mock implementation, add proper one
return sum(self.all_used_instances)
@property
def remaining_capacity(self):
# FIXME just mock implementation, add proper one
return 100 - self.total_instances
@property
def is_provisioned(self):
return (self.state == 'active') or (self.state == 'error')
@property
def is_provisioning(self):
return (self.state == 'provisioning')
@classmethod
def provision_all(cls, request):
tuskarclient(request).data_centers.provision_all()
class ResourceClass(StringIdAPIResourceWrapper):
"""Wrapper for the ResourceClass object returned by the
dummy model.
"""
_attrs = ['id', 'name', 'service_type', 'image_id', 'racks']
@classmethod
def get(cls, request, resource_class_id):
rc = cls(tuskarclient(request).resource_classes.get(resource_class_id))
rc.request = request
return rc
@classmethod
def create(self, request, name, service_type, image_id, flavors):
return ResourceClass(
tuskarclient(request).resource_classes.create(
name=name,
service_type=service_type,
image_id=image_id,
flavors=flavors))
@classmethod
def list(cls, request):
return [cls(rc, request) for rc in (
tuskarclient(request).resource_classes.list())]
@classmethod
def update(cls, request, resource_class_id, name, service_type, image_id,
flavors):
resource_class = cls(tuskarclient(request).resource_classes.update(
resource_class_id,
name=name,
service_type=service_type,
image_id=image_id,
flavors=flavors))
## FIXME: flavors have to be updated separately, seems less than ideal
for flavor_id in resource_class.flavors_ids:
Flavor.delete(request, resource_class_id=resource_class.id,
flavor_id=flavor_id)
for flavor in flavors:
Flavor.create(request,
resource_class_id=resource_class.id,
**flavor)
return resource_class
@property
def deletable(self):
return (len(self.racks) <= 0)
@classmethod
def delete(cls, request, resource_class_id):
tuskarclient(request).resource_classes.delete(resource_class_id)
@property
def racks_ids(self):
""" List of unicode ids of racks added to resource class """
return [
unicode(rack['id']) for rack in self.racks]
@property
def list_racks(self):
""" List of racks added to ResourceClass """
if not hasattr(self, '_racks'):
self._racks = [Rack.get(self.request, rid) for rid in (
self.racks_ids)]
return self._racks
def set_racks(self, request, racks_ids):
# FIXME: there is a bug now in tuskar, we have to remove all racks at
# first and then add new ones:
# https://github.com/tuskar/tuskar/issues/37
tuskarclient(request).resource_classes.update(self.id, racks=[])
racks = [{'id': rid} for rid in racks_ids]
tuskarclient(request).resource_classes.update(self.id, racks=racks)
@property
def racks_count(self):
return len(self.racks)
@property
def all_racks(self):
""" List of racks added to ResourceClass + list of free racks,
meaning racks that don't belong to any ResourceClass"""
if not hasattr(self, '_all_racks'):
self._all_racks =\
[r for r in (
Rack.list(self.request)) if (
r.resource_class_id is None or
str(r.resource_class_id) == self.id)]
return self._all_racks
@property
def nodes(self):
if not hasattr(self, '_nodes'):
self._nodes = [n for n in Node.list(self.request)
if n.rack_id in self.racks_ids]
return self._nodes
@property
def nodes_count(self):
return len(self.nodes)
@property
def flavors_ids(self):
""" List of unicode ids of flavors added to resource class """
return [unicode(flavor.id) for flavor in self.list_flavors]
@property
def list_flavors(self):
if not hasattr(self, '_flavors'):
# FIXME just a mock of used instances, add real values
used_instances = 0
added_flavors = tuskarclient(self.request).flavors.list(self.id)
self._flavors = []
for f in added_flavors:
flavor_obj = Flavor(f, self.request)
#flavor_obj.max_vms = f.max_vms
# FIXME just a mock of used instances, add real values
used_instances += 5
flavor_obj.used_instances = used_instances
self._flavors.append(flavor_obj)
return self._flavors
@property
def all_used_instances(self):
return [flavor.used_instances for flavor in self.list_flavors]
@property
def total_instances(self):
# FIXME just mock implementation, add proper one
return sum(self.all_used_instances)
@property
def remaining_capacity(self):
# FIXME just mock implementation, add proper one
return 100 - self.total_instances
@property
def capacities(self):
"""Aggregates Rack capacities values
"""
if not hasattr(self, '_capacities'):
capacities = [rack.list_capacities for rack in self.list_racks]
def add_capacities(c1, c2):
return [Capacity({'name': a.name,
'value': int(a.value) + int(b.value),
'unit': a.unit}) for a, b in zip(c1, c2)]
self._capacities = reduce(add_capacities, capacities)
return self._capacities
@property
def vm_capacity(self):
""" Resource Class VM Capacity is maximum value from It's Flavors
max_vms (considering flavor sizes are multiples), multipled by
number of Racks in Resource Class.
"""
if not hasattr(self, '_vm_capacity'):
try:
value = self.racks_count * max([flavor.max_vms for flavor in
self.list_flavors])
except Exception:
value = _("Unable to retrieve vm capacity")
self._vm_capacity = Capacity({'name': _("VM Capacity"),
'value': value,
'unit': _('VMs')})
return self._vm_capacity
@property
def aggregated_alerts(self):
# FIXME: for now return only list of racks (particular alerts are not
# used)
return [rack
for rack in self.list_racks
if rack.alerts + rack.aggregated_alerts]
@property
def has_provisioned_rack(self):
return any([rack.is_provisioned for rack in self.list_racks])
class Flavor(StringIdAPIResourceWrapper):
"""Wrapper for the Flavor object returned by Tuskar.
"""
_attrs = ['id', 'name', 'max_vms']
@classmethod
def get(cls, request, resource_class_id, flavor_id):
flavor = cls(tuskarclient(request).flavors.get(resource_class_id,
flavor_id))
flavor.request = request
return flavor
@classmethod
def create(cls, request, **kwargs):
return cls(tuskarclient(request).flavors.create(
kwargs['resource_class_id'],
name=kwargs['name'],
max_vms=kwargs['max_vms'],
capacities=kwargs['capacities']))
@classmethod
def delete(cls, request, **kwargs):
tuskarclient(request).flavors.delete(
kwargs['resource_class_id'],
kwargs['flavor_id'])
@property
def capacities(self):
if not hasattr(self, '_capacities'):
## FIXME: should we distinguish between tuskar
## capacities and our internal capacities?
CapacityStruct = collections.namedtuple('CapacityStruct',
'name value unit')
self._capacities = [Capacity(CapacityStruct(name=c['name'],
value=c['value'],
unit=c['unit']))
for c in self._apiresource.capacities]
return self._capacities
def capacity(self, capacity_name):
key = "_%s" % capacity_name
if not hasattr(self, key):
try:
capacity = [c for c in self.capacities if (
c.name == capacity_name)][0]
except Exception:
# FIXME: test this
capacity = Capacity(
name=capacity_name,
value=_('Unable to retrieve '
'(Is the flavor configured properly?)'),
unit='')
setattr(self, key, capacity)
return getattr(self, key)
@property
def cpu(self):
return self.capacity('cpu')
@property
def memory(self):
return self.capacity('memory')
@property
def storage(self):
return self.capacity('storage')
@property
def ephemeral_disk(self):
return self.capacity('ephemeral_disk')
@property
def swap_disk(self):
return self.capacity('swap_disk')
@property
def running_virtual_machines(self):
# FIXME: arbitrary number
return random.randint(0, int(self.cpu.value))
# defines a random average of capacity - API should probably be able to
# determine average of capacity based on capacity value and obejct_id
def vms_over_time(self, start_time, end_time):
values = []
current_time = start_time
while current_time <= end_time:
values.append(
{'date': current_time,
'value': random.randint(0, self.running_virtual_machines)})
current_time += datetime.timedelta(hours=1)
return values