diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c67aecb --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +*.pyc +*.bak +*.csv +*.swp +*.DS_Store diff --git a/DockerReadme.md b/DockerReadme.md new file mode 100644 index 0000000..8503aeb --- /dev/null +++ b/DockerReadme.md @@ -0,0 +1,3 @@ +Make sure rooms.csv file contains list of rooms. +docker build -t room-finder . +docker run -d -p 5000:5000 room-finder diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..77240a7 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,9 @@ +FROM ubuntu:latest +RUN apt-get update -y +RUN apt-get install -y python-pip python-dev build-essential +COPY . /app +WORKDIR /app +ENV PYTHONPATH $PYTHONPATH:/app/roomfinder +RUN pip install Flask==0.10.1 +ENTRYPOINT ["python"] +CMD ["service/webserver.py"] diff --git a/README.md b/README.md index 2868f19..249003e 100644 --- a/README.md +++ b/README.md @@ -4,15 +4,19 @@ roomfinder Python scripts for finding free conference rooms from a Microsoft Exchange Server. Requirements: - + - curl - Python 2.7 - - Access to Exchange Web Service (EWS) API of a Microsoft Exchange Server 2010 + - Access to Exchange Web Service (EWS) API of a Microsoft Exchange Server 2010 + +Before running on the command-line, edit 'exchange_api.py' and modify your DOMAIN +to your organization's domain e.g. 'example.com', so that the URL points to +your Microsoft Exchange Server, e.g. 'https://mail.example.com/ews/exchange.asmx' -Usage: +Command-line Usage: $ python find_rooms.py -h - usage: find_rooms.py [-h] -url URL -u USER [-d] prefix [prefix ...] + usage: find_rooms.py [-h] -u USER [-d] prefix [prefix ...] positional arguments: prefix A list of prefixes to search for. E.g. 'conference @@ -20,61 +24,70 @@ Usage: optional arguments: -h, --help show this help message and exit - -url URL, --url URL url for exhange server, e.g. - 'https://mail.domain.com/ews/exchange.asmx'. -u USER, --user USER user name for exchange/outlook -d, --deep Attemp a deep search (takes longer). + Example: - - $ python find_rooms.py Konferenzr. Konfi -url https://mail.mycompany.com/ews/exchange.asmx -u maier --deep + + $ python find_rooms.py SJC19 -u sgersapp Password: - After searching for prefix 'Konferenzr.' we found 100 rooms. - After deep search for prefix 'Konferenzr.' we found 143 rooms. - After searching for prefix 'Konfi' we found 151 rooms. - After deep search for prefix 'Konfi' we found 151 rooms. -This will create a CSV file `rooms.csv` holding a list of all rooms found with the prefix `Konfi` and `Konferenzr.` in their display names. +This will create a CSV file `rooms.csv` holding a list of all rooms found with the prefix `SJC19` in their display names. -After doing so, you can get the status for each of the rooms by calling +After doing so, you can check the status for each of the rooms by calling $ python find_available_room.py -h - usage: find_available_room.py [-h] -url URL -u USER [-start STARTTIME] + usage: find_available_room.py [-h] -u USER [-start STARTTIME] [-end ENDTIME] [-f FILE] - - optional arguments: - -h, --help show this help message and exit - -url URL, --url URL url for exhange server, e.g. - 'https://mail.domain.com/ews/exchange.asmx'. - -u USER, --user USER user name for exchange/outlook - -start STARTTIME, --starttime STARTTIME - Starttime e.g. 2014-07-02T11:00:00 (default = now) - -end ENDTIME, --endtime ENDTIME - Endtime e.g. 2014-07-02T12:00:00 (default = now+1h) - -f FILE, --file FILE csv filename with rooms to check (default=rooms.csv). - Format: Name,email + + optional arguments: + -h, --help show this help message and exit + -u USER, --user USER user name for exchange/outlook + -prefix PREFIX, --prefix PREFIX + A prefix to search for. e.g. 'SJC19 SJC18' + -start STARTTIME, --starttime STARTTIME + Starttime e.g. 2014-07-02T11:00:00 (default = now) + -duration DURATION, --duration DURATION + Duration e.g. 1h or 15m (default = 1h) + -f FILE, --file FILE csv filename with room info (default=rooms.csv). Example: - - $ python find_available_room.py -url https://mail.mycompany.com/ews/exchange.asmx -u maier -start 2014-07-03T13:00:00 -end 2014-07-03T17:00:00 - Password: - Busy Konferenzr. Asterix konf.asterix@mycompany.com - Tentative Konferenzr. Personal Konferenzr.Personal@mycompany.com - Busy Konferenzr. Obelix konferenzr.obelix@mycompany.com - Busy Konfi Idefix konfi_idefix@mycompany.com - Free Konferenzr. Miraculix miraculix@mycompany.com - ... -Since the auto-generated list `rooms.csv` can be very huge it is recommended to copy that list to another file, e.g. `favorite_rooms.csv` and edit that file so that it only holds the meetings rooms you are interested in. After doing so, you can get the status for you favorite rooms very quickly using: + $ python find_available_room.py -u sgersapp -start 2014-07-03T13:00:00 -duration 1h - $ python find_available_room.py -url https://mail.mycompany.com/ews/exchange.asmx -u maier -start 2014-07-03T13:00:00 -end 2014-07-03T17:00:00 -f favorite_rooms.csv - Password: - Free Konferenzr. 007 007@mycompany.com - Busy Konferenzr. Asterix konf.asterix@mycompany.com - Busy Konferenzr. Obelix konferenzr.obelix@mycompany.com +Results are logged to the log file (default: access.log) + +Eventually, you can reserve a rooms by calling + + $ python book_room.py -h + usage: book_room.py [-h] -u USER [-start STARTTIME] [-d DURATION] -e ROOMEMAIL + -r ROOMNAME + + optional arguments: + -h, --help show this help message and exit + -u USER, --user USER user name for exchange/outlook + -start STARTTIME, --starttime STARTTIME + Starttime e.g. 2014-07-02T11:00:00 (default = now) + -d DURATION, --duration DURATION + Duration e.g. 1h or 15m (default = 1h) + -e ROOMEMAIL, --roomemail ROOMEMAIL + Email address of the room + -r ROOMNAME, --roomname ROOMNAME + Name of room + + +Example: + + $ python book_room.py -u sgersapp -start 2014-07-03T13:00:00 -duration 1h -r SJC19-3-SAISH -e ROOM_SAISH@example.com + +You will receive a confirmation email from Exchange if the reservation is accepted or rejected. - +Before starting the web-app, edit 'CONFIG' and pick the number of worker threads. +Generate a certificate and private key and point CONFIG to their locations. +Web-App: + $ ./run.sh diff --git a/book_room.py b/book_room.py new file mode 100644 index 0000000..07008c5 --- /dev/null +++ b/book_room.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +APIs to request an Exchange Server reserve a room +""" + +import argparse +import base64 +import getpass +import sys +import urllib + +import common +from exchange_api import ExchangeApi + +reload(sys) +sys.setdefaultencoding("utf-8") + +class ReserveAvailRoom(object): + """ Class to request an Exchange Server to reserve a room """ + + def __init__(self, user, password, + roomname, roomemail=None, + start_time=common.time_now(), duration=None, end_time=None, + raw_password=None, + timezone=common.SJ_TIME_ZONE): + self.user = user + self.roomname = roomname + if roomemail is None: + self.roomemail = common.read_room_list()[roomname]["email"] + else: + self.roomemail = roomemail + self.start_time = start_time + self.timezone = self._calc_timezone_str(timezone) + password = raw_password or base64.b64decode(urllib.unquote(password)) + self.exchange_api = ExchangeApi(user, password) + if end_time is None: + self.end_time = common.end_time(self.start_time, duration) + if duration is None: + self.end_time = end_time + + def _calc_timezone_str(self, timezone): + try: + timezone = int(timezone) + except ValueError: + timezone = common.SJ_TIME_ZONE + hours_offset = timezone / 60 + minutes_offset = timezone % 60 + sign = "-" if hours_offset < 0 else "" + return "{}PT{}H{}M".format(sign, abs(hours_offset), abs(minutes_offset)) + + def reserve_room(self): + """ Request reservation of specified room """ + response = self.exchange_api.reserve_room( \ + room_email=self.roomemail, + room_name=self.roomname, + start_time=self.start_time, + end_time=self.end_time, + timezone_offset=self.timezone) + return response + +def run(): + """ Parse command-line arguments and request room reservation """ + parser = argparse.ArgumentParser() + parser.add_argument("-u", "--user", help="user name for exchange/outlook", required=True) + parser.add_argument("-start", "--starttime", + help="Starttime e.g. 2014-07-02T11:00:00 (default = now)", + default=common.time_now()) + parser.add_argument("-d", "--duration", + help="Duration e.g. 30m (default = 1h)", + default='1h') + parser.add_argument("-e", "--roomemail", + help="Email address of room", + required=True) + parser.add_argument("-r", "--roomname", + help="Name of room", + required=True) + + args = parser.parse_args() + args.password = base64.b64encode(getpass.getpass("Password:")) + + room_finder = ReserveAvailRoom(user=args.user, password=args.password, + roomname=args.roomname, roomemail=args.roomemail, + start_time=args.starttime, duration=args.duration) + room_finder.reserve_room() + + +if __name__ == '__main__': + run() diff --git a/certs/cert.pem b/certs/cert.pem new file mode 100644 index 0000000..fb30e56 --- /dev/null +++ b/certs/cert.pem @@ -0,0 +1,39 @@ +-----BEGIN CERTIFICATE----- +MIIGxjCCBK6gAwIBAgIJAOUp2FQbFrEcMA0GCSqGSIb3DQEBBQUAMIGcMQswCQYD +VQQGEwJVUzELMAkGA1UECBMCQ0ExETAPBgNVBAcTCFNhbiBKb3NlMRcwFQYDVQQK +Ew5TYWlzaCBHZXJzYXBwYTEUMBIGA1UECxMLUm9vbSBGaW5kZXIxHTAbBgNVBAMT +FHJvb21maW5kZXIuY2lzY28uY29tMR8wHQYJKoZIhvcNAQkBFhBzYWlzaGdAZ21h +aWwuY29tMCAXDTE2MTAxNzA1MTUzMloYDzIxMTYwOTIzMDUxNTMyWjCBnDELMAkG +A1UEBhMCVVMxCzAJBgNVBAgTAkNBMREwDwYDVQQHEwhTYW4gSm9zZTEXMBUGA1UE +ChMOU2Fpc2ggR2Vyc2FwcGExFDASBgNVBAsTC1Jvb20gRmluZGVyMR0wGwYDVQQD +ExRyb29tZmluZGVyLmNpc2NvLmNvbTEfMB0GCSqGSIb3DQEJARYQc2Fpc2hnQGdt +YWlsLmNvbTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALHWAKrOAWFh +cEtCNc0a5Bb24Ij1BQhop/laS8bvvREWXNOyqU4aQlbO/g5eDzVieqrwQmXY3YPk +A53MEqp3pXlNQnV9W+BjMPPAIlBS9R6+1BcuGudss/mgEglqKmdCDiAAjI0X8kC5 +lpeNGT+JAhyoNUK5yfPgdNyJaBA8u0oasTbw+Tn2RQLdP5sdPCQahJC28GEOYivU +TewM2vIEHh+p2uZZovQ/fA+NxSdXeS7Oa96HzPcqOEBEkgtNvogKuxuAvP+P/nUd +hz2RtA5Hl1nIvqGKAfqErVgbEBmiPDuKCC9Zmg/d4ifaLbbN0e2NqYyahQXamJ4z +2LcSQS2jXt1pFGNF7RHkeNhTiSCs3KLCdBLdeDK3k6sGb1pw5BZLLxBEGrmSwaLR +2CrPPC0sDr+HbOljAkmuDth+AGlYwCPVjq+urCwKz9TFaZErcF5y2JLwEbBQ02bB +2gUyfLuEKOSLAs96Xb7m0eoTHR26JLYmvdH9nexepPA/YovaIKdk0q9Dtcqj9RqC +ecNeiy+Lo3izxv06b7s0Iu27WzoXhUrT1bOc9uRWfmnLQczM1rwv7RRRC+5irAQz +sn3damHV9lqJE/zW+ZRGvKb4QEt5awg6h9//7+CmftBaTO4RixZru8pLMSrrZ308 +9JdI5z4q7s3AnwU5oJj8/WRMnE25MzRJAgMBAAGjggEFMIIBATAdBgNVHQ4EFgQU +FvMqU84chLjsHOWLcFBrbMODUkIwgdEGA1UdIwSByTCBxoAUFvMqU84chLjsHOWL +cFBrbMODUkKhgaKkgZ8wgZwxCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJDQTERMA8G +A1UEBxMIU2FuIEpvc2UxFzAVBgNVBAoTDlNhaXNoIEdlcnNhcHBhMRQwEgYDVQQL +EwtSb29tIEZpbmRlcjEdMBsGA1UEAxMUcm9vbWZpbmRlci5jaXNjby5jb20xHzAd +BgkqhkiG9w0BCQEWEHNhaXNoZ0BnbWFpbC5jb22CCQDlKdhUGxaxHDAMBgNVHRME +BTADAQH/MA0GCSqGSIb3DQEBBQUAA4ICAQA+A2Qn1eEC6Euucw6Pz2TWayq58M8x +iklr1YKFJshW/gjvfoUM2ZMVvaeUMy/AJSfhn13KzmWnbC67Z+DzykMhCESF+VAK +T71AAQrPG9DqlrbIYA78+ClMLtzfhp+KXkQRNZqGvBeaDHbYmaYVmDHYkg8OqoR0 +waehcllJjGzXs39dp9ogulRdNL/LL7xT+lJTz/LkIwipQcd76MZMJP5b8erJgoe1 +t1QxDVn/8JWl98W5E49Xl0QxGPytx2hXLettkKJaj43Ebm66G4pNQm0KMQxUDehT +B/SjTsacHIZakijDN0GPco3aVlDb5cBVW3lKYLG3U6O/eUVBnb9BE1TtaDzwz/vN +7wFoibSfSMHPEnVWFr72lG5GoCSxmJXJ+7adPk97yRzZb73wEmacJ5F5ZxCScDRm +4DNM1FB821as5Wiz6/7HsDrrQjMbi1yH3aqjU3Aj5rPbLDJ3ith/2GzEXBfuTXh3 +I3klTJOmopkej3mB5JYF0SZ2l1+X9Zh8+XmkX3o/5XR+PyKLIuVSs/qnfpoaGVdG +yUN8yvj1PPgwQc1wpRPycJPb6ZMzaCFxCSJNrrIgASXiGUZdH4pjBD3n/Hxoix+f +H2+zU9i9rTF7EZEdmFe5Q6B76trbxAraZR+LcDIm9H+cGNGEVvzMbtHk+4u1giji +S524huW9+tHqLw== +-----END CERTIFICATE----- diff --git a/certs/key.pem b/certs/key.pem new file mode 100644 index 0000000..59edd8f --- /dev/null +++ b/certs/key.pem @@ -0,0 +1,51 @@ +-----BEGIN RSA PRIVATE KEY----- +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 +-----END RSA PRIVATE KEY----- diff --git a/common.py b/common.py new file mode 100644 index 0000000..ec8fb6d --- /dev/null +++ b/common.py @@ -0,0 +1,179 @@ +""" +Common declarations and functions +""" +import csv +import datetime +import logging +import os + +HTTPS_ENABLED = True +HTTP_PORT = 8080 +HTTPS_PORT = 8443 + +PWD = os.getcwd() +ROOMS_CSV = os.path.join(PWD, 'rooms.csv') +COORDS_CSV = os.path.join(PWD, 'coords.csv') +AVAILIBILITY_TEMPLATE = os.path.join(PWD, 'getavailibility_template.xml') +SERVICE_DIR = os.path.join(PWD, 'service') +CERT_DIR = os.path.join(PWD, 'certdir') +TEMPLATE_FOLDER = os.path.join(SERVICE_DIR, 'templates') +FLOORMAP_DIR = os.path.join(TEMPLATE_FOLDER, 'floormap') + +ROOMS_CACHE = None +ROOMNAMES_CACHE = None +CITIES_CACHE = None +COORDS_CACHE = {} +BUILDINGS_CACHE = {} +FLOORS_CACHE = {} + +SJ_TIME_ZONE = "420" + +logging.basicConfig(filename='access.log', + level=logging.DEBUG, + format='%(asctime)s %(levelname)s: (%(name)s) %(message)s', + datefmt='%a %b %d %Y %H:%M:%S') +LOGGER = logging.getLogger('roomfinder') +logging.getLogger('werkzeug').setLevel(logging.ERROR) + +def time_now(): + return datetime.datetime.now().replace(microsecond=0).isoformat() + +def end_time(start_time, duration): + """ Calculate end time, given start time and duration """ + try: + if 'h' in duration and duration.endswith('m'): + hours, mins = map(int, duration[:-1].split('h')) + elif duration.endswith('h'): + hours, mins = int(duration[:-1]), 0 + elif duration.endswith('m'): + hours, mins = 0, int(duration[:-1]) + else: + duration = int(duration) + if duration < 15: + hours, mins = duration, 0 + else: + hours, mins = 0, duration + except ValueError: + hours, mins = 1, 0 + + start = datetime.datetime.strptime(start_time, "%Y-%m-%dT%H:%M:%S") + return (start + datetime.timedelta(hours=hours, minutes=mins)).isoformat() + +def read_room_list(filename=ROOMS_CSV): + global ROOMS_CACHE + + if ROOMS_CACHE is not None: + return ROOMS_CACHE + rooms = {} + + try: + with open(filename, 'r') as fhandle: + reader = csv.reader(fhandle) + for room_name, room_email, room_size, city, country in reader: + rooms[room_name] = {"name" : room_name, + "size" : int(room_size), + "email" : room_email, + "city" : city, + "country" : country, + } + except IOError as exception: + LOGGER.warning("Error opening %s: %s", filename, str(exception)) + + ROOMS_CACHE = rooms + return rooms + +def get_roomname_list(filename=ROOMS_CSV): + global ROOMNAMES_CACHE + + if ROOMNAMES_CACHE is not None: + return ROOMNAMES_CACHE + + rooms = read_room_list(filename=filename) + ROOMNAMES_CACHE = sorted(rooms) + return ROOMNAMES_CACHE + +def get_building_list(city, filename=ROOMS_CSV): + if city in BUILDINGS_CACHE: + return BUILDINGS_CACHE[city] + + rooms = read_room_list(filename=filename) + buildings = set() + for roomname, roominfo in rooms.iteritems(): + if roominfo["city"] == city: + buildings.add(roomname.split('-')[0]) + + BUILDINGS_CACHE[city] = sorted(buildings) + return BUILDINGS_CACHE[city] + +def get_floor_list(buildingname, filename=ROOMS_CSV): + if buildingname in FLOORS_CACHE: + return FLOORS_CACHE[buildingname] + + roomnames = get_roomname_list(filename=filename) + floors = set() + for roomname in roomnames: + if roomname.startswith(buildingname): + floors.add(roomname.split('-')[1]) + + FLOORS_CACHE[buildingname] = sorted(floors) + return FLOORS_CACHE[buildingname] + +def get_city_list(filename=ROOMS_CSV): + global CITIES_CACHE + + if CITIES_CACHE is not None: + return CITIES_CACHE + + rooms = read_room_list(filename=filename) + cities = set() + for roominfo in rooms.itervalues(): + cities.add(roominfo["city"]) + + CITIES_CACHE = sorted(cities) + return CITIES_CACHE + +def get_city_coords(filename=COORDS_CSV): + if len(COORDS_CACHE) > 0: + return COORDS_CACHE + + cities = get_city_list() + + with open(filename, "r") as fhandle: + reader = csv.reader(fhandle) + for city, latitude, longitude in reader: + if city in cities: + COORDS_CACHE[city] = float(latitude), float(longitude) + + return COORDS_CACHE + +def get_closest_city(latitude, longitude): + coords_dict = get_city_coords() + + closest_city = None + closest_distance = None + + def distance(coords): + city_lat, city_long = coords + return (city_lat - latitude) ** 2 + (city_long - longitude) ** 2 + + for city, city_coords in coords_dict.iteritems(): + city_distance = distance(city_coords) + if closest_city is None or city_distance < closest_distance: + closest_city = city + closest_distance = city_distance + + return closest_city + +def write_room_list(rooms, filename=ROOMS_CSV): + global ROOMS_CACHE + ROOMS_CACHE = rooms + + with open(filename, "wb") as fhandle: + writer = csv.writer(fhandle) + for name in sorted(rooms): + room_info = rooms[name] + email = room_info["email"] + size = room_info["size"] + city = room_info["city"] + country = room_info["country"] + writer.writerow([name, email, size, city, country]) diff --git a/coords.csv b/coords.csv new file mode 100644 index 0000000..4f3a342 --- /dev/null +++ b/coords.csv @@ -0,0 +1,9 @@ +Bangalore,12.9716,77.5946 +London,51.5074,-0.1278 +New York,40.7128,-74.0059 +San Francisco,37.7749,-122.4194 +San Jose,37.3382,-121.8863 +Seattle,47.6062,-122.3321 +Sydney,-33.8688,151.2093 +Tokyo,35.6895,139.6917 +Vancouver,49.2827,-123.1207 diff --git a/exchange.cfg b/exchange.cfg new file mode 100644 index 0000000..875e944 --- /dev/null +++ b/exchange.cfg @@ -0,0 +1,5 @@ +[exchange] +domain = cisco.com +anon_user = roomfinder +anon_password = ******* + diff --git a/exchange_api.py b/exchange_api.py new file mode 100644 index 0000000..781da16 --- /dev/null +++ b/exchange_api.py @@ -0,0 +1,162 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +APIs to communicate with the Exchange Server +""" + +import ConfigParser +import pipes +import string +import subprocess +import sys +import xml.etree.ElementTree as ET + +reload(sys) +sys.setdefaultencoding("utf-8") + +URL = 'https://mail.{}/ews/exchange.asmx' +SCHEME_TYPES = './/{http://schemas.microsoft.com/exchange/services/2006/types}' +CURL_COMMAND = "curl --silent --header 'content-type: text/xml;charset=utf-8' --data '{data}' --ntlm -u {user}:{password} {url}" +AVAILABILITY_XML = None +RESERVE_XML = None +FIND_XML = None + +class ExchangeApi(object): + """ Class to communicate with the Exchange Server """ + + def __init__(self, user, password, cfg='exchange.cfg'): + self.user = user + self.password = password + + config = ConfigParser.ConfigParser() + if config.read(cfg): + self.domain = config.get('exchange', 'domain') + self.anon_user = config.get('exchange', 'anon_user') + self.anon_password = config.get('exchange', 'anon_password') + else: + self.domain = 'example.com' + self.anon_user = self.user + self.anon_password = self.password + + self.url = URL.format(self.domain) + self.command = 'curl --silent --header "content-type: text/xml;charset=utf-8"' \ + + " --data '{data}'" \ + + " --ntlm " \ + + "-u {user}:{password}" \ + + " {url}" + + def _read_template(self, filename): + with open(filename, "r") as fhandle: + return string.Template(fhandle.read()) + + def _curl(self, post_data, user, password): + curl_command = CURL_COMMAND.format(data=post_data, + user=pipes.quote(user), + password=pipes.quote(password), + url=self.url) + curl_process = subprocess.Popen(curl_command, + stdout=subprocess.PIPE, + shell=True) + response = curl_process.communicate()[0] + if not response: + raise Exception("Authentication failure") + return response + + def room_status(self, room_email, start_time, end_time, timezone_offset): + """ Lookup availability status of specified room """ + global AVAILABILITY_XML + if AVAILABILITY_XML is None: + AVAILABILITY_XML = self._read_template("getavailibility_template.xml") + + data = unicode(AVAILABILITY_XML.substitute(timezone=timezone_offset, + email=room_email, + starttime=start_time, + endtime=end_time)) + + if len(self.user) and len(self.password): + response = self._curl(data, self.user, self.password) + else: + response = self._curl(data, self.anon_user, self.anon_password) + + tree = ET.fromstring(response) + status = "Free" + elems = tree.findall(SCHEME_TYPES + "MergedFreeBusy") + freebusy = '' + for elem in elems: + freebusy = elem.text + if '2' in freebusy: + status = "Busy" + elif '3' in freebusy: + status = "Unavailable" + elif '1' in freebusy: + status = "Tentative" + + return {'freebusy': freebusy, 'status': status, 'email' : room_email,} + + def reserve_room(self, room_email, room_name, start_time, end_time, timezone_offset): + """ Request reservation of specified room """ + global RESERVE_XML + if RESERVE_XML is None: + RESERVE_XML = self._read_template("reserve_resource_template.xml") + + user_email = self.user + '@' + self.domain + room_name = room_name.replace("'", "") # Quotes cause all sorts of errors + meeting_body = '{0} booked via RoomFinder by {1}'.format(room_name, user_email) + subject = 'RoomFinder: {0}'.format(room_name) + + data = unicode(RESERVE_XML.substitute(resourceemail=room_email, + useremail=user_email, + subject=subject, + starttime=start_time, + endtime=end_time, + meeting_body=meeting_body, + conf_room=room_name, + timezone=timezone_offset, + )) + + response = self._curl(data, self.user, self.password) + return 'Success' in response + + def _parse_room_size(self, roomname): + try: + return int(roomname[roomname.find('(') + 1 : roomname.find(')')]) + except ValueError: + return 1 + + def _polish(self, elem_list): + if len(elem_list) == 0 or elem_list[0] is None or elem_list[0].text is None: + return "" + else: + return elem_list[0].text + + + def find_rooms(self, prefix): + """ Search for rooms with names starting with specified prefix """ + global FIND_XML + if FIND_XML is None: + FIND_XML = self._read_template("resolvenames_template.xml") + + room_info = {} + data = unicode(FIND_XML.substitute(name=prefix)) + if len(self.user) and len(self.password): + response = self._curl(data, self.user, self.password) + else: + response = self._curl(data, self.anon_user, self.anon_password) + + tree = ET.fromstring(response) + elems = tree.findall(SCHEME_TYPES + "Resolution") + for elem in elems: + email = self._polish(elem.findall(SCHEME_TYPES + "EmailAddress")) + name = self._polish(elem.findall(SCHEME_TYPES + "DisplayName")) + city = self._polish(elem.findall(SCHEME_TYPES + "City")) + country = self._polish(elem.findall(SCHEME_TYPES + "CountryOrRegion")) + if len(name) > 0 and len(email) > 0 and len(city) > 0: + roomsize = self._parse_room_size(name) + if roomsize: + room_info[name] = {"name" : name, + "size" : roomsize, + "email" : email, + "city" : city.title(), + "country" : country.title(), + } + return room_info diff --git a/find_available_room.py b/find_available_room.py index 205115f..53e9310 100644 --- a/find_available_room.py +++ b/find_available_room.py @@ -1,67 +1,162 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- +""" +APIs to query an Exchange Server for availability status of rooms +""" +import argparse +import base64 +import getpass import sys +import threading +import urllib + +import common +from operator import add +from exchange_api import ExchangeApi + reload(sys) sys.setdefaultencoding("utf-8") -import subprocess -import getpass -from string import Template -import xml.etree.ElementTree as ET -import csv, codecs -import argparse -import datetime +SEPARATOR = "-" * 120 + "\n" +TABLE_FORMAT = "{0:40s} {1:64s} {2:20s}\n" +TABLE_HEADER = SEPARATOR + TABLE_FORMAT.format("Status", "Room", "Email") + SEPARATOR + +class AvailRoomFinder(object): + """ Class to query an Exchange Server for availability status of rooms """ + + def __init__(self, user, password, + start_time=common.time_now(), duration=None, end_time=None, + filename=common.ROOMS_CSV, timezone=common.SJ_TIME_ZONE): + self.rooms = common.read_room_list(filename) + self.user = user + self.start_time = start_time + self.room_info = {} + self.timezone = timezone or common.SJ_TIME_ZONE + self.error = None + self.exchange_api = ExchangeApi(user, base64.b64decode(urllib.unquote(password))) + if end_time is None: + self.end_time = common.end_time(self.start_time, duration) + if duration is None: + self.end_time = end_time + + def search_free(self, prefix, min_size=1): + """ Look for available rooms from the list of selected rooms """ + selected_rooms = {} + for roomname, room_info in self.rooms.iteritems(): + size = room_info["size"] + if roomname.startswith(prefix) and size >= min_size: + selected_rooms[roomname] = room_info + + selected_room_info = self.search(selected_rooms) + free_room_info = {} + for roomname, roominfo in selected_room_info.iteritems(): + if roominfo['status'] == 'Free': + free_room_info[roomname] = selected_room_info[roomname] + return free_room_info + + def search_common_free(self, emails): + """ Look for common free times for selected emails """ + def clean_free_busy(schedule): + freebusy = list(schedule['freebusy']) + return map(lambda x: int(int(x) > 0) * 100, freebusy) + + schedules = self.search(emails).values() + all_freebusy = map(clean_free_busy, schedules) + valid_freebusy = filter(lambda x: len(x) > 1, all_freebusy) + combined_freebusy = map(sum, zip(*valid_freebusy)) + + N = len(valid_freebusy) + percent_combined_freebusy = map(lambda x: x/N, combined_freebusy) + return percent_combined_freebusy + + def _query(self, roomname): + if '@' not in roomname: + room_size = self.rooms[roomname]["size"] + email = self.rooms[roomname]["email"] + common.LOGGER.debug("Querying for room %s", roomname) + else: + room_size = 0 + email = roomname + common.LOGGER.debug("Querying for email %s", roomname) + + try: + room_info = self.exchange_api.room_status( \ + room_email=email, + start_time=self.start_time, + end_time=self.end_time, + timezone_offset=self.timezone) + + room_info['size'] = room_size + room_info['name'] = roomname + self.room_info[roomname] = room_info + + except Exception as exception: + self.error = exception + common.LOGGER.warning("Exception querying room %s: %s", roomname, str(exception)) + + def search(self, selected_rooms=None): + """ Lookup availability status of rooms from the list of selected rooms """ + if selected_rooms is None: + selected_rooms = self.rooms + worker_threads = [] -now = datetime.datetime.now().replace(microsecond=0) -starttime_default = now.isoformat() -end_time_default = None + common.LOGGER.info("User %s searching for a room from %s to %s", + self.user, self.start_time, self.end_time) -parser = argparse.ArgumentParser() -parser.add_argument("-url","--url", help="url for exhange server, e.g. 'https://mail.domain.com/ews/exchange.asmx'.",required=True) -parser.add_argument("-u","--user", help="user name for exchange/outlook",required=True) -parser.add_argument("-start","--starttime", help="Starttime e.g. 2014-07-02T11:00:00 (default = now)", default=starttime_default) -parser.add_argument("-end","--endtime", help="Endtime e.g. 2014-07-02T12:00:00 (default = now+1h)", default=end_time_default) -#parser.add_argument("-n","--now", help="Will set starttime to now and endtime to now+1h", action="store_true") -parser.add_argument("-f","--file", help="csv filename with rooms to check (default=rooms.csv). Format: Name,email",default="rooms.csv") + for roomname in selected_rooms: + thread = threading.Thread(target=self._query, args=(roomname, )) + thread.start() + worker_threads.append(thread) -args=parser.parse_args() + for thread in worker_threads: + thread.join() -url = args.url + if self.error is not None: + raise self.error # pylint: disable=E0702 -rooms={} -reader = csv.reader(codecs.open(args.file, 'r', encoding='utf-8')) -for row in reader: - rooms[unicode(row[1])]=unicode(row[0]) + output_table = TABLE_HEADER + for name, info in self.room_info.iteritems(): + output_table += TABLE_FORMAT.format( + info['status'] + '-' + info['freebusy'], + name, info['email']) + output_table += SEPARATOR -start_time = args.starttime -if not args.endtime: - start = datetime.datetime.strptime( start_time, "%Y-%m-%dT%H:%M:%S" ) - end_time = (start + datetime.timedelta(hours=1)).isoformat() -else: - end_time = args.endtime + common.LOGGER.debug("\n%s", output_table) -user = args.user -password = getpass.getpass("Password:") + return self.room_info -print "Searching for a room from " + start_time + " to " + end_time + ":" -print "{0:10s} {1:64s} {2:64s}".format("Status", "Room", "Email") +def run(): + """ Parse command-line arguments and invoke room availability finder """ + parser = argparse.ArgumentParser() + parser.add_argument("-u", "--user", help="user name for exchange/outlook") + parser.add_argument("-prefix", "--prefix", + help="A prefix to search for. e.g. 'SJC19- SJC18-'", + default='SJC19-2-') + parser.add_argument("-start", "--starttime", + help="Starttime e.g. 2014-07-02T11:00:00 (default = now)", + default=common.time_now()) + parser.add_argument("-duration", "--duration", + help="Duration e.g. 1h or 15m (default = 1h)", + default='1h') + parser.add_argument("-f", "--file", + help="csv filename with room info (default={}).".format(common.ROOMS_CSV), + default=common.ROOMS_CSV) -xml_template = open("getavailibility_template.xml", "r").read() -xml = Template(xml_template) -for room in rooms: - data = unicode(xml.substitute(email=room,starttime=start_time,endtime=end_time)) + args = parser.parse_args() - header = "\"content-type: text/xml;charset=utf-8\"" - command = "curl --silent --header " + header +" --data '" + data + "' --ntlm "+"--negotiate "+ "-u "+ user+":"+password+" "+ url - response = subprocess.Popen(command, stdout=subprocess.PIPE, shell=True).communicate()[0] + if args.user: + args.password = base64.b64encode(getpass.getpass("Password:")) + else: + args.user = 'anon' + args.password = '' - tree = ET.fromstring(response) + room_finder = AvailRoomFinder(user=args.user, password=args.password, + start_time=args.starttime, duration=args.duration, + filename=args.file) +# print room_finder.search_free(prefix=args.prefix) + print room_finder.search_common_free(['sgersapp@cisco.com', 'ratri@cisco.com']) - status = "Free" - # arrgh, namespaces!! - elems=tree.findall(".//{http://schemas.microsoft.com/exchange/services/2006/types}BusyType") - for elem in elems: - status=elem.text - print "{0:10s} {1:64s} {2:64s}".format(status, rooms[room], room) +if __name__ == '__main__': + run() diff --git a/find_rooms.py b/find_rooms.py index cf7fae6..11006a2 100644 --- a/find_rooms.py +++ b/find_rooms.py @@ -1,67 +1,88 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- +""" +APIs to query an Exchange Server for list of rooms +""" + +import argparse +import getpass +import string import sys + +import common +import exchange_api + reload(sys) sys.setdefaultencoding("utf-8") -from string import Template -from string import letters -from string import digits -import subprocess -import getpass -import xml.etree.ElementTree as ET -import argparse -import csv -import operator - -def findRooms(prefix): - rooms={} - data = unicode(xml.substitute(name=prefix)) - - header = "\"content-type: text/xml;charset=utf-8\"" - command = "curl --silent --header " + header +" --data '" + data + "' --ntlm "+"--negotiate "+ "-u "+ user+":"+password+" "+ url - response = subprocess.Popen(command, stdout=subprocess.PIPE, shell=True).communicate()[0] - tree = ET.fromstring(response) - - elems=tree.findall(".//{http://schemas.microsoft.com/exchange/services/2006/types}Resolution") - for elem in elems: - email = elem.findall(".//{http://schemas.microsoft.com/exchange/services/2006/types}EmailAddress") - name = elem.findall(".//{http://schemas.microsoft.com/exchange/services/2006/types}DisplayName") - if len(email) > 0 and len(name) > 0: - rooms[email[0].text] = name[0].text - return rooms - -parser = argparse.ArgumentParser() -parser.add_argument("prefix", nargs='+',help="A list of prefixes to search for. E.g. 'conference confi'") -parser.add_argument("-url","--url", help="url for exhange server, e.g. 'https://mail.domain.com/ews/exchange.asmx'.",required=True) -parser.add_argument("-u","--user", help="user name for exchange/outlook", required=True) -parser.add_argument("-d","--deep", help="Attemp a deep search (takes longer).", action="store_true") -args=parser.parse_args() - -url = args.url -user = args.user -password = getpass.getpass("Password:") - -xml_template = open("resolvenames_template.xml", "r").read() -xml = Template(xml_template) - -rooms={} - -for prefix in args.prefix: - rooms.update(findRooms(prefix)) - print "After searching for prefix '" + prefix + "' we found " + str(len(rooms)) + " rooms." - - deep = args.deep - - if deep: - symbols = letters + digits - for symbol in symbols: - prefix_deep = prefix + " " + symbol - rooms.update(findRooms(prefix_deep)) - - print "After deep search for prefix '" + prefix + "' we found " + str(len(rooms)) + " rooms." - -writer = csv.writer(open("rooms.csv", "wb")) -for item in sorted(rooms.iteritems(), key=operator.itemgetter(1)): - writer.writerow([item[1],item[0]]) +class RoomFinder(object): + """ Class to query an Exchange Server for list of rooms """ + + def __init__(self, user, password, filename='rooms.csv', append=True): + self.user = user + self.filename = filename + self.exchange_api = exchange_api.ExchangeApi(self.user, password) + self.rooms = {} + if append: + self.rooms = common.read_room_list(self.filename) + + def _search(self, prefix): + return self.exchange_api.find_rooms(prefix=prefix) + + def _search_to_be_deleted(self, prefix, newrooms): + to_be_deleted = [] + for room in self.rooms: + if room not in newrooms and room.startswith(prefix): + to_be_deleted.append(room) + print "--", room + return to_be_deleted + + def search(self, prefix, deep=False): + """ Search for rooms with names starting with specified prefix """ + rooms_found = self._search(prefix) + to_be_deleted = self._search_to_be_deleted(prefix, rooms_found) + + if deep: + symbols = string.letters + string.digits + for symbol in symbols: + prefix_deep = prefix + " " + symbol + rooms_found.update(self._search(prefix_deep)) + + common.LOGGER.info("Search for prefix '%s' yielded %d rooms.", prefix, len(rooms_found)) + self.rooms.update(rooms_found) + for room in to_be_deleted: + common.LOGGER.info("Deleting room '%s' room for prefix '%s'.", room, prefix) + del self.rooms[room] + + def dump(self): + """ Dump the results to specified file """ + if not len(self.rooms): + common.LOGGER.warning("No results found, check your arguments for mistakes") + return 0 + + common.write_room_list(self.rooms, filename=self.filename) + return len(self.rooms) + +def run(): + """ Parse command-line arguments and invoke room finder """ + parser = argparse.ArgumentParser() + parser.add_argument("prefix", nargs='+', help="A prefix to search for. e.g. 'SJC19- SJC18-'") + parser.add_argument("-u", "--user", help="user name for exchange/outlook", required=True) + parser.add_argument("-d", "--deep", help="Attemp a deep slow search", action="store_true") + args = parser.parse_args() + + args.password = getpass.getpass("Password:") + + finder = RoomFinder(args.user, args.password) + + for prefix in args.prefix: + finder.search(prefix, args.deep) + + if finder.dump(): + exit(0) + else: + exit(1) + +if __name__ == '__main__': + run() diff --git a/getavailibility_template.xml b/getavailibility_template.xml index ebee671..f057160 100644 --- a/getavailibility_template.xml +++ b/getavailibility_template.xml @@ -4,7 +4,7 @@ - -60 + $timezone 0 @@ -13,7 +13,7 @@ Sunday - -60 + 0 5 3 @@ -36,7 +36,7 @@ $starttime $endtime - 60 + 15 DetailedMerged diff --git a/gunicorn.cfg b/gunicorn.cfg new file mode 100644 index 0000000..5f1bf36 --- /dev/null +++ b/gunicorn.cfg @@ -0,0 +1,5 @@ +workers = 10 +bind = "0.0.0.0:8443" +certfile = "certs/cert.pem" +keyfile = "certs/key.pem" + diff --git a/reserve_resource_template.xml b/reserve_resource_template.xml new file mode 100644 index 0000000..0efcb87 --- /dev/null +++ b/reserve_resource_template.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + $subject + $meeting_body + 15 + $starttime + $endtime + $conf_room + + + + $resourceemail + + + + + + + $useremail + + + + + $timezone + + P0D + + Sunday + Second + September + + 01:59:59 + + + P0D + + Friday + First + April + + 03:00:00 + + + + + + + diff --git a/room_status.py b/room_status.py new file mode 100644 index 0000000..c7755be --- /dev/null +++ b/room_status.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +APIs to query an Exchange Server for availability status of rooms +""" + +import argparse +import sys + +import common +from exchange_api import ExchangeApi + +reload(sys) +sys.setdefaultencoding("utf-8") + +SEPARATOR = "-" * 120 + "\n" +TABLE_FORMAT = "{0:40s} {1:64s} {2:20s}\n" +TABLE_HEADER = SEPARATOR + TABLE_FORMAT.format("Status", "Room", "Email") + SEPARATOR + +class RoomStatus(object): + """ Class to query an Exchange Server for status of specified room """ + + def __init__(self): + self.start_time = common.time_now() + self.end_time = common.end_time(self.start_time, "15m") + self.room_info = {} + self.timezone = common.SJ_TIME_ZONE + self.error = None + self.exchange_api = ExchangeApi('', '') + + def status(self, room_email): + common.LOGGER.debug("Querying for %s", room_email) + + try: + if '@' not in room_email: + room_info = self.exchange_api.find_rooms(prefix=room_email) + if len(room_info) != 1: + raise Exception('No room with that name') + room_email = room_info.keys()[0]["email"] + + room_info = self.exchange_api.room_status( \ + room_email=room_email, + start_time=self.start_time, + end_time=self.end_time, + timezone_offset=self.timezone) + + if not room_info.get('freebusy'): + raise Exception('Room not found') + + return room_info + except Exception as exception: + self.error = exception + common.LOGGER.warning("Exception querying room %s: %s", room_email, exception.message) + return {'error': exception.message} + +def run(): + """ Parse command-line arguments and invoke room availability finder """ + parser = argparse.ArgumentParser() + parser.add_argument("-r", "--roomemail", help="e-mail address of the room", required=True) + + args = parser.parse_args() + room = RoomStatus() + print room.status(args.roomemail) + + +if __name__ == '__main__': + run() diff --git a/run.sh b/run.sh new file mode 100755 index 0000000..3737203 --- /dev/null +++ b/run.sh @@ -0,0 +1,3 @@ +#!/bin/sh + +PYTHONPATH=. gunicorn --config=gunicorn.cfg service.webserver:APP diff --git a/service/__init__.py b/service/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/service/static/css/style.css b/service/static/css/style.css new file mode 100644 index 0000000..b2b51c1 --- /dev/null +++ b/service/static/css/style.css @@ -0,0 +1,199 @@ +label { + font-size: x-small; +} + +.cityLabel { + margin-left: 20px; +} + +.attendeesLabel { + margin-left: 60%; +} + +.buildingLabel { + clear: left; + float: left; + margin-left: 20px; +} + +.floorLabel { + float: left; + margin-left: 57%; +} + +#citySelect, #buildingSelect{ + width: 60%; + background-color: lightgray; +} + +#roomSizeSelect, #floorSelect{ + width: 20%; + background-color: lightgray; +} + +.buildingFloorSelectContainer { + clear: left; +} + +.selectStyle { + width: 30%; + margin-left: 20px; + float: left; + padding: 10px; +} + +.leftAndRightDivContainer { + width: 100%; + display: table; +} +.tableRow { + display: table-row; +} +.leftDiv { + width: 40%; + display: table-cell; + border-style: solid; + border-width: 1px; +} +.rightDiv { + width: 60%; + display: table-cell; + border-style: solid; + border-width: 1px; + background-color: lightgray; +} +.titleContainer { + border-bottom: 2px solid #ddd; +} + +.titleText { + margin-left: 40px; +} + +.dateContainer { + clear: left; + margin: 40px 0px 0px 20px; +} + +.dateText { + float: left; + margin-right: 20px; +} + +.dateInput { + float: left; + width: 60%; +} + +.timeText { + clear: left; + float: left; + margin-left: 20px; +} + + + +.table-borderless tbody tr td, .table-borderless tbody tr th, .table-borderless thead tr th { + border: none; + td { + width: 90px; + } +} + +.tableContainer { + height: 300px; + margin: 10px 20px 0px 20px; + overflow: scroll; +} + +.searchButton { + margin: 20px; + width: 90%; + height: 40px; + background-color: lightgray; + border: 0.5px solid #888888; + box-shadow: 1px 2px 4px #888888; +} + +hr { + border: 0; + height: 1px; + margin: 0px 20px 0px 20px; + background: #333; + clear: left; +} + +.timeLabel { + width: 30px; + display:block; + text-align:right; + float: left; +} + +.timeLabelHidden { + width: 0px; + display:block; + text-align:right; + float: left; + visibility: hidden; +} + +.timeTableRow { + margin: 5px 0px 0px 0px; +} + +.tableButton { + width: 40%; + height: 10%; + margin-left: 10px; + background-color: white; + border: 0.5px solid #888888; + box-shadow: 1px 2px 3px #888888; +} + +.tableBtnEnabled { + box-shadow: 1px 0px 1px 1px deepskyblue; + background-color: lightcyan; +} +.timeLabelEnabled { + color: deepskyblue; +} + +.roomNamesRow { + margin: 5px 0px 0px 40px; +} + +.roomNamesRadioLbl { + margin-left: 10px; + font-size: small; +} + +.rightDivLbl { + margin-left: 10px; + margin-top: 10px; + font-size: medium; +} + +.userNamePswdLbl { + margin-left: 45px; +} + +.userNamePswdInputText { + margin-left: 40px; + width: 40%; +} + +.reserveButton { + margin: 40px; + width: 20%; + height: 30px; + background-color: lightgray; + + box-shadow: 1px 0px 1px 1px #888888; +} + +.mapContainer { + margin: 5px 0px 0px 40px; + display: none; + height: 400px; +} diff --git a/service/static/js/main.js b/service/static/js/main.js new file mode 100644 index 0000000..e30edfe --- /dev/null +++ b/service/static/js/main.js @@ -0,0 +1,298 @@ +var cities = []; +var buildings = []; +var floors = [];; +var attendees = ["1", "2", "4", "5", "6", "7", "8", "9", "10", "15", "20", + "25", "30", "50", "70", "100"]; +//var times = ["00:00", "01:00", "02:00", "03:00", "04:00", "05:00", "06:00", +// "07:00", "08:00", "09:00", "10:00", "11:00", "12:00", "13:00", +// "14:00", "15:00", "16:00", "17:00", "18:00", "18:00", "19:00", +// "20:00", "21:00", "22:00", "23:00"]; +var times = ["00:00", "00:30", "01:00", "01:30", "02:00", "02:30", "03:00", + "03:30", "04:00", "04:30", "05:00", "05:30", "06:00", "06:30", + "07:00", "07:30", "08:00", "08:30", "09:00", "09:30", "10:00", + "10:30", "11:00", "11:30", "12:00", "12:30", "13:00", "13:30", + "14:00", "14:30", "15:00", "15:30", "16:00", "16:30", "17:00", + "17:30", "18:00", "18:30", "19:00", "19:30", "20:00", "20:30", + "21:00", "21:30", "22:00", "22:30", "23:00", "23:30",]; +var startTimeIndex = null; +var endTimeIndex = null; + +var selectedRoom; + +function init(){ + loadCitiesList(); + + hideUsernamePasswordFields(); + hideRoomList(); + + createCombo(buildingSelect,buildings); + createCombo(floorSelect,floors); + createCombo(citySelect,cities); + createCombo(roomSizeSelect,attendees); + + citySelect.value = "San Jose"; + loadBuildingList(citySelect.value); + buildingSelect.value = "SJC19"; + loadFloorList(buildingSelect.value); + floorSelect.value = "2"; + + if (navigator.geolocation) { + navigator.geolocation.getCurrentPosition(getCity); + } + + setTodayDate(); + createTimeRows(times); + selectCurrentTime(); +} + +function createCombo(container, data) { + var options = ''; + container.options.length = 0; + for (var i=0; i < data.length; i++) { + container.options.add(new Option(data[i], data[i])); + } +} + +function loadCitiesList() { + var xmlHttp = new XMLHttpRequest(); + xmlHttp.open("GET", "/showcities", false); + xmlHttp.send(null); + cities = JSON.parse(xmlHttp.responseText); +} + +function getCity(position) { + var xmlHttp = new XMLHttpRequest(); + xmlHttp.open("GET", "/getcity?latitude=" + position.coords.latitude + "&longitude=" + position.coords.longitude, false); + xmlHttp.send(null); + closestCity = JSON.parse(xmlHttp.responseText); + if (citySelect.value != closestCity) { + citySelect.value = closestCity; + loadBuildingList(citySelect.value); + loadFloorList(buildingSelect.value); + } +} + +function loadFloorList(buildingname) { + var xmlHttp = new XMLHttpRequest(); + xmlHttp.open("GET", "/showfloors?buildingname=" + buildingname, false); + xmlHttp.send(null); + floors = JSON.parse(xmlHttp.responseText); + createCombo(floorSelect, floors); +} + +function loadBuildingList(city) { + var xmlHttp = new XMLHttpRequest(); + xmlHttp.open("GET", "/showbuildings?city=" + city, false); + xmlHttp.send(null); + buildings = JSON.parse(xmlHttp.responseText); + createCombo(buildingSelect, buildings); + resultMessage.style.display = "none"; + resultMap.style.display = "none"; +} + +function createTimeRows(data) { + var i = 0; + for (i = 0; i < data.length; i++) { + tableContainer.innerHTML += "
"; + tableContainer.innerHTML += ""; + tableContainer.innerHTML += ""; + i++; + tableContainer.innerHTML += ""; + tableContainer.innerHTML += ""; + tableContainer.innerHTML += "
"; + } + + tableContainer.innerHTML += "
"; + tableContainer.innerHTML += "
"; +} + +function handleTime(index) { + if ((startTimeIndex == null) || (index < startTimeIndex)) { + startTimeIndex = index; + endTimeIndex = index; + } + else if (startTimeIndex == endTimeIndex) { + endTimeIndex = index; + } + else { + startTimeIndex = index; + endTimeIndex = index; + } + + var btn; + var lbl; + for (var i=0 ; i= startTimeIndex && i <= endTimeIndex) { + btn.classList.add('tableBtnEnabled'); + lbl.classList.add('timeLabelEnabled'); + } + else { + btn.classList.remove('tableBtnEnabled'); + lbl.classList.remove('timeLabelEnabled'); + } + } +} + +function selectCurrentTime() { + var date = new Date(); + var current_hour = date.getHours(); + var current_min = date.getMinutes(); + + if (current_min < 15) { + current_min = "00"; + } + else if (current_min < 45) { + current_min = "30"; + } + else { + current_hour++; + current_min = "00"; + } + + if (current_hour < 10) { + current_hour = "0" + current_hour; + } + else { + current_hour = "" + current_hour; + } + + var selectTime = current_hour + ":" + current_min; + + for (var i=0 ; i'; + for (var key in rooms_json) { + var roomemail = rooms_json[key]["email"]; + if (typeof roomemail != "undefined") { + roomNamesContainer.innerHTML += '
'; + } + } +} + +function handleSelectRoomBtn (radioBtn) { + selectedRoom = radioBtn.value; + showUsernamePasswordFields(); +} + +function handleReserveBtnClick() { + var passwordb64 = encodeURIComponent(btoa(passwordInput.value)); + var timezone = new Date().getTimezoneOffset(); + var starttime = (eval('timeBtn' + startTimeIndex)).value; + var endtime = (eval('timeBtn' + (endTimeIndex + 1))).value; + var queryString = `\?user=${userNameInput.value}\&password=${passwordb64}&roomname=${selectedRoom}&starttime=${starttime}&endtime=${endtime}&date=${dateInput.value}&timezone=${timezone}`; + var xmlHttp = new XMLHttpRequest(); + + if (userNameInput.value == "") { + userNameInput.style.backgroundColor = "yellow"; + } + else { + userNameInput.style.backgroundColor = ""; + } + + if (passwordInput.value == "") { + passwordInput.style.backgroundColor = "yellow"; + } + else { + passwordInput.style.backgroundColor = ""; + } + + if (userNameInput.value == "" || passwordInput.value == "") { + return; + } + + url = "/bookroom"; + url = url.concat(queryString); + + xmlHttp.open("GET", url, false); // false for synchronous request + xmlHttp.send(null); + hideRoomList(); + hideUsernamePasswordFields(); + resultMessage.style.display = "block"; + resultMap.style.display = "block"; + resultMessage.innerHTML = selectedRoom; + resultMap.innerHTML = xmlHttp.responseText; +} + +function hideRoomList() { + roomNamesContainer.innerHTML = ''; + roomNamesContainer.style.display = "none"; +} + +function showUsernamePasswordFields() { + var userpassHTML = ""; + userpassHTML += "
"; + userpassHTML += ""; + userpassHTML += "
"; + userpassHTML += ""; + userpassHTML += ""; + + usernamePasswordContainer.style.display = "block"; + usernamePasswordContainer.innerHTML = userpassHTML; +} + +function hideUsernamePasswordFields() { + usernamePasswordContainer.style.display = "none"; +} + diff --git a/service/templates/floormap/.gitignore b/service/templates/floormap/.gitignore new file mode 100644 index 0000000..5e7d273 --- /dev/null +++ b/service/templates/floormap/.gitignore @@ -0,0 +1,4 @@ +# Ignore everything in this directory +* +# Except this file +!.gitignore diff --git a/service/templates/index.html b/service/templates/index.html new file mode 100644 index 0000000..f407e91 --- /dev/null +++ b/service/templates/index.html @@ -0,0 +1,60 @@ + + + + Room finder + + + + + + + + + +
+
+

