[CORD-772] Persisting GUI Extensions
Change-Id: Ib5d3cbec98d89ead39e1df22fd1e2593589fcdb4
diff --git a/xos/core/models/__init__.py b/xos/core/models/__init__.py
index 67489a4..c7f0297 100644
--- a/xos/core/models/__init__.py
+++ b/xos/core/models/__init__.py
@@ -5,11 +5,11 @@
from .service import Service, Tenant, TenantWithContainer, CoarseTenant, ServicePrivilege, TenantRoot, TenantRootPrivilege, TenantRootRole, TenantPrivilege, TenantRole, Subscriber, Provider
from .service import ServiceAttribute, TenantAttribute, ServiceRole, ServiceMonitoringAgentInfo
from .service import ServiceController, ServiceControllerResource, LoadableModule, LoadableModuleResource, Library
-from .service import XOSComponent, XOSComponentLink, XOSComponentVolume
+from .service import XOSComponent, XOSComponentLink, XOSComponentVolume, XOSComponentVolumeContainer
from .tag import Tag
from .role import Role
from .site import Site, Deployment, DeploymentRole, DeploymentPrivilege, Controller, ControllerRole, ControllerSite, SiteDeployment,Diag
-from .dashboard import DashboardView, ControllerDashboardView
+from .dashboard import DashboardView, ControllerDashboardView, XOSGuiExtension
from .user import User, UserDashboardView
from .serviceclass import ServiceClass
from .site import ControllerManager, ControllerDeletionManager, ControllerLinkManager,ControllerLinkDeletionManager
diff --git a/xos/core/models/dashboard.py b/xos/core/models/dashboard.py
index f8870bb..d57c8ba 100644
--- a/xos/core/models/dashboard.py
+++ b/xos/core/models/dashboard.py
@@ -4,6 +4,7 @@
from core.models.plcorebase import StrippedCharField, ModelLink
from core.models.site import ControllerLinkManager, ControllerLinkDeletionManager
+
class DashboardView(PlCoreBase):
name = StrippedCharField(max_length=200, unique=True, help_text="Name of the View")
url = StrippedCharField(max_length=1024, help_text="URL of Dashboard")
@@ -13,8 +14,8 @@
icon_active = models.CharField(max_length=200, default="default-icon-active.png", help_text="Icon for active Dashboard")
deployments = models.ManyToManyField(Deployment, blank=True, related_name="dashboardviews", help_text="Deployments that should be included in this view")
+ def __unicode__(self): return u'%s' % (self.name)
- def __unicode__(self): return u'%s' % (self.name)
class ControllerDashboardView(PlCoreBase):
objects = ControllerLinkManager()
@@ -23,7 +24,13 @@
dashboardView = models.ForeignKey(DashboardView, related_name='controllerdashboardviews')
enabled = models.BooleanField(default=True)
url = StrippedCharField(max_length=1024, help_text="URL of Dashboard")
- xos_links=[ModelLink(Controller,via='controller'),ModelLink(DashboardView,via='dashboardview')]
+ xos_links = [ModelLink(Controller, via='controller'), ModelLink(DashboardView, via='dashboardview')]
+class XOSGuiExtension(PlCoreBase):
+ """Persist GUI Extension"""
+ class Meta:
+ app_label = "core"
+ name = StrippedCharField(max_length=200, unique=True, help_text="Name of the GUI Extensions")
+ files = StrippedCharField(max_length=1024, help_text="List of comma separated file composing the view")
diff --git a/xos/core/models/service.py b/xos/core/models/service.py
index c864ff9..58891f3 100644
--- a/xos/core/models/service.py
+++ b/xos/core/models/service.py
@@ -229,7 +229,7 @@
# NOTE can this be the same of XOSVolume??
class XOSComponentVolume(PlCoreBase):
component = models.ForeignKey(XOSComponent, related_name='volumes', help_text="The Component object for this Volume")
- name = StrippedCharField(max_length=30, help_text="Volume Name")
+ name = StrippedCharField(max_length=300, help_text="Volume Name")
container_path = StrippedCharField(max_length=1024, unique=True, help_text="Path of Volume in Container")
host_path = StrippedCharField(max_length=1024, help_text="Path of Volume in Host")
read_only = models.BooleanField(default=False, help_text="True if mount read-only")
@@ -241,6 +241,18 @@
super(XOSComponentVolume, self).save(*args, **kwds)
+class XOSComponentVolumeContainer(PlCoreBase):
+ component = models.ForeignKey(XOSComponent, related_name='volumecontainers', help_text="The Component object for this VolumeContainer")
+ name = StrippedCharField(max_length=300, help_text="Volume Name")
+ container = StrippedCharField(max_length=300, help_text="Volume Name")
+
+ def save(self, *args, **kwds):
+ existing = XOSComponentVolumeContainer.objects.filter(name=self.name)
+ if len(existing) > 0:
+ raise XOSValidationError('XOSComponentVolumeContainer for %s:%s already defined' % (self.container_path, self.host_path))
+ super(XOSComponentVolumeContainer, self).save(*args, **kwds)
+
+
class ServiceController(LoadableModule):
synchronizer_run = StrippedCharField(max_length=1024, help_text="synchronizer run command", null=True, blank=True)
synchronizer_config = StrippedCharField(max_length=1024, help_text="synchronizer config file", null=True, blank=True)
diff --git a/xos/synchronizers/onboarding/templates/docker-compose.yml.j2 b/xos/synchronizers/onboarding/templates/docker-compose.yml.j2
index 72824f8..55252d1 100644
--- a/xos/synchronizers/onboarding/templates/docker-compose.yml.j2
+++ b/xos/synchronizers/onboarding/templates/docker-compose.yml.j2
@@ -45,11 +45,19 @@
{%- for volume in container.volumes %}
{%- if volume.read_only %}
- {{ volume.host_path }}:{{ volume.container_path }}:ro
+{%- elif volume.host_path == "" %}
+ - {{ volume.container_path }}
{%- else %}
- {{ volume.host_path }}:{{ volume.container_path }}
{%- endif %}
{%- endfor %}
{%- endif %}
+{%- if container.volumes_from %}
+ volumes_from:
+{%- for volume in container.volumes_from %}
+ - {{ volume }}
+{%- endfor %}
+{%- endif %}
{%- if container.expose %}
expose:
{%- for expose in container.expose %}
diff --git a/xos/synchronizers/onboarding/xosbuilder.py b/xos/synchronizers/onboarding/xosbuilder.py
index 864e972..31d9a5f 100644
--- a/xos/synchronizers/onboarding/xosbuilder.py
+++ b/xos/synchronizers/onboarding/xosbuilder.py
@@ -379,17 +379,28 @@
"container_path": volume.container_path,
"read_only": volume.read_only})
- port = c.ports.split(":")
+ # creating containervolumes list
+ component_containervolume_list = []
+ for volume in c.volumecontainers.all():
+ component_containervolume_list.append(volume.container)
+
+ if c.ports:
+ port = c.ports.split(":")
+ ports = {
+ port[0]: port[1]
+ }
+ else:
+ ports = {}
+
containers[c.name] = {
"image": c.image,
"command": c.command,
"networks": networks,
- "ports": {
- port[0]: port[1]
- },
+ "ports": ports,
"links": component_links,
"external_links": component_external_links,
- "volumes": component_volume_list
+ "volumes": component_volume_list,
+ "volumes_from": component_containervolume_list,
}
if c.no_start:
diff --git a/xos/tosca/custom_types/xos.m4 b/xos/tosca/custom_types/xos.m4
index 7ec5581..409e036 100644
--- a/xos/tosca/custom_types/xos.m4
+++ b/xos/tosca/custom_types/xos.m4
@@ -66,6 +66,16 @@
required: false
description: True if mount read only
+ tosca.nodes.XOSGuiExtension:
+ derived_from: tosca.nodes.Root
+ description: A GUI Extension that can be loaded at runtime and need to be persisted
+ properties:
+ xos_base_props
+ files:
+ type: string
+ required: false
+ description: List of comma separated file composing the view
+
tosca.nodes.Service:
derived_from: tosca.nodes.Root
description: >
@@ -301,6 +311,21 @@
required: false
description: True if mount read only
+ tosca.nodes.ComponentVolumeContainer:
+ derived_from: tosca.nodes.Root
+ description: >
+ Container Volumes used by XOS components.
+ properties:
+ xos_base_props
+ name:
+ type: string
+ required: false
+ description: Identifier of the Container Volume
+ container:
+ type: string
+ required: false
+ description: Name of the Container Volume
+
tosca.relationships.LinkOfComponent:
derived_from: tosca.relationships.Root
valid_target_types: [ tosca.capabilities.xos.ComponentLink ]
@@ -309,6 +334,10 @@
derived_from: tosca.relationships.Root
valid_target_types: [ tosca.capabilities.xos.ComponentVolume ]
+ tosca.relationships.VolumeContainerOfComponent:
+ derived_from: tosca.relationships.Root
+ valid_target_types: [ tosca.capabilities.xos.ComponentVolumeContainer ]
+
tosca.nodes.Tenant:
derived_from: tosca.nodes.Root
description: >
diff --git a/xos/tosca/custom_types/xos.yaml b/xos/tosca/custom_types/xos.yaml
index 62b9bc6..1af0009 100644
--- a/xos/tosca/custom_types/xos.yaml
+++ b/xos/tosca/custom_types/xos.yaml
@@ -111,6 +111,31 @@
required: false
description: True if mount read only
+ tosca.nodes.XOSGuiExtension:
+ derived_from: tosca.nodes.Root
+ description: A GUI Extension that can be loaded at runtime and need to be persisted
+ properties:
+ no-delete:
+ type: boolean
+ default: false
+ description: Do not allow Tosca to delete this object
+ no-create:
+ type: boolean
+ default: false
+ description: Do not allow Tosca to create this object
+ no-update:
+ type: boolean
+ default: false
+ description: Do not allow Tosca to update this object
+ replaces:
+ type: string
+ required: false
+ descrption: Replaces/renames this object
+ files:
+ type: string
+ required: false
+ description: List of comma separated file composing the view
+
tosca.nodes.Service:
derived_from: tosca.nodes.Root
description: >
@@ -484,6 +509,36 @@
required: false
description: True if mount read only
+ tosca.nodes.ComponentVolumeContainer:
+ derived_from: tosca.nodes.Root
+ description: >
+ Container Volumes used by XOS components.
+ properties:
+ no-delete:
+ type: boolean
+ default: false
+ description: Do not allow Tosca to delete this object
+ no-create:
+ type: boolean
+ default: false
+ description: Do not allow Tosca to create this object
+ no-update:
+ type: boolean
+ default: false
+ description: Do not allow Tosca to update this object
+ replaces:
+ type: string
+ required: false
+ descrption: Replaces/renames this object
+ name:
+ type: string
+ required: false
+ description: Identifier of the Container Volume
+ container:
+ type: string
+ required: false
+ description: Name of the Container Volume
+
tosca.relationships.LinkOfComponent:
derived_from: tosca.relationships.Root
valid_target_types: [ tosca.capabilities.xos.ComponentLink ]
@@ -492,6 +547,10 @@
derived_from: tosca.relationships.Root
valid_target_types: [ tosca.capabilities.xos.ComponentVolume ]
+ tosca.relationships.VolumeContainerOfComponent:
+ derived_from: tosca.relationships.Root
+ valid_target_types: [ tosca.capabilities.xos.ComponentVolumeContainer ]
+
tosca.nodes.Tenant:
derived_from: tosca.nodes.Root
description: >
diff --git a/xos/tosca/resources/dashboardview.py b/xos/tosca/resources/dashboardview.py
index 94ed911..a7bd265 100644
--- a/xos/tosca/resources/dashboardview.py
+++ b/xos/tosca/resources/dashboardview.py
@@ -1,5 +1,5 @@
from xosresource import XOSResource
-from core.models import DashboardView, Site, Deployment, SiteDeployment
+from core.models import DashboardView, Site, Deployment, SiteDeployment, XOSGuiExtension
class XOSDashboardView(XOSResource):
provides = "tosca.nodes.DashboardView"
@@ -36,3 +36,9 @@
self.postprocess(dashboard)
self.info("Created DashboardView '%s'" % (str(dashboard), ))
+
+
+class XOSXOSGuiExtension(XOSResource):
+ provides = "tosca.nodes.XOSGuiExtension"
+ xos_model = XOSGuiExtension
+ copyin_props = ["name", "files"]
diff --git a/xos/tosca/resources/xoscomponent.py b/xos/tosca/resources/xoscomponent.py
index a6c86bb..6a10c73 100644
--- a/xos/tosca/resources/xoscomponent.py
+++ b/xos/tosca/resources/xoscomponent.py
@@ -1,5 +1,5 @@
from xosresource import XOSResource
-from core.models import XOSComponent, XOSComponentLink, XOSComponentVolume
+from core.models import XOSComponent, XOSComponentLink, XOSComponentVolume, XOSComponentVolumeContainer
class XOSXOSComponent(XOSResource):
@@ -38,3 +38,19 @@
args["component"] = self.get_xos_object(XOSComponent, throw_exception=throw_exception, name=component_name)
return args
+
+
+class XOSXOSComponentVolumeContainer(XOSResource):
+ provides = "tosca.nodes.ComponentVolumeContainer"
+ xos_model = XOSComponentVolumeContainer
+ copyin_props = ["name", "container"]
+ name_field = "name"
+
+ def get_xos_args(self, throw_exception=True):
+ args = super(XOSXOSComponentVolumeContainer, self).get_xos_args()
+
+ component_name = self.get_requirement("tosca.relationships.VolumeContainerOfComponent", throw_exception=throw_exception)
+ if component_name:
+ args["component"] = self.get_xos_object(XOSComponent, throw_exception=throw_exception, name=component_name)
+
+ return args
\ No newline at end of file
diff --git a/xos/xos/xosapi.py b/xos/xos/xosapi.py
index f343f33..3b3d12e 100644
--- a/xos/xos/xosapi.py
+++ b/xos/xos/xosapi.py
@@ -96,6 +96,9 @@
url(r'xos/flavors/$', FlavorList.as_view(), name='flavor-list-legacy'),
url(r'xos/flavors/(?P<pk>[a-zA-Z0-9\-]+)/$', FlavorDetail.as_view(), name ='flavor-detail-legacy'),
+ url(r'xos/xosguiextensions/$', XOSGuiExtensionList.as_view(), name='xosguiextension-list-legacy'),
+ url(r'xos/xosguiextensions/(?P<pk>[a-zA-Z0-9\-]+)/$', XOSGuiExtensionDetail.as_view(), name ='xosguiextension-detail-legacy'),
+
url(r'xos/ports/$', PortList.as_view(), name='port-list-legacy'),
url(r'xos/ports/(?P<pk>[a-zA-Z0-9\-]+)/$', PortDetail.as_view(), name ='port-detail-legacy'),
@@ -346,6 +349,9 @@
url(r'api/core/flavors/$', FlavorList.as_view(), name='flavor-list'),
url(r'api/core/flavors/(?P<pk>[a-zA-Z0-9\-]+)/$', FlavorDetail.as_view(), name ='flavor-detail'),
+ url(r'api/core/xosguiextensions/$', XOSGuiExtensionList.as_view(), name='xosguiextension-list'),
+ url(r'api/core/xosguiextensions/(?P<pk>[a-zA-Z0-9\-]+)/$', XOSGuiExtensionDetail.as_view(), name ='xosguiextension-detail'),
+
url(r'api/core/ports/$', PortList.as_view(), name='port-list'),
url(r'api/core/ports/(?P<pk>[a-zA-Z0-9\-]+)/$', PortDetail.as_view(), name ='port-detail'),
@@ -559,6 +565,7 @@
'invoices': reverse('invoice-list-legacy', request=request, format=format),
'sliceprivileges': reverse('sliceprivilege-list-legacy', request=request, format=format),
'flavors': reverse('flavor-list-legacy', request=request, format=format),
+ 'xosguiextensions': reverse('xosguiextension-list-legacy', request=request, format=format),
'ports': reverse('port-list-legacy', request=request, format=format),
'serviceroles': reverse('servicerole-list-legacy', request=request, format=format),
'controllersites': reverse('controllersite-list-legacy', request=request, format=format),
@@ -647,6 +654,7 @@
'invoices': reverse('invoice-list', request=request, format=format),
'sliceprivileges': reverse('sliceprivilege-list', request=request, format=format),
'flavors': reverse('flavor-list', request=request, format=format),
+ 'xosguiextensions': reverse('xosguiextension-list', request=request, format=format),
'ports': reverse('port-list', request=request, format=format),
'serviceroles': reverse('servicerole-list', request=request, format=format),
'controllersites': reverse('controllersite-list', request=request, format=format),
@@ -1325,7 +1333,7 @@
return None
class Meta:
model = XOSComponent
- fields = ('humanReadableName', 'validators', 'id','created','updated','enacted','policed','backend_register','backend_need_delete','backend_need_reap','backend_status','deleted','write_protect','lazy_blocked','no_sync','no_policy','xos','name','base_url','version','provides','requires','image','command','ports','extra',)
+ fields = ('humanReadableName', 'validators', 'id','created','updated','enacted','policed','backend_register','backend_need_delete','backend_need_reap','backend_status','deleted','write_protect','lazy_blocked','no_sync','no_policy','xos','name','base_url','version','provides','requires','image','command','ports','extra','no_start',)
class XOSComponentIdSerializer(XOSModelSerializer):
id = IdField()
@@ -1341,7 +1349,7 @@
return None
class Meta:
model = XOSComponent
- fields = ('humanReadableName', 'validators', 'id','created','updated','enacted','policed','backend_register','backend_need_delete','backend_need_reap','backend_status','deleted','write_protect','lazy_blocked','no_sync','no_policy','xos','name','base_url','version','provides','requires','image','command','ports','extra',)
+ fields = ('humanReadableName', 'validators', 'id','created','updated','enacted','policed','backend_register','backend_need_delete','backend_need_reap','backend_status','deleted','write_protect','lazy_blocked','no_sync','no_policy','xos','name','base_url','version','provides','requires','image','command','ports','extra','no_start',)
@@ -1459,6 +1467,41 @@
+class XOSGuiExtensionSerializer(serializers.HyperlinkedModelSerializer):
+ id = IdField()
+
+ humanReadableName = serializers.SerializerMethodField("getHumanReadableName")
+ validators = serializers.SerializerMethodField("getValidators")
+ def getHumanReadableName(self, obj):
+ return str(obj)
+ def getValidators(self, obj):
+ try:
+ return obj.getValidators()
+ except:
+ return None
+ class Meta:
+ model = XOSGuiExtension
+ fields = ('humanReadableName', 'validators', 'id','created','updated','enacted','policed','backend_register','backend_need_delete','backend_need_reap','backend_status','deleted','write_protect','lazy_blocked','no_sync','no_policy','name','files',)
+
+class XOSGuiExtensionIdSerializer(XOSModelSerializer):
+ id = IdField()
+
+ humanReadableName = serializers.SerializerMethodField("getHumanReadableName")
+ validators = serializers.SerializerMethodField("getValidators")
+ def getHumanReadableName(self, obj):
+ return str(obj)
+ def getValidators(self, obj):
+ try:
+ return obj.getValidators()
+ except:
+ return None
+ class Meta:
+ model = XOSGuiExtension
+ fields = ('humanReadableName', 'validators', 'id','created','updated','enacted','policed','backend_register','backend_need_delete','backend_need_reap','backend_status','deleted','write_protect','lazy_blocked','no_sync','no_policy','name','files',)
+
+
+
+
class PortSerializer(serializers.HyperlinkedModelSerializer):
id = IdField()
@@ -3856,6 +3899,8 @@
Flavor: FlavorSerializer,
+ XOSGuiExtension: XOSGuiExtensionSerializer,
+
Port: PortSerializer,
ServiceRole: ServiceRoleSerializer,
@@ -4698,7 +4743,7 @@
serializer_class = XOSComponentSerializer
id_serializer_class = XOSComponentIdSerializer
filter_backends = (filters.DjangoFilterBackend,)
- filter_fields = ('id','created','updated','enacted','policed','backend_register','backend_need_delete','backend_need_reap','backend_status','deleted','write_protect','lazy_blocked','no_sync','no_policy','xos','name','base_url','version','provides','requires','image','command','ports','extra',)
+ filter_fields = ('id','created','updated','enacted','policed','backend_register','backend_need_delete','backend_need_reap','backend_status','deleted','write_protect','lazy_blocked','no_sync','no_policy','xos','name','base_url','version','provides','requires','image','command','ports','extra','no_start',)
def get_serializer_class(self):
no_hyperlinks=False
@@ -4881,6 +4926,53 @@
+class XOSGuiExtensionList(XOSListCreateAPIView):
+ queryset = XOSGuiExtension.objects.select_related().all()
+ serializer_class = XOSGuiExtensionSerializer
+ id_serializer_class = XOSGuiExtensionIdSerializer
+ filter_backends = (filters.DjangoFilterBackend,)
+ filter_fields = ('id','created','updated','enacted','policed','backend_register','backend_need_delete','backend_need_reap','backend_status','deleted','write_protect','lazy_blocked','no_sync','no_policy','name','files',)
+
+ def get_serializer_class(self):
+ no_hyperlinks=False
+ if hasattr(self.request,"query_params"):
+ no_hyperlinks = self.request.query_params.get('no_hyperlinks', False)
+ if (no_hyperlinks):
+ return self.id_serializer_class
+ else:
+ return self.serializer_class
+
+ def get_queryset(self):
+ if (not self.request.user.is_authenticated()):
+ raise XOSNotAuthenticated()
+ return XOSGuiExtension.select_by_user(self.request.user)
+
+
+class XOSGuiExtensionDetail(XOSRetrieveUpdateDestroyAPIView):
+ queryset = XOSGuiExtension.objects.select_related().all()
+ serializer_class = XOSGuiExtensionSerializer
+ id_serializer_class = XOSGuiExtensionIdSerializer
+
+ def get_serializer_class(self):
+ no_hyperlinks=False
+ if hasattr(self.request,"query_params"):
+ no_hyperlinks = self.request.query_params.get('no_hyperlinks', False)
+ if (no_hyperlinks):
+ return self.id_serializer_class
+ else:
+ return self.serializer_class
+
+ def get_queryset(self):
+ if (not self.request.user.is_authenticated()):
+ raise XOSNotAuthenticated()
+ return XOSGuiExtension.select_by_user(self.request.user)
+
+ # update() is handled by XOSRetrieveUpdateDestroyAPIView
+
+ # destroy() is handled by XOSRetrieveUpdateDestroyAPIView
+
+
+
class PortList(XOSListCreateAPIView):
queryset = Port.objects.select_related().all()
serializer_class = PortSerializer