├── .gitignore ├── CHANGELOG.rst ├── CONTRIBUTING.rst ├── LICENSE ├── MANIFEST.in ├── README.rst ├── ovhcli ├── __init__.py ├── cli.py ├── command.py ├── context.py ├── decorators.py ├── modules │ ├── __init__.py │ ├── me │ │ ├── __init__.py │ │ ├── commands.py │ │ ├── controllers.py │ │ └── utils.py │ ├── setup │ │ ├── __init__.py │ │ ├── commands.py │ │ ├── controllers.py │ │ └── utils.py │ └── webhosting │ │ ├── __init__.py │ │ ├── commands.py │ │ ├── controllers.py │ │ └── utils.py ├── output.py └── settings.py ├── requirements-dev.txt ├── requirements.txt ├── setup.py ├── tests ├── __init__.py ├── base.py ├── commands │ ├── __init__.py │ ├── test_me.py │ ├── test_setup.py │ └── test_webhosting.py ├── fixtures │ ├── __init__.py │ ├── me.py │ ├── output.py │ ├── setup.py │ └── webhosting.py └── units │ ├── __init__.py │ ├── test_context.py │ ├── test_output.py │ └── test_setup.py └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | __pycache__ 3 | 4 | # Packages 5 | *.egg 6 | *.egg-info 7 | dist 8 | build 9 | sdist 10 | 11 | # Unit test 12 | .tox 13 | 14 | # IDE 15 | .idea* 16 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | ========= 2 | CHANGELOG 3 | ========= 4 | 5 | 0.0.1 6 | ===== 7 | 8 | * initial commit -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | Contributing to ovh-cli 2 | ======================= 3 | 4 | This project accepts contributions. In order to contribute, you should 5 | pay attention to a few things: 6 | 7 | 1. your code must follow the coding style rules 8 | 2. your code must be unit-tested 9 | 3. your code must be documented 10 | 4. your work must be signed 11 | 5. the format of the submission must be email patches or GitHub Pull Requests 12 | 13 | 14 | Coding and documentation Style: 15 | ------------------------------- 16 | 17 | - The coding style follows `PEP-8: Style Guide for Python Code `_ (~100 chars/lines is a good limit) 18 | - The documentation style follows `PEP-257: Docstring Conventions `_ 19 | 20 | A good practice is to frequently run you code through `pylint `_ 21 | and make sure the code grades does not decrease. 22 | 23 | Submitting Modifications: 24 | ------------------------- 25 | 26 | The contributions should be email patches. The guidelines are the same 27 | as the patch submission for the Linux kernel except for the DCO which 28 | is defined below. The guidelines are defined in the 29 | 'SubmittingPatches' file, available in the directory 'Documentation' 30 | of the Linux kernel source tree. 31 | 32 | It can be accessed online too: 33 | 34 | https://www.kernel.org/doc/Documentation/SubmittingPatches 35 | 36 | You can submit your patches via GitHub 37 | 38 | Licensing for new files: 39 | ------------------------ 40 | 41 | ovh-cli is licensed under a (modified) BSD license. Anything contributed to 42 | ovh-cli must be released under this license. 43 | 44 | When introducing a new file into the project, please make sure it has a 45 | copyright header making clear under which license it's being released. 46 | 47 | Developer Certificate of Origin: 48 | -------------------------------- 49 | 50 | To improve tracking of contributions to this project we will use a 51 | process modeled on the modified DCO 1.1 and use a "sign-off" procedure 52 | on patches that are being emailed around or contributed in any other 53 | way. 54 | 55 | The sign-off is a simple line at the end of the explanation for the 56 | patch, which certifies that you wrote it or otherwise have the right 57 | to pass it on as an open-source patch. The rules are pretty simple: 58 | if you can certify the below: 59 | 60 | By making a contribution to this project, I certify that: 61 | 62 | (a) The contribution was created in whole or in part by me and I have 63 | the right to submit it under the open source license indicated in 64 | the file; or 65 | 66 | (b) The contribution is based upon previous work that, to the best of 67 | my knowledge, is covered under an appropriate open source License 68 | and I have the right under that license to submit that work with 69 | modifications, whether created in whole or in part by me, under 70 | the same open source license (unless I am permitted to submit 71 | under a different license), as indicated in the file; or 72 | 73 | (c) The contribution was provided directly to me by some other person 74 | who certified (a), (b) or (c) and I have not modified it. 75 | 76 | (d) The contribution is made free of any other party's intellectual 77 | property claims or rights. 78 | 79 | (e) I understand and agree that this project and the contribution are 80 | public and that a record of the contribution (including all 81 | personal information I submit with it, including my sign-off) is 82 | maintained indefinitely and may be redistributed consistent with 83 | this project or the open source license(s) involved. 84 | 85 | 86 | then you just add a line saying 87 | 88 | Signed-off-by: Random J Developer 89 | 90 | using your real name (sorry, no pseudonyms or anonymous contributions.) 91 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013-2016, OVH SAS. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above copyright 10 | notice, this list of conditions and the following disclaimer in the 11 | documentation and/or other materials provided with the distribution. 12 | * Neither the name of OVH SAS nor the 13 | names of its contributors may be used to endorse or promote products 14 | derived from this software without specific prior written permission. 15 | 16 | THIS SOFTWARE IS PROVIDED BY OVH SAS AND CONTRIBUTORS ``AS IS'' AND ANY 17 | EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL OVH SAS AND CONTRIBUTORS BE LIABLE FOR ANY 20 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 23 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.txt *.rst *.in *.ini 2 | include LICENSE 3 | recursive-include ovhcli *.py 4 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | OVH Cli 2 | ======= 3 | 4 | OVH Command Line Interface. 5 | 6 | .. code:: 7 | 8 | $ ovh webhosting config mydomain.fr 9 | +---------+-------------+----------------+---------------+------+--------+----------+ 10 | | #ID | Environment | Engine version | Container | Path | Engine | Firewall | 11 | +---------+-------------+----------------+---------------+------+--------+----------+ 12 | | 1994114 | production | 5.6 | stable | | php | security | 13 | +---------+-------------+----------------+---------------+------+--------+----------+ 14 | 15 | $ ovh webhosting config:update mydomain.fr --engine-version=7.0 16 | [*] The configuration will be updated in a few seconds. 17 | 18 | $ ovh webhosting config mydomain.fr 19 | +---------+-------------+----------------+---------------+------+--------+----------+ 20 | | #ID | Environment | Engine version | Container | Path | Engine | Firewall | 21 | +---------+-------------+----------------+---------------+------+--------+----------+ 22 | | 2023413 | production | 7.0 | stable | | php | security | 23 | +---------+-------------+----------------+---------------+------+--------+----------+ 24 | 25 | Installation 26 | ============ 27 | 28 | The OVH Cli works with Python 2.7+ and Python 3.3+. 29 | 30 | The easiest way to get the latest stable release is to grab it from `pypi 31 | `_ using ``pip`` : 32 | 33 | .. code:: bash 34 | 35 | $ pip install ovhcli 36 | 37 | Or if you are not using a ``virtualenv`` : 38 | 39 | .. code:: bash 40 | 41 | $ sudo pip install ovhcli 42 | 43 | If you want to upgrade it : 44 | 45 | .. code:: bash 46 | 47 | $ pip install --upgrade ovhcli 48 | 49 | Alternatively, you may get latest development version directly from Git : 50 | 51 | .. code:: bash 52 | 53 | $ pip install -e git+https://github.com/ovh/ovh-cli.git#egg=ovh-cli 54 | 55 | Getting started 56 | =============== 57 | 58 | The Cli uses the public OVH API to manage the user products. A ``setup`` command 59 | is provided to help you creating the required tokens : 60 | 61 | .. code:: 62 | 63 | $ ovh setup init 64 | Welcome to the OVH Cli. 65 | 66 | This tool uses the public OVH API to manage your products. In order to 67 | work, 3 tokens that you must generate are required : 68 | 69 | - the application key (AK) 70 | - the application secret (AS) 71 | - the consumer key (CK) 72 | 73 | What's your context : 74 | 75 | 1) You already have the keys (AK, AS and CK) 76 | 2) You just have AK and AS, the CK must be generated 77 | 3) You have no keys 78 | 79 | Your choice [1]: 3 80 | 81 | [-] Please visit the following link to authenticate you and obtain your keys (AK, AS and CK) : 82 | [-] https://api.ovh.com/createToken/index.cgi?GET=/*&POST=/*&PUT=/*&DELETE=/* 83 | Press any key to continue ... 84 | 85 | Endpoint [ovh-eu]: ovh-eu 86 | Application key: 87 | Application secret: 88 | Consumer key: 89 | [*] Configuration file created. 90 | 91 | Commands help 92 | ============= 93 | 94 | Each command and subcommand provides a ``--help`` parameter : 95 | 96 | .. code:: 97 | 98 | $ ovh webhosting --help 99 | Usage: ovh webhosting [OPTIONS] COMMAND [ARGS]... 100 | 101 | Manage and configure your WebHosting products. 102 | 103 | Options: 104 | --help Show this message and exit. 105 | 106 | Commands: 107 | config Display the ovhConfig information. 108 | config:update Update the ovhConfig information. 109 | info Display information about a service. 110 | info:countries Display the service countries. 111 | info:quota Display the service quota. 112 | list List the services. 113 | users List the users of a service. 114 | users:create Add a new user to a service. 115 | users:remove Remove a user from a service. 116 | users:show Information about a user. 117 | users:update Update an existing user. 118 | 119 | JSON output 120 | =========== 121 | 122 | By default, the OVH Cli displays the output in a pretty table representation. When it's possible, a ``--json`` parameter is provided to return the content as pure JSON : 123 | 124 | .. code:: 125 | 126 | $ ovh webhosting users mydomain.fr --full 127 | +-------------+------+-------+--------+-----------------+ 128 | | Login | Home | State | Ssh | Primary account | 129 | +-------------+------+-------+--------+-----------------+ 130 | | johndoe | . | rw | active | True | 131 | | johndoe-foo | foo | rw | none | False | 132 | +-------------+------+-------+--------+-----------------+ 133 | 134 | $ ovh webhosting users mydomain.fr --full --json 135 | [{"iisRemoteRights": null, "sshState": "none", "webDavRights": null, "login": "johndoe-foo", "isPrimaryAccount": false, "state": "rw", "home": "foo"}, {"iisRemoteRights": null, "sshState": "active", "webDavRights": null, "login": "johndoe", "isPrimaryAccount": true, "state": "rw", "home": "."}] 136 | 137 | Contributing 138 | ============ 139 | 140 | See `CONTRIBUTING.rst `_ for contribution guidelines. 141 | 142 | License 143 | ======= 144 | 145 | 3-Clause BSD -------------------------------------------------------------------------------- /ovhcli/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf8 -*- 2 | 3 | __version__ = '0.0.1' 4 | -------------------------------------------------------------------------------- /ovhcli/cli.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf8 -*- 2 | 3 | import click 4 | 5 | from ovhcli.command import ModuleCollection 6 | from ovhcli.context import OvhContext 7 | from ovhcli.settings import MODULES_FOLDER 8 | 9 | 10 | context = OvhContext 11 | pass_ovh = click.make_pass_decorator(context, ensure=True) 12 | 13 | 14 | @click.command(cls=ModuleCollection, module_folder=MODULES_FOLDER) 15 | @click.version_option(prog_name='OVH Cli') 16 | @click.option('-d', '--debug', is_flag=True, help='Enable the debug mode.') 17 | @pass_ovh 18 | def cli(ovh, debug): 19 | """OVH Command Line Interface""" 20 | ovh.debug_mode = debug 21 | -------------------------------------------------------------------------------- /ovhcli/command.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf8 -*- 2 | 3 | import importlib 4 | import os 5 | 6 | from click import CommandCollection 7 | from ovh.exceptions import APIError 8 | 9 | from ovhcli.settings import MODULES_FOLDER 10 | 11 | 12 | def is_module(module): 13 | """Check if a given string is an existing module contained in the 14 | ``MODULES_FOLDER`` constant.""" 15 | if (os.path.isdir(os.path.join(MODULES_FOLDER, module)) and 16 | not module.startswith('_')): 17 | return True 18 | 19 | return False 20 | 21 | 22 | class ModuleCollection(CommandCollection): 23 | """A module collection is a command collection that fetch the commands 24 | specified in a module folder. 25 | 26 | A module folder has the following structure : 27 | 28 | modules/ 29 | ├── foo/ 30 | │ ├── __init__.py 31 | │ ├── commands.py 32 | │ ├── controllers.py 33 | │ └── utils.py 34 | ├── ... 35 | 36 | The main command is defined in ``modules/foo/__init__.py``. Its subcommands 37 | are defined in ``modules/foo/commands.py``. 38 | """ 39 | 40 | def __init__(self, name=None, module_folder=None, **attrs): 41 | CommandCollection.__init__(self, name, **attrs) 42 | self.module_folder = module_folder 43 | 44 | def list_commands(self, ctx): 45 | """List the modules contained in the ``MODULES_FOLDER`` constant.""" 46 | commands = [ 47 | module for module 48 | in sorted(os.listdir(self.module_folder)) 49 | if is_module(module) 50 | ] 51 | 52 | return commands 53 | 54 | def get_command(self, ctx, name): 55 | """List the commands for a specific module.""" 56 | try: 57 | # Import the module 58 | mod = importlib.import_module('ovhcli.modules.{}'.format(name)) 59 | 60 | # Import the commands 61 | importlib.import_module('ovhcli.modules.{}.commands'.format(name)) 62 | 63 | return getattr(mod, name) 64 | 65 | except ImportError: 66 | pass 67 | 68 | def invoke(self, ctx): 69 | """Call the command. If an error in the OVH API is raised, display a 70 | message with the corresponding error.""" 71 | try: 72 | super(ModuleCollection, self).invoke(ctx) 73 | except APIError as e: 74 | 75 | # Debug mode 76 | if ctx.obj.debug_mode: 77 | raise 78 | 79 | ctx.obj.error(e) 80 | -------------------------------------------------------------------------------- /ovhcli/context.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf8 -*- 2 | 3 | import importlib 4 | import json as _json 5 | import os 6 | from time import strftime 7 | 8 | import click 9 | import ovh 10 | from ovh.exceptions import APIError, InvalidRegion 11 | 12 | from ovhcli.command import is_module 13 | from ovhcli.output import Output 14 | from ovhcli.settings import MODULES_FOLDER 15 | 16 | 17 | class OvhContext(click.Context): 18 | def __init__(self): 19 | self.debug_mode = False 20 | self.commit = False 21 | self._json = False 22 | self._controllers = {} 23 | 24 | if not self._controllers: 25 | self.load_controllers() 26 | 27 | def __getattribute__(self, item): 28 | """Used to dynamically call the method of a controller in a command 29 | function. If the specified controller does not exist, just return 30 | the class attribute. 31 | 32 | For example, the line ``ovh.foo.bar()`` in the following command 33 | calls the ``modules.foo.controllers.Foo.bar`` method : 34 | 35 | @foo.command('bar') 36 | @pass_ovh 37 | def bar(ovh): 38 | data = ovh.foo.bar() 39 | """ 40 | if item in object.__getattribute__(self, '_controllers'): 41 | cls = object.__getattribute__(self, '_controllers')[item] 42 | 43 | # The setup module does not require a client instance 44 | if item != 'setup': 45 | client = self.get_ovh_client() 46 | cls.client = client 47 | 48 | return cls 49 | 50 | return object.__getattribute__(self, item) 51 | 52 | def get_ovh_client(self): 53 | """Get the OVH client.""" 54 | try: 55 | client = ovh.Client() 56 | except InvalidRegion: 57 | self.error('The configuration was not found.') 58 | self.error('Please use `ovh setup init` to create it.') 59 | self.exit() 60 | 61 | return client 62 | 63 | def load_controllers(self): 64 | """Load the controllers for each module specified in the 65 | ``MODULE_FOLDER`` constant. 66 | 67 | If a module can't be imported for any reason, we do not display it.""" 68 | modules = [ 69 | module for module 70 | in sorted(os.listdir(MODULES_FOLDER)) 71 | if is_module(module) 72 | ] 73 | 74 | for module in modules: 75 | try: 76 | controller = importlib.import_module( 77 | 'ovhcli.modules.{}.controllers'.format(module) 78 | ) 79 | 80 | self._controllers[module] = getattr( 81 | controller, 82 | module.capitalize() 83 | ) 84 | except ImportError: 85 | # Do not raise error if the controller is not provided 86 | # Some modules does not require one 87 | pass 88 | 89 | def echo(self, message, prefix='', color='white'): 90 | """Print a message with a colored prefix unless the ``--json`` 91 | parameter is specified.""" 92 | try: 93 | json = self.json 94 | except AttributeError: 95 | json = False 96 | 97 | if not json: 98 | if prefix: 99 | prefix = '[{}] '.format(click.style(prefix, fg=color)) 100 | click.echo(u"{}{}".format(prefix, message)) 101 | 102 | def debug(self, message): 103 | """Print a debug message if the debug mode is enabled.""" 104 | if self.debug_mode: 105 | self.echo(message, 'debug', 'blue') 106 | 107 | def info(self, message): 108 | """Print an information message.""" 109 | self.echo(message, '-', 'cyan') 110 | 111 | def time_echo(self, message): 112 | """Print an information message with a formatted date.""" 113 | self.echo(message, strftime('%H:%M:%S'), 'cyan') 114 | 115 | def success(self, message): 116 | """Print a success message.""" 117 | self.echo(message, '*', 'green') 118 | 119 | def warning(self, message): 120 | """Print a warning message.""" 121 | self.echo(message, 'warning', 'yellow') 122 | 123 | def error(self, message): 124 | """Print an error message.""" 125 | self.echo(message, 'error', 'red') 126 | 127 | def table(self, data, custom_func=None, exclude=[], sort=None): 128 | """ 129 | Print a pretty table unless the ``--json`` parameter is specified. 130 | 131 | If no custom function is given, use the ``Output`` class to generate 132 | the table.""" 133 | try: 134 | json = self.json 135 | except AttributeError: 136 | json = False 137 | 138 | # Print the result as json 139 | if json: 140 | click.echo(_json.dumps(data)) 141 | return 142 | 143 | # Use the custom function if provided 144 | if custom_func: 145 | self.echo(custom_func(data)) 146 | return 147 | 148 | # Otherwise, print the table with Output class 149 | table = Output(data, exclude=exclude, sort=sort) 150 | self.echo(table.convert()) 151 | 152 | def display_task(self, task): 153 | """Print a task status.""" 154 | name = task['function'] 155 | 156 | if task['status'] in ['init', 'todo', 'doing']: 157 | self.success( 158 | 'The task {} has been launched.'.format(name) 159 | ) 160 | elif task['status'] == 'done': 161 | self.success( 162 | 'The task {} is done.'.format(name) 163 | ) 164 | elif task['status'] == 'cancelled': 165 | self.warning( 166 | 'The task {} has been cancelled.'.format(name) 167 | ) 168 | else: 169 | self.error( 170 | 'The task {} fell in an error state.'.format(name) 171 | ) 172 | -------------------------------------------------------------------------------- /ovhcli/decorators.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf8 -*- 2 | 3 | from click.decorators import option 4 | 5 | 6 | def confirm_option(*params_decls, **attrs): 7 | """This decorator adds a confirmation message for critical actions : it can 8 | be bypassed with a ``--confirm`` parameter or answering ``yes`` to the 9 | prompted message. 10 | 11 | All parameters are optionals. 12 | 13 | Example usage:: 14 | 15 | @module.command('users:remove') 16 | @click.argument('username') 17 | @confirm_option(help='Do you really want to remove this user ?') 18 | @pass_ovh 19 | def remove_user(ovh, username): 20 | pass 21 | """ 22 | def decorator(f): 23 | def callback(ctx, param, value): 24 | if not value: 25 | ctx.abort() 26 | 27 | attrs.setdefault('is_flag', True) 28 | attrs.setdefault('callback', callback) 29 | attrs.setdefault('expose_value', False) 30 | attrs.setdefault('prompt', 'Confirm this action ?') 31 | attrs.setdefault('help', 'Confirm the action') 32 | 33 | return option(*(params_decls or ('--confirm',)), **attrs)(f) 34 | 35 | return decorator 36 | 37 | 38 | def json_option(*params_decls, **attrs): 39 | """This decorator adds a ``--json`` parameter which can be used to display 40 | the controller results in JSON format. 41 | 42 | It should be used with a function supporting it, for example the 43 | ``ovhcli.context.OvhContext.table`` function. 44 | 45 | Example usage:: 46 | 47 | @module.command('users:list') 48 | @json_option() 49 | @pass_ovh 50 | def list_users(ovh): 51 | data = [{'username': 'john'}, {'username': 'bob'}] 52 | ovh.table(data) 53 | """ 54 | def decorator(f): 55 | def callback(ctx, param, value): 56 | try: 57 | ctx.obj.json = value 58 | except AttributeError: 59 | pass 60 | 61 | attrs.setdefault('is_flag', True) 62 | attrs.setdefault('callback', callback) 63 | attrs.setdefault('expose_value', False) 64 | attrs.setdefault('help', 'Return the JSON value.') 65 | 66 | return option(*(params_decls or ('--json',)), **attrs)(f) 67 | 68 | return decorator 69 | -------------------------------------------------------------------------------- /ovhcli/modules/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf8 -*- 2 | 3 | 4 | class OvhModule(object): 5 | """Useful helpers will be defined here.""" 6 | -------------------------------------------------------------------------------- /ovhcli/modules/me/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf8 -*- 2 | 3 | import click 4 | 5 | 6 | @click.group(short_help="Manage your personal information.") 7 | def me(): 8 | """Manage your personal information.""" 9 | -------------------------------------------------------------------------------- /ovhcli/modules/me/commands.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf8 -*- 2 | 3 | import click 4 | 5 | from ovhcli.cli import pass_ovh 6 | from ovhcli.decorators import json_option 7 | from ovhcli.modules.me import me 8 | 9 | 10 | @me.command('info', short_help='Display your personal information.') 11 | @json_option() 12 | @pass_ovh 13 | def info(ovh): 14 | """Get your personal information.""" 15 | me = ovh.me.info() 16 | ovh.success('Welcome here %s' % me['firstname']) 17 | ovh.table(me, exclude=['companyNationalIdentificationNumber', 18 | 'nationalIdentificationNumber']) 19 | 20 | 21 | @me.command('apps', short_help='List the applications.') 22 | @json_option() 23 | @pass_ovh 24 | def applications(ovh): 25 | """List the applications.""" 26 | applications = ovh.me.get_applications() 27 | ovh.table(applications, sort='-applicationId') 28 | 29 | 30 | @me.command('apps:show', 31 | short_help='Display information about an application.') 32 | @click.argument('applicationId') 33 | @json_option() 34 | @pass_ovh 35 | def application(ovh, applicationid): 36 | """Display information about an application.""" 37 | app = ovh.me.get_application(applicationid) 38 | ovh.table(app) 39 | 40 | 41 | @me.command('apps:credentials', 42 | short_help='Get the credentials of an application.') 43 | @click.argument('application_id') 44 | @json_option() 45 | @pass_ovh 46 | def credentials(ovh, application_id): 47 | """Get the credentials of an application.""" 48 | creds = ovh.me.get_credentials(application_id) 49 | ovh.table(creds, exclude=['applicationId', 'ovhSupport', 'rules']) 50 | 51 | 52 | @me.command('apps:rules', short_help='Get the rules of a credential.') 53 | @click.argument('credential_id') 54 | @json_option() 55 | @pass_ovh 56 | def rules(ovh, credential_id): 57 | """Get the rules of a credential.""" 58 | rules = ovh.me.get_rules(credential_id) 59 | ovh.table(rules) 60 | -------------------------------------------------------------------------------- /ovhcli/modules/me/controllers.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf8 -*- 2 | 3 | from ovhcli.modules import OvhModule 4 | 5 | 6 | class Me(OvhModule): 7 | 8 | @classmethod 9 | def info(cls): 10 | return cls.client.get('/me') 11 | 12 | @classmethod 13 | def get_application(cls, application_id): 14 | url = '/me/api/application/{}'.format(application_id) 15 | return cls.client.get(url) 16 | 17 | @classmethod 18 | def get_applications(cls): 19 | apps = [cls.get_application(app) 20 | for app in cls.client.get('/me/api/application')] 21 | 22 | return apps 23 | 24 | @classmethod 25 | def get_credential(cls, credential_id): 26 | url = '/me/api/credential/{}'.format(credential_id) 27 | return cls.client.get(url) 28 | 29 | @classmethod 30 | def get_credentials(cls, application_id=None): 31 | params = {} 32 | 33 | if application_id: 34 | # Check if this App exists 35 | app = cls.get_application(application_id) 36 | 37 | params['applicationId'] = app['applicationId'] 38 | 39 | credentials = cls.client.get('/me/api/credential', **params) 40 | 41 | data = [cls.get_credential(credential) for credential in credentials] 42 | return data 43 | 44 | @classmethod 45 | def get_rules(cls, credential_id): 46 | credential = cls.get_credential(credential_id) 47 | return credential['rules'] 48 | -------------------------------------------------------------------------------- /ovhcli/modules/me/utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf8 -*- 2 | -------------------------------------------------------------------------------- /ovhcli/modules/setup/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf8 -*- 2 | 3 | import click 4 | 5 | 6 | @click.group(short_help="Setup and configure the OVH Cli.") 7 | def setup(): 8 | """Setup and configure the OVH Cli.""" 9 | -------------------------------------------------------------------------------- /ovhcli/modules/setup/commands.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf8 -*- 2 | 3 | import click 4 | 5 | from ovhcli.cli import pass_ovh 6 | from ovhcli.modules.setup import setup 7 | from ovhcli.modules.setup.utils import (check_choice, check_endpoint, 8 | config_file_exists, create_config_file, 9 | launch_setup_by_choice) 10 | 11 | 12 | WELCOME_MESSAGE = '''Welcome to the OVH Cli. 13 | 14 | This tool uses the public OVH API to manage your products. In order to 15 | work, 3 tokens that you must generate are required : 16 | 17 | - the application key (AK) 18 | - the application secret (AS) 19 | - the consumer key (CK) 20 | 21 | What's your context : 22 | 23 | 1) You already have the keys (AK, AS and CK) 24 | 2) You just have AK and AS, the CK must be generated 25 | 3) You have no keys 26 | ''' 27 | 28 | 29 | @setup.command('init', short_help='Generate the configuration file.') 30 | @click.option('--force', is_flag=True, help="Erase the existing file.") 31 | @pass_ovh 32 | def init(ovh, force): 33 | """Generate the configuration file.""" 34 | if config_file_exists(): 35 | if not force: 36 | ovh.error('A configuration file already exists ' 37 | '(use --force to erase it).') 38 | ovh.exit() 39 | else: 40 | ovh.warning('The configuration file will be erased.\n') 41 | 42 | # Display the welcome message 43 | ovh.echo(WELCOME_MESSAGE) 44 | 45 | # According to the choice, we launch the good def 46 | choice = click.prompt('Your choice', default=1, value_proc=check_choice) 47 | ovh.echo('') 48 | 49 | launch_setup_by_choice(ovh, choice) 50 | -------------------------------------------------------------------------------- /ovhcli/modules/setup/controllers.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf8 -*- 2 | 3 | from ovhcli.modules import OvhModule 4 | 5 | 6 | class Setup(OvhModule): 7 | pass 8 | -------------------------------------------------------------------------------- /ovhcli/modules/setup/utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf8 -*- 2 | 3 | import os 4 | 5 | import click 6 | import ovh 7 | from click.exceptions import UsageError 8 | from ovh.client import ENDPOINTS 9 | 10 | from ovhcli.settings import CONFIG_PATH, CONFIG_TEMPLATE, CREATE_TOKEN_LINK 11 | 12 | 13 | def check_choice(value): 14 | """Check if the given choice is in the allowed range.""" 15 | if value not in ['1', '2', '3']: 16 | raise UsageError('Choice must be 1, 2 or 3') 17 | 18 | return int(value) 19 | 20 | 21 | def check_endpoint(value): 22 | """Check if the given endpoint is in the allowed ones.""" 23 | endpoints = ENDPOINTS.keys() 24 | 25 | if value not in endpoints: 26 | raise UsageError('This endpoint does not exist ({})'.format( 27 | ', '.join(sorted(endpoints)) 28 | )) 29 | 30 | return value 31 | 32 | 33 | def config_file_exists(): 34 | """Check if the configuration file already exists.""" 35 | return os.path.isfile(CONFIG_PATH) 36 | 37 | 38 | def create_config_file(endpoint, application_key, 39 | application_secret, consumer_key): 40 | """ 41 | Create the configuration file. 42 | :param endpoint: 43 | :param application_key: 44 | :param application_secret: 45 | :param consumer_key: 46 | :return: 47 | """ 48 | template = CONFIG_TEMPLATE.replace('{ENDPOINT}', endpoint) 49 | template = template.replace('{AK}', application_key) 50 | template = template.replace('{AS}', application_secret) 51 | template = template.replace('{CK}', consumer_key) 52 | 53 | with open(CONFIG_PATH, 'w') as f: 54 | f.write(template) 55 | 56 | return True 57 | 58 | 59 | def get_ck_validation(endpoint, application_key, application_secret): 60 | """Return a validation request with full access to the API.""" 61 | client = ovh.Client(endpoint=endpoint, application_key=application_key, 62 | application_secret=application_secret) 63 | ck = client.new_consumer_key_request() 64 | ck.add_recursive_rules(ovh.API_READ_WRITE, '/') 65 | validation = ck.request() 66 | 67 | return validation 68 | 69 | 70 | def launch_setup_1(ctx): 71 | """Choice 1 : user already has the 3 tokens (AK, AS and CK).""" 72 | endpoint = click.prompt('Endpoint', default='ovh-eu', 73 | value_proc=check_endpoint) 74 | application_key = click.prompt('Application key') 75 | application_secret = click.prompt('Application secret') 76 | consumer_key = click.prompt('Consumer key') 77 | 78 | create_config_file(endpoint, application_key, 79 | application_secret, consumer_key) 80 | 81 | ctx.success('Configuration file created.') 82 | 83 | 84 | def launch_setup_2(ctx): 85 | """Choice 1 : user has the AK and AS tokens. We generate for him a link to 86 | validate the CK token.""" 87 | endpoint = click.prompt('Endpoint', default='ovh-eu', 88 | value_proc=check_endpoint) 89 | application_key = click.prompt('Application key') 90 | application_secret = click.prompt('Application secret') 91 | 92 | ctx.echo('') 93 | validation = get_ck_validation(endpoint, application_key, 94 | application_secret) 95 | ctx.info("Please visit the following link to authenticate you and " 96 | "validate the token :") 97 | ctx.info(validation['validationUrl']) 98 | click.pause() 99 | 100 | create_config_file(endpoint, application_key, 101 | application_secret, validation['consumerKey']) 102 | 103 | ctx.success('Configuration file created.') 104 | 105 | 106 | def launch_setup_3(ctx): 107 | """Choice 3 : the user does not have key, we provide him a link to 108 | generate it.""" 109 | ctx.info("Please visit the following link to authenticate you and " 110 | "obtain your keys (AK, AS and CK) :") 111 | ctx.info(CREATE_TOKEN_LINK) 112 | click.pause() 113 | ctx.echo('') 114 | 115 | launch_setup_1(ctx) 116 | 117 | 118 | def launch_setup_by_choice(ctx, choice): 119 | """Call the good setup process.""" 120 | choices = { 121 | 1: launch_setup_1, 122 | 2: launch_setup_2, 123 | 3: launch_setup_3 124 | } 125 | 126 | # Launch the good process 127 | try: 128 | choices[choice](ctx) 129 | except KeyError: 130 | ctx.error('Invalid choice') 131 | -------------------------------------------------------------------------------- /ovhcli/modules/webhosting/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf8 -*- 2 | 3 | import click 4 | 5 | 6 | @click.group(short_help="Manage your WebHosting services.") 7 | def webhosting(): 8 | """Manage and configure your WebHosting products.""" 9 | -------------------------------------------------------------------------------- /ovhcli/modules/webhosting/commands.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf8 -*- 2 | 3 | import click 4 | 5 | from ovhcli.cli import pass_ovh 6 | from ovhcli.decorators import confirm_option, json_option 7 | from ovhcli.modules.webhosting import webhosting 8 | from ovhcli.modules.webhosting.utils import (display_services, display_quota, 9 | display_users, display_full_users, 10 | display_config) 11 | 12 | 13 | # Constants 14 | CONFIG_CONTAINERS = ['jessie.i386', 'legacy', 'stable', 'testing'] 15 | CONFIG_ENGINES = ['php', 'phpcgi'] 16 | CONFIG_ENGINE_VERSIONS = ['5.4', '5.5', '5.6', '7.0'] 17 | CONFIG_ENVIRONMENTS = ['development', 'production'] 18 | CONFIG_FIREWALL = ['none', 'security'] 19 | 20 | 21 | @webhosting.command('list', short_help='List the services.') 22 | @json_option() 23 | @pass_ovh 24 | def list(ovh): 25 | """List your Web Hosting services.""" 26 | services = ovh.webhosting.list() 27 | ovh.table(services, display_services) 28 | 29 | 30 | @webhosting.command('info', short_help='Display information about a service.') 31 | @click.argument('service') 32 | @json_option() 33 | @pass_ovh 34 | def info(ovh, service): 35 | """Display information about a service.""" 36 | service = ovh.webhosting.info(service) 37 | ovh.table(service, exclude=['countriesIp', 'phpVersions']) 38 | 39 | 40 | @webhosting.command('info:quota', short_help='Display the service quota.') 41 | @click.argument('service') 42 | @json_option() 43 | @pass_ovh 44 | def quota(ovh, service): 45 | """Display the free and used quotas of a service.""" 46 | quota = ovh.webhosting.quota(service) 47 | ovh.table(quota, display_quota) 48 | 49 | 50 | @webhosting.command('info:countries', 51 | short_help='Display the service countries.') 52 | @click.argument('service') 53 | @json_option() 54 | @pass_ovh 55 | def countries(ovh, service): 56 | """Display the countries of a service.""" 57 | countries = ovh.webhosting.countries(service) 58 | ovh.table(countries, sort='country') 59 | 60 | 61 | @webhosting.command('config', short_help='Display the .ovhconfig information.') 62 | @click.argument('service') 63 | @click.option('--historical', is_flag=True, 64 | help="Display the historical results.") 65 | @click.option('--all', is_flag=True, help="Display all the results.") 66 | @json_option() 67 | @pass_ovh 68 | def config(ovh, service, historical, all): 69 | """Display the .ovhconfig of a service.""" 70 | configs = ovh.webhosting.config( 71 | service, 72 | historical, 73 | all 74 | ) 75 | ovh.table(configs, display_config) 76 | 77 | 78 | @webhosting.command('config:update', 79 | short_help='Update the .ovhconfig information.') 80 | @click.argument('service') 81 | @click.option('--engine', type=click.Choice(CONFIG_ENGINES), default=None, 82 | help='Change the engine.') 83 | @click.option('--engine-version', type=click.Choice(CONFIG_ENGINE_VERSIONS), 84 | default=None, help='Change the engine version.') 85 | @click.option('--container', type=click.Choice(CONFIG_CONTAINERS), 86 | default=None, help='Change the container.') 87 | @click.option('--environment', type=click.Choice(CONFIG_ENVIRONMENTS), 88 | default=None, help='Change the environment.') 89 | @click.option('--firewall', type=click.Choice(CONFIG_FIREWALL), 90 | default=None, help='Change the http firewall.') 91 | @confirm_option() 92 | @pass_ovh 93 | def update_config(ovh, service, container, engine, engine_version, 94 | environment, firewall): 95 | """Update the .ovhconfig information.""" 96 | task = ovh.webhosting.update_config( 97 | service=service, 98 | ovh_config=None, 99 | container=container, 100 | engine=engine, 101 | engine_version=engine_version, 102 | environment=environment, 103 | firewall=firewall 104 | ) 105 | 106 | if task: 107 | ovh.success('The configuration will be updated in a few seconds.') 108 | 109 | return task 110 | 111 | 112 | @webhosting.command('users', short_help='List the users of a service.') 113 | @click.argument('service') 114 | @click.option('--full', is_flag=True, 115 | help="Display all information about the users.") 116 | @json_option() 117 | @pass_ovh 118 | def get_users(ovh, service, full): 119 | """Display the users of a service.""" 120 | users = ovh.webhosting.get_users(service, full) 121 | output_func = display_full_users if full else display_users 122 | 123 | ovh.table(users, output_func) 124 | 125 | 126 | @webhosting.command('users:show', short_help='Information about a user.') 127 | @click.option('--login', '-l', help='Login of the user.', required=True) 128 | @click.argument('service') 129 | @json_option() 130 | @pass_ovh 131 | def get_user(ovh, service, login): 132 | """Display information about a user of a service.""" 133 | user = ovh.webhosting.get_user(service, login) 134 | ovh.table(user) 135 | 136 | 137 | @webhosting.command('users:create', short_help='Add a new user to a service.') 138 | @click.argument('service') 139 | @click.option('--login', '-l', help='Login for the new user.', required=True) 140 | @click.option('--password', '-p', help='Password for the new user.', 141 | prompt=True, hide_input=True, confirmation_prompt=True) 142 | @click.option('--home', '-h', help='Home directory.', default='.', 143 | show_default=True) 144 | @click.option('--ssh', help='Enable the SSH.', is_flag=True) 145 | @confirm_option() 146 | @pass_ovh 147 | def create_user(ovh, service, login, password, home, ssh): 148 | """Add a new user to a service.""" 149 | task = ovh.webhosting.create_user(service, login, password, home, ssh) 150 | 151 | if task: 152 | ovh.success('User {} will be created in a few seconds.'.format( 153 | login 154 | )) 155 | 156 | 157 | @webhosting.command('users:update', short_help='Update a user information.') 158 | @click.argument('service') 159 | @click.option('--login', '-l', help='Login of the user to update.', 160 | required=True) 161 | @click.option('--home', '-h', help='Home directory.') 162 | @click.option('--ssh', help='Enable the SSH.', is_flag=True) 163 | @click.option('--enabled/--disabled', help='Enable or disable the user.') 164 | @confirm_option() 165 | @pass_ovh 166 | def update_user(ovh, service, login, home, ssh, enabled): 167 | """Update a user information.""" 168 | user = ovh.webhosting.update_user(ovh, service, login, 169 | home, ssh, enabled) 170 | 171 | if user: 172 | msg = 'Information about {} will be updated in a few seconds.' 173 | ovh.success(msg.format(login)) 174 | 175 | 176 | @webhosting.command('users:remove', short_help='Remove a user from a service.') 177 | @click.argument('service') 178 | @click.option('--login', '-l', help='Login of the user to remove.', 179 | required=True) 180 | @confirm_option() 181 | @pass_ovh 182 | def remove_user(ovh, service, login): 183 | """Remove a user from a service.""" 184 | task = ovh.webhosting.remove_user(ovh, service, login) 185 | 186 | if task: 187 | ovh.success('User {} will be removed in a few seconds.'.format( 188 | login 189 | )) 190 | -------------------------------------------------------------------------------- /ovhcli/modules/webhosting/controllers.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf8 -*- 2 | 3 | from ovhcli.modules import OvhModule 4 | 5 | 6 | class Webhosting(OvhModule): 7 | 8 | @classmethod 9 | def list(cls): 10 | return cls.client.get('/hosting/web') 11 | 12 | @classmethod 13 | def info(cls, service): 14 | return cls.client.get('/hosting/web/{}'.format( 15 | service 16 | )) 17 | 18 | @classmethod 19 | def quota(cls, service): 20 | info = cls.info(service) 21 | quota = { 22 | 'used': info['quotaUsed'], 23 | 'size': info['quotaSize'], 24 | } 25 | 26 | return quota 27 | 28 | @classmethod 29 | def countries(cls, service): 30 | info = cls.info(service) 31 | return info['countriesIp'] 32 | 33 | @classmethod 34 | def get_user(cls, service, username): 35 | url = '/hosting/web/{}/user/{}'.format(service, username) 36 | return cls.client.get(url) 37 | 38 | @classmethod 39 | def get_users(cls, service, full=False): 40 | usernames = cls.client.get('/hosting/web/{}/user'.format( 41 | service 42 | )) 43 | 44 | # Return only usernames by default 45 | if not full: 46 | return usernames 47 | 48 | # Otherwise fetch all information by user 49 | users = [cls.get_user(service, username) for username in usernames] 50 | 51 | return users 52 | 53 | @classmethod 54 | def create_user(cls, service, login, password, home, ssh): 55 | url = '/hosting/web/{}/user'.format(service) 56 | ssh = 'active' if ssh else 'none' 57 | 58 | params = { 59 | 'home': home, 60 | 'login': login, 61 | 'password': password, 62 | 'sshState': ssh 63 | } 64 | 65 | task = cls.client.post(url, **params) 66 | return task 67 | 68 | @classmethod 69 | def update_user(cls, ovh, service, login, home, ssh, state): 70 | url = '/hosting/web/{}/user/{}'.format(service, login) 71 | params = {} 72 | 73 | if home: 74 | params['home'] = home 75 | 76 | if ssh: 77 | ssh = 'active' if ssh else 'none' 78 | params['sshState'] = ssh 79 | 80 | if state: 81 | state = 'rw' if state else 'off' 82 | params['state'] = state 83 | 84 | cls.client.put(url, **params) 85 | return True 86 | 87 | @classmethod 88 | def remove_user(cls, ovh, service, login): 89 | url = '/hosting/web/{}/user/{}'.format(service, login) 90 | return cls.client.delete(url) 91 | 92 | @classmethod 93 | def config(cls, service, historical=False, all=False): 94 | configs = [] 95 | params = {} 96 | 97 | if not all: 98 | if historical: 99 | params['historical'] = 'true' 100 | else: 101 | params['historical'] = 'false' 102 | 103 | conf_ids = cls.client.get('/hosting/web/{}/ovhConfig'.format( 104 | service, 105 | ), **params) 106 | 107 | for _id in conf_ids: 108 | conf = cls.client.get('/hosting/web/{}/ovhConfig/{}'.format( 109 | service, 110 | _id 111 | )) 112 | configs.append(conf) 113 | 114 | return configs 115 | 116 | @classmethod 117 | def update_config(cls, service, ovh_config=None, container=None, 118 | engine=None, engine_version=None, environment=None, 119 | firewall=None): 120 | params = {} 121 | 122 | # Get the last ovhConfig ID if not provided 123 | if not ovh_config: 124 | ovh_config = cls.config(service)[0]['id'] 125 | 126 | if container: 127 | params['container'] = container 128 | 129 | if engine: 130 | params['engineName'] = engine 131 | 132 | if engine_version: 133 | params['engineVersion'] = engine_version 134 | 135 | if environment: 136 | params['environment'] = environment 137 | 138 | if firewall: 139 | params['httpFirewall'] = firewall 140 | 141 | # Change the service configuration 142 | url = '/hosting/web/{}/ovhConfig/{}/changeConfiguration'.format( 143 | service, 144 | ovh_config 145 | ) 146 | 147 | task = cls.client.post(url, **params) 148 | return task 149 | -------------------------------------------------------------------------------- /ovhcli/modules/webhosting/utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf8 -*- 2 | 3 | from terminaltables import AsciiTable 4 | 5 | 6 | def display_services(services): 7 | """Display the user services.""" 8 | data = [['Services']] 9 | 10 | for service in sorted(services): 11 | data.append([service]) 12 | 13 | table = AsciiTable(data) 14 | 15 | return table.table 16 | 17 | 18 | def display_quota(quota): 19 | """Display the quota for a service.""" 20 | used = '{} {}'.format( 21 | "%.2f" % quota['used']['value'], 22 | quota['used']['unit'] 23 | ) 24 | size = '{} {}'.format( 25 | "%.2f" % quota['size']['value'], 26 | quota['size']['unit'] 27 | ) 28 | 29 | table = AsciiTable([["Used", used], ["Size", size]]) 30 | 31 | return table.table 32 | 33 | 34 | def display_users(users): 35 | """Display the list of users by their username.""" 36 | data = [['Users']] 37 | 38 | for user in sorted(users): 39 | data.append([user]) 40 | 41 | table = AsciiTable(data) 42 | 43 | return table.table 44 | 45 | 46 | def display_full_users(users): 47 | """Display the list of users with all information.""" 48 | data = [['Login', 'Home', 'State', 'Ssh', 'Primary account']] 49 | 50 | # Sort the list of users by login 51 | users = sorted(users, key=lambda k: k['login']) 52 | 53 | for user in users: 54 | data.append([ 55 | user['login'], 56 | user['home'], 57 | user['state'], 58 | user['sshState'], 59 | user['isPrimaryAccount'] 60 | ]) 61 | 62 | table = AsciiTable(data) 63 | 64 | return table.table 65 | 66 | 67 | def display_config(configs): 68 | """Display the .ovhconfig file information.""" 69 | data = [['#ID', 'Environment', 'Engine version', 'Container', 'Path', 70 | 'Engine', 'Firewall']] 71 | 72 | for config in configs: 73 | data.append([ 74 | config['id'], 75 | config['environment'], 76 | config['engineVersion'], 77 | config['container'], 78 | config['path'], 79 | config['engineName'], 80 | config['httpFirewall'] 81 | ]) 82 | 83 | table = AsciiTable(data) 84 | 85 | return table.table 86 | -------------------------------------------------------------------------------- /ovhcli/output.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf8 -*- 2 | 3 | import json 4 | 5 | from terminaltables import AsciiTable 6 | 7 | 8 | class Output: 9 | """ 10 | This module can be used to convert a simple JSON value in a pretty table 11 | representation. 12 | 13 | Two representations can be converted : a ``plain`` JSON or a ``list`` : 14 | 15 | >>> from ovhcli.output import Output 16 | >>> output = Output({'username': 'john', 'country': 'US'}) 17 | >>> print(output.convert()) 18 | +----------+-------+ 19 | | Property | Value | 20 | +----------+-------+ 21 | | country | US | 22 | | username | john | 23 | +----------+-------+ 24 | >>> output = Output([{'username': 'john', 'country': 'US'}, 25 | ... {'username': 'nico', 'country': 'FR'}]) 26 | >>> print(output.convert()) 27 | +---------+----------+ 28 | | country | username | 29 | +---------+----------+ 30 | | US | john | 31 | | FR | nico | 32 | +---------+----------+ 33 | """ 34 | def __init__(self, data, exclude=[], sort=None): 35 | self._data = data 36 | self.exclude = exclude 37 | self.sort = sort 38 | self._json = None 39 | 40 | @property 41 | def json(self): 42 | if not self._json: 43 | self._json = self._data 44 | 45 | return self._json 46 | 47 | def iter_list(self, data): 48 | """Iterate over a list data.""" 49 | 50 | def get_dict_value(l, key): 51 | try: 52 | value = l[key] 53 | except KeyError: 54 | return '' 55 | 56 | if not value and not isinstance(value, bool): 57 | return '' 58 | elif (isinstance(value, list)) and len(value) == 0: 59 | return '' 60 | elif isinstance(value, list): 61 | return value 62 | elif isinstance(value, dict): 63 | return json.dumps(value, sort_keys=True, ensure_ascii=False) 64 | 65 | return str(value) 66 | 67 | # Get all headers 68 | headers = sorted( 69 | list({k for d in data for k in d.keys() if k not in self.exclude}) 70 | ) 71 | 72 | # Sort the data 73 | if self.sort: 74 | reverse = False 75 | 76 | if self.sort.startswith('-'): 77 | reverse = True 78 | self.sort = self.sort[1:] 79 | 80 | data = sorted(data, key=lambda k: k[self.sort], reverse=reverse) 81 | 82 | # Convert the data in rows 83 | rows = [[get_dict_value(row, header) for header in headers] 84 | for row in data] 85 | 86 | rows.insert(0, headers) 87 | 88 | table = AsciiTable(rows) 89 | return table.table 90 | 91 | def iter_plain(self, data): 92 | """Iterate over a plain data.""" 93 | 94 | def get_value(value): 95 | if not value and not isinstance(value, bool): 96 | return '' 97 | elif (isinstance(value, list)) and len(value) == 0: 98 | return '' 99 | elif isinstance(value, list): 100 | return value 101 | elif isinstance(value, dict): 102 | return json.dumps(value, sort_keys=True, ensure_ascii=False) 103 | 104 | return str(value) 105 | 106 | d = [[k, get_value(v)] 107 | for k, v in sorted(data.items()) 108 | if k not in self.exclude] 109 | 110 | d.insert(0, ['Property', 'Value']) 111 | 112 | table = AsciiTable(d) 113 | 114 | return table.table 115 | 116 | def convert(self): 117 | """Dispatch the conversion if the JSON value is an instance of list 118 | or an instance of dict.""" 119 | if isinstance(self.json, list): 120 | return self.iter_list(self.json) 121 | 122 | if isinstance(self.json, dict): 123 | return self.iter_plain(self.json) 124 | 125 | return self.json 126 | -------------------------------------------------------------------------------- /ovhcli/settings.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf8 -*- 2 | 3 | import os 4 | 5 | 6 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 7 | MODULES_FOLDER = os.path.abspath( 8 | os.path.join(os.path.dirname(__file__), 'modules') 9 | ) 10 | 11 | CONFIG_PATH = os.path.join(os.path.expanduser("~"), '.ovh.conf') 12 | CONFIG_TEMPLATE = '''[default] 13 | endpoint={ENDPOINT} 14 | 15 | [{ENDPOINT}] 16 | application_key={AK} 17 | application_secret={AS} 18 | consumer_key={CK} 19 | 20 | [ovh-cli] 21 | ''' 22 | 23 | CREATE_TOKEN_LINK = 'https://api.ovh.com/createToken/index.cgi?GET=/*&' \ 24 | 'POST=/*&PUT=/*&DELETE=/*' 25 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | -r requirements.txt 2 | mock==2.0.0 -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | ovh==0.4.5 2 | click==6.6 3 | colorama==0.3.7 4 | terminaltables==3.0.0 -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | import ovhcli 4 | 5 | 6 | entry_points = { 7 | 'console_scripts': [ 8 | 'ovh=ovhcli.cli:cli', 9 | ] 10 | } 11 | 12 | requirements = open('requirements.txt').read() 13 | 14 | readme = open('README.rst').read() 15 | 16 | setup( 17 | name="ovhcli", 18 | version=ovhcli.__version__, 19 | url='http://github.com/ovh/ovh-cli', 20 | author='Nicolas Crocfer', 21 | author_email='nicolas.crocfer@corp.ovh.com', 22 | description="OVH Command Line Interface", 23 | long_description=readme, 24 | packages=['ovhcli'], 25 | include_package_data=True, 26 | install_requires=requirements, 27 | entry_points=entry_points, 28 | classifiers=( 29 | 'Development Status :: 5 - Production/Stable', 30 | 'Environment :: Console', 31 | 'Natural Language :: English', 32 | 'Intended Audience :: Developers', 33 | 'License :: OSI Approved :: MIT License', 34 | 'Operating System :: OS Independent', 35 | 'Programming Language :: Python', 36 | 'Programming Language :: Python :: 2', 37 | 'Programming Language :: Python :: 2.7', 38 | 'Programming Language :: Python :: 3', 39 | 'Programming Language :: Python :: 3.3', 40 | 'Programming Language :: Python :: 3.4' 41 | ), 42 | ) 43 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf8 -*- 2 | -------------------------------------------------------------------------------- /tests/base.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf8 -*- 2 | 3 | import unittest 4 | try: 5 | from unittest import mock 6 | except ImportError: 7 | import mock 8 | 9 | # Used in the tests 10 | from mock import patch 11 | 12 | from click.testing import CliRunner 13 | 14 | 15 | class CliTestCase(unittest.TestCase): 16 | 17 | maxDiff = None 18 | 19 | def setUp(self): 20 | # Mock the OVH client 21 | patcher = mock.patch('ovhcli.context.OvhContext.get_ovh_client') 22 | self.addCleanup(patcher.stop) 23 | self.mock_client = patcher.start() 24 | 25 | self.runner = CliRunner() 26 | -------------------------------------------------------------------------------- /tests/commands/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- -------------------------------------------------------------------------------- /tests/commands/test_me.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from ovhcli.modules.me import commands 4 | from ovhcli.modules.me.controllers import Me 5 | 6 | from ..base import CliTestCase 7 | from ..base import patch 8 | from ..fixtures.me import (info, get_applications, get_application, 9 | get_credentials, get_rules) 10 | 11 | 12 | class MeTest(CliTestCase): 13 | 14 | @patch.object(Me, 'info') 15 | def test_show_info(self, mock): 16 | mock.return_value = info() 17 | result = self.runner.invoke(commands.info) 18 | self.assertEqual(result.output, """\ 19 | [*] Welcome here John 20 | +-----------------+-----------------------------------+ 21 | | Property | Value | 22 | +-----------------+-----------------------------------+ 23 | | address | 2 rue Kellermann | 24 | | area | | 25 | | birthCity | | 26 | | birthDay | | 27 | | city | Roubaix | 28 | | corporationType | | 29 | | country | FR | 30 | | currency | {"code": "EUR", "symbol": "EURO"} | 31 | | email | john@doe.com | 32 | | fax | | 33 | | firstname | John | 34 | | language | fr_FR | 35 | | legalform | individual | 36 | | name | Doe | 37 | | nichandle | dj12345-ovh | 38 | | organisation | | 39 | | ovhCompany | ovh | 40 | | ovhSubsidiary | FR | 41 | | phone | +33.123456789 | 42 | | sex | | 43 | | spareEmail | | 44 | | state | complete | 45 | | vat | | 46 | | zip | 59100 | 47 | +-----------------+-----------------------------------+ 48 | """) 49 | 50 | @patch.object(Me, 'get_applications') 51 | def test_list_applications(self, mock): 52 | mock.return_value = get_applications() 53 | result = self.runner.invoke(commands.applications) 54 | self.assertEqual(result.output, """\ 55 | +---------------+------------------+---------------+----------+--------+ 56 | | applicationId | applicationKey | description | name | status | 57 | +---------------+------------------+---------------+----------+--------+ 58 | | 20003 | 1BAbFJLrfvOr9vu0 | Lorem ipsum 3 | foobar-3 | active | 59 | | 20002 | Cpc4mPw9vdoaLwy0 | Lorem ipsum 2 | foobar-2 | active | 60 | | 20001 | j1sWWzqb1dw0GyUI | Lorem ipsum 1 | foobar-1 | active | 61 | +---------------+------------------+---------------+----------+--------+ 62 | """) 63 | 64 | @patch.object(Me, 'get_application') 65 | def test_show_application(self, mock): 66 | mock.return_value = get_application('20001') 67 | result = self.runner.invoke(commands.application, ['20001']) 68 | self.assertEqual(result.output, """\ 69 | +----------------+------------------+ 70 | | Property | Value | 71 | +----------------+------------------+ 72 | | applicationId | 20001 | 73 | | applicationKey | j1sWWzqb1dw0GyUI | 74 | | description | Lorem ipsum 1 | 75 | | name | foobar-1 | 76 | | status | active | 77 | +----------------+------------------+ 78 | """) 79 | 80 | @patch.object(Me, 'get_credentials') 81 | def test_list_credentials(self, mock): 82 | mock.return_value = get_credentials('20001') 83 | result = self.runner.invoke(commands.credentials, ['20001']) 84 | self.assertEqual(result.output, """\ 85 | +---------------------------+--------------+---------------------------+---------------------------+-----------+ 86 | | creation | credentialId | expiration | lastUse | status | 87 | +---------------------------+--------------+---------------------------+---------------------------+-----------+ 88 | | 2016-08-03T17:52:21+02:00 | 50000002 | 2016-08-04T17:52:21+02:00 | 2016-08-03T17:51:12+02:00 | validated | 89 | | 2016-08-03T17:47:33+02:00 | 50000001 | 2016-08-04T17:47:33+02:00 | 2016-08-03T17:50:23+02:00 | validated | 90 | +---------------------------+--------------+---------------------------+---------------------------+-----------+ 91 | """) 92 | 93 | @patch.object(Me, 'get_rules') 94 | def test_list_rules(self, mock): 95 | mock.return_value = get_rules('50000001') 96 | result = self.runner.invoke(commands.rules, ['50000001']) 97 | self.assertEqual(result.output, """\ 98 | +--------+------+ 99 | | method | path | 100 | +--------+------+ 101 | | GET | /* | 102 | | POST | /* | 103 | | PUT | /* | 104 | | DELETE | /* | 105 | +--------+------+ 106 | """) 107 | -------------------------------------------------------------------------------- /tests/commands/test_setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from ovhcli.modules.setup import commands 4 | 5 | from ..base import CliTestCase 6 | from ..base import patch 7 | 8 | 9 | class SetupTest(CliTestCase): 10 | 11 | @patch('ovhcli.modules.setup.commands.config_file_exists') 12 | def test_conf_already_exists(self, mock): 13 | mock.return_value = True 14 | result = self.runner.invoke(commands.init) 15 | 16 | self.assertEqual(result.output, """\ 17 | [error] A configuration file already exists (use --force to erase it). 18 | """) 19 | 20 | @patch('ovhcli.modules.setup.commands.config_file_exists') 21 | @patch('ovhcli.modules.setup.commands.click') 22 | def test_erase_conf_file(self, mock_file_exists, mock_click): 23 | mock_file_exists.return_value = True 24 | mock_click.prompt.side_effect = exit 25 | result = self.runner.invoke(commands.init, ['--force']) 26 | 27 | self.assertTrue(result.output.startswith("""\ 28 | [warning] The configuration file will be erased. 29 | """)) 30 | 31 | @patch('ovhcli.modules.setup.utils.CONFIG_PATH', './testing.conf') 32 | def test_setup_1(self): 33 | inputs = '1\novh-eu\nCHOICE1_AK\nCHOICE1_AS\nCHOICE1_CK\n' 34 | 35 | with self.runner.isolated_filesystem(): 36 | result = self.runner.invoke(commands.init, input=inputs) 37 | self.assertEqual(result.output, """\ 38 | {welcome} 39 | Your choice [1]: 1 40 | 41 | Endpoint [ovh-eu]: ovh-eu 42 | Application key: CHOICE1_AK 43 | Application secret: CHOICE1_AS 44 | Consumer key: CHOICE1_CK 45 | [*] Configuration file created. 46 | """.format(welcome=commands.WELCOME_MESSAGE)) 47 | 48 | with open('./testing.conf', 'r') as f: 49 | self.assertEqual(f.read(), """\ 50 | [default] 51 | endpoint=ovh-eu 52 | 53 | [ovh-eu] 54 | application_key=CHOICE1_AK 55 | application_secret=CHOICE1_AS 56 | consumer_key=CHOICE1_CK 57 | 58 | [ovh-cli] 59 | """) 60 | 61 | @patch('ovhcli.modules.setup.utils.CONFIG_PATH', './testing.conf') 62 | @patch('ovhcli.modules.setup.utils.get_ck_validation') 63 | def test_setup_2(self, mock_ck): 64 | mock_ck.return_value = {'validationUrl': 'https://eu.api.ovh.com/auth/?credentialToken=IwUoqGPCh2x0aAyTwkJN1vngnVv4RLHepdILexvmx2it6jMNAyL2yeBTfNQ7Hv0b', 65 | 'consumerKey': 'CHOICE2_CK'} 66 | 67 | inputs = '2\novh-eu\nCHOICE2_AK\nCHOICE2_AS\nc\n' 68 | 69 | with self.runner.isolated_filesystem(): 70 | result = self.runner.invoke(commands.init, input=inputs) 71 | self.assertEqual(result.output, """\ 72 | {welcome} 73 | Your choice [1]: 2 74 | 75 | Endpoint [ovh-eu]: ovh-eu 76 | Application key: CHOICE2_AK 77 | Application secret: CHOICE2_AS 78 | 79 | [-] Please visit the following link to authenticate you and validate the token : 80 | [-] https://eu.api.ovh.com/auth/?credentialToken=IwUoqGPCh2x0aAyTwkJN1vngnVv4RLHepdILexvmx2it6jMNAyL2yeBTfNQ7Hv0b 81 | [*] Configuration file created. 82 | """.format(welcome=commands.WELCOME_MESSAGE)) 83 | 84 | with open('./testing.conf', 'r') as f: 85 | self.assertEqual(f.read(), """\ 86 | [default] 87 | endpoint=ovh-eu 88 | 89 | [ovh-eu] 90 | application_key=CHOICE2_AK 91 | application_secret=CHOICE2_AS 92 | consumer_key=CHOICE2_CK 93 | 94 | [ovh-cli] 95 | """) 96 | 97 | @patch('ovhcli.modules.setup.utils.CONFIG_PATH', './testing.conf') 98 | def test_setup_3(self): 99 | inputs = '3\novh-eu\nCHOICE3_AK\nCHOICE3_AS\nCHOICE3_CK\n' 100 | 101 | with self.runner.isolated_filesystem(): 102 | result = self.runner.invoke(commands.init, input=inputs) 103 | self.assertEqual(result.output, """\ 104 | {welcome} 105 | Your choice [1]: 3 106 | 107 | [-] Please visit the following link to authenticate you and obtain your keys (AK, AS and CK) : 108 | [-] https://api.ovh.com/createToken/index.cgi?GET=/*&POST=/*&PUT=/*&DELETE=/* 109 | 110 | Endpoint [ovh-eu]: ovh-eu 111 | Application key: CHOICE3_AK 112 | Application secret: CHOICE3_AS 113 | Consumer key: CHOICE3_CK 114 | [*] Configuration file created. 115 | """.format(welcome=commands.WELCOME_MESSAGE)) 116 | 117 | with open('./testing.conf', 'r') as f: 118 | self.assertEqual(f.read(), """\ 119 | [default] 120 | endpoint=ovh-eu 121 | 122 | [ovh-eu] 123 | application_key=CHOICE3_AK 124 | application_secret=CHOICE3_AS 125 | consumer_key=CHOICE3_CK 126 | 127 | [ovh-cli] 128 | """) 129 | -------------------------------------------------------------------------------- /tests/commands/test_webhosting.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from ovhcli.modules.webhosting import commands 4 | from ovhcli.modules.webhosting.controllers import Webhosting 5 | 6 | from ..base import CliTestCase 7 | from ..base import patch 8 | from ..fixtures.webhosting import list as _list, info, get_users, config 9 | 10 | 11 | class WebhostingTest(CliTestCase): 12 | 13 | @patch.object(Webhosting, 'list') 14 | def test_list_services(self, mock): 15 | mock.return_value = _list() 16 | result = self.runner.invoke(commands.list) 17 | self.assertEqual(result.output, """\ 18 | +-------------+ 19 | | Services | 20 | +-------------+ 21 | | johndoe.ovh | 22 | | mydomain.fr | 23 | | ovh.com | 24 | +-------------+ 25 | """) 26 | 27 | @patch.object(Webhosting, 'info') 28 | def test_show_service(self, mock): 29 | mock.return_value = info() 30 | result = self.runner.invoke(commands.info, ['mydomain.fr']) 31 | self.assertEqual(result.output, """\ 32 | +---------------------+-------------------------------------------+ 33 | | Property | Value | 34 | +---------------------+-------------------------------------------+ 35 | | availableBoostOffer | | 36 | | boostOffer | | 37 | | cluster | cluster006 | 38 | | clusterIp | 213.186.33.17 | 39 | | clusterIpv6 | 2001:41d0:1:1b00:213:186:33:17 | 40 | | datacenter | p19 | 41 | | displayName | | 42 | | filer | 2049 | 43 | | hasCdn | False | 44 | | hasHostedSsl | False | 45 | | home | /homez.2049/johndoe | 46 | | hostingIp | 213.186.33.17 | 47 | | hostingIpv6 | 2001:41d0:1:1b00:213:184:33:14 | 48 | | offer | perso2014 | 49 | | operatingSystem | linux | 50 | | primaryLogin | johndoe | 51 | | quotaSize | {"unit": "GB", "value": 100} | 52 | | quotaUsed | {"unit": "MB", "value": 37.4374389648438} | 53 | | recommendedOffer | | 54 | | resourceType | shared | 55 | | serviceName | mydomain.fr | 56 | | state | active | 57 | | token | NWdPiouPiouSiDeuEy/2Pg | 58 | | trafficQuotaSize | | 59 | | trafficQuotaUsed | | 60 | +---------------------+-------------------------------------------+ 61 | """) 62 | 63 | @patch.object(Webhosting, 'info') 64 | def test_show_quota(self, mock): 65 | mock.return_value = info() 66 | result = self.runner.invoke(commands.quota, ['mydomain.fr']) 67 | self.assertEqual(result.output, """\ 68 | +------+-----------+ 69 | | Used | 37.44 MB | 70 | +------+-----------+ 71 | | Size | 100.00 GB | 72 | +------+-----------+ 73 | """) 74 | 75 | @patch.object(Webhosting, 'info') 76 | def test_list_countries(self, mock): 77 | mock.return_value = info() 78 | result = self.runner.invoke(commands.countries, ['mydomain.fr']) 79 | self.assertEqual(result.output, """\ 80 | +---------+----------------+---------------------------------+ 81 | | country | ip | ipv6 | 82 | +---------+----------------+---------------------------------+ 83 | | CZ | 94.23.175.17 | 2001:41d0:1:1b00:94:23:175:17 | 84 | | DE | 87.98.247.17 | 2001:41d0:1:1b00:87:98:247:17 | 85 | | ES | 87.98.231.17 | 2001:41d0:1:1b00:87:98:231:17 | 86 | | FI | 188.165.143.17 | 2001:41d0:1:1b00:188:165:143:17 | 87 | | FR | 213.186.33.17 | 2001:41d0:1:1b00:213:186:33:17 | 88 | | IE | 188.165.7.17 | 2001:41d0:1:1b00:188:165:7:17 | 89 | | IT | 94.23.64.17 | 2001:41d0:1:1b00:94:23:64:17 | 90 | | LT | 188.165.31.17 | 2001:41d0:1:1b00:188:165:31:17 | 91 | | NL | 94.23.151.17 | 2001:41d0:1:1b00:94:23:151:17 | 92 | | PL | 87.98.239.17 | 2001:41d0:1:1b00:87:98:239:17 | 93 | | PT | 94.23.79.17 | 2001:41d0:1:1b00:94:23:79:17 | 94 | | UK | 87.98.255.17 | 2001:41d0:1:1b00:87:98:255:17 | 95 | +---------+----------------+---------------------------------+ 96 | """) 97 | 98 | @patch.object(Webhosting, 'get_users') 99 | def test_list_users(self, mock): 100 | mock.return_value = get_users() 101 | result = self.runner.invoke(commands.get_users, ['mydomain.fr']) 102 | self.assertEqual(result.output, """\ 103 | +-------------+ 104 | | Users | 105 | +-------------+ 106 | | johndoe | 107 | | johndoe-foo | 108 | +-------------+ 109 | """) 110 | 111 | @patch.object(Webhosting, 'get_users') 112 | def test_list_full_users(self, mock): 113 | mock.return_value = get_users(full=True) 114 | args = ['mydomain.fr', '--full'] 115 | result = self.runner.invoke(commands.get_users, args) 116 | self.assertEqual(result.output, """\ 117 | +-------------+------+-------+--------+-----------------+ 118 | | Login | Home | State | Ssh | Primary account | 119 | +-------------+------+-------+--------+-----------------+ 120 | | johndoe | . | rw | none | False | 121 | | johndoe-foo | foo | rw | active | False | 122 | +-------------+------+-------+--------+-----------------+ 123 | """) 124 | 125 | @patch.object(Webhosting, 'config') 126 | def test_list_config(self, mock): 127 | mock.return_value = config() 128 | result = self.runner.invoke(commands.config, ['mydomain.fr']) 129 | self.assertEqual(result.output, """\ 130 | +---------+-------------+----------------+-----------+------+--------+----------+ 131 | | #ID | Environment | Engine version | Container | Path | Engine | Firewall | 132 | +---------+-------------+----------------+-----------+------+--------+----------+ 133 | | 2000001 | production | 5.6 | legacy | | php | none | 134 | +---------+-------------+----------------+-----------+------+--------+----------+ 135 | """) 136 | 137 | @patch.object(Webhosting, 'update_config') 138 | def test_update_config(self, mock): 139 | mock.return_value = True 140 | args = ['mydomain.fr', '--confirm'] 141 | result = self.runner.invoke(commands.update_config, args) 142 | self.assertEqual(result.output, """\ 143 | [*] The configuration will be updated in a few seconds. 144 | """) 145 | 146 | @patch.object(Webhosting, 'create_user') 147 | def test_create_user(self, mock): 148 | mock.return_value = True 149 | args = ['mydomain.fr', '-l', 'foobar', '--confirm'] 150 | input = 'password1234\npassword1234\n' 151 | result = self.runner.invoke(commands.create_user, args, input=input) 152 | self.assertEqual('\n'.join( 153 | [s.strip() for s in result.output.split('\n')] 154 | ), """\ 155 | Password: 156 | Repeat for confirmation: 157 | [*] User foobar will be created in a few seconds. 158 | """) 159 | 160 | @patch.object(Webhosting, 'remove_user') 161 | def test_remove_user(self, mock): 162 | mock.return_value = True 163 | args = ['mydomain.fr', '-l', 'foobar', '--confirm'] 164 | result = self.runner.invoke(commands.remove_user, args) 165 | self.assertEqual(result.output, """\ 166 | [*] User foobar will be removed in a few seconds. 167 | """) 168 | 169 | @patch.object(Webhosting, 'update_user') 170 | def test_update_user(self, mock): 171 | mock.return_value = True 172 | args = ['mydomain.fr', '-l', 'foobar', '--confirm'] 173 | result = self.runner.invoke(commands.update_user, args) 174 | self.assertEqual(result.output, """\ 175 | [*] Information about foobar will be updated in a few seconds. 176 | """) 177 | -------------------------------------------------------------------------------- /tests/fixtures/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | -------------------------------------------------------------------------------- /tests/fixtures/me.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | def info(): 5 | return { 6 | 'country': 'FR', 7 | 'firstname': 'John', 8 | 'legalform': 'individual', 9 | 'name': 'Doe', 10 | 'currency': { 11 | 'code': 'EUR', 12 | 'symbol': 'EURO' 13 | }, 14 | 'ovhSubsidiary': 'FR', 15 | 'birthDay': None, 16 | 'organisation': '', 17 | 'spareEmail': None, 18 | 'area': '', 19 | 'phone': '+33.123456789', 20 | 'nationalIdentificationNumber': None, 21 | 'ovhCompany': 'ovh', 22 | 'email': 'john@doe.com', 23 | 'companyNationalIdentificationNumber': None, 24 | 'language': 'fr_FR', 25 | 'fax': '', 26 | 'zip': '59100', 27 | 'nichandle': 'dj12345-ovh', 28 | 'corporationType': None, 29 | 'sex': None, 30 | 'birthCity': None, 31 | 'state': 'complete', 32 | 'city': 'Roubaix', 33 | 'vat': '', 34 | 'address': '2 rue Kellermann' 35 | } 36 | 37 | 38 | def get_applications(): 39 | return [ 40 | { 41 | 'status': 'active', 42 | 'applicationKey': 'j1sWWzqb1dw0GyUI', 43 | 'applicationId': 20001, 44 | 'name': 'foobar-1', 45 | 'description': 'Lorem ipsum 1' 46 | }, { 47 | 'status': 'active', 48 | 'applicationKey': '1BAbFJLrfvOr9vu0', 49 | 'applicationId': 20003, 50 | 'name': 'foobar-3', 51 | 'description': 'Lorem ipsum 3' 52 | }, { 53 | 'status': 'active', 54 | 'applicationKey': 'Cpc4mPw9vdoaLwy0', 55 | 'applicationId': 20002, 56 | 'name': 'foobar-2', 57 | 'description': 'Lorem ipsum 2' 58 | } 59 | ] 60 | 61 | 62 | def get_credentials(app_id): 63 | return [cred for cred in [ 64 | { 65 | 'ovhSupport': False, 66 | 'rules': [ 67 | { 68 | 'method': 'GET', 'path': '/*' 69 | }, { 70 | 'method': 'POST', 'path': '/*' 71 | }, { 72 | 'method': 'PUT', 'path': '/*' 73 | }, { 74 | 'method': 'DELETE', 'path': '/*' 75 | } 76 | ], 77 | 'expiration': '2016-08-04T17:52:21+02:00', 78 | 'status': 'validated', 79 | 'credentialId': 50000002, 80 | 'applicationId': 20001, 81 | 'creation': '2016-08-03T17:52:21+02:00', 82 | 'lastUse': '2016-08-03T17:51:12+02:00' 83 | }, { 84 | 'ovhSupport': True, 85 | 'rules': [ 86 | { 87 | 'method': 'GET', 'path': '/*' 88 | }, { 89 | 'method': 'POST', 'path': '/*' 90 | }, { 91 | 'method': 'PUT', 'path': '/*' 92 | }, { 93 | 'method': 'DELETE', 'path': '/*' 94 | } 95 | ], 96 | 'expiration': '2016-08-04T17:47:33+02:00', 97 | 'status': 'validated', 98 | 'credentialId': 50000001, 99 | 'applicationId': 20001, 100 | 'creation': '2016-08-03T17:47:33+02:00', 101 | 'lastUse': '2016-08-03T17:50:23+02:00' 102 | } 103 | ] if cred['applicationId'] == int(app_id)] 104 | 105 | 106 | def get_application(app_id): 107 | return next((app for app in get_applications() 108 | if app['applicationId'] == int(app_id))) 109 | 110 | 111 | def get_credential(credential_id): 112 | return next((app for app in get_credentials('20001') 113 | if app['credentialId'] == int(credential_id))) 114 | 115 | 116 | def get_rules(credential_id): 117 | return next((app for app in get_credentials('20001') 118 | if app['credentialId'] == int(credential_id)))['rules'] 119 | -------------------------------------------------------------------------------- /tests/fixtures/output.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | PLAIN_SIMPLE = {'name': 'john'} 4 | PLAIN_NULL = {'foobar': None} 5 | PLAIN_LIST = {'foobar': ['foo', 'bar']} 6 | PLAIN_EMPTY_LIST = {'foobar': []} 7 | PLAIN_DICT = {'foobar': {'foo': 'bar'}} 8 | PLAIN_MULTIPLE = {'name': 'john', 'age': 45, 'city': 'Lille', 'country': 'FR'} 9 | 10 | LIST_SIMPLE = [{'key1': 'foo1', 'key2': 'foo2'}, {'key1': 'bar1', 'key2': 'bar2'}] 11 | LIST_TO_SORT = [{'key': 'a'}, {'key': 'c'}, {'key': 'd'}, {'key': 'b'}] 12 | LIST_MISSING_FIELDS = [{'key1': 'foo1'}, {'key2': 'bar2'}] 13 | LIST_NULL = [{'key': 'foo'}, {'key': None}] 14 | LIST_EMPTY_LIST = [{'key': 'foo'}, {'key': []}] 15 | LIST_NON_EMPTY_LIST = [{'key': 'foo'}, {'key': ['foo', 'bar']}] 16 | LIST_DICT = [{'key': 'foo'}, {'key': {'foo': 'bar'}}] -------------------------------------------------------------------------------- /tests/fixtures/setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | FAKE_ENDPOINTS = { 4 | 'ENDPOINT1': 'http://foo.bar', 5 | 'ENDPOINT2': 'http://foo.bar', 6 | } 7 | -------------------------------------------------------------------------------- /tests/fixtures/webhosting.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | def list(): 5 | return [ 6 | 'ovh.com', 7 | 'mydomain.fr', 8 | 'johndoe.ovh' 9 | ] 10 | 11 | 12 | def info(): 13 | return { 14 | 'quotaUsed': { 15 | 'unit': 'MB', 16 | 'value': 37.4374389648438 17 | }, 18 | 'cluster': 'cluster006', 19 | 'boostOffer': None, 20 | 'displayName': None, 21 | 'clusterIpv6': '2001:41d0:1:1b00:213:186:33:17', 22 | 'datacenter': 'p19', 23 | 'token': 'NWdPiouPiouSiDeuEy/2Pg', 24 | 'resourceType': 'shared', 25 | 'recommendedOffer': None, 26 | 'trafficQuotaSize': None, 27 | 'availableBoostOffer': [], 28 | 'home': '/homez.2049/johndoe', 29 | 'trafficQuotaUsed': None, 30 | 'hasCdn': False, 31 | 'filer': '2049', 32 | 'serviceName': 'mydomain.fr', 33 | 'offer': 'perso2014', 34 | 'hostingIpv6': '2001:41d0:1:1b00:213:184:33:14', 35 | 'primaryLogin': 'johndoe', 36 | 'state': 'active', 37 | 'countriesIp': [ 38 | { 39 | 'ipv6': '2001:41d0:1:1b00:188:165:7:17', 40 | 'ip': '188.165.7.17', 41 | 'country': 'IE' 42 | }, { 43 | 'ipv6': '2001:41d0:1:1b00:94:23:79:17', 44 | 'ip': '94.23.79.17', 45 | 'country': 'PT' 46 | }, { 47 | 'ipv6': '2001:41d0:1:1b00:87:98:255:17', 48 | 'ip': '87.98.255.17', 49 | 'country': 'UK' 50 | }, { 51 | 'ipv6': '2001:41d0:1:1b00:94:23:64:17', 52 | 'ip': '94.23.64.17', 53 | 'country': 'IT' 54 | }, { 55 | 'ipv6': '2001:41d0:1:1b00:87:98:231:17', 56 | 'ip': '87.98.231.17', 57 | 'country': 'ES' 58 | }, { 59 | 'ipv6': '2001:41d0:1:1b00:87:98:239:17', 60 | 'ip': '87.98.239.17', 61 | 'country': 'PL' 62 | }, { 63 | 'ipv6': '2001:41d0:1:1b00:94:23:175:17', 64 | 'ip': '94.23.175.17', 65 | 'country': 'CZ' 66 | }, { 67 | 'ipv6': '2001:41d0:1:1b00:94:23:151:17', 68 | 'ip': '94.23.151.17', 69 | 'country': 'NL' 70 | }, { 71 | 'ipv6': '2001:41d0:1:1b00:188:165:143:17', 72 | 'ip': '188.165.143.17', 73 | 'country': 'FI' 74 | }, { 75 | 'ipv6': '2001:41d0:1:1b00:188:165:31:17', 76 | 'ip': '188.165.31.17', 77 | 'country': 'LT' 78 | }, { 79 | 'ipv6': '2001:41d0:1:1b00:213:186:33:17', 80 | 'ip': '213.186.33.17', 81 | 'country': 'FR' 82 | }, { 83 | 'ipv6': '2001:41d0:1:1b00:87:98:247:17', 84 | 'ip': '87.98.247.17', 85 | 'country': 'DE' 86 | } 87 | ], 88 | 'hasHostedSsl': False, 89 | 'operatingSystem': 'linux', 90 | 'phpVersions': [ 91 | { 92 | 'version': '5.3', 93 | 'support': 'END_OF_LIFE' 94 | }, { 95 | 'version': '5.5', 96 | 'support': 'SECURITY_FIXES' 97 | }, { 98 | 'version': '5.2', 99 | 'support': 'END_OF_LIFE' 100 | }, { 101 | 'version': '7.0', 102 | 'support': 'SUPPORTED' 103 | }, { 104 | 'version': '5.4', 105 | 'support': 'END_OF_LIFE' 106 | }, { 107 | 'version': '5.6', 108 | 'support': 'SUPPORTED' 109 | }, { 110 | 'version': '4.4', 111 | 'support': 'END_OF_LIFE' 112 | } 113 | ], 114 | 'quotaSize': { 115 | 'unit': 'GB', 116 | 'value': 100 117 | }, 118 | 'clusterIp': '213.186.33.17', 119 | 'hostingIp': '213.186.33.17' 120 | } 121 | 122 | 123 | def get_users(full=False): 124 | if full: 125 | return [ 126 | { 127 | 'iisRemoteRights': None, 128 | 'webDavRights': None, 129 | 'isPrimaryAccount': False, 130 | 'home': 'foo', 131 | 'sshState': 'active', 132 | 'state': 'rw', 133 | 'login': 'johndoe-foo' 134 | }, { 135 | 'iisRemoteRights': None, 136 | 'webDavRights': None, 137 | 'isPrimaryAccount': False, 138 | 'home': '.', 139 | 'sshState': 'none', 140 | 'state': 'rw', 141 | 'login': 'johndoe' 142 | } 143 | ] 144 | 145 | return ['johndoe-foo', 'johndoe'] 146 | 147 | 148 | def config(): 149 | return [ 150 | { 151 | 'path': '', 152 | 'creationDate': '2016-08-04T19:54:48+02:00', 153 | 'httpFirewall': 'none', 154 | 'environment': 'production', 155 | 'id': 2000001, 156 | 'container': 'legacy', 157 | 'fileExist': True, 158 | 'engineVersion': '5.6', 159 | 'engineName': 'php', 160 | 'historical': False 161 | } 162 | ] 163 | -------------------------------------------------------------------------------- /tests/units/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | -------------------------------------------------------------------------------- /tests/units/test_context.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import click 4 | 5 | from ovhcli.cli import pass_ovh 6 | 7 | from ..base import CliTestCase 8 | from ..base import patch 9 | 10 | 11 | class ContextTest(CliTestCase): 12 | 13 | def test_simple_echo(self): 14 | @click.command() 15 | @pass_ovh 16 | def cli(ovh): 17 | ovh.echo('foobar') 18 | 19 | result = self.runner.invoke(cli) 20 | self.assertEqual(result.output, 'foobar\n') 21 | 22 | def test_echo_with_prefix(self): 23 | @click.command() 24 | @pass_ovh 25 | def cli(ovh): 26 | ovh.echo('foobar', prefix='PREFIX') 27 | 28 | result = self.runner.invoke(cli) 29 | self.assertEqual(result.output, '[PREFIX] foobar\n') 30 | 31 | def test_echo_with_json(self): 32 | @click.command() 33 | @pass_ovh 34 | def cli(ovh): 35 | ovh.json = True 36 | ovh.echo('foobar') 37 | 38 | result = self.runner.invoke(cli) 39 | self.assertEqual(result.output, '') 40 | 41 | def test_debug_no_activated(self): 42 | @click.command() 43 | @pass_ovh 44 | def cli(ovh): 45 | ovh.debug('foobar') 46 | 47 | result = self.runner.invoke(cli) 48 | self.assertEqual(result.output, '') 49 | 50 | def test_debug_activated(self): 51 | @click.command() 52 | @pass_ovh 53 | def cli(ovh): 54 | ovh.debug_mode = True 55 | ovh.debug('foobar') 56 | 57 | result = self.runner.invoke(cli) 58 | self.assertEqual(result.output, '[debug] foobar\n') 59 | 60 | def test_success(self): 61 | @click.command() 62 | @pass_ovh 63 | def cli(ovh): 64 | ovh.success('foobar') 65 | 66 | result = self.runner.invoke(cli) 67 | self.assertEqual(result.output, '[*] foobar\n') 68 | 69 | def test_info(self): 70 | @click.command() 71 | @pass_ovh 72 | def cli(ovh): 73 | ovh.info('foobar') 74 | 75 | result = self.runner.invoke(cli) 76 | self.assertEqual(result.output, '[-] foobar\n') 77 | 78 | def test_warning(self): 79 | @click.command() 80 | @pass_ovh 81 | def cli(ovh): 82 | ovh.warning('foobar') 83 | 84 | result = self.runner.invoke(cli) 85 | self.assertEqual(result.output, '[warning] foobar\n') 86 | 87 | def test_error(self): 88 | @click.command() 89 | @pass_ovh 90 | def cli(ovh): 91 | ovh.error('foobar') 92 | 93 | result = self.runner.invoke(cli) 94 | self.assertEqual(result.output, '[error] foobar\n') 95 | 96 | @patch('ovhcli.context.strftime') 97 | def test_time_echo(self, mock_time): 98 | mock_time.return_value = '12:34:56' 99 | 100 | @click.command() 101 | @pass_ovh 102 | def cli(ovh): 103 | ovh.time_echo('foobar') 104 | 105 | result = self.runner.invoke(cli) 106 | self.assertEqual(result.output, '[12:34:56] foobar\n') 107 | 108 | def test_table(self): 109 | @click.command() 110 | @pass_ovh 111 | def cli(ovh): 112 | ovh.table({'username': 'john'}) 113 | 114 | result = self.runner.invoke(cli) 115 | self.assertEqual(result.output, """+----------+-------+ 116 | | Property | Value | 117 | +----------+-------+ 118 | | username | john | 119 | +----------+-------+ 120 | """) 121 | 122 | def test_table_with_json(self): 123 | @click.command() 124 | @pass_ovh 125 | def cli(ovh): 126 | ovh.json = True 127 | ovh.table({'username': 'john'}) 128 | 129 | result = self.runner.invoke(cli) 130 | self.assertEqual(result.output, '{"username": "john"}\n') 131 | 132 | def test_table_with_custom_function(self): 133 | @click.command() 134 | @pass_ovh 135 | def cli(ovh): 136 | def output(data): 137 | return 'Username --> {}'.format(data['username']) 138 | 139 | ovh.table({'username': 'john'}, output) 140 | 141 | result = self.runner.invoke(cli) 142 | self.assertEqual(result.output, 'Username --> john\n') 143 | 144 | def test_display_task(self): 145 | @click.command() 146 | @pass_ovh 147 | def cli(ovh): 148 | ovh.display_task({'function': 'foo', 'status': 'init'}) 149 | ovh.display_task({'function': 'foo', 'status': 'todo'}) 150 | ovh.display_task({'function': 'foo', 'status': 'doing'}) 151 | ovh.display_task({'function': 'foo', 'status': 'done'}) 152 | ovh.display_task({'function': 'foo', 'status': 'cancelled'}) 153 | ovh.display_task({'function': 'foo', 'status': 'bar'}) 154 | 155 | result = self.runner.invoke(cli) 156 | self.assertEqual(result.output, """\ 157 | [*] The task foo has been launched. 158 | [*] The task foo has been launched. 159 | [*] The task foo has been launched. 160 | [*] The task foo is done. 161 | [warning] The task foo has been cancelled. 162 | [error] The task foo fell in an error state. 163 | """) 164 | -------------------------------------------------------------------------------- /tests/units/test_output.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from ovhcli.output import Output 4 | 5 | from ..base import CliTestCase 6 | from ..fixtures import output 7 | 8 | 9 | class OutputTest(CliTestCase): 10 | 11 | def test_plain_simple(self): 12 | out = Output(output.PLAIN_SIMPLE) 13 | self.assertEqual(out.convert(), """+----------+-------+ 14 | | Property | Value | 15 | +----------+-------+ 16 | | name | john | 17 | +----------+-------+""") 18 | 19 | def test_plain_with_null(self): 20 | out = Output(output.PLAIN_NULL) 21 | self.assertEqual(out.convert(), """+----------+-------+ 22 | | Property | Value | 23 | +----------+-------+ 24 | | foobar | | 25 | +----------+-------+""") 26 | 27 | def test_plain_with_list(self): 28 | out = Output(output.PLAIN_LIST) 29 | self.assertEqual(out.convert(), """+----------+----------------+ 30 | | Property | Value | 31 | +----------+----------------+ 32 | | foobar | ['foo', 'bar'] | 33 | +----------+----------------+""") 34 | 35 | def test_plain_with_empty_list(self): 36 | out = Output(output.PLAIN_EMPTY_LIST) 37 | self.assertEqual(out.convert(), """+----------+-------+ 38 | | Property | Value | 39 | +----------+-------+ 40 | | foobar | | 41 | +----------+-------+""") 42 | 43 | def test_plain_with_dict(self): 44 | out = Output(output.PLAIN_DICT) 45 | self.assertEqual(out.convert(), """+----------+----------------+ 46 | | Property | Value | 47 | +----------+----------------+ 48 | | foobar | {"foo": "bar"} | 49 | +----------+----------------+""") 50 | 51 | def test_plain_multiple(self): 52 | out = Output(output.PLAIN_MULTIPLE) 53 | self.assertEqual(out.convert(), """+----------+-------+ 54 | | Property | Value | 55 | +----------+-------+ 56 | | age | 45 | 57 | | city | Lille | 58 | | country | FR | 59 | | name | john | 60 | +----------+-------+""") 61 | 62 | def test_plain_multiple_with_excludes(self): 63 | out = Output(output.PLAIN_MULTIPLE, exclude=['city', 'age']) 64 | self.assertEqual(out.convert(), """+----------+-------+ 65 | | Property | Value | 66 | +----------+-------+ 67 | | country | FR | 68 | | name | john | 69 | +----------+-------+""") 70 | 71 | def test_list_simple(self): 72 | out = Output(output.LIST_SIMPLE) 73 | self.assertEqual(out.convert(), """+------+------+ 74 | | key1 | key2 | 75 | +------+------+ 76 | | foo1 | foo2 | 77 | | bar1 | bar2 | 78 | +------+------+""") 79 | 80 | def test_list_multiple_with_excludes(self): 81 | out = Output(output.LIST_SIMPLE, exclude=['key2']) 82 | self.assertEqual(out.convert(), """+------+ 83 | | key1 | 84 | +------+ 85 | | foo1 | 86 | | bar1 | 87 | +------+""") 88 | 89 | def test_list_sort(self): 90 | out = Output(output.LIST_TO_SORT, sort='key') 91 | self.assertEqual(out.convert(), """+-----+ 92 | | key | 93 | +-----+ 94 | | a | 95 | | b | 96 | | c | 97 | | d | 98 | +-----+""") 99 | 100 | def test_list_reverse_sort(self): 101 | out = Output(output.LIST_TO_SORT, sort='-key') 102 | self.assertEqual(out.convert(), """+-----+ 103 | | key | 104 | +-----+ 105 | | d | 106 | | c | 107 | | b | 108 | | a | 109 | +-----+""") 110 | 111 | def test_list_missing_keys(self): 112 | out = Output(output.LIST_MISSING_FIELDS) 113 | self.assertEqual(out.convert(), """+------+------+ 114 | | key1 | key2 | 115 | +------+------+ 116 | | foo1 | | 117 | | | bar2 | 118 | +------+------+""") 119 | 120 | def test_list_with_null(self): 121 | out = Output(output.LIST_NULL) 122 | self.assertEqual(out.convert(), """+-----+ 123 | | key | 124 | +-----+ 125 | | foo | 126 | | | 127 | +-----+""") 128 | 129 | def test_list_with_empty_list(self): 130 | out = Output(output.LIST_EMPTY_LIST) 131 | self.assertEqual(out.convert(), """+-----+ 132 | | key | 133 | +-----+ 134 | | foo | 135 | | | 136 | +-----+""") 137 | 138 | def test_list_with_list(self): 139 | out = Output(output.LIST_NON_EMPTY_LIST) 140 | self.assertEqual(out.convert(), """+----------------+ 141 | | key | 142 | +----------------+ 143 | | foo | 144 | | ['foo', 'bar'] | 145 | +----------------+""") 146 | 147 | def test_list_with_dict(self): 148 | out = Output(output.LIST_DICT) 149 | self.assertEqual(out.convert(), """+----------------+ 150 | | key | 151 | +----------------+ 152 | | foo | 153 | | {"foo": "bar"} | 154 | +----------------+""") 155 | -------------------------------------------------------------------------------- /tests/units/test_setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from click.exceptions import UsageError 4 | 5 | from ovhcli.modules.setup.utils import (check_choice, check_endpoint, 6 | config_file_exists, create_config_file) 7 | 8 | from ..base import CliTestCase 9 | from ..base import patch 10 | from ..fixtures.setup import FAKE_ENDPOINTS 11 | 12 | 13 | class SetupTest(CliTestCase): 14 | 15 | def test_check_valid_choices(self): 16 | self.assertEqual(check_choice('1'), 1) 17 | self.assertEqual(check_choice('2'), 2) 18 | self.assertEqual(check_choice('3'), 3) 19 | 20 | def test_check_invalid_choice(self): 21 | self.assertRaises(UsageError, check_choice, 'foobar') 22 | 23 | @patch('ovhcli.modules.setup.utils.ENDPOINTS', FAKE_ENDPOINTS) 24 | def test_check_valid_endpoints(self): 25 | self.assertEqual(check_endpoint('ENDPOINT1'), 'ENDPOINT1') 26 | self.assertEqual(check_endpoint('ENDPOINT2'), 'ENDPOINT2') 27 | 28 | @patch('ovhcli.modules.setup.utils.ENDPOINTS', FAKE_ENDPOINTS) 29 | def test_check_invalid_endpoint(self): 30 | self.assertRaises(UsageError, check_endpoint, 'foobar') 31 | 32 | with self.assertRaises(UsageError) as cm: 33 | check_endpoint('foobar') 34 | self.assertEqual('This endpoint does not exist (ENDPOINT1, ENDPOINT2)', 35 | str(cm.exception)) 36 | 37 | @patch('ovhcli.modules.setup.utils.CONFIG_PATH', './testing.conf') 38 | def test_check_config_file_exists(self): 39 | with self.runner.isolated_filesystem(): 40 | with open('./testing.conf', 'w') as f: 41 | f.write('foobar') 42 | 43 | self.assertTrue(config_file_exists()) 44 | 45 | @patch('ovhcli.modules.setup.utils.CONFIG_PATH', './testing.conf') 46 | def test_check_config_file_not_exists(self): 47 | self.assertFalse(config_file_exists()) 48 | 49 | @patch('ovhcli.modules.setup.utils.CONFIG_PATH', './testing.conf') 50 | def test_check_create_config_file(self): 51 | with self.runner.isolated_filesystem(): 52 | result = create_config_file('foobar', 'app_key', 'app_secret', 53 | 'consumer_key') 54 | 55 | self.assertTrue(result) 56 | 57 | with open('./testing.conf', 'r') as f: 58 | self.assertEqual(f.read(), """\ 59 | [default] 60 | endpoint=foobar 61 | 62 | [foobar] 63 | application_key=app_key 64 | application_secret=app_secret 65 | consumer_key=consumer_key 66 | 67 | [ovh-cli] 68 | """) 69 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py27,py33,py34,py35 3 | 4 | [testenv] 5 | deps=-rrequirements-dev.txt 6 | commands = python -m unittest discover 7 | --------------------------------------------------------------------------------