Skip to content

Commit

Permalink
Added rest recorder
Browse files Browse the repository at this point in the history
  • Loading branch information
yaroslavNqualisystems committed Oct 25, 2019
1 parent 80bfe90 commit 17e16ed
Show file tree
Hide file tree
Showing 13 changed files with 644 additions and 45 deletions.
Empty file.
29 changes: 29 additions & 0 deletions cloudshell/recorder/rest/configuration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import os
from functools import lru_cache

import yaml

from cloudshell.recorder.rest.model import RestRequest, RestSession


class Configuration(object):
SESSION_KEY = 'Session'
RECORDS_KEY = 'Requests'

def __init__(self, config_path):
self._config_path = config_path

@property
@lru_cache()
def _config(self):
with open(self._config_path, 'r') as config:
return yaml.load(config)

def get_session(self):
return RestSession(**self._config.get(self.SESSION_KEY))

def get_requests(self):
table = []
for record_args in self._config.get(self.RECORDS_KEY):
table.append(RestRequest(**record_args))
return table
48 changes: 48 additions & 0 deletions cloudshell/recorder/rest/default.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDEYq0VRhFTYhdU
s0aCa3R5fooGe5o956zbdx2S6lXnIZIE3ButzzE28VCLoR7qIZb08+1thU6HPpLC
th0fIUQ30+tjmQhK3KPwFXpaJ32svuf15rQ5qXj34Uz3q8SXYhUZQs+zMnOTynee
9EOgdozfLsLn8XJ5iwJseLp5M5WQHMteVoR7bOlzw3Pem39hGMv2p1CCkGZR2LH5
INEKpOWAx6pR6hhltr7Mu1/pluDJRt6cVliAERYhiatQwI76ZbPY5BJ5yVvcX7Ra
7HlXDCisfHzSHDcohVQc9iSRRQQfUVGuzxWjtTz3cLcHSl5puvE4qmIyCgtI7Jqk
Sz7wSp85AgMBAAECggEBAIFOt8iaKi5NGC+dG4AkYLzepOM/33+DXD3lYIOr6IQX
hp3MkC0Rj8Ytfmtx23kuRMdlFaBXdi129gte/i3RKanYz9T0npEsVoVunvFdr1Jf
ITy4e1K8U/pjTtqFLxy+aEYJx9imD5PwbHMtQpoVdQw/Lfaq9EUzUpU5qQ7E+GW6
Z6wTO4v8t4IR0+4ETPGAZTqabV3dNrnjaJP30EI9wwk+I0XPeKzGNZQ15FqOp8tR
cS9Eyt2L1r1iWNziP0Lnz7oP0k0BhSgHsZSae7FuXtFrsxmQnggS0MeBdg2kurbk
8ZvcH9RHTH+b+ZZ5XwUCHDhl4Sp5lbxj78rgorNqeAECgYEA80aq3Ox1J5JfZHHf
kZWu/sDjdguNCEEob+A3s2YzBU2QqNsD1jrR5pDI+mXMbpvrdx29luB6CJwbdmWE
P1F0AYB9OT1BDEMlENgckT65ui7eD2a6TMYx0wGU5fys/qKjC3cKXzTghwArMRZf
HPKr/lFTqgG+ClqoNSMKQaYr8PECgYEAzqgrXzvdEGL/H9jMqi1KuQWMLszCIUYp
1iESST78njBcPDJZyRJQ8eVPv06FzgA3Eg/iWrdHxNo8PdiQt9oSJM7dbPOEHyHu
1dc4OOV63b0O4ZIWgPiUeOHwlqv5pRLxDDd/Qa24g86YbdEaZgTc0tpqyS1isDaJ
/ucyGe5xkskCgYBWqUvgm1M4n4nfzDjqMbo/AhOFT6QT1rJC+EqfW769Gt53aF27
iQSQ7+7IVE847843hp6tCpYuTv3xdURhEfETP+Rb6ZgseBbSI0o563BaBKwSLHQd
OVUyZ5PVQeeWZfVS9jr5o2qAbOz19ZQ4SbI/TFVTcH90TMsy8qKKtFle4QKBgF7n
iiuNIWOSIxnsBWmtrSA1NQCVFh4Ty8jDnVM38uluyhz2/pbBq5y7M5lmpTpKjP/l
fY0tmG8Fzh6U4zkbk7OFsNiFKMrnWXipAu/WK0vDtB7RaTZHcl/lWwjG57nwbfSv
U+jEr/UQHp5oJhht6T+IAPxstGK6WTtPz4lrIItJAoGAJcAuIN4aOinOdC1twzEK
JldVBqf2IFg/D2pSn6h1FZ4uh6gs9yIJTniBBPUHfqdeg+bez+XxXQfsuSJsAs6+
sDUzdzzQ/PsL5KZiR/XVTWditYBuMofKMlSLf7nsF/kgAolmlzqc02b0bBipNU7D
lSTW5FLznGl8c2wXdffcbzc=
-----END PRIVATE KEY-----
-----BEGIN CERTIFICATE-----
MIIDNDCCAhwCCQCj4EmTXe2D0zANBgkqhkiG9w0BAQsFADBcMQswCQYDVQQGEwJV
QTENMAsGA1UECAwES2lldjENMAsGA1UEBwwES2lldjEOMAwGA1UECgwFUXVhbGkx
CzAJBgNVBAsMAklUMRIwEAYDVQQDDAlsb2NhbGhvc3QwHhcNMTkxMDE4MTQyODQ3
WhcNMjAxMDE3MTQyODQ3WjBcMQswCQYDVQQGEwJVQTENMAsGA1UECAwES2lldjEN
MAsGA1UEBwwES2lldjEOMAwGA1UECgwFUXVhbGkxCzAJBgNVBAsMAklUMRIwEAYD
VQQDDAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDE
Yq0VRhFTYhdUs0aCa3R5fooGe5o956zbdx2S6lXnIZIE3ButzzE28VCLoR7qIZb0
8+1thU6HPpLCth0fIUQ30+tjmQhK3KPwFXpaJ32svuf15rQ5qXj34Uz3q8SXYhUZ
Qs+zMnOTynee9EOgdozfLsLn8XJ5iwJseLp5M5WQHMteVoR7bOlzw3Pem39hGMv2
p1CCkGZR2LH5INEKpOWAx6pR6hhltr7Mu1/pluDJRt6cVliAERYhiatQwI76ZbPY
5BJ5yVvcX7Ra7HlXDCisfHzSHDcohVQc9iSRRQQfUVGuzxWjtTz3cLcHSl5puvE4
qmIyCgtI7JqkSz7wSp85AgMBAAEwDQYJKoZIhvcNAQELBQADggEBALOeVzCPqxYo
JfVoVbZ842QJyPrcVn/Y+EThz2BoCpmDoca1w/EF9KuhDtdV2iFZRHKnep+w5VPJ
p8K+UH50YEkQtCHM8Budx2BCuGiInCF6zX3EC0LsmB4pC1Q38UKsycNnRr1p20nz
36DBvSLdECVUiern8EktlKzYXsOX/FgRTFoz6t8iQwHaf11XSccYyeYV4PCInck4
+YaYYXylgUUtAjGD8rRmlfEM1NiIL1RYivOGsCnRJHxHnIysdPgw7gOoQ2kD5xmZ
FX0EWTO/CldSL92+b2HqlZmg1yCdfuCpZvc7guJLaaQy3lwFuy0Z0vV+tlm/LvBm
89boXvLV61A=
-----END CERTIFICATE-----
13 changes: 13 additions & 0 deletions cloudshell/recorder/rest/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from cloudshell.recorder.rest.configuration import Configuration
from cloudshell.recorder.rest.rest_recorder import RestRecorder

