From 57a4e56eb3cc8146be5926cb18990e8005f6ed4d Mon Sep 17 00:00:00 2001 From: Lisa Bekdache Date: Sat, 5 Feb 2022 16:42:09 +0000 Subject: [PATCH] Add documentation --- netbox_ipmi_ovh/__init__.py | 2 +- netbox_ipmi_ovh/exceptions.py | 11 +++++++++++ netbox_ipmi_ovh/ipmi.py | 33 ++++++++++++++++++++++++++++++--- netbox_ipmi_ovh/navigation.py | 1 + netbox_ipmi_ovh/views.py | 24 ++++++++++++++++++++++-- 5 files changed, 65 insertions(+), 6 deletions(-) diff --git a/netbox_ipmi_ovh/__init__.py b/netbox_ipmi_ovh/__init__.py index 94f1f64..3737527 100644 --- a/netbox_ipmi_ovh/__init__.py +++ b/netbox_ipmi_ovh/__init__.py @@ -3,7 +3,7 @@ class NetboxIpmiOvhConfig(PluginConfig): name = 'netbox_ipmi_ovh' verbose_name = 'Netbox ipmi ovh' - description = '' + description = 'A plugin used to add a button in the dcim.device to allow easier access to the IPMI for OVH managed bare metal servers.' version = '0.0.1' author = 'Lisa Bekdache' author_email = 'lisa.bekdache@gmail.com' diff --git a/netbox_ipmi_ovh/exceptions.py b/netbox_ipmi_ovh/exceptions.py index 6f7e1f0..8c785d6 100644 --- a/netbox_ipmi_ovh/exceptions.py +++ b/netbox_ipmi_ovh/exceptions.py @@ -1,10 +1,21 @@ class NetboxIpmiOvh(Exception): + """ + Base exception for the netbox-ipmi-ovh plugin + """ pass class NetboxIpmiOvhTimeout(NetboxIpmiOvh): + """ + Exception when OVH is too slow to answer + i.e BMC is down on the server + """ pass class NetboxIpmiOvhError(NetboxIpmiOvh): + """ + Exception for wrong arguments or error thrown + by the OVH API + """ pass diff --git a/netbox_ipmi_ovh/ipmi.py b/netbox_ipmi_ovh/ipmi.py index 3a43240..439686d 100644 --- a/netbox_ipmi_ovh/ipmi.py +++ b/netbox_ipmi_ovh/ipmi.py @@ -2,9 +2,20 @@ from netbox_ipmi_ovh.exceptions import NetboxIpmiOvhTimeout, NetboxIpmiOvhError def wait_for_ovh_task(client, service_name, task_id): - # ovh timeout is between 120 seconds and 20minutes in worst case (trust me i tried) - # todo: maybe do an async task later? - for _ in range(120): + """ + Wait for OVH task to finish + :param client: Instance of ovh.Client() + :param service_name: Name of the dedicated server (by ovh name: nsXXXXXX.ip-XX-YY-ZZ.tld + :param task_id: ID of the task from /dedicated/server/{service_name}/features/ipmi/access + + :raises NetboxIpmiOvhError: Raised when status is changed to ovhError/customerError and ipmi + cannot be given + :raises NetboxIpmiOvhTimeout: Raised when requests take more than 120seconds, should happend + rarely / never except if your BMC is dying + + :return: Nothing, if it returns then the command is successful + """ + for _ in range(120): # ovh timeout is between 120 seconds and 20minutes in worst case (trust me i tried) result = client.get(f"/dedicated/server/{service_name}/task/{task_id}") if result["status"] == "done": return @@ -16,9 +27,23 @@ def wait_for_ovh_task(client, service_name, task_id): def request_ipmi_access(client, service_name, access_type, ssh_key=None, ip_to_allow=None): + """ + Request an IPMI access by calling the OVH API + :param client: Instance of ovh.Client() + :param service_name: Name of the dedicated server (by ovh name: nsXXXXXX.ip-XX-YY-ZZ.tld + :param access_type: IPMI console access type + :param ssh_key: SSH key name to allow access on KVM/IP interface with (name from /me/sshKey) + :param ip_to_allow: IP to allow connection from for this IPMI session + + :return: URL to kvmIP, ssh command line in string or JNLP xml string depending on the access type + :rtype: str + """ # I could also do the check of the features in the template_content # but i do not want to slow down the loading of the page, as we don't # know if the OVH api is up or down or very slow + + + # Retreive available features for the server (sometime somes are not enabled or even IPMI is not enabled) result = client.get( f'/dedicated/server/{service_name}/features/ipmi' ) @@ -34,6 +59,7 @@ def request_ipmi_access(client, service_name, access_type, ssh_key=None, ip_to_a f"Supported access types: {', '.join(supported_features)} " ) + # https://api.ovh.com/console/#/dedicated/server/{serviceName}/features/ipmi/access#POST parameters = { "type": access_type, "ttl": 15, @@ -56,6 +82,7 @@ def request_ipmi_access(client, service_name, access_type, ssh_key=None, ip_to_a wait_for_ovh_task(client, service_name, result['taskId']) + # finally retreive the IPMI access result = client.get( f'/dedicated/server/{service_name}/features/ipmi/access', type=access_type diff --git a/netbox_ipmi_ovh/navigation.py b/netbox_ipmi_ovh/navigation.py index dae6c4b..73b0f56 100644 --- a/netbox_ipmi_ovh/navigation.py +++ b/netbox_ipmi_ovh/navigation.py @@ -1,5 +1,6 @@ from extras.plugins import PluginMenuItem +# Add the IPMI Config link to the menu Plugins menu_items = ( PluginMenuItem( link_text="IPMI OVH", link="plugins:netbox_ipmi_ovh:ipmi_config", diff --git a/netbox_ipmi_ovh/views.py b/netbox_ipmi_ovh/views.py index 7d43d42..b1d526d 100644 --- a/netbox_ipmi_ovh/views.py +++ b/netbox_ipmi_ovh/views.py @@ -18,10 +18,13 @@ NETBOX_CURRENT_VERSION = version.parse(settings.VERSION) PLUGIN_SETTINGS = settings.PLUGINS_CONFIG.get("netbox_ipmi_ovh", {}) + +# Required configuration OVH_ENDPOINTS = PLUGIN_SETTINGS["endpoints"] OVH_ENDPOINT_FIELD = PLUGIN_SETTINGS["ovh_endpoint_field"] OVH_SERVER_NAME_FIELD = PLUGIN_SETTINGS["ovh_server_name_field"] +# Allowed access type for an IPMI access MAPPING_ACCESS_TYPE = { "kvmipHtml5URL": lambda access: redirect(access), "kvmipJnlp": lambda access: HttpResponse(access, content_type='application/x-java-jnlp-file'), @@ -34,6 +37,9 @@ class BaseIpmiView(PermissionRequiredMixin, View): @staticmethod def _get_user_config(user): + """ + Retrieve user-specific settings + """ try: usercfg = UserIpmiCfg.objects.get(user=user) except UserIpmiCfg.DoesNotExist: @@ -48,6 +54,11 @@ class UserIpmiCfgView(BaseIpmiView): template_name = 'netbox_ipmi_ovh/ipmi_config.html' def post(self, request): + """ + Update the user configuration: + - ssh_key_name: SSH key name to allow access on KVM/IP interface with (name from /me/sshKey) + - allowed_ip: IP to allow connection from for this IPMI session + """ usercfg = self._get_user_config(request.user) form = UserIpmiCfgForm(data=request.POST, instance=usercfg) @@ -80,19 +91,28 @@ class IpmiView(BaseIpmiView): template_error = "netbox_ipmi_ovh/ipmi_error.html" def get(self, request): + """ + Query the OVH API to retrieive an IPMI access for a device + Works only on bare-metal dedicated cloud servers + """ device_id = request.GET.get("device") access_type = request.GET.get("type") usercfg = self._get_user_config(request.user) device = Device.objects.get(id=device_id) ovh_server_name = getattr(device, OVH_SERVER_NAME_FIELD) + if not ovh_server_name: + return render(request, self.template_error, { + "error_message": f"Server name not found, please fill the field {OVH_SERVER_NAME_FIELD}" + }) + if hasattr(device, OVH_ENDPOINT_FIELD): ovh_endpoint = getattr(device, OVH_ENDPOINT_FIELD) elif OVH_ENDPOINT_FIELD in device.custom_field_data: ovh_endpoint = device.custom_field_data[OVH_ENDPOINT_FIELD] else: return render(request, self.template_error, { - "error_message": "No OVH endpoint has been detected, cannot process request" + "error_message": f"No OVH endpoint has been detected, cannot process request, please fill the field {OVH_ENDPOINT_FIELD}" }) if ovh_endpoint not in OVH_ENDPOINTS: @@ -102,7 +122,7 @@ def get(self, request): client = Client(**OVH_ENDPOINTS[ovh_endpoint]) - # trying to retrieive user IP to send to ovh + # trying to retreive user IP to send to ovh # if no ip is send in args to ovh api, they will use the ip used for # the http call to their api, and it will be the server ip