Mise à jour de Monitor.py et autres scripts

This commit is contained in:
Debian
2025-07-23 10:46:27 +02:00
parent 7081418ce0
commit 7de3e0fb50
8604 changed files with 2789953 additions and 295 deletions

View File

@@ -0,0 +1,45 @@
# Copyright (c) 2013-2024, OVH SAS.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of OVH SAS nor the
# names of its contributors may be used to endorse or promote products
# derived from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY OVH SAS AND CONTRIBUTORS ``AS IS'' AND ANY
# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL OVH SAS AND CONTRIBUTORS BE LIABLE FOR ANY
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
# flake8: noqa
from .client import Client
from .consumer_key import API_READ_ONLY, API_READ_WRITE, API_READ_WRITE_SAFE, ConsumerKeyRequest
from .exceptions import (
APIError,
BadParametersError,
Forbidden,
HTTPError,
InvalidCredential,
InvalidKey,
InvalidRegion,
InvalidResponse,
NetworkError,
NotCredential,
NotGrantedCall,
ReadOnlyError,
ResourceConflictError,
ResourceNotFoundError,
)

View File

@@ -0,0 +1,618 @@
# Copyright (c) 2013-2024, OVH SAS.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of OVH SAS nor the
# names of its contributors may be used to endorse or promote products
# derived from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY OVH SAS AND CONTRIBUTORS ````AS IS'' AND ANY
# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL OVH SAS AND CONTRIBUTORS BE LIABLE FOR ANY
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""
This module provides a simple python wrapper over the OVH REST API.
It handles requesting credential, signing queries...
- To get your API keys: https://eu.api.ovh.com/createApp/
- To get started with API:
https://help.ovhcloud.com/csm/en-gb-api-getting-started-ovhcloud-api?id=kb_article_view&sysparm_article=KB0042784
"""
import hashlib
import json
import keyword
import time
from urllib.parse import urlencode
from requests import Session
from requests.exceptions import RequestException
from . import config
from .consumer_key import ConsumerKeyRequest
from .exceptions import (
APIError,
BadParametersError,
Forbidden,
HTTPError,
InvalidConfiguration,
InvalidCredential,
InvalidKey,
InvalidRegion,
InvalidResponse,
NetworkError,
NotCredential,
NotGrantedCall,
ResourceConflictError,
ResourceExpiredError,
ResourceNotFoundError,
)
from .oauth2 import OAuth2
# Mapping between OVH API region names and corresponding endpoints
ENDPOINTS = {
"ovh-eu": "https://eu.api.ovh.com/1.0",
"ovh-us": "https://api.us.ovhcloud.com/1.0",
"ovh-ca": "https://ca.api.ovh.com/1.0",
"kimsufi-eu": "https://eu.api.kimsufi.com/1.0",
"kimsufi-ca": "https://ca.api.kimsufi.com/1.0",
"soyoustart-eu": "https://eu.api.soyoustart.com/1.0",
"soyoustart-ca": "https://ca.api.soyoustart.com/1.0",
}
# Default timeout for each request. 180 seconds connect, 180 seconds read.
TIMEOUT = 180
# OAuth2 token provider URLs
OAUTH2_TOKEN_URLS = {
"ovh-eu": "https://www.ovh.com/auth/oauth2/token",
"ovh-ca": "https://ca.ovh.com/auth/oauth2/token",
"ovh-us": "https://us.ovhcloud.com/auth/oauth2/token",
}
class Client:
"""
Low level OVH Client. It abstracts all the authentication and request
signing logic along with some nice tools helping with key generation.
All low level request logic including signing and error handling takes place
in :py:func:`Client.call` function. Convenient wrappers
:py:func:`Client.get` :py:func:`Client.post`, :py:func:`Client.put`,
:py:func:`Client.delete` should be used instead. :py:func:`Client.post`,
:py:func:`Client.put` both accept arbitrary list of keyword arguments
mapped to ``data`` param of :py:func:`Client.call`.
Example usage:
.. code:: python
from ovh import Client, APIError
REGION = 'ovh-eu'
APP_KEY="<application key>"
APP_SECRET="<application secret key>"
CONSUMER_KEY="<consumer key>"
client = Client(REGION, APP_KEY, APP_SECRET, CONSUMER_KEY)
try:
print(client.get('/me'))
except APIError as e:
print("Ooops, failed to get my info:", e.msg)
"""
def __init__(
self,
endpoint=None,
application_key=None,
application_secret=None,
consumer_key=None,
timeout=TIMEOUT,
config_file=None,
client_id=None,
client_secret=None,
):
"""
Creates a new Client. No credential check is done at this point.
When using OAuth2 authentication, ``client_id`` and ``client_secret``
will be used to initiate a Client Credential OAuth2 flow.
When using the OVHcloud authentication method, the ``application_key``
identifies your application while ``application_secret`` authenticates
it. On the other hand, the ``consumer_key`` uniquely identifies your
application's end user without requiring his personal password.
If any of ``endpoint``, ``application_key``, ``application_secret``,
``consumer_key``, ``client_id`` or ``client_secret`` is not provided,
this client will attempt to locate from them from environment,
``~/.ovh.cfg`` or ``/etc/ovh.cfg``.
See :py:mod:`ovh.config` for more information on supported
configuration mechanisms.
``timeout`` can either be a float or a tuple. If it is a float it
sets the same timeout for both connection and read. If it is a tuple
connection and read timeout will be set independently. To use the
latter approach you need at least requests v2.4.0. Default value is
180 seconds for connection and 180 seconds for read.
:param str endpoint: API endpoint to use. Valid values in ``ENDPOINTS``
:param str application_key: Application key as provided by OVHcloud
:param str application_secret: Application secret key as provided by OVHcloud
:param str consumer_key: uniquely identifies
:param str client_id: OAuth2 client ID
:param str client_secret: OAuth2 client secret
:param tuple timeout: Connection and read timeout for each request
:param float timeout: Same timeout for both connection and read
:raises InvalidRegion: if ``endpoint`` can't be found in ``ENDPOINTS``.
"""
configuration = config.ConfigurationManager()
# Load a custom config file if requested
if config_file is not None:
configuration.read(config_file)
# load endpoint
if endpoint is None:
endpoint = configuration.get("default", "endpoint")
try:
self._endpoint = ENDPOINTS[endpoint]
except KeyError:
raise InvalidRegion("Unknown endpoint %s. Valid endpoints: %s", endpoint, ENDPOINTS.keys())
# load keys
if application_key is None:
application_key = configuration.get(endpoint, "application_key")
self._application_key = application_key
if application_secret is None:
application_secret = configuration.get(endpoint, "application_secret")
self._application_secret = application_secret
if consumer_key is None:
consumer_key = configuration.get(endpoint, "consumer_key")
self._consumer_key = consumer_key
# load OAuth2 data
if client_id is None:
client_id = configuration.get(endpoint, "client_id")
self._client_id = client_id
if client_secret is None:
client_secret = configuration.get(endpoint, "client_secret")
self._client_secret = client_secret
# configuration validation
if bool(self._client_id) is not bool(self._client_secret):
raise InvalidConfiguration("Invalid OAuth2 config, both client_id and client_secret must be given")
if bool(self._application_key) is not bool(self._application_secret):
raise InvalidConfiguration(
"Invalid authentication config, both application_key and application_secret must be given"
)
if self._client_id is not None and self._application_key is not None:
raise InvalidConfiguration(
"Can't use both application_key/application_secret and OAuth2 client_id/client_secret"
)
if self._client_id is None and self._application_key is None:
raise InvalidConfiguration(
"Missing authentication information, you need to provide at least an application_key/application_secret"
" or a client_id/client_secret"
)
if self._client_id and endpoint not in OAUTH2_TOKEN_URLS:
raise InvalidConfiguration(
"OAuth2 authentication is not compatible with endpoint "
+ endpoint
+ " (it can only be used with ovh-eu, ovh-ca and ovh-us)"
)
# when in OAuth2 mode, instantiate the oauthlib client
if self._client_id:
self._oauth2 = OAuth2(
client_id=self._client_id,
client_secret=self._client_secret,
token_url=OAUTH2_TOKEN_URLS[endpoint],
)
else:
self._oauth2 = None
# lazy load time delta
self._time_delta = None
# use a requests session to reuse HTTPS connections between requests
self._session = Session()
# Override default timeout
self._timeout = timeout
# high level API
@property
def time_delta(self):
"""
Request signatures are valid only for a short amount of time to mitigate
risk of attack replay scenarii which requires to use a common time
reference. This function queries endpoint's time and computes the delta.
This entrypoint does not require authentication.
This method is *lazy*. It will only load it once even though it is used
for each request.
.. note:: You should not need to use this property directly
:returns: time distance between local and server time in seconds.
:rtype: int
"""
if self._time_delta is None:
server_time = self.get("/auth/time", _need_auth=False)
self._time_delta = server_time - int(time.time())
return self._time_delta
def new_consumer_key_request(self):
"""
Create a new consumer key request. This is the recommended way to create
a new consumer key request.
Full example:
>>> import ovh
>>> client = ovh.Client("ovh-eu")
>>> ck = client.new_consumer_key_request()
>>> ck.add_rules(ovh.API_READ_ONLY, "/me")
>>> ck.add_recursive_rules(ovh.API_READ_WRITE, "/sms")
>>> ck.request()
{
'state': 'pendingValidation',
'consumerKey': 'TnpZAd5pYNqxk4RhlPiSRfJ4WrkmII2i',
'validationUrl': 'https://eu.api.ovh.com/auth/?credentialToken=now2OOAVO4Wp6t7bemyN9DMWIobhGjFNZSHmixtVJM4S7mzjkN2L5VBfG96Iy1i0'
}
""" # noqa:E501
return ConsumerKeyRequest(self)
def request_consumerkey(self, access_rules, redirect_url=None, allowedIPs=None):
"""
Create a new "consumer key" identifying this application's end user. API
will return a ``consumerKey`` and a ``validationUrl``. The end user must
visit the ``validationUrl``, authenticate and validate the requested
``access_rules`` to link his account to the ``consumerKey``. Once this
is done, he may optionally be redirected to ``redirect_url`` and the
application can start using the ``consumerKey``. If adding an ``allowedIPs``
parameter, the generated credentials will only be usable from these IPs.
The new ``consumerKey`` is automatically loaded into
``self._consumer_key`` and is ready to used as soon as validated.
As signing requires a valid ``consumerKey``, the method does not require
authentication, only a valid ``applicationKey``
``access_rules`` is a list of the form:
.. code:: python
# Grant full, unrestricted API access
access_rules = [
{'method': 'GET', 'path': '/*'},
{'method': 'POST', 'path': '/*'},
{'method': 'PUT', 'path': '/*'},
{'method': 'DELETE', 'path': '/*'}
]
To request a new consumer key, you may use a code like:
.. code:: python
try:
input = raw_input
except NameError:
pass
# Request RO, /me API access
access_rules = [
{'method': 'GET', 'path': '/me'},
]
# Request token
validation = client.request_consumerkey(access_rules, redirect_url="https://optional-redirect-url.example.org", allowedIPs=["127.0.0.1/32"])
print("Please visit", validation['validationUrl'], "to authenticate")
input("and press Enter to continue...")
# Print nice welcome message
print("Welcome", client.get('/me')['firstname'])
:param list access_rules: Mapping specifying requested privileges.
:param str redirect_url: Where to redirect end user upon validation (optional).
:param list allowedIPs: CIDRs that will be allowed to use these credentials (optional).
:raises APIError: When ``self.call`` fails.
:returns: dict with ``consumerKey`` and ``validationUrl`` keys
:rtype: dict
""" # noqa:E501
res = self.post(
"/auth/credential",
_need_auth=False,
accessRules=access_rules,
redirection=redirect_url,
allowedIPs=allowedIPs,
)
self._consumer_key = res["consumerKey"]
return res
# API shortcuts
def _canonicalize_kwargs(self, kwargs):
"""
If an API needs an argument colliding with a Python reserved keyword, it
can be prefixed with an underscore. For example, ``from`` argument of
``POST /email/domain/{domain}/redirection`` may be replaced by ``_from``
:param dict kwargs: input kwargs
:return dict: filtered kawrgs
"""
arguments = {}
for k, v in kwargs.items():
if k[0] == "_" and k[1:] in keyword.kwlist:
k = k[1:]
arguments[k] = v
return arguments
def _prepare_query_string(self, kwargs):
"""
Boolean needs to be send as lowercase 'false' or 'true' in querystring.
This function prepares arguments for querystring and encodes them.
:param dict kwargs: input kwargs
:return string: prepared querystring
"""
arguments = {}
for k, v in kwargs.items():
if isinstance(v, bool):
v = str(v).lower()
elif v is None:
v = "null"
arguments[k] = v
return urlencode(arguments)
def get(self, _target, _need_auth=True, **kwargs):
"""
'GET' :py:func:`Client.call` wrapper.
Query string parameters can be set either directly in ``_target`` or as
keyword arguments. If an argument collides with a Python reserved
keyword, prefix it with a '_'. For instance, ``from`` becomes ``_from``.
:param string _target: API method to call
:param string _need_auth: If True, send authentication headers. This is
the default
"""
if kwargs:
kwargs = self._canonicalize_kwargs(kwargs)
query_string = self._prepare_query_string(kwargs)
if query_string != "":
if "?" in _target:
_target = "%s&%s" % (_target, query_string)
else:
_target = "%s?%s" % (_target, query_string)
return self.call("GET", _target, None, _need_auth)
def put(self, _target, _need_auth=True, **kwargs):
"""
'PUT' :py:func:`Client.call` wrapper
Body parameters can be set either directly in ``_target`` or as keyword
arguments. If an argument collides with a Python reserved keyword,
prefix it with a '_'. For instance, ``from`` becomes ``_from``.
:param string _target: API method to call
:param string _need_auth: If True, send authentication headers. This is
the default
"""
kwargs = self._canonicalize_kwargs(kwargs)
if not kwargs:
kwargs = None
return self.call("PUT", _target, kwargs, _need_auth)
def post(self, _target, _need_auth=True, **kwargs):
"""
'POST' :py:func:`Client.call` wrapper
Body parameters can be set either directly in ``_target`` or as keyword
arguments. If an argument collides with a Python reserved keyword,
prefix it with a '_'. For instance, ``from`` becomes ``_from``.
:param string _target: API method to call
:param string _need_auth: If True, send authentication headers. This is
the default
"""
kwargs = self._canonicalize_kwargs(kwargs)
if not kwargs:
kwargs = None
return self.call("POST", _target, kwargs, _need_auth)
def delete(self, _target, _need_auth=True, **kwargs):
"""
'DELETE' :py:func:`Client.call` wrapper
Query string parameters can be set either directly in ``_target`` or as
keyword arguments. If an argument collides with a Python reserved
keyword, prefix it with a '_'. For instance, ``from`` becomes ``_from``.
:param string _target: API method to call
:param string _need_auth: If True, send authentication headers. This is
the default
"""
if kwargs:
kwargs = self._canonicalize_kwargs(kwargs)
query_string = self._prepare_query_string(kwargs)
if query_string != "":
if "?" in _target:
_target = "%s&%s" % (_target, query_string)
else:
_target = "%s?%s" % (_target, query_string)
return self.call("DELETE", _target, None, _need_auth)
# low level helpers
def call(self, method, path, data=None, need_auth=True):
"""
Low level call helper. If ``consumer_key`` is not ``None``, inject
authentication headers and sign the request.
Request signature is a sha1 hash on following fields, joined by '+'
- application_secret
- consumer_key
- METHOD
- full request url
- body
- server current time (takes time delta into account)
:param str method: HTTP verb. Usually one of GET, POST, PUT, DELETE
:param str path: api entrypoint to call, relative to endpoint base path
:param data: any json serializable data to send as request's body
:param boolean need_auth: if False, bypass signature
:raises HTTPError: when underlying request failed for network reason
:raises InvalidResponse: when API response could not be decoded
"""
# attempt request
try:
result = self.raw_call(method=method, path=path, data=data, need_auth=need_auth)
except RequestException as error:
raise HTTPError("Low HTTP request failed error", error)
status = result.status_code
# attempt to decode and return the response
try:
if status != 204:
json_result = result.json()
else:
json_result = None
except ValueError as error:
raise InvalidResponse("Failed to decode API response", error)
# error check
if status >= 100 and status < 300:
return json_result
elif status == 403 and json_result.get("errorCode") == "NOT_GRANTED_CALL":
raise NotGrantedCall(json_result.get("message"), response=result)
elif status == 403 and json_result.get("errorCode") == "NOT_CREDENTIAL":
raise NotCredential(json_result.get("message"), response=result)
elif status == 403 and json_result.get("errorCode") == "INVALID_KEY":
raise InvalidKey(json_result.get("message"), response=result)
elif status == 403 and json_result.get("errorCode") == "INVALID_CREDENTIAL":
raise InvalidCredential(json_result.get("message"), response=result)
elif status == 403 and json_result.get("errorCode") == "FORBIDDEN":
raise Forbidden(json_result.get("message"), response=result)
elif status == 404:
raise ResourceNotFoundError(json_result.get("message"), response=result)
elif status == 400:
raise BadParametersError(json_result.get("message"), response=result)
elif status == 409:
raise ResourceConflictError(json_result.get("message"), response=result)
elif status == 460:
raise ResourceExpiredError(json_result.get("message"), response=result)
elif status == 0:
raise NetworkError()
else:
raise APIError(json_result.get("message"), response=result)
def _get_target(self, path):
"""
_get_target returns the URL to target given an endpoint and a path.
If the path starts with `/v1` or `/v2`, then remove the trailing `/1.0` from the endpoint.
:param str path: path to use prefix from
:returns: target with one of /1.0 and /v1|2 path segment
:rtype: str
"""
endpoint = self._endpoint
if endpoint.endswith("/1.0") and path.startswith(("/v1", "/v2")):
endpoint = endpoint[:-4]
return endpoint + path
def raw_call(self, method, path, data=None, need_auth=True, headers=None):
"""
Lowest level call helper. If ``consumer_key`` is not ``None``, inject
authentication headers and sign the request.
Will return ``requests.Response`` object or let any
``requests`` exception pass through.
Request signature is a sha1 hash on following fields, joined by '+'
- application_secret
- consumer_key
- METHOD
- full request url
- body
- server current time (takes time delta into account)
:param str method: HTTP verb. Usually one of GET, POST, PUT, DELETE
:param str path: api entrypoint to call, relative to endpoint base path
:param data: any json serializable data to send as request's body
:param boolean need_auth: if False, bypass signature
:param dict headers: A dict containing the headers that should be sent to
the OVH API. ``raw_call`` will override the
OVH API authentication headers, as well as
the Content-Type header.
"""
body = ""
target = self._get_target(path)
if headers is None:
headers = {}
# include payload
if data is not None:
headers["Content-type"] = "application/json"
body = json.dumps(data, separators=(",", ":")) # Separators to prevent adding useless spaces
# sign request. Never sign 'time' or will recurse infinitely
if need_auth:
if self._oauth2:
return self._oauth2.session.request(method, target, headers=headers, data=body, timeout=self._timeout)
if not self._application_secret:
raise InvalidKey("Invalid ApplicationSecret '%s'" % self._application_secret)
if not self._consumer_key:
raise InvalidKey("Invalid ConsumerKey '%s'" % self._consumer_key)
now = str(int(time.time()) + self.time_delta)
signature = hashlib.sha1()
signature.update(
"+".join([self._application_secret, self._consumer_key, method.upper(), target, body, now]).encode(
"utf-8"
)
)
headers["X-Ovh-Consumer"] = self._consumer_key
headers["X-Ovh-Timestamp"] = now
headers["X-Ovh-Signature"] = "$1$" + signature.hexdigest()
headers["X-Ovh-Application"] = self._application_key
return self._session.request(method, target, headers=headers, data=body, timeout=self._timeout)

View File

@@ -0,0 +1,123 @@
# Copyright (c) 2013-2024, OVH SAS.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of OVH SAS nor the
# names of its contributors may be used to endorse or promote products
# derived from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY OVH SAS AND CONTRIBUTORS ````AS IS'' AND ANY
# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL OVH SAS AND CONTRIBUTORS BE LIABLE FOR ANY
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""
The straightforward way to use OVH's API keys is to embed them directly in the
application code. While this is very convenient, it lacks of elegance and
flexibility.
Alternatively it is suggested to use configuration files or environment
variables so that the same code may run seamlessly in multiple environments.
Production and development for instance.
This wrapper will first look for direct instantiation parameters then
``OVH_ENDPOINT``, ``OVH_APPLICATION_KEY``, ``OVH_APPLICATION_SECRET`` and
``OVH_CONSUMER_KEY`` environment variables. If either of these parameter is not
provided, it will look for a configuration file of the form:
.. code:: ini
[default]
; general configuration: default endpoint
endpoint=ovh-eu
[ovh-eu]
; configuration specific to 'ovh-eu' endpoint
application_key=my_app_key
application_secret=my_application_secret
consumer_key=my_consumer_key
client_id=my_client_id
client_secret=my_client_secret
The client will successively attempt to locate this configuration file in
1. Current working directory: ``./ovh.conf``
2. Current user's home directory ``~/.ovh.conf``
3. System wide configuration ``/etc/ovh.conf``
This lookup mechanism makes it easy to overload credentials for a specific
project or user.
"""
from configparser import NoOptionError, NoSectionError, RawConfigParser
import os
__all__ = ["config"]
#: Locations where to look for configuration file by *increasing* priority
CONFIG_PATH = [
"/etc/ovh.conf",
os.path.expanduser("~/.ovh.conf"),
os.path.realpath("./ovh.conf"),
]
class ConfigurationManager:
"""
Application wide configuration manager
"""
def __init__(self):
"""
Create a config parser and load config from environment.
"""
# create config parser
self.config = RawConfigParser()
self.config.read(CONFIG_PATH)
def get(self, section, name):
"""
Load parameter ``name`` from configuration, respecting priority order.
Most of the time, ``section`` will correspond to the current api
``endpoint``. ``default`` section only contains ``endpoint`` and general
configuration.
:param str section: configuration section or region name. Ignored when
looking in environment
:param str name: configuration parameter to lookup
"""
# 1/ try env
try:
return os.environ["OVH_" + name.upper()]
except KeyError:
pass
# 2/ try from specified section/endpoint
try:
return self.config.get(section, name)
except (NoSectionError, NoOptionError):
pass
# not found, sorry
return None
def read(self, config_file):
# Read an other config file
self.config.read(config_file)
#: System wide instance :py:class:`ConfigurationManager` instance
config = ConfigurationManager()

