# Copyright 2021 99cloud # # 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. from __future__ import annotations import sys from logging import StreamHandler from pathlib import Path, PurePath from typing import Dict from urllib.parse import urlparse import click import skyline_nginx from jinja2 import Template from keystoneauth1.identity.v3 import Password from keystoneauth1.session import Session from keystoneclient.client import Client as KeystoneClient from pydantic import BaseModel from skyline_console import static_path from skyline_log import LOG, setup from skyline_nginx.config import CONF, configure class CommandException(Exception): EXIT_CODE = 1 class ProxyEndpoint(BaseModel): part: str location: str url: str def get_system_session() -> Session: auth = Password( auth_url=CONF.openstack.keystone_url, user_domain_name=CONF.openstack.system_user_domain, username=CONF.openstack.system_user_name, password=CONF.openstack.system_user_password, project_name=CONF.openstack.system_project, project_domain_name=CONF.openstack.system_project_domain, reauthenticate=True, ) return Session(auth=auth, verify=False, timeout=30) def get_proxy_endpoints() -> Dict[str, ProxyEndpoint]: ks_client = KeystoneClient( session=get_system_session(), interface=CONF.openstack.interface_type, region_name=CONF.openstack.default_region, ) endpoints_list = ks_client.endpoints.list(interface=CONF.openstack.interface_type) service_list = ks_client.services.list() services = {s.id: s.type for s in service_list} endpoints = {} for endpoint in endpoints_list: proxy = ProxyEndpoint(part="", location="", url="") region = endpoint.region service_type = services.get(endpoint.service_id) service = CONF.openstack.service_mapping.get(service_type) if service is None: continue if f"{region}-{service_type}" in endpoints: raise KeyError( f'Region "{region}" service type "{service_type}" conflict in endpoints.', ) proxy.part = f"# {region} {service}" location = PurePath("/").joinpath( CONF.openstack.nginx_prefix, region.lower(), service, ) proxy.location = f"{str(location)}/" raw_url = urlparse(endpoint.url) path = "" if raw_url.path: raw_path = PurePath(raw_url.path) if len(raw_path.parts) > 1: if ( raw_path.match("%(tenant_id)s") or raw_path.match("AUTH_%(tenant_id)s") or raw_path.match("$(project_id)s") or raw_path.match("AUTH_$(project_id)s") ): path = "" if str(raw_path.parents[1]) == "/" else raw_path.parents[1] elif raw_path.match("v[0-9]") or raw_path.match("v[0-9][.][0-9]"): path = "" if str(raw_path.parents[0]) == "/" else raw_path.parents[0] else: path = raw_path proxy.url = raw_url._replace(path=f"{str(path)}/").geturl() endpoints[f"{region}-{service_type}"] = proxy return dict(sorted(endpoints.items(), key=lambda d: d[0])) @click.command(help="Generate nginx proxy config file.") @click.option( "-o", "--output-file", "output_file_path", help=( "The path of the output file, this file is to generate a reverse proxy configuration " "file based on the openstack endpoint and should be used in the location part of nginx." ), ) @click.option( "--ssl-certfile", "ssl_certfile", help=("SSL certificate file path."), ) @click.option( "--ssl-keyfile", "ssl_keyfile", help=("SSL key file path."), ) def main(output_file_path: str, ssl_certfile: str, ssl_keyfile: str) -> None: try: configure("skyline") setup(StreamHandler(), debug=CONF.default.debug) template_file_path = ( Path(skyline_nginx.__file__).parent.joinpath("templates").joinpath("nginx.conf.j2") ) content = "" with template_file_path.open() as f: content = f.read() template = Template(content) endpoints = get_proxy_endpoints() context = { "skyline_console_static_path": static_path, "endpoints": [i.dict() for i in endpoints.values()], } if ssl_certfile: context.update(ssl_certfile=ssl_certfile) if ssl_keyfile: context.update(ssl_certfile=ssl_keyfile) result = template.render(**context) if output_file_path: with open(output_file_path, mode="w") as f: f.write(result) else: print(result) except CommandException as e: LOG.error(e) sys.exit(e.EXIT_CODE) if __name__ == "__main__": main()