if __name__ == '__main__':
record_table = "request_table_visionedge.yaml"
base_url = "https://192.168.51.68:8000/api"

conf = Configuration(record_table)

recorder = RestRecorder(base_url)
recorder.initialize(conf.get_session())
recorder.record(conf.get_requests())
recorder.save('saved_table.yaml')
30 changes: 30 additions & 0 deletions cloudshell/recorder/rest/model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
class RestSession(object):
def __init__(self, username=None, password=None, token=None, headers=None):
self.username = username
self.password = password
self.token = token
self.headers = headers


class RestRequest(object):
def __init__(self, uri, method='GET', data=None, headers=None):
self.method = method
self.uri = uri
self.data = data
self.headers = headers

def __hash__(self):
return hash(self.method) | hash(self.uri) | hash(self.data)

def __eq__(self, other):
"""
:param RestRequest other:
"""
return self.uri == other.uri and self.method == other.method and self.data == other.data


class RestResponse(object):
def __init__(self, status_code, headers, content):
self.status_code = status_code
self.headers = headers
self.content = content
106 changes: 106 additions & 0 deletions cloudshell/recorder/rest/request_handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import os
from functools import lru_cache
from http.server import BaseHTTPRequestHandler
from urllib.error import HTTPError
from urllib.parse import urlparse
import vcr
import urllib3



