Revision 16d7b9ff

b/docs/commands.rst
143 143

  
144 144
.. code-block:: text
145 145

  
146
    addr    :  List a server's nic address
146
    addr    :  List a server nic address
147 147
    console :  Get a VNC console
148 148
    create  :  Create a server
149 149
    delete  :  Delete a server
150
    firewall:  Manage server's firewall profile
151
        set :  Set the server's firewall profile
152
        get :  Get the server's firewall profile
150
    firewall:  Manage server firewall profile
151
        set :  Set the server firewall profile
152
        get :  Get the server firewall profile
153 153
    ip      :  Manage floating IPs for the servers
154 154
        attach:  Attach a floating ip to a server with server_id
155
        info  :  A floating IPs' details
155
        info  :  A floating IP details
156 156
        detach:  Detach floating ip from server
157 157
        list  :  List all floating ips
158 158
        create:  Create a new floating IP
......
165 165
        set   :  Add / update server metadata
166 166
        delete:  Delete a piece of server metadata
167 167
    reboot  :  Reboot a server
168
    rename  :  Update a server's name
168
    rename  :  Update a server name
169 169
    shutdown:  Shutdown a server
170 170
    start   :  Start a server
171 171
    stats   :  Get server statistics
......
445 445
    -rw-rw-r-- 1 ******** ******** 20M Nov 26 15:42 rndm_remote.file
446 446
    [file]: !diff rndm_local.file rndm_remote.file
447 447

  
448
.. Note:: In kamaki shell, ! is used to execute OS shell commands (e.g. bash)
448
.. Note:: In kamaki shell, ! is used to execute OS shell commands (e.g., bash)
449 449

  
450 450
.. warning:: The container:object/path syntax does not function if the
451 451
    container and / or the object path contain one or more : characters. To use
452 452
    containers and objects with : use the --container and --dst-container
453
    arguments, e.g. to copy test.py object from example:dev container to
453
    arguments, e.g., to copy test.py object from example:dev container to
454 454
    example:deploy ::
455 455

  
456 456
        $ kamaki file copy --container=example:dev test.py --dst-container=example:deploy
b/docs/developers/adding-commands.rst
2 2
===============
3 3

  
4 4
Kamaki commands are implemented as python classes, decorated with a special
5
decorator called *command*. This decorator is a method of kamaki.cli that adds
6
a new command in a CommandTree structure (kamaki.cli.commant_tree). The later
7
is used by interfaces to manage kamaki commands.
5
decorator called *command*. This decorator is a method of *kamaki.cli* that
6
adds a new command in a *CommandTree* structure. A *CommandTree* (package
7
*kamaki.cli.commant_tree*) is a data structure used by kamaki to manage command
8
namespaces.
8 9

  
9
In the following, a set of kamaki commands will be implemented::
10
For demonstration purposes, the following set of kamaki commands will be
11
implemented in this document::
10 12

  
11 13
    mygrp1 list all                         //show a list
12 14
    mygrp1 list details [--match=<>]        //show list of details
13 15
    mygrp2 list all [regular expression] [-l]       //list all subjects
14 16
    mygrp2 info <id> [name]      //get information for subject with id
15 17

  
16
There are two command sets to implement, namely mygrp1 and mygrp2. The first
17
will contain two commands, namely list-all and list-details. The second one
18
will also contain two commands, list-all and info. To avoid ambiguities,
19
command names should rather be prefixed with the group they belong to, e.g.
20
mygrp1-list-all and mygrp2-list-all.
18
There are two command groups to implement i.e., *mygrp1* and *mygrp2*,
19
containing two commands each (*list_all*, *list_details* and *list_all*, *info*
20
respectively). To avoid ambiguities, command names are prefixed with the
21
command group they belong to, e.g., *mygrp1_list_all* and *mygrp2_list_all*.
22
The underscore is used to separate command namespaces.
21 23

  
22
The first command has the simplest possible syntax: no parameters, no runtime
23
arguments. The second accepts an optional runtime argument with a value. The
24
third features an optional argument and an optional runtime flag argument. The
25
last is an example of a command with an obligatory and an optional argument.
24
The first command (*mygrp1_list_all*) has the simplest possible syntax: no
25
parameters, no runtime arguments. The second accepts an optional runtime argument with a value. The third features an optional parameter and an optional
26
runtime flag argument. The last is an example of a command with an obligatory
27
and an optional parameter.
26 28

  
27
Samples of the expected behavior in one-command mode are following:
29
Examples of the expected behavior in one-command mode:
28 30

  
29 31
.. code-block:: console
30 32

  
......
35 37
         - - - -
36 38
        list
37 39
    $ kamaki mygrp1 list
38
        Syntax Error
39 40

  
40 41
        Options
41 42
         - - - -
42 43
        all        show a list
43 44
        details     show a list of details
44 45
    $ kamaki mygrp1 list all
45
        ... (mygrp1 client method is called) ...
46
        ... (a mygrp1_list_all instance runs) ...
46 47
    $ kamaki mygrp2 list all 'Z[.]' -l
47
        ... (mygrp2 client method is called) ...
48
        ... (a mygrp2_list_all instance runs) ...
48 49
    $
49 50

  
50
The above example will be used throughout the present guide.
51

  
52 51
The CommandTree structure
53 52
-------------------------
54 53

  
55
CommandTree manages a command by its path. Each command is stored in multiple
56
nodes on the tree, so that the last term is a leaf and the route from root to
57
that leaf represents the command path. For example the commands *file upload*,
58
*file list* and *file info* are stored together as shown bellow::
54
CommandTree manages a command by its namespace. Each command is stored in
55
a tree path, where each node is a name. A leaf is the end term of a namespace and contains a pointer to the command class to be executed.
56

  
57
Here is an example from the actual kamaki command structure, where the commands
58
*file upload*, *file list* and *file info* are represented as shown bellow::
59 59

  
60 60
    - file
61 61
    ''''''''|- info
62 62
            |- list
63 63
            |- upload
64 64

  
65
The example used in the present, should result to the creation of two trees::
65
Now, let's load the showcase example on CommandTrees::
66 66

  
67 67
    - mygrp1
68 68
    ''''''''|- list
......
74 74
            '''''''|- all
75 75
            |- info
