├── .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 |
--------------------------------------------------------------------------------