|
| 1 | +============== |
| 2 | +Command Errors |
| 3 | +============== |
| 4 | + |
| 5 | +Handling errors in OpenStackClient commands is fairly straightforward. An |
| 6 | +exception is thrown and handled by the application-level caller. |
| 7 | + |
| 8 | +Note: There are many cases that need to be filled out here. The initial |
| 9 | +version of this document considers the general command error handling as well |
| 10 | +as the specific case of commands that make multiple REST API calls and how to |
| 11 | +handle when one or more of those calls fails. |
| 12 | + |
| 13 | +General Command Errors |
| 14 | +====================== |
| 15 | + |
| 16 | +The general pattern for handling OpenStackClient command-level errors is to |
| 17 | +raise a CommandError exception with an appropriate message. This should include |
| 18 | +conditions arising from arguments that are not valid/allowed (that are not otherwise |
| 19 | +enforced by ``argparse``) as well as errors arising from external conditions. |
| 20 | + |
| 21 | +External Errors |
| 22 | +--------------- |
| 23 | + |
| 24 | +External errors are a result of things outside OpenStackClient not being as |
| 25 | +expected. |
| 26 | + |
| 27 | +Example |
| 28 | +~~~~~~~ |
| 29 | + |
| 30 | +This example is taken from ``keypair create`` where the ``--public-key`` option |
| 31 | +specifies a file containing the public key to upload. If the file is not found, |
| 32 | +the IOError exception is trapped and a more specific CommandError exception is |
| 33 | +raised that includes the name of the file that was attempted to be opened. |
| 34 | + |
| 35 | +.. code-block:: python |
| 36 | +
|
| 37 | + class CreateKeypair(command.ShowOne): |
| 38 | + """Create new public key""" |
| 39 | +
|
| 40 | + ## ... |
| 41 | +
|
| 42 | + def take_action(self, parsed_args): |
| 43 | + compute_client = self.app.client_manager.compute |
| 44 | +
|
| 45 | + public_key = parsed_args.public_key |
| 46 | + if public_key: |
| 47 | + try: |
| 48 | + with io.open( |
| 49 | + os.path.expanduser(parsed_args.public_key), |
| 50 | + "rb" |
| 51 | + ) as p: |
| 52 | + public_key = p.read() |
| 53 | + except IOError as e: |
| 54 | + msg = "Key file %s not found: %s" |
| 55 | + raise exceptions.CommandError( |
| 56 | + msg % (parsed_args.public_key, e), |
| 57 | + ) |
| 58 | +
|
| 59 | + keypair = compute_client.keypairs.create( |
| 60 | + parsed_args.name, |
| 61 | + public_key=public_key, |
| 62 | + ) |
| 63 | +
|
| 64 | + ## ... |
| 65 | +
|
| 66 | +REST API Errors |
| 67 | +=============== |
| 68 | + |
| 69 | +Most commands make a single REST API call via the supporting client library |
| 70 | +or SDK. Errors based on HTML return codes are usually handled well by default, |
| 71 | +but in some cases more specific or user-friendly messages need to be logged. |
| 72 | +Trapping the exception and raising a CommandError exception with a useful |
| 73 | +message is the correct approach. |
| 74 | + |
| 75 | +Multiple REST API Calls |
| 76 | +----------------------- |
| 77 | + |
| 78 | +Some CLI commands make multiple calls to library APIs and thus REST APIs. |
| 79 | +Most of the time these are ``create`` or ``set`` commands that expect to add or |
| 80 | +change a resource on the server. When one of these calls fails, the behaviour |
| 81 | +of the remainder of the command handler is defined as such: |
| 82 | + |
| 83 | +* Whenever possible, all API calls will be made. This may not be possible for |
| 84 | + specific commands where the subsequent calls are dependent on the results of |
| 85 | + an earlier call. |
| 86 | + |
| 87 | +* Any failure of an API call will be logged for the user |
| 88 | + |
| 89 | +* A failure of any API call results in a non-zero exit code |
| 90 | + |
| 91 | +* In the cases of failures in a ``create`` command a follow-up mode needs to |
| 92 | + be present that allows the user to attempt to complete the call, or cleanly |
| 93 | + remove the partially-created resource and re-try. |
| 94 | + |
| 95 | +The desired behaviour is for commands to appear to the user as idempotent |
| 96 | +whenever possible, i.e. a partial failure in a ``set`` command can be safely |
| 97 | +retried without harm. ``create`` commands are a harder problem and may need |
| 98 | +to be handled by having the proper options in a set command available to allow |
| 99 | +recovery in the case where the primary resource has been created but the |
| 100 | +subsequent calls did not complete. |
| 101 | + |
| 102 | +Example |
| 103 | +~~~~~~~ |
| 104 | + |
| 105 | +This example is taken from the ``volume snapshot set`` command where ``--property`` |
| 106 | +arguments are set using the volume manager's ``set_metadata()`` method, |
| 107 | +``--state`` arguments are set using the ``reset_state()`` method, and the |
| 108 | +remaining arguments are set using the ``update()`` method. |
| 109 | + |
| 110 | +.. code-block:: python |
| 111 | +
|
| 112 | + class SetSnapshot(command.Command): |
| 113 | + """Set snapshot properties""" |
| 114 | +
|
| 115 | + ## ... |
| 116 | +
|
| 117 | + def take_action(self, parsed_args): |
| 118 | + volume_client = self.app.client_manager.volume |
| 119 | + snapshot = utils.find_resource( |
| 120 | + volume_client.volume_snapshots, |
| 121 | + parsed_args.snapshot, |
| 122 | + ) |
| 123 | +
|
| 124 | + kwargs = {} |
| 125 | + if parsed_args.name: |
| 126 | + kwargs['name'] = parsed_args.name |
| 127 | + if parsed_args.description: |
| 128 | + kwargs['description'] = parsed_args.description |
| 129 | +
|
| 130 | + result = 0 |
| 131 | + if parsed_args.property: |
| 132 | + try: |
| 133 | + volume_client.volume_snapshots.set_metadata( |
| 134 | + snapshot.id, |
| 135 | + parsed_args.property, |
| 136 | + ) |
| 137 | + except SomeException: # Need to define the exceptions to catch here |
| 138 | + self.app.log.error("Property set failed") |
| 139 | + result += 1 |
| 140 | +
|
| 141 | + if parsed_args.state: |
| 142 | + try: |
| 143 | + volume_client.volume_snapshots.reset_state( |
| 144 | + snapshot.id, |
| 145 | + parsed_args.state, |
| 146 | + ) |
| 147 | + except SomeException: # Need to define the exceptions to catch here |
| 148 | + self.app.log.error("State set failed") |
| 149 | + result += 1 |
| 150 | +
|
| 151 | + try: |
| 152 | + volume_client.volume_snapshots.update( |
| 153 | + snapshot.id, |
| 154 | + **kwargs |
| 155 | + ) |
| 156 | + except SomeException: # Need to define the exceptions to catch here |
| 157 | + self.app.log.error("Update failed") |
| 158 | + result += 1 |
| 159 | +
|
| 160 | + # NOTE(dtroyer): We need to signal the error, and a non-zero return code, |
| 161 | + # without aborting prematurely |
| 162 | + if result > 0: |
| 163 | + raise SomeNonFatalException |
0 commit comments