primitive 0.2.68__py3-none-any.whl → 0.2.72__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.
Potentially problematic release.
This version of primitive might be problematic. Click here for more details.
- primitive/__about__.py +1 -1
- primitive/agent/actions.py +8 -5
- primitive/agent/pxe.py +71 -0
- primitive/agent/runner.py +56 -45
- primitive/cli.py +2 -0
- primitive/client.py +2 -0
- primitive/files/actions.py +23 -1
- primitive/hardware/actions.py +202 -2
- primitive/hardware/commands.py +13 -2
- primitive/messaging/provider.py +1 -0
- primitive/monitor/actions.py +11 -6
- primitive/network/actions.py +89 -44
- primitive/network/commands.py +17 -6
- primitive/network/ssh.py +53 -12
- primitive/network/ui.py +9 -3
- primitive/operating_systems/__init__.py +0 -0
- primitive/operating_systems/actions.py +473 -0
- primitive/operating_systems/commands.py +246 -0
- primitive/operating_systems/graphql/__init__.py +0 -0
- primitive/operating_systems/graphql/mutations.py +32 -0
- primitive/operating_systems/graphql/queries.py +36 -0
- primitive/organizations/actions.py +6 -0
- primitive/utils/cache.py +11 -0
- primitive/utils/checksums.py +44 -0
- {primitive-0.2.68.dist-info → primitive-0.2.72.dist-info}/METADATA +1 -1
- {primitive-0.2.68.dist-info → primitive-0.2.72.dist-info}/RECORD +29 -21
- {primitive-0.2.68.dist-info → primitive-0.2.72.dist-info}/WHEEL +0 -0
- {primitive-0.2.68.dist-info → primitive-0.2.72.dist-info}/entry_points.txt +0 -0
- {primitive-0.2.68.dist-info → primitive-0.2.72.dist-info}/licenses/LICENSE.txt +0 -0
primitive/monitor/actions.py
CHANGED
|
@@ -94,7 +94,7 @@ class Monitor(BaseAction):
|
|
|
94
94
|
if hardware.get("isController", False):
|
|
95
95
|
self.primitive.hardware.get_and_set_switch_info()
|
|
96
96
|
self.primitive.network.push_switch_and_interfaces_info()
|
|
97
|
-
self.primitive.
|
|
97
|
+
self.primitive.hardware.push_own_system_info()
|
|
98
98
|
self.primitive.hardware._sync_children(hardware=hardware)
|
|
99
99
|
|
|
100
100
|
sleep_amount = 5
|
|
@@ -191,11 +191,16 @@ class Monitor(BaseAction):
|
|
|
191
191
|
job_run_id = None
|
|
192
192
|
else:
|
|
193
193
|
hardware_id = hardware.get("id", None) if hardware else None
|
|
194
|
-
execution_hardware_id =
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
194
|
+
execution_hardware_id = None
|
|
195
|
+
if job_run_data:
|
|
196
|
+
execution_hardware = job_run_data.get(
|
|
197
|
+
"executionHardware", None
|
|
198
|
+
)
|
|
199
|
+
execution_hardware_id = (
|
|
200
|
+
execution_hardware.get("id", None)
|
|
201
|
+
if execution_hardware
|
|
202
|
+
else None
|
|
203
|
+
)
|
|
199
204
|
|
|
200
205
|
if (
|
|
201
206
|
hardware_id is not None
|
primitive/network/actions.py
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
from pathlib import Path
|
|
2
|
-
|
|
2
|
+
from subprocess import PIPE, Popen
|
|
3
|
+
import json
|
|
3
4
|
import requests
|
|
4
5
|
import serial
|
|
5
6
|
from loguru import logger
|
|
6
7
|
from paramiko import SSHClient
|
|
7
8
|
from typing import TypedDict
|
|
9
|
+
import re
|
|
8
10
|
|
|
11
|
+
from primitive.hardware.actions import does_executable_exist
|
|
9
12
|
from primitive.messaging.provider import MESSAGE_TYPES
|
|
10
13
|
from primitive.utils.actions import BaseAction
|
|
11
14
|
|
|
@@ -39,6 +42,11 @@ def mac_address_manufacturer_style_to_ieee(mac: str) -> str:
|
|
|
39
42
|
return ":".join(mac[i : i + 2] for i in range(0, 12, 2))
|
|
40
43
|
|
|
41
44
|
|
|
45
|
+
def natural_interface_key(s):
|
|
46
|
+
# extract numbers after "Ethernet" or subports
|
|
47
|
+
return [int(t) if t.isdigit() else t for t in re.split(r"(\d+)", s)]
|
|
48
|
+
|
|
49
|
+
|
|
42
50
|
class SwitchConnectionInfo(TypedDict):
|
|
43
51
|
vendor: str
|
|
44
52
|
hostname: str
|
|
@@ -46,6 +54,12 @@ class SwitchConnectionInfo(TypedDict):
|
|
|
46
54
|
password: str
|
|
47
55
|
|
|
48
56
|
|
|
57
|
+
class MacAddressEntry(TypedDict):
|
|
58
|
+
ip_address: str | None
|
|
59
|
+
mac_address: str
|
|
60
|
+
vlan: str
|
|
61
|
+
|
|
62
|
+
|
|
49
63
|
class Network(BaseAction):
|
|
50
64
|
def __init__(self, *args, **kwargs) -> None:
|
|
51
65
|
super().__init__(*args, **kwargs)
|
|
@@ -137,6 +151,8 @@ class Network(BaseAction):
|
|
|
137
151
|
return None
|
|
138
152
|
|
|
139
153
|
def get_switch_info(self):
|
|
154
|
+
if self.switch_connection_info is None:
|
|
155
|
+
self.primitive.hardware.get_and_set_switch_info()
|
|
140
156
|
if self.is_switch_api_enabled():
|
|
141
157
|
switch_info = self.get_switch_info_via_api()
|
|
142
158
|
if switch_info:
|
|
@@ -145,26 +161,46 @@ class Network(BaseAction):
|
|
|
145
161
|
return None
|
|
146
162
|
|
|
147
163
|
def get_interfaces_info(self):
|
|
164
|
+
if self.switch_connection_info is None:
|
|
165
|
+
self.primitive.hardware.get_and_set_switch_info()
|
|
148
166
|
if self.is_switch_api_enabled():
|
|
149
167
|
interfaces_info = self.get_interfaces_via_api()
|
|
150
168
|
mac_address_info = self.get_mac_address_info_via_api()
|
|
169
|
+
|
|
151
170
|
ip_arp_table_info = self.get_ip_arp_table_via_api()
|
|
171
|
+
controllers_neighbors = self.get_ip_arp_table_via_ip_command()
|
|
152
172
|
|
|
153
173
|
if interfaces_info and mac_address_info and ip_arp_table_info:
|
|
154
174
|
for interface, mac_info in mac_address_info.items():
|
|
155
|
-
if interface in interfaces_info
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
175
|
+
if interface in interfaces_info:
|
|
176
|
+
mac_addresses: dict[str, MacAddressEntry] = {}
|
|
177
|
+
for entry in mac_info:
|
|
178
|
+
mac_addresses[entry.get("macAddress", "")] = {
|
|
179
|
+
"mac_address": entry.get("macAddress", ""),
|
|
180
|
+
"ip_address": None,
|
|
181
|
+
"vlan": entry.get("vlanId", ""),
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
for neighbor in controllers_neighbors:
|
|
185
|
+
if neighbor.get("lladdr", "") in mac_addresses:
|
|
186
|
+
mac_addresses[neighbor.get("lladdr", "")][
|
|
166
187
|
"ip_address"
|
|
167
|
-
] =
|
|
188
|
+
] = neighbor.get("dst", None)
|
|
189
|
+
|
|
190
|
+
interfaces_info[interface]["mac_addresses"] = mac_addresses
|
|
191
|
+
|
|
192
|
+
if interface in ip_arp_table_info:
|
|
193
|
+
for ip_arp in ip_arp_table_info[interface]:
|
|
194
|
+
for mac_address_entry in interfaces_info[interface][
|
|
195
|
+
"mac_addresses"
|
|
196
|
+
].values():
|
|
197
|
+
if (
|
|
198
|
+
ip_arp.get("mac_address", "")
|
|
199
|
+
in mac_address_entry["mac_address"]
|
|
200
|
+
):
|
|
201
|
+
mac_address_entry["ip_address"] = ip_arp.get(
|
|
202
|
+
"ip_address", None
|
|
203
|
+
)
|
|
168
204
|
|
|
169
205
|
return interfaces_info
|
|
170
206
|
|
|
@@ -245,25 +281,27 @@ class Network(BaseAction):
|
|
|
245
281
|
# }
|
|
246
282
|
arista_interfaces_info = response.get("result", [])[0]
|
|
247
283
|
formatted_interfaces_info = {
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
"interfaceStatuses", {}
|
|
261
|
-
).items()
|
|
262
|
-
)
|
|
263
|
-
).items()
|
|
264
|
-
},
|
|
265
|
-
# "raw_output": arista_interfaces_info,
|
|
284
|
+
k: {
|
|
285
|
+
"interface_name": k,
|
|
286
|
+
"interface_type": v.get("interfaceType", ""),
|
|
287
|
+
"link_status": v.get("linkStatus", ""),
|
|
288
|
+
"line_protocol_status": v.get("lineProtocolStatus", ""),
|
|
289
|
+
"mac_addresses": {},
|
|
290
|
+
}
|
|
291
|
+
for k, v in dict(
|
|
292
|
+
sorted(
|
|
293
|
+
arista_interfaces_info.get("interfaceStatuses", {}).items()
|
|
294
|
+
)
|
|
295
|
+
).items()
|
|
266
296
|
}
|
|
297
|
+
if formatted_interfaces_info:
|
|
298
|
+
formatted_interfaces_info = {
|
|
299
|
+
k: formatted_interfaces_info[k]
|
|
300
|
+
for k in sorted(
|
|
301
|
+
formatted_interfaces_info.keys(), key=natural_interface_key
|
|
302
|
+
)
|
|
303
|
+
}
|
|
304
|
+
|
|
267
305
|
return formatted_interfaces_info
|
|
268
306
|
|
|
269
307
|
def get_mac_address_info_via_api(self):
|
|
@@ -297,9 +335,14 @@ class Network(BaseAction):
|
|
|
297
335
|
.get("tableEntries", [])
|
|
298
336
|
)
|
|
299
337
|
table_entries.sort(key=lambda x: x["lastMove"])
|
|
338
|
+
|
|
300
339
|
for entry in table_entries:
|
|
301
340
|
if entry.get("interface") not in interface_to_mac_address_info:
|
|
302
|
-
interface_to_mac_address_info[entry.get("interface")] = entry
|
|
341
|
+
interface_to_mac_address_info[entry.get("interface")] = [entry]
|
|
342
|
+
else:
|
|
343
|
+
interface_to_mac_address_info[entry.get("interface")].append(
|
|
344
|
+
entry
|
|
345
|
+
)
|
|
303
346
|
|
|
304
347
|
return interface_to_mac_address_info
|
|
305
348
|
|
|
@@ -329,6 +372,16 @@ class Network(BaseAction):
|
|
|
329
372
|
)
|
|
330
373
|
return ip_to_mac_address_info
|
|
331
374
|
|
|
375
|
+
def get_ip_arp_table_via_ip_command(self):
|
|
376
|
+
if does_executable_exist("ip") is False:
|
|
377
|
+
return []
|
|
378
|
+
|
|
379
|
+
command = "ip --json neigh show"
|
|
380
|
+
ip_result = None
|
|
381
|
+
with Popen(command.split(" "), stdout=PIPE) as process:
|
|
382
|
+
ip_result = json.loads(process.stdout.read().decode("utf-8"))
|
|
383
|
+
return ip_result
|
|
384
|
+
|
|
332
385
|
def serial_connect(self):
|
|
333
386
|
self.ser = serial.Serial()
|
|
334
387
|
self.ser.port = self.switch_tty_name
|
|
@@ -408,10 +461,11 @@ class Network(BaseAction):
|
|
|
408
461
|
|
|
409
462
|
return False
|
|
410
463
|
|
|
411
|
-
def push_switch_and_interfaces_info(self):
|
|
464
|
+
def push_switch_and_interfaces_info(self, interfaces_info: dict | None = None):
|
|
465
|
+
logger.debug("Pushing switch and interfaces info")
|
|
412
466
|
if self.primitive.messaging.ready and self.switch_connection_info is not None:
|
|
413
467
|
switch_info = self.get_switch_info()
|
|
414
|
-
interfaces_info = self.get_interfaces_info()
|
|
468
|
+
interfaces_info = interfaces_info or self.get_interfaces_info()
|
|
415
469
|
|
|
416
470
|
message = {"switch_info": {}, "interfaces_info": {}}
|
|
417
471
|
if switch_info:
|
|
@@ -424,13 +478,4 @@ class Network(BaseAction):
|
|
|
424
478
|
message_type=MESSAGE_TYPES.SWITCH_AND_INTERFACES_INFO,
|
|
425
479
|
message=message,
|
|
426
480
|
)
|
|
427
|
-
|
|
428
|
-
def push_own_network_interfaces(self):
|
|
429
|
-
if self.primitive.messaging.ready:
|
|
430
|
-
message = {
|
|
431
|
-
"network_interfaces": self.primitive.hardware._get_network_interfaces()
|
|
432
|
-
}
|
|
433
|
-
self.primitive.messaging.send_message(
|
|
434
|
-
message_type=MESSAGE_TYPES.OWN_NETWORK_INTERFACES,
|
|
435
|
-
message=message,
|
|
436
|
-
)
|
|
481
|
+
logger.debug("Switch and interfaces info pushed")
|
primitive/network/commands.py
CHANGED
|
@@ -28,13 +28,24 @@ def switch(context):
|
|
|
28
28
|
print_result(message=message, context=context)
|
|
29
29
|
|
|
30
30
|
|
|
31
|
-
@cli.command("
|
|
31
|
+
@cli.command("interfaces")
|
|
32
32
|
@click.pass_context
|
|
33
|
-
|
|
34
|
-
""
|
|
33
|
+
@click.option(
|
|
34
|
+
"--push",
|
|
35
|
+
is_flag=True,
|
|
36
|
+
show_default=True,
|
|
37
|
+
default=False,
|
|
38
|
+
help="Push current interface info.",
|
|
39
|
+
)
|
|
40
|
+
def interfaces(context, push: bool = False):
|
|
41
|
+
"""Interfaces"""
|
|
35
42
|
primitive: Primitive = context.obj.get("PRIMITIVE")
|
|
36
|
-
|
|
43
|
+
interfaces_info = primitive.network.get_interfaces_info()
|
|
44
|
+
if push:
|
|
45
|
+
primitive.network.push_switch_and_interfaces_info(
|
|
46
|
+
interfaces_info=interfaces_info
|
|
47
|
+
)
|
|
37
48
|
if context.obj["JSON"]:
|
|
38
|
-
print_result(message=
|
|
49
|
+
print_result(message=interfaces_info, context=context)
|
|
39
50
|
else:
|
|
40
|
-
render_ports_table(
|
|
51
|
+
render_ports_table(interfaces_info)
|
primitive/network/ssh.py
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
from loguru import logger
|
|
1
2
|
import paramiko
|
|
2
3
|
import socket
|
|
3
4
|
import time
|
|
5
|
+
from paramiko import SSHClient
|
|
4
6
|
|
|
5
7
|
|
|
6
8
|
def test_ssh_connection(hostname, username, password=None, key_filename=None, port=22):
|
|
@@ -25,7 +27,13 @@ def test_ssh_connection(hostname, username, password=None, key_filename=None, po
|
|
|
25
27
|
try:
|
|
26
28
|
if password:
|
|
27
29
|
ssh_client.connect(
|
|
28
|
-
hostname=hostname,
|
|
30
|
+
hostname=hostname,
|
|
31
|
+
port=port,
|
|
32
|
+
username=username,
|
|
33
|
+
password=password,
|
|
34
|
+
banner_timeout=60,
|
|
35
|
+
timeout=60,
|
|
36
|
+
auth_timeout=60,
|
|
29
37
|
)
|
|
30
38
|
elif key_filename:
|
|
31
39
|
ssh_client.connect(
|
|
@@ -33,26 +41,29 @@ def test_ssh_connection(hostname, username, password=None, key_filename=None, po
|
|
|
33
41
|
port=port,
|
|
34
42
|
username=username,
|
|
35
43
|
key_filename=key_filename,
|
|
44
|
+
banner_timeout=60,
|
|
45
|
+
timeout=60,
|
|
46
|
+
auth_timeout=60,
|
|
36
47
|
)
|
|
37
48
|
else:
|
|
38
|
-
|
|
49
|
+
logger.error(
|
|
39
50
|
"Error: Either password or key_filename must be provided for authentication."
|
|
40
51
|
)
|
|
41
52
|
return False
|
|
42
53
|
|
|
43
|
-
|
|
54
|
+
logger.info(f"Successfully connected to {hostname} as {username}")
|
|
44
55
|
return True
|
|
45
56
|
except paramiko.AuthenticationException:
|
|
46
|
-
|
|
57
|
+
logger.debug(f"Authentication failed for {username} on {hostname}")
|
|
47
58
|
return False
|
|
48
|
-
except paramiko.SSHException as
|
|
49
|
-
|
|
59
|
+
except paramiko.SSHException as exception:
|
|
60
|
+
logger.debug(f"SSH error connecting to {hostname}: {exception}")
|
|
50
61
|
return False
|
|
51
|
-
except socket.error as
|
|
52
|
-
|
|
62
|
+
except socket.error as exception:
|
|
63
|
+
logger.debug(f"Socket error connecting to {hostname}: {exception}")
|
|
53
64
|
return False
|
|
54
|
-
except Exception as
|
|
55
|
-
|
|
65
|
+
except Exception as exception:
|
|
66
|
+
logger.debug(f"An unexpected error occurred: {exception}")
|
|
56
67
|
return False
|
|
57
68
|
finally:
|
|
58
69
|
ssh_client.close()
|
|
@@ -85,10 +96,40 @@ def wait_for_ssh(
|
|
|
85
96
|
hostname, username, password=password, key_filename=key_filename, port=port
|
|
86
97
|
):
|
|
87
98
|
return True
|
|
88
|
-
|
|
99
|
+
logger.debug(f"Waiting for SSH to become available on {hostname}...")
|
|
89
100
|
time.sleep(10)
|
|
90
101
|
|
|
91
|
-
|
|
102
|
+
logger.warning(
|
|
92
103
|
f"Timeout reached: Unable to connect to {hostname} via SSH within {timeout} seconds."
|
|
93
104
|
)
|
|
94
105
|
return False
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def run_command(
|
|
109
|
+
hostname,
|
|
110
|
+
username,
|
|
111
|
+
command: str,
|
|
112
|
+
password=None,
|
|
113
|
+
key_filename=None,
|
|
114
|
+
port=22,
|
|
115
|
+
):
|
|
116
|
+
ssh_client = SSHClient()
|
|
117
|
+
ssh_client.load_system_host_keys()
|
|
118
|
+
ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
|
119
|
+
ssh_client.connect(
|
|
120
|
+
hostname=hostname,
|
|
121
|
+
port=port,
|
|
122
|
+
username=username,
|
|
123
|
+
password=password,
|
|
124
|
+
key_filename=key_filename,
|
|
125
|
+
)
|
|
126
|
+
stdin, stdout, stderr = ssh_client.exec_command(command)
|
|
127
|
+
|
|
128
|
+
stdout_string = stdout.read().decode("utf-8").rstrip("\n")
|
|
129
|
+
stderr_string = stderr.read().decode("utf-8").rstrip("\n")
|
|
130
|
+
if stdout_string != b"":
|
|
131
|
+
logger.info(stdout_string)
|
|
132
|
+
if stderr_string != b"":
|
|
133
|
+
logger.error(stderr_string)
|
|
134
|
+
|
|
135
|
+
ssh_client.close()
|
primitive/network/ui.py
CHANGED
|
@@ -8,12 +8,18 @@ def render_ports_table(ports_dict) -> None:
|
|
|
8
8
|
table = Table(show_header=True, header_style="bold #FFA800")
|
|
9
9
|
table.add_column("Port")
|
|
10
10
|
table.add_column("Status")
|
|
11
|
-
table.add_column("MAC Address")
|
|
12
|
-
table.add_column("IP Address")
|
|
11
|
+
table.add_column("MAC Address | IP | VLAN")
|
|
13
12
|
|
|
14
13
|
for k, v in ports_dict.items():
|
|
15
14
|
table.add_row(
|
|
16
|
-
k,
|
|
15
|
+
k,
|
|
16
|
+
v.get("link_status"),
|
|
17
|
+
"\n".join(
|
|
18
|
+
[
|
|
19
|
+
f"{key} | {values.get('ip_address')} | VLAN {values.get('vlan')}"
|
|
20
|
+
for key, values in v.get("mac_addresses", {}).items()
|
|
21
|
+
]
|
|
22
|
+
),
|
|
17
23
|
)
|
|
18
24
|
|
|
19
25
|
console.print(table)
|
|
File without changes
|