76 76

  
77
Each command group should be stored on a different CommandTree. For that
78
reason, command specification modules should contain a list of CommandTree
79
objects, named *_commands*
77
Each command group should be stored on a different CommandTree.
78

  
79
For that reason, command specification modules should contain a list of CommandTree objects, named *_commands*. This mechanism allows any interface
80
application to load the list of commands from the *_commands* array.
80 81

  
81
A command group information (name, description) is provided at CommandTree
82
structure initialization:
82
The first name of the command path and a description (name, description) are needed to initializeg a CommandTree:
83 83

  
84 84
.. code-block:: python
85 85

  
......
88 88

  
89 89
    _commands = [_mygrp1_commands, _mygrp2_commands]
90 90

  
91

  
91 92
The command decorator
92 93
---------------------
93 94

  
94
The *command* decorator mines all the information necessary to build a command
95
specification which is then inserted in a CommanTree instance::
95
All commands are specified by subclasses of *kamaki.cli.commands._command_init*
96
These classes are called "command specifications".
97

  
98
The *command* decorator mines all the information needed to build a namespace
99
from a command specification::
96 100

  
97 101
    class code  --->  command()  -->  updated CommandTree structure
98 102

  
99
Kamaki interfaces make use of this CommandTree structure. Optimizations are
103
Kamaki interfaces make use of the CommandTree structure. Optimizations are
100 104
possible by using special parameters on the command decorator method.
101 105

  
102 106
.. code-block:: python
......
108 112

  
109 113
        :param prefix: of the commands allowed to be inserted ('' for all)
110 114

  
111
        :param descedants_depth: is the depth of the tree descedants of the
115
        :param descedants_depth: is the depth of the tree descendants of the
112 116
            prefix command.
113 117
    """
114 118

  
115 119
Creating a new command specification set
116 120
----------------------------------------
117 121

  
118
A command specification developer should create a new module (python file) with as many classes as the command specifications to be offered. Each class should be decorated with *command*.
122
A command specification developer should create a new module (python file) with
123
one command specification class per command. Each class should be decorated
124
with *command*.
119 125

  
120 126
.. code-block:: python
121 127

  
......
129 135
    ...
130 136

  
131 137
A list of CommandTree structures must exist in the module scope, with the name
132
_commands, as shown above. Different CommandTree objects correspond to
133
different command groups.
138
*_commands*. Different CommandTree objects correspond to different command
139
groups.
134 140

  
135
Get command description
141
Set command description
136 142
-----------------------
137 143

  
138 144
The description of each command is the first line of the class commend. The
......
143 149

  
144 150
    ...
145 151
    @command(_mygrp2_commands)
146
    class mygrp2_info()
147
        """get information for subject with id"""
152
    class mygrp2_info():
153
        """get information for subject with id
154
        Anything from this point and bellow constitutes the long description
155
        Please, mind the indentation, pep8 is not forgiving.
156
        """
148 157
        ...
149 158

  
159
Description placeholders
160
------------------------
161

  
162
There is possible to create an empty command, that can act as a description
163
placeholder. For example, the *mygrp1_list* namespace does not correspond to an
164
executable command, but it can have a helpful description. In that case, create
165
a command specification class with a command and no code:
166

  
167
.. code-block:: python
168

  
169
    @command(_mygrp1_commands)
170
    class mygrp1_list():
171
        """List mygrp1 objects.
172
        There are two versions: short and detailed
173
        """
174

  
175
.. warning:: A command specification class with no description is invalid and
176
    will cause an error.
177

  
150 178
Declare run-time argument
151 179
-------------------------
152 180

  
153
The argument mechanism allows the definition of run-time arguments. Some basic
154
argument types are defined at the
181
A special argument mechanism allows the definition of run-time arguments. This
182
mechanism is based on argparse and is designed to simplify argument definitions
183
when specifying commands.
184

  
185
Some basic argument types are defined at the
155 186
`argument module <code.html#module-kamaki.cli.argument>`_, but it is not
156
uncommon to extent these classes in order to achieve specialized type checking
157
and syntax control (e.g. at
158
`pithos cli module <code.html#module-kamaki.cli.commands.pithos>`_).
187
a bad idea to extent these classes in order to achieve specialized type
188
checking and syntax control. Still, in most cases, the argument types of the
189
argument package are enough for most cases.
159 190

  
160
To declare a run-time argument on a specific command, the object class should
161
initialize a dict called *arguments* , where Argument objects are stored. Each
162
argument object is a possible run-time argument. Syntax checking happens at
163
client level, while the type checking is implemented in the Argument code
164
(thus, many different Argument types might be needed).`
191
To declare a run-time argument on a specific command, the specification class
192
should contain a dict called *arguments* , where Argument objects are stored.
193
Each argument object is a run-time argument. Syntax checking happens at client
194
level, while the type checking is implemented in the Argument code (e.g.,
195
IntArgument checks if the value is an int).
165 196

  
166 197
.. code-block:: python
167 198

  
......
190 221

  
191 222
        arguments = dict(
192 223
            match=ValueArgument(
193
            'Filter output to match string', ('-m', --match'))
224
                'Filter output to match string', ('-m', --match'))
194 225
        )
195 226

  
196 227
Accessing run-time arguments
197 228
----------------------------
198 229

  
199
To access run-time arguments, users can use the _command_init interface, which
200
implements __item__ accessors to handle run-time argument values. In specific,
201
an instance of _command_init can use brackets to set or read <argument>.value .
230
To access run-time arguments, users can use the *_command_init* interface,
231
which implements *__item__* accessors to handle run-time argument values. In
232
other words, one may get the value of an argument with *self[<argument>]*.
202 233

  
203 234
.. code-block:: python
204 235

  
......
222 253
The main method and command parameters
223 254
--------------------------------------
224 255

  
225
The command behavior for each command / class is coded in *main*. The
226
parameters of *main* method defines the command parameters part of the syntax.
227
In specific::
256
The command behavior for each command class is coded in *main*. The
257
parameters of *main* method affect the syntax of the command. In specific::
228 258

  
229 259
    main(self, param)                   - obligatory parameter <param>
230 260
    main(self, param=None)              - optional parameter [param]
......
236 266
    main(self, *args)                   - arbitary number of params [...]
237 267
    main(self, param1____param2, *args) - <param1:param2> [...]
238 268

  
239
The information that can be mined by *command* for each individual command is
240
presented in the following:
269
Let's have a look at the command specification class again, and highlight the
270
parts that affect the command syntax:
241 271

  
242 272
.. code-block:: python
243 273
    :linenos:
......
245 275
    from kamaki.cli.argument import FlagArgument
246 276
    ...
247 277

  
248
    _commands = [_mygrp1_commands, _mygrp2=commands]
278
    _commands = [_mygrp1_commands, _mygrp2_commands]
249 279
    ...
250 280

  
251 281
    @command(_mygrp2_commands)
252 282
    class mygrp2_list_all():
253
        """List all subjects"""
283
        """List all subjects
