Skip to content

Commit

Permalink
Implementing REST API.
Browse files Browse the repository at this point in the history
New transport.
New flags:
* rest: boolean / Enable REST API
* ssl_verify: boolean / Do not check TLS certificate
* schema: string / "http" or "https"
* path: string / "/rpc" URL path of the API
Exception handling
  • Loading branch information
lspgn committed Jun 26, 2017
1 parent 6e71f51 commit ce03da0
Show file tree
Hide file tree
Showing 3 changed files with 240 additions and 11 deletions.
38 changes: 27 additions & 11 deletions lib/jnpr/junos/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -1056,6 +1056,10 @@ def __init__(self, *vargs, **kvargs):
# ----------------------------------------

hostname = vargs[0] if len(vargs) else kvargs.get('host')
self.rest = kvargs.get('rest', False)
self._ssl_verify = kvargs.get('ssl_verify', True)
self._schema = kvargs.get('schema', 'https')
self._path = kvargs.get('path', '/rpc')

self._port = kvargs.get('port', 830)
self._gather_facts = kvargs.get('gather_facts', True)
Expand Down Expand Up @@ -1184,17 +1188,29 @@ def open(self, *vargs, **kvargs):
(self._ssh_private_key_file is None))

# open connection using ncclient transport
self._conn = netconf_ssh.connect(
host=self._hostname,
port=self._port,
username=self._auth_user,
password=self._auth_password,
hostkey_verify=False,
key_filename=self._ssh_private_key_file,
allow_agent=allow_agent,
ssh_config=self._sshconf_lkup(),
device_params={'name': 'junos', 'local': False})
self._conn._session.add_listener(DeviceSessionListener(self))
if self.rest:
from jnpr.junos.transport.rest import Rest
self._conn = Rest(
host=self._hostname,
port=self._port,
path=self._path,
schema=self._schema,
user=self._auth_user,
password=self._auth_password,
dev=self)
self.connected = True
else:
self._conn = netconf_ssh.connect(
host=self._hostname,
port=self._port,
username=self._auth_user,
password=self._auth_password,
hostkey_verify=False,
key_filename=self._ssh_private_key_file,
allow_agent=allow_agent,
ssh_config=self._sshconf_lkup(),
device_params={'name': 'junos', 'local': False})
self._conn._session.add_listener(DeviceSessionListener(self))
except NcErrors.AuthenticationError as err:
# bad authentication credentials
raise EzErrors.ConnectAuthError(self)
Expand Down
212 changes: 212 additions & 0 deletions lib/jnpr/junos/transport/rest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
import sys
import logging
import requests
import warnings

# 3rd-party packages
from ncclient.devices.junos import JunosDeviceHandler
from lxml import etree
from ncclient.xml_ import NCElement
from jnpr.junos.device import _Connection
from ncclient import manager
from ncclient.operations.rpc import RPCReply, RPCError
import ncclient.operations.errors as NcOpErrors
import ncclient.transport.errors as NcErrors

# local modules
from jnpr.junos.rpcmeta import _RpcMetaExec
from jnpr.junos.factcache import _FactCache
from jnpr.junos import jxml as JXML
from jnpr.junos import exception as EzErrors
from jnpr.junos.ofacts import *
from jnpr.junos.decorators import timeoutDecorator, normalizeDecorator, \
ignoreWarnDecorator

logger = logging.getLogger("jnpr.junos.rest")

# -------------------------------------------------------------------------
# Rest
# -------------------------------------------------------------------------

class Rest():

def __init__(self, **kvargs):
self._device_handler = manager.make_device_handler(None)

self._tty = None
self._ofacts = {}
self.connected = False
self._skip_logout = True
self.results = dict(changed=False, failed=False, errmsg=None)

self._hostname = kvargs.get('host')
self._schema = kvargs.get('schema', 'https')
self._path = kvargs.get('path', '/rpc')
self.dev = kvargs.get('dev', None)
self._ssl_verify = kvargs.get('ssl_verify', True)
self._auth_user = kvargs.get('user', 'root')
self._auth_password = kvargs.get(
'password',
'') or kvargs.get(
'passwd',
'')
self._port = kvargs.get('port', '443')
self._mode = kvargs.get('mode', 'rest')
self._timeout = kvargs.get('timeout', '0.5')
self._normalize = kvargs.get('normalize', False)

self._attempts = kvargs.get('attempts', 10)
self._gather_facts = kvargs.get('gather_facts', False)
self._fact_style = kvargs.get('fact_style', 'new')
if self._fact_style != 'new':
warnings.warn('fact-style %s will be removed in '
'a future release.' %
(self._fact_style), RuntimeWarning)

self.rpc = _RpcMetaExec(self)
self._manages = []
self.junos_dev_handler = JunosDeviceHandler(
device_params={'name': 'junos',
'local': False})
if self._fact_style == 'old':
self.facts = self.ofacts
else:
self.facts = _FactCache(self)

