pirogue-admin-client 2.0.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pirogue_admin_client/__init__.py +101 -0
- pirogue_admin_client/adapters.py +273 -0
- pirogue_admin_client/cli.py +349 -0
- pirogue_admin_client/types.py +12 -0
- pirogue_admin_client-2.0.4.dist-info/METADATA +138 -0
- pirogue_admin_client-2.0.4.dist-info/RECORD +10 -0
- pirogue_admin_client-2.0.4.dist-info/WHEEL +5 -0
- pirogue_admin_client-2.0.4.dist-info/entry_points.txt +2 -0
- pirogue_admin_client-2.0.4.dist-info/licenses/LICENSE +674 -0
- pirogue_admin_client-2.0.4.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
from cryptography import x509
|
|
2
|
+
from enum import Enum
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import grpc
|
|
6
|
+
import yaml
|
|
7
|
+
from cryptography.x509.oid import NameOID
|
|
8
|
+
|
|
9
|
+
from pirogue_admin_api import PIROGUE_ADMIN_TCP_PORT
|
|
10
|
+
from pirogue_admin_client.adapters import SystemAdapter, NetworkAdapter, ServicesAdapter, access_token_call_credentials
|
|
11
|
+
|
|
12
|
+
ADMIN_VAR_DIR = '/var/lib/pirogue/admin'
|
|
13
|
+
USERLAND_CLIENT_CONFIG_FILENAME = '.pirogue-admin-client.conf'
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class PirogueAdminClientAdapter(SystemAdapter, NetworkAdapter, ServicesAdapter):
|
|
17
|
+
_host: str
|
|
18
|
+
_port: int
|
|
19
|
+
_token: str
|
|
20
|
+
_certificate: str
|
|
21
|
+
|
|
22
|
+
def __init__(self, host: str = None, port: int = None,
|
|
23
|
+
token: str = None, certificate: str = None):
|
|
24
|
+
self._host = host
|
|
25
|
+
self._port = port
|
|
26
|
+
self._token = token
|
|
27
|
+
self._certificate = certificate
|
|
28
|
+
|
|
29
|
+
self._local_pirogue_client_config_path = Path(ADMIN_VAR_DIR, 'client.yaml')
|
|
30
|
+
self._userland_client_config_path = Path(Path.home(), USERLAND_CLIENT_CONFIG_FILENAME)
|
|
31
|
+
|
|
32
|
+
# _use_tls will be updated by _load_configuration
|
|
33
|
+
self._use_tls = False
|
|
34
|
+
self._load_configuration()
|
|
35
|
+
|
|
36
|
+
chan_str = f'{self._host}:{self._port}'
|
|
37
|
+
token_call_injector = access_token_call_credentials(self._token)
|
|
38
|
+
|
|
39
|
+
secure_channel = grpc.ssl_channel_credentials(
|
|
40
|
+
str.encode(self._certificate) if (self._certificate is not None and
|
|
41
|
+
self._certificate != 'public') else None)
|
|
42
|
+
local_channel = grpc.local_channel_credentials(grpc.LocalConnectionType.LOCAL_TCP)
|
|
43
|
+
|
|
44
|
+
composite_credentials = grpc.composite_channel_credentials(
|
|
45
|
+
secure_channel if self._use_tls else local_channel,
|
|
46
|
+
token_call_injector,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
options = ()
|
|
50
|
+
if self._certificate is not None and self._certificate != 'public':
|
|
51
|
+
cert_decoded = x509.load_pem_x509_certificate(str.encode(self._certificate))
|
|
52
|
+
(common_name,) = cert_decoded.subject.get_attributes_for_oid(NameOID.COMMON_NAME)
|
|
53
|
+
cn_target = common_name.value
|
|
54
|
+
options = (('grpc.ssl_target_name_override', f'{cn_target}',),)
|
|
55
|
+
|
|
56
|
+
channel = grpc.secure_channel(chan_str, composite_credentials, options)
|
|
57
|
+
|
|
58
|
+
super(PirogueAdminClientAdapter, self).__init__(channel)
|
|
59
|
+
|
|
60
|
+
def _load_configuration(self):
|
|
61
|
+
loaded_config = None
|
|
62
|
+
|
|
63
|
+
if self._local_pirogue_client_config_path.exists():
|
|
64
|
+
loaded_config = yaml.safe_load(self._local_pirogue_client_config_path.read_text())
|
|
65
|
+
elif self._userland_client_config_path.exists():
|
|
66
|
+
loaded_config = yaml.safe_load(self._userland_client_config_path.read_text())
|
|
67
|
+
|
|
68
|
+
if isinstance(loaded_config, dict): # Prevents existing but empty file
|
|
69
|
+
self._host = loaded_config['host'] if self._host is None else self._host
|
|
70
|
+
self._port = loaded_config['port'] if self._port is None else self._port
|
|
71
|
+
self._token = loaded_config['token'] if self._token is None else self._token
|
|
72
|
+
# Support previous existing version
|
|
73
|
+
# where 'certificate' key could not be present
|
|
74
|
+
if 'certificate' in loaded_config:
|
|
75
|
+
if self._certificate is None:
|
|
76
|
+
self._certificate = loaded_config['certificate']
|
|
77
|
+
|
|
78
|
+
self._host = 'localhost' if self._host is None else self._host
|
|
79
|
+
self._port = PIROGUE_ADMIN_TCP_PORT if self._port is None else self._port
|
|
80
|
+
|
|
81
|
+
self._use_tls = self._host not in ['localhost', '127.0.0.1', 'ip6-localhost']
|
|
82
|
+
|
|
83
|
+
if self._host in (None, ''):
|
|
84
|
+
raise RuntimeError("Can't connect without host parameter")
|
|
85
|
+
if self._port in (None, '', 0):
|
|
86
|
+
raise RuntimeError("Can't connect without port parameter")
|
|
87
|
+
if self._token in (None, ''):
|
|
88
|
+
raise RuntimeError("Can't connect without token parameter")
|
|
89
|
+
|
|
90
|
+
def save_configuration(self):
|
|
91
|
+
if self._local_pirogue_client_config_path.exists():
|
|
92
|
+
raise RuntimeError('Cannot save configuration on PiRogue, this may interfere with daemon configuration')
|
|
93
|
+
else:
|
|
94
|
+
with open(self._userland_client_config_path, 'w') as out_fs:
|
|
95
|
+
yaml.safe_dump({
|
|
96
|
+
'host': self._host,
|
|
97
|
+
'port': self._port,
|
|
98
|
+
'token': self._token,
|
|
99
|
+
'certificate': 'public' if self._certificate is None else self._certificate,
|
|
100
|
+
}, out_fs)
|
|
101
|
+
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import logging
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional, Callable
|
|
6
|
+
|
|
7
|
+
import grpc
|
|
8
|
+
import yaml
|
|
9
|
+
from google.protobuf import empty_pb2
|
|
10
|
+
from google.protobuf.json_format import MessageToDict, ParseDict
|
|
11
|
+
from google.protobuf.wrappers_pb2 import Int32Value, UInt32Value, StringValue
|
|
12
|
+
|
|
13
|
+
from pirogue_admin_api import system_pb2, system_pb2_grpc
|
|
14
|
+
from pirogue_admin_api import services_pb2, services_pb2_grpc
|
|
15
|
+
from pirogue_admin_api import network_pb2, network_pb2_grpc
|
|
16
|
+
|
|
17
|
+
from pirogue_admin_api import (
|
|
18
|
+
PIROGUE_ADMIN_AUTH_HEADER, PIROGUE_ADMIN_AUTH_SCHEME,
|
|
19
|
+
PIROGUE_ADMIN_TCP_PORT)
|
|
20
|
+
from pirogue_admin_api.network_pb2 import WifiConfiguration, VPNPeerAddRequest, ClosePortRequest, IsolatedPort, PublicAccessRequest
|
|
21
|
+
from pirogue_admin_api.services_pb2 import DashboardConfiguration, SuricataRulesSource
|
|
22
|
+
from pirogue_admin_client.types import OperatingMode
|
|
23
|
+
|
|
24
|
+
EMPTY = empty_pb2.Empty()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class NoPiRogueAdminConnection(BaseException):
|
|
28
|
+
""""""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ApplyConfigurationError(BaseException):
|
|
32
|
+
""""""
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _inject_token_request(callback: grpc.AuthMetadataPluginCallback,
|
|
36
|
+
token: Optional[str], error: Optional[Exception]):
|
|
37
|
+
metadata = ((PIROGUE_ADMIN_AUTH_HEADER, '%s %s' % (PIROGUE_ADMIN_AUTH_SCHEME, token)),)
|
|
38
|
+
callback(metadata, error)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class TokenAuthMetadataPlugin(grpc.AuthMetadataPlugin):
|
|
42
|
+
"""Metadata wrapper for raw access token credentials."""
|
|
43
|
+
_token: str
|
|
44
|
+
|
|
45
|
+
def __init__(self, access_token: str):
|
|
46
|
+
self._token = access_token
|
|
47
|
+
|
|
48
|
+
def __call__(self, context: grpc.AuthMetadataContext,
|
|
49
|
+
callback: grpc.AuthMetadataPluginCallback):
|
|
50
|
+
_inject_token_request(callback, self._token, None)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def access_token_call_credentials(access_token):
|
|
54
|
+
"""Construct CallCredentials from an access token.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
access_token: A string to place directly in the http request
|
|
58
|
+
authorization header, for example
|
|
59
|
+
"authorization: Token <access_token>".
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
A CallCredentials.
|
|
63
|
+
"""
|
|
64
|
+
return grpc.metadata_call_credentials(
|
|
65
|
+
TokenAuthMetadataPlugin(access_token), None)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class BaseAdapter:
|
|
69
|
+
|
|
70
|
+
def __init__(self, channel):
|
|
71
|
+
pass
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class SystemAdapter(BaseAdapter):
|
|
75
|
+
_stub_system: system_pb2_grpc.SystemStub
|
|
76
|
+
|
|
77
|
+
def __init__(self, channel):
|
|
78
|
+
super(SystemAdapter, self).__init__(channel)
|
|
79
|
+
self._stub_system = system_pb2_grpc.SystemStub(channel)
|
|
80
|
+
|
|
81
|
+
def get_configuration_tree(self):
|
|
82
|
+
answer = self._stub_system.GetConfigurationTree(EMPTY)
|
|
83
|
+
answer = MessageToDict(answer, preserving_proto_field_name=True)
|
|
84
|
+
return answer
|
|
85
|
+
|
|
86
|
+
def get_configuration(self):
|
|
87
|
+
answer = self._stub_system.GetConfiguration(EMPTY)
|
|
88
|
+
answer = MessageToDict(answer, preserving_proto_field_name=True)
|
|
89
|
+
if 'variables' in answer:
|
|
90
|
+
answer = answer['variables']
|
|
91
|
+
return answer
|
|
92
|
+
|
|
93
|
+
def get_operating_mode(self):
|
|
94
|
+
answer = self._stub_system.GetOperatingMode(EMPTY)
|
|
95
|
+
return OperatingMode(answer.mode)
|
|
96
|
+
|
|
97
|
+
def get_status(self):
|
|
98
|
+
answer = self._stub_system.GetStatus(EMPTY)
|
|
99
|
+
answer = MessageToDict(answer, preserving_proto_field_name=True)
|
|
100
|
+
return answer
|
|
101
|
+
|
|
102
|
+
def get_packages_info(self):
|
|
103
|
+
answer = self._stub_system.GetPackagesInfo(EMPTY)
|
|
104
|
+
answer = MessageToDict(answer, preserving_proto_field_name=True)
|
|
105
|
+
if 'packages' in answer:
|
|
106
|
+
answer = answer['packages']
|
|
107
|
+
return answer
|
|
108
|
+
|
|
109
|
+
def get_hostname(self):
|
|
110
|
+
answer = self._stub_system.GetHostname(EMPTY)
|
|
111
|
+
answer = MessageToDict(answer)
|
|
112
|
+
return answer
|
|
113
|
+
|
|
114
|
+
def set_hostname(self, hostname: str):
|
|
115
|
+
answer = self._stub_system.SetHostname(StringValue(value=hostname))
|
|
116
|
+
|
|
117
|
+
def get_locale(self):
|
|
118
|
+
answer = self._stub_system.GetLocale(EMPTY)
|
|
119
|
+
answer = MessageToDict(answer)
|
|
120
|
+
return answer
|
|
121
|
+
|
|
122
|
+
def set_locale(self, locale: str):
|
|
123
|
+
answer = self._stub_system.SetLocale(StringValue(value=locale))
|
|
124
|
+
|
|
125
|
+
def get_timezone(self):
|
|
126
|
+
answer = self._stub_system.GetTimezone(EMPTY)
|
|
127
|
+
answer = MessageToDict(answer)
|
|
128
|
+
return answer
|
|
129
|
+
|
|
130
|
+
def set_timezone(self, timezone: str):
|
|
131
|
+
answer = self._stub_system.SetTimezone(StringValue(value=timezone))
|
|
132
|
+
|
|
133
|
+
def list_connected_devices(self):
|
|
134
|
+
answer = self._stub_system.ListConnectedDevices(EMPTY)
|
|
135
|
+
answer = MessageToDict(answer)
|
|
136
|
+
return answer
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class NetworkAdapter(BaseAdapter):
|
|
140
|
+
_stub_network: network_pb2_grpc.NetworkStub
|
|
141
|
+
|
|
142
|
+
def __init__(self, channel):
|
|
143
|
+
super(NetworkAdapter, self).__init__(channel)
|
|
144
|
+
self._stub_network = network_pb2_grpc.NetworkStub(channel)
|
|
145
|
+
|
|
146
|
+
def get_wifi_configuration(self):
|
|
147
|
+
answer = self._stub_network.GetWifiConfiguration(EMPTY)
|
|
148
|
+
answer = MessageToDict(answer, preserving_proto_field_name=True)
|
|
149
|
+
return answer
|
|
150
|
+
|
|
151
|
+
def set_wifi_configuration(self, ssid: str = None, passphrase: str = None, country_code: str = None):
|
|
152
|
+
if ssid is None and passphrase is None and country_code is None:
|
|
153
|
+
raise ValueError('please provide at least one of ssid, passphrase or country_code')
|
|
154
|
+
message = WifiConfiguration()
|
|
155
|
+
if ssid:
|
|
156
|
+
message.ssid = ssid
|
|
157
|
+
if passphrase:
|
|
158
|
+
message.passphrase = passphrase
|
|
159
|
+
if country_code:
|
|
160
|
+
message.country_code = country_code
|
|
161
|
+
logging.debug(f'set_wifi_configuration({message})')
|
|
162
|
+
answer = self._stub_network.SetWifiConfiguration(message)
|
|
163
|
+
|
|
164
|
+
def list_vpn_peers(self):
|
|
165
|
+
answer = self._stub_network.ListVPNPeers(EMPTY)
|
|
166
|
+
answer = MessageToDict(answer, preserving_proto_field_name=True)
|
|
167
|
+
if 'peers' in answer:
|
|
168
|
+
answer = answer['peers']
|
|
169
|
+
return answer
|
|
170
|
+
|
|
171
|
+
def get_vpn_peer(self, idx: int):
|
|
172
|
+
idx_param = Int32Value()
|
|
173
|
+
idx_param.value = int(idx)
|
|
174
|
+
answer = self._stub_network.GetVPNPeer(idx_param)
|
|
175
|
+
answer = MessageToDict(answer, preserving_proto_field_name=True)
|
|
176
|
+
return answer
|
|
177
|
+
|
|
178
|
+
def get_vpn_peer_config(self, idx: int):
|
|
179
|
+
idx_param = Int32Value()
|
|
180
|
+
idx_param.value = int(idx)
|
|
181
|
+
answer = self._stub_network.GetVPNPeerConfig(idx_param)
|
|
182
|
+
answer = MessageToDict(answer, preserving_proto_field_name=True)
|
|
183
|
+
return answer
|
|
184
|
+
|
|
185
|
+
def add_vpn_peer(self, comment: str = None, public_key: str = None):
|
|
186
|
+
add_request = VPNPeerAddRequest(comment=comment, public_key=public_key)
|
|
187
|
+
answer = self._stub_network.AddVPNPeer(add_request)
|
|
188
|
+
answer = MessageToDict(answer, preserving_proto_field_name=True)
|
|
189
|
+
return answer
|
|
190
|
+
|
|
191
|
+
def delete_vpn_peer(self, idx: int):
|
|
192
|
+
idx_param = Int32Value()
|
|
193
|
+
idx_param.value = int(idx)
|
|
194
|
+
answer = self._stub_network.DeleteVPNPeer(idx_param)
|
|
195
|
+
answer = MessageToDict(answer, preserving_proto_field_name=True)
|
|
196
|
+
return answer
|
|
197
|
+
|
|
198
|
+
def reset_administration_token(self):
|
|
199
|
+
answer = self._stub_network.ResetAdministrationToken(EMPTY)
|
|
200
|
+
answer = MessageToDict(answer)
|
|
201
|
+
return answer
|
|
202
|
+
|
|
203
|
+
def get_administration_token(self):
|
|
204
|
+
answer = self._stub_network.GetAdministrationToken(EMPTY)
|
|
205
|
+
answer = MessageToDict(answer)
|
|
206
|
+
return answer
|
|
207
|
+
|
|
208
|
+
def get_administration_certificate(self):
|
|
209
|
+
answer = self._stub_network.GetAdministrationCertificate(EMPTY)
|
|
210
|
+
answer = MessageToDict(answer)
|
|
211
|
+
return answer
|
|
212
|
+
|
|
213
|
+
def get_administration_clis(self):
|
|
214
|
+
answer = self._stub_network.GetAdministrationCLIs(EMPTY)
|
|
215
|
+
answer = MessageToDict(answer)
|
|
216
|
+
return answer
|
|
217
|
+
|
|
218
|
+
def enable_external_public_access(self, domain: str, email: str):
|
|
219
|
+
request = PublicAccessRequest(domain=domain, email=email)
|
|
220
|
+
answer = self._stub_network.EnableExternalPublicAccess(request)
|
|
221
|
+
|
|
222
|
+
def disable_external_public_access(self):
|
|
223
|
+
answer = self._stub_network.DisableExternalPublicAccess(EMPTY)
|
|
224
|
+
|
|
225
|
+
def list_isolated_open_ports(self):
|
|
226
|
+
answer = self._stub_network.ListIsolatedOpenPorts(EMPTY)
|
|
227
|
+
answer = MessageToDict(answer, preserving_proto_field_name=True)
|
|
228
|
+
if 'ports' in answer:
|
|
229
|
+
answer = answer['ports']
|
|
230
|
+
return answer
|
|
231
|
+
|
|
232
|
+
def open_isolated_port(self, incoming_port: int, outgoing_port: int = None):
|
|
233
|
+
request = IsolatedPort(port=int(incoming_port))
|
|
234
|
+
if outgoing_port:
|
|
235
|
+
request.destination_port = int(outgoing_port)
|
|
236
|
+
answer = self._stub_network.OpenIsolatedPort(request)
|
|
237
|
+
|
|
238
|
+
def close_isolated_port(self, incoming_port: int = None):
|
|
239
|
+
request = ClosePortRequest()
|
|
240
|
+
if incoming_port:
|
|
241
|
+
request.port = int(incoming_port)
|
|
242
|
+
answer = self._stub_network.CloseIsolatedPort(request)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
class ServicesAdapter(BaseAdapter):
|
|
246
|
+
_stub_services: services_pb2_grpc.ServicesStub
|
|
247
|
+
|
|
248
|
+
def __init__(self, channel):
|
|
249
|
+
super(ServicesAdapter, self).__init__(channel)
|
|
250
|
+
self._stub_services = services_pb2_grpc.ServicesStub(channel)
|
|
251
|
+
|
|
252
|
+
def set_dashboard_configuration(self, password: str):
|
|
253
|
+
request = DashboardConfiguration(password=password)
|
|
254
|
+
answer = self._stub_services.SetDashboardConfiguration(request)
|
|
255
|
+
|
|
256
|
+
def get_dashboard_configuration(self):
|
|
257
|
+
answer = self._stub_services.GetDashboardConfiguration(EMPTY)
|
|
258
|
+
answer = MessageToDict(answer, preserving_proto_field_name=True)
|
|
259
|
+
return answer
|
|
260
|
+
|
|
261
|
+
def list_suricata_rules_sources(self):
|
|
262
|
+
answer = self._stub_services.ListSuricataRulesSources(EMPTY)
|
|
263
|
+
answer = MessageToDict(answer, preserving_proto_field_name=True)
|
|
264
|
+
if 'sources' in answer:
|
|
265
|
+
answer = answer['sources']
|
|
266
|
+
return answer
|
|
267
|
+
|
|
268
|
+
def add_suricata_rules_source(self, name: str, url: str):
|
|
269
|
+
add_request = SuricataRulesSource(name=name, url=url)
|
|
270
|
+
answer = self._stub_services.AddSuricataRulesSource(add_request)
|
|
271
|
+
|
|
272
|
+
def delete_suricata_rules_source(self, name: str):
|
|
273
|
+
answer = self._stub_services.DeleteSuricataRulesSource(StringValue(value=name))
|