Skip to content
This repository has been archived by the owner on Sep 17, 2019. It is now read-only.

Define generic napalm metaclass to catch exceptions #259

Open
wants to merge 5 commits into
base: develop
Choose a base branch
from

Conversation

mirceaulinic
Copy link
Member

@mirceaulinic mirceaulinic commented Jun 5, 2017

And raise a napalm exception instead.
As discussed under napalm-automation/napalm-junos#161

Log trace:

[ERROR   ] Raised jnpr.junos.exception.ConnectClosedError
Traceback (most recent call last):
  File "/home/admin/napalm-base/napalm_base/base.py", line 51, in fun
    return meth(*args, **kwargs)
  File "/home/admin/napalm-junos/napalm_junos/junos.py", line 1069, in get_arp_table
    arp_table_raw.get()
  File "/usr/local/lib/python2.7/dist-packages/jnpr/junos/factory/optable.py", line 64, in get
    self.xml = getattr(self.RPC, self.GET_RPC)(**rpc_args)
  File "/usr/local/lib/python2.7/dist-packages/jnpr/junos/rpcmeta.py", line 336, in _exec_rpc
    return self._junos.execute(rpc, **dec_args)
  File "/usr/local/lib/python2.7/dist-packages/jnpr/junos/decorators.py", line 63, in wrapper
    result = function(*args, **kwargs)
  File "/usr/local/lib/python2.7/dist-packages/jnpr/junos/decorators.py", line 31, in wrapper
    return function(*args, **kwargs)
  File "/usr/local/lib/python2.7/dist-packages/jnpr/junos/device.py", line 695, in execute
    raise EzErrors.ConnectClosedError(self)
ConnectClosedError: ConnectClosedError(ip-172-31-9-153.us-east-2.compute.internal)
[INFO    ] Raising ConnectionClosedException instead
[ERROR   ] Traceback (most recent call last):
  File "/home/admin/salt/salt/utils/napalm.py", line 153, in call
    out = getattr(napalm_device.get('DRIVER'), method)(*args, **kwargs)
  File "/home/admin/napalm-base/napalm_base/base.py", line 51, in fun
    return meth(*args, **kwargs)
  File "/home/admin/napalm-junos/napalm_junos/junos.py", line 1069, in get_arp_table
    arp_table_raw.get()
  File "/usr/local/lib/python2.7/dist-packages/jnpr/junos/factory/optable.py", line 64, in get
    self.xml = getattr(self.RPC, self.GET_RPC)(**rpc_args)
  File "/usr/local/lib/python2.7/dist-packages/jnpr/junos/rpcmeta.py", line 336, in _exec_rpc
    return self._junos.execute(rpc, **dec_args)
  File "/usr/local/lib/python2.7/dist-packages/jnpr/junos/decorators.py", line 63, in wrapper
    result = function(*args, **kwargs)
  File "/usr/local/lib/python2.7/dist-packages/jnpr/junos/decorators.py", line 31, in wrapper
    return function(*args, **kwargs)
  File "/usr/local/lib/python2.7/dist-packages/jnpr/junos/device.py", line 695, in execute
    raise EzErrors.ConnectClosedError(self)
ConnectionClosedException: ConnectClosedError(ip-172-31-9-153.us-east-2.compute.internal)

@mirceaulinic mirceaulinic added this to the 0.25.0 milestone Jun 5, 2017
@mirceaulinic mirceaulinic requested review from ktbyers and dbarrosop June 5, 2017 14:36
And raise a napalm exception instead.
As discussed under napalm-automation/napalm-junos#161
from napalm_base import validate

log = logging.getLogger(__file__)

ERROR_MAP = {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be a class attribute and it should be empty for the base driver. Then each driver can implement it's own ERROR_MAP.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, makes sense. Just that I'm not sure yet what's the best approach... this might be a chicken and egg type thing (napalm-base depends on the driver, which depends on napalm-base). I will investigate.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

napalm-base depends on the driver

But it doesn't. It's just a class method that uses a class attribute. Your subclass just happens to override the class attribute. The parent class has no idea about it :)

err_class = ERROR_MAP[err_full_name]
log.info('Raising {} instead'.format(err_class.__name__))
raise err_class(str(error))
# Raise everything else, using the original class.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be great if the else statement here would do something like this:

