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/11 17:11:20 UTC

[trafficcontrol] 10/21: Added -k/--insecure option; bugfixes

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 1fe55720aa970e032021bc6ed42446669d3df569
Author: ocket8888 <Br...@comcast.com>
AuthorDate: Wed Oct 3 15:41:42 2018 +0000

    Added -k/--insecure option; bugfixes
---
 .../cdn-in-a-box/ort/traffic_ops_ort/__init__.py   |  8 ++-
 .../ort/traffic_ops_ort/config_files.py            | 62 ++++++++++++++++------
 .../cdn-in-a-box/ort/traffic_ops_ort/to_api.py     | 47 +++++++++++-----
 3 files changed, 85 insertions(+), 32 deletions(-)

diff --git a/infrastructure/cdn-in-a-box/ort/traffic_ops_ort/__init__.py b/infrastructure/cdn-in-a-box/ort/traffic_ops_ort/__init__.py
index 1274c77..cf6d7f2 100644
--- a/infrastructure/cdn-in-a-box/ort/traffic_ops_ort/__init__.py
+++ b/infrastructure/cdn-in-a-box/ort/traffic_ops_ort/__init__.py
@@ -60,11 +60,13 @@ def doMain(args:argparse.Namespace) -> int:
 
 	logging.info("ATS root installation directory set to: '%s'", configuration.TS_ROOT)
 
+	configuration.VERIFY = not args.insecure
+
 	if not configuration.setTOURL(args.Traffic_Ops_URL):
 		logging.critical("Malformed or invalid Traffic_Ops_URL: '%s'", args.Traffic_Ops_URL)
 		return 1
 
-	logging.info("Traffic Ops URL '%s' set and verified")
+	logging.info("Traffic Ops URL '%s' set and verified", configuration.TO_URL)
 
 	if not configuration.setTOCredentials(args.Traffic_Ops_Login):
 		logging.critical("Traffic Ops login credentials invalid or incorrect.")
@@ -123,6 +125,10 @@ def main():
 	                         " (e.g. '/opt/trafficserver')",
 	                    type=str,
 	                    default="/")