284
        Refers to the subject accessible by current user
285
        """
254 286

  
255 287
        arguments = dict(FlagArgument('detailed list', '-l'))
256 288

  
257 289
        def main(self, reg_exp=None):
258 290
            ...
259 291

  
260
This will load the following information on the CommandTree:
292
The above lines contain the following information:
293

  
294
* Namespace and name (line 8): mygrp2 list all
295
* Short (line 9) and long (line 10) description
296
* Parameters (line 15): [reg exp]
297
* Runtime arguments (line 13): [-l]
298
* Runtime arguments help (line 13): detailed list
261 299

  
262
* Syntax (from lines 8,12,19): mygrp list all [reg exp] [-l]
263
* Description (form line 9): List all subjects
264
* Arguments help (from line 13,14): -l: detailed list
300
.. tip:: It is suggested to code the main functionality in a member method
301
    called *_run*. This allows the separation between syntax and logic. For
302
    example, an external library may need to call a command without caring
303
    about its command line behavior.
265 304

  
266 305
Letting kamaki know
267 306
-------------------
......
270 309
option. To demonstrate this, let the command specifications coded above be
271 310
stored in a file named *grps.py*.
272 311

  
273
The developer should move file *grps.py* to kamaki/cli/commands, the default
274
place for command specifications, although running a command specification from
275
a different path is also a kamaki feature.
312
The developer should move the file *grps.py* to *kamaki/cli/commands*, the
313
default place for command specifications
276 314

  
277
The user has to use a configuration file where the following is added:
315
These lines should be contained in the kamaki configuration file for a new
316
command specification module to work:
278 317
::
279 318

  
280 319
    [global]
......
288 327
    $ kamaki config set mygrp1_cli grps
289 328
    $ kamaki config set mygrp2_cli grps
290 329

  
291
Command specification modules don't need to live in kamaki/cli/commands,
292
although this is suggested for uniformity. If a command module exist in another
293
path::
330
.. note:: running a command specification from a different path is supported.
331
    To achieve this, add a *<group>_cli = </path/to/module>* line in the
332
    configure file under the *global* section.
333
::
294 334

  
295 335
    [global]
296 336
    mygrp_cli = /another/path/grps.py
......
320 360

  
321 361

  
322 362
    @command(_mygrp1_commands)
363
    class mygrp1_list(_command_init):
364
        """List mygrp1 objects.
365
        There are two versions: short and detailed