Conference Room Finder

+
+
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+
+ + +
+ +
+
+
+
+
+ +
+
+
+
+ + + diff --git a/service/webserver.py b/service/webserver.py new file mode 100755 index 0000000..1fd9632 --- /dev/null +++ b/service/webserver.py @@ -0,0 +1,189 @@ +""" +Webservice APIs for room finder backend +""" + +import base64 +import collections +import json +import os +import pipes +import socket +import subprocess + +import common +import flask + +from book_room import ReserveAvailRoom +from find_available_room import AvailRoomFinder +from room_status import RoomStatus + +APP = flask.Flask(__name__, template_folder=common.TEMPLATE_FOLDER) + +@APP.route('/') +def index(): + """ Serve static index file """ + return flask.render_template('index.html') + +@APP.route('/getfloormap', methods=['GET']) +def get_floor_map(): + bldgfloorname = flask.request.args.get('bldgfloorname') + return flask.send_file(os.path.join(common.FLOORMAP_DIR, bldgfloorname)) + +QueryParam = collections.namedtuple('QueryParam', 'buildingname, floor, date, starttime, endtime, user, password, attendees, timezone') +RoomStatusQueryParam = collections.namedtuple('QueryParam', 'roomemail') +BookRoomQueryParam = collections.namedtuple('QueryParam', 'roomname, date, starttime, endtime, user, password, timezone') + +@APP.route('/getcity', methods=['GET']) +def get_city(): + """ Get closest city in JSON """ + latitude = flask.request.args.get('latitude') + longitude = flask.request.args.get('longitude') + city = common.get_closest_city(float(latitude), float(longitude)) + common.LOGGER.info("Closest city is %s based on coordinates: %s, %s", + city, latitude, longitude) + return json.dumps(city) + +@APP.route('/showcities', methods=['GET']) +def show_cities(): + """ Serve list of cities in JSON """ + cities = common.get_city_list() + common.LOGGER.debug("Read list of %d cities from database", len(cities)) + return json.dumps(cities) + +@APP.route('/showbuildings', methods=['GET']) +def show_buldings(): + """ Serve list of buildings in JSON """ + city = flask.request.args.get('city') + buildings = common.get_building_list(city) + common.LOGGER.debug("%d buildings in %s", len(buildings), city) + return json.dumps(buildings) + +@APP.route('/showfloors', methods=['GET']) +def show_floors(): + """ Serve list of floors in JSON """ + buildingname = flask.request.args.get('buildingname') + floors = common.get_floor_list(buildingname) + common.LOGGER.debug("%d floors in %s", len(floors), buildingname) + if len(floors) > 1: + return json.dumps(floors + ["Any"]) + else: + return json.dumps(floors) + +# Example Query +# http://127.0.0.1/roomstatus?room_email=a@a.com +@APP.route('/roomstatus', methods=['GET']) +def room_status(): + """ Checks if room is free for next 15 mins """ + queryparam = RoomStatusQueryParam(roomemail=flask.request.args.get('roomemail')) + room = RoomStatus() + status = room.status(queryparam.roomemail) + return json.dumps(status) + + +# Example Query +# http://127.0.0.1/showrooms?building_floor_name=ABC&starttime=2016-08-25T09:00:00-13:00&duration=1h&user=USER&password=password +@APP.route('/showrooms', methods=['GET']) +def show_rooms(): + """ Serve list of rooms in JSON """ + queryparam = QueryParam(buildingname=flask.request.args.get('buildingname'), + floor=flask.request.args.get('floor'), + date=flask.request.args.get('date'), + starttime=flask.request.args.get('starttime'), + endtime=flask.request.args.get('endtime'), + user=flask.request.args.get('user'), + password=flask.request.args.get('password'), + attendees=flask.request.args.get('attendees'), + timezone=flask.request.args.get('timezone')) + + if queryparam.floor.startswith("Any"): + prefix = queryparam.buildingname + else: + prefix = queryparam.buildingname + '-' + queryparam.floor + + try: + room_finder = AvailRoomFinder(user=queryparam.user, + password=queryparam.password, + start_time=queryparam.date + "T" + queryparam.starttime + ":00", + end_time=queryparam.date + "T" + queryparam.endtime + ":00", + timezone=queryparam.timezone) + rooms_info = room_finder.search_free(prefix, min_size=int(queryparam.attendees)) + except Exception as exception: + common.LOGGER.warning("User %s query resulted in an error: %s", + queryparam.user, str(exception)) + rooms_info = {"Error" : str(exception)} + return json.dumps(rooms_info) + +# Example Query +# http://127.0.0.1/schedule?emails=a@a.com,b@b.com&date=2016-08-25&timezone=420 +@APP.route('/schedule', methods=['GET']) +def show_schedule(): + """ Serve schedule of users in JSON """ + date = date=flask.request.args.get('date') + emails = flask.request.args.get('emails').split(',') + timezone = flask.request.args.get('timezone') + + try: + room_finder = AvailRoomFinder(user='anon', + password='', + start_time=date + "T00:00:00", + end_time=date + "T23:59:59", + timezone=timezone) + rooms_info = room_finder.search_common_free(emails) + except Exception as exception: + common.LOGGER.warning("Query for emails %s resulted in an error: %s", + ', '.join(emails), str(exception)) + rooms_info = {"Error" : str(exception)} + return json.dumps(rooms_info) + + + +@APP.route('/bookroom', methods=['GET']) +def book_room(): + """ Reserve specified room """ + queryparam = BookRoomQueryParam(roomname=flask.request.args.get('roomname'), + date=flask.request.args.get('date'), + starttime=flask.request.args.get('starttime'), + endtime=flask.request.args.get('endtime'), + user=flask.request.args.get('user'), + password=flask.request.args.get('password'), + timezone=flask.request.args.get('timezone')) + + room_finder = ReserveAvailRoom(user=queryparam.user, + password=queryparam.password, + roomname=queryparam.roomname, + start_time=queryparam.date + "T" + queryparam.starttime + ":00", + end_time=queryparam.date + "T" + queryparam.endtime + ":00", + timezone=queryparam.timezone) + try: + if room_finder.reserve_room(): + common.LOGGER.warning("User %s reservation of %s succeeded", + queryparam.user, queryparam.roomname) + bldg, floor, unused = queryparam.roomname.split('-', 2) + URL = "https://wwwin.cisco.com/c/dam/cec/organizations/gbs/wpr/FloorPlans/{}-AFP-{}.pdf".format(bldg, floor) + curl_command = "curl --location-trusted -L --ntlm -c cookies.txt -u " + pipes.quote(queryparam.user) + ":" + pipes.quote(base64.b64decode(queryparam.password)) + " " + URL + " -o " + common.FLOORMAP_DIR + "/{}-AFP-{}.pdf".format(bldg, floor) + curl_process = subprocess.Popen(curl_command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True) + response = curl_process.communicate()[0] + common.LOGGER.warning("Floor map for building %s floor %s downloaded", bldg, floor) + return "Reservation Requested


