primitive 0.2.61__py3-none-any.whl → 0.2.64__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.
- primitive/__about__.py +1 -1
- primitive/agent/actions.py +33 -3
- primitive/agent/runner.py +99 -3
- primitive/cli.py +2 -0
- primitive/client.py +2 -0
- primitive/exec/actions.py +4 -0
- primitive/hardware/actions.py +232 -15
- primitive/hardware/commands.py +8 -2
- primitive/hardware/graphql/fragments.py +10 -0
- primitive/hardware/graphql/mutations.py +16 -1
- primitive/hardware/graphql/queries.py +48 -0
- primitive/jobs/graphql/fragments.py +4 -0
- primitive/jobs/graphql/queries.py +4 -4
- primitive/messaging/provider.py +9 -4
- primitive/monitor/actions.py +28 -6
- primitive/network/__init__.py +0 -0
- primitive/network/actions.py +436 -0
- primitive/network/commands.py +40 -0
- primitive/network/redfish.py +121 -0
- primitive/network/ui.py +19 -0
- primitive/provisioning/actions.py +8 -1
- primitive/provisioning/graphql/queries.py +1 -1
- primitive/reservations/graphql/queries.py +1 -1
- {primitive-0.2.61.dist-info → primitive-0.2.64.dist-info}/METADATA +2 -1
- {primitive-0.2.61.dist-info → primitive-0.2.64.dist-info}/RECORD +28 -23
- {primitive-0.2.61.dist-info → primitive-0.2.64.dist-info}/WHEEL +0 -0
- {primitive-0.2.61.dist-info → primitive-0.2.64.dist-info}/entry_points.txt +0 -0
- {primitive-0.2.61.dist-info → primitive-0.2.64.dist-info}/licenses/LICENSE.txt +0 -0
|
@@ -30,6 +30,47 @@ query hardwareList(
|
|
|
30
30
|
"""
|
|
31
31
|
)
|
|
32
32
|
|
|
33
|
+
hardware_with_parent_list = (
|
|
34
|
+
hardware_fragment
|
|
35
|
+
+ """
|
|
36
|
+
query hardwareWithParentList(
|
|
37
|
+
$before: String
|
|
38
|
+
$after: String
|
|
39
|
+
$first: Int
|
|
40
|
+
$last: Int
|
|
41
|
+
$filters: HardwareFilters
|
|
42
|
+
) {
|
|
43
|
+
hardwareList(
|
|
44
|
+
before: $before
|
|
45
|
+
after: $after
|
|
46
|
+
first: $first
|
|
47
|
+
last: $last
|
|
48
|
+
filters: $filters
|
|
49
|
+
) {
|
|
50
|
+
totalCount
|
|
51
|
+
edges {
|
|
52
|
+
cursor
|
|
53
|
+
node {
|
|
54
|
+
...HardwareFragment
|
|
55
|
+
parent {
|
|
56
|
+
id
|
|
57
|
+
pk
|
|
58
|
+
name
|
|
59
|
+
slug
|
|
60
|
+
manufacturer {
|
|
61
|
+
id
|
|
62
|
+
pk
|
|
63
|
+
name
|
|
64
|
+
slug
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
"""
|
|
72
|
+
)
|
|
73
|
+
|
|
33
74
|
nested_children_hardware_list = (
|
|
34
75
|
hardware_fragment
|
|
35
76
|
+ """
|
|
@@ -97,3 +138,10 @@ query hardwareDetails(
|
|
|
97
138
|
}
|
|
98
139
|
"""
|
|
99
140
|
)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
hardware_secret = """
|
|
144
|
+
query hardwareSecret($hardwareId: ID!) {
|
|
145
|
+
hardwareSecret(hardwareId: $hardwareId)
|
|
146
|
+
}
|
|
147
|
+
"""
|
|
@@ -74,7 +74,7 @@ query jobRuns(
|
|
|
74
74
|
job_run_query = (
|
|
75
75
|
job_run_fragment
|
|
76
76
|
+ """
|
|
77
|
-
query jobRun($id:
|
|
77
|
+
query jobRun($id: ID!) {
|
|
78
78
|
jobRun(id: $id) {
|
|
79
79
|
...JobRunFragment
|
|
80
80
|
}
|
|
@@ -83,7 +83,7 @@ query jobRun($id: GlobalID!) {
|
|
|
83
83
|
)
|
|
84
84
|
|
|
85
85
|
github_app_token_for_job_run_query = """
|
|
86
|
-
query ghAppTokenForJobRun($jobRunId:
|
|
86
|
+
query ghAppTokenForJobRun($jobRunId: ID!) {
|
|
87
87
|
ghAppTokenForJobRun(jobRunId: $jobRunId)
|
|
88
88
|
}
|
|
89
89
|
"""
|
|
@@ -91,7 +91,7 @@ query ghAppTokenForJobRun($jobRunId: GlobalID!) {
|
|
|
91
91
|
job_run_status_query = (
|
|
92
92
|
job_run_status_fragment
|
|
93
93
|
+ """
|
|
94
|
-
query jobRun($id:
|
|
94
|
+
query jobRun($id: ID!) {
|
|
95
95
|
jobRun(id: $id) {
|
|
96
96
|
...JobRunStatusFragment
|
|
97
97
|
}
|
|
@@ -101,7 +101,7 @@ query jobRun($id: GlobalID!) {
|
|
|
101
101
|
|
|
102
102
|
|
|
103
103
|
job_secrets_for_job_run_query = """
|
|
104
|
-
query jobSecretsForJobRun($jobRunId:
|
|
104
|
+
query jobSecretsForJobRun($jobRunId: ID!) {
|
|
105
105
|
jobSecretsForJobRun(jobRunId: $jobRunId)
|
|
106
106
|
}
|
|
107
107
|
"""
|
primitive/messaging/provider.py
CHANGED
|
@@ -3,16 +3,18 @@ import json
|
|
|
3
3
|
from datetime import datetime, timezone
|
|
4
4
|
from typing import TYPE_CHECKING
|
|
5
5
|
from uuid import uuid4
|
|
6
|
+
|
|
6
7
|
import pika
|
|
8
|
+
from loguru import logger
|
|
7
9
|
from pika import credentials
|
|
10
|
+
|
|
11
|
+
from primitive.utils.actions import BaseAction
|
|
12
|
+
from primitive.__about__ import __version__
|
|
8
13
|
from ..utils.x509 import (
|
|
9
14
|
are_certificate_files_present,
|
|
10
15
|
create_ssl_context,
|
|
11
16
|
read_certificate_common_name,
|
|
12
17
|
)
|
|
13
|
-
from loguru import logger
|
|
14
|
-
|
|
15
|
-
from primitive.utils.actions import BaseAction
|
|
16
18
|
|
|
17
19
|
if TYPE_CHECKING:
|
|
18
20
|
import primitive.client
|
|
@@ -26,6 +28,8 @@ CELERY_TASK_NAME = "hardware.tasks.task_receive_hardware_message"
|
|
|
26
28
|
class MESSAGE_TYPES(enum.Enum):
|
|
27
29
|
CHECK_IN = "CHECK_IN"
|
|
28
30
|
METRICS = "METRICS"
|
|
31
|
+
SWITCH_AND_INTERFACES_INFO = "SWITCH_AND_INTERFACES_INFO"
|
|
32
|
+
OWN_NETWORK_INTERFACES = "OWN_NETWORK_INTERFACES"
|
|
29
33
|
|
|
30
34
|
|
|
31
35
|
class MessagingProvider(BaseAction):
|
|
@@ -52,7 +56,7 @@ class MessagingProvider(BaseAction):
|
|
|
52
56
|
elif primitive.host == "api.test.primitive.tech":
|
|
53
57
|
rabbitmq_host = "rabbitmq-cluster.test.primitive.tech"
|
|
54
58
|
elif primitive.host == "localhost:8000":
|
|
55
|
-
rabbitmq_host = "
|
|
59
|
+
rabbitmq_host = primitive.host.split(":")[0]
|
|
56
60
|
RABBITMQ_PORT = 5671
|
|
57
61
|
|
|
58
62
|
if not are_certificate_files_present():
|
|
@@ -99,6 +103,7 @@ class MessagingProvider(BaseAction):
|
|
|
99
103
|
|
|
100
104
|
headers = {
|
|
101
105
|
"fingerprint": self.fingerprint,
|
|
106
|
+
"version": __version__,
|
|
102
107
|
"token": self.token,
|
|
103
108
|
"argsrepr": "()",
|
|
104
109
|
"id": message_uuid,
|
primitive/monitor/actions.py
CHANGED
|
@@ -39,7 +39,7 @@ class Monitor(BaseAction):
|
|
|
39
39
|
# only set is_available after we've checked that no active reservation is present
|
|
40
40
|
# setting is_available of the parent also effects the children,
|
|
41
41
|
# which may have active reservations as well
|
|
42
|
-
self.primitive.hardware.
|
|
42
|
+
self.primitive.hardware.check_in(is_online=True)
|
|
43
43
|
except Exception as exception:
|
|
44
44
|
logger.exception(f"Error checking in hardware: {exception}")
|
|
45
45
|
sys.exit(1)
|
|
@@ -59,6 +59,8 @@ class Monitor(BaseAction):
|
|
|
59
59
|
active_reservation_data = None
|
|
60
60
|
previous_reservation_id = None
|
|
61
61
|
active_reservation_id = None
|
|
62
|
+
hardware = None
|
|
63
|
+
job_run_data = None
|
|
62
64
|
|
|
63
65
|
last_provisioned_reservation_id = None
|
|
64
66
|
|
|
@@ -68,8 +70,8 @@ class Monitor(BaseAction):
|
|
|
68
70
|
# obtains an active JobRun's ID
|
|
69
71
|
if not RUNNING_IN_CONTAINER:
|
|
70
72
|
# self.primitive.hardware.push_metrics()
|
|
71
|
-
|
|
72
73
|
hardware = self.primitive.hardware.get_own_hardware_details()
|
|
74
|
+
|
|
73
75
|
# fetch the latest hardware and activeReservation details
|
|
74
76
|
if active_reservation_data := hardware["activeReservation"]:
|
|
75
77
|
active_reservation_id = active_reservation_data.get("id", None)
|
|
@@ -85,9 +87,14 @@ class Monitor(BaseAction):
|
|
|
85
87
|
and active_reservation_id is None
|
|
86
88
|
and previous_reservation_id is None
|
|
87
89
|
):
|
|
88
|
-
self.primitive.hardware.
|
|
90
|
+
self.primitive.hardware.check_in(
|
|
89
91
|
is_available=True, is_online=True
|
|
90
92
|
)
|
|
93
|
+
# if the hardware is a control node, get the latest switch info
|
|
94
|
+
if hardware.get("isController", False):
|
|
95
|
+
self.primitive.hardware.get_and_set_switch_info()
|
|
96
|
+
self.primitive.network.push_switch_and_interfaces_info()
|
|
97
|
+
self.primitive.network.push_own_network_interfaces()
|
|
91
98
|
self.primitive.hardware._sync_children(hardware=hardware)
|
|
92
99
|
|
|
93
100
|
sleep_amount = 5
|
|
@@ -138,7 +145,7 @@ class Monitor(BaseAction):
|
|
|
138
145
|
|
|
139
146
|
# Golden state for normal reservation
|
|
140
147
|
if not job_run_id and active_reservation_id:
|
|
141
|
-
self.primitive.hardware.
|
|
148
|
+
self.primitive.hardware.check_in(
|
|
142
149
|
is_available=False, is_online=True
|
|
143
150
|
)
|
|
144
151
|
sleep_amount = 5
|
|
@@ -148,7 +155,7 @@ class Monitor(BaseAction):
|
|
|
148
155
|
sleep(sleep_amount)
|
|
149
156
|
continue
|
|
150
157
|
|
|
151
|
-
#
|
|
158
|
+
# job_run_id can come from 3 places:
|
|
152
159
|
# 1. an explicitly passed job_run_id
|
|
153
160
|
# 2. the previous reservation has an job_run_id (kill old PIDs)
|
|
154
161
|
# 3. the active reservation has an job_run_id (check status)
|
|
@@ -183,6 +190,21 @@ class Monitor(BaseAction):
|
|
|
183
190
|
status_value = None
|
|
184
191
|
job_run_id = None
|
|
185
192
|
else:
|
|
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
|
+
)
|
|
199
|
+
|
|
200
|
+
if (
|
|
201
|
+
hardware_id is not None
|
|
202
|
+
and execution_hardware_id is not None
|
|
203
|
+
and (hardware_id != execution_hardware_id)
|
|
204
|
+
):
|
|
205
|
+
logger.info(
|
|
206
|
+
f"Job Run {job_run_id} is being executed by the controller. Monitoring may stop."
|
|
207
|
+
)
|
|
186
208
|
logger.info(
|
|
187
209
|
f"Job Run {job_run_id} with Status {status_value} with PID {parent_pid}. [sleeping {sleep_amount} seconds]"
|
|
188
210
|
)
|
|
@@ -193,7 +215,7 @@ class Monitor(BaseAction):
|
|
|
193
215
|
logger.info("Stopping primitive monitor...")
|
|
194
216
|
try:
|
|
195
217
|
if not RUNNING_IN_CONTAINER:
|
|
196
|
-
self.primitive.hardware.
|
|
218
|
+
self.primitive.hardware.check_in(
|
|
197
219
|
is_available=False, is_online=False, stopping_agent=True
|
|
198
220
|
)
|
|
199
221
|
|
|
File without changes
|
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
import requests
|
|
4
|
+
import serial
|
|
5
|
+
from loguru import logger
|
|
6
|
+
from paramiko import SSHClient
|
|
7
|
+
from typing import TypedDict
|
|
8
|
+
|
|
9
|
+
from primitive.messaging.provider import MESSAGE_TYPES
|
|
10
|
+
from primitive.utils.actions import BaseAction
|
|
11
|
+
|
|
12
|
+
# 0. see if this controller node hardware has a switch connected to it
|
|
13
|
+
# see if there is any information about the switch from the database
|
|
14
|
+
# 1. check if the switch is reachable by an IP
|
|
15
|
+
# 2. if reachable, check if the API is enabled
|
|
16
|
+
# 3. if API is enabled, return True
|
|
17
|
+
# 4. if API is not enabled
|
|
18
|
+
# - check if we can do ssh connection
|
|
19
|
+
# - if ssh connection is possible, enable the API, return True
|
|
20
|
+
# - if ssh connection is not possible, check for serial connection
|
|
21
|
+
# - if serial connection is possible, enable the API, return True
|
|
22
|
+
# - if serial connection is not possible, return False
|
|
23
|
+
# 5. if switch is not reachable by IP, SSH or Serial, return False
|
|
24
|
+
# 6. if the switch is reachable, ask for the switch information
|
|
25
|
+
# 7. json form that switch information and send it over rabbitmq
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def mac_address_manufacturer_style_to_ieee(mac: str) -> str:
|
|
29
|
+
"""
|
|
30
|
+
Convert Arista-style MAC (xxxx.xxxx.xxxx) into IEEE style (xx:xx:xx:xx:xx:xx).
|
|
31
|
+
Example: '54b2.0319.7692' -> '54:b2:03:19:76:92'
|
|
32
|
+
"""
|
|
33
|
+
# Remove dots
|
|
34
|
+
mac = mac.replace(".", "")
|
|
35
|
+
# Ensure correct length
|
|
36
|
+
if len(mac) != 12:
|
|
37
|
+
raise ValueError("Invalid Arista MAC format")
|
|
38
|
+
# Split into pairs
|
|
39
|
+
return ":".join(mac[i : i + 2] for i in range(0, 12, 2))
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class SwitchConnectionInfo(TypedDict):
|
|
43
|
+
vendor: str
|
|
44
|
+
hostname: str
|
|
45
|
+
username: str
|
|
46
|
+
password: str
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class Network(BaseAction):
|
|
50
|
+
def __init__(self, *args, **kwargs) -> None:
|
|
51
|
+
super().__init__(*args, **kwargs)
|
|
52
|
+
self.switch_tty_name = None
|
|
53
|
+
|
|
54
|
+
self.switch_connection_info: SwitchConnectionInfo | None = None
|
|
55
|
+
self.remote_switch = None
|
|
56
|
+
self.local_switch = None
|
|
57
|
+
|
|
58
|
+
# session info
|
|
59
|
+
# if it is set to None it has not been checked yet
|
|
60
|
+
self.switch_api_available: bool | None = None
|
|
61
|
+
self.switch_ssh_available: bool | None = None
|
|
62
|
+
|
|
63
|
+
def is_switch_api_enabled(self) -> bool:
|
|
64
|
+
if self.switch_api_available is not None:
|
|
65
|
+
return self.switch_api_available
|
|
66
|
+
|
|
67
|
+
self.switch_api_available = False
|
|
68
|
+
if (
|
|
69
|
+
self.switch_connection_info
|
|
70
|
+
and self.switch_connection_info["vendor"] == "arista"
|
|
71
|
+
):
|
|
72
|
+
response = requests.get(
|
|
73
|
+
f"http://{self.switch_connection_info['hostname']}/eapi"
|
|
74
|
+
)
|
|
75
|
+
if response.ok:
|
|
76
|
+
self.switch_api_available = True
|
|
77
|
+
return self.switch_api_available
|
|
78
|
+
|
|
79
|
+
def is_switch_ssh_enabled(self) -> bool:
|
|
80
|
+
if self.switch_ssh_available is not None:
|
|
81
|
+
return self.switch_ssh_available
|
|
82
|
+
|
|
83
|
+
self.switch_ssh_available = False
|
|
84
|
+
if self.switch_connection_info:
|
|
85
|
+
ssh_client = SSHClient()
|
|
86
|
+
ssh_client.load_system_host_keys()
|
|
87
|
+
|
|
88
|
+
ssh_hostname = self.switch_connection_info.get("hostname", None)
|
|
89
|
+
ssh_username = self.switch_connection_info.get("username", None)
|
|
90
|
+
ssh_password = self.switch_connection_info.get("password", None)
|
|
91
|
+
if ssh_hostname and ssh_username and ssh_password:
|
|
92
|
+
try:
|
|
93
|
+
ssh_client.connect(
|
|
94
|
+
hostname=ssh_hostname,
|
|
95
|
+
username=ssh_username,
|
|
96
|
+
password=ssh_password,
|
|
97
|
+
timeout=5,
|
|
98
|
+
)
|
|
99
|
+
self.switch_ssh_available = True
|
|
100
|
+
except Exception as exception:
|
|
101
|
+
logger.error(f"Error connecting to switch via SSH: {exception}")
|
|
102
|
+
return self.switch_ssh_available
|
|
103
|
+
|
|
104
|
+
def arista_eapi_request_handler(self, command):
|
|
105
|
+
# example commands "show version" "show interfaces status"
|
|
106
|
+
if not self.switch_connection_info:
|
|
107
|
+
return None
|
|
108
|
+
|
|
109
|
+
url = f"http://{self.switch_connection_info['hostname']}/command-api"
|
|
110
|
+
with requests.Session() as session:
|
|
111
|
+
session.auth = (
|
|
112
|
+
self.switch_connection_info["username"],
|
|
113
|
+
self.switch_connection_info["password"],
|
|
114
|
+
)
|
|
115
|
+
session.headers.update({"Content-type": "application/json-rpc"})
|
|
116
|
+
payload = {
|
|
117
|
+
"jsonrpc": "2.0",
|
|
118
|
+
"method": "runCmds",
|
|
119
|
+
"params": {
|
|
120
|
+
"version": "latest",
|
|
121
|
+
"format": "json",
|
|
122
|
+
"cmds": [command],
|
|
123
|
+
},
|
|
124
|
+
"id": 1,
|
|
125
|
+
}
|
|
126
|
+
with session:
|
|
127
|
+
response = session.post(
|
|
128
|
+
url=url,
|
|
129
|
+
json=payload,
|
|
130
|
+
)
|
|
131
|
+
if response.ok:
|
|
132
|
+
return response.json()
|
|
133
|
+
else:
|
|
134
|
+
logger.error(
|
|
135
|
+
f"Error connecting to eAPI: {response.status_code} {response.text}"
|
|
136
|
+
)
|
|
137
|
+
return None
|
|
138
|
+
|
|
139
|
+
def get_switch_info(self):
|
|
140
|
+
if self.is_switch_api_enabled():
|
|
141
|
+
switch_info = self.get_switch_info_via_api()
|
|
142
|
+
if switch_info:
|
|
143
|
+
return switch_info
|
|
144
|
+
|
|
145
|
+
return None
|
|
146
|
+
|
|
147
|
+
def get_interfaces_info(self):
|
|
148
|
+
if self.is_switch_api_enabled():
|
|
149
|
+
interfaces_info = self.get_interfaces_via_api()
|
|
150
|
+
mac_address_info = self.get_mac_address_info_via_api()
|
|
151
|
+
ip_arp_table_info = self.get_ip_arp_table_via_api()
|
|
152
|
+
|
|
153
|
+
if interfaces_info and mac_address_info and ip_arp_table_info:
|
|
154
|
+
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][
|
|
166
|
+
"ip_address"
|
|
167
|
+
] = ip_arp.get("ip_address", "")
|
|
168
|
+
|
|
169
|
+
return interfaces_info
|
|
170
|
+
|
|
171
|
+
return None
|
|
172
|
+
|
|
173
|
+
def get_mac_address_info(self):
|
|
174
|
+
if self.is_switch_api_enabled():
|
|
175
|
+
mac_address_info = self.get_mac_address_info_via_api()
|
|
176
|
+
if mac_address_info:
|
|
177
|
+
return mac_address_info
|
|
178
|
+
|
|
179
|
+
return None
|
|
180
|
+
|
|
181
|
+
def get_switch_info_via_api(self):
|
|
182
|
+
formatted_switch_info = None
|
|
183
|
+
if (
|
|
184
|
+
self.switch_connection_info
|
|
185
|
+
and self.switch_connection_info["vendor"] == "arista"
|
|
186
|
+
):
|
|
187
|
+
if response := self.arista_eapi_request_handler("show version"):
|
|
188
|
+
arista_version_info = response.get("result", [])[0]
|
|
189
|
+
# example output:
|
|
190
|
+
# {
|
|
191
|
+
# "imageFormatVersion": "3.0",
|
|
192
|
+
# "uptime": 6038503.06,
|
|
193
|
+
# "modelName": "DCS-7050TX-64-R",
|
|
194
|
+
# "internalVersion": "4.28.5.1M-30127723.42851M",
|
|
195
|
+
# "memTotal": 3982512,
|
|
196
|
+
# "mfgName": "Arista",
|
|
197
|
+
# "serialNumber": "JPE16121065",
|
|
198
|
+
# "systemMacAddress": "44:4c:a8:a3:61:77",
|
|
199
|
+
# "bootupTimestamp": 1753302953.365648,
|
|
200
|
+
# "memFree": 2546096,
|
|
201
|
+
# "version": "4.28.5.1M",
|
|
202
|
+
# "configMacAddress": "00:00:00:00:00:00",
|
|
203
|
+
# "isIntlVersion": false,
|
|
204
|
+
# "imageOptimization": "Strata-4GB",
|
|
205
|
+
# "internalBuildId": "9adca383-a3bd-4507-b53b-d99ca7d61291",
|
|
206
|
+
# "hardwareRevision": "01.11",
|
|
207
|
+
# "hwMacAddress": "44:4c:a8:a3:61:77",
|
|
208
|
+
# "architecture": "i686"
|
|
209
|
+
# },
|
|
210
|
+
formatted_switch_info = {
|
|
211
|
+
"vendor": arista_version_info.get("mfgName", ""),
|
|
212
|
+
"model": arista_version_info.get("modelName", ""),
|
|
213
|
+
"serial_number": arista_version_info.get("serialNumber", ""),
|
|
214
|
+
"mac_address": arista_version_info.get("systemMacAddress", ""),
|
|
215
|
+
# "raw_output": arista_version_info,
|
|
216
|
+
}
|
|
217
|
+
return formatted_switch_info
|
|
218
|
+
|
|
219
|
+
def get_interfaces_via_api(self):
|
|
220
|
+
formatted_interfaces_info = None
|
|
221
|
+
if (
|
|
222
|
+
self.switch_connection_info
|
|
223
|
+
and self.switch_connection_info["vendor"] == "arista"
|
|
224
|
+
):
|
|
225
|
+
if response := self.arista_eapi_request_handler("show interfaces status"):
|
|
226
|
+
# example output:
|
|
227
|
+
# {
|
|
228
|
+
# "interfaceStatuses": {
|
|
229
|
+
# "Ethernet10": {
|
|
230
|
+
# "vlanInformation": {
|
|
231
|
+
# "interfaceMode": "bridged",
|
|
232
|
+
# "vlanId": 1,
|
|
233
|
+
# "interfaceForwardingModel": "bridged"
|
|
234
|
+
# },
|
|
235
|
+
# "bandwidth": 0,
|
|
236
|
+
# "interfaceType": "10GBASE-T",
|
|
237
|
+
# "description": "george-michael-oob",
|
|
238
|
+
# "autoNegotiateActive": true,
|
|
239
|
+
# "duplex": "duplexUnknown",
|
|
240
|
+
# "autoNegotigateActive": true,
|
|
241
|
+
# "linkStatus": "notconnect",
|
|
242
|
+
# "lineProtocolStatus": "down"
|
|
243
|
+
# },
|
|
244
|
+
# }
|
|
245
|
+
# }
|
|
246
|
+
arista_interfaces_info = response.get("result", [])[0]
|
|
247
|
+
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,
|
|
266
|
+
}
|
|
267
|
+
return formatted_interfaces_info
|
|
268
|
+
|
|
269
|
+
def get_mac_address_info_via_api(self):
|
|
270
|
+
interface_to_mac_address_info = None
|
|
271
|
+
if (
|
|
272
|
+
self.switch_connection_info
|
|
273
|
+
and self.switch_connection_info["vendor"] == "arista"
|
|
274
|
+
):
|
|
275
|
+
if response := self.arista_eapi_request_handler("show mac address-table"):
|
|
276
|
+
# {
|
|
277
|
+
# "multicastTable": {"tableEntries": []},
|
|
278
|
+
# "unicastTable": {
|
|
279
|
+
# "tableEntries": [
|
|
280
|
+
# {
|
|
281
|
+
# "macAddress": "20:37:f0:6b:d6:8c",
|
|
282
|
+
# "lastMove": 1759519474.903184,
|
|
283
|
+
# "interface": "Ethernet46",
|
|
284
|
+
# "moves": 1,
|
|
285
|
+
# "entryType": "dynamic",
|
|
286
|
+
# "vlanId": 1,
|
|
287
|
+
# },
|
|
288
|
+
# ]
|
|
289
|
+
# },
|
|
290
|
+
# "disabledMacLearningVlans": [],
|
|
291
|
+
# }
|
|
292
|
+
|
|
293
|
+
interface_to_mac_address_info = {}
|
|
294
|
+
table_entries = (
|
|
295
|
+
response.get("result", [])[0]
|
|
296
|
+
.get("unicastTable", [])
|
|
297
|
+
.get("tableEntries", [])
|
|
298
|
+
)
|
|
299
|
+
table_entries.sort(key=lambda x: x["lastMove"])
|
|
300
|
+
for entry in table_entries:
|
|
301
|
+
if entry.get("interface") not in interface_to_mac_address_info:
|
|
302
|
+
interface_to_mac_address_info[entry.get("interface")] = entry
|
|
303
|
+
|
|
304
|
+
return interface_to_mac_address_info
|
|
305
|
+
|
|
306
|
+
def get_ip_arp_table_via_api(self):
|
|
307
|
+
ip_to_mac_address_info = {}
|
|
308
|
+
if (
|
|
309
|
+
self.switch_connection_info
|
|
310
|
+
and self.switch_connection_info["vendor"] == "arista"
|
|
311
|
+
):
|
|
312
|
+
if response := self.arista_eapi_request_handler("show ip arp"):
|
|
313
|
+
table_entries = response.get("result", [])[0].get("ipV4Neighbors", [])
|
|
314
|
+
for entry in table_entries:
|
|
315
|
+
interface = entry["interface"].split(", ")[
|
|
316
|
+
1
|
|
317
|
+
] # 'Vlan1, Ethernet46',
|
|
318
|
+
mac_address = mac_address_manufacturer_style_to_ieee(
|
|
319
|
+
entry["hwAddress"]
|
|
320
|
+
)
|
|
321
|
+
if interface not in ip_to_mac_address_info:
|
|
322
|
+
ip_to_mac_address_info[interface] = []
|
|
323
|
+
ip_to_mac_address_info[interface].append(
|
|
324
|
+
{
|
|
325
|
+
"ip_address": entry["address"],
|
|
326
|
+
"mac_address": mac_address,
|
|
327
|
+
"age": entry["age"],
|
|
328
|
+
}
|
|
329
|
+
)
|
|
330
|
+
return ip_to_mac_address_info
|
|
331
|
+
|
|
332
|
+
def serial_connect(self):
|
|
333
|
+
self.ser = serial.Serial()
|
|
334
|
+
self.ser.port = self.switch_tty_name
|
|
335
|
+
self.ser.baudrate = 9600
|
|
336
|
+
self.ser.open()
|
|
337
|
+
|
|
338
|
+
def serial_disconnect(self):
|
|
339
|
+
if self.ser.is_open:
|
|
340
|
+
self.ser.close()
|
|
341
|
+
|
|
342
|
+
def send_serial_command(self, command):
|
|
343
|
+
if not self.ser.is_open:
|
|
344
|
+
self.serial_connect()
|
|
345
|
+
self.ser.write(command.encode("utf-8") + b"\n")
|
|
346
|
+
response = self.ser.read_all().decode("utf-8")
|
|
347
|
+
return response
|
|
348
|
+
|
|
349
|
+
def get_tty_devices(self):
|
|
350
|
+
tty_devices = list(Path("/dev").glob("tty.*"))
|
|
351
|
+
return [str(device) for device in tty_devices]
|
|
352
|
+
|
|
353
|
+
def get_switch_tty_device_name(self):
|
|
354
|
+
if self.switch_tty_name is not None:
|
|
355
|
+
return self.switch_tty_name
|
|
356
|
+
|
|
357
|
+
tty_devices = self.get_tty_devices()
|
|
358
|
+
for device in tty_devices:
|
|
359
|
+
if "usbserial" in device or "usbmodem" in device:
|
|
360
|
+
self.switch_tty_name = device
|
|
361
|
+
break
|
|
362
|
+
if "ttyUSB0" in device:
|
|
363
|
+
self.switch_tty_name = device
|
|
364
|
+
break
|
|
365
|
+
return self.switch_tty_name
|
|
366
|
+
|
|
367
|
+
def arista_enable_api_via_ssh(self):
|
|
368
|
+
# TODO: implement this function
|
|
369
|
+
return True
|
|
370
|
+
|
|
371
|
+
def arista_enable_api_via_tty(self):
|
|
372
|
+
# TODO: implement this function
|
|
373
|
+
return True
|
|
374
|
+
# configure terminal
|
|
375
|
+
# management api http-commands
|
|
376
|
+
# protocol http
|
|
377
|
+
# no protocol https
|
|
378
|
+
# exit
|
|
379
|
+
# write memory
|
|
380
|
+
|
|
381
|
+
def enable_switch_api_via_ssh(self):
|
|
382
|
+
if (
|
|
383
|
+
self.switch_connection_info
|
|
384
|
+
and self.switch_connection_info["vendor"] == "arista"
|
|
385
|
+
):
|
|
386
|
+
return self.arista_enable_api_via_ssh()
|
|
387
|
+
return False
|
|
388
|
+
|
|
389
|
+
def enable_switch_api_via_tty(self):
|
|
390
|
+
if (
|
|
391
|
+
self.switch_connection_info
|
|
392
|
+
and self.switch_connection_info["vendor"] == "arista"
|
|
393
|
+
):
|
|
394
|
+
return self.arista_enable_api_via_tty()
|
|
395
|
+
return False
|
|
396
|
+
|
|
397
|
+
def enable_switch_api(self):
|
|
398
|
+
if self.is_switch_ssh_enabled():
|
|
399
|
+
result = self.enable_switch_api_via_ssh()
|
|
400
|
+
return result
|
|
401
|
+
|
|
402
|
+
elif self.switch_tty_name or self.get_switch_tty_device_name():
|
|
403
|
+
self.switch_tty_name = (
|
|
404
|
+
self.switch_tty_name or self.get_switch_tty_device_name()
|
|
405
|
+
)
|
|
406
|
+
result = self.enable_switch_api_via_tty()
|
|
407
|
+
return result
|
|
408
|
+
|
|
409
|
+
return False
|
|
410
|
+
|
|
411
|
+
def push_switch_and_interfaces_info(self):
|
|
412
|
+
if self.primitive.messaging.ready and self.switch_connection_info is not None:
|
|
413
|
+
switch_info = self.get_switch_info()
|
|
414
|
+
interfaces_info = self.get_interfaces_info()
|
|
415
|
+
|
|
416
|
+
message = {"switch_info": {}, "interfaces_info": {}}
|
|
417
|
+
if switch_info:
|
|
418
|
+
message["switch_info"] = switch_info
|
|
419
|
+
if interfaces_info:
|
|
420
|
+
message["interfaces_info"] = interfaces_info
|
|
421
|
+
|
|
422
|
+
if message:
|
|
423
|
+
self.primitive.messaging.send_message(
|
|
424
|
+
message_type=MESSAGE_TYPES.SWITCH_AND_INTERFACES_INFO,
|
|
425
|
+
message=message,
|
|
426
|
+
)
|
|
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
|
+
)
|