diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4cb9dd9..bde6a94 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -49,8 +49,6 @@ jobs: run: make test - name: Upload coverage to Codecov uses: codecov/codecov-action@v3 - - name: Selenium test - run: make selenium shellcheck: name: Shellcheck @@ -61,15 +59,3 @@ jobs: uses: ludeeus/action-shellcheck@master with: scandir: . - - docker: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - uses: docker/setup-buildx-action@v2 - - name: test docker image - run: | - docker compose -f examples/docker-compose.yml --project-directory . build --pull - PASS=testing docker compose -f examples/docker-compose.yml --project-directory . up -d - sleep 10 - test "$(curl -4k -u test:testing https://localhost:8443/test)" = "OK" diff --git a/Dockerfile b/Dockerfile index 66b6483..c3c8114 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,10 +4,7 @@ RUN zypper addrepo https://download.opensuse.org/repositories/SUSE:/CA/openSUSE_ zypper --gpg-auto-import-keys -n install ca-certificates-suse && \ zypper -n install \ python3-apache-libcloud \ - python3-cachetools \ python3-cryptography \ - python3-Jinja2 \ - python3-pyramid \ python3-python-dateutil \ python3-pytz \ python3-PyYAML && \ diff --git a/Makefile b/Makefile index 051bfb9..eaf869c 100644 --- a/Makefile +++ b/Makefile @@ -18,11 +18,7 @@ pylint: .PHONY: test test: - @SKIP_SELENIUM=1 TZ=Europe/Berlin pytest --capture=sys -v --cov --cov-report term-missing - -.PHONY: selenium -selenium: - @pytest tests/test_selenium.py + TZ=Europe/Berlin pytest --capture=sys -v --cov --cov-report term-missing .PHONY: mypy mypy: diff --git a/README.md b/README.md index e0403a4..6ecb812 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Docker image available at `ghcr.io/ricardobranco777/cloudview:latest` ## Usage ``` -usage: cloudview.py [-h] [-c CONFIG] [-f FORMAT] [-l {none,debug,info,warning,error,critical}] [-o {text,html,json}] [-p PORT] [-P {ec2,gce,azure_arm,openstack}] [-r] +usage: cloudview.py [-h] [-c CONFIG] [-f FORMAT] [-l {none,debug,info,warning,error,critical}] [-o {text,json}] [-P {ec2,gce,azure_arm,openstack}] [-r] [-s {name,state,time}] [-S {error,migrating,normal,paused,pending,rebooting,reconfiguring,running,starting,stopped,stopping,suspended,terminated,unknown,updating}] [-t TIME_FORMAT] [-v] [--version] @@ -25,9 +25,8 @@ options: output fields (default: provider,name,size,state,time,location) -l {none,debug,info,warning,error,critical}, --log {none,debug,info,warning,error,critical} logging level (default: error) - -o {text,html,json}, --output {text,html,json} + -o {text,json}, --output {text,json} output type (default: text) - -p PORT, --port PORT run a web server on specified port (default: None) -P {ec2,gce,azure_arm,openstack}, --providers {ec2,gce,azure_arm,openstack} list only specified providers (default: None) -r, --reverse reverse sort (default: False) @@ -59,16 +58,6 @@ NOTES: The [cloudview](scripts/cloudview) script scans `clouds.yaml` and environment variables to execute the proper `docker` command. -## To run the web server with Docker Compose: - -If you have a TLS key pair, put the certificates in `cert.pem`, the private key in `key.pem` and the file containing the passphrase to the private key in `key.txt`. Then edit the [docker-compose.yml](examples/docker-compose.yml) file to mount the directory to `/etc/nginx/ssl` in the container like this: `- "/path/to/tls:/etc/nginx/ssl:ro"`. Set and export the `NGINX_HOST` environment variable with the FQDN of your host. - -For HTTP Basic Authentication, create a file named `auth.htpasswd` in the same directory with the TLS key pair. - -If you don't have a TLS key pair, a self-signed certificate and a random password for logging in will be generated. You can see the latter with `docker compose logs`. The user is `test`. - -After running `docker compose build` & `docker compose up -d` you can browse to [https://localhost:8443](https://localhost:8443) - ## Debugging - For debugging you can set the `LIBCLOUD_DEBUG` environment variable to a path like `/dev/stderr` @@ -79,7 +68,3 @@ After running `docker compose build` & `docker compose up -d` you can browse to - [Azure](https://libcloud.readthedocs.io/en/stable/compute/drivers/azure_arm.html) - [GCE](https://libcloud.readthedocs.io/en/stable/compute/drivers/gce.html) - [Openstack](https://libcloud.readthedocs.io/en/stable/compute/drivers/openstack.html) - -## Similar projects - - - [public cloud watch](https://github.com/SUSE/pcw/) diff --git a/cloudview/azure.py b/cloudview/azure.py index dbbdd99..90216d2 100644 --- a/cloudview/azure.py +++ b/cloudview/azure.py @@ -7,13 +7,12 @@ import os from functools import cached_property -from cachetools import cached, TTLCache from libcloud.compute.base import Node, NodeDriver from libcloud.compute.providers import get_driver from libcloud.compute.types import Provider, LibcloudError from requests.exceptions import RequestException -from cloudview.instance import Instance, CSP, CACHED_SECONDS +from cloudview.instance import Instance, CSP from cloudview.utils import utc_date @@ -80,7 +79,6 @@ def _get_instance(self, instance_id: str, params: dict) -> Instance: node = self.driver.ex_get_node(instance_id) return self._node_to_instance(node) - @cached(cache=TTLCache(maxsize=1, ttl=CACHED_SECONDS)) def _get_instances(self) -> list[Instance]: return [self._node_to_instance(node) for node in self.driver.list_nodes()] diff --git a/cloudview/cloudview.py b/cloudview/cloudview.py index 1ff39b8..08d8c0c 100644 --- a/cloudview/cloudview.py +++ b/cloudview/cloudview.py @@ -8,17 +8,8 @@ import logging import sys from concurrent.futures import ThreadPoolExecutor -from json import JSONEncoder -from io import StringIO from operator import itemgetter from typing import Any -from urllib.parse import urlencode, quote, unquote - -from wsgiref.simple_server import make_server -from pyramid.view import view_config -from pyramid.config import Configurator -from pyramid.response import Response -from pyramid.request import Request import yaml from libcloud.compute.types import Provider, LibcloudError @@ -93,114 +84,21 @@ def print_instances(client: CSP) -> None: ) for instance in instances: instance.provider = "/".join([instance.provider, instance.cloud]) - if args.output == "html": - params = urlencode(instance.params) - resource = "/".join([instance.provider.lower(), f"{instance.id}?{params}"]) - instance.href = f"instance/{resource}" assert not isinstance(instance.time, str) instance.time = dateit(instance.time, args.time) Output().info(instance) -def print_info() -> Response | None: +def print_info() -> None: """ Print information about instances """ clients = get_clients(config_file=args.config) - sys.stdout = StringIO() if args.port else sys.stdout Output().header() if len(clients) > 0: with ThreadPoolExecutor(max_workers=len(clients)) as executor: executor.map(print_instances, clients) Output().footer() - if args.port: - response = sys.stdout.getvalue() # type: ignore - sys.stdout.close() - return response - return None - - -def handle_requests(request: Request) -> Response | None: - """ - Handle HTTP requests - """ - logging.info(request) - response = print_info() - return Response(response) - - -def test(request: Request | None = None) -> Response | None: - """ - Used for testing - """ - if request: - logging.info(request) - response = "OK" - return Response(response) - return None - - -def not_found() -> Response: - """ - Not found! - """ - return Response("Not found!", status=404) - - -def valid_elem(elem: str) -> bool: - """ - Validates URL path component - """ - return ( - 0 < len(elem) < 64 - and elem.isascii() - and "/" not in elem - and elem == unquote(quote(elem, safe="")) - ) - - -@view_config(route_name="instance") -def handle_instance(request: Request) -> Response: - """ - Handle HTTP requests for instances - """ - logging.info(request) - provider = request.matchdict["provider"] - cloud = request.matchdict["cloud"] - instance_id = request.matchdict["instance_id"] - if ( - provider not in PROVIDERS - or not valid_elem(cloud) - or not valid_elem(instance_id) - ): - return not_found() - client = list(get_clients(config_file=args.config, provider=provider, cloud=cloud))[ - 0 - ] - if client is None: - return not_found() - if client is not None: - info = client.get_instance(instance_id, **request.params) - if info is None: - return not_found() - response = JSONEncoder(default=str, indent=4, sort_keys=True).encode(info.extra) - return Response(response, content_type="application/json; charset=utf-8") - - -def web_server() -> None: - """ - Setup the WSGI server - """ - with Configurator() as config: - config.add_route("handle_requests", "/") - config.add_view(handle_requests, route_name="handle_requests") - config.add_route("test", "/test") - config.add_view(test, route_name="test") - config.add_route("instance", "instance/{provider}/{cloud}/{instance_id}") - config.scan() - app = config.make_wsgi_app() - server = make_server("0.0.0.0", args.port, app) - server.serve_forever() def parse_args() -> argparse.Namespace: @@ -230,12 +128,9 @@ def parse_args() -> argparse.Namespace: "-o", "--output", default="text", - choices=["text", "html", "json"], + choices=["text", "json"], help="output type", ) - argparser.add_argument( - "-p", "--port", type=port_number, help="run a web server on specified port" - ) argparser.add_argument( "-P", "--providers", @@ -266,15 +161,6 @@ def parse_args() -> argparse.Namespace: return argparser.parse_args() -def port_number(port: str) -> int: - """ - Check port argument - """ - if port.isdigit() and 1 <= int(port) <= 65535: - return int(port) - raise argparse.ArgumentTypeError(f"{port} is an invalid port number") - - def main() -> None: """ Main function @@ -309,14 +195,7 @@ def main() -> None: if args.verbose: keys |= {"id": ""} - if args.port: - args.output = "html" - Output(type=args.output.lower(), keys=keys, refresh_seconds=600) - - if args.port: - web_server() - sys.exit(1) - + Output(type=args.output.lower(), keys=keys) print_info() diff --git a/cloudview/ec2.py b/cloudview/ec2.py index dcfcda1..03ec55c 100644 --- a/cloudview/ec2.py +++ b/cloudview/ec2.py @@ -7,12 +7,11 @@ import os import concurrent.futures -from cachetools import cached, TTLCache from libcloud.compute.base import Node, NodeDriver from libcloud.compute.providers import get_driver from libcloud.compute.types import Provider, LibcloudError, InvalidCredsError -from cloudview.instance import Instance, CSP, CACHED_SECONDS +from cloudview.instance import Instance, CSP from cloudview.utils import utc_date @@ -62,7 +61,6 @@ def _get_instance(self, instance_id: str, params: dict) -> Instance: node = self._drivers[region].list_nodes(ex_node_ids=[instance_id])[0] return self._node_to_instance(node, region) - @cached(cache=TTLCache(maxsize=1, ttl=CACHED_SECONDS)) def _get_instances(self) -> list[Instance]: instances = [] with concurrent.futures.ThreadPoolExecutor( diff --git a/cloudview/footer.html b/cloudview/footer.html deleted file mode 100644 index 6754d21..0000000 --- a/cloudview/footer.html +++ /dev/null @@ -1,18 +0,0 @@ -
Last updated:
- - - -