primitive 0.2.63__py3-none-any.whl → 0.2.66__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.

@@ -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
+ """
@@ -38,6 +38,10 @@ fragment JobRunFragment on JobRun {
38
38
  branch
39
39
  repoFullName
40
40
  }
41
+ executionHardware {
42
+ id
43
+ pk
44
+ }
41
45
  }
42
46
  """
43
47
 
@@ -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 = "localhost"
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,
@@ -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.check_in_http(is_online=True)
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.check_in_http(
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.check_in_http(
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
- # job_run_data can come from 3 places:
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.check_in_http(
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
+ )
@@ -0,0 +1,40 @@
1
+ from typing import TYPE_CHECKING
2
+
3
+ import click
4
+ from primitive.utils.printer import print_result
5
+ from primitive.network.ui import render_ports_table
6
+
7
+ if TYPE_CHECKING:
8
+ from ..client import Primitive
9
+
10
+
11
+ @click.group()
12
+ @click.pass_context
13
+ def cli(context):
14
+ """Network"""
15
+ pass
16
+
17
+
18
+ @cli.command("switch")
19
+ @click.pass_context
20
+ def switch(context):
21
+ """Switch"""
22
+ primitive: Primitive = context.obj.get("PRIMITIVE")
23
+ switch_info = primitive.network.get_switch_info()
24
+ if context.obj["JSON"]:
25
+ message = switch_info
26
+ else:
27
+ message = f"Vendor: {switch_info.get('vendor')}. Model: {switch_info.get('model')}. IP: {switch_info.get('ip_address')}"
28
+ print_result(message=message, context=context)
29
+
30
+
31
+ @cli.command("ports")
32
+ @click.pass_context
33
+ def ports(context):
34
+ """Ports"""
35
+ primitive: Primitive = context.obj.get("PRIMITIVE")
36
+ ports_info = primitive.network.get_interfaces_info()
37
+ if context.obj["JSON"]:
38
+ print_result(message=ports_info, context=context)
39
+ else:
40
+ render_ports_table(ports_info.get("interfaces"))