def open(self, *vargs, **kvargs):
gather_facts = kvargs.get('gather_facts', self._gather_facts)
if gather_facts is True:
logger.info('facts: retrieving device facts...')
self.facts_refresh()
self.results['facts'] = self.facts
return self

def close_session(self):
return True

def close(self, skip_logout=True):
pass

@ignoreWarnDecorator
def _rpc_reply(self, rpc_cmd_e):
encode = None if sys.version < '3' else 'unicode'
rpc_cmd = etree.tostring(rpc_cmd_e, encoding=encode) \
if isinstance(rpc_cmd_e, etree._Element) else rpc_cmd_e
try:
reply = self._rpc_query(rpc_cmd)
except requests.exceptions.HTTPError as e:
logger.error('HTTP error: {}'.format(e))
raise EzErrors.ConnectError(self.dev, e)
except requests.exceptions.ConnectTimeout as e:
logger.error('ConnectTimeout error: {}'.format(e))
raise EzErrors.ConnectError(self.dev, e)
rpc_rsp_e = NCElement(reply,
self.junos_dev_handler.transform_reply()
)
return rpc_rsp_e

def _parse_multipart(self, boundary, payload):
lines = payload.split('\n')
extracted = []
enable_capture = False
enable_parsing = False
for line in lines:
# Parsing the HTTP query result.
# Delimiters are the boundaries.
# The position of the dashes indicates if beginning or end.
if '--'+boundary == line:
enable_capture = True
extracted.append([])
elif '--'+boundary+'--' == line:
enable_capture = False
enable_parsing = False
elif enable_capture:
if line == '':
enable_parsing = True
elif enable_parsing:
extracted[len(extracted)-1].append(line)
extracted_join = []
for extract in extracted:
extracted_join.append("\n".join(extract))
return extracted_join

def _parse_headers(self, headers):
content_type = headers.get('Content-Type', '')
content_type_value = content_type.split('; ')
for kv in content_type_value:
kv_list = kv.split('=')
if kv_list[0] == 'boundary':
return kv_list[1]
return None

def _rpc_query(self, cmd):
reply = requests.post('{}://{}:{}{}'.format(self._schema, self._hostname, self._port, self._path),
data = cmd,
auth = (self._auth_user, self._auth_password),
verify = self._ssl_verify,
timeout = float(self._timeout),
headers={'Accept': 'application/xml', 'Content-Type': 'application/xml'})
reply.raise_for_status()
boundary = self._parse_headers(reply.headers)
parsed = self._parse_multipart(boundary, reply.text)

if reply.ok:
self.connected = True

# Queries done using HTTP REST do not provide the RPC reply tag the NCElement expects.
return '<rpc-reply>{document}</rpc-reply>'.format(document=parsed[0])

# ------------------------------------------------------------------------
# execute
# ------------------------------------------------------------------------

@normalizeDecorator
@timeoutDecorator
def execute(self, rpc_cmd, ignore_warning=False, **kvargs):
if isinstance(rpc_cmd, str):
rpc_cmd_e = etree.XML(rpc_cmd)
elif isinstance(rpc_cmd, etree._Element):
rpc_cmd_e = rpc_cmd
else:
raise ValueError(
"Dont know what to do with rpc of type %s" %
rpc_cmd.__class__.__name__)

# invoking a bad RPC will cause a connection object exception
# will will be raised directly to the caller ... for now ...
# @@@ need to trap this and re-raise accordingly.

try:
rpc_rsp_e = self._rpc_reply(rpc_cmd_e,
ignore_warning=ignore_warning)
except NcOpErrors.TimeoutExpiredError:
# err is a TimeoutExpiredError from ncclient,
# which has no such attribute as xml.
raise EzErrors.RpcTimeoutError(self, rpc_cmd_e.tag, self.timeout)
except NcErrors.TransportError:
raise EzErrors.ConnectClosedError(self)
except RPCError as ex:
if hasattr(ex, 'xml'):
rsp = JXML.remove_namespaces(ex.xml)
message = rsp.findtext('error-message')
# see if this is a permission error
if message and message == 'permission denied':
raise EzErrors.PermissionError(cmd=rpc_cmd_e,
rsp=rsp,
errs=ex)
else:
rsp = None
raise EzErrors.RpcError(cmd=rpc_cmd_e,
rsp=rsp,
errs=ex)
# Something unexpected happened - raise it up
except Exception as err:
warnings.warn("An unknown exception occured - please report.",
RuntimeWarning)
raise

if kvargs.get('to_py'):
return kvargs['to_py'](self, rpc_rsp_e, **kvargs)
else:
return rpc_rsp_e

1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ PyYAML>=3.10
netaddr
six
pyserial
requests

0 comments on commit ce03da0

Please sign in to comment.