Merge branch 'fixes'
diff --git a/xos/cord/__init__.py b/xos/cord/__init__.py
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/xos/cord/__init__.py
@@ -0,0 +1 @@
+
diff --git a/xos/cord/models.py b/xos/cord/models.py
new file mode 100644
index 0000000..594eb31
--- /dev/null
+++ b/xos/cord/models.py
@@ -0,0 +1,248 @@
+from django.db import models
+from core.models import Service, PlCoreBase, Slice, Sliver, Tenant, Node, Image
+from core.models.plcorebase import StrippedCharField
+import os
+from django.db import models
+from django.forms.models import model_to_dict
+from django.db.models import Q
+from operator import itemgetter, attrgetter, methodcaller
+
+"""
+import os
+import sys
+sys.path.append("/opt/xos")
+os.environ.setdefault("DJANGO_SETTINGS_MODULE", "xos.settings")
+import django
+from core.models import *
+from hpc.models import *
+from cord.models import *
+django.setup()
+svc = VOLTService.get_service_objects().all()[0]
+
+t = VOLTTenant(provider_service=svc)
+t.caller = User.objects.all()[0]
+t.save()
+
+for v in VOLTTenant.objects.all():
+ v.caller = User.objects.all()[0]
+ v.delete()
+
+for v in VCPETenant.objects.all():
+ v.caller = User.objects.all()[0]
+ v.delete()
+"""
+
+class ConfigurationError(Exception):
+ pass
+
+# -------------------------------------------
+# VOLT
+# -------------------------------------------
+
+class VOLTService(Service):
+ KIND = "vOLT"
+
+ class Meta:
+ app_label = "cord"
+ verbose_name = "vOLT Service"
+ proxy = True
+
+class VOLTTenant(Tenant):
+ class Meta:
+ proxy = True
+
+ KIND = "vOLT"
+
+ @property
+ def vcpe(self):
+ vcpe_id=self.get_attribute("vcpe_id")
+ if not vcpe_id:
+ return None
+ vcpes=VCPETenant.objects.filter(id=vcpe_id)
+ if not vcpes:
+ return None
+ return vcpes[0]
+
+ @vcpe.setter
+ def vcpe(self, value):
+ if value:
+ self.set_attribute("vcpe_id", value.id)
+ else:
+ self.set_attribute("vcpe_id", None)
+
+ def manage_vcpe(self):
+ # Each VOLT object owns exactly one VCPE object
+
+ if self.deleted:
+ return
+
+ if self.vcpe is None:
+ vcpeServices = VCPEService.get_service_objects().all()
+ if not vcpeServices:
+ raise ConfigurationError("No VCPE Services available")
+
+ vcpe = VCPETenant(provider_service = vcpeServices[0],
+ subscriber_tenant = self)
+ vcpe.caller = self.caller
+ vcpe.save()
+
+ try:
+ self.vcpe = vcpe
+ self.save()
+ except:
+ vcpe.delete()
+ raise
+
+ def cleanup_vcpe(self):
+ if self.vcpe:
+ self.vcpe.delete()
+ self.vcpe = None
+
+ def save(self, *args, **kwargs):
+ if not getattr(self, "caller", None):
+ raise TypeError("VOLTTenant's self.caller was not set")
+ super(VOLTTenant, self).save(*args, **kwargs)
+ self.manage_vcpe()
+
+ def delete(self, *args, **kwargs):
+ self.cleanup_vcpe()
+ super(VOLTTenant, self).delete(*args, **kwargs)
+
+# -------------------------------------------
+# VCPE
+# -------------------------------------------
+
+class VCPEService(Service):
+ KIND = "vCPE"
+
+ class Meta:
+ app_label = "cord"
+ verbose_name = "vCPE Service"
+ proxy = True
+
+class VCPETenant(Tenant):
+ class Meta:
+ proxy = True
+
+ KIND = "vCPE"
+
+ default_attributes = {"firewall_enable": False,
+ "firewall_rules": "accept all anywhere anywhere",
+ "url_filter_enable": False,
+ "url_filter_rules": "allow all",
+ "cdn_enable": False,
+ "sliver_id": None}
+
+ @property
+ def image(self):
+ # TODO: logic to pick an image based on the feature set
+ # just use Ubuntu 14.04 for now...
+ return Image.objects.get(name="Ubuntu 14.04 LTS")
+
+ @property
+ def sliver(self):
+ sliver_id=self.get_attribute("sliver_id")
+ if not sliver_id:
+ return None
+ slivers=Sliver.objects.filter(id=sliver_id)
+ if not slivers:
+ return None
+ return slivers[0]
+
+ @sliver.setter
+ def sliver(self, value):
+ if value:
+ self.set_attribute("sliver_id", value.id)
+ else:
+ self.set_attribute("sliver_id", None)
+
+ @property
+ def firewall_enable(self):
+ return self.get_attribute("firewall_enable", self.default_attributes["firewall_enable"])
+
+ @firewall_enable.setter
+ def firewall_enable(self, value):
+ self.set_attribute("firewall_enable", value)
+
+ @property
+ def firewall_rules(self):
+ return self.get_attribute("firewall_rules", self.default_attributes["firewall_rules"])
+
+ @firewall_rules.setter
+ def firewall_rules(self, value):
+ self.set_attribute("firewall_rules", value)
+
+ @property
+ def url_filter_enable(self):
+ return self.get_attribute("url_filter_enable", self.default_attributes["url_filter_enable"])
+
+ @url_filter_enable.setter
+ def url_filter_enable(self, value):
+ self.set_attribute("url_filter_enable", value)
+
+ @property
+ def url_filter_rules(self):
+ return self.get_attribute("url_filter_rules", self.default_attributes["url_filter_rules"])
+
+ @url_filter_rules.setter
+ def url_filter_rules(self, value):
+ self.set_attribute("url_filter_rules", value)
+
+ @property
+ def cdn_enable(self):
+ return self.get_attribute("cdn_enable", self.default_attributes["cdn_enable"])
+
+ @cdn_enable.setter
+ def cdn_enable(self, value):
+ self.set_attribute("cdn_enable", value)
+
+ def pick_node(self):
+ nodes = list(Node.objects.all())
+ # 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.slivers.all().count())
+ return nodes[0]
+
+ def manage_sliver(self):
+ # Each VCPE object owns exactly one sliver.
+
+ if self.deleted:
+ return
+
+ if (self.sliver is not None) and (self.sliver.image != self.image):
+ self.sliver.delete()
+ self.sliver = None
+ if self.sliver is None:
+ if not self.provider_service.slices.count():
+ raise ConfigurationError("The VCPE service has no slicers")
+
+ node =self.pick_node()
+ sliver = Sliver(slice = self.provider_service.slices.all()[0],
+ node = node,
+ image = self.image,
+ creator = self.caller,
+ deployment = node.site_deployment.deployment)
+ sliver.save()
+
+ try:
+ self.sliver = sliver
+ self.save()
+ except:
+ sliver.delete()
+ raise
+
+ def cleanup_sliver(self):
+ if self.sliver:
+ self.sliver.delete()
+ self.sliver = None
+
+ def save(self, *args, **kwargs):
+ if not getattr(self, "caller", None):
+ raise TypeError("VCPETenant's self.caller was not set")
+ super(VCPETenant, self).save(*args, **kwargs)
+ self.manage_sliver()
+
+ def delete(self, *args, **kwargs):
+ self.cleanup_sliver()
+ super(VCPETenant, self).delete(*args, **kwargs)
+
diff --git a/xos/core/admin.py b/xos/core/admin.py
index e6911be..b3cee88 100644
--- a/xos/core/admin.py
+++ b/xos/core/admin.py
@@ -180,12 +180,12 @@
return super(XOSAdminMixin, self).changelist_view(request, extra_context=extra_context)
- def add_view(self, request, extra_context = None):
+ def add_view(self, request, form_url='', extra_context = None):
extra_context = extra_context or {}
self.add_extra_context(request, extra_context)
- return super(XOSAdminMixin, self).add_view(request, extra_context=extra_context)
+ return super(XOSAdminMixin, self).add_view(request, form_url, extra_context=extra_context)
def __user_is_readonly(self, request):
return request.user.isReadOnlyUser()
@@ -728,9 +728,9 @@
suit_classes = 'suit-tab suit-tab-serviceattrs'
class ServiceAdmin(XOSBaseAdmin):
- list_display = ("backend_status_icon","name","description","versionNumber","enabled","published")
+ list_display = ("backend_status_icon","name","kind","versionNumber","enabled","published")
list_display_links = ('backend_status_icon', 'name', )
- fieldList = ["backend_status_text","name","description","versionNumber","enabled","published","view_url","icon_url"]
+ fieldList = ["backend_status_text","name","kind","description","versionNumber","enabled","published","view_url","icon_url"]
fieldsets = [(None, {'fields': fieldList, 'classes':['suit-tab suit-tab-general']})]
inlines = [ServiceAttrAsTabInline,SliceInline]
readonly_fields = ('backend_status_text', )
diff --git a/xos/core/models/__init__.py b/xos/core/models/__init__.py
index 97a281a..928679b 100644
--- a/xos/core/models/__init__.py
+++ b/xos/core/models/__init__.py
@@ -1,7 +1,7 @@
from .plcorebase import PlCoreBase,PlCoreBaseManager,PlCoreBaseDeletionManager,PlModelMixIn
from .project import Project
from .singletonmodel import SingletonModel
-from .service import Service
+from .service import Service, Tenant
from .service import ServiceAttribute
from .tag import Tag
from .role import Role
diff --git a/xos/core/models/controlleruser.py b/xos/core/models/controlleruser.py
index 0900df7..6b11b24 100644
--- a/xos/core/models/controlleruser.py
+++ b/xos/core/models/controlleruser.py
@@ -15,7 +15,9 @@
controller = models.ForeignKey(Controller,related_name='controllersusers')
kuser_id = StrippedCharField(null=True, blank=True, max_length=200, help_text="Keystone user id")
- composite_primary_key = ('user', 'controller', 'kuser_id')
+
+ class Meta:
+ unique_together = ('user', 'controller')
def __unicode__(self): return u'%s %s' % (self.controller, self.user)
@@ -40,7 +42,8 @@
site_privilege = models.ForeignKey('SitePrivilege', related_name='controllersiteprivileges')
role_id = StrippedCharField(null=True, blank=True, max_length=200, db_index=True, help_text="Keystone id")
- composite_primary_key = ('controller', 'site_privilege', 'role_id')
+ class Meta:
+ unique_together = ('controller', 'site_privilege', 'role_id')
def __unicode__(self): return u'%s %s' % (self.controller, self.site_privilege)
@@ -73,7 +76,9 @@
slice_privilege = models.ForeignKey('SlicePrivilege', related_name='controllersliceprivileges')
role_id = StrippedCharField(null=True, blank=True, max_length=200, db_index=True, help_text="Keystone id")
- composite_primary_key = ('controller', 'slice_privilege')
+
+ class Meta:
+ unique_together = ('controller', 'slice_privilege')
def __unicode__(self): return u'%s %s' % (self.controller, self.slice_privilege)
diff --git a/xos/core/models/image.py b/xos/core/models/image.py
index 0e12473..21d4f23 100644
--- a/xos/core/models/image.py
+++ b/xos/core/models/image.py
@@ -19,7 +19,8 @@
image = models.ForeignKey(Image,related_name='imagedeployments')
deployment = models.ForeignKey(Deployment,related_name='imagedeployments')
- composite_primary_key = ('image', 'deployment')
+ class Meta:
+ unique_together = ('image', 'deployment')
def __unicode__(self): return u'%s %s' % (self.image, self.deployment)
@@ -32,7 +33,8 @@
image = models.ForeignKey(Image,related_name='controllerimages')
controller = models.ForeignKey(Controller,related_name='controllerimages')
glance_image_id = StrippedCharField(null=True, blank=True, max_length=200, help_text="Glance image id")
-
- composite_primary_key = ('image', 'controller')
+
+ class Meta:
+ unique_together = ('image', 'controller')
def __unicode__(self): return u'%s %s' % (self.image, self.controller)
diff --git a/xos/core/models/network.py b/xos/core/models/network.py
index 5346785..ae70d2e 100644
--- a/xos/core/models/network.py
+++ b/xos/core/models/network.py
@@ -155,9 +155,9 @@
router_id = models.CharField(null=True, blank=True, max_length=256, help_text="Quantum router id")
subnet_id = models.CharField(null=True, blank=True, max_length=256, help_text="Quantum subnet id")
subnet = models.CharField(max_length=32, blank=True)
-
-
- composite_primary_key = ('network', 'controller')
+
+ class Meta:
+ unique_together = ('network', 'controller')
@staticmethod
def select_by_user(user):
@@ -176,11 +176,12 @@
network = models.ForeignKey(Network,related_name='networkslices')
slice = models.ForeignKey(Slice,related_name='networkslices')
- composite_primary_key = ('network', 'slice')
+ class Meta:
+ unique_together = ('network', 'slice')
def save(self, *args, **kwds):
slice = self.slice
- if (slice not in self.network.permitted_slices.all()) and (slice != self.network.owner) and (not self.network.permitAllSlices):
+ if (slice not in self.network.permitted_slices.all()) and (slice != self.network.owner) and (not self.network.permit_all_slices):
# to add a sliver to the network, then one of the following must be true:
# 1) sliver's slice is in network's permittedSlices list,
# 2) sliver's slice is network's owner, or
@@ -209,11 +210,12 @@
ip = models.GenericIPAddressField(help_text="Sliver ip address", blank=True, null=True)
port_id = models.CharField(null=True, blank=True, max_length=256, help_text="Quantum port id")
- composite_primary_key = ('network', 'sliver')
+ class Meta:
+ unique_together = ('network', 'sliver')
def save(self, *args, **kwds):
slice = self.sliver.slice
- if (slice not in self.network.permitted_slices.all()) and (slice != self.network.owner) and (not self.network.permitAllSlices):
+ if (slice not in self.network.permitted_slices.all()) and (slice != self.network.owner) and (not self.network.permit_all_slices):
# to add a sliver to the network, then one of the following must be true:
# 1) sliver's slice is in network's permittedSlices list,
# 2) sliver's slice is network's owner, or
diff --git a/xos/core/models/plcorebase.py b/xos/core/models/plcorebase.py
index a5f73e5..3760fde 100644
--- a/xos/core/models/plcorebase.py
+++ b/xos/core/models/plcorebase.py
@@ -151,10 +151,6 @@
deleted = models.BooleanField(default=False)
write_protect = models.BooleanField(default=False)
- # XXX Django has no official support for composite primray keys yet
- # so we will hack in an inefficient solution here.
- composite_primary_key = []
-
class Meta:
# Changing abstract to False would require the managers of subclasses of
# PlCoreBase to be customized individually.
@@ -190,28 +186,6 @@
self.enacted=None
self.save(update_fields=['enacted','deleted'], silent=silent)
- def check_composite_primary_key(self):
- try:
- composite_key_exists = (self.composite_primary_key!=None) and (self.composite_primary_key!=[])
- except AttributeError:
- composite_key_exists = False
-
- if (not composite_key_exists):
- return
-
- # dictionary containing cpk field name and value
- cpk_fields = dict([(name, getattr(self, name)) for name in self.composite_primary_key])
- objs = self.__class__.objects.filter(**cpk_fields)
- # we can only continue if there are no matches or
- # if this record is updating itself
- if (len(objs) == 0 or
- (len(objs) == 1 and self.id and objs[0].id == self.id)):
- return
- # if we reach this point then we've matched more than 1
- # existing record or we are trying to
- msg = "%s violates composite primray key constraint on fields: %s " % (self, self.composite_primary_key)
- raise db.Error, msg
-
def save(self, *args, **kwargs):
# let the user specify silence as either a kwarg or an instance varible
@@ -229,9 +203,6 @@
if not (field in ["backend_register", "backend_status", "deleted", "enacted", "updated"]):
ignore_composite_key_check=False
- if not ignore_composite_key_check:
- self.check_composite_primary_key()
-
super(PlCoreBase, self).save(*args, **kwargs)
# This is a no-op if observer_disabled is set
diff --git a/xos/core/models/service.py b/xos/core/models/service.py
index 979a295..48d1677 100644
--- a/xos/core/models/service.py
+++ b/xos/core/models/service.py
@@ -1,16 +1,30 @@
from django.db import models
-from core.models import PlCoreBase,SingletonModel
+from core.models import PlCoreBase,SingletonModel,PlCoreBaseManager
from core.models.plcorebase import StrippedCharField
+import json
class Service(PlCoreBase):
+ # when subclassing a service, redefine KIND to describe the new service
+ KIND = "generic"
+
description = models.TextField(max_length=254,null=True, blank=True,help_text="Description of Service")
enabled = models.BooleanField(default=True)
+ kind = StrippedCharField(max_length=30, help_text="Kind of service", default=KIND)
name = StrippedCharField(max_length=30, help_text="Service Name")
versionNumber = StrippedCharField(max_length=30, help_text="Version of Service Definition")
published = models.BooleanField(default=True)
view_url = StrippedCharField(blank=True, null=True, max_length=1024)
icon_url = StrippedCharField(blank=True, null=True, max_length=1024)
+ def __init__(self, *args, **kwargs):
+ # for subclasses, set the default kind appropriately
+ self._meta.get_field("kind").default = self.KIND
+ super(Service, self).__init__(*args, **kwargs)
+
+ @classmethod
+ def get_service_objects(cls):
+ return cls.objects.filter(kind = cls.KIND)
+
def __unicode__(self): return u'%s' % (self.name)
class ServiceAttribute(PlCoreBase):
@@ -18,4 +32,64 @@
value = StrippedCharField(help_text="Attribute Value", max_length=1024)
service = models.ForeignKey(Service, related_name='serviceattributes', help_text="The Service this attribute is associated with")
+class Tenant(PlCoreBase):
+ """ A tenant is a relationship between two entities, a subscriber and a
+ provider.
+
+ The subscriber can be a User, a Service, or a Tenant.
+
+ The provider is always a Service.
+ """
+
+ # when subclassing a service, redefine KIND to describe the new service
+ KIND = "generic"
+
+ kind = StrippedCharField(max_length=30, default=KIND)
+ provider_service = models.ForeignKey(Service, related_name='tenants')
+ subscriber_service = models.ForeignKey(Service, related_name='subscriptions', blank=True, null=True)
+ subscriber_tenant = models.ForeignKey("Tenant", related_name='subscriptions', blank=True, null=True)
+ subscriber_user = models.ForeignKey("User", related_name='subscriptions', blank=True, null=True)
+ service_specific_id = StrippedCharField(max_length=30)
+ service_specific_attribute = models.TextField()
+
+ def __init__(self, *args, **kwargs):
+ # for subclasses, set the default kind appropriately
+ self._meta.get_field("kind").default = self.KIND
+ super(Tenant, self).__init__(*args, **kwargs)
+
+ def __unicode__(self):
+ if not hasattr(self, "provider_service"):
+ # When the REST API does a POST on a CordSubscriber object, for
+ # some reason there is no provider_service field. All of the other
+ # fields are there. Provider_service is even in the dir(). However,
+ # trying to getattr() on it will fail.
+ return "confused-tenant-object"
+
+ if self.subscriber_service:
+ return u'%s service %s on service %s' % (str(self.kind), str(self.subscriber_service.id), str(self.provider_service.id))
+ elif self.subscriber_tenant:
+ return u'%s tenant %s on service %s' % (str(self.kind), str(self.subscriber_tenant.id), str(self.provider_service.id))
+ else:
+ return u'%s on service %s' % (str(self.kind), str(self.provider_service.id))
+
+ # helper for extracting things from a json-encoded service_specific_attribute
+ def get_attribute(self, name, default=None):
+ if self.service_specific_attribute:
+ attributes = json.loads(self.service_specific_attribute)
+ else:
+ attributes = {}
+ return attributes.get(name, default)
+
+ def set_attribute(self, name, value):
+ if self.service_specific_attribute:
+ attributes = json.loads(self.service_specific_attribute)
+ else:
+ attributes = {}
+ attributes[name]=value
+ self.service_specific_attribute = json.dumps(attributes)
+
+ @classmethod
+ def get_tenant_objects(cls):
+ return cls.objects.filter(kind = cls.KIND)
+
diff --git a/xos/core/models/site.py b/xos/core/models/site.py
index 689dea0..1e8c7ca 100644
--- a/xos/core/models/site.py
+++ b/xos/core/models/site.py
@@ -219,7 +219,8 @@
user = models.ForeignKey('User', related_name='deploymentprivileges')
deployment = models.ForeignKey('Deployment', related_name='deploymentprivileges')
role = models.ForeignKey('DeploymentRole',related_name='deploymentprivileges')
- composite_primary_key = ('user', 'deployment', 'role')
+ class Meta:
+ unique_together = ('user', 'deployment', 'role')
def __unicode__(self): return u'%s %s %s' % (self.deployment, self.user, self.role)
@@ -281,7 +282,8 @@
controller = models.ForeignKey(Controller, null=True, blank=True, related_name='sitedeployments')
availability_zone = StrippedCharField(max_length=200, null=True, blank=True, help_text="OpenStack availability zone")
- composite_primary_key = ('site', 'deployment', 'controller')
+ class Meta:
+ unique_together = ('site', 'deployment', 'controller')
def __unicode__(self): return u'%s %s' % (self.deployment, self.site)
@@ -291,4 +293,5 @@
controller = models.ForeignKey(Controller, null=True, blank=True, related_name='controllersite')
tenant_id = StrippedCharField(null=True, blank=True, max_length=200, db_index=True, help_text="Keystone tenant id")
- composite_primary_key = ('site', 'controller')
+ class Meta:
+ unique_together = ('site', 'controller')
diff --git a/xos/core/models/slice.py b/xos/core/models/slice.py
index 44a918b..0c55791 100644
--- a/xos/core/models/slice.py
+++ b/xos/core/models/slice.py
@@ -126,7 +126,8 @@
slice = models.ForeignKey('Slice', related_name='sliceprivileges')
role = models.ForeignKey('SliceRole',related_name='sliceprivileges')
- composite_primary_key = ('user', 'slice', 'role')
+ class Meta:
+ unique_together = ('user', 'slice', 'role')
def __unicode__(self): return u'%s %s %s' % (self.slice, self.user, self.role)
@@ -155,7 +156,8 @@
slice = models.ForeignKey(Slice, related_name='controllerslices')
tenant_id = StrippedCharField(null=True, blank=True, max_length=200, help_text="Keystone tenant id")
- composite_primary_key = ('controller', 'slice')
+ class Meta:
+ unique_together = ('controller', 'slice')
def __unicode__(self): return u'%s %s' % (self.slice, self.controller)
diff --git a/xos/core/views/hpc_config.py b/xos/core/views/hpc_config.py
index 41bd334..8b5bac0 100644
--- a/xos/core/views/hpc_config.py
+++ b/xos/core/views/hpc_config.py
@@ -29,6 +29,12 @@
# to us.
hpc=None
for candidate in HpcService.objects.all():
+ if candidate.cmi_hostname == node_slicename:
+ # A hack for standalone CMIs that aren't managed by XOS. Set
+ # /etc/slicename to cmi_hostname that's configured in the
+ # HPCService object.
+ hpc = candidate
+
for slice in get_service_slices(candidate):
if slice.name == node_slicename:
hpc = candidate
diff --git a/xos/core/views/legacyapi.py b/xos/core/views/legacyapi.py
index 43b444c..5216351 100644
--- a/xos/core/views/legacyapi.py
+++ b/xos/core/views/legacyapi.py
@@ -146,7 +146,7 @@
return sites
-def GetInterfaces(slicename, node_ids):
+def GetInterfaces(slicename, node_ids, return_nat=False, return_private=False):
interfaces = []
ps_slices = Slice.objects.filter(name=slicename)
for ps_slice in ps_slices:
@@ -165,6 +165,28 @@
if (template.visibility=="public") and (template.translation=="none"):
ip=networkSliver.ip
+ if return_nat:
+ ip = None
+ for networkSliver in ps_sliver.networkslivers.all():
+ if (not networkSliver.ip):
+ continue
+ template = networkSliver.network.template
+ if (template.visibility=="private") and (template.translation=="NAT"):
+ ip=networkSliver.ip
+ if not ip:
+ continue
+
+ if return_private:
+ ip = None
+ for networkSliver in ps_sliver.networkslivers.all():
+ if (not networkSliver.ip):
+ continue
+ template = networkSliver.network.template
+ if (template.visibility=="private") and (template.translation=="none"):
+ ip=networkSliver.ip
+ if not ip:
+ continue
+
interface = {"node_id": node_id,
"ip": ip,
"broadcast": None,
@@ -197,6 +219,7 @@
perhost = {}
allinterfaces = {}
hostipmap = {}
+ hostnatmap = {}
nodes = []
if len(slices)==1:
slice = slices[0]
@@ -216,6 +239,18 @@
if interface['is_primary']:
hostipmap[nodemap[interface['node_id']]] = interface['ip']
+ hostnatmap = {}
+ interfaces = GetInterfaces(slice["planetstack_name"], node_ids, return_nat=True)
+ for interface in interfaces:
+ interface['interface_tags'] = []
+ hostnatmap[nodemap[interface['node_id']]] = interface['ip']
+
+ hostprivmap = {}
+ interfaces = GetInterfaces(slice["planetstack_name"], node_ids, return_private=True)
+ for interface in interfaces:
+ interface['interface_tags'] = []
+ hostprivmap[nodemap[interface['node_id']]] = interface['ip']
+
for nid in node_ids:
sliver_tags = GetTags(slicename,nid)
perhost[nodemap[nid]] = sliver_tags
@@ -234,6 +269,8 @@
'configuration': node_sliver_tags,
'allconfigurations':perhost,
'hostipmap':hostipmap,
+ 'hostnatmap':hostnatmap,
+ 'hostprivmap':hostprivmap,
'slivers': slivers,
'interfaces': allinterfaces,
'sites': sites,
diff --git a/xos/core/xoslib/dashboards/cord.html b/xos/core/xoslib/dashboards/cord.html
new file mode 100644
index 0000000..368f264
--- /dev/null
+++ b/xos/core/xoslib/dashboards/cord.html
@@ -0,0 +1,70 @@
+<script src="{{ STATIC_URL }}/js/vendor/underscore-min.js"></script>
+<script src="{{ STATIC_URL }}/js/vendor/backbone.js"></script>
+<script src="{{ STATIC_URL }}/js/vendor/backbone.syphon.js"></script>
+<script src="{{ STATIC_URL }}/js/vendor/backbone.wreqr.js"></script>
+<script src="{{ STATIC_URL }}/js/vendor/backbone.babysitter.js"></script>
+<script src="{{ STATIC_URL }}/js/vendor/backbone.marionette.js"></script>
+
+<link rel="stylesheet" href="//code.jquery.com/ui/1.11.2/themes/smoothness/jquery-ui.css">
+<link rel="stylesheet" type="text/css" href="{% static 'css/xosAdminDashboard.css' %}" media="all" >
+<link rel="stylesheet" type="text/css" href="{% static 'css/xosAdminSite.css' %}" media="all" >
+
+<script src="{{ STATIC_URL }}/js/xoslib/xos-util.js"></script>
+<script src="{{ STATIC_URL }}/js/xoslib/xos-defaults.js"></script>
+<script src="{{ STATIC_URL }}/js/xoslib/xos-validators.js"></script>
+<script src="{{ STATIC_URL }}/js/xoslib/xos-backbone.js"></script>
+<script src="{{ STATIC_URL }}/js/xoslib/xosHelper.js"></script>
+<script src="{{ STATIC_URL }}/js/picker.js"></script>
+<script src="{{ STATIC_URL }}/js/xosCord.js"></script>
+
+<script type="text/template" id="xos-log-template">
+ <tr id="<%= logMessageId %>" class="xos-log xos-<%= statusclass %>">
+ <td><%= what %><br>
+ <%= status %> <%= statusText %>
+ </td>
+ </tr>
+</script>
+
+<div id="xos-confirm-dialog" title="Confirmation Required">
+ Are you sure about this?
+</div>
+
+<div id="xos-error-dialog" title="Error Message">
+</div>
+
+<div id="xos-addchild-dialog" title="Add Child">
+<div id="xos-addchild-detail"></div>
+</div>
+
+<div id="contentPanel">
+<div id="contentTitle">
+</div>
+<div id="contentButtonPanel">
+<!-- This is really a convoluted way of handling the buttons. The onClick
+ handler for this Save button tells the save button inside the detail
+ form to click itself.
+-->
+
+<div id="rightButtonPanel"></div>
+
+<div class="box" id="logPanel">
+<table id="logTable">
+<tbody>
+</tbody>
+</table> <!-- end logTable -->
+</div> <!-- end logPanel -->
+</div> <!-- end contentButtonPanel -->
+<div id="contentInner">
+<div id="tabs">
+</div>
+<div id="detail"></div>
+<div id="linkedObjs1"></div>
+<div id="linkedObjs2"></div>
+<div id="linkedObjs3"></div>
+<div id="linkedObjs4"></div>
+</div> <!-- end contentInner -->
+</div> <!-- end contentPanel -->
+
+{% include 'xosAdmin.html' %}
+{% include 'xosCordSubscriber.html' %}
+
diff --git a/xos/core/xoslib/methods/cordsubscriber.py b/xos/core/xoslib/methods/cordsubscriber.py
new file mode 100644
index 0000000..1470323
--- /dev/null
+++ b/xos/core/xoslib/methods/cordsubscriber.py
@@ -0,0 +1,59 @@
+from rest_framework.decorators import api_view
+from rest_framework.response import Response
+from rest_framework.reverse import reverse
+from rest_framework import serializers
+from rest_framework import generics
+from core.models import *
+from django.forms import widgets
+from cord.models import VOLTTenant
+from core.xoslib.objects.cordsubscriber import CordSubscriber
+from plus import PlusSerializerMixin
+from xos.apibase import XOSListCreateAPIView, XOSRetrieveUpdateDestroyAPIView, XOSPermissionDenied
+
+if hasattr(serializers, "ReadOnlyField"):
+ # rest_framework 3.x
+ ReadOnlyField = serializers.ReadOnlyField
+else:
+ # rest_framework 2.x
+ ReadOnlyField = serializers.Field
+
+class CordSubscriberIdSerializer(serializers.ModelSerializer, PlusSerializerMixin):
+ id = ReadOnlyField()
+ vcpe_id = ReadOnlyField()
+ sliver = ReadOnlyField()
+ image = ReadOnlyField()
+ firewall_enable = serializers.BooleanField()
+ firewall_rules = serializers.CharField()
+ url_filter_enable = serializers.BooleanField()
+ url_filter_rules = serializers.CharField()
+ cdn_enable = serializers.BooleanField()
+ sliver_name = ReadOnlyField()
+ image_name = ReadOnlyField()
+
+ humanReadableName = serializers.SerializerMethodField("getHumanReadableName")
+
+ class Meta:
+ model = CordSubscriber
+ fields = ('humanReadableName', 'id',
+ 'service_specific_id',
+ 'vcpe_id', 'sliver', 'sliver_name', 'image', 'image_name', 'firewall_enable', 'firewall_rules', 'url_filter_enable', 'url_filter_rules', 'cdn_enable')
+
+
+ def getHumanReadableName(self, obj):
+ return obj.__unicode__()
+
+class CordSubscriberList(XOSListCreateAPIView):
+ queryset = CordSubscriber.get_tenant_objects().select_related().all()
+ serializer_class = CordSubscriberIdSerializer
+
+ method_kind = "list"
+ method_name = "cordsubscriber"
+
+class CordSubscriberDetail(XOSRetrieveUpdateDestroyAPIView):
+ queryset = CordSubscriber.get_tenant_objects().select_related().all()
+ serializer_class = CordSubscriberIdSerializer
+
+ method_kind = "detail"
+ method_name = "cordsubscriber"
+
+
diff --git a/xos/core/xoslib/objects/cordsubscriber.py b/xos/core/xoslib/objects/cordsubscriber.py
new file mode 100644
index 0000000..654d8a3
--- /dev/null
+++ b/xos/core/xoslib/objects/cordsubscriber.py
@@ -0,0 +1,82 @@
+from core.models import Slice, SlicePrivilege, SliceRole, Sliver, Site, Node, User
+from cord.models import VOLTTenant
+from plus import PlusObjectMixin
+from operator import itemgetter, attrgetter
+from rest_framework.exceptions import APIException
+
+"""
+import os
+import sys
+sys.path.append("/opt/xos")
+os.environ.setdefault("DJANGO_SETTINGS_MODULE", "xos.settings")
+import django
+from core.models import *
+from hpc.models import *
+from cord.models import *
+django.setup()
+from core.xoslib.objects.cordsubscriber import CordSubscriber
+c=CordSubscriber.get_tenant_objects().select_related().all()[0]
+"""
+
+class CordSubscriber(VOLTTenant, PlusObjectMixin):
+ class Meta:
+ proxy = True
+
+ def __init__(self, *args, **kwargs):
+ super(VOLTTenant, self).__init__(*args, **kwargs)
+
+ @property
+ def vcpe_id(self):
+ if self.vcpe:
+ return self.vcpe.id
+ else:
+ return None
+
+ @vcpe_id.setter
+ def vcpe_id(self, value):
+ pass
+
+ passthroughs = ( ("firewall_enable", "vcpe.firewall_enable"),
+ ("firewall_rules", "vcpe.firewall_rules"),
+ ("url_filter_enable", "vcpe.url_filter_enable"),
+ ("url_filter_rules", "vcpe.url_filter_rules"),
+ ("cdn_enable", "vcpe.cdn_enable"),
+ ("image", "vcpe.image.id"),
+ ("image_name", "vcpe.image.name"),
+ ("sliver", "vcpe.sliver.id"),
+ ("sliver_name", "vcpe.sliver.name") )
+
+ def __getattr__(self, key):
+ for (member_name, passthrough_name) in self.passthroughs:
+ if key==member_name:
+ parts = passthrough_name.split(".")
+ obj = self
+ for part in parts[:-1]:
+ obj = getattr(obj, part)
+ if not obj:
+ return None
+ return getattr(obj, parts[-1])
+
+ raise AttributeError("getattr: %r object has no attribute %r" %
+ (self.__class__, key))
+
+ def __setattr__(self, key, value):
+ for (member_name, passthrough_name) in self.passthroughs:
+ if key==member_name:
+ parts = passthrough_name.split(".")
+ obj = self
+ for part in parts[:-1]:
+ obj = getattr(obj, part)
+ if not obj:
+ return
+ setattr(obj, parts[-1], value)
+
+ super(CordSubscriber, self).__setattr__(key, value)
+
+
+
+
+
+
+
+
diff --git a/xos/core/xoslib/static/js/xosCord.js b/xos/core/xoslib/static/js/xosCord.js
new file mode 100644
index 0000000..96d95f1
--- /dev/null
+++ b/xos/core/xoslib/static/js/xosCord.js
@@ -0,0 +1,195 @@
+OBJS = ['cordSubscriber', ]
+
+CordAdminApp = new XOSApplication({
+ logTableId: "#logTable",
+ statusMsgId: "#statusMsg",
+ hideTabsByDefault: true
+});
+
+CordAdminApp.addRegions({
+ navigation: "#navigationPanel",
+
+ detail: "#detail",
+ linkedObjs1: "#linkedObjs1",
+ linkedObjs2: "#linkedObjs2",
+ linkedObjs3: "#linkedObjs3",
+ linkedObjs4: "#linkedObjs4",
+
+ addChildDetail: "#xos-addchild-detail",
+
+ rightButtonPanel: "#rightButtonPanel"
+});
+
+CordAdminApp.navigate = function(what, modelName, modelId) {
+ collection_name = modelName + "s";
+ if (what=="list") {
+ CordAdminApp.Router.navigate(collection_name, {trigger: true})
+ } else if (what=="detail") {
+ CordAdminApp.Router.navigate(collection_name + "/" + modelId, {trigger: true})
+ } else if (what=="add") {
+ CordAdminApp.Router.navigate("add" + firstCharUpper(modelName), {trigger: true, force: true})
+ }
+}
+
+CordAdminApp.buildViews = function() {
+ genericAddChildClass = XOSDetailView.extend({template: "#xos-add-template",
+ app: CordAdminApp});
+ CordAdminApp["genericAddChildView"] = genericAddChildClass;
+
+ genericDetailClass = XOSDetailView.extend({template: "#xos-detail-template",
+ app: CordAdminApp});
+ CordAdminApp["genericDetailView"] = genericDetailClass;
+
+ genericItemViewClass = XOSItemView.extend({template: "#xos-listitem-template",
+ app: CordAdminApp});
+ CordAdminApp["genericItemView"] = genericItemViewClass;
+
+ //genericListViewClass = XOSListView.extend({template: "#xos-list-template",
+ // app: CordAdminApp});
+
+ genericListViewClass = XOSDataTableView.extend({template: "#xos-list-template", app: CordAdminApp});
+ CordAdminApp["genericListView"] = genericListViewClass;
+
+ for (var index in OBJS) {
+ name = OBJS[index];
+ tr_template = '#xosAdmin-' + name + '-listitem-template';
+ table_template = '#xosAdmin-' + name + '-list-template';
+ detail_template = '#xosAdmin-' + name + '-detail-template';
+ add_child_template = '#xosAdmin-' + name + '-add-child-template';
+ collection_name = name + "s";
+ region_name = name + "List";
+
+ if (window["XOSDetailView_" + name]) {
+ detailClass = window["XOSDetailView_" + name].extend({template: "#xos-detail-template",
+ app: CordAdminApp});
+ } else {
+ detailClass = genericDetailClass;
+ }
+ if ($(detail_template).length) {
+ detailClass = detailClass.extend({
+ template: detail_template,
+ });
+ }
+ CordAdminApp[collection_name + "DetailView"] = detailClass;
+
+ if (window["XOSDetailView_" + name]) {
+ addClass = window["XOSDetailView_" + name].extend({template: "#xos-add-template",
+ app: CordAdminApp});
+ } else {
+ addClass = genericAddChildClass;
+ }
+ if ($(add_child_template).length) {
+ addClass = detailClass.extend({
+ template: add_child_template,
+ });
+ }
+ CordAdminApp[collection_name + "AddChildView"] = addClass;
+
+ if ($(tr_template).length) {
+ itemViewClass = XOSItemView.extend({
+ template: tr_template,
+ app: CordAdminApp,
+ });
+ } else {
+ itemViewClass = genericItemViewClass;
+ }
+
+ if ($(table_template).length) {
+ listViewClass = XOSListView.extend({
+ childView: itemViewClass,
+ template: table_template,
+ collection: xos[collection_name],
+ title: name + "s",
+ app: CordAdminApp,
+ });
+ } else {
+ listViewClass = genericListViewClass.extend( { childView: itemViewClass,
+ collection: xos[collection_name],
+ title: name + "s",
+ } );
+ }
+
+ CordAdminApp[collection_name + "ListView"] = listViewClass;
+
+ xos[collection_name].fetch(); //startPolling();
+ }
+};
+
+CordAdminApp.initRouter = function() {
+ router = XOSRouter;
+ var api = {};
+ var routes = {};
+
+ for (var index in OBJS) {
+ name = OBJS[index];
+ collection_name = name + "s";
+ nav_url = collection_name;
+ api_command = "list" + firstCharUpper(collection_name);
+ listViewName = collection_name + "ListView";
+ detailViewName = collection_name + "DetailView";
+ addChildViewName = collection_name + "AddChildView";
+
+ api[api_command] = CordAdminApp.createListHandler(listViewName, collection_name, "detail", collection_name);
+ routes[nav_url] = api_command;
+
+ nav_url = collection_name + "/:id";
+ api_command = "detail" + firstCharUpper(collection_name);
+
+ api[api_command] = CordAdminApp.createDetailHandler(detailViewName, collection_name, "detail", name);
+ routes[nav_url] = api_command;
+
+ nav_url = "add" + firstCharUpper(name);
+ api_command = "add" + firstCharUpper(name);
+ api[api_command] = CordAdminApp.createAddHandler(detailViewName, collection_name, "detail", name);
+ routes[nav_url] = api_command;
+
+ nav_url = "addChild" + firstCharUpper(name) + "/:parentModel/:parentField/:parentId";
+ api_command = "addChild" + firstCharUpper(name);
+ api[api_command] = CordAdminApp.createAddChildHandler(addChildViewName, collection_name);
+ routes[nav_url] = api_command;
+
+ nav_url = "delete" + firstCharUpper(name) + "/:id";
+ api_command = "delete" + firstCharUpper(name);
+ api[api_command] = CordAdminApp.createDeleteHandler(collection_name, name);
+ routes[nav_url] = api_command;
+ };
+
+ routes["*part"] = "listCordSubscribers";
+
+ CordAdminApp.Router = new router({ appRoutes: routes, controller: api });
+};
+
+CordAdminApp.startNavigation = function() {
+ Backbone.history.start();
+ CordAdminApp.navigationStarted = true;
+}
+
+CordAdminApp.collectionLoadChange = function() {
+ stats = xos.getCollectionStatus();
+
+ if (!CordAdminApp.navigationStarted) {
+ if (stats["isLoaded"] + stats["failedLoad"] >= stats["startedLoad"]) {
+ CordAdminApp.startNavigation();
+ } else {
+ $("#detail").html("<h3>Loading...</h3><div id='xos-startup-progress'></div>");
+ $("#xos-startup-progress").progressbar({value: stats["completedLoad"], max: stats["startedLoad"]});
+ }
+ }
+};
+
+CordAdminApp.on("start", function() {
+ CordAdminApp.buildViews();
+
+ CordAdminApp.initRouter();
+
+ // fire it once to initially show the progress bar
+ CordAdminApp.collectionLoadChange();
+
+ // fire it each time the collection load status is updated
+ Backbone.on("xoslib:collectionLoadChange", CordAdminApp.collectionLoadChange);
+});
+
+$(document).ready(function(){
+ CordAdminApp.start();
+});
+
diff --git a/xos/core/xoslib/static/js/xoslib/xos-backbone.js b/xos/core/xoslib/static/js/xoslib/xos-backbone.js
index a459458..f22a2e2 100644
--- a/xos/core/xoslib/static/js/xoslib/xos-backbone.js
+++ b/xos/core/xoslib/static/js/xoslib/xos-backbone.js
@@ -34,7 +34,9 @@
SLICEPLUS_API = XOSLIB_BASE + "/slicesplus/";
TENANTVIEW_API = XOSLIB_BASE + "/tenantview/";
- HPCVIEW_API = XOSLIB_BASE + "/hpcview";
+ HPCVIEW_API = XOSLIB_BASE + "/hpcview/";
+
+ CORDSUBSCRIBER_API = XOSLIB_BASE + "/cordsubscriber/";
XOSModel = Backbone.Model.extend({
relatedCollections: [],
@@ -725,6 +727,15 @@
detailFields: [],
});
+ define_model(this, {urlRoot: CORDSUBSCRIBER_API,
+ modelName: "cordSubscriber",
+ listFields: ["id"],
+ detailFields: ["id", "service_specific_id", "vcpe_id", "image_name", "sliver_name", "firewall_enable", "firewall_rules", "url_filter_enable", "url_filter_rules", "cdn_enable"],
+ inputType: {"firewall_enable": "checkbox",
+ "url_filter_enable": "checkbox",
+ "cdn_enable": "checkbox"},
+ });
+
/* by default, have slicePlus only fetch the slices the user can see */
this.slicesPlus.currentUserCanSee = true;
diff --git a/xos/core/xoslib/templates/xosCordSubscriber.html b/xos/core/xoslib/templates/xosCordSubscriber.html
new file mode 100644
index 0000000..7d86cf7
--- /dev/null
+++ b/xos/core/xoslib/templates/xosCordSubscriber.html
@@ -0,0 +1,16 @@
+<script type="text/template" id="xos-cord-subscriber-template">
+ <h3 class="xos-detail-title">CORD Subscriber</h3>
+ <form>
+ <table class="xos-detail-table">
+ <tr><td class="xos-label-cell">vOLT</td></tr>
+ <tr><td class="xos-label-cell">ID:</td><td><%= vOLT_id %></td></tr>
+ <tr><td class="xos-label-cell">internal ID:</td><td><%= vOLT_service_specific_id %></td></tr>
+ <tr><td class="xos-label-cell">vCPE</td></tr>
+ <tr><td class="xos-label-cell">ID:</td><td><%= vCPE_id %></td></tr>
+ <tr><td class="xos-label-cell">vBNG</td></tr>
+ <tr><td class="xos-label-cell">ID:</td><td></td></tr>
+ <tr><td class="xos-label-cell">Routeable Subnet:</td><td></td></tr>
+ </table>
+ </form>
+</script>
+
diff --git a/xos/hpc_observer/hpc_watcher.py b/xos/hpc_observer/hpc_watcher.py
index 5b9f181..b0587f5 100644
--- a/xos/hpc_observer/hpc_watcher.py
+++ b/xos/hpc_observer/hpc_watcher.py
@@ -33,6 +33,7 @@
"""
import os
+import socket
import sys
sys.path.append("/opt/xos")
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "xos.settings")
@@ -329,8 +330,11 @@
ip = sliver.get_public_ip()
if not ip:
- self.set_status(sliver, service, "watcher.DNS", "no public IP")
- continue
+ ip = socket.gethostbyname(sliver.node.name)
+
+ #if not ip:
+ # self.set_status(sliver, service, "watcher.DNS", "no public IP")
+ # continue
checks = HpcHealthCheck.objects.filter(kind="dns")
if not checks:
diff --git a/xos/openstack_observer/steps/sync_network_slivers.py b/xos/openstack_observer/steps/sync_network_slivers.py
index 78751d1..b61b93f 100644
--- a/xos/openstack_observer/steps/sync_network_slivers.py
+++ b/xos/openstack_observer/steps/sync_network_slivers.py
@@ -133,7 +133,12 @@
sliver=sliver,
ip=ip,
port_id=port["id"])
- ns.save()
+
+ try:
+ ns.save()
+ except:
+ logger.log_exc("failed to save networksliver %s" % str(ns))
+ continue
# Now, handle port forwarding
# We get the list of NetworkSlivers again, since we might have just
diff --git a/xos/tools/ansible_hosts.py b/xos/tools/ansible_hosts.py
new file mode 100644
index 0000000..e17edf6
--- /dev/null
+++ b/xos/tools/ansible_hosts.py
@@ -0,0 +1,65 @@
+#! /usr/bin/env python
+
+import json
+import os
+import requests
+import sys
+
+from operator import itemgetter, attrgetter
+
+opencloud_auth = None
+
+REST_API="http://portal.opencloud.us/xos/"
+
+NODES_API = REST_API + "nodes/"
+SITES_API = REST_API + "sites/"
+
+def get_nodes_by_site():
+ r = requests.get(SITES_API + "?no_hyperlinks=1", auth=opencloud_auth)
+ sites_list = r.json()
+ sites = {}
+ for site in sites_list:
+ site["hostnames"] = []
+ sites[str(site["id"])] = site
+
+ r = requests.get(NODES_API + "?no_hyperlinks=1", auth=opencloud_auth)
+ nodes = r.json()
+ for node in nodes:
+ site_id = str(node["site"])
+ if site_id in sites:
+ sites[site_id]["hostnames"].append(node["name"])
+
+ return sites
+
+def main():
+ global opencloud_auth
+
+ if len(sys.argv)!=3:
+ print >> sys.stderr, "syntax: get_instance_name.py <username>, <password>"
+ sys.exit(-1)
+
+ username = sys.argv[1]
+ password = sys.argv[2]
+
+ opencloud_auth=(username, password)
+
+ sites = get_nodes_by_site()
+
+ for site in sites.values():
+ if not site["hostnames"]:
+ continue
+
+ print "[%s]" % site["name"]
+ for hostname in site["hostnames"]:
+ print hostname
+ print ""
+
+ print "[all-opencloud:children]"
+ for site in sites.values():
+ if not site["hostnames"]:
+ continue
+ print site["name"]
+
+if __name__ == "__main__":
+ main()
+