Shahar Havivi has uploaded a new change for review. Change subject: First Commit ......................................................................
First Commit Working sample of Python user portal using REST API Change-Id: I1d15dd5df8992c84b2c9529168919d05ba88bc33 Signed-off-by: Shahar Havivi <[email protected]> --- A python/Broker.py A python/README A python/RestClient.py A python/RestCommand.py A python/WebHandler.py A python/ovirt.conf 6 files changed, 422 insertions(+), 0 deletions(-) git pull ssh://gerrit.ovirt.org:29418/samples-portals refs/changes/87/9387/1 diff --git a/python/Broker.py b/python/Broker.py new file mode 100755 index 0000000..2285394 --- /dev/null +++ b/python/Broker.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python + +import WebHandler + +import time +import ConfigParser +import BaseHTTPServer + +config = ConfigParser.ConfigParser() +config.read('ovirt.conf') + +hostName = config.get('Server', 'HostName') +portNumber = config.getint('Server', 'PortNumber') + +server_class = BaseHTTPServer.HTTPServer +httpd = server_class((hostName, portNumber), WebHandler.WebHandler) +print time.asctime(), "Server Starts - %s:%s" % (hostName, portNumber) +try: + httpd.serve_forever() +except KeyboardInterrupt: + pass +httpd.server_close() +print time.asctime(), "Server Stops - %s:%s" % (hostName, portNumber) diff --git a/python/README b/python/README new file mode 100644 index 0000000..3c9fb13 --- /dev/null +++ b/python/README @@ -0,0 +1,27 @@ +This is a Python sample web-portal that uses REST API to view and run VMs from oVirt Engine. +This project run a local web server which can be configure via the ovirt.conf file. + +o Running the web server: + ./Broker.py + +o conneting to the server: + http://localhost:8000/ + +o classes: + Broker + main class, run the web server + + RestClient + dispatch HTTP GET and POST RESTful methods + + RestCommand: + parse the underline xml that return from RestCommand + + WebHandler + implementation of web server + +o configuration file: + ovirt.conf: + BaseUrl: the url where the Engine is running + HostName: the local for the web server to listen to + PortNumber: the local port number for the server to listen to diff --git a/python/RestClient.py b/python/RestClient.py new file mode 100644 index 0000000..6791d2c --- /dev/null +++ b/python/RestClient.py @@ -0,0 +1,49 @@ +import base64 +import httplib +import urllib2 +import urlparse + +cookie = None + +''' +RestClient: handler RESTful post/get methods +''' +class RestClient(object): + + def doGetMethod(self, url, userName=None, password=None): + global cookie + req = urllib2.Request(url) + if cookie is None: + auth = "%s:%s" % (userName, password) + auth = base64.encodestring(auth) + req.add_header("Authorization", "Basic %s" % auth) + else: + req.add_header("Cookie", cookie) + + # run in user level API + req.add_header('filter', 'true') + # for using cookies + req.add_header('Prefer', 'persistent-auth') + response = urllib2.urlopen(req) + if not response.info().getheader('Set-Cookie') is None: + cookie = response.info().getheader('Set-Cookie') + + return response.read() + + def doPostMethod(self, url): + global cookie + u = urlparse.urlparse(url) + + headers = {"Content-type": "application/xml"} + headers['filter'] = 'true' + headers['Prefer'] = 'persistent-auth' + headers['Cookie'] = cookie + + conn = httplib.HTTPConnection(u.hostname, u.port) + conn.request("POST", u.path, body="<action/>", headers=headers) + res = conn.getresponse() + return res.read() + + def resetCookie(self): + global cookie + cookie = None diff --git a/python/RestCommand.py b/python/RestCommand.py new file mode 100644 index 0000000..097f7be --- /dev/null +++ b/python/RestCommand.py @@ -0,0 +1,97 @@ +import RestClient + +import ConfigParser +from xml.dom import minidom + +restClient = RestClient.RestClient() + +class RestCommand(object): + + def __init__(self): + global restClient + self.config = ConfigParser.ConfigParser() + self.config.read('ovirt.conf') + self.baseUrl = self.config.get('oVirt', 'BaseUrl') + + def _parseVm(self, xmlVm): + vm = {} + vm['startable'] = True + vm['stopable'] = True + vm['connectable'] = True + vm['vmid'] = xmlVm.getAttribute('id') + vm['name'] = xmlVm.getElementsByTagName('name').item(0).firstChild.nodeValue + vm['status'] = xmlVm.getElementsByTagName('status').item(0).getElementsByTagName('state').item(0).firstChild.nodeValue + vm['display'] = xmlVm.getElementsByTagName('display').item(0).getElementsByTagName('type').item(0).firstChild.nodeValue + if len(xmlVm.getElementsByTagName('port')) > 0: + vm['port'] = xmlVm.getElementsByTagName('port').item(0).firstChild.nodeValue + else: + vm['port'] = '-1' + if len(xmlVm.getElementsByTagName('address')) > 0: + vm['address'] = xmlVm.getElementsByTagName('address').item(0).firstChild.nodeValue + else: + vm['address'] = '' + return vm + + def login(self, userName, password): + self.userName = userName + self.password = password + + try: + restClient.resetCookie() + xml = restClient.doGetMethod(self.baseUrl + '/api', userName, password) + return True + except: + return False + + def getUserVms(self): + global restClient + xml = restClient.doGetMethod(self.baseUrl + '/api/vms') + dom = minidom.parseString(xml) + xmlVms = dom.getElementsByTagName('vm') + vms = [] + for xmlVm in xmlVms: + vms.append(self._parseVm(xmlVm)) + + return vms + + def getVmByVmId(self, vmid): + global restClient + xml = restClient.doGetMethod(self.baseUrl + '/api/vms/' + vmid) + dom = minidom.parseString(xml) + return self._parseVm(dom.getElementsByTagName('vm')[0]) + + ''' + Method return action results + + input: + vmid: guid of vm + action: action to run (start, stop or ticket) - there are more actions...(never tested) + + return value: + dictionary + { 'status': 'complete', + 'reason': '...', # only if failed + 'detail': '...', # only if failed + 'value': 'XXYYZZ', # the ticket (password) + 'expired': '7200', # in minutes + } + ''' + def runAction(self, vmid, action): + global restClient + ret = {} + url = '%s/api/vms/%s/%s' % (self.baseUrl, vmid, action) + + xml = restClient.doPostMethod(url) + dom = minidom.parseString(xml) + + ret['status'] = dom.getElementsByTagName('state').item(0).firstChild.nodeValue + if ret['status'] == 'failed': + ret['reason'] = dom.getElementsByTagName('reason').item(0).firstChild.nodeValue + ret['detail'] = dom.getElementsByTagName('detail').item(0).firstChild.nodeValue + + if action == 'ticket': + elem = dom.getElementsByTagName('ticket').item(0) + ret['value'] = elem.getElementsByTagName('value').item(0).firstChild.nodeValue + ret['expired'] = elem.getElementsByTagName('expiry').item(0).firstChild.nodeValue + + return ret diff --git a/python/WebHandler.py b/python/WebHandler.py new file mode 100644 index 0000000..fdab351 --- /dev/null +++ b/python/WebHandler.py @@ -0,0 +1,220 @@ +import RestCommand + +import cgi +import BaseHTTPServer + +restCommand = None + +''' +WebHandler: simple Web Server +''' +class WebHandler(BaseHTTPServer.BaseHTTPRequestHandler): + def do_HEAD(self): + self.send_response(200) + self.send_header("Content-type", "text/html") + self.end_headers() + + def do_POST(self): + if self.path == '/': + form = cgi.FieldStorage(fp=self.rfile, headers=self.headers, environ={'REQUEST_METHOD':'POST', 'CONTENT_TYPE':self.headers['Content-Type'], }) + self._login_method(form) + else: + self._page_error() + + def do_GET(self): + # strip parameters from page name in get command + # ie /mycgi?param=value + # will get /mycgi + p = self.path + if p.find('?') != -1: + p = self.path[:self.path.find('?')] + params = self.path[self.path.find('?')+1:] + + if p == '/': + self._login_method() + elif p == '/action': + self._action_method(params) + else: + self._page_error() + + def _page_error(self): + self.send_response(404) + self.send_header("Content-type", "text/html") + self.end_headers() + self.wfile.write("<html><head><title>Page not found</title></head>") + self.wfile.write("<body><p>Page not found %s</p>" % self.path) + self.wfile.write("</body></html>") + + + def _action_method(self, params): + global restCommand + self.send_response(200) + self.send_header("Content-type", "text/html") + self.end_headers() + + form = cgi.parse_qs(params) + + vmid = form['vmid'][0] + action = form['action'][0] + + res = restCommand.runAction(vmid, action) + + if res['status'] == 'failed': + html = '''<html><body> + <p style='color:red'>Action faild!</p> + <br/>Reason: %s + <br/>Details: %s + <br/> + <button onclick=javascript:history.back()>Back</button> + </body></html>''' % (res['reason'], res['detail']) + + elif action == 'ticket': + vm = restCommand.getVmByVmId(vmid) + + if self.headers['user-agent'].lower().find('windows') >= 0: + html = self._ticketIE(vm, res) + else: + html = self._ticketFirefox(vm, res) + + else: + html = '''<html><body> + VM '%s' successfully + <br/> + <button onclick=javascript:history.back()>Back</button> + </body></html> + ''' % action + + self.wfile.write(html) + + def _ticketIE(self, vm, res): + html = '''<html> + <script> + function onConnect() { + spice.HostIP = '%s'; + spice.Port = '%s'; + spice.Password = '%s' + spice.Connect(); + } + </script> +<body> + <OBJECT style='visibility: hidden' codebase='SpiceX.cab#version=1,0,0,1' ID='spice' CLASSID='CLSID:ACD6D89C-938D-49B4-8E81-DDBD13F4B48A'> + </OBJECT> + <form> + <input type=button onclick='javascript:onConnect();' value='Connect'/> + </form> +</body> +</html>''' % (vm['address'], vm['port'], res['value']) + + return html + + def _ticketFirefox(self, vm, res): + html = '''<html> + <script> + function onConnect() { + spice.hostIP = '%s'; + spice.port = '%s'; + spice.Password = '%s'; + spice.connect(); + spice.show() + } + </script> +<body> + <embed id='spice' type="application/x-spice" width=0 height=0><br> + <form> + <input type=button value='Connent' onclick='onConnect()'/> + </form> +</body> +</html>''' % (vm['address'], vm['port'], res['value']) + + return html + + + def _uservms_method(self): + #form = cgi.FieldStorage(fp=self.rfile, headers=self.headers, environ={'REQUEST_METHOD':'POST', 'CONTENT_TYPE':self.headers['Content-Type'], }) + + self.send_response(200) + self.send_header("Content-type", "text/html") + self.end_headers() + + vms = restCommand.getUserVms() + + # render the html + html = '''<html> +<body> + <center><br/><br/> + <table cellpadding="5" style='border-width: 1px; border-spacing: 2px; border-style: outset; border-color: gray; border-collapse: separate; background-color: white;'> + <tr> + <th>VM Name</th> + <th>Status</th> + <th>Dispaly</th> + <th>Start</th> + <th>Stop</th> + <th>Connect</th> + </tr> + ''' + + for vm in vms: + startable = '' + if not vm['startable']: + startable = "disabled='disabled'" + stopable = '' + if not vm['stopable']: + stopable = "disabled='disabled'" + connectable = '' + if not vm['connectable'] or vm['display'] != 'spice': + connectable = "disabled='disabled'" + + startbtn = "<button %s onclick=javascript:location.href='action?vmid=%s&action=start' type='button'>Start</button>" % (startable, vm['vmid']) + stopbtn = "<button %s onclick=javascript:location.href='action?vmid=%s&action=stop' type='button'>Stop</button>" % (stopable, vm['vmid']) + connectbtn = "<button %s onclick=javascript:location.href='action?vmid=%s&action=ticket' type='button'>Connect</button>" % (connectable, vm['vmid']) + + html = html + ''' <tr> + <td>%s</td> + <td>%s</td> + <td>%s</td> + <td>%s</td> + <td>%s</td> + <td>%s</td> + </tr>''' % (vm['name'], vm['status'], vm['display'], startbtn, stopbtn, connectbtn) + + html = html + ''' </table></center></body></html>''' + self.wfile.write(html) + + + def _login_method(self, form={}): + global restCommand + message = '' + if form.has_key("username") and form.has_key('password'): + restCommand = RestCommand.RestCommand() + if restCommand.login(form.getvalue("username"), form.getvalue("password")): + self._uservms_method() + return + else: + message = 'Login Error' + + self.send_response(200) + self.send_header("Content-type", "text/html") + self.end_headers() + + html = '''<html> +<body> +<center><br/><br/> +<form name="input" action="/" method="post"> + <table> + <tr> + <td>User name:</td> + <td><input type="text" name="username" value=""/></td> + <tr> + <td>Password:</td> + <td><input type="password" name="password" value=""/></td> + <tr> + <td/> + <td align="right"><input type="submit" value="Login"/></td> + </tr> + <tr> + <td colspan='2' style='color:red'>%s</td> + </tr> + </form> +</body> +</html>''' + self.wfile.write(html % message) diff --git a/python/ovirt.conf b/python/ovirt.conf new file mode 100644 index 0000000..777145f --- /dev/null +++ b/python/ovirt.conf @@ -0,0 +1,6 @@ +[oVirt] +BaseUrl: http://localhost:8080 + +[Server] +HostName: 0.0.0.0 +PortNumber: 8000 -- To view, visit http://gerrit.ovirt.org/9387 To unsubscribe, visit http://gerrit.ovirt.org/settings Gerrit-MessageType: newchange Gerrit-Change-Id: I1d15dd5df8992c84b2c9529168919d05ba88bc33 Gerrit-PatchSet: 1 Gerrit-Project: samples-portals Gerrit-Branch: master Gerrit-Owner: Shahar Havivi <[email protected]> _______________________________________________ Engine-patches mailing list [email protected] http://lists.ovirt.org/mailman/listinfo/engine-patches
