# 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