".format(bldg, floor) + else: + common.LOGGER.warning("User %s reservation of %s failed", + queryparam.user, queryparam.roomname) + return "reservation failed" + except Exception as exception: + common.LOGGER.warning("User %s reservation of %s resulted in an error: %s", + queryparam.user, queryparam.roomname, str(exception)) + return "reservation failed: " + str(exception) + +def create_ssl_context(): + """ Create SSL context """ + context = (os.path.join(common.CERT_DIR, 'roomfinder.cert'), + os.path.join(common.CERT_DIR, 'roomfinder.key')) + return context + +if __name__ == '__main__': + if common.HTTPS_ENABLED: + APP.run(threaded=True, host=socket.gethostname(), + ssl_context=create_ssl_context(), port=common.HTTPS_PORT) + else: + APP.run(threaded=True, host=socket.gethostname(), + port=common.HTTP_PORT) diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..7919446 --- /dev/null +++ b/setup.py @@ -0,0 +1,13 @@ +from setuptools import setup, find_packages + +setup(name='roomfinder', + author='Saish Gersappa', + author_email='sgersapp@cisco.com', + version='0.9', + packages=find_packages(), + install_requires = [ + 'argparse', + 'flask', + 'gunicorn', + ] + ) diff --git a/test_exchange_api.py b/test_exchange_api.py new file mode 100644 index 0000000..18eeaad --- /dev/null +++ b/test_exchange_api.py @@ -0,0 +1,128 @@ +""" +Unit Tests for exchange_api.py +""" + +import string + +import exchange_api +import mock +import nose + +XML_KEYS = {'XML_SOAP_ENVELOPE' : 'http://schemas.xmlsoap.org/soap/envelope/', + 'XML_SCHEMA' : 'http://schemas.microsoft.com/exchange/services/2006', + 'XML_W3_SCHEMA' : 'http://www.w3.org/2001/XMLSchema', + } + +ROOM_INFO_XML = string.Template('$room_name$room_name@example.comSMTPMailbox$room_name (12)ROOM0 Street St.Los GatosCaliforniaUnited StatesActiveDirectory$room_name') + +FIND_ROOMS_XML = string.Template('Multiple results were found.ErrorNameResolutionMultipleResults0$room_info'.format(**XML_KEYS)) + +ROOM_AVAIL_XML = string.Template('NoErrorDetailedMerged$freebusy4800111Sunday-6023SundaySunday Monday Tuesday Wednesday Thursday Friday Saturday01439'.format(**XML_KEYS)) + +ROOM_RESERVE_XML = string.Template('NoError'.format(**XML_KEYS)) + +ROOM_EMAIL = 'ROOM@example.com' +ROOM_NAME = 'ROOM' +START_TIME = "2016-11-09T11:00:00" +END_TIME = "2016-11-09T11:30:00" +TIME_ZONE_OFFSET = "480" + +@nose.tools.raises(Exception) +@mock.patch('exchange_api.subprocess.Popen') +def test_auth_failure(mock_popen): + get_popen_mock_room_avail(mock_popen, '') + api = exchange_api.ExchangeApi(user="testuser", password="testpassword") + api.find_rooms(prefix="ROOM") + assert False + +def get_popen_mock_find_rooms(mock_popen, room_info): + process_mock = mock.Mock() + attrs = {'communicate.return_value': + (FIND_ROOMS_XML.substitute(num_rooms=len(room_info), + room_info=''.join(room_info)), + '')} + process_mock.configure_mock(**attrs) + mock_popen.return_value = process_mock + +@mock.patch('exchange_api.subprocess.Popen') +def test_find_rooms(mock_popen): + room1 = ROOM_INFO_XML.substitute(room_name='ROOM1') + room2 = ROOM_INFO_XML.substitute(room_name='ROOM2') + room3 = ROOM_INFO_XML.substitute(room_name='ROOM3') + + get_popen_mock_find_rooms(mock_popen, room_info=[room1, room2, room3]) + api = exchange_api.ExchangeApi(user="testuser", password="testpassword") + response = api.find_rooms(prefix="ROOM") + assert len(response) == 3 + +def get_popen_mock_room_avail(mock_popen, freebusy): + process_mock = mock.Mock() + attrs = {'communicate.return_value': (ROOM_AVAIL_XML.substitute(freebusy=freebusy), '')} + process_mock.configure_mock(**attrs) + mock_popen.return_value = process_mock + +@mock.patch('exchange_api.subprocess.Popen') +def test_room_available(mock_popen): + freebusy = '0000' + get_popen_mock_room_avail(mock_popen, freebusy=freebusy) + + api = exchange_api.ExchangeApi(user="testuser", password="testpassword") + response = api.room_status(room_email=ROOM_EMAIL, + start_time=END_TIME, + end_time=END_TIME, + timezone_offset=TIME_ZONE_OFFSET) + + # Keys:'freebusy', 'status', 'email' + assert response['status'] == 'Free' + assert response['freebusy'] == freebusy + assert response['email'] == ROOM_EMAIL + +@mock.patch('exchange_api.subprocess.Popen') +def test_room_unavailable(mock_popen): + freebusy = '0022' + get_popen_mock_room_avail(mock_popen, freebusy=freebusy) + + api = exchange_api.ExchangeApi(user="testuser", password="testpassword") + response = api.room_status(room_email=ROOM_EMAIL, + start_time=START_TIME, + end_time=END_TIME, + timezone_offset=TIME_ZONE_OFFSET) + + # Keys:'freebusy', 'status', 'email' + assert response['status'] != 'Free' + assert response['freebusy'] == freebusy + assert response['email'] == ROOM_EMAIL + +def get_popen_mock_room_reserve(mock_popen, result): + process_mock = mock.Mock() + attrs = {'communicate.return_value': (ROOM_RESERVE_XML.substitute(result=result), '')} + process_mock.configure_mock(**attrs) + mock_popen.return_value = process_mock + +@mock.patch('exchange_api.subprocess.Popen') +def test_room_reserve_success(mock_popen): + result = 'Success' + get_popen_mock_room_reserve(mock_popen, result) + + api = exchange_api.ExchangeApi(user="testuser", password="testpassword") + response = api.reserve_room(room_email=ROOM_EMAIL, + room_name=ROOM_NAME, + start_time=START_TIME, + end_time=END_TIME, + timezone_offset=TIME_ZONE_OFFSET) + + assert response is True + +@mock.patch('exchange_api.subprocess.Popen') +def test_room_reserve_failure(mock_popen): + result = 'Failure' + get_popen_mock_room_reserve(mock_popen, result) + + api = exchange_api.ExchangeApi(user="testuser", password="testpassword") + response = api.reserve_room(room_email=ROOM_EMAIL, + room_name=ROOM_NAME, + start_time=START_TIME, + end_time=END_TIME, + timezone_offset=TIME_ZONE_OFFSET) + + assert response is False