Skip to content

Commit 07107a7

Browse files
authored
tunnel start/stop function (#6)
1 parent 93ac02a commit 07107a7

File tree

7 files changed

+161
-6
lines changed

7 files changed

+161
-6
lines changed

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
pyyaml
22
requests
3+
python3-wget

setup.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
setuptools.setup(
77
name="staroid", # Replace with your own username
8-
version="0.0.5",
8+
version="0.0.6",
99
license='MIT',
1010
author="Staroid",
1111
author_email="support@staroid.com",
@@ -41,6 +41,6 @@
4141
install_requires=[
4242
'requests',
4343
'pyyaml',
44-
44+
'python3-wget'
4545
]
4646
)

staroid/cluster.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,5 +100,5 @@ def delete(self, name):
100100
if r.status_code == 200:
101101
return cluster_to_del
102102
else:
103-
logging.error("Can not create clusters {}", r.status_code)
103+
logging.error("Can not delete clusters {}", r.status_code)
104104
return None

staroid/namespace.py

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import requests
22
import logging
33
import json
4+
import subprocess
5+
import atexit
46
from .commit import Commit
57

68
class Namespace:
@@ -37,6 +39,7 @@ class NamespaceApi:
3739
def __init__(self, staroid, cluster):
3840
self.__staroid = staroid
3941
self.__cluster = cluster
42+
self.__tunnel_processes = {}
4043

4144
def get_all(self):
4245
r = self.__staroid._api_get(
@@ -181,7 +184,7 @@ def shell_start(self, instance_name):
181184
)
182185
if r.status_code == 200:
183186
js = json.loads(r.text)
184-
return Namespace(js)
187+
return js
185188
else:
186189
logging.error("Can not start shell {}", r.status_code)
187190
return None
@@ -220,3 +223,64 @@ def get_all_resources(self, instance_name):
220223
else:
221224
logging.error("Can not get namespace resources {}", r.status_code)
222225
return None
226+
227+
def _is_tunnel_running(self, instance_name):
228+
if instance_name in self.__tunnel_processes:
229+
p = self.__tunnel_processes[instance_name]
230+
p.poll()
231+
return p.returncode == None
232+
else:
233+
return False
234+
235+
def start_tunnel(self, instance_name, tunnels):
236+
if self._is_tunnel_running(instance_name):
237+
return
238+
239+
chisel_path = self.__staroid.get_chisel_path()
240+
241+
ns = self.get(instance_name)
242+
if ns == None:
243+
return None
244+
resources = self.get_all_resources(instance_name)
245+
246+
shell_service = None
247+
for s in resources["services"]["items"]:
248+
if "labels" in s["metadata"]:
249+
if "resource.staroid.com/system" in s["metadata"]["labels"]:
250+
if s["metadata"]["labels"]["resource.staroid.com/system"] == "shell":
251+
shell_service = s
252+
break
253+
254+
if shell_service == None:
255+
raise Exception("Shell service not found")
256+
257+
tunnel_server = "https://p{}-{}--{}".format("57682", shell_service["metadata"]["name"], ns.url()[len("https://"):])
258+
cmd = [
259+
chisel_path,
260+
"client",
261+
"--header",
262+
"Authorization: token {}".format(self.__staroid.get_access_token()),
263+
"--keepalive",
264+
"10s",
265+
tunnel_server
266+
]
267+
cmd.extend(tunnels)
268+
self.__tunnel_processes[instance_name]=subprocess.Popen(cmd)
269+
atexit.register(self.__cleanup)
270+
271+
def __cleanup(self):
272+
timeout_sec = 5
273+
for p in self.__tunnel_processes.values(): # list of your processes
274+
p_sec = 0
275+
for second in range(timeout_sec):
276+
if p.poll() == None:
277+
time.sleep(1)
278+
p_sec += 1
279+
if p_sec >= timeout_sec:
280+
p.kill() # supported from python 2.6
281+
282+
def stop_tunnel(self, instance_name):
283+
if self._is_tunnel_running(instance_name):
284+
self.__tunnel_processes[instance_name].kill()
285+
del self.__tunnel_processes[instance_name]
286+

staroid/staroid.py

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,23 @@
1-
import os
1+
import os, stat
22
import yaml
33
import logging
44
import requests
55
import json
6+
from pathlib import Path
7+
from shutil import which
8+
import platform
9+
import subprocess
10+
import wget
11+
612
from .cluster import ClusterApi
713
from .namespace import NamespaceApi
814

15+
CHISEL_VERSION="1.6.0"
16+
CHISEL_ARCH_MAP={
17+
"x86_64": "amd64",
18+
"i386": "386"
19+
}
20+
921
class Org:
1022
def __init__(self, json):
1123
self.__json = json
@@ -36,10 +48,15 @@ def principal(self):
3648
class Staroid:
3749
"""Staroid client object"""
3850

39-
def __init__(self, access_token=None, account=None, config_path="~/.staroid/config.yaml"):
51+
def __init__(self, access_token=None, account=None, config_path=None, cache_dir=None, chisel_path=None):
4052
self.__api_addr = "https://staroid.com/api"
4153
self.__access_token = None
4254
self.__account = None
55+
self.__cache_dir = cache_dir
56+
self.__chisel_path = chisel_path
57+
58+
if self.__cache_dir == None:
59+
self.__cache_dir = "{}/.staroid".format(str(Path.home()))
4360

4461
# 1. set from configs
4562
self.__read_config(config_path)
@@ -65,6 +82,9 @@ def __init__(self, access_token=None, account=None, config_path="~/.staroid/conf
6582
self.__account = "{}/{}".format(user.provider(), user.principal())
6683

6784
def __read_config(self, config_path):
85+
if config_path == None:
86+
config_path = "{}/.staroid/config.yaml".format(str(Path.home()))
87+
6888
try:
6989
with open(config_path, "r") as f:
7090
logging.info("Read configuration from " + config_path)
@@ -74,6 +94,52 @@ def __read_config(self, config_path):
7494
except EnvironmentError:
7595
pass
7696

97+
def create_or_get_cache_dir(self, module = ""):
98+
"create (if not exists) or return cache dir path for module"
99+
cache_dir = "{}/{}".format(self.__cache_dir, module)
100+
if not os.path.exists(cache_dir):
101+
os.makedirs(cache_dir)
102+
return cache_dir
103+
104+
def __check_cmd(self, cmd):
105+
if which(cmd) == None:
106+
raise Exception("'{}' command not found".format(cmd))
107+
108+
def __download_chisel_if_not_exists(self):
109+
# check gunzip available
110+
self.__check_cmd("gunzip")
111+
112+
if self.__chisel_path == None:
113+
# download chisel binary for secure tunnel if not exists
114+
uname = platform.uname()
115+
uname.system.lower()
116+
if uname.machine not in CHISEL_ARCH_MAP.keys():
117+
raise Exception("Can not download chisel automatically. Please download manually from 'https://github.com/jpillora/chisel/releases/download/v{}' and set 'chisel_path' argument".format(CHISEL_VERSION))
118+
119+
download_url = "https://github.com/jpillora/chisel/releases/download/v{}/chisel_{}_{}_{}.gz".format(
120+
CHISEL_VERSION, CHISEL_VERSION, uname.system.lower(), CHISEL_ARCH_MAP[uname.machine])
121+
cache_bin = self.create_or_get_cache_dir("bin")
122+
chisel_path = "{}/chisel".format(cache_bin)
123+
124+
if not os.path.exists(chisel_path):
125+
# download
126+
filename = wget.download(download_url, cache_bin)
127+
128+
# extract
129+
subprocess.run(["gunzip", "-f", filename])
130+
131+
# rename
132+
subprocess.run(["mv", filename.replace(".gz", ""), chisel_path])
133+
134+
# chmod
135+
os.chmod(chisel_path, stat.S_IRWXU)
136+
137+
self.__chisel_path = chisel_path
138+
139+
def get_chisel_path(self):
140+
self.__download_chisel_if_not_exists()
141+
return self.__chisel_path
142+
77143
def cluster(self):
78144
c = ClusterApi(self)
79145
return c

tests/test_namespace.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,12 @@ def test_crud_namespace(self):
3737
resources = ns_api.get_all_resources("instance1")
3838
self.assertTrue(len(resources["services"]) > 0)
3939

40+
# start tunnel
41+
ns_api.start_tunnel("instance1", ["57683:localhost:57683"])
42+
43+
# stop tunnel
44+
ns_api.stop_tunnel("instance1")
45+
4046
# stop shell
4147
ns_api.shell_stop("instance1")
4248

tests/test_staroid.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import unittest
22
import tempfile
33
import os
4+
import pathlib
5+
import shutil
46

57
from staroid import Staroid
68

@@ -40,6 +42,22 @@ def test_read_config(self):
4042
if ac != None:
4143
os.environ["STAROID_ACCOUNT"] = ac
4244

45+
def test_download_chisel(self):
46+
# given
47+
tmp_dir = tempfile.mkdtemp()
48+
s = Staroid(cache_dir=tmp_dir)
49+
50+
# when
51+
chisel_path = s.get_chisel_path()
52+
53+
# then
54+
self.assertIsNotNone(chisel_path)
55+
self.assertTrue(os.path.isfile(chisel_path))
56+
57+
# clean up
58+
shutil.rmtree(pathlib.Path(tmp_dir))
59+
60+
4361
@unittest.skipUnless(integration_test_ready(), "Integration test environment is not configured")
4462
def test_read_default_account(self):
4563
# given access_token is set but account is not set

0 commit comments

Comments
 (0)