diff --git a/app.py b/app.py index 47c6eac..6ab4c4b 100644 --- a/app.py +++ b/app.py @@ -1,367 +1,374 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- # # This file is part of the Keystone browser # # Copyright (c) 2017 Bryan Davis and contributors # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License as published by the Free # Software Foundation, either version 3 of the License, or (at your option) # any later version. # # This program is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # more details. # # You should have received a copy of the GNU General Public License along # with this program. If not, see . import flask import requests import werkzeug.middleware.proxy_fix from keystone_browser import zones from keystone_browser import glance from keystone_browser import keystone from keystone_browser import ldap from keystone_browser import nova from keystone_browser import puppetclasses from keystone_browser import proxies from keystone_browser import stats from keystone_browser import utils from keystone_browser import cinder from keystone_browser import neutron from keystone_browser import trove requests.utils.default_user_agent = lambda *args, **kwargs: ( "openstack-browser (tools.openstack-browser@toolforge.org)" + f" python-requests/{requests.__version__}" ) app = flask.Flask(__name__) app.wsgi_app = werkzeug.middleware.proxy_fix.ProxyFix(app.wsgi_app) @app.route("/") def home(): ctx = {} try: cached = "purge" not in flask.request.args ctx.update( { "usage": stats.usage(cached), "flavors": nova.flavors("observer", cached).values(), } ) except Exception: app.logger.exception("Error collecting information for projects") return flask.render_template("home.html", **ctx) @app.route("/project/") def projects(): ctx = {} try: cached = "purge" not in flask.request.args ctx.update( { "projects": keystone.all_projects(cached), } ) except Exception: app.logger.exception("Error collecting information for projects") return flask.render_template("projects.html", **ctx) @app.route("/server/") def servers(): ctx = {} try: cached = "purge" not in flask.request.args ctx.update( { "servers": nova.all_servers(cached), } ) except Exception: app.logger.exception("Error collecting information for projects") return flask.render_template("servers.html", **ctx) @app.route("/project/") def project(name): cached = "purge" not in flask.request.args ctx = { "project": name, } try: users = keystone.project_users_by_role(name, cached) - members = users["admin"] + users["member"] + # Create exclusive sets of users based on descending order of "power". + # member > service accounts > viewers + members = set(users["admin"]) | set(users["member"]) service_accounts = { - role: ldap.get_users_by_uid(members, cached) - for role, members in users.items() - if role in keystone.SERVICE_ACCOUNT_ROLES and len(members) > 0 + role: set(uids) - members + for role, uids in users.items() + if role in keystone.SERVICE_ACCOUNT_ROLES and len(uids) > 0 } - viewers = users["reader"] + viewers = set(users["reader"]) - members + for uids in service_accounts.values(): + viewers = viewers - uids ctx.update( { "project": name, "members": ldap.get_users_by_uid(members, cached), "viewers": ldap.get_users_by_uid(viewers, cached), - "service_accounts": service_accounts, + "service_accounts": { + role: ldap.get_users_by_uid(uids, cached) + for role, uids in service_accounts.items() + }, "servers": nova.project_servers(name, cached), "flavors": nova.flavors(name, cached), "images": glance.images(cached), "proxies": proxies.project_proxies(name, cached), "zones": zones.all_a_records(name, cached), "limits": nova.limits(name, cached), "volumes": cinder.project_volumes(name, cached), "cinder_limits": cinder.limits(name, cached), "neutron_limits": neutron.limits(name, cached), "databases": trove.project_instances(name, cached), } ) except Exception: app.logger.exception( 'Error collecting information for project "%s"', name ) return flask.render_template("project.html", **ctx) @app.route("/project//database/") def project_database(project, name): cached = "purge" not in flask.request.args ctx = { "project": project, "name": name, } try: instance = trove.instance(project, name, cached) ctx.update( { "instance": instance, "flavors": nova.flavors(project, cached), } ) except Exception: app.logger.exception( 'Error collecting information for project "%s" database "%s"', project, name, ) return flask.render_template("databaseinstance.html", **ctx) @app.route("/user/") def user(uid): ctx = { "uid": uid, } try: cached = "purge" not in flask.request.args roles = keystone.roles_for_user(uid, cached) ctx.update( { "user": ldap.get_users_by_uid([uid], cached), "projects": roles["projects"], "domain_roles": roles["domain_roles"], } ) if ctx["user"]: ctx["user"] = ctx["user"][0] except Exception: app.logger.exception('Error collecting information for user "%s"', uid) return flask.render_template("user.html", **ctx) @app.route("/server/") def server(fqdn): name, project, tld = fqdn.split(".", 2) ctx = { "fqdn": fqdn, "project": project, } try: cached = "purge" not in flask.request.args ctx.update( { "server": nova.server(fqdn, cached), "flavors": nova.flavors(project, cached), "images": glance.images(cached), "puppetclasses": puppetclasses.classes(project, fqdn, cached), "hiera": puppetclasses.hiera(project, fqdn, cached), } ) if "user_id" in ctx["server"]: user = ldap.get_users_by_uid([ctx["server"]["user_id"]], cached) if user: ctx["owner"] = user[0] except Exception: app.logger.exception( 'Error collecting information for server "%s"', fqdn ) return flask.render_template("server.html", **ctx) @app.route("/puppetclass/") def all_puppetclasses(): ctx = {} try: cached = "purge" not in flask.request.args ctx.update({"puppetclasses": puppetclasses.all_classes(cached)}) except Exception: app.logger.exception("Error collecting the list of puppet classes") return flask.render_template("puppetclasses.html", **ctx) @app.route("/puppetclass/") def puppetclass(classname): ctx = { "puppetclass": classname, } try: cached = "purge" not in flask.request.args ctx.update({"data": puppetclasses.prefixes(classname, cached)}) except Exception: app.logger.exception( 'Error collecting information for puppet class "%s"', classname ) return flask.render_template("puppetclass.html", **ctx) @app.route("/hierakey/") def hierakey(hierakey): ctx = { "hierakey": hierakey, } try: cached = "purge" not in flask.request.args ctx.update({"data": puppetclasses.hieraprefixes(hierakey, cached)}) except Exception: app.logger.exception( 'Error collecting information for hiera key "%s"', hierakey ) return flask.render_template("hierakey.html", **ctx) @app.route("/proxy/") def all_proxies(): cached = "purge" not in flask.request.args ctx = { "proxies": proxies.all_proxies(cached), } return flask.render_template("proxies.html", **ctx) @app.route("/api/projects.json") def api_projects_json(): cached = "purge" not in flask.request.args return flask.jsonify(projects=keystone.all_projects(cached)) @app.route("/api/projects.txt") def api_projects_txt(): cached = "purge" not in flask.request.args return flask.Response( "\n".join(sorted(keystone.all_projects(cached))), mimetype="text/plain" ) @app.route("/api/dsh/project/") def api_dsh_project(name): cached = "purge" not in flask.request.args servers = nova.project_servers(name, cached) dsh = [ "{}.{}.eqiad1.wikimedia.cloud".format(server["name"], name) for server in servers ] return flask.Response("\n".join(sorted(dsh)), mimetype="text/plain") @app.route("/api/dsh/servers") def api_dsh_servers(): cached = "purge" not in flask.request.args servers = nova.all_servers(cached) dsh = [ "{}.{}.eqiad1.wikimedia.cloud".format( server["name"], server["tenant_id"] ) for server in servers ] return flask.Response("\n".join(sorted(dsh)), mimetype="text/plain") @app.route("/api/dsh/puppetclass/") def api_dsh_puppet(name): cached = "purge" not in flask.request.args data = puppetclasses.prefixes(name, cached) dsh = [] for project, d in data.items(): if project == "admin": continue try: cached = "purge" not in flask.request.args servers = nova.project_servers(project, cached) except Exception: app.logger.exception( "Error collecting the list of servers for %s", project ) servers = [] for prefix in d["prefixes"]: if prefix.endswith(".cloud"): dsh.append(prefix) else: dsh.extend( [ "{}.{}.eqiad1.wikimedia.cloud".format( server["name"], project ) for server in servers if server["name"].startswith(prefix) ] ) return flask.Response("\n".join(sorted(set(dsh))), mimetype="text/plain") @app.route("/api/hierakey/") def api_hierakey(hierakey): cached = "purge" not in flask.request.args return flask.jsonify(servers=puppetclasses.hieraprefixes(hierakey, cached)) @app.errorhandler(404) def page_not_found(e): return flask.redirect(flask.url_for("projects")) @app.template_filter("contains") def contains(haystack, needle): return needle in haystack @app.template_filter("extract_hostname") def extract_hostname(backend): """Extract a hostname from a backend description.""" return proxies.parse_backend(backend).get("hostname", "404") @app.template_test() def ipv4addr(s): """Is the given string an IPv4 address?""" return utils.is_ipv4(s) if __name__ == "__main__": app.run(port=3000, debug=True)