blob: 9bf884d3fef0a5c4bab85a712f285a859dc41e9d [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
Zack Williams5c2ea232019-01-30 15:23:01 -070015from __future__ import absolute_import, print_function
16
Scott Bakere72e7612017-02-20 10:07:09 -080017import argparse
Scott Baker96b995a2017-02-15 16:21:12 -080018import base64
19import functools
Scott Bakera2717c82019-06-06 10:55:11 -070020import hashlib
Zack Williams5c2ea232019-01-30 15:23:01 -070021import inspect
Scott Baker96b995a2017-02-15 16:21:12 -080022import os
Scott Baker96b995a2017-02-15 16:21:12 -080023import sys
Scott Baker96b995a2017-02-15 16:21:12 -080024
Matteo Scandolo10a2f3c2018-04-20 16:59:38 +020025from google.protobuf.empty_pb2 import Empty
Zack Williams045b63d2019-01-22 16:30:57 -070026
Zack Williams5c2ea232019-01-30 15:23:01 -070027import grpc
28from grpc import (composite_channel_credentials, metadata_call_credentials,
29 ssl_channel_credentials)
Zack Williams045b63d2019-01-22 16:30:57 -070030
Zack Williams5c2ea232019-01-30 15:23:01 -070031from twisted.internet import reactor
Matteo Scandoloe3d2f262018-06-05 17:45:39 -070032from xosconfig import Config
Zack Williams5c2ea232019-01-30 15:23:01 -070033
34from xosapi import orm
35import xosapi.chameleon_client.grpc_client as chameleon_client
Zack Williams045b63d2019-01-22 16:30:57 -070036
Matteo Scandoloe3d2f262018-06-05 17:45:39 -070037from multistructlog import create_logger
Zack Williams045b63d2019-01-22 16:30:57 -070038log = create_logger(Config().get("logging"))
Matteo Scandoloe3d2f262018-06-05 17:45:39 -070039
Zack Williams5c2ea232019-01-30 15:23:01 -070040currentdir = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe())))
41sys.path = [currentdir] + sys.path
42
Zack Williams045b63d2019-01-22 16:30:57 -070043SERVER_CA = "/usr/local/share/ca-certificates/local_certs.crt"
Matteo Scandoloe3d2f262018-06-05 17:45:39 -070044
Scott Baker96b995a2017-02-15 16:21:12 -080045
46class UsernamePasswordCallCredentials(grpc.AuthMetadataPlugin):
Zack Williams045b63d2019-01-22 16:30:57 -070047 """Metadata wrapper for raw access token credentials."""
48
49 def __init__(self, username, password):
Scott Baker96b995a2017-02-15 16:21:12 -080050 self._username = username
51 self._password = password
Zack Williams045b63d2019-01-22 16:30:57 -070052
53 def __call__(self, context, callback):
54 basic_auth = "Basic %s" % base64.b64encode(
55 "%s:%s" % (self._username, self._password)
56 )
57 metadata = (("authorization", basic_auth),)
Scott Baker96b995a2017-02-15 16:21:12 -080058 callback(metadata, None)
59
Zack Williams045b63d2019-01-22 16:30:57 -070060
Scott Baker96b995a2017-02-15 16:21:12 -080061class SessionIdCallCredentials(grpc.AuthMetadataPlugin):
Zack Williams045b63d2019-01-22 16:30:57 -070062 """Metadata wrapper for raw access token credentials."""
63
64 def __init__(self, sessionid):
Scott Baker96b995a2017-02-15 16:21:12 -080065 self._sessionid = sessionid
Zack Williams045b63d2019-01-22 16:30:57 -070066
67 def __call__(self, context, callback):
68 metadata = (("x-xossession", self._sessionid),)
Scott Baker96b995a2017-02-15 16:21:12 -080069 callback(metadata, None)
70
Zack Williams045b63d2019-01-22 16:30:57 -070071
Scott Baker96b995a2017-02-15 16:21:12 -080072class XOSClient(chameleon_client.GrpcClient):
73 # We layer our own reconnect_callback functionality so we can setup the
74 # ORM before calling reconnect_callback.
75
Scott Bakera2717c82019-06-06 10:55:11 -070076 def __init__(self, *args, **kwargs):
77 self.hashes = {}
78 self.restart_on_protobuf_change = False
79 super(XOSClient, self).__init__(*args, **kwargs)
80
Scott Baker96b995a2017-02-15 16:21:12 -080081 def set_reconnect_callback(self, reconnect_callback):
82 self.reconnect_callback2 = reconnect_callback
Matteo Scandolo10a2f3c2018-04-20 16:59:38 +020083
Scott Baker96b995a2017-02-15 16:21:12 -080084 return self
85
Scott Baker8203dc62019-03-14 16:16:19 -070086 def request_convenience_methods(self):
Matteo Scandolo10a2f3c2018-04-20 16:59:38 +020087
Scott Baker8203dc62019-03-14 16:16:19 -070088 convenience_methods_dir = "/var/run/xosapi/convenience"
89 if not os.path.exists(convenience_methods_dir):
90 log.info("Creating convenience methods directory", convenience_methods_dir=convenience_methods_dir)
91 os.makedirs(convenience_methods_dir)
Matteo Scandolo10a2f3c2018-04-20 16:59:38 +020092
93 try:
Matteo Scandolo5dda1a32018-05-14 14:03:10 -070094 response = self.dynamicload.GetConvenienceMethods(Empty())
95
96 if response:
Zack Williams045b63d2019-01-22 16:30:57 -070097 log.info(
Scott Baker8203dc62019-03-14 16:16:19 -070098 "Saving convenience methods",
Zack Williams045b63d2019-01-22 16:30:57 -070099 methods=[m.filename for m in response.convenience_methods],
100 )
Matteo Scandolo5dda1a32018-05-14 14:03:10 -0700101
102 for cm in response.convenience_methods:
Matteo Scandoloe3d2f262018-06-05 17:45:39 -0700103 log.debug("Saving convenience method", method=cm.filename)
Matteo Scandolo5dda1a32018-05-14 14:03:10 -0700104 save_path = os.path.join(convenience_methods_dir, cm.filename)
Zack Williams5c2ea232019-01-30 15:23:01 -0700105 open(save_path, "w").write(cm.contents)
Matteo Scandolo5dda1a32018-05-14 14:03:10 -0700106 else:
Zack Williams045b63d2019-01-22 16:30:57 -0700107 log.exception(
108 "Cannot load convenience methods, restarting the synchronzier"
109 )
110 os.execv(sys.executable, ["python"] + sys.argv)
Matteo Scandolo5dda1a32018-05-14 14:03:10 -0700111
Zack Williams045b63d2019-01-22 16:30:57 -0700112 except grpc._channel._Rendezvous as e:
Matteo Scandolo10a2f3c2018-04-20 16:59:38 +0200113 code = e.code()
114 if code == grpc.StatusCode.UNAVAILABLE:
115 # NOTE if the core is not available, restart the synchronizer
Zack Williams045b63d2019-01-22 16:30:57 -0700116 os.execv(sys.executable, ["python"] + sys.argv)
Matteo Scandolo10a2f3c2018-04-20 16:59:38 +0200117
Scott Bakera2717c82019-06-06 10:55:11 -0700118 def hash_check(self, pb2_file_name, pb2_grpc_file_name):
119 # If the protobufs have changed, then it's likely that new models
120 # have been downloaded. One way we have dealt with this in the past
121 # is to force a reload() of the affected modules. However, it seems
122 # safer to force a full synchronizer restart as this will allow
123 # the synchronizer to perform a version check against the core, and
124 # it will refresh any data structures that might be affected by the
125 # new models.
126
127 pb2_hash = hashlib.sha256(open(pb2_file_name).read())
128 pb2_grpc_hash = hashlib.sha256(open(pb2_grpc_file_name).read())
129
130 if (pb2_file_name in self.hashes) or (pb2_grpc_file_name in self.hashes):
131 if (pb2_hash != self.hashes[pb2_file_name]) or (pb2_grpc_hash != self.hashes[pb2_grpc_file_name]):
132 log.warning(
133 "Protobuf change detected, restarting the synchronzier"
134 )
135 os.execv(sys.executable, ["python"] + sys.argv)
136
137 self.hashes[pb2_file_name] = pb2_hash
138 self.hashes[pb2_grpc_file_name] = pb2_grpc_hash
139
Scott Baker96b995a2017-02-15 16:21:12 -0800140 def reconnected(self):
Zack Williams045b63d2019-01-22 16:30:57 -0700141 for api in ["modeldefs", "utility", "xos", "dynamicload"]:
Scott Baker96b995a2017-02-15 16:21:12 -0800142 pb2_file_name = os.path.join(self.work_dir, api + "_pb2.py")
143 pb2_grpc_file_name = os.path.join(self.work_dir, api + "_pb2_grpc.py")
144
145 if os.path.exists(pb2_file_name) and os.path.exists(pb2_grpc_file_name):
Scott Bakera2717c82019-06-06 10:55:11 -0700146 if self.restart_on_protobuf_change:
147 self.hash_check(pb2_file_name, pb2_grpc_file_name)
148
Scott Baker96b995a2017-02-15 16:21:12 -0800149 orig_sys_path = sys.path
150 try:
151 sys.path.append(self.work_dir)
152 m_protos = __import__(api + "_pb2")
153 m_grpc = __import__(api + "_pb2_grpc")
154 finally:
155 sys.path = orig_sys_path
156
Zack Williams045b63d2019-01-22 16:30:57 -0700157 stub_class = getattr(m_grpc, api + "Stub")
Scott Baker96b995a2017-02-15 16:21:12 -0800158
159 setattr(self, api, stub_class(self.channel))
Zack Williams045b63d2019-01-22 16:30:57 -0700160 setattr(self, api + "_pb2", m_protos)
Scott Baker96b995a2017-02-15 16:21:12 -0800161 else:
Zack Williams045b63d2019-01-22 16:30:57 -0700162 print("failed to locate api", api, file=sys.stderr)
Scott Baker96b995a2017-02-15 16:21:12 -0800163
164 if hasattr(self, "xos"):
Scott Bakerb96ba432018-02-26 09:53:48 -0800165 self.xos_orm = orm.ORMStub(self.xos, self.xos_pb2, "xos")
Scott Baker96b995a2017-02-15 16:21:12 -0800166
Matteo Scandolo10a2f3c2018-04-20 16:59:38 +0200167 # ask the core for the convenience methods
Scott Baker8203dc62019-03-14 16:16:19 -0700168 self.request_convenience_methods()
Matteo Scandolo10a2f3c2018-04-20 16:59:38 +0200169
170 # Load convenience methods after reconnect
171 orm.import_convenience_methods()
172
Scott Baker96b995a2017-02-15 16:21:12 -0800173 if self.reconnect_callback2:
174 self.reconnect_callback2()
175
176
177class InsecureClient(XOSClient):
Zack Williams045b63d2019-01-22 16:30:57 -0700178 def __init__(
179 self,
180 consul_endpoint=None,
181 work_dir="/tmp/xos_grpc_protos",
182 endpoint="localhost:50055",
183 reconnect_callback=None,
184 ):
185 super(InsecureClient, self).__init__(
186 consul_endpoint, work_dir, endpoint, self.reconnected
187 )
Scott Baker96b995a2017-02-15 16:21:12 -0800188
189 self.reconnect_callback2 = reconnect_callback
190
Zack Williams045b63d2019-01-22 16:30:57 -0700191
Scott Baker96b995a2017-02-15 16:21:12 -0800192class SecureClient(XOSClient):
Zack Williams045b63d2019-01-22 16:30:57 -0700193 def __init__(
194 self,
195 consul_endpoint=None,
196 work_dir="/tmp/xos_grpc_protos",
197 endpoint="localhost:50055",
198 reconnect_callback=None,
199 cacert=SERVER_CA,
200 username=None,
201 password=None,
202 sessionid=None,
203 ):
204 server_ca = open(cacert, "r").read()
205 if sessionid:
Scott Baker96b995a2017-02-15 16:21:12 -0800206 call_creds = metadata_call_credentials(SessionIdCallCredentials(sessionid))
207 else:
Zack Williams045b63d2019-01-22 16:30:57 -0700208 call_creds = metadata_call_credentials(
209 UsernamePasswordCallCredentials(username, password)
210 )
Scott Baker96b995a2017-02-15 16:21:12 -0800211 chan_creds = ssl_channel_credentials(server_ca)
212 chan_creds = composite_channel_credentials(chan_creds, call_creds)
213
Zack Williams045b63d2019-01-22 16:30:57 -0700214 super(SecureClient, self).__init__(
215 consul_endpoint, work_dir, endpoint, self.reconnected, chan_creds
216 )
Scott Baker96b995a2017-02-15 16:21:12 -0800217
218 self.reconnect_callback2 = reconnect_callback
219
Zack Williams045b63d2019-01-22 16:30:57 -0700220
Scott Baker96b995a2017-02-15 16:21:12 -0800221# -----------------------------------------------------------------------------
Scott Bakere72e7612017-02-20 10:07:09 -0800222# Wrappers for easy setup for test cases, etc
223# -----------------------------------------------------------------------------
224
Zack Williams045b63d2019-01-22 16:30:57 -0700225
Scott Bakere72e7612017-02-20 10:07:09 -0800226def parse_args():
227 parser = argparse.ArgumentParser()
228
Zack Williams045b63d2019-01-22 16:30:57 -0700229 defs = {
230 "grpc_insecure_endpoint": "xos-core.cord.lab:50055",
231 "grpc_secure_endpoint": "xos-core.cord.lab:50051",
232 "config": "/opt/xos/config.yml",
233 }
Scott Bakere72e7612017-02-20 10:07:09 -0800234
Zack Williams045b63d2019-01-22 16:30:57 -0700235 _help = "Path to the config file (default: %s)" % defs["config"]
Scott Bakere72e7612017-02-20 10:07:09 -0800236 parser.add_argument(
Zack Williams045b63d2019-01-22 16:30:57 -0700237 "-C",
238 "--config",
239 dest="config",
240 action="store",
241 default=defs["config"],
242 help=_help,
243 )
Scott Bakere72e7612017-02-20 10:07:09 -0800244
Zack Williams045b63d2019-01-22 16:30:57 -0700245 _help = (
246 "gRPC insecure end-point to connect to. It is a direct",
247 ". (default: %s" % defs["grpc_insecure_endpoint"],
248 )
249 parser.add_argument(
250 "-G",
251 "--grpc-insecure-endpoint",
252 dest="grpc_insecure_endpoint",
253 action="store",
254 default=defs["grpc_insecure_endpoint"],
255 help=_help,
256 )
Scott Bakere72e7612017-02-20 10:07:09 -0800257
Zack Williams045b63d2019-01-22 16:30:57 -0700258 _help = (
259 "gRPC secure end-point to connect to. It is a direct",
260 ". (default: %s" % defs["grpc_secure_endpoint"],
261 )
262 parser.add_argument(
263 "-S",
264 "--grpc-secure-endpoint",
265 dest="grpc_secure_endpoint",
266 action="store",
267 default=defs["grpc_secure_endpoint"],
268 help=_help,
269 )
Scott Bakere72e7612017-02-20 10:07:09 -0800270
Zack Williams045b63d2019-01-22 16:30:57 -0700271 parser.add_argument(
272 "-u", "--username", dest="username", action="store", default=None, help=_help
273 )
Scott Bakere72e7612017-02-20 10:07:09 -0800274
Zack Williams045b63d2019-01-22 16:30:57 -0700275 parser.add_argument(
276 "-p", "--password", dest="password", action="store", default=None, help=_help
277 )
Scott Bakere72e7612017-02-20 10:07:09 -0800278
Zack Williams045b63d2019-01-22 16:30:57 -0700279 _help = "omit startup banner log lines"
280 parser.add_argument(
281 "-n",
282 "--no-banner",
283 dest="no_banner",
284 action="store_true",
285 default=False,
286 help=_help,
287 )
Scott Bakere72e7612017-02-20 10:07:09 -0800288
289 _help = "suppress debug and info logs"
Zack Williams045b63d2019-01-22 16:30:57 -0700290 parser.add_argument("-q", "--quiet", dest="quiet", action="count", help=_help)
Scott Bakere72e7612017-02-20 10:07:09 -0800291
Zack Williams045b63d2019-01-22 16:30:57 -0700292 _help = "enable verbose logging"
293 parser.add_argument("-v", "--verbose", dest="verbose", action="count", help=_help)
Scott Bakere72e7612017-02-20 10:07:09 -0800294
295 args = parser.parse_args()
296
297 return args
298
Sapan Bhatia51885532017-08-29 01:55:39 -0400299
Scott Bakere72e7612017-02-20 10:07:09 -0800300def coreclient_reconnect(client, reconnect_callback, *args, **kwargs):
301 global coreapi
302
303 coreapi = coreclient.xos_orm
304
305 if reconnect_callback:
306 reconnect_callback(*args, **kwargs)
307
308 reactor.stop()
309
Zack Williams045b63d2019-01-22 16:30:57 -0700310
Scott Bakere72e7612017-02-20 10:07:09 -0800311def start_api(reconnect_callback, *args, **kwargs):
312 global coreclient
313
314 if kwargs.get("username", None):
315 coreclient = SecureClient(*args, **kwargs)
316 else:
317 coreclient = InsecureClient(*args, **kwargs)
318
Zack Williams045b63d2019-01-22 16:30:57 -0700319 coreclient.set_reconnect_callback(
320 functools.partial(coreclient_reconnect, coreclient, reconnect_callback)
321 )
Scott Bakere72e7612017-02-20 10:07:09 -0800322 coreclient.start()
323
324 reactor.run()
325
Zack Williams045b63d2019-01-22 16:30:57 -0700326
Scott Bakere72e7612017-02-20 10:07:09 -0800327def start_api_parseargs(reconnect_callback):
Scott Baker95f7d952017-03-09 10:04:26 -0800328 """ This function is an entrypoint for tests and other simple programs to
329 setup the API and get a callback when the API is ready.
330 """
331
Scott Bakere72e7612017-02-20 10:07:09 -0800332 args = parse_args()
333
334 if args.username:
Zack Williams045b63d2019-01-22 16:30:57 -0700335 start_api(
336 reconnect_callback,
337 endpoint=args.grpc_secure_endpoint,
338 username=args.username,
339 password=args.password,
340 )
Scott Bakere72e7612017-02-20 10:07:09 -0800341 else:
342 start_api(reconnect_callback, endpoint=args.grpc_insecure_endpoint)
343
344
Scott Bakere72e7612017-02-20 10:07:09 -0800345# -----------------------------------------------------------------------------
Scott Baker96b995a2017-02-15 16:21:12 -0800346# Self test
347# -----------------------------------------------------------------------------
348
Zack Williams045b63d2019-01-22 16:30:57 -0700349
Scott Baker96b995a2017-02-15 16:21:12 -0800350def insecure_callback(client):
Zack Williams045b63d2019-01-22 16:30:57 -0700351 print("insecure self_test start")
352 print(client.xos_orm.User.objects.all())
353 print("insecure self_test done")
Scott Baker96b995a2017-02-15 16:21:12 -0800354
355 # now start the next test
356 client.stop()
357 reactor.callLater(0, start_secure_test)
358
Zack Williams045b63d2019-01-22 16:30:57 -0700359
Scott Baker96b995a2017-02-15 16:21:12 -0800360def start_insecure_test():
Matteo Scandoloe3d2f262018-06-05 17:45:39 -0700361 client = InsecureClient(endpoint="xos-core:50055")
Scott Baker96b995a2017-02-15 16:21:12 -0800362 client.set_reconnect_callback(functools.partial(insecure_callback, client))
363 client.start()
364
Zack Williams045b63d2019-01-22 16:30:57 -0700365
Scott Baker96b995a2017-02-15 16:21:12 -0800366def secure_callback(client):
Zack Williams045b63d2019-01-22 16:30:57 -0700367 print("secure self_test start")
368 print(client.xos_orm.User.objects.all())
369 print("secure self_test done")
Scott Baker96b995a2017-02-15 16:21:12 -0800370 reactor.stop()
371
Zack Williams045b63d2019-01-22 16:30:57 -0700372
Scott Baker96b995a2017-02-15 16:21:12 -0800373def start_secure_test():
Zack Williams045b63d2019-01-22 16:30:57 -0700374 client = SecureClient(
375 endpoint="xos-core:50051", username="admin@opencord.org", password="letmein"
376 )
Scott Baker96b995a2017-02-15 16:21:12 -0800377 client.set_reconnect_callback(functools.partial(secure_callback, client))
378 client.start()
379
Zack Williams045b63d2019-01-22 16:30:57 -0700380
Scott Baker96b995a2017-02-15 16:21:12 -0800381def main():
382 reactor.callLater(0, start_insecure_test)
383
384 reactor.run()
385
Scott Baker96b995a2017-02-15 16:21:12 -0800386
Zack Williams045b63d2019-01-22 16:30:57 -0700387if __name__ == "__main__":
388 main()