blob: 46514a78f38c0d6c6657bb9c688b4c311fe5e45f [file] [log] [blame]
Matteo Scandolod2044a42017-08-07 16:08:28 -07001# Copyright 2017-present Open Networking Foundation
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
Scott Baker89704b12019-05-07 16:42:11 -070015import datetime
Zack Williams045b63d2019-01-22 16:30:57 -070016import inspect
17from apistats import REQUEST_COUNT, track_request_time
18import grpc
Scott Baker75e9d562019-02-22 09:34:00 -080019from authhelper import XOSAuthHelperMixin
20from decorators import translate_exceptions, require_authentication
Zack Williams045b63d2019-01-22 16:30:57 -070021from xos.exceptions import XOSNotAuthenticated
22from core.models import ServiceInstance
23from django.db.models import F, Q
Scott Bakerdf2a0ac2019-06-07 11:56:20 -070024from django.db import connection
Zack Williams045b63d2019-01-22 16:30:57 -070025import django.apps
26from django.contrib.auth import authenticate as django_authenticate
Scott Baker21d5e4f2017-03-25 11:00:33 -070027import fnmatch
Scott Bakerafdc4682017-02-14 14:49:05 -080028import os
29import sys
Scott Bakerb96ba432018-02-26 09:53:48 -080030from protos import utility_pb2, utility_pb2_grpc
Scott Bakerdab11192017-02-03 16:42:14 -080031from google.protobuf.empty_pb2 import Empty
Scott Bakerdab11192017-02-03 16:42:14 -080032from importlib import import_module
Matteo Scandolod9208552017-06-21 14:15:16 -070033from django.conf import settings
Scott Baker89704b12019-05-07 16:42:11 -070034from xosconfig import Config
35from multistructlog import create_logger
36
37log = create_logger(Config().get("logging"))
Zack Williams045b63d2019-01-22 16:30:57 -070038
Matteo Scandolod9208552017-06-21 14:15:16 -070039SessionStore = import_module(settings.SESSION_ENGINE).SessionStore
Scott Bakerdab11192017-02-03 16:42:14 -080040
Scott Bakerdab11192017-02-03 16:42:14 -080041
Scott Bakerafdc4682017-02-14 14:49:05 -080042# The Tosca engine expects to be run from /opt/xos/tosca/ or equivalent. It
43# needs some sys.path fixing up.
Zack Williams045b63d2019-01-22 16:30:57 -070044
Scott Bakerafdc4682017-02-14 14:49:05 -080045currentdir = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe())))
46toscadir = os.path.join(currentdir, "../tosca")
47
Zack Williams045b63d2019-01-22 16:30:57 -070048
Scott Baker21d5e4f2017-03-25 11:00:33 -070049def is_internal_model(model):
50 """ things to be excluded from the dirty_models endpoints """
Zack Williams045b63d2019-01-22 16:30:57 -070051 if "django" in model.__module__:
Scott Baker21d5e4f2017-03-25 11:00:33 -070052 return True
Zack Williams045b63d2019-01-22 16:30:57 -070053 if "cors" in model.__module__:
Scott Baker21d5e4f2017-03-25 11:00:33 -070054 return True
Zack Williams045b63d2019-01-22 16:30:57 -070055 if "contenttypes" in model.__module__:
Scott Baker21d5e4f2017-03-25 11:00:33 -070056 return True
Zack Williams045b63d2019-01-22 16:30:57 -070057 if "core.models.journal" in model.__module__: # why?
Scott Baker21d5e4f2017-03-25 11:00:33 -070058 return True
Zack Williams045b63d2019-01-22 16:30:57 -070059 if "core.models.project" in model.__module__: # why?
Scott Baker21d5e4f2017-03-25 11:00:33 -070060 return True
61 return False
62
Zack Williams045b63d2019-01-22 16:30:57 -070063
Matteo Scandolod9208552017-06-21 14:15:16 -070064def get_xproto(folder):
65 matches = []
66 for root, dirnames, filenames in os.walk(folder):
Zack Williams045b63d2019-01-22 16:30:57 -070067 for filename in fnmatch.filter(filenames, "*.xproto"):
Matteo Scandolod9208552017-06-21 14:15:16 -070068 matches.append(os.path.join(root, filename))
69 return matches
70
Zack Williams045b63d2019-01-22 16:30:57 -070071
Scott Baker75e9d562019-02-22 09:34:00 -080072class UtilityService(utility_pb2_grpc.utilityServicer, XOSAuthHelperMixin):
Scott Bakerdab11192017-02-03 16:42:14 -080073 def __init__(self, thread_pool):
74 self.thread_pool = thread_pool
Scott Baker75e9d562019-02-22 09:34:00 -080075 XOSAuthHelperMixin.__init__(self)
Scott Bakerdab11192017-02-03 16:42:14 -080076
77 def stop(self):
78 pass
79
Matteo Scandolo1d01b692018-10-03 15:53:36 -070080 @translate_exceptions("Utilities", "Login")
81 @track_request_time("Utilities", "Login")
Scott Bakerdab11192017-02-03 16:42:14 -080082 def Login(self, request, context):
83 if not request.username:
Scott Bakerf09d38a2017-04-28 14:23:05 -070084 raise XOSNotAuthenticated("No username")
Scott Bakerdab11192017-02-03 16:42:14 -080085
Zack Williams045b63d2019-01-22 16:30:57 -070086 u = django_authenticate(username=request.username, password=request.password)
Scott Bakerdab11192017-02-03 16:42:14 -080087 if not u:
Zack Williams045b63d2019-01-22 16:30:57 -070088 raise XOSNotAuthenticated(
89 "Failed to authenticate user %s" % request.username
90 )
Scott Bakerdab11192017-02-03 16:42:14 -080091
92 session = SessionStore()
93 auth = {"username": request.username, "password": request.password}
94 session["auth"] = auth
Zack Williams045b63d2019-01-22 16:30:57 -070095 session["_auth_user_id"] = u.pk
96 session["_auth_user_backend"] = u.backend
Scott Bakerdab11192017-02-03 16:42:14 -080097 session.save()
98
99 response = utility_pb2.LoginResponse()
100 response.sessionid = session.session_key
101
Zack Williams045b63d2019-01-22 16:30:57 -0700102 REQUEST_COUNT.labels("xos-core", "Utilities", "Login", grpc.StatusCode.OK).inc()
Scott Bakerdab11192017-02-03 16:42:14 -0800103 return response
104
Matteo Scandolo1d01b692018-10-03 15:53:36 -0700105 @translate_exceptions("Utilities", "Logout")
106 @track_request_time("Utilities", "Logout")
Scott Bakerdab11192017-02-03 16:42:14 -0800107 def Logout(self, request, context):
108 for (k, v) in context.invocation_metadata():
Zack Williams045b63d2019-01-22 16:30:57 -0700109 if k.lower() == "x-xossession":
Scott Bakerdab11192017-02-03 16:42:14 -0800110 s = SessionStore(session_key=v)
111 if "_auth_user_id" in s:
112 del s["_auth_user_id"]
113 s.save()
Zack Williams045b63d2019-01-22 16:30:57 -0700114 REQUEST_COUNT.labels("xos-core", "Utilities", "Login", grpc.StatusCode.OK).inc()
Scott Bakerdab11192017-02-03 16:42:14 -0800115 return Empty()
116
Matteo Scandolo1d01b692018-10-03 15:53:36 -0700117 @translate_exceptions("Utilities", "NoOp")
118 @track_request_time("Utilities", "NoOp")
Scott Baker5ebcd282017-03-16 16:11:20 -0700119 def NoOp(self, request, context):
Zack Williams045b63d2019-01-22 16:30:57 -0700120 REQUEST_COUNT.labels("xos-core", "Utilities", "NoOp", grpc.StatusCode.OK).inc()
Scott Baker5ebcd282017-03-16 16:11:20 -0700121 return Empty()
122
Matteo Scandolo1d01b692018-10-03 15:53:36 -0700123 @translate_exceptions("Utilities", "AuthenticatedNoOp")
124 @track_request_time("Utilities", "AuthenticatedNoOp")
Scott Baker75e9d562019-02-22 09:34:00 -0800125 @require_authentication
Scott Baker16778212017-05-04 14:35:00 -0700126 def AuthenticatedNoOp(self, request, context):
Zack Williams045b63d2019-01-22 16:30:57 -0700127 REQUEST_COUNT.labels(
128 "xos-core", "Utilities", "AuthenticatedNoOp", grpc.StatusCode.OK
129 ).inc()
Scott Baker16778212017-05-04 14:35:00 -0700130 return Empty()
131
Matteo Scandolo1d01b692018-10-03 15:53:36 -0700132 @translate_exceptions("Utilities", "ListDirtyModels")
133 @track_request_time("Utilities", "ListDirtyModels")
Scott Baker75e9d562019-02-22 09:34:00 -0800134 @require_authentication
Scott Baker21d5e4f2017-03-25 11:00:33 -0700135 def ListDirtyModels(self, request, context):
136 dirty_models = utility_pb2.ModelList()
137
138 models = django.apps.apps.get_models()
139 for model in models:
140 if is_internal_model(model):
141 continue
142 fieldNames = [x.name for x in model._meta.fields]
Zack Williams045b63d2019-01-22 16:30:57 -0700143 if ("enacted" not in fieldNames) or ("updated" not in fieldNames):
Scott Baker21d5e4f2017-03-25 11:00:33 -0700144 continue
Zack Williams045b63d2019-01-22 16:30:57 -0700145 if (request.class_name) and (
146 not fnmatch.fnmatch(model.__name__, request.class_name)
147 ):
Scott Baker21d5e4f2017-03-25 11:00:33 -0700148 continue
Zack Williams045b63d2019-01-22 16:30:57 -0700149 objs = model.objects.filter(Q(enacted__lt=F("updated")) | Q(enacted=None))
Scott Baker21d5e4f2017-03-25 11:00:33 -0700150 for obj in objs:
151 item = dirty_models.items.add()
152 item.class_name = model.__name__
153 item.id = obj.id
154
Zack Williams045b63d2019-01-22 16:30:57 -0700155 REQUEST_COUNT.labels(
156 "xos-core", "Utilities", "ListDirtyModels", grpc.StatusCode.OK
157 ).inc()
Scott Baker21d5e4f2017-03-25 11:00:33 -0700158 return dirty_models
159
Matteo Scandolo1d01b692018-10-03 15:53:36 -0700160 @translate_exceptions("Utilities", "SetDirtyModels")
161 @track_request_time("Utilities", "SetDirtyModels")
Scott Baker75e9d562019-02-22 09:34:00 -0800162 @require_authentication
Scott Baker21d5e4f2017-03-25 11:00:33 -0700163 def SetDirtyModels(self, request, context):
Zack Williams045b63d2019-01-22 16:30:57 -0700164 user = self.authenticate(context, required=True)
Scott Baker21d5e4f2017-03-25 11:00:33 -0700165
166 dirty_models = utility_pb2.ModelList()
167
168 models = django.apps.apps.get_models()
169 for model in models:
170 if is_internal_model(model):
171 continue
172 fieldNames = [x.name for x in model._meta.fields]
Zack Williams045b63d2019-01-22 16:30:57 -0700173 if ("enacted" not in fieldNames) or ("updated" not in fieldNames):
Scott Baker21d5e4f2017-03-25 11:00:33 -0700174 continue
Zack Williams045b63d2019-01-22 16:30:57 -0700175 if (request.class_name) and (
176 not fnmatch.fnmatch(model.__name__, request.class_name)
177 ):
Scott Baker21d5e4f2017-03-25 11:00:33 -0700178 continue
179 objs = model.objects.all()
180 for obj in objs:
181 try:
Scott Bakerf6a7a482018-04-05 12:13:08 -0700182 obj.caller = user
183 obj.save()
184 item = dirty_models.items.add()
185 item.class_name = model.__name__
186 item.id = obj.id
Zack Williams045b63d2019-01-22 16:30:57 -0700187 except Exception as e:
Scott Baker21d5e4f2017-03-25 11:00:33 -0700188 item = dirty_models.items.add()
189 item.class_name = model.__name__
190 item.id = obj.id
191 item.info = str(e)
192
Zack Williams045b63d2019-01-22 16:30:57 -0700193 REQUEST_COUNT.labels(
194 "xos-core", "Utilities", "SetDirtyModels", grpc.StatusCode.OK
195 ).inc()
Scott Baker21d5e4f2017-03-25 11:00:33 -0700196 return dirty_models
197
Matteo Scandolo1d01b692018-10-03 15:53:36 -0700198 @translate_exceptions("Utilities", "GetXproto")
199 @track_request_time("Utilities", "GetXproto")
Scott Baker75e9d562019-02-22 09:34:00 -0800200 # TODO(smbaker): Tosca engine calls this without authentication
Matteo Scandolod9208552017-06-21 14:15:16 -0700201 def GetXproto(self, request, context):
202 res = utility_pb2.XProtos()
203
Zack Williams045b63d2019-01-22 16:30:57 -0700204 core_dir = os.path.abspath(
205 os.path.dirname(os.path.realpath(__file__)) + "/../core/models/"
206 )
Matteo Scandolod9208552017-06-21 14:15:16 -0700207 core_xprotos = get_xproto(core_dir)
208
Zack Williams045b63d2019-01-22 16:30:57 -0700209 service_dir = os.path.abspath(
210 os.path.dirname(os.path.realpath(__file__)) + "/../services"
211 )
Matteo Scandolod9208552017-06-21 14:15:16 -0700212 services_xprotos = get_xproto(service_dir)
213
Zack Williams045b63d2019-01-22 16:30:57 -0700214 dynamic_service_dir = os.path.abspath(
215 os.path.dirname(os.path.realpath(__file__)) + "/../dynamic_services"
216 )
Scott Bakerb06e3e02017-12-12 11:05:53 -0800217 dynamic_services_xprotos = get_xproto(dynamic_service_dir)
218
219 xprotos = core_xprotos + services_xprotos + dynamic_services_xprotos
Matteo Scandolod9208552017-06-21 14:15:16 -0700220
221 xproto = ""
222
223 for f in xprotos:
224 content = open(f).read()
225 xproto += "\n"
226 xproto += content
227
228 res.xproto = xproto
Zack Williams045b63d2019-01-22 16:30:57 -0700229 REQUEST_COUNT.labels(
230 "xos-core", "Utilities", "GetXproto", grpc.StatusCode.OK
231 ).inc()
Matteo Scandolod9208552017-06-21 14:15:16 -0700232 return res
233
Matteo Scandolo1d01b692018-10-03 15:53:36 -0700234 @translate_exceptions("Utilities", "GetPopulatedServiceInstances")
235 @track_request_time("Utilities", "GetPopulatedServiceInstances")
Scott Baker75e9d562019-02-22 09:34:00 -0800236 @require_authentication
Matteo Scandolo488c88e2018-02-01 11:52:33 -0800237 def GetPopulatedServiceInstances(self, request, context):
238 """
239 Return a service instance with provider and subsciber service instance ids
240 """
241 response = utility_pb2.PopulatedServiceInstance()
242
243 si = ServiceInstance.objects.get(id=request.id)
244
Matteo Scandolo488c88e2018-02-01 11:52:33 -0800245 # populate the response object
246 response.id = si.id
247 response.leaf_model_name = si.leaf_model_name
248 response.owner_id = si.owner_id
249
250 if si.name:
251 response.name = si.name
252
253 # find links
254 provided_links = si.provided_links.all()
255 subscribed_links = si.subscribed_links.all()
256
257 # import pdb; pdb.set_trace()
258
259 for l in provided_links:
260 response.provided_service_instances.append(l.subscriber_service_instance.id)
261
262 for l in subscribed_links:
263 if l.subscriber_service_instance:
Zack Williams045b63d2019-01-22 16:30:57 -0700264 response.subscribed_service_instances.append(
265 l.provider_service_instance_id
266 )
Matteo Scandolo488c88e2018-02-01 11:52:33 -0800267 elif l.subscriber_service:
268 response.subscribed_service.append(l.subscriber_service.id)
269 elif l.subscriber_network:
270 response.subscribed_network.append(l.subscriber_network.id)
271
Zack Williams045b63d2019-01-22 16:30:57 -0700272 REQUEST_COUNT.labels(
273 "xos-core", "Utilities", "GetPopulatedServiceInstances", grpc.StatusCode.OK
274 ).inc()
Matteo Scandolo488c88e2018-02-01 11:52:33 -0800275 return response
Scott Baker89704b12019-05-07 16:42:11 -0700276
Scott Bakerdf2a0ac2019-06-07 11:56:20 -0700277 @translate_exceptions("Utilities", "GetVersion")
278 @track_request_time("Utilities", "GetVersion")
Scott Baker89704b12019-05-07 16:42:11 -0700279 def GetVersion(self, request, context):
280 res = utility_pb2.VersionInfo()
281
282 try:
283 res.version = open("/opt/xos/VERSION").readline().strip()
284 except Exception:
285 log.exception("Exception while determining build version")
286 res.version = "unknown"
287
288 try:
289 res.gitCommit = open("/opt/xos/COMMIT").readline().strip()
290 res.buildTime = datetime.datetime.utcfromtimestamp(
291 os.stat("/opt/xos/COMMIT").st_ctime).strftime("%Y-%m-%dT%H:%M:%SZ")
292 except Exception:
293 log.exception("Exception while determining build information")
294 res.buildDate = "unknown"
295 res.gitCommit = "unknown"
296
297 res.pythonVersion = sys.version.split("\n")[0].strip()
298 res.os = os.uname()[0].lower()
299 res.arch = os.uname()[4].lower()
300
Scott Baker89704b12019-05-07 16:42:11 -0700301 REQUEST_COUNT.labels(
302 "xos-core", "Utilities", "GetVersion", grpc.StatusCode.OK
303 ).inc()
304 return res
Scott Bakerdf2a0ac2019-06-07 11:56:20 -0700305
306 @translate_exceptions("Utilities", "GetDatabaseInfo")
307 @track_request_time("Utilities", "GetDatabaseInfo")
308 def GetDatabaseInfo(self, request, context):
309 res = utility_pb2.DatabaseInfo()
310
311 res.name = settings.DB["NAME"]
312 res.connection = "%s:%s" % (settings.DB["HOST"], settings.DB["PORT"])
313
314 # Start by assuming the db is operational, then we'll perform some tests
315 # to make sure it's working as we expect.
316 res.status = res.OPERATIONAL
317
318 # TODO(smbaker): Think about whether these are postgres-specific checks and what might happen
319 # if another db is configured.
320
321 try:
322 server_version = connection.cursor().connection.server_version
323 # example: '100003' for postgres 10.3
324 res.version = "%d.%d" % (server_version/10000, server_version % 10000)
325 except Exception:
326 res.version = "Unknown"
327 res.status = res.ERROR
328
329 if res.status == res.OPERATIONAL:
330 # Try performing a simple query that evaluates a constant. This will prove we are talking
331 # to the database.
332 try:
333 cursor = connection.cursor()
334 cursor.execute("select 1")
335 result = cursor.fetchone()
336 assert(len(result) == 1)
337 assert(result[0] == 1)
338 except Exception:
339 res.status = res.ERROR
340
341 REQUEST_COUNT.labels(
342 "xos-core", "Utilities", "GetDatabaseInfo", grpc.StatusCode.OK
343 ).inc()
344 return res