From 367a1ad3e28a98bf75a3eea68ec00418ce7c8160 Mon Sep 17 00:00:00 2001 From: Eugene K Date: Thu, 9 Jun 2022 16:42:18 -0400 Subject: [PATCH 01/11] implement hosting functions raise exceptions on errors --- src/openziti/zitilib.py | 51 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 46 insertions(+), 5 deletions(-) diff --git a/src/openziti/zitilib.py b/src/openziti/zitilib.py index 453246d..52dfa33 100644 --- a/src/openziti/zitilib.py +++ b/src/openziti/zitilib.py @@ -43,6 +43,8 @@ def __repr__(self): _ziti_version = ziti.ziti_get_version _ziti_version.restype = ctypes.POINTER(_Ver) +_ziti_lasterr = ziti.Ziti_last_error + _ziti_errorstr = ziti.ziti_errorstr _ziti_errorstr.argtypes = [ctypes.c_int] _ziti_errorstr.restype = ctypes.c_char_p @@ -55,14 +57,21 @@ def __repr__(self): ziti_socket.argtypes = [ctypes.c_int] ziti_socket.restype = ctypes.c_int -ziti_connect = ziti.Ziti_connect -ziti_connect.argtypes = [ctypes.c_int, ctypes.c_void_p, ctypes.c_char_p] - +_ziti_connect = ziti.Ziti_connect +_ziti_connect.argtypes = [ctypes.c_int, ctypes.c_void_p, ctypes.c_char_p] _ziti_connect_addr = ziti.Ziti_connect_addr _ziti_connect_addr.argtypes = [ctypes.c_int, ctypes.c_char_p, ctypes.c_int] _ziti_connect_addr.restype = ctypes.c_int +_ziti_bind = ziti.Ziti_bind +_ziti_bind.argtypes = [ctypes.c_int, ctypes.c_void_p, ctypes.c_char_p] + +_ziti_listen = ziti.Ziti_listen +_ziti_listen.argtypes = [ctypes.c_int, ctypes.c_int] + +_ziti_accept = ziti.Ziti_accept +_ziti_accept.argtypes = [ctypes.c_int, ctypes.c_char_p, ctypes.c_int] _ziti_enroll = ziti.Ziti_enroll_identity _ziti_enroll.argtypes = [ @@ -88,6 +97,16 @@ def errorstr(code): return msg.decode() +def check_error(code): + if code != 0: + err = _ziti_lasterr() + if err < 0: + msg = _ziti_errorstr(err).decode(encoding='utf-8') + else: + msg = errorstr(err) + raise Exception(err, msg) + + def init(): ziti.Ziti_lib_init() @@ -101,11 +120,33 @@ def load(path): return _load_ctx(b_obj) -def connect(fd, addr: Tuple[str, int]): +def connect(fd, ztx, service: str): + srv = bytes(service, encoding='utf-8') + check_error(_ziti_connect(fd, ztx, srv)) + + +def connect_addr(fd, addr: Tuple[str, int]): # pylint: disable=invalid-name host = bytes(addr[0], encoding='utf-8') port = addr[1] - return _ziti_connect_addr(fd, host, port) + check_error(_ziti_connect_addr(fd, host, port)) + + +def bind(fd, ztx, service): + srv = bytes(service, encoding='utf-8') + check_error(_ziti_bind(fd, ztx, srv)) + + +def listen(fd, backlog): + check_error(_ziti_listen(fd, backlog)) + + +def accept(fd): + b = ctypes.create_string_buffer(128) + clt = _ziti_accept(fd, b, 128) + if clt < 0: + check_error(clt) + return clt,(bytes(b.value).decode('utf-8'), 0) def enroll(jwt, key=None, cert=None): From 8858ddb994ecf1ad271327e23738b4b614719a52 Mon Sep 17 00:00:00 2001 From: Eugene K Date: Thu, 9 Jun 2022 16:55:48 -0400 Subject: [PATCH 02/11] implement hosting functions --- src/openziti/zitisock.py | 36 ++++++++++++++++++++++++++++++++---- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/src/openziti/zitisock.py b/src/openziti/zitisock.py index 29bdeb8..4fbf976 100644 --- a/src/openziti/zitisock.py +++ b/src/openziti/zitisock.py @@ -11,7 +11,6 @@ # 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 socket from socket import socket as PySocket from typing import Tuple @@ -21,7 +20,11 @@ class ZitiSocket(PySocket): # pylint: disable=redefined-builtin - def __init__(self, af=-1, type=-1, proto=-1, fileno=None): + def __init__(self, af=-1, type=-1, proto=-1, fileno=None, opts=None): + if opts is None: + opts = {} + self._bind_address = None + self._ziti_opts = opts self._ziti_af = af self._ziti_type = type self._ziti_proto = proto @@ -40,14 +43,39 @@ def connect(self, addr) -> None: pass if isinstance(addr, Tuple): - retcode = zitilib.connect(self._zitifd, addr) - if retcode != 0: + try: + zitilib.connect_addr(self._zitifd, addr) + except: PySocket.close(self) self._zitifd = None PySocket.__init__(self, self._ziti_af, self._ziti_type, self._ziti_proto) PySocket.connect(self, addr) + def bind(self, addr) -> None: + self._bind_address = addr + bindings = self._ziti_opts['bindings'] + cfg = bindings[addr] + if cfg is None: + raise RuntimeError(f'no ziti binding for {addr}') + ztx = cfg['ztx'] + service = cfg['service'] + ztx.bind(service, self) + + def getsockname(self): + # return this for now since frameworks expect something to be returned + return ('127.0.0.1', 0) + + def listen(self, __backlog: int = 5) -> None: + try: + zitilib.listen(self._zitifd, __backlog) + except: + super().listen(__backlog) + + def accept(self): + fd, peer = zitilib.accept(self.fileno()) + return ZitiSocket(af=self._ziti_af, type=self._ziti_type, fileno=fd), peer + def setsockopt(self, __level, __optname, __value) -> None: try: PySocket.setsockopt(self, __level, __optname, __value) From 92a2985b0a657c9d20b4f52847a8918dcdff497e Mon Sep 17 00:00:00 2001 From: Eugene K Date: Thu, 9 Jun 2022 17:08:47 -0400 Subject: [PATCH 03/11] update ziti-sdk@.28.7 --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 81c977c..d7a2999 100644 --- a/setup.cfg +++ b/setup.cfg @@ -38,4 +38,4 @@ tag_prefix = v parentdir_prefix = openziti- [openziti] -ziti_sdk_version = 0.28.4 \ No newline at end of file +ziti_sdk_version = 0.28.7 \ No newline at end of file From 9e87fb0a5057e9ec590ebfa34bdf0d4a9d8a9986 Mon Sep 17 00:00:00 2001 From: Eugene K Date: Thu, 9 Jun 2022 17:10:49 -0400 Subject: [PATCH 04/11] add ZitiContext.bind() --- src/openziti/context.py | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/src/openziti/context.py b/src/openziti/context.py index b87ee53..f5aa231 100644 --- a/src/openziti/context.py +++ b/src/openziti/context.py @@ -14,21 +14,34 @@ import socket -from . import zitilib +from . import zitilib, zitisock class ZitiContext: # pylint: disable=too-few-public-methods - def __init__(self, ctx_p): - self._ctx = ctx_p + def __init__(self, ctx): + ztx = ctx + if ctx is str: + ztx = zitilib.load(ctx) + self._ctx = ztx def connect(self, addr): # pylint: disable=invalid-name fd = zitilib.ziti_socket(socket.SOCK_STREAM) - service = bytes(addr, encoding="utf-8") - zitilib.ziti_connect(fd, self._ctx, service) + if addr is str: + zitilib.connect(fd, self._ctx, addr) + elif addr is tuple: + zitilib.connect_addr(fd, addr) + else: + raise RuntimeError(f'unsupported address {addr}') return socket.socket(socket.AF_UNIX, socket.SOCK_STREAM, 0, fd) + def bind(self, service, sock=None): + if sock is None: + sock = zitisock.ZitiSocket(type=socket.SOCK_STREAM) + zitilib.bind(sock.fileno(), self._ctx, service) + return sock + def load_identity(path) -> ZitiContext: return ZitiContext(zitilib.load(path)) From 10044bb7c42fea2debe2867bb6aacb15e85dba1e Mon Sep 17 00:00:00 2001 From: Eugene K Date: Thu, 9 Jun 2022 17:22:25 -0400 Subject: [PATCH 05/11] lazy init() --- src/openziti/zitilib.py | 1 + src/openziti/zitisock.py | 1 + 2 files changed, 2 insertions(+) diff --git a/src/openziti/zitilib.py b/src/openziti/zitilib.py index 52dfa33..6c9d700 100644 --- a/src/openziti/zitilib.py +++ b/src/openziti/zitilib.py @@ -116,6 +116,7 @@ def shutdown(): def load(path): + init() b_obj = bytes(path, encoding="utf-8") return _load_ctx(b_obj) diff --git a/src/openziti/zitisock.py b/src/openziti/zitisock.py index 4fbf976..331df77 100644 --- a/src/openziti/zitisock.py +++ b/src/openziti/zitisock.py @@ -21,6 +21,7 @@ class ZitiSocket(PySocket): # pylint: disable=redefined-builtin def __init__(self, af=-1, type=-1, proto=-1, fileno=None, opts=None): + zitilib.init() if opts is None: opts = {} self._bind_address = None From 36e07ccc0467439b22b307330071ce37048f7f1c Mon Sep 17 00:00:00 2001 From: Eugene K Date: Thu, 9 Jun 2022 17:34:21 -0400 Subject: [PATCH 06/11] enable lazy load of ziti context --- src/openziti/context.py | 10 +++++++++- src/openziti/zitisock.py | 3 ++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/openziti/context.py b/src/openziti/context.py index f5aa231..51248f3 100644 --- a/src/openziti/context.py +++ b/src/openziti/context.py @@ -21,7 +21,7 @@ class ZitiContext: # pylint: disable=too-few-public-methods def __init__(self, ctx): ztx = ctx - if ctx is str: + if isinstance(ctx, str): ztx = zitilib.load(ctx) self._ctx = ztx @@ -45,3 +45,11 @@ def bind(self, service, sock=None): def load_identity(path) -> ZitiContext: return ZitiContext(zitilib.load(path)) + + +def get_context(ztx) -> ZitiContext: + if isinstance(ztx, ZitiContext): + return ztx + if isinstance(ztx, str): + return ZitiContext(ztx) + raise RuntimeError(f'{ztx} is not a Ziti Context or a path') \ No newline at end of file diff --git a/src/openziti/zitisock.py b/src/openziti/zitisock.py index 331df77..a2c0f74 100644 --- a/src/openziti/zitisock.py +++ b/src/openziti/zitisock.py @@ -54,12 +54,13 @@ def connect(self, addr) -> None: PySocket.connect(self, addr) def bind(self, addr) -> None: + from .context import get_context self._bind_address = addr bindings = self._ziti_opts['bindings'] cfg = bindings[addr] if cfg is None: raise RuntimeError(f'no ziti binding for {addr}') - ztx = cfg['ztx'] + ztx = get_context(cfg['ztx']) service = cfg['service'] ztx.bind(service, self) From 23dbd9c3ba858c326ca847297b366b902415a368 Mon Sep 17 00:00:00 2001 From: Eugene K Date: Thu, 9 Jun 2022 17:35:32 -0400 Subject: [PATCH 07/11] add zitify decorator move MonkeyPatch into a separate file --- src/openziti/__init__.py | 33 +++-------------------- src/openziti/decor.py | 56 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 30 deletions(-) create mode 100644 src/openziti/decor.py diff --git a/src/openziti/__init__.py b/src/openziti/__init__.py index fa281de..1b62288 100644 --- a/src/openziti/__init__.py +++ b/src/openziti/__init__.py @@ -12,19 +12,14 @@ # See the License for the specific language governing permissions and # limitations under the License. -import socket as sock from os import getenv -from . import _version, context, zitilib, zitisock +from . import _version, context, zitilib, zitisock, decor _ziti_identities = filter(lambda p: p != '', map(lambda s: s.strip(), (getenv('ZITI_IDENTITIES') or "").split(';'))) -_id_map = {} - -zitilib.init() - enroll = zitilib.enroll version = zitilib.version shutdown = zitilib.shutdown @@ -35,30 +30,8 @@ if identity != '': load(identity) -_patch_methods = { - "create_connection": zitisock.create_ziti_connection, - "getaddrinfo": zitisock.ziti_getaddrinfo -} - - -class MonkeyPatch(): - def __init__(self): - self.orig_socket = sock.socket - sock.socket = zitisock.ZitiSocket - self.orig_methods = {m: sock.__dict__[m] for m, _ in - _patch_methods.items()} - for m_name, _ in _patch_methods.items(): - sock.__dict__[m_name] = _patch_methods[m_name] - - def __enter__(self): - return self - - def __exit__(self, exc_type, exc_val, exc_tb): - for m_name, _ in self.orig_methods.items(): - sock.__dict__[m_name] = self.orig_methods[m_name] - - -monkeypatch = MonkeyPatch # pylint: disable=invalid-name +monkeypatch = decor.MonkeyPatch # pylint: disable=invalid-name +zitify = decor.zitify __version__ = _version.get_versions()['version'] del _version diff --git a/src/openziti/decor.py b/src/openziti/decor.py new file mode 100644 index 0000000..d2c8b74 --- /dev/null +++ b/src/openziti/decor.py @@ -0,0 +1,56 @@ +# Copyright (c) NetFoundry Inc. +# +# 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 +# +# https://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 socket as sock +from . import zitisock + + +_patch_methods = { + "create_connection": zitisock.create_ziti_connection, + "getaddrinfo": zitisock.ziti_getaddrinfo +} + + +def _patchedSocket(patch_opts): + class patchedSocket(zitisock.ZitiSocket): + def __init__(self, *args, **kwargs): + super().__init__(*args, **dict(kwargs, opts=patch_opts)) + return patchedSocket + + +class MonkeyPatch(): + def __init__(self, **args): + self.orig_socket = sock.socket + sock.socket = _patchedSocket(args) + self.orig_methods = {m: sock.__dict__[m] for m, _ in + _patch_methods.items()} + for m_name, _ in _patch_methods.items(): + sock.__dict__[m_name] = _patch_methods[m_name] + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + for m_name, _ in self.orig_methods.items(): + sock.__dict__[m_name] = self.orig_methods[m_name] + + + + +def zitify(**zargs): + def zitify_func(func): + def zitified(*args, **kwargs): + with MonkeyPatch(**zargs): + func(*args, **kwargs) + return zitified + return zitify_func \ No newline at end of file From 714283b3076e73e1def5c3ede600d37071fa7ab3 Mon Sep 17 00:00:00 2001 From: Eugene K Date: Thu, 9 Jun 2022 17:42:00 -0400 Subject: [PATCH 08/11] add samples: - simple HTTP server - flask+waitress app --- sample/flask-of-ziti/helloFlazk.py | 39 ++++++++++++++++++++++++ sample/ziti-http-server.py | 49 ++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+) create mode 100644 sample/flask-of-ziti/helloFlazk.py create mode 100644 sample/ziti-http-server.py diff --git a/sample/flask-of-ziti/helloFlazk.py b/sample/flask-of-ziti/helloFlazk.py new file mode 100644 index 0000000..6efd4a2 --- /dev/null +++ b/sample/flask-of-ziti/helloFlazk.py @@ -0,0 +1,39 @@ +# Copyright (c) NetFoundry Inc. +# +# 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 +# +# https://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. + +from flask import Flask +import openziti +import sys + +app = Flask(__name__) +bind_opts = {} # populated in main + + +@openziti.zitify(bindings={ + ('1.2.3.4', '18080'): bind_opts +}) +def runApp(): + from waitress import serve + serve(app,host='1.2.3.4',port=18080) + + +@app.route('/') +def hello_world(): # put application's code here + return 'Have some Ziti!' + + +if __name__ == '__main__': + bind_opts['ztx'] = sys.argv[1] + bind_opts['service'] = sys.argv[2] + runApp() diff --git a/sample/ziti-http-server.py b/sample/ziti-http-server.py new file mode 100644 index 0000000..1d861ac --- /dev/null +++ b/sample/ziti-http-server.py @@ -0,0 +1,49 @@ +# Copyright (c) NetFoundry Inc. +# +# 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 +# +# https://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 sys +from http.server import BaseHTTPRequestHandler, HTTPServer +import time + +import openziti + +hostName = "localhost" +serverPort = 8080 + +cfg = dict( + ztx=sys.argv[1], + service=sys.argv[2] +) +openziti.monkeypatch(bindings={(hostName, serverPort): cfg}) + + +class MyServer(BaseHTTPRequestHandler): + def do_GET(self): + self.send_response(200) + self.send_header("Content-type", "application/json") + self.end_headers() + msg = """{"msg": "Help! I was ziified!"}""" + self.wfile.write(bytes(msg, "utf-8")) + + +if __name__ == "__main__": + webServer = HTTPServer((hostName, serverPort), MyServer) + print("Server started http://%s:%s" % (hostName, serverPort)) + + try: + webServer.serve_forever(poll_interval=600) + except KeyboardInterrupt: + pass + + webServer.server_close() + print("Server stopped.") \ No newline at end of file From 33b16e90852800e204f5781bd4e8df55d745fdfc Mon Sep 17 00:00:00 2001 From: Eugene K Date: Thu, 9 Jun 2022 18:18:09 -0400 Subject: [PATCH 09/11] update samples - simple echo socket server - simple zitified urllib3 client --- sample/flask-of-ziti/requirements.txt | 3 +++ sample/http-get.py | 17 ++++++++++++ sample/ziti-echo-server.py | 39 +++++++++++++++++++++++++++ 3 files changed, 59 insertions(+) create mode 100644 sample/flask-of-ziti/requirements.txt create mode 100644 sample/http-get.py create mode 100644 sample/ziti-echo-server.py diff --git a/sample/flask-of-ziti/requirements.txt b/sample/flask-of-ziti/requirements.txt new file mode 100644 index 0000000..65d268e --- /dev/null +++ b/sample/flask-of-ziti/requirements.txt @@ -0,0 +1,3 @@ +flask +waitress +openziti \ No newline at end of file diff --git a/sample/http-get.py b/sample/http-get.py new file mode 100644 index 0000000..0b6a891 --- /dev/null +++ b/sample/http-get.py @@ -0,0 +1,17 @@ +import sys + +import urllib3 +import openziti + +# to run this sample +# set ZITI_IDENTITIES environment variable to location of your Ziti identity file +# +# python http-get.py +# url should be the intercept address of a ziti service +if __name__ == '__main__': + openziti.monkeypatch() + http = urllib3.PoolManager() + r = http.request('GET', sys.argv[1]) + print("{0} {1}".format(r.status, r.reason)) + print(r.data.decode('utf-8')) + diff --git a/sample/ziti-echo-server.py b/sample/ziti-echo-server.py new file mode 100644 index 0000000..e9d0f67 --- /dev/null +++ b/sample/ziti-echo-server.py @@ -0,0 +1,39 @@ +# Copyright (c) NetFoundry Inc. +# +# 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 +# +# https://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 sys +import openziti + + +def run(ziti_id, service): + ztx = openziti.load(ziti_id) + server = ztx.bind(service) + server.listen() + + while True: + conn, peer = server.accept() + print(f'processing incoming client[{peer}]') + with conn: + count = 0 + while True: + data = conn.recv(1024) + if not data: + print(f'client finished after sending {count} bytes') + break + count += len(data) + conn.sendall(data) + + +if __name__ == '__main__': + run(sys.argv[1], sys.argv[2]) \ No newline at end of file From a26c9d064b02618324e3cbe3e08bf1d6bcdab209 Mon Sep 17 00:00:00 2001 From: Eugene K Date: Fri, 10 Jun 2022 12:00:09 -0400 Subject: [PATCH 10/11] fix naming, typeos --- sample/ziti-http-server.py | 2 +- src/openziti/decor.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/sample/ziti-http-server.py b/sample/ziti-http-server.py index 1d861ac..c1fc6ab 100644 --- a/sample/ziti-http-server.py +++ b/sample/ziti-http-server.py @@ -32,7 +32,7 @@ def do_GET(self): self.send_response(200) self.send_header("Content-type", "application/json") self.end_headers() - msg = """{"msg": "Help! I was ziified!"}""" + msg = """{"msg": "Help! I was zitified!"}""" self.wfile.write(bytes(msg, "utf-8")) diff --git a/src/openziti/decor.py b/src/openziti/decor.py index d2c8b74..9c2245f 100644 --- a/src/openziti/decor.py +++ b/src/openziti/decor.py @@ -29,9 +29,9 @@ def __init__(self, *args, **kwargs): class MonkeyPatch(): - def __init__(self, **args): + def __init__(self, **kwargs): self.orig_socket = sock.socket - sock.socket = _patchedSocket(args) + sock.socket = _patchedSocket(kwargs) self.orig_methods = {m: sock.__dict__[m] for m, _ in _patch_methods.items()} for m_name, _ in _patch_methods.items(): @@ -47,10 +47,10 @@ def __exit__(self, exc_type, exc_val, exc_tb): -def zitify(**zargs): +def zitify(**zkwargs): def zitify_func(func): def zitified(*args, **kwargs): - with MonkeyPatch(**zargs): + with MonkeyPatch(**zkwargs): func(*args, **kwargs) return zitified return zitify_func \ No newline at end of file From 5c4d9473443532ea833f329c9c9ee0d5b9aee124 Mon Sep 17 00:00:00 2001 From: Eugene K Date: Fri, 10 Jun 2022 12:08:39 -0400 Subject: [PATCH 11/11] update sample intructions with Python enrollment --- sample/README.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/sample/README.md b/sample/README.md index 9580c97..cfdbf89 100644 --- a/sample/README.md +++ b/sample/README.md @@ -10,10 +10,12 @@ OpenZiti Python SDK in Action - get yourself a Ziti identity from [ZEDS](https://zeds.openziti.org) - follow enrollment instructions from the site. - these instructions assume that Ziti identity is stored in `id.json` file - - _Enrollment with Python is coming soon_ + follow enrollment instructions from the site, or better yet enroll with openziti Python module + ```bash + $ python -m openziti enroll --jwt= --identity= + ``` + + the following instructions assume that Ziti identity is stored in `id.json` file - set `ZITI_IDENTITIES` environment variable to location of `id.json` file