'''
Created on Nov 20, 2017
@author: Charlie McLouth
'''
import logging
from p4rest.util import osmakedirs, JSON_DATETIME_FMT, rmtree, APP_NAME
from p4rest.exceptions import P4RestSessionError, P4RestSessionInvalidError
from datetime import datetime, timedelta
import os.path
from p4rest.p4 import P4
import hashlib
logger = logging.getLogger(__name__)
SESSION_HEADER_NAME = "p4rest.key"
class P4RestSession(dict):
'''Encapsulates a Session as a dict'''
@staticmethod
def hashkey(*args, **kwargs):
'''generate hash from the supplied arguments'''
logger.debug("hashkey() enter")
h = hashlib.sha256()
for arg in args:
if not isinstance(arg, str):
arg = str(arg)
h.update(bytes(arg, encoding="utf-8"))
for k,v in kwargs.items():
arg = k
if not isinstance(arg, str):
arg = str(arg)
h.update(bytes(arg, encoding="utf-8"))
arg = v
if not isinstance(arg, str):
arg = str(arg)
h.update(bytes(arg, encoding="utf-8"))
logger.debug("hashkey() exit")
return h.hexdigest()
@staticmethod
def hashPrivateKey(clientip, publickey):
return P4RestSession.hashkey(clientip, publickey)
@staticmethod
def createFromKey(clientip, publickey, workdir, sessionfile):
'''Load session data provided by clientip and publickey
raises P4RestSessionError if required data not provided or
if unable to read from session file or validation fails
'''
logger.debug("createFromKey() enter")
if workdir is None or sessionfile is None or clientip is None \
or publickey is None:
raise P4RestSessionInvalidError("invalid session")
if len(workdir) < 1 or len(sessionfile ) < 1 or len(clientip ) < 1 \
or len(publickey ) < 1:
raise P4RestSessionInvalidError("invalid session")
privatekey = P4RestSession.hashPrivateKey(clientip, publickey)
logger.debug("clientip:{};publickey:{};privatekey:{};".format(clientip, publickey, privatekey))
filename = os.path.join(workdir, privatekey, sessionfile)
try:
session = P4RestSession.createFromFile(filename)
except:
message = "invalid session"
logger.debug(message, exc_info=True)
raise P4RestSessionInvalidError(message)
# session is valid, although it may be expired. leave that verification
# to the caller
logger.debug("createFromKey() exit")
return session
@staticmethod
def createFromFile(filename):
'''Load session data from disk
raises P4RestSessionError if unable to read from session file or
validation fails
'''
logger.debug("createFromFile() enter")
theinstance = P4RestSession({"sessionfile": filename})
theinstance.load(bValidate=False)
theclass = theinstance.setdefault("SESSIONTYPE", "P4RestSession")
if theclass != "P4RestSession":
theinstance = globals()[theclass]({"sessionfile": filename})
theinstance.load()
else:
theinstance.validate()
logger.debug("createFromFile() exit")
return theinstance
@staticmethod
def createFromDict(data):
'''Load session data from dict
raises P4RestSessionError if unable to write to session file or
validation fails
'''
logger.debug("createFromDict() enter")
theclass = data.setdefault("SESSIONTYPE", "P4RestSession")
theinstance = globals()[theclass]()
theinstance.update(data)
theinstance.dump()
theinstance.validate()
logger.debug("createFromDict() exit")
return theinstance
def load(self, filename=None, bValidate=True):
'''Load session data from disk
raises P4RestSessionError if unable to read from session file or
validation fails
'''
logger.debug("P4RestSession.load() enter")
if filename is None:
filename = self.getfilename()
data = {}
data["sessionfile"] = filename
try:
with open(filename, 'r') as f:
for line in f:
kv = line.split("=", 1)
k = kv[0].strip()
v = ''
if(len(kv) > 0):
v = kv[1].strip()
v = self.transformStrToType(k, v)
if v is not None:
data[k] = v
except:
message = "error reading from file ({})".format(filename)
logger.debug(message, exc_info=True)
raise P4RestSessionError(message)
self.update(data)
if bValidate:
self.validate()
logger.debug("P4RestSession.load() exit")
def dump(self, filename=None):
'''Dump session data to disk
raises OSError if unable to write to the session file
raises P4RestSessionError if filename is None and no filename in dict
'''
logger.debug("P4RestSession.dump() enter")
if filename is None:
filename = self.getfilename()
dirname = os.path.dirname(filename)
if not os.path.exists(dirname):
try:
osmakedirs(dirname, exist_ok=True)
except:
message = "error creating directory ({})".format(dirname)
logger.debug(message, exc_info=True)
raise P4RestSessionError(message)
try:
with open(filename, 'w') as f:
#self["Access"] = str(datetime.utcnow().timestamp())
self["SESSIONTYPE"] = getattr(self, "__class__").__name__
for k,v in self.items():
v = self.transformTypeToStr(k, v)
if v is not None:
f.write("{}={}\n".format(k, v))
except:
message = "error writing to file ({})".format(filename)
logger.debug(message, exc_info=True)
raise P4RestSessionError(message)
logger.debug("P4RestSession.dump() exit")
def validate(self, data=None):
'''validate current object
raises P4RestSessionError if validation fails
'''
logger.debug("validate() enter")
if data is None:
data = self
# sessionfile must exist
name = "sessionfile"
filename = data.get(name, None)
if filename is None:
raise P4RestSessionError("undefined session")
if not os.path.exists(filename):
logger.debug("'{}' path doesn't exist".format(filename))
raise P4RestSessionError("undefined session")
logger.debug("validate() exit")
def getfilename(self):
'''return sessionfile from dict
raise P4RestSessionError if not defined
'''
filename = self.get("sessionfile", None)
if filename is None:
raise P4RestSessionError("undefined session")
return filename
def getdirname(self):
'''return dir of session file
raise P4RestSessionError if not defined
'''
return os.path.dirname(self.getfilename())
def transformStrToType(self, key, value):
'''virtual to override to exclude values from being loaded from the
file or provide custom transformations
'''
if key == "EXPIRES" and value is not None:
value = datetime.strptime(value, JSON_DATETIME_FMT)
return value
def transformTypeToStr(self, key, value):
'''virtual to override to exclude values from being dumped to the
file or provide custom transformations
'''
# by convention only uppercase values will be dumped
if value is None or key != key.upper():
return None
if isinstance(value, datetime):
value = value.strftime(JSON_DATETIME_FMT)
if isinstance(value, timedelta):
value = value.total_seconds()
if not isinstance(value, str):
return str(value)
return value
def isExpired(self):
'''returns true if session is expired'''
expires = self.get("EXPIRES", None)
return expires is None or datetime.utcnow() > expires
def expire(self):
'''mark the session as expired'''
logger.debug("expire() enter")
self["EXPIRES"] = None
self.dump()
logger.debug("expire() exit")
def dispose(self):
'''free any resources in between session requests
destroy the persistent session data if expired
'''
logger.debug("dispose() enter")
if self.isExpired() and os.path.exists(self.getdirname()):
try:
rmtree(self.getdirname(), ignore_errors=True)
except:
# eat all exceptions
message = "suppressing exception"
logger.debug(message, exc_info=True)
logger.debug("dispose() exit")
class P4RestSessionAdmin(P4RestSession):
'''Encapsulates an Admin Session as a dict'''
def validate(self, data=None):
'''validate current object
raises P4RestSessionError if validation fails
'''
P4RestSession.validate(self, data=data)
class P4RestSessionP4(P4RestSession):
'''Encapsulates a P4 Session as a dict'''
def validate(self, data=None):
'''validate current object
raises P4RestSessionError if validation fails
'''
logger.debug("validate() enter")
if data is None:
data = self
P4RestSession.validate(self, data)
# must have P4PORT, P4USER, P4CLIENT, P4TICKETS, P4TRUST
for name in ["P4PORT", "P4USER", "P4CLIENT"]:
value = data.get(name, None)
if value is None or not isinstance(value, str) or len(value) < 1:
message = "Invalid {}".format(name)
logger.debug("{}:{!s}".format(message, value))
raise P4RestSessionError(message)
# P4TICKETS & P4TRUST must be valid paths
for name in ["P4TICKETS", "P4TRUST"]:
value = data.get(name, None)
if value is None or not isinstance(value, str) or len(value) < 1:
message = "Invalid {}".format(name)
logger.debug("{}:{!s}".format(message, value))
raise P4RestSessionError(message)
elif not os.path.exists(value):
if not os.path.exists(os.path.dirname(value)):
message = "Invalid {}".format(name)
logger.debug("{}:{!s} path doesn't exist".format(message,
value))
raise P4RestSessionError(message)
logger.debug("validate() exit")
def transformTypeToStr(self, key, value):
if key in ["P4PASSWORD", "P4TICKET", "P4CONFIG"]:
return None
return P4RestSession.transformTypeToStr(self, key, value)
def transformStrToType(self, key, value):
if key == "TZOFFSET":
return timedelta(seconds=float(value))
if key == "TICKETEXPIRES" and value is not None:
return datetime.strptime(value, JSON_DATETIME_FMT)
return P4RestSession.transformStrToType(self, key, value)
def expire(self):
'''mark the session as expired
logout from p4 and disconnect api object
'''
logger.debug("expire() enter")
P4RestSession.expire(self)
try:
p4api = self.getP4API()
ticket, unused = self.getTicketStatus()
if ticket is not None:
p4api.run_logout()
if p4api.connected():
p4api.disconnect()
except:
# eat all exceptions
message = "suppressing exception"
logger.debug(message, exc_info=True)
logger.debug("expire() exit")
def dispose(self):
logger.debug("dispose() enter")
if self.isExpired():
self.expire()
p4api = self.pop("p4api", None)
if p4api is not None:
if p4api.connected():
p4api.disconnect()
del(p4api)
P4RestSession.dispose(self)
logger.debug("dispose() enter")
def getP4API(self):
'''return an existing P4.P4() object or create a new one
raise P4RestSessionError if not sessionfile not defined or does not
exist
raise P4Exception if something wrong in P4
'''
logger.debug("getP4API() enter")
p4api = self.get("p4api", None)
if p4api is None:
filename = self.getfilename()
if not os.path.exists(filename):
raise P4RestSessionError("undefined session")
dirname = self.getdirname()
# set the globally configured limit on command results
maxresults = self.get("MAXRESULTS", 0)
p4api = P4(limit=maxresults)
if p4api.connected():
p4api.disconnect()
p4api.exception_level = P4.RAISE_ERRORS
p4api.cwd = dirname
p4api.user = self["P4USER"]
p4api.port = self["P4PORT"]
p4api.client = self["P4CLIENT"]
p4api.prog = APP_NAME
p4api.connect()
logger.debug("user:{}".format(p4api.user))
elif not p4api.connected():
p4api.connect()
self["p4api"] = p4api
logger.debug("getP4API() exit")
return p4api
def getTicketStatus(self):
'''return a tuple containing ticket value and number of seconds until
the ticket expires
suppresses all exceptions
'''
logger.debug("getTicketStatus() enter")
ticket = None
expires = None
try:
p4api = self.getP4API()
ticket = p4api.password
if ticket is not None and len(ticket) > 0:
logger.debug(ticket)
expires = int(p4api.run_login("-s")[0].get("TicketExpiration",
0))
except:
# eat all exceptions
message = "suppressing exception"
logger.debug(message, exc_info=True)
ticket = None
expires = None
logger.debug("getTicketStatus() exit")
return ticket, expires
def run_login(self, p4password):
'''issue login to P4
raise P4RestSessionError if validation failure
raise P4Exception if something wrong in P4
'''
logger.debug("run_login() enter")
self.validate()
self.expire()
p4api = self.getP4API()
logger.debug("P4PORT={};P4USER:{}".format(self["P4PORT"], self["P4USER"]))
p4api.run_login(password=p4password)
self["EXPIRES"] = datetime.utcnow() + timedelta(seconds=300)
self.loadP4Data()
self.dump()
logger.debug("run_login() exit")
def loadP4Data(self):
'''issues p4 info command and caches important data
raise P4RestSessionError if not sessionfile not defined or does not
exist
raise P4Exception if something wrong in P4
'''
logger.debug("loadP4Data() enter")
p4api = self.getP4API()
cmdresult = p4api.run_info()
tzoffset = cmdresult[0].get("tzoffset", "0")
self["TZOFFSET"] = timedelta(seconds=int(tzoffset))
unused, expires = self.getTicketStatus()
if expires is not None and expires > 0:
self["TICKETEXPIRES"] = datetime.utcnow() + self["TZOFFSET"] + \
timedelta(seconds=expires)
elif "TICKETEXPIRES" in self:
del(self["TICKETEXPIRES"])
logger.debug("loadP4Data() exit")