Add Program model to XOS
diff --git a/xos/core/admin.py b/xos/core/admin.py
index 82239bd..c52bd17 100644
--- a/xos/core/admin.py
+++ b/xos/core/admin.py
@@ -1898,6 +1898,46 @@
     dollar_total_invoices = dollar_field("total_invoices", "Total Invoices")
     dollar_total_payments = dollar_field("total_payments", "Total Payments")
 
+class ProgramForm(forms.ModelForm):
+    class Meta:
+        model = Program
+        widgets = {
+            'contents': UploadTextareaWidget(attrs={'rows': 20, 'cols': 80, 'class': "input-xxlarge"}),
+            'description': forms.Textarea(attrs={'rows': 3, 'cols': 80, 'class': 'input-xxlarge'}),
+            'messages': forms.Textarea(attrs={'rows': 20, 'cols': 80, 'class': 'input-xxlarge'}),
+            'output': forms.Textarea(attrs={'rows': 3, 'cols': 80, 'class': 'input-xxlarge'})
+        }
+
+class ProgramAdmin(XOSBaseAdmin):
+    list_display = ("name", "status")
+    list_display_links = ('name', "status")
+
+    form=ProgramForm
+
+    fieldsets = [
+        (None, {'fields': ['name', 'command', 'kind', 'description', 'output', 'status'],
+                'classes':['suit-tab suit-tab-general']}),
+        (None, {'fields': ['contents'],
+                'classes':['suit-tab suit-tab-contents']}),
+        (None, {'fields': ['messages'],
+                'classes':['suit-tab suit-tab-messages']}),
+                ]
+
+    readonly_fields = ("status",)
+
+    @property
+    def suit_form_tabs(self):
+        tabs=[('general','Program Details'),
+              ('contents','Program Source'),
+              ('messages','Messages'),
+        ]
+
+        request=getattr(_thread_locals, "request", None)
+        if request and request.user.is_admin:
+            tabs.append( ('admin-only', 'Admin-Only') )
+
+        return tabs
+
 # Now register the new UserAdmin...
 admin.site.register(User, UserAdmin)
 # ... and, since we're not using Django's builtin permissions,
@@ -1923,6 +1963,7 @@
 admin.site.register(Network, NetworkAdmin)
 admin.site.register(Router, RouterAdmin)
 admin.site.register(NetworkTemplate, NetworkTemplateAdmin)
+admin.site.register(Program, ProgramAdmin)
 #admin.site.register(Account, AccountAdmin)
 #admin.site.register(Invoice, InvoiceAdmin)
 
diff --git a/xos/core/models/__init__.py b/xos/core/models/__init__.py
index 973598d..6ade26b 100644
--- a/xos/core/models/__init__.py
+++ b/xos/core/models/__init__.py
@@ -28,4 +28,5 @@
 from .reservation import Reservation
 from .network import Network, NetworkParameterType, NetworkParameter, NetworkSliver, NetworkTemplate, Router, NetworkSlice, ControllerNetwork
 from .billing import Account, Invoice, Charge, UsableObject, Payment
+from .program import Program
 
diff --git a/xos/core/models/program.py b/xos/core/models/program.py
new file mode 100644
index 0000000..01e17fa
--- /dev/null
+++ b/xos/core/models/program.py
@@ -0,0 +1,42 @@
+from django.db import models
+from core.models import PlCoreBase,SingletonModel,PlCoreBaseManager,User
+from core.models.plcorebase import StrippedCharField
+from xos.exceptions import *
+from operator import attrgetter
+import json
+
+class Program(PlCoreBase):
+    KIND_CHOICES = (('tosca', 'Tosca'), )
+    COMMAND_CHOICES = (('run', 'Run'), ('destroy', 'Destroy'), )
+
+    name = StrippedCharField(max_length=30, help_text="Service Name")
+    description = models.TextField(max_length=254,null=True, blank=True,help_text="Description of Service")
+    kind = StrippedCharField(max_length=30, help_text="Kind of service", choices=KIND_CHOICES)
+    command = StrippedCharField(blank=True, null=True, max_length=30, help_text="Command to run", choices=COMMAND_CHOICES)
+
+    owner = models.ForeignKey(User, null=True, related_name="programs")
+
+    contents = models.TextField(blank=True, null=True, help_text="Contents of Program")
+    output = models.TextField(blank=True, null=True, help_text="Output of Program")
+    messages = models.TextField(blank=True, null=True, help_text="Debug messages")
+    status = models.TextField(blank=True, null=True, max_length=30, help_text="Status of program")
+
+    @classmethod
+    def select_by_user(cls, user):
+        return cls.objects.all()
+
+    def __unicode__(self): return u'%s' % (self.name)
+
+    def can_update(self, user):
+        return True
+
+    def save(self, *args, **kwargs):
+        # set creator on first save
+        if not self.owner and hasattr(self, 'caller'):
+            self.owner = self.caller
+
+        if (self.command in ["run", "destroy"]) and (self.status in ["complete", "exception"]):
+            self.status = "queued"
+
+        super(Program, self).save(*args, **kwargs)
+
diff --git a/xos/core/models/service.py b/xos/core/models/service.py
index 2b0d952..0940176 100644
--- a/xos/core/models/service.py
+++ b/xos/core/models/service.py
@@ -5,6 +5,8 @@
 from operator import attrgetter
 import json
 
+COARSE_KIND="coarse"
+
 class AttributeMixin(object):
     # helper for extracting things from a json-encoded service_specific_attribute
     def get_attribute(self, name, default=None):
@@ -336,7 +338,7 @@
     class Meta:
         proxy = True
 
-    KIND = "coarse"
+    KIND = COARSE_KIND
 
     def save(self, *args, **kwargs):
         if (not self.subscriber_service):