https://github.com/napalm-automation/napalm-base/blob/develop/napalm_base/base.py#L87

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potentially raising an UncaughtException or something like that. Something we can distinguish easily.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good.

Copy link
Member

@dbarrosop dbarrosop left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall looks great, just proposed a few minor changes.

@mirceaulinic
Copy link
Member Author

mirceaulinic commented Jun 6, 2017

@dbarrosop @ktbyers pushed the changes as suggested.

The current approach requires the driver to define a map like:

class JunOSDriver(NetworkDriver):
    """JunOSDriver class - inherits NetworkDriver from napalm_base."""

    ERROR_MAP = {
        'jnpr.junos.exception.ConnectClosedError': napalm_base.exceptions.ConnectionClosedException,
        'jnpr.junos.exception.ConnectAuthError': napalm_base.exceptions.ConnectAuthError,
        'jnpr.junos.exception.ConnectTimeoutError': napalm_base.exceptions.ConnectTimeoutError,
        'jnpr.junos.exception.LockError': napalm_base.exceptions.LockError,
        'jnpr.junos.exception.UnlockError': napalm_base.exceptions.UnlockError,
        'jnpr.junos.exception.CommitError': napalm_base.exceptions.CommitError
    }

We can have either have the mapping between the full path, either 1:1 class, either both (although if we have both the code might become even less obvious).
Having the errors identified as str has the advantage that we don't need to import the exception class.
What do you think?

@dbarrosop
Copy link
Member

I would import the classes directly to avoid issues that are only triggered at runtime because someone misspelled a path or class name.

@ktbyers
Copy link
Contributor

ktbyers commented Jun 6, 2017

I still don't like using the metaclass. I think a decorator would be a more logical construct to use.

The thing I don't like is that it is hidden in the code. When someone looks at the code in napalm-ios, napalm-eos, et cetera, they will have no clue that this is going on.

Also the pattern we are applying falls pretty logically into the decorator pattern.

Something like

@normalize_exceptions
def commit_config(self):

The negative being we are going to explicitly have to decorate functions in each driver, but even with this I think the decorator pattern is a more logical solution than the metaclass.

We could still have normalize_exceptions code in napalm-base and have a map similar to what was proposed earlier.

Having this pattern makes it clear that we are normalizing exceptions and where this normalization applies to.

@mirceaulinic
Copy link
Member Author

mirceaulinic commented Jun 6, 2017

I'd fine with an explicit decorator.

FYI: this is exactly what this metaclass does - decorates all the methods (with _raise_napalm_error), without requiring the user to decorate them manually.
(I'm not keen on implementing it like that, and I agree it's less obvious)

@ktbyers
Copy link
Contributor

ktbyers commented Jun 6, 2017

Yes, understood...it is a trade-off of slightly more work versus hidden-behind-the-scenes.

@mirceaulinic
Copy link
Member Author

I'm fine with any of the approaches. So @ktbyers @dbarrosop please let me know which one would be preferable (if you see any advantages/disadvantages).
No rush on this, we can leave it aside a few days and think more about it...

@ktbyers
Copy link
Contributor

ktbyers commented Jun 6, 2017

Sounds good...

@dbarrosop
Copy link
Member

I'd rather use the metaclass to avoid "forgetting" about it. Just make sure that the metaclass and the wrapper function are well documented and it should be fine. The traceback should show the wrapper function if something goes bad so not super concerned about the whole stuff failing without being super explicit about it.

@ktbyers
Copy link
Contributor

ktbyers commented Jun 6, 2017

I won't put up any more objections on this decorator vs metaclass so don't hold it up because of me...

@dbarrosop
Copy link
Member

@mirceaulinic when you have the time link a PR using this. Would be great to have them hand in hand to see how it's used :)

@bewing
Copy link
Member

bewing commented Aug 22, 2017

Looks like https://pythonhosted.org/six/#six.reraise is needed for full compatibility between py2 and py3 for re-raising exceptions.

Standalone code in https://stackoverflow.com/questions/34463087/raise-exception-in-python-2-x-and-3-x

@mirceaulinic
Copy link
Member Author

Hi @bewing - thanks for suggestion. I recall I had some changes not pushed yet, hope I didn't stash them. Let me revisit this somewhere next week.

@mirceaulinic mirceaulinic modified the milestones: 0.25.0, 0.27.0 Oct 7, 2017
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants