Skip to content

Commit

Permalink
version 2.2.1 changes
Browse files Browse the repository at this point in the history
  • Loading branch information
rschmied committed May 17, 2021
1 parent b2e7a48 commit 10d891b
Show file tree
Hide file tree
Showing 12 changed files with 567 additions and 69 deletions.
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "virl2_client"
version = "2.1.0"
version = "2.2.1"
description = "VIRL2 Client Library"
authors = ["Simon Knight <simknigh@cisco.com>", "Ralph Schmieder <rschmied@cisco.com>"]
license = "Apache-2.0"
Expand Down
17 changes: 16 additions & 1 deletion virl2_client/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,20 @@
from .link import Link
from .node import Node
from .node_image_definitions import NodeImageDefinitions
from .users import UserManagement
from .groups import GroupManagement
from .system import SystemManagement

__all__ = ("Interface", "Lab", "Link", "Node", "Licensing")
__all__ = (
"Interface",
"Lab",
"Link",
"Node",
"Context",
"NodeImageDefinitions",
"Licensing",
"SystemManagement",
"UserManagement",
"GroupManagement",
"TokenAuth"
)
9 changes: 9 additions & 0 deletions virl2_client/models/authentication.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ def handle_401_unauthorized(self, resp, **kwargs): # pylint: disable=W0613
self.token = None
# repeat last request that has failed
token = self.authenticate()
logger.warning("re-auth called on 401 unauthorized")
request = resp.request.copy()
request.headers["Authorization"] = "Bearer {}".format(token)
request.deregister_hook("response", self.handle_401_unauthorized)
Expand Down Expand Up @@ -82,6 +83,14 @@ def authenticate(self):
self.token = response.json()
return self.token

def logout(self, clear_all_sessions=False):
url = urljoin(self.client_library._base_url, "logout")
if clear_all_sessions:
url = url + "?clear_all_sessions=true"
response = self.client_library.session.delete(url)
response.raise_for_status()
return response.json()


class Context:
def __init__(self, base_url, requests_session=None, client_uuid=None):
Expand Down
4 changes: 2 additions & 2 deletions virl2_client/models/cl_pyats.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,8 @@ def sync_testbed(self, username, password):

testbed_yaml = self._lab.get_pyats_testbed()
data = loader.load(io.StringIO(testbed_yaml))
data.devices.terminal_server.connections.cli.username = username
data.devices.terminal_server.connections.cli.password = password
data.devices.terminal_server.credentials.default.username = username
data.devices.terminal_server.credentials.default.password = password
self._testbed = data

def run_command(self, node_label, command):
Expand Down
157 changes: 157 additions & 0 deletions virl2_client/models/groups.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
#
# Python bindings for the Cisco VIRL 2 Network Simulation Platform
#
# This file is part of VIRL 2
#
# Copyright 2021 Cisco Systems Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

import logging


logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)


class GroupManagement(object):

def __init__(self, context):
self.ctx = context

@property
def base_url(self):
return self.ctx.base_url + "groups"

def groups(self):
"""
Get the list of available groups.
:return: list of group objects
:rtype: list
"""
response = self.ctx.session.get(self.base_url)
response.raise_for_status()
return response.json()

def get_group(self, group_id):
"""
Gets the info for specified group..
:param group_id: group name
:type group_id: str
:return: group object
:rtype: dict
"""
url = self.base_url + "/{}".format(group_id)
response = self.ctx.session.get(url)
response.raise_for_status()
return response.json()

def delete_group(self, group_id):
"""
Deletes a group.
:param group_id: group name
:type group_id: str
:return: None
"""
url = self.base_url + "/{}".format(group_id)
response = self.ctx.session.delete(url)
response.raise_for_status()

def create_group(self, name, description="", members=None, labs=None):
"""
Creates a group.
:param name: group name
:type name: str
:param description: group description
:type description: str
:param members: group members
:type members: List[str]
:param labs: group labs
:type labs: List[Dict[str, str]]
:return: created group object
:rtype: dict
"""
data = {
"name": name,
"description": description,
"members": members or [],
"labs": labs or []
}
response = self.ctx.session.post(self.base_url, json=data)
response.raise_for_status()
return response.json()

def update_group(self, group_id, name=None, description=None, members=None,
labs=None):
"""
Updates a group.
:param group_id: group name
:type group_id: str
:param name: new group name
:type name: str
:param description: group description
:type description: str
:param members: group members
:type members: List[str]
:param labs: group labs
:type labs: List[Dict[str, str]]
:return: updated group object
:rtype: dict
"""
data = {}
if name is not None:
data["name"] = name
if description is not None:
data["description"] = description
if members is not None:
data["members"] = members
if labs is not None:
data["labs"] = labs
url = self.base_url + "/{}".format(group_id)
response = self.ctx.session.put(url, json=data)
response.raise_for_status()
return response.json()

def group_members(self, group_id):
"""
Gets group members.
:param group_id: group name
:type group_id: str
:return: list of users associated with this group
:rtype: List[str]
"""
url = self.base_url + "/{}/members".format(group_id)
response = self.ctx.session.get(url)
response.raise_for_status()
return response.json()