366
        """
367

  
368

  
369
    @command(_mygrp1_commands)
323 370
    class mygrp1_list_all(_command_init):
324 371
        """show a list"""
325 372

  
326
        def main(self):
373
        def _run():
327 374
            ...
328 375

  
376
        def main(self):
377
            self._run()
378

  
329 379

  
330 380
    @command(_mygrp1_commands)
331 381
    class mygrp1_list_details(_command_init):
......
336 386
                'Filter output to match string', ('-m', --match'))
337 387
        )
338 388

  
339
        def main(self):
340
            ...
389
        def _run(self):
341 390
            match_value = self['match']
342 391
            ...
343 392

  
393
        def main(self):
394
        self._run()
395

  
396

  
397
    #The following will also create a mygrp2_list command with no description
398

  
344 399

  
345 400
    @command(_mygrp2_commands)
346 401
    class mygrp2_list_all(_command_init):
......
350 405
            list=FlagArgument('detailed listing', '-l')
351 406
        )
352 407

  
353
        def main(self, regular_expression=None):
354
            ...
355
            detail_flag = self['list']
408
        def _run(self, regexp):
356 409
            ...
357
            if detail_flag:
410
            if self['list']:
358 411
                ...
359
            ...
360
            if regular_expression:
412
            else:
361 413
                ...
362
            ...
414

  
415
        def main(self, regular_expression=None):
416
            self._run(regular_expression)
363 417

  
364 418

  
365 419
    @command(_mygrp2_commands)
366 420
    class mygrp2_info(_command_init):
367 421
        """get information for subject with id"""
368 422

  
369
        def main(self, id, name=''):
423
        def _run(self, grp_id, grp_name):
370 424
            ...
425

  
426
        def main(self, id, name=''):
427
            self._run(id, name) 
b/docs/developers/clients-api.rst
3 3

  
4 4
Kamaki features a clients API for building third-party client applications that
5 5
communicate with OpenStack and / or Synnefo cloud services. The package is
6
called kamaki.clients and servers as a lib.
6
called *kamaki.clients* and serves as a lib.
7 7

  
8
A showcase of an application built on kamaki.clients is kamaki.cli, the command
9
line interface of kamaki.
8
A showcase of an application built on *kamaki.clients* is *kamaki.cli*, the
9
command line interface of kamaki.
10 10

  
11 11
Since Synnefo services are build as OpenStack extensions, an inheritance
12 12
approach has been chosen for implementing clients for both. In specific,
13
the *compute*, *storage* and *image* modules are clients of the OS compute, OS
14
object-store, respectively. On the contrary, all the other modules are Synnefo
15
extensions (*cyclades* extents *compute*, *pithos* and *pithos_rest_api*
16
extent *storage*) or novel Synnefo services (e.g., *astakos* for IM, *image*
17
for *plankton*).
13
the *compute*, *storage* and *image* modules are client implementations for the
14
OpenStack compute and OpenStack object-store APIs, respectively. The rest of the
15
modules implement the Synnefo extensions (i.e., *cyclades* and 
16
*cyclades_rest_api* extents *compute*, *pithos* and *pithos_rest_api* extent
17
*storage*) or novel Synnefo services (*image* for *plankton*).
18 18

  
19 19
Setup a client instance
20 20
-----------------------
21 21

  
22
External applications may instantiate one or more kamaki clients.
22
There is a client for every API, therefore an external applications should
23
instantiate they kamaki clients they need. For example, to manage virtual
24
servers and stored objects / files, an application would probably need to
25
instantiate the CycladesClient and PithosClient respectively.
23 26

  
24 27
.. code-block:: python
25 28
    :emphasize-lines: 1
26 29

  
27
    Example 1.1: Instantiate Cyclades and Pithos client
30
    Example 1.1: Instantiate Cyclades and Pithos clients
28 31

  
29 32

  
30 33
    from kamaki.clients.cyclades import CycladesClient
......
33 36
    my_cyclades_client = CycladesClient(base_url, token)
34 37
    my_pithos_client = PithosClient(base_url, token, account, container)
35 38

  
36
.. note:: *cyclades* and *pithos* clients inherit all methods of *compute*
37
    and *storage* clients respectively. Separate compute or storage objects
38
    should be used only when implementing applications for strict OS Compute or
39
    OS Storage services.
39
.. note:: *cyclades* and *pithos* clients inherit ComputeClient from *compute*
40
    and StorageClient from *storage*, respectively. Separate ComputeClient or
41
    StorageClient objects should be used only when implementing applications for
42
    strict OpenStack Compute or Storage services.
40 43

  
41 44
Using endpoints to get the base_url
42 45
-----------------------------------
43 46

  
44
In OpenStack, each service (e.g. `compute`, `object-store`, etc.) has a number
45
of `endpoints`. These `endpoints` are actually URIs that are needed as prefixes
46
for the API calls the kamaki client generates. In this context, we need just
47
one of these these `endpoints`, the `publicURL`, which is also referred to as
48
`base_url` in kamaki client libraries.
47
In OpenStack, each service (e.g., `compute`, `object-store`, etc.) has a number
48
of `endpoints`. These `endpoints` are actually URIs that are used by kamaki as
49
prefixes to form the corresponding API calls. Client applications need just
50
one of these these `endpoints`, namely the `publicURL`, which is also referred
51
to as `base_url` in kamaki client libraries.
49 52

  
50
In general, this is the suggested way of getting the base_url::
53
Here are instructions for getting the base_url for a service::
51 54

  
52 55
    1. From the deployment UI get the AUTHENTICATION_URL and TOKEN
53 56
        (Example 1.2)
......
59 62
        (Example 1.3)
60 63

  
61 64
The AstakosClient is a client for the Synnefo/Astakos server. Synnefo/Astakos
62
is an advanced identity server based on OpenStack identity specifications.
63
Therefore, it can be used to get the `base_url` values needed for initializing
64
kamaki clients. Kamaki simplifies this process with the astakos client library.
65
is an identity server that implements the OpenStack identity API. Therefore, it
66
can be used to get the `base_url` values needed for initializing kamaki clients.
67
Kamaki simplifies this process with the astakos client library.
65 68

  
66 69
Let's review the process with examples.
67 70

  
......
93 96
    pithos_endpoints = my_astakos_client.get_service_endpoints('object-store')
94 97
    pithos_base_url = pithos_endpoints['publicURL']
95 98

  
96
The ``get_service_endpoints`` method gets the service name as an argument. Here
97
are the service names for the most popular kamaki clients::
99
The ``get_service_endpoints`` method is called with the service name as an
100
argument. Here are the service names for the kamaki clients::
98 101

  
99
    storage, pithos     -->     object-store
100
    compute, cyclades   -->     compute
101
    image               -->     image
102
    astakos             -->     identity
102
    storage.StorageClient, pithos.PithosClient      -->     object-store
103
    compute.ComputeClient, cyclades.CycladesClient  -->     compute
104
    image.ImageClient                               -->     image
105
    astakos.AstakosClient                           -->     identity, account
103 106

  
104 107
Use client methods
105 108
------------------
106 109

  
107
Client methods can now be called. Developers are advised to
108
consult :ref:`the-client-api-ref` for details on the available methods and how
109
to use them.
110
At this point we assume that we can initialize a client, so the initialization
111
step will be omitted in most of the examples that follow.
112

  
113
The next step is to take a look at the member methods of each particular client.
114
A detailed catalog of the member methods for all client classes can be found at
115
:ref:`the-client-api-ref`
110 116

  
111 117
In the following example, the *cyclades* and *pithos* clients of example 1.1
112
are used to extract some information, that is then printed to the standard
113
output.
118
are used to extract some information through the remote service APIs. The information is then printed to the standard output.
114 119

  
115 120

  
116 121
.. code-block:: python
......
119 124
    Example 1.4: Print server name and OS for server with server_id
120 125
                Print objects in container mycont
121 126

  
122

  
123 127
    srv = my_cyclades_client.get_server_info(server_id)
124 128
    print("Server Name: %s (with OS %s" % (srv['name'], srv['os']))
125 129

  
......
130 134
.. code-block:: console
131 135
    :emphasize-lines: 1
132 136

  
133
    Run of examples 1.1 + 1.4
137
    * A run of examples 1.1 + 1.4 *
134 138

  
135 139

  
136 140
    $ python test_script.py
......
143 147
Error handling
144 148
--------------
145 149

  
146
The kamaki.clients standard error is ClientError. A ClientError is raised for
147
any kind of kamaki.clients errors (errors reported by servers, type errors in
150
The *kamaki.clients* error class is ClientError. A ClientError is raised for
151
any kind of *kamaki.clients* errors (errors reported by servers, type errors in
148 152
arguments, etc.).
149 153

  
150 154
A ClientError contains::
151 155

  
152 156
    message     The error message.
153
    status      An optional error code, e.g. after a server error.
157
    status      An optional error code, e.g., after a server error.
154 158
    details     Optional list of messages with error details.
155 159

  
156 160
The following example concatenates examples 1.1 to 1.4 plus error handling
......
159 163

  
160 164
    Example 1.5: Error handling
161 165

  
166
    from kamaki.clients import ClientError
167

  
162 168
    from kamaki.clients.astakos import AstakosClient
163 169
    from kamaki.clients.cyclades import CycladesClient
164 170
    from kamaki.clients.pithos import PithosClient
165 171

  
166 172
    try:
167 173
        my_astakos_client = AstakosClient(AUTHENTICATION_URL, TOKEN)
174
        my_astakos_client.authenticate()
168 175
    except ClientError:
169 176
        print('Failed to authenticate user token')
170 177
        return 1
......
297 304
            print 'Image %s registered with id %s' % (r['name'], r['id'])
298 305
        except ClientError:
299 306
            print 'Failed to register image %s' % IMAGE_PATH
307

  
308
Two servers and a private network
309
'''''''''''''''''''''''''''''''''
310

  
311
.. code-block:: python
312

  
313
    #! /user/bin/python
314

  
315
    from kamaki.clients.astakos import AstakosClient
316
    from kamaki.clients.cyclades import CycladesClient
317

  
318
    AUTHENTICATION_URL = 'https://accounts.example.com/identity/v2.0'
319
    TOKEN = 'replace this with your token'
320

  
321
    astakos = AstakosClient(AUTHENTICATION_URL, TOKEN)
322

  
323
    cyclades_endpoints = user.get_service_endpoints('compute')
324
    CYCLADES_URL = cyclades_endpoints['publicURL']
325

  
326
    FLAVOR_ID = 'put your flavor id here'
327
    IMAGE_ID = 'put your image id here'
328

  
329
    cyclades = CycladesClient(CYCLADES_URL, TOKEN)
330

  
331
    srv1 = cyclades.create_server('server 1', FLAVOR_ID, IMAGE_ID)
332
    srv2 = cyclades.create_server('server 2', FLAVOR_ID, IMAGE_ID)
333

  
334
    srv_state1 = cyclades.wait_server(srv1['id'])
335
    assert srv_state1 in ('ACTIVE'), 'Server 1 built failure'
336

  
337
    srv_state2 = cyclades.wait_server(srv2['id'])
338
    assert srv_state2 in ('ACTIVE'), 'Server 2 built failure'
339

  
340
    net = cyclades.create_network('My private network')
341
    net_state = cyclades.wait_network(net['id'])
342
    assert net_state in ('ACTIVE', ), 'Network built failure'
343

  
344
    cyclades.connect_server(srv1['id'], net['id'])
345
    cyclades.connect_server(srv2['id'], net['id'])
b/docs/developers/extending-clients-api.rst
1 1
Extending kamaki.clients
2 2
========================
3 3

  
4
By default, kamaki clients are REST clients (they manage HTTP requests and
5
responses to communicate with services).
4
By default, kamaki clients implement REST APIs, therefore they manage HTTP
5
requests and responses to communicate with services.
6 6

  
7 7
How to build a client
8 8
---------------------
......
17 17

  
18 18
    from kamaki.clients import Client, ClientError
19 19

  
20

  
20 21
    class MyNewClient(Client):
21 22
        """MyNewClient Description Here"""
22 23

  
......
100 101
-----------
101 102

  
102 103
The kamaki.clients package contains a set of fine-grained unit-tests for all
103
its packages. 
104
APIs. 
104 105

  
105 106
.. note:: unit tests require the optional python-mock package, version 1.X or
106 107
    better
......
123 124

  
124 125
    $ python test.py
125 126

  
126
To test a specific class, add the class name as an argument. E.g. for the
127
To test a specific class, add the class name as an argument. e.g., for the
127 128
Client class:
128 129

  
129 130
.. code-block:: console
130 131

  
131 132
    $ python test.py Client
132 133

  
133
To test a specific method in a class, apply an extra argument, e.g. for the
134
To test a specific method in a class, apply an extra argument, e.g., for the
134 135
request method in the Client class:
135 136

  
136 137
.. code-block:: console
......
138 139
    $ python test.py Client request
139 140

  
140 141
Each package contains a test module (test.py) which is also runnable from the
141
command line. E.g. in the pithos package there is a test module which
142
command line. e.g., in the pithos package there is a test module which
142 143
contains, among others, the **download** sub-test:
143 144

  
144 145
.. code-block:: console
......
154 155
    # Test kamaki.clients.pithos.PithosClient.download
155 156
    $ python test.py Pithos download
156 157

  
157
To fully test a specific package, run test.py from the package location. E.g.
158
To fully test a specific package, run test.py from the package location. e.g.,
158 159
to test everything in kamaki.clients.pithos package:
159 160

  
160 161
.. code-block:: console
......
169 170
of this package. All test modules contain a set of classes that extent the
170 171
TestCase class. They also contain a main method to run the tests.
171 172

  
172
By convention, testing classes are named as <Tested Class> where <Test Class>
173
is the name of the tested class or module. Methods not grouped in classes are
174
tested by classes named after their respective module.
173
By convention, testing classes have the same name as the class they test.
174
Methods not grouped in classes are tested by classes named after their
175
respective module.
175 176

  
176
For example, the kamaki.clients.pithos.PithosClient class is tested by the
177
kamaki.clients.pithos.test.PithosClient class, while the methods in
178
kamaki.clients.utils module are tested by the kamaki.clients.utils.test.Utils
179
testing class.
177
For example, the *kamaki.clients.pithos.PithosClient* class is tested by the
178
*kamaki.clients.pithos.test.PithosClient* class, while the methods in
179
*kamaki.clients.utils* module are tested by *kamaki.clients.utils.test.Utils*.
180 180

  
181 181
Adding unit tests
182 182
^^^^^^^^^^^^^^^^^
183 183

  
184
After modifying or extending kamaki.clients method, classes, modules or
184
After modifying or extending *kamaki.clients* method, classes, modules or
185 185
packages, it is a good practice to also modify or extend the corresponding
186 186
unit tests. What's more, it is recommended to modify or implement the testing
187
of new behavior before implementing the behavior itself. The aim for
188
kamaki.clients package is an 1 to 1 mapping between methods and their tests.
187
of new behavior before implementing the behavior itself. The goal is to
188
preserve an 1 to 1 mapping between methods and their tests.
189 189

  
190 190
Modifying an existing method
191 191
""""""""""""""""""""""""""""
......
195 195
test module under the same package, in a TestCase subclass that is named with a
196 196
name similar to the package or class that contains the tested method.
197 197

  
198
Example 1: to modify the kamaki.clients.utils.filter_in method, the programmer
199
has to also adjust the kamaki.clients.utils.test.Utils.test_filter_in method.
200

  
201
Example 2: to modify the kamaki.clients.pithos.PithosRestClient.object_get, the
198
Example: to modify *kamaki.clients.pithos.PithosRestClient.object_get*, the
202 199
programmer has to also adjust the
203
kamaki.clients.pithos.test.PithosRestClient.test_object_get method.
200
*kamaki.clients.pithos.test.PithosRestClient.test.object_get* method.
204 201

  
205 202
Adding a new method
206 203
"""""""""""""""""""
207 204

  
208
Programmers who want to implement a new method in an existing class, are
209
encouraged to implement the corresponding unit test first. In order to do that,
210
they should find the testing class that is mapped to the class or module they
211
need to extend.
205
To implement a new method in an existing class, developers are encouraged to
206
implement the corresponding unit test first. In order to do that, they should
207
find the testing class that is mapped to the class or module they working on.
212 208

  
213
Example 1: To add a **list_special** method to
214
kamaki.clients.astakos.AstakosClient, extend the
215
kamaki.clients.astakos.test.AstakosClient class, as shown bellow:
209
Example: Adding **list_special** to *kamaki.clients.astakos.AstakosClient*,
210
requires changes to *kamaki.clients.astakos.test.AstakosClient* too, as shown
211
bellow:
216 212

  
217 213
.. code-block:: python
218 214

  
......
224 220
            """Test the list_special method"""
