Library support

Change-Id: I25c37cb3aeea767896fab87e4b1fb11449abe8a0
diff --git a/xos/core/admin.py b/xos/core/admin.py
index 83492ab..fa33a7f 100644
--- a/xos/core/admin.py
+++ b/xos/core/admin.py
@@ -1051,8 +1051,8 @@
                       ('serviceprivileges', 'Privileges')
                       )
 
-class ServiceControllerResourceInline(XOSTabularInline):
-    model = ServiceControllerResource
+class LoadableModuleResourceInline(XOSTabularInline):
+    model = LoadableModuleResource
     fields = ['name', 'kind', 'format', 'url']
     extra = 0
     suit_classes = 'suit-tab suit-tab-resources'
@@ -1063,12 +1063,27 @@
     fieldList = ["backend_status_text", "name", "xos", "version", "provides", "requires", "base_url", "synchronizer_run", "synchronizer_config", "no_start"]
     fieldsets = [
         (None, {'fields': fieldList, 'classes': ['suit-tab suit-tab-general']})]
-    inlines = [ServiceControllerResourceInline]
+    inlines = [LoadableModuleResourceInline]
     readonly_fields = ('backend_status_text', )
 
     user_readonly_fields = fieldList
 
-    suit_form_tabs = (('general', 'Service Details'),
+    suit_form_tabs = (('general', 'Service Controller Details'),
+                      ('resources', 'Resources'),
+                      )
+
+class LibraryAdmin(XOSBaseAdmin):
+    list_display = ("backend_status_icon", "name",)
+    list_display_links = ('backend_status_icon', 'name',)
+    fieldList = ["backend_status_text", "name", "xos", "version", "provides", "requires", "base_url"]
+    fieldsets = [
+        (None, {'fields': fieldList, 'classes': ['suit-tab suit-tab-general']})]
+    inlines = [LoadableModuleResourceInline]
+    readonly_fields = ('backend_status_text', )
+
+    user_readonly_fields = fieldList
+
+    suit_form_tabs = (('general', 'Library Details'),
                       ('resources', 'Resources'),
                       )
 
@@ -2451,6 +2466,7 @@
 admin.site.register(Slice, SliceAdmin)
 admin.site.register(Service, ServiceAdmin)
 admin.site.register(ServiceController, ServiceControllerAdmin)
+admin.site.register(Library, LibraryAdmin)
 admin.site.register(XOS, XosModelAdmin)
 admin.site.register(Network, NetworkAdmin)
 admin.site.register(Port, PortAdmin)
diff --git a/xos/core/models/__init__.py b/xos/core/models/__init__.py
index abf8da2..4138b22 100644
--- a/xos/core/models/__init__.py
+++ b/xos/core/models/__init__.py
@@ -4,7 +4,7 @@
 from .xosmodel import XOS, XOSVolume
 from .service import Service, Tenant, TenantWithContainer, CoarseTenant, ServicePrivilege, TenantRoot, TenantRootPrivilege, TenantRootRole, TenantPrivilege, TenantRole, Subscriber, Provider
 from .service import ServiceAttribute, TenantAttribute, ServiceRole
-from .service import ServiceController, ServiceControllerResource, LoadableModule, LoadableModuleResource
+from .service import ServiceController, ServiceControllerResource, LoadableModule, LoadableModuleResource, Library
 from .tag import Tag
 from .role import Role
 from .site import Site, Deployment, DeploymentRole, DeploymentPrivilege, Controller, ControllerRole, ControllerSite, SiteDeployment,Diag
diff --git a/xos/core/models/service.py b/xos/core/models/service.py
index 2685fe4..a6ee7a8 100644
--- a/xos/core/models/service.py
+++ b/xos/core/models/service.py
@@ -187,6 +187,10 @@
         else:
             return self.url
 
+class Library(LoadableModule):
+    # for now, it's exactly like a LoadableModule
+    pass
+
 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/model-deps b/xos/synchronizers/onboarding/model-deps
index 4aa86c7..e887a31 100644
--- a/xos/synchronizers/onboarding/model-deps
+++ b/xos/synchronizers/onboarding/model-deps
@@ -1,8 +1,12 @@
 {
     "XOS": [
-        "ServiceController"
+        "ServiceController",
+        "Library"
     ], 
     "ServiceController": [
         "ServiceControllerResource"
+    ],
+    "Library": [
+        "ServiceControllerResource"
     ]
 }
diff --git a/xos/synchronizers/onboarding/steps/sync_library.py b/xos/synchronizers/onboarding/steps/sync_library.py
new file mode 100644
index 0000000..5a8a4c1
--- /dev/null
+++ b/xos/synchronizers/onboarding/steps/sync_library.py
@@ -0,0 +1,47 @@
+import os
+import sys
+import base64
+from django.db.models import F, Q
+from xos.config import Config
+from synchronizers.base.syncstep import SyncStep, DeferredException
+from core.models import XOS, Library
+from xos.logger import Logger, logging
+from synchronizers.base.ansible import run_template
+
+# xosbuilder will be in steps/..
+parentdir = os.path.join(os.path.dirname(__file__),"..")
+sys.path.insert(0,parentdir)
+
+from xosbuilder import XOSBuilder
+
+logger = Logger(level=logging.INFO)
+
+class SyncLibrary(SyncStep, XOSBuilder):
+    provides=[Library]
+    observes=Library
+    requested_interval=0
+
+    def __init__(self, **args):
+        SyncStep.__init__(self, **args)
+        XOSBuilder.__init__(self)
+
+    def sync_record(self, sc):
+        logger.info("Sync'ing Library %s" % sc)
+
+        if sc.xos and (not sc.xos.enable_build):
+            raise DeferredException("XOS build is currently disabled")
+
+        unready = self.check_controller_unready(sc)
+        if unready:
+            raise Exception("Controller %s has unready resources: %s" % (str(sc), ",".join([str(x) for x in unready])))
+
+        # There's nothing to actually do, since there's no synchronizer
+        # container for libraries.
+
+    def delete_record(self, m):
+        pass
+
+    def fetch_pending(self, deleted=False):
+        pend = super(SyncLibrary, self).fetch_pending(deleted)
+        return pend
+
diff --git a/xos/tosca/custom_types/xos.m4 b/xos/tosca/custom_types/xos.m4
index 6372f9c..f500ca7 100644
--- a/xos/tosca/custom_types/xos.m4
+++ b/xos/tosca/custom_types/xos.m4
@@ -164,6 +164,69 @@
                 required: false
                 description: url of resource, may be relative to base_url or absolute
 
+    tosca.nodes.Library:
+        derived_from: tosca.nodes.Root
+        description: >
+            An XOS Library.
+        properties:
+            xos_base_props
+            base_url:
+                type: string
+                required: false
+                description: Base url, to allow resources to use relative URLs
+            version:
+                type: string
+                required: false
+                description: Version number of this Service Controller
+            provides:
+                type: string
+                required: false
+                description: Comma-separated list of things provided
+            requires:
+                type: string
+                required: false
+                description: Comma-separated list of requirements
+            models:
+                type: string
+                required: false
+                description: url of models.py
+            admin:
+                type: string
+                required: false
+                description: url of admin.py
+            django_library:
+                type: string
+                required: false
+                description: libraries used by admin or models
+            admin_template:
+                type: string
+                required: false
+                description: url of admin html template
+            tosca_custom_types:
+                type: string
+                required: false
+                description: url of tosca custom_types
+            tosca_resource:
+                type: string
+                required: false
+                description: url of tosca resource
+            rest_service:
+                type: string
+                required: false
+                description: url of REST API service file
+            rest_tenant:
+                type: string
+                required: false
+                description: url of REST API tenant file
+            private_key:
+                type: string
+                required: false
+                description: private key
+            public_key:
+                type: string
+                required: false
+                description: public key
+
     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 44ade1f..9c79786 100644
--- a/xos/tosca/custom_types/xos.yaml
+++ b/xos/tosca/custom_types/xos.yaml
@@ -287,6 +287,84 @@
                 required: false
                 description: url of resource, may be relative to base_url or absolute
 
+    tosca.nodes.Library:
+        derived_from: tosca.nodes.Root
+        description: >
+            An XOS Library.
+        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
+            base_url:
+                type: string
+                required: false
+                description: Base url, to allow resources to use relative URLs
+            version:
+                type: string
+                required: false
+                description: Version number of this Service Controller
+            provides:
+                type: string
+                required: false
+                description: Comma-separated list of things provided
+            requires:
+                type: string
+                required: false
+                description: Comma-separated list of requirements
+            models:
+                type: string
+                required: false
+                description: url of models.py
+            admin:
+                type: string
+                required: false
+                description: url of admin.py
+            django_library:
+                type: string
+                required: false
+                description: libraries used by admin or models
+            admin_template:
+                type: string
+                required: false
+                description: url of admin html template
+            tosca_custom_types:
+                type: string
+                required: false
+                description: url of tosca custom_types
+            tosca_resource:
+                type: string
+                required: false
+                description: url of tosca resource
+            rest_service:
+                type: string
+                required: false
+                description: url of REST API service file
+            rest_tenant:
+                type: string
+                required: false
+                description: url of REST API tenant file
+            private_key:
+                type: string
+                required: false
+                description: private key
+            public_key:
+                type: string
+                required: false
+                description: public key
+
     tosca.nodes.Tenant:
         derived_from: tosca.nodes.Root
         description: >
diff --git a/xos/tosca/resources/library.py b/xos/tosca/resources/library.py
new file mode 100644
index 0000000..777fc3a
--- /dev/null
+++ b/xos/tosca/resources/library.py
@@ -0,0 +1,16 @@
+from core.models import LoadableModule, LoadableModuleResource, Library
+
+from loadablemodule import XOSLoadableModule
+
+# This is like ServiceController, but with the synchronizer stuff removed
+
+class XOSLibrary(XOSLoadableModule):
+    provides = "tosca.nodes.Library"
+    xos_model = Library
+    copyin_props = ["version", "provides", "requires", "base_url"]
+
+    def postprocess(self, obj):
+        super(XOSLibrary, self).postprocess(obj)
+
+
+
diff --git a/xos/tosca/resources/loadablemodule.py b/xos/tosca/resources/loadablemodule.py
new file mode 100644
index 0000000..92e3ebb
--- /dev/null
+++ b/xos/tosca/resources/loadablemodule.py
@@ -0,0 +1,68 @@
+from core.models import LoadableModule, LoadableModuleResource
+
+from xosresource import XOSResource
+
+class XOSLoadableModule(XOSResource):
+
+    # This doesn't provide anything. It's a base class for XOSLibrary and XOSServiceController
+
+    def postprocess_resource_prop(self, obj, kind, format):
+        values = self.get_property(kind)
+        if values:
+            for i,value in enumerate(values.split(",")):
+                value = value.strip()
+                subdirectory = None
+
+                name=kind
+                if i>0:
+                    name = "%s_%d" %( name, i)
+
+                if (" " in value):
+                    parts=value.split()
+                    for part in parts[:-1]:
+                       if ":" in part:
+                           (lhs, rhs) = part.split(":", 1)
+                           if lhs=="subdirectory":
+                               subdirectory=rhs
+                           else:
+                               raise Exception("Malformed value %s" % value)
+                       else:
+                           raise Exception("Malformed value %s" % value)
+                    value = parts[-1]
+
+
+                scr = LoadableModuleResource.objects.filter(loadable_module=obj, name=name, kind=kind, format=format)
+                if scr:
+                    scr=scr[0]
+                    if (scr.url != value) or (scr.subdirectory!=subdirectory):
+                        self.info("updating resource %s" % kind)
+                        scr.url = value
+                        scr.subdirectory = subdirectory
+                        scr.save()
+                else:
+                    self.info("adding resource %s" % kind)
+                    scr = LoadableModuleResource(loadable_module=obj, name=name, kind=kind, format=format, url=value, subdirectory=subdirectory)
+                    scr.save()
+
+    def postprocess(self, obj):
+        # allow these common resource to be specified directly by the LoadableModule tosca object and its descendents
+        self.postprocess_resource_prop(obj, "models", "python")
+        self.postprocess_resource_prop(obj, "admin", "python")
+        self.postprocess_resource_prop(obj, "django_library", "python")
+        self.postprocess_resource_prop(obj, "admin_template", "raw")
+        self.postprocess_resource_prop(obj, "tosca_custom_types", "yaml")
+        self.postprocess_resource_prop(obj, "tosca_resource", "python")
+        self.postprocess_resource_prop(obj, "private_key", "raw")
+        self.postprocess_resource_prop(obj, "public_key", "raw")
+        self.postprocess_resource_prop(obj, "rest_service", "python")
+        self.postprocess_resource_prop(obj, "rest_tenant", "python")
+
+    def save_created_obj(self, xos_obj):
+        if xos_obj.requires and xos_obj.requires.strip():
+            (satisfied, missing) = LoadableModule.dependency_check([x.strip() for x in xos_obj.requires.split(",")])
+            if missing:
+                raise Exception("missing dependencies for Loadable Module %s: %s" % (xos_obj.name, ", ".join(missing)))
+
+        super(XOSLoadableModule, self).save_created_obj(xos_obj)
+
+
diff --git a/xos/tosca/resources/servicecontroller.py b/xos/tosca/resources/servicecontroller.py
index e14c604..e5fa6e2 100644
--- a/xos/tosca/resources/servicecontroller.py
+++ b/xos/tosca/resources/servicecontroller.py
@@ -1,77 +1,15 @@
-import os
-import pdb
-import sys
-import tempfile
-sys.path.append("/opt/tosca")
-from translator.toscalib.tosca_template import ToscaTemplate
-
 from core.models import ServiceController, ServiceControllerResource, LoadableModule, LoadableModuleResource
 
-from xosresource import XOSResource
+from loadablemodule import XOSLoadableModule
 
-class XOSServiceController(XOSResource):
+class XOSServiceController(XOSLoadableModule):
     provides = "tosca.nodes.ServiceController"
     xos_model = ServiceController
     copyin_props = ["version", "provides", "requires", "base_url", "synchronizer_run", "synchronizer_config"]
 
-    def postprocess_resource_prop(self, obj, kind, format):
-        values = self.get_property(kind)
-        if values:
-            for i,value in enumerate(values.split(",")):
-                value = value.strip()
-                subdirectory = None
-
-                name=kind
-                if i>0:
-                    name = "%s_%d" %( name, i)
-
-                if (" " in value):
-                    parts=value.split()
-                    for part in parts[:-1]:
-                       if ":" in part:
-                           (lhs, rhs) = part.split(":", 1)
-                           if lhs=="subdirectory":
-                               subdirectory=rhs
-                           else:
-                               raise Exception("Malformed value %s" % value)
-                       else:
-                           raise Exception("Malformed value %s" % value)
-                    value = parts[-1]
-
-
-                scr = LoadableModuleResource.objects.filter(loadable_module=obj, name=name, kind=kind, format=format)
-                if scr:
-                    scr=scr[0]
-                    if (scr.url != value) or (scr.subdirectory!=subdirectory):
-                        self.info("updating resource %s" % kind)
-                        scr.url = value
-                        scr.subdirectory = subdirectory
-                        scr.save()
-                else:
-                    self.info("adding resource %s" % kind)
-                    scr = LoadableModuleResource(loadable_module=obj, name=name, kind=kind, format=format, url=value, subdirectory=subdirectory)
-                    scr.save()
-
     def postprocess(self, obj):
         # allow these common resource to be specified directly by the ServiceController tosca object
-        self.postprocess_resource_prop(obj, "models", "python")
-        self.postprocess_resource_prop(obj, "admin", "python")
-        self.postprocess_resource_prop(obj, "django_library", "python")
-        self.postprocess_resource_prop(obj, "admin_template", "raw")
-        self.postprocess_resource_prop(obj, "tosca_custom_types", "yaml")
-        self.postprocess_resource_prop(obj, "tosca_resource", "python")
+        super(XOSServiceController, self).postprocess(obj)
         self.postprocess_resource_prop(obj, "synchronizer", "manifest")
-        self.postprocess_resource_prop(obj, "private_key", "raw")
-        self.postprocess_resource_prop(obj, "public_key", "raw")
-        self.postprocess_resource_prop(obj, "rest_service", "python")
-        self.postprocess_resource_prop(obj, "rest_tenant", "python")
-
-    def save_created_obj(self, xos_obj):
-        if xos_obj.requires and xos_obj.requires.strip():
-            (satisfied, missing) = ServiceController.dependency_check([x.strip() for x in xos_obj.requires.split(",")])
-            if missing:
-                raise Exception("missing dependencies for ServiceController %s: %s" % (xos_obj.name, ", ".join(missing)))
-
-        super(XOSServiceController, self).save_created_obj(xos_obj)
 
 
diff --git a/xos/tosca/samples/library-onboard.yaml b/xos/tosca/samples/library-onboard.yaml
new file mode 100644
index 0000000..30e20c9
--- /dev/null
+++ b/xos/tosca/samples/library-onboard.yaml
@@ -0,0 +1,18 @@
+tosca_definitions_version: tosca_simple_yaml_1_0
+
+description: Sample library onboard
+
+imports:
+   - custom_types/xos.yaml
+
+topology_template:
+  node_templates:
+    library#sample:
+      type: tosca.nodes.Library
+      properties:
+          base_url: file:///opt/xos/
+          tosca_resource: tosca/samples/samplelibrary.py
+          requires: vsg, vtn, onos
+
+
+
diff --git a/xos/tosca/samples/samplelibrary.py b/xos/tosca/samples/samplelibrary.py
new file mode 100644
index 0000000..7a6030a
--- /dev/null
+++ b/xos/tosca/samples/samplelibrary.py
@@ -0,0 +1,3 @@
+# some empty file used to test library onboarding
+
+pass