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/find_available_room.py b/find_available_room.py index 205115f..d9c5be7 100644 --- a/find_available_room.py +++ b/find_available_room.py @@ -1,67 +1,145 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- +import argparse +import csv +import datetime +import getpass +import subprocess import sys +import xml.etree.ElementTree as ET + +from string import Template + +URL = 'https://mail.cisco.com/ews/exchange.asmx' +SCHEME_TYPES = './/{http://schemas.microsoft.com/exchange/services/2006/types}' +TIME_NOW = datetime.datetime.now().replace(microsecond=0).isoformat() +TIME_ZONE = '' + 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 +class AvailRoomFinder(object): + + def __init__(self, user, password, + start_time=TIME_NOW, duration='1h', + roominfo='rooms.csv'): + self.rooms = self._read_room_list(roominfo) + self.user = user + self.password = password + self.start_time = start_time + + try: + if 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: + self.duration = (1, 0) + + start = datetime.datetime.strptime(start_time, "%Y-%m-%dT%H:%M:%S") + self.end_time = (start + datetime.timedelta(hours=hours, minutes=mins)).isoformat() + + def _read_room_list(self, filename): + rooms = {} + + with open(filename, 'r') as fhandle: + reader = csv.reader(fhandle) + for row in reader: + rooms[unicode(row[1])] = (unicode(row[0]), int(row[2])) + + return rooms + + def search_free(self, prefix, min_size=1, print_to_stdout=False): + selected_rooms = {} + for email in self.rooms: + name, size = self.rooms[email] + if name.startswith(prefix) and size > min_size: + selected_rooms[email] = (name, size) + + selected_room_info = self.search(selected_rooms, print_to_stdout) + free_room_info = {} + for email in selected_room_info: + if selected_room_info[email]['status'] == 'Free': + free_room_info[email] = selected_room_info[email] + return free_room_info + + def search(self, selected_rooms=None, print_to_stdout=False): + if selected_rooms is None: + selected_rooms = self.rooms + room_info = {} + + if print_to_stdout: + print "Searching for a room from " + self.start_time + " to " + self.end_time + ":" + print "{0:20s} {1:64s} {2:64s}".format("Status", "Room", "Email") -now = datetime.datetime.now().replace(microsecond=0) -starttime_default = now.isoformat() -end_time_default = None + xml_template = open("getavailibility_template.xml", "r").read() + xml = Template(xml_template) -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 email in selected_rooms: + data = unicode(xml.substitute(email=email, + starttime=self.start_time + TIME_ZONE, + endtime=self.end_time + TIME_ZONE)) -args=parser.parse_args() + header = "\"content-type: text/xml;charset=utf-8\"" + command = "curl --silent --header " + header \ + + " --data '" + data \ + + "' --ntlm " \ + + "-u "+ self.user + ":" + self.password \ + + " " + URL + response = subprocess.Popen(command, stdout=subprocess.PIPE, shell=True).communicate()[0] + if not response: + return room_info -url = args.url + tree = ET.fromstring(response) -rooms={} -reader = csv.reader(codecs.open(args.file, 'r', encoding='utf-8')) -for row in reader: - rooms[unicode(row[1])]=unicode(row[0]) + status = "Free" + elems = tree.findall(SCHEME_TYPES + "MergedFreeBusy") + 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" -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 + name, size = self.rooms[email] + room_info[name] = {'size': size, 'freebusy': freebusy, 'status': status} -user = args.user -password = getpass.getpass("Password:") + if print_to_stdout: + print "{0:20s} {1:64s} {2:64s}".format(status + '-' + freebusy, self.rooms[email], email) -print "Searching for a room from " + start_time + " to " + end_time + ":" -print "{0:10s} {1:64s} {2:64s}".format("Status", "Room", "Email") + return room_info -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)) +def run(): + parser = argparse.ArgumentParser() + parser.add_argument("-u", "--user", help="user name for exchange/outlook", required=True) + parser.add_argument("-prefix", "--prefix", + help="A prefix to search for. e.g. 'SJC19- SJC18-'", + default='') + parser.add_argument("-start", "--starttime", + help="Starttime e.g. 2014-07-02T11:00:00 (default = now)", + default=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=rooms.csv).", + default="rooms.csv") - 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] + args = parser.parse_args() + args.password = getpass.getpass("Password:") - tree = ET.fromstring(response) + room_finder = AvailRoomFinder(args.user, args.password, args.starttime, args.duration, args.file) + print room_finder.search_free(prefix=args.prefix, print_to_stdout=True) - 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..3f5f520 100644 --- a/find_rooms.py +++ b/find_rooms.py @@ -1,67 +1,110 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -import sys -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 +import subprocess +import sys + +from string import Template +from string import letters +from string import digits + +URL = 'https://mail.cisco.com/ews/exchange.asmx' +SCHEME_TYPES = './/{http://schemas.microsoft.com/exchange/services/2006/types}' + +reload(sys) +sys.setdefaultencoding("utf-8") + +class RoomFinder(object): + + def __init__(self, user, password): + self.user = user + self.password = password + xml_template = open("resolvenames_template.xml", "r").read() + self.xml = Template(xml_template) + self.rooms = {} + + def _search(self, prefix): + tmp_rooms = {} + data = unicode(self.xml.substitute(name=prefix)) + + header = '"content-type: text/xml;charset=utf-8"' + command = "curl --silent --header " + header \ + + " --data '" + data \ + + "' --ntlm " \ + + "-u "+ self.user + ":" + self.password \ + + " " + URL + response = subprocess.Popen(command, stdout=subprocess.PIPE, shell=True).communicate()[0] + + if not response.strip(): + # something went wrong + return tmp_rooms + + tree = ET.fromstring(response) + + elems = tree.findall(SCHEME_TYPES + "Resolution") + for elem in elems: + email = elem.findall(SCHEME_TYPES + "EmailAddress") + name = elem.findall(SCHEME_TYPES + "DisplayName") + if len(email) and len(name): + roomsize = self.room_size(name[0].text) + if roomsize: + tmp_rooms[email[0].text] = (name[0].text, roomsize) + return tmp_rooms + + def search(self, prefix, deep=False): + rooms_found = self._search(prefix) -def findRooms(prefix): - rooms={} - data = unicode(xml.substitute(name=prefix)) + if deep: + symbols = letters + digits + for symbol in symbols: + prefix_deep = prefix + " " + symbol + rooms_found.update(self._search(prefix_deep)) - 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) + print "Search for prefix '" + prefix + "' yielded " + str(len(rooms_found)) + " rooms." + self.rooms.update(self._search(prefix)) - 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 + def room_size(self, roomname): + try: + return int(roomname[roomname.find('(') + 1 : roomname.find(')')]) + except ValueError: + return 0 -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() + def dump(self, filename='rooms.csv'): + if not len(self.rooms): + print "Check your arguments for mistakes" + return 0 -url = args.url -user = args.user -password = getpass.getpass("Password:") + with open(filename, "wb") as fhandle: + writer = csv.writer(fhandle) + for email, room_info in sorted(self.rooms.iteritems(), key=operator.itemgetter(1)): + name, size = room_info + writer.writerow([name, email, size]) -xml_template = open("resolvenames_template.xml", "r").read() -xml = Template(xml_template) + return len(self.rooms) -rooms={} +def run(): + 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() -for prefix in args.prefix: - rooms.update(findRooms(prefix)) - print "After searching for prefix '" + prefix + "' we found " + str(len(rooms)) + " rooms." + args.password = getpass.getpass("Password:") - deep = args.deep + finder = RoomFinder(args.user, args.password) - if deep: - symbols = letters + digits - for symbol in symbols: - prefix_deep = prefix + " " + symbol - rooms.update(findRooms(prefix_deep)) + for prefix in args.prefix: + finder.search(prefix, args.deep) - print "After deep search for prefix '" + prefix + "' we found " + str(len(rooms)) + " rooms." + if finder.dump(): + exit(0) + else: + exit(1) -writer = csv.writer(open("rooms.csv", "wb")) -for item in sorted(rooms.iteritems(), key=operator.itemgetter(1)): - writer.writerow([item[1],item[0]]) +if __name__ == '__main__': + run() diff --git a/getavailibility_template.xml b/getavailibility_template.xml index ebee671..265b7a1 100644 --- a/getavailibility_template.xml +++ b/getavailibility_template.xml @@ -4,16 +4,16 @@ - -60 + 420 - 0 + -60 5 10 Sunday - -60 + 0 5 3 @@ -36,7 +36,7 @@ $starttime $endtime - 60 + 15 DetailedMerged diff --git a/service/webserver.py b/service/webserver.py new file mode 100755 index 0000000..979aa80 --- /dev/null +++ b/service/webserver.py @@ -0,0 +1,74 @@ +import codecs +import csv +import datetime +import json +import os +import subprocess +import xml.etree.ElementTree as ET + +from collections import namedtuple +from flask import Flask +from shutil import copyfile +from string import Template +from find_available_room import AvailRoomFinder + +PWD = os.getcwd() + +CONFIG = { + 'home' : PWD, + 'roomscsv' : PWD + '/rooms.csv', + 'roomssearchcsv' : PWD + '/roomssearch.csv', + 'availibility_template' : PWD + '/getavailibility_template.xml', + 'URL': "https://mail.cisco.com/ews/exchange.asmx", + 'allrooms' : PWD + '/allrooms.csv', + } + +app = Flask(__name__) + +@app.route('/') +def index(): + return "Welcome to Room Finder Web Service" + + +from flask import request + +QueryParam = namedtuple('QueryParam', 'roomname, starttime, duration, user, password') + +@app.route('/showbuldings', methods=['GET']) +def show_buldings(): + buldings = [] + with open(CONFIG['allrooms'],'r') as f: + for line in f.readlines(): + buldingname = line.split('-')[0] + if buldingname not in buldings: + buldings.append(buldingname) + return json.dumps(buldings) + + +# Example Query +# http://127.0.0.1:5000/showrooms?roomname=ABC&starttime=2016-08-25T09:00:00-13:00&duration=1h&user=USER&password=password +@app.route('/showrooms', methods=['GET']) +def show_rooms(): + queryparam = QueryParam( + roomname=request.args.get('roomname'), + starttime=request.args.get('starttime'), + duration=request.args.get('duration'), + user = request.args.get('user'), + password = request.args.get('password'), + ) + + _create_tmp_rooms_file(queryparam.roomname) + + room_finder = AvailRoomFinder(queryparam.user, queryparam.password, queryparam.starttime, queryparam.duration, CONFIG['roomssearchcsv']) + rooms_info = room_finder.search_free(prefix=queryparam.roomname, print_to_stdout=True) + + return json.dumps(rooms_info) + +def _create_tmp_rooms_file(roomname): + if 'all' in roomname: + copyfile(CONFIG['roomscsv'], CONFIG['roomssearchcsv']) + else: + open(CONFIG['roomssearchcsv'],'w').writelines([ line for line in open(CONFIG['roomscsv']) if roomname in line]) + +if __name__ == '__main__': + app.run(debug=True) diff --git a/ui/index.html b/ui/index.html new file mode 100644 index 0000000..3fdfbb1 --- /dev/null +++ b/ui/index.html @@ -0,0 +1,52 @@ + + + + + + + +
+
+ +
+
+
+ User Name: + Password: +
+ +