225 221
            ...
226 222

  
227
Example 2: To add a **get_random_int** method in kamaki.clients.utils module,
228
extend the kamaki.clients.utils.test.Utils test class, as shown bellow:
229

  
230
.. code-block:: python
231

  
232
    # file: ${kamaki}/kamaki/clients/utils/__init__.py
233

  
234
    class Utils(TestCase):
235
        ...
236
        def test_get_random_int(self):
237
            """Test the get_random_int method"""
238
            ...
239

  
240 223
Implementing a new class or module
241 224
""""""""""""""""""""""""""""""""""
242 225

  
243 226
Each class or module needs a seperate test sub-module. By convention, each
244
class or module under the kamaki.clients should be located in a separate
227
class or module under *kamaki.clients*, should be located in a separate
245 228
directory.
246 229

  
247
Example 1: To add a NewService class that implements the kamaki.clients.Client
248
interface: 
230
Example 1: To add a NewService class that implements *kamaki.clients.Client*: 
249 231

  
250
* create a new_service package and implement the unit tests in the kamaki.clients.new_service.test module:
232
* create a new_service package and implement the unit tests in
233
    *kamaki.clients.new_service.test*:
251 234

  
252 235
.. code-block:: console
253 236

  
254 237
    $ mkdir new_service && touch new_service/test.py
255 238

  
256
* create the file that will hold the package code and implement the module there:
239
* create the file to code the package implementation:
257 240

  
258 241
.. code-block:: console
259 242

  
260 243
    $ touch new_service/__init__.py
261 244

  
262
* Create the test class and methods in kamaki.clients.new_service.test
245
* Create the test class and methods in *kamaki.clients.new_service.test*
263 246

  
264 247
.. code-block:: python
265 248

  
......
283 266
        def method1(self, ...):
284 267
            ...
285 268

  
286
* Expose the new tests to top test module, by importing the test class to kamaki.clients.test
269
* Import the test class to *kamaki.clients.test*:
287 270

  
288
..code-block:: python
271
.. code-block:: python
289 272

  
290 273
    # file: ${kamaki}/kamaki/clients/test.py
291

  
292 274
    from kamaki.clients.new_service.test import NewService
293 275

  
294 276
.. note:: If the new class or module is part of an existing sub-package, it is
b/docs/developers/logging.rst
1 1
Logging
2 2
=======
3 3

  
4
Kamaki uses the standard Python logger package to log some of its
5
functionality.
4
Kamaki uses the standard Python logger package to log some of its functionality.
6 5

  
7
All kamaki loggers are named or prefixed after the package they log, e.g.
8
a logger at `kamaki/cli/argument.__init__.py` should be called
6
All kamaki loggers are named or prefixed after the package they log, e.g.,
7
a logger at `kamaki/cli/argument/__init__.py` should be called
9 8
`kamaki.cli.argument` whereas a logger at `kamaki/clients/conf.py` should be
10
named `kamaki.clients/conf`. In `kamaki/clients/__init__.py` there are two
9
named `kamaki.clients.conf`. In `kamaki/clients/__init__.py` there are two
11 10
loggers that use the package name as prefix, and they detailed bellow.
12 11

  
13 12
Monitor requests and responses
14 13
------------------------------
15 14

  
16 15
The `kamaki.clients` logger contains two subloggers that monitor the HTTP
17
communication of with the servers::
16
API calls::
18 17

  
19 18
	kamaki.clients.send   for kamaki requests to the server
20 19
	kamaki.clients.recv   for server responses to kamaki
21 20

  
22 21
These are the only loggers used for purposes other than mere debugging. Both
23 22
loggers are defined in the CLI code and are used to (a) log HTTP communication
24
to the log file as well as to (b) show users the HTTP requests and responses if
25
kamaki cli is called with options like "verbose" or "debug".
23
to the log file as well as to (b) show users the HTTP requests and responses in
24
"verbose" or "debug" modes.
26 25

  
27 26
Logger in external code
28 27
-----------------------
......
68 67
	<   content-type: application/json; charset=UTF-8
69 68
	< data size: 2425
70 69

  
71
.. note:: user token and http body content are not logged by default
70
.. note:: user token and http body content are not logged by default. This can
71
	be switched on and off by modifing the *kamaki.client.Client.LOG_TOKEN* and
72
	*kamaki.client.Client.LOG_DATA* flags
72 73

  
73 74
As a second example, let's suppose that we need to see only the http requests
74
of the `pithos.list_objects()` method. We decide to print these to the console.
75
To achieve that goal, we should get a stream logger and deactivate it when we
76
do not need it anymore.
75
of the `pithos.list_objects()` method and print these to stdout. To achieve
76
that goal, we should get a stream logger and deactivate it when we do not need
77
it anymore.
77 78

  
78 79

  
79 80
.. code-block:: python
b/docs/examplesdir/imageregister.rst
10 10

  
11 11
    <container>:<object path>
12 12

  
13
    e.g.:
13
    e.g.,:
14 14

  
15 15
    pithos:debian_base3.diskdump
16 16

  
......
301 301

  
302 302
We can use the same idea to change the values of other metadata like disk
303 303
format, container format or status. On the other hand, we cannot modify the
304
id, owner, location, checksum and dates. E.g., to publish and unpublish:
304
id, owner, location, checksum and dates. e.g., to publish and unpublish:
305 305

  
306 306
.. code-block:: console
307 307

  
......
552 552

  
553 553
    #!/bin/bash
554 554

  
555
    userid=... # e.g. s0m3-u53r-1d
556
    container=... # e.g. pithos
555
    userid=... # e.g., s0m3-u53r-1d
556
    container=... # e.g., pithos
557 557

  
558 558
    for path in images/*.diskdump; do
559 559
        location=$container:${path}
b/docs/examplesdir/server.rst
127 127

  
128 128
    -p <local file path>[,<remote path>[,<remote owner>[,<remote group>[,<mode>]]]]
129 129

  
130
    e.g.
130
    e.g.,
131 131

  
132 132
    -p /home/someuser/.ssh/id_rsa.pub,/root/.ssh/authorized_keys,root,root,0777
133 133

  
134 134
.. note:: In case of omitting an optional part of the personality string, the
135
    default behavior depends on the remote server, e.g. for a debian image we
135
    default behavior depends on the remote server, e.g., for a debian image we
136 136
    expect the file to have root ownership, if the ownership is not specified.
137 137

  
138 138
Create a virtual server while injecting current user public key to root account
......
194 194

  
195 195
.. note:: There is no reason to limit injections to ssh keys. Users with an
196 196
    adequate understanding of the remote OS are encouraged to prepare and
197
    inject all kinds of useful files, e.g. **lists of package sources**,
197
    inject all kinds of useful files, e.g., **lists of package sources**,
198 198
    **default user profiles**, **device mount configurations**, etc.
b/docs/index.rst
12 12
development API for managing clouds.
13 13

  
14 14
As a development library, it implements the OpenStack APIs,
15
along with custom extensions as used by Synnefo
15
along with the custom extensions introduced by Synnefo
16 16
(`Synnefo IaaS <http://www.synnefo.org/>`_).
17 17

  
18 18
./kamaki is open source and released under a 2-clause BSD License.
b/docs/installation.rst
27 27

  
28 28
.. warning:: Debian Squeeze users may replace "wheezy" with "squeeze"
29 29

  
30
* Make sure the GPG public key for the Synnefo development team is added:
30
* Make sure the GPG public key for the Synnefo repository is added:
31 31

  
32 32
    .. code-block:: console
33 33

  
......
86 86

  
87 87
    $ sudo apt-get install python-mock
88 88

  
89
.. warning:: kamaki.clients unit-tests need python-mock 1.X or better. e.g.::
89
.. warning:: kamaki.clients unit-tests need python-mock 1.X or better. e.g.,::
90 90

  
91 91
    $ sudo apt-get install python-mock=1.0.1
92 92

  
......
118 118
Setup a virtual enviroment (optional)
119 119
"""""""""""""""""""""""""""""""""""""
120 120

  
121
Use virtualenv to setup kamaki and Synnefo services in a sandbox
122
environment.
121
Use virtualenv to setup kamaki and Synnefo services in a sandbox environment.
123 122

  
124 123
.. code-block:: console
125 124

  
b/docs/overview.rst
5 5
-------
6 6

  
7 7
Kamaki was created in 2011 by the `Synnefo <http://www.synnefo.org>`_
8
development team of the *Greek Research and Technology Network (GRNET)*,
8
development team at the *Greek Research and Technology Network (GRNET)*,
9 9
initially as an internal project and later as a multipurpose tool for all
10 10
users.
11 11

  
......
24 24
Kamaki has been designed to act as a command line client as well as a python
25 25
library for client developers. It is widely used in various Synnefo and okeanos
26 26
components. Third parties are also encouraged to use the kamaki library for
27
developing their own python-based cloud-client applications.
27
developing their own python-based cloud applications.
28 28

  
29 29
As Synnefo became a full, scalable and stable cloud solution, kamaki also
30 30
evolved to an intuitive, multipurpose tool, available to a wider user base.
......
42 42

  
43 43
* internally by the Synnefo development team to test the Synnefo software,
44 44

  
45
* by the deployment team which operates GRNET's ~okeanos service
45
* by the deployment team which operates GRNET ~okeanos service
46 46

  
47 47
* as the main Pithos+ client in Linux and other Unix-like environments
48 48

  
b/docs/setup.rst
11 11
.. warning:: Users of kamaki 0.8.X or older should consult the
12 12
    `migration guide <#migrating-from-kamaki-0-8-x-to-0-9-or-better>`_ first.
13 13

  
14
Kamaki has to be configured for a specific Synnefo deployment, with an
15
authentication url and user token pair. Users should also pick an alias to name
16
the cloud configuration. This can be any single word, e.g., "default",
14
To set up Kamaki for a specific Synnefo deployment, users need an
15
**authentication URL** and a **user token**. Users should also pick an alias to
16
name the cloud configuration. This can be any single word, e.g., "default",
17 17
"mycloud"or whatever suits the user.
18 18

  
19 19
.. code-block:: console
......
21 21
    $ kamaki config set cloud.<cloud alias>.url <cloud-authentication-URL>
22 22
    $ kamaki config set cloud.<cloud alias>.token myt0k3n==
23 23

  
24
If only one cloud is configured, kamaki automatically picks it as the default.
24
If only one cloud is configured, it is automatically considered the default.
25 25
Otherwise, a default cloud should be specified:
26 26

  
27 27
.. code-block:: console
......
80 80
At this point, we should examine the kamaki output. Most options are renamed to
81 81
match the latest configuration file version specifications.
82 82

  
83
Let's take a look at the discarded options:
83
Lets take a look at the discarded options:
84 84

  
85 85
* `global.account` and `user.account` are not used anymore.
86 86
    The same is true for the synonyms `store.account` and `pithos.account`.
......
118 118
---------------
119 119

  
120 120
The following refers to users of multiple Synnefo and/or Open Stack
121
deployments. In the following, a Synnefo or Open Stack cloud deployment will
121
deployments. In the following, a Synnefo (or Open Stack) cloud deployment will
122 122
be called **a cloud**.
123 123

  
124 124
Multiple clouds can be configured and manager in a single  kamaki setup, since
b/docs/usage.rst
40 40

  
41 41
In favor of interactive shell:
42 42

  
43
* tab completion for commands (if supported by the user's shell)
44
* session history with ↑ or ↓ keys (if supported by the user's shell)
43
* tab completion for commands (if supported by the user shell)
44
* session history with ↑ or ↓ keys (if supported by the user shell)
45 45
* shorter commands with command context switching
46 46
* re-run old commands with /history
47 47

  
......
248 248
                            filter by flavor id
249 249

  
250 250
    Details:
251
    Use filtering arguments (e.g. --name-like) to manage long server lists
251
    Use filtering arguments (e.g., --name-like) to manage long server lists
252 252

  
253 253
.. _using-history-ref:
254 254

  
b/kamaki/cli/__init__.py
110 110

  
111 111
def command(cmd_tree, prefix='', descedants_depth=1):
112 112
    """Load a class as a command
