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.

@@ -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.network.push_own_network_interfaces()
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
- job_run_data.get("executionHardware", {}).get("id", None)
196
- if job_run_data
197
- else None
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
@@ -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.get("interfaces", {}):
156
- interfaces_info["interfaces"][interface]["mac_address"] = (
157
- mac_info.get("macAddress", "")
158
- )
159
- if interface in ip_arp_table_info:
160
- for ip_arp in ip_arp_table_info[interface]:
161
- if (
162
- interfaces_info["interfaces"][interface]["ip_address"]
163
- == ""
164
- ):
165
- interfaces_info["interfaces"][interface][
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
- ] = ip_arp.get("ip_address", "")
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
- "interfaces": {
249
- k: {
250
- "interface_name": k,
251
- "interface_type": v.get("interfaceType", ""),
252
- "link_status": v.get("linkStatus", ""),
253
- "line_protocol_status": v.get("lineProtocolStatus", ""),
254
- "mac_address": "",
255
- "ip_address": "",
256
- }
257
- for k, v in dict(
258
- sorted(
259
- arista_interfaces_info.get(
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")
@@ -28,13 +28,24 @@ def switch(context):
28
28
  print_result(message=message, context=context)
29
29
 
30
30
 
31
- @cli.command("ports")
31
+ @cli.command("interfaces")
32
32
  @click.pass_context
33
- def ports(context):
34
- """Ports"""
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
- ports_info = primitive.network.get_interfaces_info()
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=ports_info, context=context)
49
+ print_result(message=interfaces_info, context=context)
39
50
  else:
40
- render_ports_table(ports_info.get("interfaces"))
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, port=port, username=username, password=password
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
- print(
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
- print(f"Successfully connected to {hostname} as {username}")
54
+ logger.info(f"Successfully connected to {hostname} as {username}")
44
55
  return True
45
56
  except paramiko.AuthenticationException:
46
- print(f"Authentication failed for {username} on {hostname}")
57
+ logger.debug(f"Authentication failed for {username} on {hostname}")
47
58
  return False
48
- except paramiko.SSHException as e:
49
- print(f"SSH error connecting to {hostname}: {e}")
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 e:
52
- print(f"Socket error connecting to {hostname}: {e}")
62
+ except socket.error as exception:
63
+ logger.debug(f"Socket error connecting to {hostname}: {exception}")
53
64
  return False
54
- except Exception as e:
55
- print(f"An unexpected error occurred: {e}")
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
- print(f"Waiting for SSH to become available on {hostname}...")
99
+ logger.debug(f"Waiting for SSH to become available on {hostname}...")
89
100
  time.sleep(10)
90
101
 
91
- print(
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, v.get("link_status"), v.get("mac_address"), v.get("ip_address")
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