class RestSimHTTPRequestHandler(BaseHTTPRequestHandler):
URL = None
RECORD_PATH = None
# CASSETTE_FILE = None
CASSETTE_NAME_TEMPLATE = "{scheme}_{host}_{port}.yaml"
RECORD_MODE = False
CONN_CONT_MANAGER = None
VCR_CONT_MANAGER = None

# @property
# def _dst_url(self):
# return "{}{}".format(self.URL, self.path)

@property
@lru_cache()
def _dst_url_obj(self):
return urlparse(self.URL)

@property
def _dst_scheme(self):
return self._dst_url_obj.scheme

@property
def _dst_hostname(self):
return self._dst_url_obj.hostname

@property
def _dst_port(self):
return self._dst_url_obj.port

def _cassette_name(self):
if not self._dst_port and self._dst_scheme.lower() == 'http':
port = '80'
elif not self._dst_port:
port = '443'
else:
port = self._dst_port
return self.CASSETTE_NAME_TEMPLATE.format(scheme=self._dst_scheme, host=self._dst_hostname, port=port)

@lru_cache()
def _cassette_path(self):
if self.RECORD_PATH and os.path.isfile(self.RECORD_PATH):
return self.RECORD_PATH
elif self.RECORD_PATH and os.path.isdir(self.RECORD_PATH):
return os.path.join(self.RECORD_PATH, self._cassette_name())
else:
return self._cassette_name()

def _assign_headers(self, headers):
for key, val in headers.items():
self.send_header(key, val)
self.end_headers()

def _send_response(self, code, headers, data):
self.send_response(code)
self._assign_headers(headers)
self.wfile.write(data)

def _handle_request(self, method, handle_data=False):
# if self.RECORD_MODE:
# vcr_cass_man = vcr.use_cassette(self._cassette_path(), record_mode='new_episodes',
# match_on=('method', 'path', 'query'))
# else:
# vcr_cass_man = vcr.use_cassette(self._cassette_path(), match_on=('method', 'path', 'query'))
with self.VCR_CONT_MANAGER as cass:
if handle_data:
content_len = int(self.headers.get('Content-Length', 0))
data = self.rfile.read(content_len)
else:
data = None
try:
with urllib3.connectionpool.connection_from_url(self.URL, cert_reqs='CERT_NONE') as conn:
resp = conn.urlopen(method=method, url=self.path, headers=self.headers, body=data)
self._send_response(resp.status, resp.headers, resp.data)
except HTTPError as e:
self._send_response(e.code, e.headers, e.msg.encode())

def do_GET(self):
method = "GET"
self._handle_request(method)

def do_POST(self):
method = "POST"
self._handle_request(method, True)

def do_PUT(self):
method = "PUT"
self._handle_request(method, True)

def do_PATCH(self):
method = "PATCH"
self._handle_request(method, True)