113
        e.g. spec_cmd0_cmd1 will be command spec cmd0
113
        e.g., spec_cmd0_cmd1 will be command spec cmd0
114 114

  
115 115
        :param cmd_tree: is initialized in cmd_spec file and is the structure
116 116
            where commands are loaded. Var name should be _commands
b/kamaki/cli/command_tree/__init__.py
150 150
    def find_best_match(self, terms):
151 151
        """Find a command that best matches a given list of terms
152 152

  
153
        :param terms: (list of str) match against paths in cmd_tree, e.g.
153
        :param terms: (list of str) match against paths in cmd_tree, e.g.,
154 154
            ['aa', 'bb', 'cc'] matches aa_bb_cc
155 155

  
156 156
        :returns: (Command, list) the matching command, the remaining terms or
b/kamaki/cli/commands/astakos.py
78 78
@command(user_cmds)
79 79
class user_authenticate(_user_init, _optional_json):
80 80
    """Authenticate a user
81
    Get user information (e.g. unique account name) from token
81
    Get user information (e.g., unique account name) from token
82 82
    Token should be set in settings:
83 83
    *  check if a token is set    /config whoami cloud.default.token
84 84
    *  permanently set a token    /config set cloud.default.token <token>
b/kamaki/cli/commands/cyclades.py
65 65
    '  SERVER_PATH: destination location inside server Image',
