You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@trafficcontrol.apache.org by da...@apache.org on 2018/10/26 17:38:07 UTC
[trafficcontrol] 02/04: Created a script to generate routes to
configuration files for a sample set of servers common between two Traffic
Ops instances
This is an automated email from the ASF dual-hosted git repository.
dangogh pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/trafficcontrol.git
commit f06ebd0a4499a2912123792328174ff690e26d5e
Author: ocket8888 <oc...@gmail.com>
AuthorDate: Tue Oct 23 09:20:05 2018 -0600
Created a script to generate routes to configuration files for a sample set of servers common between two Traffic Ops instances
---
traffic_ops/testing/compare/genConfigRoutes.py | 277 +++++++++++++++++++++++++
1 file changed, 277 insertions(+)
diff --git a/traffic_ops/testing/compare/genConfigRoutes.py b/traffic_ops/testing/compare/genConfigRoutes.py
new file mode 100755
index 0000000..1542230
--- /dev/null
+++ b/traffic_ops/testing/compare/genConfigRoutes.py
@@ -0,0 +1,277 @@
+#!/usr/bin/env python3
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you 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.
+
+"""
+This script is meant to generate a list of Traffic Ops API routes that point to configuration files
+for cache servers. It verifies that servers of the same name both exist and have the same routes.
+"""
+
+import argparse
+import logging
+import os
+import random
+import time
+import typing
+import sys
+
+random.seed(time.time())
+
+#: The repository root directory
+ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", ".."))
+
+#: An absolute path to the Traffic Ops python packages (This assumes that the script is run from
+#: within the repository's normal directory structure)
+TO_LIBS_PATH = os.path.join(ROOT, "traffic_control", "clients", "python", "trafficops")
+
+sys.path.insert(0, TO_LIBS_PATH)
+sys.path.insert(0, os.path.join(TO_LIBS_PATH, "trafficops"))
+from trafficops.tosession import TOSession
+from common.restapi import LoginError, OperationError, InvalidJSONError
+
+#: A format specifier for logging output. Propagates to all imported modules.
+LOG_FMT = "%(levelname)s: %(asctime)s line %(lineno)d in %(module)s.%(funcName)s: %(message)s"
+
+__version__ = "1.0.0"
+
+def getConfigRoutesForServers(servers:typing.List[dict], inst:TOSession) \
+ -> typing.Generator[str, None, None]:
+ """
+ Generates a list of routes to the config files for a given set of servers and a given traffic
+ ops instance
+
+ :param servers: a list of server objects
+ :param inst: A valid, authenticated, and connected Traffic Ops instance
+ :returns: A list of routes to config files for the ``servers``. These will be relative to the
+ url of the ``inst``
+ """
+ for server in servers:
+ for file in inst.getServerConfigFiles(servername=server.hostName)[0].configFiles:
+ if "apiUri" in file:
+ yield file.apiUri
+ else:
+ logging.info("config file %s for server %s has non-API URI - skipping",
+ file.location, server.hostName)
+
+def getCRConfigs(A:TOSession, B:TOSession) -> typing.Generator[str, None, None]:
+ """
+ Generates a list of routes to CRConfig files for all CDNs present in both A and B
+
+ :param A: The first Traffic Ops instance
+ :param B: The second Traffic Ops instance
+ :returns: A list of routes to CRConfig files
+ """
+ cdns = {c.name for c in A.get_cdns()[0]}.intersection({c.name for c in B.get_cdns()[0]})
+
+ if not cdns:
+ logging.error("The two instances have NO CDNs in common! This almost certainly means that "\
+ "you're not doing what you want to do")
+ yield from []
+ else:
+ yield from ["CRConfig-Snapshots/%s/CRConfig.json" % cdn for cdn in cdns]
+
+
+
+def genRoutes(A:TOSession, B:TOSession) -> typing.List[str]:
+ """
+ Generates routes to check for ATS config files from two valid Traffic Ops sessions
+
+ :param A: The first Traffic Ops instance
+ :param B: The second Traffic Ops instance
+ :returns: A list of routes representative of the configuration files for a bunch of servers
+ """
+ profiles = ({p.id: p for p in A.get_profiles()[0]}, {p.id: p for p in B.get_profiles()[0]})
+ profileIds = (set(profiles[0].keys()), set(profiles[1].keys()))
+
+ # Differences and intersections:
+ for key in profileIds[0].difference(profileIds[1]):
+ del profiles[0][key]
+ logging.warning("profile %s found in %s but not in %s!", key, A.to_url, B.to_url)
+ for key in profileIds[1].difference(profileIds[0]):
+ del profiles[1][key]
+ logging.warning("profile %s found in %s but not in %s!", key, B.to_url, A.to_url)
+
+ # Now only check for identical profiles - we wouldn't expect the config files generated from
+ # different profiles to be the same.
+ commonProfiles = set()
+ for profileId, profile in profiles[0].items():
+ if profiles[1][profileId].name == profile.name:
+ commonProfiles.add((profileId, profile.name, profile.type))
+ else:
+ logging.error("profile %s is not the same profile in both instances!", profileId)
+
+ sampleServers = []
+ for profile in commonProfiles:
+ if profile[2] == "ATS_PROFILE":
+ servers = A.get_servers(query_params={"profileId": profile[0]})[0]
+ try:
+ serverIndex = random.randint(0, len(servers)-1)
+ sampleServer = servers[serverIndex]
+ del servers[serverIndex]
+ while not B.get_servers(query_params={"id": sampleServer.id})[0]:
+ logging.warning("Server %s found in %s but not in %s!", sampleServer.id,
+ A.to_url, B.to_url)
+ serverIndex = random.randint(0, len(servers)-1)
+ sampleServer = servers[serverIndex]
+ del servers[serverIndex]
+ except (IndexError, ValueError):
+ logging.error("Server list for profile %s exhausted without finding a sample!",
+ profile[1])
+ else:
+ sampleServers.append(sampleServer)
+
+ generatedRoutes = set()
+ for route in getConfigRoutesForServers(sampleServers, A):
+ if route not in generatedRoutes:
+ yield route
+ generatedRoutes.add(route)
+
+ for route in getCRConfigs(A, B):
+ if route not in generatedRoutes:
+ yield route
+ generatedRoutes.add(route)
+
+def main(kwargs:argparse.Namespace) -> int:
+ """
+ Runs the commandline specified by ``kwargs``.
+
+ :param kwargs: An object that provides the attribute namespace representing this script's
+ options. See ``genConfigRoutes.py --help`` for more information.
+ :returns: an exit code for the program
+ :raises KeyError: when ``kwargs`` does not faithfully represent a valid command line
+ """
+ global LOG_FMT
+
+ if kwargs.quiet:
+ level = logging.CRITICAL + 1
+ else:
+ level = logging.getLevelName(kwargs.log_level)
+
+ try:
+ logging.basicConfig(level=level, format=LOG_FMT)
+ logging.getLogger().setLevel(level)
+ except ValueError:
+ print("Unrecognized log level:", kwargs.log_level, file=sys.stderr)
+ return 1
+
+ instanceA = kwargs.InstanceA
+ instanceB = kwargs.InstanceB
+ loginA = kwargs.LoginA.split(':')
+ loginB = kwargs.LoginB.split(':') if kwargs.LoginB else loginA
+ verify = not kwargs.insecure
+
+ # Peel off all schemas
+ if instanceA.startswith("https://"):
+ instanceA = instanceA[8:]
+ elif instanceA.startswith("http://"):
+ instanceA = instanceA[7:]
+
+ if instanceB.startswith("https://"):
+ instanceB = instanceB[8:]
+ elif instanceB.startswith("http://"):
+ instanceB = instanceB[7:]
+
+ # Parse out port numbers, if specified
+ try:
+ if ':' in instanceA:
+ instanceA = instanceA.split(':')
+ if len(instanceA) != 2:
+ logging.critical("'%s' is not a valid Traffic Ops URL!", kwargs.InstanceA)
+ return 1
+ instanceA = {"host": instanceA[0], "port": int(instanceA[1])}
+ else:
+ instanceA = {"host": instanceA, "port": 443}
+ except TypeError as e:
+ logging.critical("'%s' is not a valid port number!", instanceA[1])
+ logging.debug("%s", e, exc_info=True, stack_info=True)
+ return 1
+
+ try:
+ if ':' in instanceB:
+ instanceB = instanceB.split(':')
+ if len(instanceB) != 2:
+ logging.critical("'%s' is not a valid Traffic Ops URL!", kwargs.InstanceB)
+ instanceB = {"host": instanceB[0], "port": int(instanceB[1])}
+ else:
+ instanceB = {"host": instanceB, "port": 443}
+ except TypeError as e:
+ logging.critical("'%s' is not a valid port number!", instanceB[1])
+ logging.debug("%s", e, exc_info=True, stack_info=True)
+ return 1
+
+ # Instantiate connections and login
+ with TOSession(host_ip=instanceA["host"], host_port=instanceA["port"], verify_cert=verify) as A,\
+ TOSession(host_ip=instanceB["host"], host_port=instanceB["port"], verify_cert=verify) as B:
+
+
+ try:
+ A.login(loginA[0], loginA[1])
+ B.login(loginB[0], loginB[1])
+ except OSError as e:
+ logging.debug("%s", e, exc_info=True, stack_info=True)
+ logging.critical("Failed to connect to Traffic Ops")
+ return 2
+ except (OperationError, LoginError) as e:
+ logging.debug("%s", e, exc_info=True, stack_info=True)
+ logging.critical("Failed to log in to Traffic Ops")
+ logging.error("Error was '%s' - are you sure your URLs and credentials are correct?", e)
+ for route in genRoutes(A, B):
+ print(route)
+
+ return 0
+
+
+if __name__ == '__main__':
+ parser = argparse.ArgumentParser(description="A simple script to generate API routes to server"\
+ " configuration files for a given pair of Traffic Ops "\
+ "instances. This, for the purpose of using the 'compare' tool",
+ formatter_class=argparse.ArgumentDefaultsHelpFormatter)
+
+ parser.add_argument("InstanceA",
+ help="The full URL of the first Traffic Ops instance",
+ type=str)
+ parser.add_argument("InstanceB",
+ help="The full URL of the second Traffic Ops instance",
+ type=str)
+ parser.add_argument("LoginA",
+ help="A login string containing credentials for logging into the first "\
+ "Traffic Ops instance (InstanceA) in the format 'username:password'.",
+ type=str)
+ parser.add_argument("LoginB",
+ help="A login string containing credentials for logging into the second "\
+ "Traffic Ops instance (InstanceB) in the format 'username:password'. "\
+ "If not given, LoginA will be re-used for the second connection",
+ type=str,
+ nargs='?')
+ parser.add_argument("-k", "--insecure",
+ help="Do not verify SSL certificate signatures against *either* Traffic "\
+ "Ops instance",
+ action="store_true")
+ parser.add_argument("-v", "--version",
+ help="Print version information and exit",
+ action="version",
+ version="%(prog)s v"+__version__)
+ parser.add_argument("-l", "--log_level",
+ help="Sets the Python log level, one of 'DEBUG', 'INFO', 'WARN', 'ERROR', "\
+ "OR 'CRITICAL'",
+ type=str,
+ default="INFO")
+ parser.add_argument("-q", "--quiet",
+ help="Suppresses all logging output - even for critical errors",
+ action="store_true")
+ args = parser.parse_args()
+ exit(main(args))