Compare commits
21 Commits
c9f6f3472f
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0b92e342e5 | ||
|
|
1cc225917e | ||
|
|
61892d8f51 | ||
|
|
8cebbf743a | ||
|
|
a107662e99 | ||
|
|
1d0f5ed685 | ||
|
|
5b8380aeee | ||
|
|
65c82fbaf5 | ||
|
|
974fff6038 | ||
|
|
586e52282b | ||
|
|
e3392f6c76 | ||
|
|
de0e09af9d | ||
|
|
ca96d2600a | ||
|
|
56eae15e27 | ||
|
|
db79199319 | ||
|
|
2e109db121 | ||
|
|
b9dce8e417 | ||
|
|
d185dc6c7c | ||
|
|
68566a24fb | ||
|
|
08733bd493 | ||
|
|
a1c57733f6 |
201
LICENSE
Normal file
201
LICENSE
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright [yyyy] [name of copyright owner]
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
@@ -310,6 +310,9 @@ print(interfaces.json())
|
|||||||
| MikroTik | `routeros`, `ros`, `mikrotik` |
|
| MikroTik | `routeros`, `ros`, `mikrotik` |
|
||||||
| Qtech | `qtech` |
|
| Qtech | `qtech` |
|
||||||
| Huawei | `huawei`, `vrp` |
|
| Huawei | `huawei`, `vrp` |
|
||||||
|
| Eltex | `eltex` |
|
||||||
|
| H3C | `h3c` |
|
||||||
|
| Quasar | `qos`, `quasar` |
|
||||||
|
|
||||||
|
|
||||||
Ключи реестра — это значения поля `model`, возвращаемого API для узла. Регистр не учитывается.
|
Ключи реестра — это значения поля `model`, возвращаемого API для узла. Регистр не учитывается.
|
||||||
|
|||||||
21
oxi/adapter.py
Normal file
21
oxi/adapter.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
from typing import Optional
|
||||||
|
from requests.adapters import HTTPAdapter
|
||||||
|
from urllib3.util import Retry
|
||||||
|
|
||||||
|
|
||||||
|
class OxiAdapter(HTTPAdapter):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
timeout: Optional[int] = None,
|
||||||
|
max_retries: int = 3,
|
||||||
|
*args,
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
|
self.timeout = timeout
|
||||||
|
retry = Retry(total=max_retries, backoff_factor=0.3)
|
||||||
|
super().__init__(*args, max_retries=retry, **kwargs)
|
||||||
|
|
||||||
|
def send(self, request, **kwargs):
|
||||||
|
if kwargs.get("timeout") is None:
|
||||||
|
kwargs["timeout"] = self.timeout
|
||||||
|
return super().send(request, **kwargs)
|
||||||
14
oxi/conf.py
14
oxi/conf.py
@@ -16,7 +16,7 @@ class ModelView(Generic[TModel]):
|
|||||||
def __init__(self, model: TModel | list[TModel]):
|
def __init__(self, model: TModel | list[TModel]):
|
||||||
self._model = model
|
self._model = model
|
||||||
|
|
||||||
def json(self) -> str:
|
def dump_json(self) -> str:
|
||||||
if isinstance(self._model, list):
|
if isinstance(self._model, list):
|
||||||
return json.dumps(
|
return json.dumps(
|
||||||
[item.model_dump(by_alias=True) for item in self._model],
|
[item.model_dump(by_alias=True) for item in self._model],
|
||||||
@@ -24,6 +24,11 @@ class ModelView(Generic[TModel]):
|
|||||||
)
|
)
|
||||||
return self._model.model_dump_json(by_alias=True)
|
return self._model.model_dump_json(by_alias=True)
|
||||||
|
|
||||||
|
def dump(self) -> dict | list:
|
||||||
|
if isinstance(self._model, list):
|
||||||
|
return [item.model_dump(by_alias=True) for item in self._model]
|
||||||
|
return self._model.model_dump(by_alias=True)
|
||||||
|
|
||||||
def __iter__(self) -> Iterator[TModel]:
|
def __iter__(self) -> Iterator[TModel]:
|
||||||
if isinstance(self._model, list):
|
if isinstance(self._model, list):
|
||||||
return iter(self._model)
|
return iter(self._model)
|
||||||
@@ -67,8 +72,11 @@ class NodeConfig:
|
|||||||
def text(self):
|
def text(self):
|
||||||
return self._response.text
|
return self._response.text
|
||||||
|
|
||||||
def json(self):
|
def dump_json(self):
|
||||||
return self._parsed_data.model_dump_json()
|
return self._parsed_data.model_dump_json(by_alias=True)
|
||||||
|
|
||||||
|
def dump(self):
|
||||||
|
return self._parsed_data.model_dump(by_alias=True)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.text
|
return self.text
|
||||||
|
|||||||
33
oxi/core.py
33
oxi/core.py
@@ -1,5 +1,8 @@
|
|||||||
from typing import Optional
|
from typing import Optional
|
||||||
from requests import Session
|
from requests import HTTPError, Session
|
||||||
|
|
||||||
|
from oxi.adapter import OxiAdapter
|
||||||
|
from oxi.exception import OxiAPIError
|
||||||
from .node import Node
|
from .node import Node
|
||||||
|
|
||||||
|
|
||||||
@@ -12,12 +15,24 @@ class OxiAPI:
|
|||||||
verify: bool = True,
|
verify: bool = True,
|
||||||
):
|
):
|
||||||
self.base_url = url.rstrip("/")
|
self.base_url = url.rstrip("/")
|
||||||
self._session = Session()
|
self._session = self.__create_session(username, password, verify)
|
||||||
self._session.verify = verify
|
|
||||||
if username and password:
|
|
||||||
self._session.auth = (username, password)
|
|
||||||
self.node = Node(self._session, self.base_url)
|
self.node = Node(self._session, self.base_url)
|
||||||
|
|
||||||
|
def __create_session(
|
||||||
|
self,
|
||||||
|
username: Optional[str] = None,
|
||||||
|
password: Optional[str] = None,
|
||||||
|
verify: bool = True,
|
||||||
|
) -> Session:
|
||||||
|
session = Session()
|
||||||
|
adapter = OxiAdapter(timeout=10, max_retries=3)
|
||||||
|
session.mount("https://", adapter)
|
||||||
|
session.mount("http://", adapter)
|
||||||
|
session.verify = verify
|
||||||
|
if username and password:
|
||||||
|
session.auth = (username, password)
|
||||||
|
return session
|
||||||
|
|
||||||
def __enter__(self):
|
def __enter__(self):
|
||||||
return self
|
return self
|
||||||
|
|
||||||
@@ -26,3 +41,11 @@ class OxiAPI:
|
|||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
return self._session.close()
|
return self._session.close()
|
||||||
|
|
||||||
|
def reload(self):
|
||||||
|
try:
|
||||||
|
reload_response = self._session.get(f"{self.base_url}/reload")
|
||||||
|
reload_response.raise_for_status()
|
||||||
|
except HTTPError as e:
|
||||||
|
raise OxiAPIError.from_http_error(e, context="Reload Oxidized") from e
|
||||||
|
return reload_response.status_code
|
||||||
|
|||||||
34
oxi/exception.py
Normal file
34
oxi/exception.py
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
from typing import TYPE_CHECKING, Optional
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from requests import HTTPError
|
||||||
|
|
||||||
|
_STATUS_MESSAGES: dict[int, str] = {
|
||||||
|
401: "Unauthorized",
|
||||||
|
403: "Forbidden",
|
||||||
|
500: "Internal Server Error",
|
||||||
|
502: "Bad Gateway",
|
||||||
|
503: "Service Unavailable",
|
||||||
|
504: "Gateway Timeout",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class OxiAPIError(Exception):
|
||||||
|
def __init__(self, message: str, status_code: Optional[int] = None):
|
||||||
|
super().__init__(message)
|
||||||
|
self.status_code = status_code
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
if self.status_code is not None:
|
||||||
|
return f"OxiAPIError: {self.args[0]} (HTTP {self.status_code})"
|
||||||
|
return f"OxiAPIError: {self.args[0]}"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_http_error(cls, e: "HTTPError", context: str = "") -> "OxiAPIError":
|
||||||
|
status = e.response.status_code
|
||||||
|
if status == 404:
|
||||||
|
message = f"{context} not found" if context else "Not found"
|
||||||
|
else:
|
||||||
|
base = _STATUS_MESSAGES.get(status) or e.response.reason or f"HTTP {status}"
|
||||||
|
message = f"{context}: {base}" if context else base
|
||||||
|
return cls(message, status)
|
||||||
@@ -8,7 +8,7 @@ class Base(BaseModel):
|
|||||||
|
|
||||||
class System(BaseModel):
|
class System(BaseModel):
|
||||||
"""
|
"""
|
||||||
Requred
|
Required
|
||||||
"""
|
"""
|
||||||
|
|
||||||
model: str
|
model: str
|
||||||
@@ -18,7 +18,7 @@ class System(BaseModel):
|
|||||||
|
|
||||||
class Interfaces(Base):
|
class Interfaces(Base):
|
||||||
"""
|
"""
|
||||||
Requred
|
Required
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name: str = Field(alias="interface")
|
name: str = Field(alias="interface")
|
||||||
@@ -32,7 +32,7 @@ class Vlans(Base):
|
|||||||
Optional
|
Optional
|
||||||
"""
|
"""
|
||||||
|
|
||||||
vlan_id: int = Field(alias="id")
|
vlan_id: int
|
||||||
name: str | None = Field(default=None, alias="description")
|
name: str | None = Field(default=None, alias="description")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,10 @@ from oxi.interfaces.base import BaseDevice
|
|||||||
class Qtech(BaseDevice):
|
class Qtech(BaseDevice):
|
||||||
template = "qtech.ttp"
|
template = "qtech.ttp"
|
||||||
|
|
||||||
|
def system(self) -> dict:
|
||||||
|
system = self.raw["system"]
|
||||||
|
return system
|
||||||
|
|
||||||
def vlans(self) -> list[dict]:
|
def vlans(self) -> list[dict]:
|
||||||
vlans_ttp = self.raw["vlans"]
|
vlans_ttp = self.raw["vlans"]
|
||||||
vlans = []
|
vlans = []
|
||||||
|
|||||||
36
oxi/interfaces/models/quasar.py
Normal file
36
oxi/interfaces/models/quasar.py
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
from oxi.interfaces import BaseDevice, register_parser
|
||||||
|
|
||||||
|
|
||||||
|
@register_parser(["quasar", "qos"])
|
||||||
|
class Quasar(BaseDevice):
|
||||||
|
template = "quasar.ttp"
|
||||||
|
|
||||||
|
def interfaces(self) -> list[dict]:
|
||||||
|
ether_interfaces: dict = self.raw["interfaces"]
|
||||||
|
interfaces: list[dict] = []
|
||||||
|
bulk_interfaces: dict = self.raw["bulkinterfaces"]
|
||||||
|
for key, value in bulk_interfaces.items():
|
||||||
|
interfaces.append(
|
||||||
|
{
|
||||||
|
"interface": key,
|
||||||
|
"description": value.get("description"),
|
||||||
|
"ip_address": value.get("ip_address"),
|
||||||
|
"mask": value.get("mask"),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
interfaces.append(ether_interfaces)
|
||||||
|
return interfaces
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
with open("./test7.txt") as file:
|
||||||
|
data = file.read()
|
||||||
|
quasar = Quasar(data)
|
||||||
|
qt = quasar.parse()
|
||||||
|
print(qt)
|
||||||
|
print()
|
||||||
|
with open("./test8.txt") as file:
|
||||||
|
data = file.read()
|
||||||
|
quasar = Quasar(data)
|
||||||
|
qt = quasar.parse()
|
||||||
|
print(qt)
|
||||||
@@ -9,18 +9,23 @@ default_system = {
|
|||||||
</vars>
|
</vars>
|
||||||
|
|
||||||
<group name="system" default="default_system">
|
<group name="system" default="default_system">
|
||||||
! {{ model | ORPHRASE }} Series Software, Version {{ ignore }} Build {{ version | strip(",") }}{{ ignore('.*') }}
|
! {{ model | ORPHRASE | _start_ }} Series Software, Version {{ ignore }} Build {{ version | strip(",") }}{{ ignore('.*') }}
|
||||||
! Serial num:{{ serial_number | strip(",") }}{{ ignore('.*') }}
|
! Serial num:{{ serial_number | strip(",") }}{{ ignore('.*') }}
|
||||||
|
|
||||||
|
! System description : {{ description | PHRASE | _start_ }}({{ model }}) By {{ vendor }}
|
||||||
|
! System description : {{ description | PHRASE | _start_ }}({{ model }})
|
||||||
|
! System software version : {{ description | PHRASE }}, Release({{ version }})
|
||||||
|
! System serial number : {{ serial_number }}
|
||||||
</group>
|
</group>
|
||||||
|
|
||||||
<group name="interfaces">
|
<group name="interfaces">
|
||||||
interface {{ interface }}
|
interface {{ interface | ORPHRASE }}
|
||||||
description {{ description | ORPHRASE }}
|
description {{ description | ORPHRASE }}
|
||||||
ip address {{ ip_address }} {{ mask | to_cidr }}
|
ip address {{ ip_address }} {{ mask | to_cidr }}
|
||||||
</group>
|
</group>
|
||||||
|
|
||||||
<group name="vlans">
|
<group name="vlans">
|
||||||
vlan {{ vlan_ids | contains(",", "-") | unrange("-", ",") | _start_ }}
|
vlan {{ vlan_ids | contains(",", "-") | unrange("-", ",") }}
|
||||||
,{{ vlan_tail | unrange("-", ",") }}
|
,{{ vlan_tail | unrange("-", ",") }}
|
||||||
vlan {{ vlan_id | _start_ }}
|
vlan {{ vlan_id | _start_ }}
|
||||||
name {{ name | ORPHRASE }}
|
name {{ name | ORPHRASE }}
|
||||||
|
|||||||
66
oxi/interfaces/models/templates/quasar.ttp
Normal file
66
oxi/interfaces/models/templates/quasar.ttp
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
<doc>
|
||||||
|
Базовый шаблон для нового устройства. Скопируйте этот файл, переименуйте
|
||||||
|
в <vendor>.ttp и заполните группы под формат конфигурации вашего устройства.
|
||||||
|
|
||||||
|
Обязательные группы: system, interfaces.
|
||||||
|
Опциональная группа: vlans — добавляйте только если устройство поддерживает VLAN.
|
||||||
|
|
||||||
|
--- Группа system ---
|
||||||
|
Должна возвращать одиночный словарь с полями:
|
||||||
|
model (str) — модель устройства
|
||||||
|
serial_number (str) — серийный номер
|
||||||
|
version (str) — версия прошивки
|
||||||
|
|
||||||
|
--- Группа interfaces ---
|
||||||
|
Должна возвращать список словарей. Каждый элемент:
|
||||||
|
interface (str) — имя интерфейса (alias поля name)
|
||||||
|
ip_address (str|None) — IPv4-адрес
|
||||||
|
mask (int|None) — длина префикса (напр. 24)
|
||||||
|
description (str|None) — описание интерфейса
|
||||||
|
|
||||||
|
Если устройство возвращает маску в виде 255.255.255.0, конвертируйте
|
||||||
|
её в prefix length в методе interfaces() класса устройства.
|
||||||
|
|
||||||
|
--- Группа vlans ---
|
||||||
|
Должна возвращать список словарей. Каждый элемент:
|
||||||
|
id (int) — номер VLAN (alias поля vlan_id)
|
||||||
|
description (str|None) — название VLAN (alias поля name)
|
||||||
|
|
||||||
|
--- Полезные модификаторы TTP ---
|
||||||
|
{{ field | ORPHRASE }} — одно слово или фраза до конца строки
|
||||||
|
{{ field | _start_ }} — начало новой записи группы
|
||||||
|
{{ field | strip('"') }} — убрать кавычки
|
||||||
|
{{ field | replace("yes","True") }} — замена подстроки
|
||||||
|
{{ field | exclude("pattern") }} — пропустить строку при совпадении
|
||||||
|
{{ ignore }} — захватить и выбросить значение
|
||||||
|
{{ ignore('.*') }} — выбросить всё до конца строки
|
||||||
|
|
||||||
|
Подробнее: docs/templates.md
|
||||||
|
</doc>
|
||||||
|
<vars>
|
||||||
|
default_system = {
|
||||||
|
"model": "",
|
||||||
|
"serial_number": "",
|
||||||
|
"version": ""
|
||||||
|
}
|
||||||
|
</vars>
|
||||||
|
|
||||||
|
<group name="system" default="default_system">
|
||||||
|
# Component Version {{ _start_ }}
|
||||||
|
# Assembly {{ version }}
|
||||||
|
# Product Name {{ model | ORPHRASE }}
|
||||||
|
# Product Serial Number {{ serial_number }}
|
||||||
|
|
||||||
|
# Subsystem Version {{ _start_ }}
|
||||||
|
# Engine {{ version }}
|
||||||
|
# Product Name {{ model | ORPHRASE }}
|
||||||
|
# Product Serial Number {{ serial_number }}
|
||||||
|
</group>
|
||||||
|
|
||||||
|
<group name="interfaces">
|
||||||
|
{{ ignore("\\s*") }}config {{ interface }} ipv4 address {{ ip_address }}/{{ mask }} gateway {{ gateway }} {{ ignore }}
|
||||||
|
</group>
|
||||||
|
|
||||||
|
<group name="bulkinterfaces.{{ interface }}">
|
||||||
|
{{ ignore("\\s*") }}config interface {{ interface | _start_ }} description {{ description | ORPHRASE | strip('"')}}
|
||||||
|
</group>
|
||||||
11
oxi/node.py
11
oxi/node.py
@@ -1,5 +1,9 @@
|
|||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from requests import HTTPError
|
||||||
|
|
||||||
|
from oxi.exception import OxiAPIError
|
||||||
|
|
||||||
from .view import NodeView
|
from .view import NodeView
|
||||||
|
|
||||||
|
|
||||||
@@ -11,15 +15,16 @@ class Node:
|
|||||||
def __init__(self, session: "Session", base_url: str):
|
def __init__(self, session: "Session", base_url: str):
|
||||||
self._session = session
|
self._session = session
|
||||||
self._base_url = base_url
|
self._base_url = base_url
|
||||||
self._data = None
|
|
||||||
|
|
||||||
def __call__(self, name: str) -> NodeView:
|
def __call__(self, name: str) -> NodeView:
|
||||||
|
try:
|
||||||
url = f"{self._base_url}/node/show/{name}"
|
url = f"{self._base_url}/node/show/{name}"
|
||||||
if not url.endswith(".json"):
|
if not url.endswith(".json"):
|
||||||
url += ".json"
|
url += ".json"
|
||||||
response = self._session.get(url)
|
response = self._session.get(url)
|
||||||
if response.status_code == 500:
|
response.raise_for_status()
|
||||||
raise ValueError(f"page {url} not found")
|
except HTTPError as e:
|
||||||
|
raise OxiAPIError.from_http_error(e, context=f"Node {name}") from e
|
||||||
return NodeView(
|
return NodeView(
|
||||||
session=self._session, base_url=self._base_url, data=response.json()
|
session=self._session, base_url=self._base_url, data=response.json()
|
||||||
)
|
)
|
||||||
|
|||||||
35
oxi/view.py
35
oxi/view.py
@@ -14,22 +14,47 @@ class NodeView:
|
|||||||
self._base_url = base_url
|
self._base_url = base_url
|
||||||
self._data = data
|
self._data = data
|
||||||
|
|
||||||
|
def _updater(self) -> int:
|
||||||
|
response = self._session.get(f"{self._base_url}/node/next/{self.full_name}")
|
||||||
|
response.raise_for_status()
|
||||||
|
return response.status_code
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def ip(self):
|
def name(self) -> str:
|
||||||
|
return self._data.get("name")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ip(self) -> str:
|
||||||
return self._data.get("ip")
|
return self._data.get("ip")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def full_name(self):
|
def full_name(self) -> str:
|
||||||
return self._data.get("full_name")
|
return self._data.get("full_name")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def group(self):
|
def group(self) -> str:
|
||||||
return self._data.get("group")
|
return self._data.get("group")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def model(self):
|
def model(self) -> str:
|
||||||
return self._data.get("model")
|
return self._data.get("model")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def last_status(self) -> str:
|
||||||
|
last = self._data.get("last") or {}
|
||||||
|
return last.get("status")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def last_check(self) -> str:
|
||||||
|
last = self._data.get("last") or {}
|
||||||
|
return last.get("start")
|
||||||
|
|
||||||
|
def refresh(self) -> str:
|
||||||
|
result = self._updater()
|
||||||
|
if result != 200:
|
||||||
|
raise ValueError(f"Failed to refresh node {self.full_name}")
|
||||||
|
return "OK"
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def config(self):
|
def config(self) -> NodeConfig:
|
||||||
return NodeConfig(self._session, self.full_name, self.model, self._base_url)
|
return NodeConfig(self._session, self.full_name, self.model, self._base_url)
|
||||||
|
|||||||
@@ -5,14 +5,27 @@ build-backend = "setuptools.build_meta"
|
|||||||
[project]
|
[project]
|
||||||
name = "oxipy"
|
name = "oxipy"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
description = "Oxi API client"
|
description = "Oxidized API client"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
license = { file = "LICENSE" }
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
|
classifiers = [
|
||||||
|
"Programming Language :: Python :: 3",
|
||||||
|
"License :: OSI Approved :: Apache Software License",
|
||||||
|
"Operating System :: OS Independent",
|
||||||
|
]
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"pydantic>=2.12.5",
|
"pydantic>=2.12.5",
|
||||||
"requests>=2.32.5",
|
"requests>=2.32.5",
|
||||||
"ttp>=0.10.0",
|
"ttp>=0.10.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.setuptools.packages.find]
|
[tool.setuptools.packages.find]
|
||||||
where = ["."]
|
where = ["."]
|
||||||
include = ["oxi*"]
|
include = ["oxi*"]
|
||||||
|
|
||||||
|
[tool.setuptools]
|
||||||
|
include-package-data = true
|
||||||
|
|
||||||
|
[tool.setuptools.package-data]
|
||||||
|
"oxi" = ["**/*.ttp"]
|
||||||
Reference in New Issue
Block a user