Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove -p option & web server support #23

Merged
merged 1 commit into from
Sep 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 0 additions & 14 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"
3 changes: 0 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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 && \
Expand Down
6 changes: 1 addition & 5 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
19 changes: 2 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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]

Expand All @@ -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)
Expand Down Expand Up @@ -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`
Expand All @@ -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/)
10 changes: 1 addition & 9 deletions cloudview/azure.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -75,12 +74,6 @@ def driver(self) -> NodeDriver:
raise LibcloudError(f"{exc}") from exc
return self._driver

def _get_instance(self, instance_id: str, params: dict) -> Instance:
instance_id = params["id"]
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()]

Expand All @@ -95,5 +88,4 @@ def _node_to_instance(self, node: Node) -> Instance:
state=node.state,
location=node.extra["location"],
extra=node.extra,
params={"id": node.id},
)
127 changes: 3 additions & 124 deletions cloudview/cloudview.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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()


Expand Down
16 changes: 4 additions & 12 deletions cloudview/ec2.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand All @@ -28,7 +27,7 @@ def get_creds() -> dict[str, str]:
return creds


class EC2(CSP):
class EC2(CSP): # pylint: disable=too-few-public-methods
"""
Class for handling EC2 stuff
"""
Expand All @@ -50,19 +49,13 @@ def __init__(self, cloud: str = "", **creds) -> None:
def _list_instances_in_region(self, region: str) -> list[Instance]:
try:
return [
self._node_to_instance(node, region)
self._node_to_instance(node)
for node in self._drivers[region].list_nodes(ex_filters=None)
]
except InvalidCredsError:
pass
return []

def _get_instance(self, instance_id: str, params: dict) -> Instance:
region = params["region"]
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(
Expand All @@ -76,7 +69,7 @@ def _get_instances(self) -> list[Instance]:
instances.extend(future.result())
return instances

def _node_to_instance(self, node: Node, region: str) -> Instance:
def _node_to_instance(self, node: Node) -> Instance:
return Instance(
provider=Provider.EC2,
cloud=self.cloud,
Expand All @@ -87,5 +80,4 @@ def _node_to_instance(self, node: Node, region: str) -> Instance:
state=node.state,
location=node.extra["availability"],
extra=node.extra,
params={"region": region},
)
18 changes: 0 additions & 18 deletions cloudview/footer.html

This file was deleted.

Loading
Loading