+
+ + + + + + +
+
+
+ + + + + + + + + + +
+
+ + +

+ +
+
+ + +
+
+ + diff --git a/ui/main.js b/ui/main.js new file mode 100644 index 0000000..e5cc82b --- /dev/null +++ b/ui/main.js @@ -0,0 +1,75 @@ +var buildings = ["SJC02", "SJC03", "SJC04", "SJC05", + "SJC06", "SJC07", "SJC08", "SJC09", + "SJC10", "SJC11", "SJC12", "SJC13", + "SJC14", "SJC15", "SJC16", "SJC17", + "SJC18", "SJC19", "SJC20", "SJC21", + "SJC22", "SJC23", "SJC24", "SJC28", + "SJC29", "SJC30", "SJC31", "SJC32", + "SJCA", "SJCB", "SJCD", "SJCI", "SJCJ", "SJCK", + "SJCL", "SJCM", "SJCMR1", "SJCMR3", "SJCN", + "SJCO", "SJCP", "SJCPLM01", "SJCQ", "SJCSYC02"] +var floors = ["1", "2", "3", "4", "5"]; +var times_hours = ["00", "01", "02","03", + "04", "05", "06", "07", + "08", "09", "10", "11", + "12", "13", "14", "15", + "16", "17", "18", "19", + "20", "21", "22", "23"]; +var times_mins = ["00", "15", "30","45"]; +var duration_hours = ["0h", "1h", "2h","3h", + "4h", "5h", "6h", "7h"]; +var duration_mins = ["", "15m", "30m","45m"]; +var sizes = ["1", "2", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16", "17", "18", "20", "23", "24", "25", "30", "36", "50", "60", "65", "68", "70", "100", "120", "410"] +var date_years = ["2016", "2017"]; +var date_months = ["01", "02", "03", "04", "05", "06", "07", "08", "09", "10", "11", "12"]; +var date_days = ["01", "02", "03", "04", "05", "06", "07", "08", "09", "10", + "11", "12", "13", "14", "15", "16", "17", "18", "19", "20", + "21", "22", "23", "24", "25", "26", "27", "28", "29", "30", + "31"]; + + +function init(){ + + createCombo(buildingSelect, buildings); + createCombo(floorSelect, floors); + createCombo(startTimeHourSelect, times_hours); + createCombo(startTimeMinSelect, times_mins); + createCombo(durationHourSelect, duration_hours); + createCombo(durationMinSelect, duration_mins); + createCombo(roomSizeSelect, sizes); + createCombo(dateYearSelect, date_years); + createCombo(dateMonthSelect, date_months); + createCombo(dateDaySelect, date_days); + +} + +function createCombo(container, data) { + var options = ''; + for (var i = 0; i < data.length; i++) { + container.options.add(new Option(data[i], data[i])); + } +} + +function submitClickHandler() { + + var queryString = `?userName=${userNameInput.value}&password=${passwordInput.value}&buildingName=${buildingSelect.value}&floor=${floorSelect.value}&startTime=${startTimeSelect.value}&endTime=${endTimeSelect.value}`; + loadRooms(queryString); + //console.log(queryString); +} + +function loadRooms(postData) { + var xhttp = new XMLHttpRequest(); + xhttp.onreadystatechange = function() { + if (xhttp.readyState == 4 && xhttp.status == 200) { + //document.getElementById("demo").innerHTML = xhttp.responseText; + console.log(xhttp.responseText) + } + }; + xhttp.open("POST", "http://www.w3schools.com/ajax/demo_post2.asp", true); + xhttp.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); + xhttp.send(postData); +} + + + +