66 66
    '  OWNER: virtual servers user id of the remote destination file',
67 67
    '  GROUP: virtual servers group id or name of the destination file',
68
    '  MODEL: permition in octal (e.g. 0777 or o+rwx)']
68
    '  MODEL: permition in octal (e.g., 0777 or o+rwx)']
69 69

  
70 70

  
71 71
class _service_wait(object):
......
136 136
@command(server_cmds)
137 137
class server_list(_init_cyclades, _optional_json, _name_filter, _id_filter):
138 138
    """List virtual servers accessible by user
139
    Use filtering arguments (e.g. --name-like) to manage long server lists
139
    Use filtering arguments (e.g., --name-like) to manage long server lists
140 140
    """
141 141

  
142 142
    PERMANENTS = ('id', 'name')
......
259 259
    Contains:
260 260
    - name, id, status, create/update dates
261 261
    - network interfaces
262
    - metadata (e.g. os, superuser) and diagnostics
262
    - metadata (e.g., os, superuser) and diagnostics
263 263
    - hardware flavor and os image ids
264 264
    """
265 265

  
b/kamaki/cli/commands/history.py
91 91
    .   2.  <order-id-1>-<order-id-2> : pick all commands ordered in the range
92 92
    .       [<order-id-1> - <order-id-2>]
93 93
    .   - the above can be mixed and repeated freely, separated by spaces
94
    .       e.g. pick 2 4-7 -3
95
    .   - Use negative integers to count from the end of the list, e.g.:
94
    .       e.g., pick 2 4-7 -3
95
    .   - Use negative integers to count from the end of the list, e.g.,:
96 96
    .       -2 means : the command before the last one
97 97
    .       -2-5 means : last 2 commands + the first 5
98 98
    .       -5--2 means : the last 5 commands except the last 2
......
151 151
    .   1.  <order-id> : pick the <order-id>th command
152 152
    .   2.  <order-id-1>-<order-id-2> : pick all commands ordered in the range
153 153
    .       [<order-id-1> - <order-id-2>]
154
    .   - Use negative integers to count from the end of the list, e.g.:
154
    .   - Use negative integers to count from the end of the list, e.g.,:
155 155
    .       -2 means : the command before the last one
156 156
    .       -2-5 means : last 2 commands + the first 5
157 157
    .       -5--2 mean
b/kamaki/cli/commands/image.py
407 407
class image_register(_init_image, _optional_json):
408 408
    """(Re)Register an image file to an Image service