def group_labs(self, group_id):
"""
Get the list of labs that are associated with this group.
:param group_id: group name
:type group_id: str
:return: list of labs associated with this group
:rtype: List[str]
"""
url = self.base_url + "/{}/labs".format(group_id)
response = self.ctx.session.get(url)
response.raise_for_status()
return response.json()
27 changes: 27 additions & 0 deletions virl2_client/models/lab.py
Original file line number Diff line number Diff line change
Expand Up @@ -1222,3 +1222,30 @@ def download(self):
response = self.session.get(url)
response.raise_for_status()
return response.text

@property
def groups(self):
"""
Returns the groups this lab is associated with.
:return: associated groups
:rtype: List[Dict[str, str]]
"""
url = self.lab_base_url + "/groups"
response = self.session.get(url)
response.raise_for_status()
return response.json()

def update_lab_groups(self, group_list):
"""
Modifies lab / group association
:param group_list: list of objects consisting of group id and permission
:rtype: group_list: List[Dict[str, str]]
:return: updated objects consisting of group id and permission
:rtype: List[Dict[str, str]]
"""
url = self.lab_base_url + "/groups"
response = self.session.put(url, json=group_list)
response.raise_for_status()
return response.json()
12 changes: 11 additions & 1 deletion virl2_client/models/licensing.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,16 @@ def set_default_transport(self):
proxy_port=DEFAULT_PROXY_PORT,
)

def set_product_license(self, product_license):
"""
Setup a product license.
"""
url = self.base_url + "/product_license"
response = self.ctx.session.put(url, json=product_license)
response.raise_for_status()
logger.info("Product license was accepted by the agent.")
return response.status_code == 204

def get_certificate(self):
"""
Setup a licensing public certificate for internal deployment
Expand Down Expand Up @@ -230,7 +240,7 @@ def request_reservation(self):

def complete_reservation(self, authorization_code):
"""
Complete reservation by installing authorization code from CSSM.
Complete reservation by installing authorization code from SSMS.
"""
# TODO
url = self.base_url + "/reservation/complete"
Expand Down
8 changes: 7 additions & 1 deletion virl2_client/models/node.py
Original file line number Diff line number Diff line change
Expand Up @@ -402,7 +402,13 @@ def extract_configuration(self):
url = self._base_url + "/extract_configuration"
response = self.session.put(url)
response.raise_for_status()
self._config = response.json()

def console_logs(self, console_id, lines=None):
query = "?lines=%d" % lines if lines else ""
url = self._base_url + "/consoles/%d/log%s" % (console_id, query)
response = self.session.get(url)
response.raise_for_status()
return response.json()

def console_key(self):
url = self._base_url + "/keys/console"
Expand Down
49 changes: 21 additions & 28 deletions virl2_client/models/node_image_definitions.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

import logging
import os
from requests_toolbelt import MultipartEncoder, MultipartEncoderMonitor

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -141,7 +142,7 @@ def upload_image_file(self, filename, rename=None, chunk_size_mb=10):
:type filename: str
:param rename: Optional filename to rename to
:type rename: str
:param chunk_size_mb: Optional size of upload chunk (mb)
:param chunk_size_mb: Optional size of upload chunk (mb) (deprecated since 2.2.0)
:type chunk_size_mb: int
"""
url = self._base_url + "images/upload"
Expand All @@ -152,15 +153,12 @@ def upload_image_file(self, filename, rename=None, chunk_size_mb=10):
print("Uploading %s" % name)
headers = {"X-Original-File-Name": name}

total_size = os.path.getsize(filename)
mpe = MultipartEncoder(fields={"field0": (name, open(filename, "rb"))})
monitor = MultipartEncoderMonitor(mpe, progress_callback)

with open(filename, "rb") as fh:
chunk_iter = read_file_as_chunks(
fh, chunk_size_mb=chunk_size_mb, total_size=total_size
)
response = self.session.post(url, headers=headers, data=chunk_iter)
response.raise_for_status()
print("Upload completed")
response = self.session.post(url, data=monitor, headers=headers)
response.raise_for_status()
print("Upload completed")

def download_image_file_list(self):
url = self._base_url + "list_image_definition_drop_folder/"
Expand Down Expand Up @@ -240,23 +238,18 @@ def create_image_definition(self, image_id, node_definition_id, label, disk_imag
return response.json()


def read_file_as_chunks(file_object, total_size=None, chunk_size_mb=10):
# TODO: look at requests toolbelt for this
bytes_in_mb = 1024 * 1024
chunk_size = chunk_size_mb * bytes_in_mb
counter = 0
total_chunks = total_size / chunk_size
print(
"Uploading {0} MB in {1}MB chunks".format(
total_size / bytes_in_mb, chunk_size_mb
def progress_callback(monitor):
# Track progress in the monitor instance itself.
if not hasattr(monitor, "curr_progress"):
monitor.curr_progress = -1

progress = int(100 * (monitor.bytes_read / monitor.len))
progress = min(progress, 100)
# Report progress every increment of 10%
if progress > monitor.curr_progress and progress % 10 == 0:
print(
"Progress: {0} of {1} bytes ({2}%)".format(
monitor.bytes_read, monitor.len, progress
)
)
)
while True:
data = file_object.read(chunk_size)
progress = int(100 * (counter / total_chunks))
progress = min(progress, 100)
print("Progress: {0}%".format(progress))
counter += 1
if not data:
break
yield data
monitor.curr_progress = progress
Loading

0 comments on commit 10d891b

Please sign in to comment.