blob: b03ac700380feb176f871049e65eee6b25957b92 [file] [log] [blame]
# Copyright 2017-present Open Networking Foundation
#
# 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.
from xos.exceptions import *
from service_decl import *
class Scheduler(object):
# XOS Scheduler Abstract Base Class
# Used to implement schedulers that pick which node to put instances on
def __init__(self, slice):
self.slice = slice
def pick(self):
# this method should return a tuple (node, parent)
# node is the node to instantiate on
# parent is for container_vm instances only, and is the VM that will
# hold the container
raise Exception("Abstract Base")
class LeastLoadedNodeScheduler(Scheduler):
# This scheduler always return the node with the fewest number of
# instances.
def __init__(self, slice, label=None):
super(LeastLoadedNodeScheduler, self).__init__(slice)
self.label = label
def pick(self):
from core.models import Node
# start with all nodes
nodes = Node.objects.all()
# if a label is set, then filter by label
if self.label:
nodes = nodes.filter(nodelabels__name=self.label)
# if slice.default_node is set, then filter by default_node
if self.slice.default_node:
nodes = nodes.filter(name = self.slice.default_node)
# convert to list
nodes = list(nodes)
# sort so that we pick the least-loaded node
nodes = sorted(nodes, key=lambda node: node.instances.all().count())
if not nodes:
raise Exception(
"LeastLoadedNodeScheduler: No suitable nodes to pick from")
# TODO: logic to filter nodes by which nodes are up, and which
# nodes the slice can instantiate on.
# nodes = sorted(nodes, key=lambda node: node.instances.all().count())
return [nodes[0], None]
class ContainerVmScheduler(Scheduler):
# This scheduler picks a VM in the slice with the fewest containers inside
# of it. If no VMs are suitable, then it creates a VM.
MAX_VM_PER_CONTAINER = 10
def __init__(self, slice):
super(ContainerVmScheduler, self).__init__(slice)
@property
def image(self):
from core.models import Image
# If slice has default_image set then use it
if self.slice.default_image:
return self.slice.default_image
raise XOSProgrammingError("Please set a default image for %s" % self.slice.name)
def make_new_instance(self):
from core.models import Instance, Flavor
flavors = Flavor.objects.filter(name="m1.small")
if not flavors:
raise XOSConfigurationError("No m1.small flavor")
(node, parent) = LeastLoadedNodeScheduler(self.slice).pick()
instance = Instance(slice=self.slice,
node=node,
image=self.image,
creator=self.slice.creator,
deployment=node.site_deployment.deployment,
flavor=flavors[0],
isolation="vm",
parent=parent)
instance.save()
# We rely on a special naming convention to identify the VMs that will
# hole containers.
instance.name = "%s-outer-%s" % (instance.slice.name, instance.id)
instance.save()
return instance
def pick(self):
from core.models import Instance, Flavor
for vm in self.slice.instances.filter(isolation="vm"):
avail_vms = []
if (vm.name.startswith("%s-outer-" % self.slice.name)):
container_count = Instance.objects.filter(parent=vm).count()
if (container_count < self.MAX_VM_PER_CONTAINER):
avail_vms.append((vm, container_count))
# sort by least containers-per-vm
avail_vms = sorted(avail_vms, key=lambda x: x[1])
print "XXX", avail_vms
if avail_vms:
instance = avail_vms[0][0]
return (instance.node, instance)
instance = self.make_new_instance()
return (instance.node, instance)
class Service(Service_decl):
class Meta:
proxy = True
KIND = "generic"
def __init__(self, *args, **kwargs):
# for subclasses, set the default kind appropriately
# TODO: rethink this -- remember the class variable bug
self._meta.get_field("kind").default = self.KIND
super(Service, self).__init__(*args, **kwargs)
@property
def serviceattribute_dict(self):
attrs = {}
for attr in self.serviceattributes.all():
attrs[attr.name] = attr.value
return attrs
def get_scalable_nodes(self, slice, max_per_node=None, exclusive_slices=[]):
"""
Get a list of nodes that can be used to scale up a slice.
slice - slice to scale up
max_per_node - maximum numbers of instances that 'slice' can have on a single node
exclusive_slices - list of slices that must have no nodes in common with 'slice'.
"""
# late import to get around order-of-imports constraint in __init__.py
from core.models import Node, Instance
nodes = list(Node.objects.all())
conflicting_instances = Instance.objects.filter(
slice__in=exclusive_slices)
conflicting_nodes = Node.objects.filter(
instances__in=conflicting_instances)
nodes = [x for x in nodes if x not in conflicting_nodes]
# If max_per_node is set, then limit the number of instances this slice
# can have on a single node.
if max_per_node:
acceptable_nodes = []
for node in nodes:
existing_count = node.instances.filter(slice=slice).count()
if existing_count < max_per_node:
acceptable_nodes.append(node)
nodes = acceptable_nodes
return nodes
def pick_node(self, slice, max_per_node=None, exclusive_slices=[]):
# Pick the best node to scale up a slice.
nodes = self.get_scalable_nodes(slice, max_per_node, exclusive_slices)
nodes = sorted(nodes, key=lambda node: node.instances.all().count())
if not nodes:
return None
return nodes[0]
def adjust_scale(self, slice_hint, scale, max_per_node=None, exclusive_slices=[]):
# late import to get around order-of-imports constraint in __init__.py
from core.models import Instance
slices = [x for x in self.slices.all() if slice_hint in x.name]
for slice in slices:
while slice.instances.all().count() > scale:
s = slice.instances.all()[0]
# print "drop instance", s
s.delete()
while slice.instances.all().count() < scale:
node = self.pick_node(slice, max_per_node, exclusive_slices)
if not node:
# no more available nodes
break
image = slice.default_image
if not image:
raise XOSConfigurationError(
"No default_image for slice %s" % slice.name)
flavor = slice.default_flavor
if not flavor:
raise XOSConfigurationError(
"No default_flavor for slice %s" % slice.name)
s = Instance(slice=slice,
node=node,
creator=slice.creator,
image=image,
flavor=flavor,
deployment=node.site_deployment.deployment)
s.save()
# print "add instance", s
def get_vtn_src_nets(self):
nets = []
for slice in self.slices.all():
for ns in slice.networkslices.all():
if not ns.network:
continue
# if ns.network.template.access in ["direct", "indirect"]:
# # skip access networks; we want to use the private network
# continue
if "management" in ns.network.name:
# don't try to connect the management network to anything
continue
if ns.network.name in ["wan_network", "lan_network"]:
# we don't want to attach to the vCPE's lan or wan network
# we only want to attach to its private network
# TODO: fix hard-coding of network name
continue
for cn in ns.network.controllernetworks.all():
if cn.net_id:
net = {"name": ns.network.name, "net_id": cn.net_id}
nets.append(net)
return nets
def get_vtn_nets(self):
nets = []
for slice in self.slices.all():
for ns in slice.networkslices.all():
if not ns.network:
continue
if ns.network.template.access not in ["direct", "indirect"]:
# skip anything that's not an access network
continue
for cn in ns.network.controllernetworks.all():
if cn.net_id:
net = {"name": ns.network.name, "net_id": cn.net_id}
nets.append(net)
return nets
def get_vtn_dependencies_nets(self):
provider_nets = []
for tenant in self.subscribed_tenants.all():
if tenant.provider_service:
for net in tenant.provider_service.get_vtn_nets():
if not net in provider_nets:
net["bidirectional"] = tenant.connect_method!="private-unidirectional"
provider_nets.append(net)
return provider_nets
def get_vtn_dependencies_ids(self):
return [x["net_id"] for x in self.get_vtn_dependencies_nets()]
def get_vtn_dependencies_names(self):
return [x["name"] + "_" + x["net_id"] for x in self.get_vtn_dependencies_nets()]
def get_vtn_src_ids(self):
return [x["net_id"] for x in self.get_vtn_src_nets()]
def get_vtn_src_names(self):
return [x["name"] + "_" + x["net_id"] for x in self.get_vtn_src_nets()]
def get_composable_networks(self):
SUPPORTED_VTN_SERVCOMP_KINDS = ['VSG','PRIVATE']
nets = []
for slice in self.slices.all():
for net in slice.networks.all():
if (net.template.vtn_kind not in SUPPORTED_VTN_SERVCOMP_KINDS) or (net.owner != slice):
continue
if not net.controllernetworks.exists():
continue
nets.append(net)
return nets