409 409
    The image file must be stored at a pithos repository
410
    Some metadata can be set by user (e.g. disk-format) while others are set
411
    only automatically (e.g. image id). There are also some custom user
410
    Some metadata can be set by user (e.g., disk-format) while others are set
411
    only automatically (e.g., image id). There are also some custom user
412 412
    metadata, called properties.
413 413
    A register command creates a remote meta file at
414 414
    .  <container>:<image path>.meta
b/kamaki/cli/commands/pithos.py
1884 1884
                msg = 'Failed to convert %s to bytes' % user_input,
1885 1885
                raiseCLIError(qe, msg, details=[
1886 1886
                    'Syntax: containerlimit set <limit>[format] [container]',
1887
                    'e.g.: containerlimit set 2.3GB mycontainer',
1887
                    'e.g.,: containerlimit set 2.3GB mycontainer',
1888 1888
                    'Valid formats:',
1889 1889
                    '(*1024): B, KiB, MiB, GiB, TiB',
1890 1890
                    '(*1000): B, KB, MB, GB, TB'])
......
2036 2036
    Deleted objects may still have versions that can be used to restore it and
2037 2037
    get information about its previous state.
2038 2038
    The version number can be used in a number of other commands, like info,
2039
    copy, move, meta. See these commands for more information, e.g.
2039
    copy, move, meta. See these commands for more information, e.g.,
2040 2040
    /file info -h
2041 2041
    """
2042 2042

  
b/kamaki/cli/commands/snf-astakos.py
99 99
@command(snfastakos_cmds)
100 100
class astakos_user_info(_astakos_init, _optional_json):
101 101
    """Authenticate a user
102
    Get user information (e.g. unique account name) from token
102
    Get user information (e.g., unique account name) from token
103 103
    Token should be set in settings:
104 104
    *  check if a token is set    /config get cloud.default.token
105 105
    *  permanently set a token    /config set cloud.default.token <token>
......
412 412

  
413 413
    arguments = dict(
414 414
        accept=CommaSeparatedListArgument(
415
            'commission ids to accept (e.g. --accept=11,12,13,...',
415
            'commission ids to accept (e.g., --accept=11,12,13,...',
416 416
            '--accept'),
417 417
        reject=CommaSeparatedListArgument(
418
            'commission ids to reject (e.g. --reject=11,12,13,...',
418
            'commission ids to reject (e.g., --reject=11,12,13,...',
419 419
            '--reject'),
420 420
    )
421 421

  

Also available in: Unified diff