View File

@@ -0,0 +1,111 @@
# Copyright (c) 2013-2024, OVH SAS.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of OVH SAS nor the
# names of its contributors may be used to endorse or promote products
# derived from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY OVH SAS AND CONTRIBUTORS ````AS IS'' AND ANY
# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL OVH SAS AND CONTRIBUTORS BE LIABLE FOR ANY
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""
This module provides a consumer key creation helper. Consumer keys are linked
with permissions defining which endpoint they are allowed to call. Just like
a physical key can unlock some doors but not others.
OVH API consumer keys authorization is pattern based. This makes it extremely
powerful and flexible as it may apply on only a very specific subset of the API
but it's also trickier to get right on simple scenarios.
Hence this module
"""
# Common authorization patterns
API_READ_ONLY = ["GET"]
API_READ_WRITE = ["GET", "POST", "PUT", "DELETE"]
API_READ_WRITE_SAFE = ["GET", "POST", "PUT"]
class ConsumerKeyRequest(object):
"""
ConsumerKey request. The generated consumer key will be linked to the
client's ``application_key``. When performing the request, the
``consumer_key`` will automatically be registered in the client.
It is recommended to save the generated key as soon as it validated to avoid
requesting a new one on each API access.
"""
def __init__(self, client):
"""
Create a new consumer key helper on API ``client``. The keys will be
tied to the ``application_key`` defined in the client.
"""
self._client = client
self._access_rules = []
def request(self, redirect_url=None, allowedIPs=None):
"""
Create the consumer key with the configures autorizations. The user will
need to validate it before it can be used with the API
>>> ck.request()
{
'state': 'pendingValidation',
'consumerKey': 'TnpZAd5pYNqxk4RhlPiSRfJ4WrkmII2i',
'validationUrl': 'https://eu.api.ovh.com/auth/?credentialToken=now2OOAVO4Wp6t7bemyN9DMWIobhGjFNZSHmixtVJM4S7mzjkN2L5VBfG96Iy1i0'
}
""" # noqa: E501
return self._client.request_consumerkey(self._access_rules, redirect_url, allowedIPs)
def add_rule(self, method, path):
"""
Add a new rule to the request. Will grant the ``(method, path)`` tuple.
Path can be any API route pattern like ``/sms/*`` or ``/me``. For example,
to grant RO access on personal data:
>>> ck.add_rule("GET", "/me")
"""
self._access_rules.append({"method": method.upper(), "path": path})
def add_rules(self, methods, path):
"""
Add rules for ``path`` pattern, for each methods in ``methods``. This is
a convenient helper over ``add_rule``. For example, this could be used
to grant all access on the API at once:
>>> ck.add_rules(["GET", "POST", "PUT", "DELETE"], "/*")
"""
for method in methods:
self.add_rule(method, path)
def add_recursive_rules(self, methods, path):
"""
Use this method to grant access on a full API tree. This is the
recommended way to grant access in the API. It will take care of granted
the root call *AND* sub-calls for you. Which is commonly forgotten...
For example, to grant a full access on ``/sms``:
>>> ck.add_recursive_rules(["GET", "POST", "PUT", "DELETE"], "/sms")
"""
path = path.rstrip("*/ ")
if path:
self.add_rules(methods, path)
self.add_rules(methods, path + "/*")

