diff --git a/FAQ.md b/FAQ.md deleted file mode 100644 index 318b08d..0000000 --- a/FAQ.md +++ /dev/null @@ -1 +0,0 @@ -# Frequently Asked Questions diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 087f92f..0000000 --- a/LICENSE +++ /dev/null @@ -1,15 +0,0 @@ -Apache Software License 2.0 - -Copyright (c) 2021, Network to Code, LLC - -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. diff --git a/README.md b/README.md index 3be2d73..b2aaeab 100644 --- a/README.md +++ b/README.md @@ -1,37 +1,22 @@ -# Nautobot Chatops Extension Arista +# Arista CloudVision ChatOps -An extension for [Nautobot](https://github.com/nautobot/nautobot) [Chatops Plugin](https://github.com/nautobot/nautobot-plugin-chatops/) +Using the [Nautobot ChatOps](https://github.com/nautobot/nautobot-plugin-chatops/) base framework, this app adds the ability to gather tag data, device configuration, devices in a specific container, task logs, configlets, device's common vulnerabilities and exposures, and device events from Arista's CloudVision using Slack, Webex Team, MS Teams, and Mattermost. -The extension is available as a Python package in PyPI and can be installed with pip - -```shell -pip install git+https://github.com/nautobot/nautobot-plugin-chatops-arista-cloudvision.git -``` - -This ChatOps Extension to Nautobot ChatOps Plugin requires environment variables to be set up depending on if you are using a CVAAS (Cloudvision as a Service) or Cloudvision on-premise. +## Screenshots -For CVAAS the following environment variables must be set. +![cloudvision_get_active_events](https://user-images.githubusercontent.com/38091261/128059429-4e4dc269-2113-411b-9721-9ef281a361c5.PNG) -- `CVAAS_TOKEN`: Token generated from CVAAS service account. Documentation for that process can be found [here](https://www.arista.com/assets/data/pdf/qsg/qsg-books/QS_CloudVision_as_a_Service.pdf) in section 1.7 +![cloudvision_get_configlet](https://user-images.githubusercontent.com/38091261/128059458-d6395d63-6909-4219-9dcb-dff1801cbda2.PNG) -For on premise instance of Cloudvision, these environment variables must be set. +![cloudvision_get_device_cve](https://user-images.githubusercontent.com/38091261/128059481-2ff60896-81e4-46ae-992b-7d179403fe8f.PNG) -- `CVP_USERNAME`: The username that will be used to authenticate to Cloudvision. -- `CVP_PASSWORD`: The password for the configured username. -- `CVP_HOST`: The IP or hostname of the on premise Cloudvision appliance. -- `CVP_INSECURE`: If this is set to `True`, the appliance cert will be downloaded and automatically trusted. Otherwise, the appliance is expected to have a valid certificate. -- `ON_PREM`: By default this is set to False, this must be changed to `True` if using an on-prem instance of Cloudvision. +## Installation -Once you have updated your environment file, restart both nautobot and nautobot-worker +The extension is available as a Python package in PyPI and can be installed with pip +```shell +pip install nautobot-chatops-arista-cloudvision ``` -$ sudo systemctl daemon-reload -$ sudo systemctl restart nautobot nautobot-worker -``` - -## Usage - -### Nautobot Config You must first update the Nautobot configuration file with a new entry in the `PLUGINS_CONFIG` dictionary. @@ -42,30 +27,30 @@ PLUGINS_CONFIG = { 'slack_api_token': os.getenv("SLACK_API_TOKEN"), 'slack_signing_secret': os.getenv("SLACK_SIGNING_SECRET") }, - 'nautobot_plugin_chatops_cloudvision' : { + 'nautobot_chatops_arista_cloudvision' : { 'cvaas_token': os.getenv("CVAAS_TOKEN"), 'cvp_username': os.getenv("CVP_USERNAME"), 'cvp_password': os.getenv("CVP_PASSWORD"), 'cvp_host': os.getenv("CVP_HOST"), - "cvp_insecure": os.getenv("CVP_INSECURE"), + 'cvp_insecure': os.getenv("CVP_INSECURE"), 'on_prem': os.getenv("ON_PREM") } } ``` -After that, you must update environment variables depending on if you are using a CVAAS (Cloudvision as a Service) or Cloudvision on-premise. To update environment variables in Nautobot check out our blog post [here](http://blog.networktocode.com/post/creating-custom-chat-commands-using-nautobot-chatops/) +After that, you must update environment variables depending on if you are using a CVaaS (CloudVision as a Service) or CloudVision on-premise. To update environment variables in Nautobot check out our blog post [here](http://blog.networktocode.com/post/creating-custom-chat-commands-using-nautobot-chatops/) For CVAAS the following environment variables must be set. - `CVAAS_TOKEN`: Token generated from CVAAS service account. Documentation for that process can be found [here](https://www.arista.com/assets/data/pdf/qsg/qsg-books/QS_CloudVision_as_a_Service.pdf) in section 1.7 -For on premise instance of Cloudvision, these environment variables must be set. +For on premise instance of CloudVision, these environment variables must be set. -- `CVP_USERNAME`: The username that will be used to authenticate to Cloudvision. +- `CVP_USERNAME`: The username that will be used to authenticate to CloudVision. - `CVP_PASSWORD`: The password for the configured username. -- `CVP_HOST`: The IP or hostname of the on premise Cloudvision appliance. +- `CVP_HOST`: The IP or hostname of the on premise CloudVision appliance. - `CVP_INSECURE`: If this is set to `True`, the appliance cert will be downloaded and automatically trusted. Otherwise, the appliance is expected to have a valid certificate. -- `ON_PREM`: By default this is set to False, this must be changed to `True` if using an on-prem instance of Cloudvision. +- `ON_PREM`: By default this is set to False, this must be changed to `True` if using an on-prem instance of CloudVision. Once you have updated your environment file, restart both nautobot and nautobot-worker @@ -73,6 +58,7 @@ Once you have updated your environment file, restart both nautobot and nautobot- $ sudo systemctl daemon-reload $ sudo systemctl restart nautobot nautobot-worker ``` +## Usage ### Command setup @@ -87,7 +73,7 @@ The following commands are available: - `get-task-logs [task-id]`: Get the logs of the specified task. - `get-applied-configlets [filter-type] [filter-value]`: Get applied configlets to either a specified container or device. - `get-active-events [filter-type] [filter-value] [start-time] [end-time]`: Get active events in a given time frame. Filter-type can be filtered by device, type or severity. Filter-value is dynamically created based on the filter-type. Start-time accepts ISO time format as well as relative time inputs. Examples of that are `-2w`, `-2d`, `-2h` which will go back two weeks, two days and two hours, respectively. -- `get-applied-image-bundles [filter-type] [image-bundle-name]`: Gets the devices and containers an image bundle is applied to. Can also specify the `all` parameter to get a list of all the image bundles on Cloudvision. +- `get-tags [device-name]`: Get system or user tags assigned to a device. - `get-device-cve [device-name]`: Gets all the CVEs of the specified device. Can also specifiy the `all` parameter to get a count of CVE account for each device. ## Contributing @@ -103,6 +89,8 @@ The project is following Network to Code software development guideline and is l ### Development Environment +> A slack workspace is needed to test in a development environment. + The development environment can be used in 2 ways. First, with a local poetry environment if you wish to develop outside of Docker. Second, inside of a docker container. #### Invoke tasks @@ -110,7 +98,7 @@ The development environment can be used in 2 ways. First, with a local poetry en The [PyInvoke](http://www.pyinvoke.org/) library is used to provide some helper commands based on the environment. There are a few configuration parameters which can be passed to PyInvoke to override the default configuration: * `nautobot_ver`: the version of Nautobot to use as a base for any built docker containers (default: 1.0.1) -* `project_name`: the default docker compose project name (default: nautobot_plugin_chatops_cloudvision) +* `project_name`: the default docker compose project name (default: nautobot_chatops_arista_cloudvision) * `python_ver`: the version of Python to use as a base for any built docker containers (default: 3.6) * `local`: a boolean flag indicating if invoke tasks should be run on the host or inside the docker containers (default: False, commands will be run in docker containers) * `compose_dir`: the full path to a directory containing the project compose files @@ -126,7 +114,7 @@ Using PyInvoke these configuration options can be overridden using [several meth ```shell --- -nautobot_plugin_chatops_cloudvision: +nautobot_chatops_arista_cloudvision: local: true compose_files: - "docker-compose.requirements.yml" @@ -208,14 +196,6 @@ Each command can be executed with `invoke `. Environment variables `INV unittest Run Django unit tests for the plugin. ``` -## Screenshots - -![cloudvision_get_active_events](https://user-images.githubusercontent.com/38091261/128059429-4e4dc269-2113-411b-9721-9ef281a361c5.PNG) -![cloudvision_get_configlet](https://user-images.githubusercontent.com/38091261/128059458-d6395d63-6909-4219-9dcb-dff1801cbda2.PNG) -![cloudvision_get_device_cve](https://user-images.githubusercontent.com/38091261/128059481-2ff60896-81e4-46ae-992b-7d179403fe8f.PNG) - - - ## Questions For any questions or comments, please check the [FAQ](FAQ.md) first and feel free to swing by the [Network to Code slack channel](https://networktocode.slack.com/) (channel #networktocode). diff --git a/development/nautobot_config.py b/development/nautobot_config.py index 58ee4ff..5e5bbc2 100644 --- a/development/nautobot_config.py +++ b/development/nautobot_config.py @@ -244,7 +244,7 @@ def is_truthy(arg): PAGINATE_COUNT = int(os.environ.get("PAGINATE_COUNT", 50)) # Enable installed plugins. Add the name of each plugin to the list. -PLUGINS = ["nautobot_plugin_chatops_cloudvision"] +PLUGINS = ["nautobot_chatops_arista_cloudvision"] # Plugins configuration settings. These settings are used by various plugins that the user may have installed. # Each key in the dictionary is the name of an installed plugin and its value is a dictionary of settings. diff --git a/invoke.example.yml b/invoke.example.yml deleted file mode 100644 index 4d65347..0000000 --- a/invoke.example.yml +++ /dev/null @@ -1,11 +0,0 @@ ---- -nautobot_plugin_chatops_cloudvision: - project_name: "nautobot_plugin_chatops_cloudvision" - nautobot_ver: "1.0.1" - local: false - python_ver: "3.6" - compose_dir: "development" - compose_files: - - "docker-compose.requirements.yml" - - "docker-compose.base.yml" - - "docker-compose.dev.yml" diff --git a/mkdocs.yml b/mkdocs.yml deleted file mode 100644 index c7ca29b..0000000 --- a/mkdocs.yml +++ /dev/null @@ -1,23 +0,0 @@ ---- -dev_addr: "127.0.0.1:8001" -edit_uri: "edit/main/nautobot-chatops-extension-arista/docs" -site_name: "NautobotChatopsExtensionArista Documentation" -site_url: "https://nautobot-chatops-extension-arista.readthedocs.io/" -repo_url: "https://github.com/networktocode-llc/cookiecutter-ntc/tree/main/nautobot-plugin" -python: - install: - - requirements: "docs/requirements.txt" -theme: - name: "readthedocs" - navigation_depth: 4 - hljs_languages: - - "django" - - "yaml" -extra_css: - - "extra.css" -markdown_extensions: - - "admonition" - - toc: - permalink: true -nav: - - Introduction: "index.md" diff --git a/nautobot_plugin_chatops_cloudvision/__init__.py b/nautobot_chatops_arista_cloudvision/__init__.py similarity index 53% rename from nautobot_plugin_chatops_cloudvision/__init__.py rename to nautobot_chatops_arista_cloudvision/__init__.py index 1e739bf..9735d53 100644 --- a/nautobot_plugin_chatops_cloudvision/__init__.py +++ b/nautobot_chatops_arista_cloudvision/__init__.py @@ -1,4 +1,4 @@ -"""Plugin declaration for nautobot_plugin_chatops_cloudvision.""" +"""Plugin declaration for nautobot_chatops_arista_cloudvision.""" __version__ = "0.1.0" @@ -6,14 +6,14 @@ class NautobotChatopsExtensionAristaConfig(PluginConfig): - """Plugin configuration for the nautobot_plugin_chatops_cloudvision plugin.""" + """Plugin configuration for the nautobot_chatops_arista_cloudvision plugin.""" - name = "nautobot_plugin_chatops_cloudvision" - verbose_name = "Nautobot Plugin Chatops Cloudvision" + name = "nautobot_chatops_arista_cloudvision" + verbose_name = "Nautobot Chatops Arista Cloudvision Integration" version = __version__ author = "Network to Code, LLC" - description = "Nautobot Plugin Chatops Cloudvision." - base_url = "nautobot_plugin_chatops_cloudvision" + description = "Nautobot Chatops Arista Cloudvision Integration." + base_url = "nautobot_chatops_arista_cloudvision" required_settings = [] min_version = "1.0.0" max_version = "1.9999" diff --git a/nautobot_plugin_chatops_cloudvision/cvpgrpcutils.py b/nautobot_chatops_arista_cloudvision/cvpgrpcutils.py similarity index 100% rename from nautobot_plugin_chatops_cloudvision/cvpgrpcutils.py rename to nautobot_chatops_arista_cloudvision/cvpgrpcutils.py diff --git a/nautobot_chatops_arista_cloudvision/tests/__init__.py b/nautobot_chatops_arista_cloudvision/tests/__init__.py new file mode 100644 index 0000000..cbd5b73 --- /dev/null +++ b/nautobot_chatops_arista_cloudvision/tests/__init__.py @@ -0,0 +1 @@ +"""Unit tests for nautobot_chatops_arista_cloudvision plugin.""" diff --git a/nautobot_plugin_chatops_cloudvision/tests/test_api.py b/nautobot_chatops_arista_cloudvision/tests/test_api.py similarity index 94% rename from nautobot_plugin_chatops_cloudvision/tests/test_api.py rename to nautobot_chatops_arista_cloudvision/tests/test_api.py index 2135d24..9a9a312 100644 --- a/nautobot_plugin_chatops_cloudvision/tests/test_api.py +++ b/nautobot_chatops_arista_cloudvision/tests/test_api.py @@ -1,4 +1,4 @@ -"""Unit tests for nautobot_plugin_chatops_cloudvision.""" +"""Unit tests for nautobot_chatops_arista_cloudvision.""" from django.contrib.auth import get_user_model from django.test import TestCase from django.urls import reverse diff --git a/nautobot_plugin_chatops_cloudvision/tests/test_basic.py b/nautobot_chatops_arista_cloudvision/tests/test_basic.py similarity index 89% rename from nautobot_plugin_chatops_cloudvision/tests/test_basic.py rename to nautobot_chatops_arista_cloudvision/tests/test_basic.py index 883665d..5795f91 100644 --- a/nautobot_plugin_chatops_cloudvision/tests/test_basic.py +++ b/nautobot_chatops_arista_cloudvision/tests/test_basic.py @@ -3,7 +3,7 @@ import os import toml -from nautobot_plugin_chatops_cloudvision import __version__ as project_version +from nautobot_chatops_arista_cloudvision import __version__ as project_version class TestVersion(unittest.TestCase): diff --git a/nautobot_plugin_chatops_cloudvision/utils.py b/nautobot_chatops_arista_cloudvision/utils.py similarity index 62% rename from nautobot_plugin_chatops_cloudvision/utils.py rename to nautobot_chatops_arista_cloudvision/utils.py index 8081f02..6b85f13 100644 --- a/nautobot_plugin_chatops_cloudvision/utils.py +++ b/nautobot_chatops_arista_cloudvision/utils.py @@ -11,19 +11,19 @@ fullpath = os.path.abspath(__file__) directory = os.path.dirname(fullpath) -PLUGIN_SETTINGS = settings.PLUGINS_CONFIG["nautobot_plugin_chatops_cloudvision"] +PLUGIN_SETTINGS = settings.PLUGINS_CONFIG["nautobot_chatops_arista_cloudvision"] -CVAAS_TOKEN = PLUGIN_SETTINGS.get("CVAAS_TOKEN") +CVAAS_TOKEN = PLUGIN_SETTINGS.get("cvaas_token") CVAAS_ADDR = "apiserver.arista.io:443" -CVAAS_TOKEN_PATH = f"{directory}/cvaas_token.txt" -CVP_USERNAME = PLUGIN_SETTINGS.get("CVP_USERNAME") -CVP_PASSWORD = PLUGIN_SETTINGS.get("CVP_PASSWORD") -CVP_HOST = PLUGIN_SETTINGS.get("CVP_HOST") -CVP_INSECURE = PLUGIN_SETTINGS.get("CVP_INSECURE") -ON_PREM = PLUGIN_SETTINGS.get("ON_PREM") -CVP_TOKEN_PATH = f"{directory}/token.txt" -CRT_FILE_PATH = f"{directory}/cvp.crt" +CVP_USERNAME = PLUGIN_SETTINGS.get("cvp_username") +CVP_PASSWORD = PLUGIN_SETTINGS.get("cvp_password") +CVP_HOST = PLUGIN_SETTINGS.get("cvp_host") +CVP_INSECURE = PLUGIN_SETTINGS.get("cvp_insecure") +ON_PREM = PLUGIN_SETTINGS.get("on_prem") +CVP_TOKEN = PLUGIN_SETTINGS.get("cvp_token") +CVP_TOKEN_PATH = "token.txt" # nosec +CRT_FILE_PATH = "cvp.crt" CVP_LOGO_PATH = "cloudvision/CloudvisionLogoSquare.png" @@ -60,19 +60,9 @@ def prompt_for_image_bundle_name_or_all(action_id, help_text, dispatcher): def connect_cvp(): """Connect to an instance of Cloudvision.""" - if ON_PREM: + if ON_PREM.lower() == "true": clnt = CvpClient() clnt.connect([CVP_HOST], CVP_USERNAME, CVP_PASSWORD) - token_request = requests.post( - f"https://{CVP_HOST}/cvpservice/login/authenticate.do", - auth=(CVP_USERNAME, CVP_PASSWORD), - verify=False, # nosec - ) - with open("cvp_token.txt", "w") as file: - file.write(token_request.json()["sessionId"]) - if CVP_INSECURE: - with open("cvp.crt", "w") as file: - file.write(ssl.get_server_certificate((CVP_HOST, 443))) else: clnt = CvpClient() clnt.connect(["www.arista.io"], username="", password="", is_cvaas=True, api_token=CVAAS_TOKEN) # nosec @@ -231,11 +221,30 @@ def get_severity_events(filter_value): def get_active_events_data(apiserverAddr=None, token=None, certs=None, key=None, ca=None): # pylint: disable=invalid-name """Gets a list of active event types from CVP.""" - if ON_PREM: - apiserverAddr = CVP_HOST - else: - apiserverAddr = CVAAS_ADDR - token = CVAAS_TOKEN_PATH + if check_on_prem(): + apiserverAddr = f"{CVP_HOST}:8443" + get_token_crt() + + pathElts = [ + "events", + "activeEvents", + ] + query = [create_query([(pathElts, [])], "analytics")] + events = [] + with GRPCClient(apiserverAddr, token=CVP_TOKEN_PATH, ca=CRT_FILE_PATH) as client: + for batch in client.get(query): + for notif in batch["notifications"]: + for info in notif["updates"].values(): + single_event = {} + single_event["title"] = info["title"] + single_event["severity"] = info["severity"] + single_event["description"] = info["description"] + single_event["deviceId"] = get_cloudvision_devices_by_sn(info["data"]["deviceId"]) + events.append(single_event) + return events + + apiserverAddr = CVAAS_ADDR + token = CVAAS_TOKEN pathElts = [ "events", @@ -243,7 +252,7 @@ def get_active_events_data(apiserverAddr=None, token=None, certs=None, key=None, ] query = [create_query([(pathElts, [])], "analytics")] events = [] - with GRPCClient(apiserverAddr, token=token, key=key, ca=ca, certs=certs) as client: + with GRPCClient(apiserverAddr, tokenValue=token, key=key, ca=ca, certs=certs) as client: for batch in client.get(query): for notif in batch["notifications"]: for info in notif["updates"].values(): @@ -259,13 +268,52 @@ def get_active_events_data(apiserverAddr=None, token=None, certs=None, key=None, def get_active_events_data_filter( filter_type, filter_value, start_time, end_time, apiserverAddr=None, token=None, certs=None, key=None, ca=None ): - # pylint: disable=invalid-name,too-many-arguments,too-many-locals,too-many-branches,no-member + # pylint: disable=invalid-name,too-many-arguments,too-many-locals,too-many-branches,no-member, too-many-statements """Gets a list of active event types from CVP in a specific time range.""" - if ON_PREM: - apiserverAddr = CVP_HOST - else: - apiserverAddr = CVAAS_ADDR - token = CVAAS_TOKEN_PATH + if check_on_prem(): + apiserverAddr = f"{CVP_HOST}:8443" + get_token_crt() + + start = Timestamp() + if isinstance(start_time, str): + start_ts = datetime.fromisoformat(start_time) + else: + start_ts = start_time + start.FromDatetime(start_ts) + + end = Timestamp() + end_ts = datetime.fromisoformat(end_time) + end.FromDatetime(end_ts) + + pathElts = [ + "events", + "activeEvents", + ] + query = [create_query([(pathElts, [])], "analytics")] + events = [] + with GRPCClient(apiserverAddr, token=CVP_TOKEN_PATH, ca=CRT_FILE_PATH) as client: + for batch in client.get(query, start=start, end=end): + for notif in batch["notifications"]: + for info in notif["updates"].values(): + single_event = {} + single_event["title"] = info["title"] + single_event["severity"] = info["severity"] + single_event["description"] = info["description"] + single_event["deviceId"] = get_cloudvision_devices_by_sn(info["data"]["deviceId"]) + if filter_type == "severity": + if single_event["severity"] == filter_value: + events.append(single_event) + elif filter_type == "device": + if single_event["deviceId"] == filter_value: + events.append(single_event) + elif filter_type == "type": + if info["eventType"] == filter_value: + events.append(single_event) + + return events + + apiserverAddr = CVAAS_ADDR + token = CVAAS_TOKEN start = Timestamp() if isinstance(start_time, str): @@ -284,7 +332,7 @@ def get_active_events_data_filter( ] query = [create_query([(pathElts, [])], "analytics")] events = [] - with GRPCClient(apiserverAddr, token=token, key=key, ca=ca, certs=certs) as client: + with GRPCClient(apiserverAddr, tokenValue=token, key=key, ca=ca, certs=certs) as client: for batch in client.get(query, start=start, end=end): for notif in batch["notifications"]: for info in notif["updates"].values(): @@ -309,11 +357,25 @@ def get_active_events_data_filter( def get_active_severity_types(apiserverAddr=None, token=None, certs=None, key=None, ca=None): """Gets a list of active event types from CVP.""" # pylint: disable=invalid-name - if ON_PREM: - apiserverAddr = CVP_HOST - else: - apiserverAddr = CVAAS_ADDR - token = CVAAS_TOKEN_PATH + if check_on_prem(): + apiserverAddr = f"{CVP_HOST}:8443" + get_token_crt() + + pathElts = [ + "events", + "type", + ] + query = [create_query([(pathElts, [])], "analytics")] + event_types = [] + with GRPCClient(apiserverAddr, token=CVP_TOKEN_PATH, ca=CRT_FILE_PATH) as client: + for batch in client.get(query): + for notif in batch["notifications"]: + for info in notif["updates"]: + event_types.append(info) + return event_types + + apiserverAddr = CVAAS_ADDR + token = CVAAS_TOKEN pathElts = [ "events", @@ -321,7 +383,7 @@ def get_active_severity_types(apiserverAddr=None, token=None, certs=None, key=No ] query = [create_query([(pathElts, [])], "analytics")] event_types = [] - with GRPCClient(apiserverAddr, token=token, key=key, ca=ca, certs=certs) as client: + with GRPCClient(apiserverAddr, tokenValue=token, key=key, ca=ca, certs=certs) as client: for batch in client.get(query): for notif in batch["notifications"]: for info in notif["updates"]: @@ -338,42 +400,30 @@ def get_applied_tags(device_id): return result -def get_image_bundles(): - """Get image bundle from cloudvision.""" - clnt = connect_cvp() - result = clnt.api.get_image_bundles() - return result["data"] - - -def get_images(image_bundle_name=None): - """Get images that are on Cloudvision.""" - clnt = connect_cvp() - if not image_bundle_name: - result = clnt.api.get_images() - return result["data"] - combined_applied = {} - url_containers = f"/image/getImageBundleAppliedContainers.do?imageName={image_bundle_name}&startIndex=0&endIndex=0" - url_devices = f"/image/getImageBundleAppliedDevices.do?imageName={image_bundle_name}&startIndex=0&endIndex=0" - result_containers = clnt.get(url_containers) - result_devices = clnt.get(url_devices) - combined_applied["containers"] = result_containers["data"] - combined_applied["devices"] = result_devices["data"] - return combined_applied - - def get_device_bugs_data(device_id, apiserverAddr=None, token=None, certs=None, key=None, ca=None): """Get bugs associated with a device.""" # pylint: disable=invalid-name,too-many-arguments - if ON_PREM: - apiserverAddr = CVP_HOST - else: - apiserverAddr = CVAAS_ADDR - token = CVAAS_TOKEN_PATH + if check_on_prem(): + apiserverAddr = f"{CVP_HOST}:8443" + get_token_crt() + + pathElts = ["tags", "BugAlerts", "devices"] + query = [create_query([(pathElts, [])], "analytics")] + bugs = [] + with GRPCClient(apiserverAddr, token=CVP_TOKEN_PATH, ca=CRT_FILE_PATH) as client: + for batch in client.get(query): + for notif in batch["notifications"]: + if notif["updates"].get(device_id): + return notif["updates"].get(device_id) + return bugs + + apiserverAddr = CVAAS_ADDR + token = CVAAS_TOKEN pathElts = ["tags", "BugAlerts", "devices"] query = [create_query([(pathElts, [])], "analytics")] bugs = [] - with GRPCClient(apiserverAddr, token=token, key=key, ca=ca, certs=certs) as client: + with GRPCClient(apiserverAddr, tokenValue=token, key=key, ca=ca, certs=certs) as client: for batch in client.get(query): for notif in batch["notifications"]: if notif["updates"].get(device_id): @@ -384,11 +434,29 @@ def get_device_bugs_data(device_id, apiserverAddr=None, token=None, certs=None, def get_bug_info(bug_id, apiserverAddr=None, token=None): """Get detailed information about a bug given its identifier.""" # pylint: disable=invalid-name - if ON_PREM: - apiserverAddr = CVP_HOST - else: - apiserverAddr = CVAAS_ADDR - token = CVAAS_TOKEN_PATH + if check_on_prem(): + apiserverAddr = f"{CVP_HOST}:8443" + get_token_crt() + + pathElts = [ + "BugAlerts", + "bugs", + int(bug_id), + ] + query = [create_query([(pathElts, [])], "analytics")] + bug_info = {} + with GRPCClient(apiserverAddr, token=CVP_TOKEN_PATH, ca=CRT_FILE_PATH) as client: + for batch in client.get(query): + for notif in batch["notifications"]: + bug_info["identifier"] = bug_id + bug_info["summary"] = notif["updates"]["alertNote"] + bug_info["severity"] = notif["updates"]["severity"] + bug_info["versions_fixed"] = notif["updates"]["versionFixed"] + return bug_info + + apiserverAddr = CVAAS_ADDR + token = CVAAS_TOKEN + pathElts = [ "BugAlerts", "bugs", @@ -396,7 +464,7 @@ def get_bug_info(bug_id, apiserverAddr=None, token=None): ] query = [create_query([(pathElts, [])], "analytics")] bug_info = {} - with GRPCClient(apiserverAddr, token=token) as client: + with GRPCClient(apiserverAddr, tokenValue=token) as client: for batch in client.get(query): for notif in batch["notifications"]: bug_info["identifier"] = bug_id @@ -409,17 +477,54 @@ def get_bug_info(bug_id, apiserverAddr=None, token=None): def get_bug_device_report(apiserverAddr=None, token=None): """Get how many bugs each device has.""" # pylint: disable=invalid-name - if ON_PREM: - apiserverAddr = CVP_HOST - else: - apiserverAddr = CVAAS_ADDR - token = CVAAS_TOKEN_PATH + if check_on_prem(): + apiserverAddr = f"{CVP_HOST}:8443" + get_token_crt() + + pathElts = ["BugAlerts", "DevicesBugsCount"] + query = [create_query([(pathElts, [])], "analytics")] + bug_count = {} + with GRPCClient(apiserverAddr, token=CVP_TOKEN_PATH, ca=CRT_FILE_PATH) as client: + for batch in client.get(query): + for notif in batch["notifications"]: + bug_count = notif["updates"] + return bug_count + + apiserverAddr = CVAAS_ADDR + token = CVAAS_TOKEN pathElts = ["BugAlerts", "DevicesBugsCount"] query = [create_query([(pathElts, [])], "analytics")] bug_count = {} - with GRPCClient(apiserverAddr, token=token) as client: + with GRPCClient(apiserverAddr, tokenValue=token) as client: for batch in client.get(query): for notif in batch["notifications"]: bug_count = notif["updates"] return bug_count + + +def check_on_prem(): + """Checks environment variable 'on_prem'.""" + if ON_PREM.lower() == "false": + return False + return True + + +def get_token_crt(): + """Writes cert and user token to files for onprem GRPClient use.""" + if CVP_INSECURE.lower() == "true": + request = requests.post( + f"https://{CVP_HOST}/cvpservice/login/authenticate.do", + auth=(CVP_USERNAME, CVP_PASSWORD), + verify=False, # nosec + ) + else: + request = requests.post( + f"https://{CVP_HOST}/cvpservice/login/authenticate.do", auth=(CVP_USERNAME, CVP_PASSWORD) + ) + + with open("token.txt", "w") as tokenfile: + tokenfile.write(request.json()["sessionId"]) + + with open("cvp.crt", "w") as cert_file: + cert_file.write(ssl.get_server_certificate((CVP_HOST, 8443))) diff --git a/nautobot_plugin_chatops_cloudvision/worker.py b/nautobot_chatops_arista_cloudvision/worker.py similarity index 89% rename from nautobot_plugin_chatops_cloudvision/worker.py rename to nautobot_chatops_arista_cloudvision/worker.py index 9fdd752..3f2a01b 100644 --- a/nautobot_plugin_chatops_cloudvision/worker.py +++ b/nautobot_chatops_arista_cloudvision/worker.py @@ -6,7 +6,7 @@ from django.conf import settings from nautobot_chatops.workers import subcommand_of, handle_subcommands # pylint: disable=import-error from nautobot_chatops.choices import CommandStatusChoices # pylint: disable=import-error -import nautobot_plugin_chatops_cloudvision.cvpgrpcutils as grpcutils +import nautobot_chatops_arista_cloudvision.cvpgrpcutils as grpcutils from .utils import ( prompt_for_events_filter, prompt_for_device_or_container, @@ -36,7 +36,7 @@ logger = logging.getLogger("rq.worker") dir_path = os.path.dirname(os.path.realpath(__file__)) -PLUGIN_SETTINGS = settings.PLUGINS_CONFIG["nautobot_plugin_chatops_cloudvision"] +PLUGIN_SETTINGS = settings.PLUGINS_CONFIG["nautobot_chatops_arista_cloudvision"] def cloudvision_logo(dispatcher): @@ -46,19 +46,21 @@ def cloudvision_logo(dispatcher): def check_credentials(dispatcher): """Check whether to use on prem or cloud instance of Cloudvision.""" - if PLUGIN_SETTINGS.get("on_prem"): + if PLUGIN_SETTINGS.get("on_prem").lower() == "true": if ( not PLUGIN_SETTINGS.get("cvp_username") and not PLUGIN_SETTINGS.get("cvp_password") - and not PLUGIN_SETTINGS.get("cvp_url") + and not PLUGIN_SETTINGS.get("cvp_host") ): dispatcher.send_warning( - "Please ensure environment variables CVP_USERNAME, CVP_PASSWORD and CVP_URL are set." + "Please ensure environment variables CVP_USERNAME, CVP_PASSWORD and CVP_URL are set and your nautobot config file is updated." ) return False else: if not PLUGIN_SETTINGS.get("cvaas_token"): - dispatcher.send_warning("Please ensure environment variable CVAAS_TOKEN is set.") + dispatcher.send_warning( + "Please ensure environment variable CVAAS_TOKEN is set and your nautobot config file is updated." + ) return False return True @@ -83,7 +85,8 @@ def get_devices_in_container(dispatcher, container_name=None): return False dispatcher.send_markdown( - f"Standby {dispatcher.user_mention()}, I'm getting the devices from the container {container_name}." + f"Standby {dispatcher.user_mention()}, I'm getting the devices from the container {container_name}.", + ephemeral=True, ) devices = get_cloudvision_container_devices(container_name) @@ -121,7 +124,8 @@ def get_configlet(dispatcher, configlet_name=None): return False dispatcher.send_markdown( - f"Standby {dispatcher.user_mention()}, I'm getting the configuration of the {configlet_name} configlet." + f"Standby {dispatcher.user_mention()}, I'm getting the configuration of the {configlet_name} configlet.", + ephemeral=True, ) config = get_configlet_config(configlet_name) @@ -148,7 +152,8 @@ def get_device_configuration(dispatcher, device_name=None): return False dispatcher.send_markdown( - f"Stand by {dispatcher.user_mention()}, I'm getting the running configuration for {device_name}." + f"Stand by {dispatcher.user_mention()}, I'm getting the running configuration for {device_name}.", + ephemeral=True, ) device = next(device for device in device_list if device["hostname"] == device_name) @@ -177,7 +182,9 @@ def get_task_logs(dispatcher, task_id=None): return False - dispatcher.send_markdown(f"Stand by {dispatcher.user_mention()}, I'm getting the logs of task {task_id}.") + dispatcher.send_markdown( + f"Stand by {dispatcher.user_mention()}, I'm getting the logs of task {task_id}.", ephemeral=True + ) single_task = next(task for task in task_list if task["workOrderId"] == task_id) single_task_cc_id = single_task.get("ccIdV2") @@ -245,7 +252,8 @@ def get_applied_configlets(dispatcher, filter_type=None, filter_value=None): applied_configlets = get_applied_configlets_device_id(filter_value, device_list) dispatcher.send_markdown( - f"Stand by {dispatcher.user_mention()}, I'm getting the configs applied to the {filter_type} {filter_value}." + f"Stand by {dispatcher.user_mention()}, I'm getting the configs applied to the {filter_type} {filter_value}.", + ephemeral=True, ) dispatcher.send_blocks( dispatcher.command_response_header( @@ -322,7 +330,7 @@ def get_active_events(dispatcher, filter_type=None, filter_value=None, start_tim if not start_time: dispatcher.prompt_for_text( f"cloudvision get-active-events {filter_type} {filter_value}", - "Enter start time in ISO format.", + "Enter start time in ISO format or enter a relative time using 'h' for hours, 'd' for days, and 'w' for weeks. Ex: '-2d'", "Start Time", ) return False @@ -330,7 +338,7 @@ def get_active_events(dispatcher, filter_type=None, filter_value=None, start_tim if not end_time: dispatcher.prompt_for_text( f"cloudvision get-active-events {filter_type} {filter_value} {start_time}", - "Enter end time in ISO format.", + "Enter start time in ISO format or enter a relative time using 'h' for hours, 'd' for days, and 'w' for weeks. Ex: '-2d'. You may also type 'now' to use the current time.", "End Time", ) return False @@ -354,24 +362,27 @@ def get_active_events(dispatcher, filter_type=None, filter_value=None, start_tim filter_type=filter_type, filter_value=filter_value, start_time=start_time, end_time=end_time ) dispatcher.send_markdown( - f"Stand by {dispatcher.user_mention()}, I'm getting the desired events with severity level {filter_value}." + f"Stand by {dispatcher.user_mention()}, I'm getting the desired events with severity level {filter_value}.", + ephemeral=True, ) elif filter_type == "device": active_events = get_active_events_data_filter( filter_type=filter_type, filter_value=filter_value, start_time=start_time, end_time=end_time ) dispatcher.send_markdown( - f"Stand by {dispatcher.user_mention()}, I'm getting the desired events with for device {filter_value}." + f"Stand by {dispatcher.user_mention()}, I'm getting the desired events with for device {filter_value}.", + ephemeral=True, ) elif filter_type == "type": active_events = get_active_events_data_filter( filter_type=filter_type, filter_value=filter_value, start_time=start_time, end_time=end_time ) dispatcher.send_markdown( - f"Stand by {dispatcher.user_mention()}, I'm getting the desired events with for event type {filter_value}." + f"Stand by {dispatcher.user_mention()}, I'm getting the desired events with for event type {filter_value}.", + ephemeral=True, ) - dispatcher.send_markdown(f"Stand by {dispatcher.user_mention()}, I'm getting those events.") + dispatcher.send_markdown(f"Stand by {dispatcher.user_mention()}, I'm getting those events.", ephemeral=True) header = ["Title", "Severity", "Description", "Device"] rows = [(event["title"], event["severity"], event["description"], event["deviceId"]) for event in active_events] @@ -409,14 +420,19 @@ def get_tags(dispatcher, device_name=None): dispatcher.prompt_from_menu("cloudvision get-tags", "Select a device.", choices) return False - dispatcher.send_markdown(f"Stand by {dispatcher.user_mention()}, I'm getting the tags for {device_name}.") + dispatcher.send_markdown( + f"Stand by {dispatcher.user_mention()}, I'm getting the tags for {device_name}.", ephemeral=True + ) device_id = get_device_id_from_hostname(device_name) tags = grpcutils.get_device_tags(device_id, PLUGIN_SETTINGS) dispatcher.send_blocks( dispatcher.command_response_header( - "cloudvision", "get-tags", [("Device Name", device_name)], "information", cloudvision_logo(dispatcher) + "cloudvision", + "get-tags", + [("Device Name", device_name)], + "information", ) ) @@ -446,7 +462,9 @@ def get_device_cve(dispatcher, device_name=None): if device_name == "all": bug_count = get_bug_device_report() - dispatcher.send_markdown(f"Stand by {dispatcher.user_mention()}, I'm getting that CVE report for you.") + dispatcher.send_markdown( + f"Stand by {dispatcher.user_mention()}, I'm getting that CVE report for you.", ephemeral=True + ) dispatcher.send_blocks( dispatcher.command_response_header( diff --git a/nautobot_plugin_chatops_cloudvision/api/__init__.py b/nautobot_plugin_chatops_cloudvision/api/__init__.py deleted file mode 100644 index cbaaf61..0000000 --- a/nautobot_plugin_chatops_cloudvision/api/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""REST API module for nautobot_plugin_chatops_cloudvision plugin.""" diff --git a/nautobot_plugin_chatops_cloudvision/migrations/__init__.py b/nautobot_plugin_chatops_cloudvision/migrations/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/nautobot_plugin_chatops_cloudvision/tests/__init__.py b/nautobot_plugin_chatops_cloudvision/tests/__init__.py deleted file mode 100644 index 0e5dc55..0000000 --- a/nautobot_plugin_chatops_cloudvision/tests/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Unit tests for nautobot_plugin_chatops_cloudvision plugin.""" diff --git a/pyproject.toml b/pyproject.toml index 4482799..2f0e363 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,11 +13,11 @@ include = [ "README.md", ] packages = [ - { include = "nautobot_plugin_chatops_cloudvision" }, + { include = "nautobot_chatops_arista_cloudvision" }, ] [tool.poetry.plugins."nautobot.workers"] -"cloudvision" = "nautobot_plugin_chatops_cloudvision.worker:cloudvision_chatbot" +"cloudvision" = "nautobot_chatops_arista_cloudvision.worker:cloudvision_chatbot" [tool.poetry.dependencies] # Used for local development diff --git a/tasks.py b/tasks.py index a28fd1e..7bb8683 100644 --- a/tasks.py +++ b/tasks.py @@ -33,13 +33,13 @@ def is_truthy(arg): # Use pyinvoke configuration for default values, see http://docs.pyinvoke.org/en/stable/concepts/configuration.html -# Variables may be overwritten in invoke.yml or by the environment variables INVOKE_NAUTOBOT_PLUGIN_CHATOPS_CLOUDVISION_xxx -namespace = Collection("nautobot_plugin_chatops_cloudvision") +# Variables may be overwritten in invoke.yml or by the environment variables INVOKE_NAUTOBOT_CHATOPS_ARISTA_CLOUDVISION_xxx +namespace = Collection("nautobot_chatops_arista_cloudvision") namespace.configure( { - "nautobot_plugin_chatops_cloudvision": { + "nautobot_chatops_arista_cloudvision": { "nautobot_ver": "1.0.1", - "project_name": "nautobot_plugin_chatops_cloudvision", + "project_name": "nautobot_chatops_arista_cloudvision", "python_ver": "3.6", "local": False, "compose_dir": os.path.join(os.path.dirname(__file__), "development"), @@ -82,12 +82,12 @@ def docker_compose(context, command, **kwargs): **kwargs: Passed through to the context.run() call. """ build_env = { - "NAUTOBOT_VER": context.nautobot_plugin_chatops_cloudvision.nautobot_ver, - "PYTHON_VER": context.nautobot_plugin_chatops_cloudvision.python_ver, + "NAUTOBOT_VER": context.nautobot_chatops_arista_cloudvision.nautobot_ver, + "PYTHON_VER": context.nautobot_chatops_arista_cloudvision.python_ver, } - compose_command = f'docker-compose --project-name {context.nautobot_plugin_chatops_cloudvision.project_name} --project-directory "{context.nautobot_plugin_chatops_cloudvision.compose_dir}"' - for compose_file in context.nautobot_plugin_chatops_cloudvision.compose_files: - compose_file_path = os.path.join(context.nautobot_plugin_chatops_cloudvision.compose_dir, compose_file) + compose_command = f'docker-compose --project-name {context.nautobot_chatops_arista_cloudvision.project_name} --project-directory "{context.nautobot_chatops_arista_cloudvision.compose_dir}"' + for compose_file in context.nautobot_chatops_arista_cloudvision.compose_files: + compose_file_path = os.path.join(context.nautobot_chatops_arista_cloudvision.compose_dir, compose_file) compose_command += f' -f "{compose_file_path}"' compose_command += f" {command}" print(f'Running docker-compose command "{command}"') @@ -96,7 +96,7 @@ def docker_compose(context, command, **kwargs): def run_command(context, command, **kwargs): """Wrapper to run a command locally or inside the nautobot container.""" - if is_truthy(context.nautobot_plugin_chatops_cloudvision.local): + if is_truthy(context.nautobot_chatops_arista_cloudvision.local): context.run(command, **kwargs) else: # Check if netbox is running, no need to start another netbox container to run a command @@ -128,7 +128,7 @@ def build(context, force_rm=False, cache=True): if force_rm: command += " --force-rm" - print(f"Building Nautobot with Python {context.nautobot_plugin_chatops_cloudvision.python_ver}...") + print(f"Building Nautobot with Python {context.nautobot_chatops_arista_cloudvision.python_ver}...") docker_compose(context, command) @@ -220,7 +220,7 @@ def createsuperuser(context, user="admin"): ) def makemigrations(context, name=""): """Perform makemigrations operation in Django.""" - command = "nautobot-server makemigrations nautobot_plugin_chatops_cloudvision" + command = "nautobot-server makemigrations nautobot_chatops_arista_cloudvision" if name: command += f" --name {name}" @@ -292,7 +292,7 @@ def hadolint(context): @task def pylint(context): """Run pylint code analysis.""" - command = 'pylint --init-hook "import nautobot; nautobot.setup()" --rcfile pyproject.toml nautobot_plugin_chatops_cloudvision' + command = 'pylint --init-hook "import nautobot; nautobot.setup()" --rcfile pyproject.toml nautobot_chatops_arista_cloudvision' run_command(context, command) @@ -327,7 +327,7 @@ def check_migrations(context): "buffer": "Discard output from passing tests", } ) -def unittest(context, keepdb=False, label="nautobot_plugin_chatops_cloudvision", failfast=False, buffer=True): +def unittest(context, keepdb=False, label="nautobot_chatops_arista_cloudvision", failfast=False, buffer=True): """Run Nautobot unit tests.""" command = f"coverage run --module nautobot.core.cli test {label}" @@ -343,7 +343,7 @@ def unittest(context, keepdb=False, label="nautobot_plugin_chatops_cloudvision", @task def unittest_coverage(context): """Report on code test coverage as measured by 'invoke unittest'.""" - command = "coverage report --skip-covered --include 'nautobot_plugin_chatops_cloudvision/*' --omit *migrations*" + command = "coverage report --skip-covered --include 'nautobot_chatops_arista_cloudvision/*' --omit *migrations*" run_command(context, command) @@ -356,7 +356,7 @@ def unittest_coverage(context): def tests(context, failfast=False): """Run all tests for this plugin.""" # If we are not running locally, start the docker containers so we don't have to for each test - if not is_truthy(context.nautobot_plugin_chatops_cloudvision.local): + if not is_truthy(context.nautobot_chatops_arista_cloudvision.local): print("Starting Docker Containers...") start(context) # Sorted loosely from fastest to slowest