+	parser.add_argument("-k", "--insecure",
+	                    help="Skip verification of SSL certificates for Traffic Ops connections. "\
+	                         "DON'T use this in production!",
+	                    action="store_true")
 	parser.add_argument("-v", "--version",
 	                    action="version",
 	                    version="%(prog)s v"+__version__,
diff --git a/infrastructure/cdn-in-a-box/ort/traffic_ops_ort/config_files.py b/infrastructure/cdn-in-a-box/ort/traffic_ops_ort/config_files.py
index 5ef3efe..06c69d8 100644
--- a/infrastructure/cdn-in-a-box/ort/traffic_ops_ort/config_files.py
+++ b/infrastructure/cdn-in-a-box/ort/traffic_ops_ort/config_files.py
@@ -22,6 +22,7 @@ presumably for a cache server
 
 import os
 import logging
+import typing
 
 #: Holds a set of service names that need reloaded configs, mapped to a boolean which indicates
 #: whether (:const:`True`) or not (:const:`False`) a full restart is required.
@@ -65,7 +66,7 @@ class ConfigFile():
 			self.fname = raw["fnameOnDisk"]
 			self.location = raw["location"]
 			if "apiUri" in raw:
-				self.URI = '/'.join((TO_URL, raw["apiURI"].lstrip('/')))
+				self.URI = '/'.join((TO_URL, raw["apiUri"].lstrip('/')))
 			else:
 				self.URI = raw["url"]
 			self.scope = raw["scope"]
@@ -122,7 +123,7 @@ class ConfigFile():
 			else:
 				cookies = None
 
-			self.contents = utils.getTextResponse(self.URI, cookies=cookies)
+			self.contents = utils.getTextResponse(self.URI, cookies=cookies, verify=conf.VERIFY)
 		except ValueError as e:
 			raise ConnectionError from e
 
@@ -136,23 +137,24 @@ class ConfigFile():
 		:raises OSError: if the backup directory does not exist, or a backup of this file
 			could not be written into it.
 		"""
-		from .configuration import MODE
+		from .configuration import MODE, Modes
+		from .utils import getYesNoResponse
 
 		backupfile = os.path.join(BACKUP_DIR, self.fname)
 		willClobber = False
 		if os.path.isfile(backupfile):
 			willClobber = True
 
-		if MODE == MODE.INTERACTIVE:
+		if MODE is Modes.INTERACTIVE:
 			prmpt = ("Write backup file %s%%s?" % backupfile)
 			prmpt %= " - will clobber existing file by the same name - " if willClobber else ''
-			if not getYesNoResponse(prmpt, default='Y')
+			if not getYesNoResponse(prmpt, default='Y'):
 				return
 
 		elif willClobber:
 			logging.warning("Clobbering existing backup file '%s'!", backupfile)
 
-		if MODE != MODE.REPORT:
+		if MODE is not Modes.REPORT:
 			with open(backupfile, 'w') as fp:
 				fp.write(contents)
 
@@ -165,15 +167,31 @@ class ConfigFile():
 
 		:raises OSError: when reading/writing files fails for some reason
 		"""
-		from .to_api import SERVER_INFO
-		from .configuration import MODE
+		from . import utils
+		from .configuration import MODE, Modes, SERVER_INFO
 		from .services import NEEDED_RELOADS, FILES_THAT_REQUIRE_RELOADS
 
 		finalContents = sanitizeContents(str(self))
 		logging.info("Sanitized output: \n%s", finalContents)
 
+		if not os.path.isdir(self.location):
+			if MODE is Modes.INTERACTIVE and\
+			   not utils.getYesNoResponse("Create configuration directory %s?" % self.path, 'Y'):
+				logging.warning("%s will not be created - some services may not work properly!",
+				                self.path)
+				return
+
+			logging.info("Directory %s will be created", self.location)
+			logging.info("File %s will be created", self.path)
+
+			if MODE is not Modes.REPORT:
+				os.makedirs(self.location)
+				with open(self.path, 'x') as fp:
+					fp.write(finalContents)
+				return
+
 		if not os.path.isfile(self.path):
-			if MODE == MODE.INTERACTIVE and\
+			if MODE is Modes.INTERACTIVE and\
 			   not utils.getYesNoResponse("Create configuration file %s?"%self.path, default='Y'):
 				logging.warning("%s will not be created - some services may not work properly!",
 				                self.path)
@@ -181,7 +199,7 @@ class ConfigFile():
 
 			logging.info("File %s will be created", self.path)
 
-			if MODE != MODE.REPORT:
+			if MODE is not Modes.REPORT:
 				with open(self.path, 'x') as fp:
 					fp.write(finalContents)
 				return
@@ -190,13 +208,18 @@ class ConfigFile():
 			onDiskContents = fp.readlines()
 			if filesDiffer(finalContents.splitlines(), onDiskContents):
 				self.backup(''.join(onDiskContents))
-				if MODE != MODE.REPORT:
+				if MODE is not Modes.REPORT:
 					fp.seek(0)
 					fp.truncate()
+
+					# Ensure POSIX-compliant files
+					if not finalContents.endswith('\n'):
+						finalContents += '\n'
+
 					fp.write(finalContents)
 					if self.fname in FILES_THAT_REQUIRE_RELOADS:
 						NEEDED_RELOADS.add(FILES_THAT_REQUIRE_RELOADS[self.fname])
-				logging.info("File written to %s", path)
+				logging.info("File written to %s", self.path)
 			else:
 				logging.info("File doesn't differ from disk; nothing to do")
 
@@ -218,7 +241,7 @@ def filesDiffer(a:typing.List[str], b:typing.List[str]) -> bool:
 	if len(a) != len(b):
 		return True
 
-	for l, i in enumerate(a):
+	for i, l in enumerate(a):
 		if l != b[i]:
 			return True
 
@@ -232,9 +255,14 @@ def sanitizeContents(raw:str) -> str:
 	:returns: The same contents, but with special replacement strings parsed out and HTML-encoded
 		symbols decoded to their literal values
 	"""
-	from .to_api import SERVER_INFO
+	from .configuration import SERVER_INFO
 	out = []
-	for line in raw.format(SERVER_INFO).splitlines():
+
+	# These double curly braces escape the behaviour of Python's `str.format` method to attempt
+	# to use curly brace-enclosed text as a key into a dictonary of its arguments. They'll be
+	# rendered into single braces in the output of `.format`, leaving the string ultimately
+	# unchanged in that respect.
+	for line in raw.replace('{', "{{").replace('}', "}}").format(SERVER_INFO).splitlines():
 		tmp=(" ".join(line.split())).strip() #squeezes spaces and trims leading and trailing spaces
 		tmp=tmp.replace("&amp;", '&') #decodes HTML-encoded ampersands
 		tmp=tmp.replace("&gt;", '>') #decodes HTML-encoded greater-than symbols
@@ -256,8 +284,8 @@ def initBackupDir():
 	logging.info("Initializing backup dir %s", BACKUP_DIR)
 
 	if not os.path.isdir(BACKUP_DIR):
-		if MODE != Modes.REPORT:
-			os.mkdir(backupdir)
+		if conf.MODE != conf.Modes.REPORT:
+			os.mkdir(BACKUP_DIR)
 		else:
 			logging.error("Cannot create non-existent backup dir in REPORT mode!")
 	else:
diff --git a/infrastructure/cdn-in-a-box/ort/traffic_ops_ort/to_api.py b/infrastructure/cdn-in-a-box/ort/traffic_ops_ort/to_api.py
index cc929ab..0bc1aa1 100644
--- a/infrastructure/cdn-in-a-box/ort/traffic_ops_ort/to_api.py
+++ b/infrastructure/cdn-in-a-box/ort/traffic_ops_ort/to_api.py
@@ -70,7 +70,7 @@ class ServerInfo():
 		try:
 			self.cdnId =         raw["cdnId"]
 			self.cdnName =       raw["cdnName"]
-			self.profile =       raw["profile"]
+			self.profileName =   raw["profileName"]
 			self.profileId =     raw["profileId"]
 			self.serverId =      raw["serverId"]
 			self.serverIpv4 =    raw["serverIpv4"]
@@ -109,7 +109,7 @@ class ServerInfo():
 		return fmt.replace("__SERVER_TCP_PORT__", str(self.serverTcpPort)\
 		                                          if self.serverTcpPort != 80 else "")
 
-def TOPost(uri:str, data:dict, verify:bool = True):
+def TOPost(uri:str, data:dict) -> str:
 	"""
 	POSTs the passed data in a request to the specified API endpoint
 
@@ -121,12 +121,28 @@ def TOPost(uri:str, data:dict, verify:bool = True):
 				to the request path; callers need not worry about whether the ``uri`` ought to
 				begin with a slash.
 
-	:param verify: Whether or not to verify the Traffic Ops server's SSL keys during the request
-		handshake (only has meaning if Traffic Ops is serving via HTTPS)
+	:returns: The Traffic Ops server's response to the POST request - possibly empty - as a UTF-8
+		string
 	:raises ConnectionError: when an error occurs trying to communicate with Traffic Ops
 	"""
+	from . import configuration as conf
 
-def getTOJSONResponse(uri:str, verify:bool = True) -> dict:
+	uri = '/'.join((conf.TO_URL, uri.lstrip('/')))
+	logging.info("POSTing %r to %s", data, uri)
+
+	if datetime.datetime.now().timestamp() >= conf.TO_COOKIE:
+		try:
+			conf.getNewTOCookie()
+		except PermissionError as e:
+			raise ConnectionError from e
+
+	resp = requests.post(uri, cookies=conf.TO_COOKIE, verify=conf.VERIFY, data=data)
+
+	logging.debug("Raw response from Traffic Ops: %s\n%s\n%s", resp, resp.headers, resp.content)
+
+	return resp.text
+
+def getTOJSONResponse(uri:str) -> dict:
 	"""
 	A wrapper around :func:`traffic_ops_ort.utils.getJSONResponse` that handles cookies and
 	tacks on the top-level Traffic Ops URL.
@@ -138,8 +154,6 @@ def getTOJSONResponse(uri:str, verify:bool = True) -> dict:
 				to the request path; callers need not worry about whether the ``uri`` ought to
 				begin with a slash.
 
-	:param verify: Whether or not to verify the Traffic Ops server's SSL keys during the request
-		handshake (only has meaning if Traffic Ops is serving via HTTPS)
 	:returns: The decoded JSON response as an object
 
 			.. note:: If the API response containes a 'response' object, this function will
@@ -162,7 +176,9 @@ def getTOJSONResponse(uri:str, verify:bool = True) -> dict:
 		except PermissionError as e:
 			raise ConnectionError from e
 
-	resp=utils.getJSONResponse(uri,cookies={conf.TO_COOKIE.name:conf.TO_COOKIE.value},verify=verify)
+	resp = utils.getJSONResponse(uri,
+	                             cookies = {conf.TO_COOKIE.name:conf.TO_COOKIE.value},
+	                             verify = conf.VERIFY)
 
 	if "response" in resp:
 		if "alerts" in resp:
@@ -200,7 +216,7 @@ def getUpdateStatus(host:str) -> dict:
 	if host in CACHED_UPDATE_STATUS:
 		return CACHED_UPDATE_STATUS[host]
 
-	CACHED_UPDATE_STATUS[host] = getTOJSONResponse("api/1.3/servers/%s/updateStatus" % host)
+	CACHED_UPDATE_STATUS[host] = getTOJSONResponse("api/1.3/servers/%s/update_status" % host)
 
 	return CACHED_UPDATE_STATUS[host]
 
@@ -288,20 +304,23 @@ def updateTrafficOps():
 	"""
 	Updates Traffic Ops's knowledge of this server's update status.
 	"""
-	from .configuration import MODE
+	from .configuration import MODE, Modes, HOSTNAME
 	from .utils import getYesNoResponse as getYN
 
-	if MODE == MODE.INTERACTIVE and not getYN("Update Traffic Ops?", default='Y'):
+	if MODE is Modes.INTERACTIVE and not getYN("Update Traffic Ops?", default='Y'):
 		logging.warning("Update will not be performed; you should do this manually")
 		return
 
 	logging.info("Updating Traffic Ops")
 
-	if MODE == MODE.REPORT:
+	if MODE is Modes.REPORT:
 		return
 
 	payload = {"updated": False, "reval_updated": False}
-	repsonse = utils.
+	response = TOPost("/update/%s" % HOSTNAME[0], payload)
+
+	if response:
+		logging.info("Traffic Ops response: %s", response)
 
 def getMyConfigFiles() -> typing.List[dict]:
 	"""
@@ -327,7 +346,7 @@ def getMyConfigFiles() -> typing.List[dict]:
 	except KeyError as e:
 		raise ValueError from e
 
-def getMyChkconfig() -> typing.List[dict]
+def getMyChkconfig() -> typing.List[dict]:
 	"""
 	Fetches the 'chkconfig' for this server