CORD-2538 implement UnloadModels
Change-Id: I28bcc840cc7d69580ffa7c27195ccadebbe5d63a
diff --git a/xos/coreapi/Makefile b/xos/coreapi/Makefile
index 9c20c37..c07f041 100644
--- a/xos/coreapi/Makefile
+++ b/xos/coreapi/Makefile
@@ -19,7 +19,10 @@
start:
bash -c "source env.sh && python ./core_main.py"
-prep: app_lists rebuild_protos compile_protos try_models makemigrations migrate
+prep: unload_unwanted app_lists rebuild_protos compile_protos try_models makemigrations migrate
+
+unload_unwanted:
+ python unload_unwanted_apps.py
app_lists:
python app_list_builder.py
diff --git a/xos/coreapi/dynamicbuild.py b/xos/coreapi/dynamicbuild.py
index 13032b3..da59b37 100644
--- a/xos/coreapi/dynamicbuild.py
+++ b/xos/coreapi/dynamicbuild.py
@@ -53,7 +53,7 @@
for item in request.attics:
self.pre_validate_file(item)
- def handle_loadmodels_request(self, request):
+ def load_manifest_from_request(self, request):
manifest_fn = os.path.join(self.manifest_dir, request.name + ".json")
if os.path.exists(manifest_fn):
try:
@@ -64,9 +64,14 @@
else:
manifest = {}
+ return (manifest, manifest_fn)
+
+ def handle_loadmodels_request(self, request):
+ (manifest, manifest_fn) = self.load_manifest_from_request(request)
+
# TODO: Check version number to make sure this is not a downgrade ?
- hash = self.generate_request_hash(request)
+ hash = self.generate_request_hash(request, state="load")
if hash == manifest.get("hash"):
# The hash of the incoming request is identical to the manifest that we have saved, so this request is a
# no-op.
@@ -75,7 +80,7 @@
self.pre_validate_models(request)
- manifest = self.save_models(request, hash=hash)
+ manifest = self.save_models(request, state="load", hash=hash)
self.run_xosgenx_service(manifest)
@@ -86,27 +91,49 @@
return self.SOMETHING_CHANGED
- # TODO: schedule a restart
+ def handle_unloadmodels_request(self, request):
+ (manifest, manifest_fn) = self.load_manifest_from_request(request)
- def generate_request_hash(self, request):
+ # TODO: Check version number to make sure this is not a downgrade ?
+
+ hash = self.generate_request_hash(request, state="unload")
+ if hash == manifest.get("hash"):
+ # The hash of the incoming request is identical to the manifest that we have saved, so this request is a
+ # no-op.
+ log.info("Models are already up-to-date; skipping dynamic unload.", name=request.name)
+ return self.NOTHING_TO_DO
+
+ manifest = self.save_models(request, state="unload", hash=hash)
+
+ self.remove_service(manifest)
+
+ log.debug("Saving service manifest", name=request.name)
+ file(manifest_fn, "w").write(json.dumps(manifest))
+
+ log.info("Finished UnloadModels request", name=request.name)
+
+ return self.SOMETHING_CHANGED
+
+ def generate_request_hash(self, request, state):
# TODO: could we hash the request rather than individually hashing the subcomponents of the request?
m = hashlib.sha1()
m.update(request.name)
m.update(request.version)
- for item in request.xprotos:
- m.update(item.filename)
- m.update(item.contents)
- for item in request.decls:
- m.update(item.filename)
- m.update(item.contents)
- for item in request.decls:
- m.update(item.filename)
- m.update(item.contents)
+ if (state == "load"):
+ for item in request.xprotos:
+ m.update(item.filename)
+ m.update(item.contents)
+ for item in request.decls:
+ m.update(item.filename)
+ m.update(item.contents)
+ for item in request.decls:
+ m.update(item.filename)
+ m.update(item.contents)
return m.hexdigest()
- def save_models(self, request, hash=None):
+ def save_models(self, request, state, hash=None):
if not hash:
- hash = self.generate_request_hash(request)
+ hash = self.generate_request_hash(request, state)
service_dir = os.path.join(self.services_dir, request.name)
if not os.path.exists(service_dir):
@@ -126,6 +153,7 @@
service_manifest = {"name": request.name,
"version": request.version,
"hash": hash,
+ "state": state,
"dir": service_dir,
"manifest_fn": manifest_fn,
"dest_dir": os.path.join(self.services_dest_dir, request.name),
@@ -133,22 +161,23 @@
"decls": [],
"attics": []}
- for item in request.xprotos:
- file(os.path.join(service_dir, item.filename), "w").write(item.contents)
- service_manifest["xprotos"].append({"filename": item.filename})
+ if (state == "load"):
+ for item in request.xprotos:
+ file(os.path.join(service_dir, item.filename), "w").write(item.contents)
+ service_manifest["xprotos"].append({"filename": item.filename})
- for item in request.decls:
- file(os.path.join(service_dir, item.filename), "w").write(item.contents)
- service_manifest["decls"].append({"filename": item.filename})
+ for item in request.decls:
+ file(os.path.join(service_dir, item.filename), "w").write(item.contents)
+ service_manifest["decls"].append({"filename": item.filename})
- if request.attics:
- attic_dir = os.path.join(service_dir, "attic")
- service_manifest["attic_dir"] = attic_dir
- if not os.path.exists(attic_dir):
- os.makedirs(attic_dir)
- for item in request.attics:
- file(os.path.join(attic_dir, item.filename), "w").write(item.contents)
- service_manifest["attics"].append({"filename": item.filename})
+ if request.attics:
+ attic_dir = os.path.join(service_dir, "attic")
+ service_manifest["attic_dir"] = attic_dir
+ if not os.path.exists(attic_dir):
+ os.makedirs(attic_dir)
+ for item in request.attics:
+ file(os.path.join(attic_dir, item.filename), "w").write(item.contents)
+ service_manifest["attics"].append({"filename": item.filename})
return service_manifest
@@ -218,3 +247,26 @@
shutil.copyfile(attic_header_py_src, service_header_py_dest)
elif os.path.exists(service_header_py_dest):
os.remove(service_header_py_dest)
+
+ def remove_service(self, manifest):
+ # remove any xproto files, otherwise "make rebuild_protos" will pick them up
+ if os.path.exists(manifest["dir"]):
+ for fn in os.listdir(manifest["dir"]):
+ fn = os.path.join(manifest["dir"], fn)
+ if fn.endswith(".xproto"):
+ os.remove(fn)
+
+ # Rather than trying to unmigrate while the core is running, let's handle unmigrating the service while we're
+ # outside of the core process. We're going to save the manifest file, and the manifest file will have
+ # {"state": "unload"} in it. That can be our external signal to unmigrate.
+
+ # This is what unmigrate will do, external to this process:
+ # 1) remove the models (./manage.py migrate my_app_name zero)
+ # 2) remove the contenttypes
+ # # does step 1 already do this?
+ # from django.contrib.contenttypes.models import ContentType
+ # for c in ContentType.objects.all():
+ # if not c.model_class():
+ # print "deleting %s" % c
+ # c.delete()
+ # 3) Remove the service files
diff --git a/xos/coreapi/protos/dynamicload.proto b/xos/coreapi/protos/dynamicload.proto
index d4142a1..4504580 100644
--- a/xos/coreapi/protos/dynamicload.proto
+++ b/xos/coreapi/protos/dynamicload.proto
@@ -37,6 +37,11 @@
LoadModelsStatus status = 1;
};
+message UnloadModelsRequest {
+ string name = 1;
+ string version = 2;
+};
+
service dynamicload {
rpc LoadModels(LoadModelsRequest) returns (LoadModelsReply) {
option (google.api.http) = {
@@ -44,4 +49,10 @@
body: "*"
};
}
+ rpc UnloadModels(UnloadModelsRequest) returns (LoadModelsReply) {
+ option (google.api.http) = {
+ post: "/xosapi/v1/dynamicload/unload_models"
+ body: "*"
+ };
+ }
};
\ No newline at end of file
diff --git a/xos/coreapi/test_dynamicbuild.py b/xos/coreapi/test_dynamicbuild.py
index 35359ac..f6838d5 100644
--- a/xos/coreapi/test_dynamicbuild.py
+++ b/xos/coreapi/test_dynamicbuild.py
@@ -13,6 +13,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
+import json
import os
import shutil
import sys
@@ -35,6 +36,11 @@
for (k,v) in kwargs.items():
setattr(self, k, v)
+class DynamicUnloadRequest():
+ def __init__(self, **kwargs):
+ for (k,v) in kwargs.items():
+ setattr(self, k, v)
+
class TestDynamicBuild(unittest.TestCase):
def setUp(self):
global dynamicbuild
@@ -82,6 +88,9 @@
version = "1",
xprotos = [self.example_xproto_item])
+ self.example_unload_request = DynamicUnloadRequest(name = "exampleservice",
+ version = "1")
+
self.builder = dynamicbuild.DynamicBuilder(base_dir = self.base_dir)
def tearDown(self):
@@ -95,16 +104,18 @@
self.builder.pre_validate_models(self.example_request)
def test_generate_request_hash(self):
- hash = self.builder.generate_request_hash(self.example_request)
+ hash = self.builder.generate_request_hash(self.example_request, state="load")
self.assertEqual(hash, "162de5012a8399883344085cbc232a2e627c5091")
def test_handle_loadmodels_request(self):
with patch.object(dynamicbuild.DynamicBuilder, "save_models", wraps=self.builder.save_models) as save_models, \
- patch.object(dynamicbuild.DynamicBuilder, "run_xosgenx_service", wraps=self.builder.run_xosgenx_service) as run_xosgenx_service:
+ patch.object(dynamicbuild.DynamicBuilder, "run_xosgenx_service", wraps=self.builder.run_xosgenx_service) as run_xosgenx_service, \
+ patch.object(dynamicbuild.DynamicBuilder, "remove_service", wraps=self.builder.remove_service) as remove_service:
result = self.builder.handle_loadmodels_request(self.example_request)
save_models.assert_called()
run_xosgenx_service.assert_called()
+ remove_service.assert_not_called()
self.assertEqual(result, self.builder.SOMETHING_CHANGED)
@@ -118,6 +129,27 @@
self.assertTrue(os.path.exists(os.path.join(service_dir, "models.py")))
self.assertTrue(os.path.exists(os.path.join(service_dir, "security.py")))
+ manifest = json.loads(open(os.path.join(self.builder.manifest_dir, "exampleservice.json"), "r").read())
+ self.assertEqual(manifest.get("state"), "load")
+
+ def test_handle_unloadmodels_request(self):
+ with patch.object(dynamicbuild.DynamicBuilder, "save_models", wraps=self.builder.save_models) as save_models, \
+ patch.object(dynamicbuild.DynamicBuilder, "run_xosgenx_service", wraps=self.builder.run_xosgenx_service) as run_xosgenx_service, \
+ patch.object(dynamicbuild.DynamicBuilder, "remove_service", wraps=self.builder.remove_service) as remove_service:
+ result = self.builder.handle_unloadmodels_request(self.example_unload_request)
+
+ save_models.assert_called()
+ run_xosgenx_service.assert_not_called()
+ remove_service.assert_called()
+
+ self.assertEqual(result, self.builder.SOMETHING_CHANGED)
+
+ self.assertTrue(os.path.exists(self.builder.manifest_dir))
+ self.assertTrue(os.path.exists(os.path.join(self.builder.manifest_dir, "exampleservice.json")))
+
+ manifest = json.loads(open(os.path.join(self.builder.manifest_dir, "exampleservice.json"), "r").read())
+ self.assertEqual(manifest.get("state"), "unload")
+
def test_handle_loadmodels_request_twice(self):
result = self.builder.handle_loadmodels_request(self.example_request)
self.assertEqual(result, self.builder.SOMETHING_CHANGED)
@@ -126,7 +158,7 @@
self.assertEqual(result, self.builder.NOTHING_TO_DO)
def test_save_models(self):
- manifest = self.builder.save_models(self.example_request)
+ manifest = self.builder.save_models(self.example_request, state="load")
dynamic_dir = os.path.join(self.base_dir, "dynamic_services", "exampleservice")
service_dir = os.path.join(self.base_dir, "services", "exampleservice")
@@ -138,6 +170,19 @@
self.assertEqual(manifest["dest_dir"], service_dir)
self.assertEqual(len(manifest["xprotos"]), 1)
+ def test_save_models_precomputed_hash(self):
+ manifest = self.builder.save_models(self.example_request, state="load", hash="1234")
+
+ dynamic_dir = os.path.join(self.base_dir, "dynamic_services", "exampleservice")
+ service_dir = os.path.join(self.base_dir, "services", "exampleservice")
+
+ self.assertEqual(manifest["name"], self.example_request.name)
+ self.assertEqual(manifest["version"], self.example_request.version)
+ self.assertEqual(manifest["hash"], "1234")
+ self.assertEqual(manifest["dir"], dynamic_dir)
+ self.assertEqual(manifest["dest_dir"], service_dir)
+ self.assertEqual(len(manifest["xprotos"]), 1)
+
def main():
unittest.main()
diff --git a/xos/coreapi/unload_unwanted_apps.py b/xos/coreapi/unload_unwanted_apps.py
new file mode 100644
index 0000000..a4a1120
--- /dev/null
+++ b/xos/coreapi/unload_unwanted_apps.py
@@ -0,0 +1,49 @@
+
+# Copyright 2017-present Open Networking Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import json
+import os
+import shutil
+
+class AppUnloader(object):
+ def __init__(self):
+ self.manifests_dir = "/opt/xos/dynamic_services/manifests"
+
+ def unload_all_eligible(self):
+ if not os.path.exists(self.manifests_dir):
+ # nothing to do...
+ return
+
+ for fn in os.listdir(self.manifests_dir):
+ if fn.endswith(".json"):
+ manifest_fn = os.path.join(self.manifests_dir, fn)
+ manifest = json.loads(open(manifest_fn).read())
+ if manifest.get("state") == "unload":
+ self.unload_service(manifest)
+
+ def unload_service(self, manifest):
+ if not os.path.exists(manifest["dest_dir"]):
+ # service is deleted -- nothing to do
+ return
+
+ os.system("cd /opt/xos; python ./manage.py migrate %s zero" % manifest["name"])
+
+ # be paranoid about calling rmtree
+ assert(os.path.abspath(manifest["dest_dir"]).startswith("/opt/xos"))
+
+ shutil.rmtree(manifest["dest_dir"])
+
+if __name__ == "__main__":
+ AppUnloader().unload_all_eligible()
diff --git a/xos/coreapi/xos_dynamicload_api.py b/xos/coreapi/xos_dynamicload_api.py
index f7d79ed..c7327f8 100644
--- a/xos/coreapi/xos_dynamicload_api.py
+++ b/xos/coreapi/xos_dynamicload_api.py
@@ -35,10 +35,7 @@
def stop(self):
pass
- #@translate_exceptions
def LoadModels(self, request, context):
- print "load models"
-
try:
builder = DynamicBuilder()
result = builder.handle_loadmodels_request(request)
@@ -54,4 +51,20 @@
import traceback; traceback.print_exc()
raise e
+ def UnloadModels(self, request, context):
+ try:
+ builder = DynamicBuilder()
+ result = builder.handle_unloadmodels_request(request)
+
+ if (result == builder.SOMETHING_CHANGED):
+ self.server.delayed_shutdown(5)
+
+ response = dynamicload_pb2.LoadModelsReply()
+ response.status = response.SUCCESS
+
+ return response
+ except Exception, e:
+ import traceback; traceback.print_exc()
+ raise e
+
diff --git a/xos/xos_client/xossh b/xos/xos_client/xossh
index 600211f..3edc606 100644
--- a/xos/xos_client/xossh
+++ b/xos/xos_client/xossh
@@ -112,10 +112,32 @@
def listModelDefs():
return current_client.modeldefs.ListModelDefs(Empty())
+def loadModels(name, version, xproto_filenames=[], decl_filenames=[], attic_filenames=[]):
+ request = current_client.dynamicload_pb2.LoadModelsRequest(name=name, version=version)
+ for fn in xproto_filenames:
+ item = request.xprotos.add()
+ item.filename = fn
+ item.contents = open(fn).read()
+ for fn in decl_filenames:
+ item = request.decls.add()
+ item.filename = fn
+ item.contents = open(fn).read()
+ for fn in attic_filenames:
+ item = request.attics.add()
+ item.filename = fn
+ item.contents = open(fn).read()
+ return current_client.dynamicload.LoadModels(request)
+
+def unloadModels(name, version):
+ request = current_client.dynamicload_pb2.UnloadModelsRequest(name=name, version=version)
+ return current_client.dynamicload.UnloadModels(request)
+
def listUtility():
print 'setDirtyModels(class_name=None)'
print 'listDirtyModels(class_name=None)'
print 'listModelDefs()'
+ print 'loadModels(name, version, xproto_filenames, decl_filenames, attic_filenames)'
+ print 'unloadModels(name, version)'
def examples():
print 'Slice.objects.all() # list all slices'