[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