View File

@@ -0,0 +1,111 @@
# Copyright (c) 2013-2024, OVH SAS.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of OVH SAS nor the
# names of its contributors may be used to endorse or promote products
# derived from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY OVH SAS AND CONTRIBUTORS ``AS IS'' AND ANY
# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL OVH SAS AND CONTRIBUTORS BE LIABLE FOR ANY
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""
All exceptions used in OVH SDK derives from `APIError`
"""
class APIError(Exception):
"""Base OVH API exception, all specific exceptions inherits from it."""
def __init__(self, *args, **kwargs):
self.response = kwargs.pop("response", None)
if self.response is not None:
self.query_id = self.response.headers.get("X-OVH-QUERYID")
else:
self.query_id = None
super(APIError, self).__init__(*args, **kwargs)
def __str__(self):
if self.query_id: # pragma: no cover
return "{} \nOVH-Query-ID: {}".format(super(APIError, self).__str__(), self.query_id)
else: # pragma: no cover
return super(APIError, self).__str__()
class HTTPError(APIError):
"""Raised when the request fails at a low level (DNS, network, ...)"""
class InvalidKey(APIError):
"""Raised when trying to sign request with invalid key"""
class InvalidCredential(APIError):
"""Raised when trying to sign request with invalid consumer key"""
class InvalidConfiguration(APIError):
"""Raised when trying to load an invalid configuration into a client"""
class InvalidResponse(APIError):
"""Raised when api response is not valid json"""
class InvalidRegion(APIError):
"""Raised when region is not in `REGIONS`."""
class ReadOnlyError(APIError):
"""Raised when attempting to modify readonly data."""
class ResourceNotFoundError(APIError):
"""Raised when requested resource does not exist."""
class BadParametersError(APIError):
"""Raised when request contains bad parameters."""
class ResourceConflictError(APIError):
"""Raised when trying to create an already existing resource."""
class NetworkError(APIError):
"""Raised when there is an error from network layer."""
class NotGrantedCall(APIError):
"""Raised when there is an error from network layer."""
class NotCredential(APIError):
"""Raised when there is an error from network layer."""
class Forbidden(APIError):
"""Raised when there is an error from network layer."""
class ResourceExpiredError(APIError):
"""Raised when requested resource expired."""
class OAuth2FailureError(APIError):
"""Raised when the OAuth2 workflow fails"""

View File

@@ -0,0 +1,125 @@
# Copyright (c) 2013-2024, OVH SAS.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of OVH SAS nor the
# names of its contributors may be used to endorse or promote products
# derived from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY OVH SAS AND CONTRIBUTORS ````AS IS'' AND ANY
# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL OVH SAS AND CONTRIBUTORS BE LIABLE FOR ANY
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""
Thanks to https://github.com/requests/requests-oauthlib/issues/260 for the base used in this file.
"""
from oauthlib.oauth2 import BackendApplicationClient, MissingTokenError, OAuth2Error, TokenExpiredError
from requests_oauthlib import OAuth2Session
from .exceptions import OAuth2FailureError
class RefreshOAuth2Session(OAuth2Session):
_error = None
def __init__(self, token_url, **kwargs):
self.token_url = token_url
super().__init__(**kwargs)
# This hijacks the hook mechanism to save details about the last token creation failure.
# For now, there is no easy other way to access to these details;
# see https://github.com/requests/requests-oauthlib/pull/441
self.register_compliance_hook("access_token_response", self.save_error)
self.register_compliance_hook("refresh_token_response", self.save_error)
# See __init__, used as compliance hooks
def save_error(self, resp):
if 200 <= resp.status_code <= 299:
self._error = "Received invalid body: " + resp.text
if resp.status_code >= 400:
self._error = "Token creation failed with status_code={}, body={}".format(resp.status_code, resp.text)
return resp
# Wraps OAuth2Session.fetch_token to enrich returned exception messages, wrapped in an unique class
def fetch_token(self, *args, **kwargs):
try:
return super().fetch_token(*args, **kwargs)
except MissingTokenError as e:
desc = "OAuth2 failure: " + e.description
if self._error:
desc += " " + self._error
raise OAuth2FailureError(desc) from e
except OAuth2Error as e:
raise OAuth2FailureError("OAuth2 failure: " + str(e)) from e
# Wraps OAuth2Session.request to handle TokenExpiredError by fetching a new token and retrying
def request(self, *args, **kwargs):
try:
return super().request(*args, **kwargs)
except TokenExpiredError:
self.token = self.fetch_token(token_url=self.token_url, **self.auto_refresh_kwargs)
self.token_updater(self.token)
return super().request(*args, **kwargs)
class OAuth2:
_session = None
_token = None
def __init__(self, client_id, client_secret, token_url):
self.client_id = client_id
self.client_secret = client_secret
self.token_url = token_url
def token_updater(self, token):
self._token = token
@property
def session(self):
if self._session is None:
self._session = RefreshOAuth2Session(
token_url=self.token_url,
client=BackendApplicationClient(
client_id=self.client_id,
scope=["all"],
),
token=self.token,
token_updater=self.token_updater,
auto_refresh_kwargs={
"client_id": self.client_id,
"client_secret": self.client_secret,
},
)
return self._session
@property
def token(self):
if self._token is None:
self._token = RefreshOAuth2Session(
token_url=self.token_url,
client=BackendApplicationClient(
client_id=self.client_id,
scope=["all"],
),
).fetch_token(
token_url=self.token_url,
client_id=self.client_id,
client_secret=self.client_secret,
)
return self._token