def do_DELETE(self):
method = "DELETE"
self._handle_request(method)
17 changes: 17 additions & 0 deletions cloudshell/recorder/rest/request_table.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
Session:
username: admin
password: admin
token:
headers:
Accept: '*/*'
Authorization: MachineName=;Token=
ClientTimeZoneId: UTC
Content-Type: text/xml
DateTimeFormat: MM/dd/yyyy HH:mm
Requests:
- data: <Logon><username>admin</username><password>admin</password><domainName>Global</domainName></Logon>
method: post
uri: /ResourceManagerApiService/Logon
- data: <GetServerDateAndTime />
method: post
uri: /ResourceManagerApiService/GetServerDateAndTime
7 changes: 7 additions & 0 deletions cloudshell/recorder/rest/request_table_visionedge.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Session:
username: admin
password: admin
Requests:
- uri: /api/ports
- uri: /api/cte_ports
- uri: /api/cte_filters
108 changes: 108 additions & 0 deletions cloudshell/recorder/rest/rest_cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import logging
import os
import ssl
import sys
import urllib
from http.server import HTTPServer
from urllib.parse import urlparse

import click
import pkg_resources
import urllib3
import vcr

from cloudshell.recorder.rest.request_handler import RestSimHTTPRequestHandler

urllib3.disable_warnings()

CASSETTE_NAME_TEMPLATE = "{scheme}_{host}_{port}.yaml"
MATCH_ON = ('method', 'path', 'query')


def _enable_debug():
logging.basicConfig(stream=sys.stdout, level=logging.DEBUG,
format="%(asctime)s [%(levelname)s]: %(name)s %(module)s - %(funcName)-20s %(message)s")


@click.group(invoke_without_command=True)
@click.option(u'--version', u'-v', is_flag=True, default=False, help='Package version.')
@click.pass_context
def rest_cli(ctx, version):
"""For more information on a specific command, type migration_tool COMMAND --help"""
if version:
version = pkg_resources.get_distribution("cloudshell-recording").version
click.echo('Version: {}'.format(version))
ctx.exit(0)
else:
if not ctx.invoked_subcommand:
click.echo(ctx.get_help())


@rest_cli.command()
@click.option("--host", "-h", default="localhost",
help="Listen host. Default: localhost")
@click.option("--port", "-p", default=None,
help="Listen port. Default: dst url port")
@click.option("--scheme", "-s", default=None, help="Listen scheme, http or https, Default: dst url scheme.")
@click.option("--file-path", "-f", default=None, help="Record path. Default: current path.")
@click.option("--debug", "-d", is_flag=True, default=False, help="Enable debug.")
@click.option("--record", "-r", "record_mode", is_flag=True, default=False, help="Enable record mode.")
@click.argument(u'url', type=str, default=None, required=True)
def record(host, port, scheme, file_path, debug, record_mode, url):
if debug:
_enable_debug()

RestSimHTTPRequestHandler.URL = url
RestSimHTTPRequestHandler.RECORD_PATH = file_path
RestSimHTTPRequestHandler.RECORD_MODE = False

if record_mode:
vcr_cass_man = vcr.use_cassette(_cassette_path(url, file_path), record_mode='new_episodes',
match_on=MATCH_ON)
else:
vcr_cass_man = vcr.use_cassette(_cassette_path(url, file_path), match_on=MATCH_ON)

RestSimHTTPRequestHandler.VCR_CONT_MANAGER = vcr_cass_man

url_obj = urllib.parse.urlparse(url)
if not port and not url_obj.port:
if scheme == "http" or url_obj.scheme == "http":
port = 80
else:
port = 443
else:
port = port or url_obj.port
server = HTTPServer((host, port), RestSimHTTPRequestHandler)
if (scheme and scheme == "https") or url_obj.scheme == "https":
server.socket = ssl.wrap_socket(server.socket, server_side=True,
certfile=os.path.join(os.path.dirname(__file__), 'default.pem'))
server.serve_forever()


def _cassette_name(url):
url_obj = urllib.parse.urlparse(url)
if not url_obj.port and url_obj.scheme.lower() == 'http':
port = '80'
elif not url_obj.port:
port = '443'
else:
port = url_obj.port
return CASSETTE_NAME_TEMPLATE.format(scheme=url_obj.scheme, host=url_obj.hostname, port=port)


def _cassette_path(url, path):
if path and os.path.isfile(path):
return path
elif path and os.path.isdir(path):
return os.path.join(path, _cassette_name(url))
else:
return _cassette_name(url)


@rest_cli.command()
def simulate():
pass


if __name__ == '__main__':
rest_cli()
Loading

0 comments on commit 17e16ed

Please sign in to comment.