mcphargus / python-glpi

A python interface to the GLPI webservices plugin
GNU General Public License v2.0
15 stars 9 forks source link

code proposition : RESTClient refactoring #5

Open frague59 opened 11 years ago

frague59 commented 11 years ago

Hi,

From your code, I've created a new way to access to the GLPI service. I've refactored the connexion / aithentification process and have curryed all glpi methods.

#!/usr/bin/env python
# -*- coding:utf-8 -*-

import urllib, urllib2
import json
import sys, os, warnings
# import getpass
import logging

DEBUG = True

logging.basicConfig(level=logging.DEBUG)

def curry(_curried_func, *args, **kwargs):
    def _curried(*moreargs, **morekwargs):
        return _curried_func(*(args + moreargs), **dict(kwargs, **morekwargs))
    return _curried

class RESTClient(object):
    """
    .. note:: If any boolean arguments are defined, they're
    automatically added to the GET request, which means the
    webservices API will treat them as being true. You've been warned.
    """

    # List all methods defined in the REST API
    METHODS = dict(test='glpi.test',
                   status='glpi.status',
                   listAllMethods='glpi.listAllMethods',
                   listEntities='glpi.listEntities',
                   doLogin='glpi.doLogin',
                   listKnowBaseItems='glpi.listKnowBaseItems',
                   getKnowBaseItem='glpi.getKnowBaseItem',
                   getDocument='glpi.getDocument',
                   doLogout='glpi.doLogout',
                   getMyInfo='glpi.getMyInfo',
                   listMyProfiles='glpi.listMyProfiles',
                   setMyProfile='glpi.setMyProfile',
                   listMyEntities='glpi.listMyEntities',
                   setMyEntity='glpi.setMyEntity',
                   listDropdownValues='glpi.listDropdownValues',
                   listGroups='glpi.listGroups',
                   listHelpdeskTypes='glpi.listHelpdeskTypes',
                   listHelpdeskItems='glpi.listHelpdeskItems',
                   listTickets='glpi.listTickets',
                   listUsers='glpi.listUsers',
                   listInventoryObjects='glpi.listInventoryObjects',
                   listObjects='glpi.listObjects',
                   getObject='glpi.getObject',
                   createObjects='glpi.createObjects',
                   deleteObjects='glpi.deleteObjects',
                   updateObjects='glpi.updateObjects',
                   linkObjects='glpi.linkObjects',
                   getInfocoms='glpi.getInfocoms',
                   getContracts='glpi.getContracts',
                   getComputer='glpi.getComputer',
                   getComputerInfoComs='glpi.getComputerInfoComs',
                   getComputerContracts='glpi.getComputerContracts',
                   getNetworkports='glpi.getNetworkports',
                   listComputers='glpi.listComputers',
                   getPhones='glpi.getPhones',
                   getNetworkEquipment='glpi.getNetworkEquipment',
                   getTicket='glpi.getTicket',
                   createTicket='glpi.createTicket',
                   addTicketFollowup='glpi.addTicketFollowup',
                   addTicketDocument='glpi.addTicketDocument',
                   addTicketObserver='glpi.addTicketObserver',
                   setTicketSatisfaction='glpi.setTicketSatisfaction',
                   setTicketValidation='glpi.setTicketValidation',)

    def __init__(self, host='glpi', baseurl='/glpi',):
        '''
        Initialize the RESTClient instance, adding supported glpi-methods by currying self._get_METHOD
        '''
        self.BASEURL = baseurl
        self._url = 'http://' + host + '/' + self.BASEURL + '/plugins/webservices/rest.php?'
        for method in self.METHODS.keys():
            setattr(self, 'get_%s' % (method),
                    curry(self._get_METHOD, self.METHODS.get(method)))

    def _get_METHOD(self, method, **kwargs):
        '''

        Private method that perform the call to glpi server with the glpi method
        and providing parameters as kwargs
        @param method: GLPI method to call
        @param kwargs: parameters to send to glpi test
        '''
        if not self.connected:
            self.connect(login_name='webservices', login_password='webservices')
        response = self.send_request(method, **kwargs)
        DEBUG and logging.debug(u'RESTClient::get_%s() '
                      u'response = %s'
                      % (method, response,))

        return response

    # Attributes
    _session = None
    _url = None

    @property
    def session(self):
        '''
        The session string
        '''
        return self._session

    @property
    def connected(self):
        '''
        tests in client is connected to the server
        '''
        return self.session is not None

    @property
    def url(self):
        ''' The complete url '''
        return self._url

    @staticmethod
    def _is_fault(response):
        '''
        tests if a fault has been raised by the server
        @param response: the decoded respose object
        @return: False if there is no error
        @raise Exception: if a server side error has been raised
        '''
        DEBUG and logging.debug(u'RESTClient::_is_fault() '
                     u'response = %s'
                     % (response,))

        if (isinstance(response, (dict))):
            faultCode = response.get('faultCode')
            faultString = response.get('faultString')
            if (faultCode is not None):
                raise Exception('Fault returned by GLPI service : %s : %s' % (faultCode, faultString,))
        # There is no server-side fault.
        return False

    def _build_url(self, method, params=None):
        '''
        private method to build the final url sent to the server
        @param method: the glpi method
        @param params: params to join to the request
        @return: the final url that will be sent
        '''
        _params = {"method": method, }
        if params:
            _params.update(params)
        _url = self.url + urllib.urlencode(_params)
        logging.debug(u'RESTClient::_build_url(%s, %s) '
                     u'_url = %s '
                     % (method, params, _url,))
        return _url

    def send_request(self, method, **kwargs):
        '''
        send a request to GLPI server and returns the json-decoded response

        @param method: glpi-method
        @param kwargs: extra-parameters to pass to the method
        @return: response object json-decoded
        @raise Exception: if the server raises {'faultCode', 'faultString'}
            or if something has failed.
        '''
        self.connected and logging.debug(u'RESTClient::get(%s, %s) '
                                         u'CONNECTED !'
                                         % (method, kwargs,))
        # Prepares parameters
        _params = {'method': method, }
        if kwargs:
            _params.update(kwargs)

        # Append session to parameters
        if self.session:
            _params['session'] = self.session

        url = self._build_url(method, _params)
        request = urllib2.Request(url)

        # Sends the request
        try:
            response = urllib2.urlopen(request)
            headers = response.headers
            read = response.read()
            logging.debug(u'RESTClient::send_request(%s, %s) '
                     u'response: %s /'
                     % (method, kwargs, read,))
            json_decoded = json.loads(read)
            if not RESTClient._is_fault(json_decoded):  # An exception is raised on protocol or glpi method error
                return json_decoded
        except Exception as e:
            # Exception is re-raised
            raise e

    def connect(self, login_name=None, login_password=None):
        """
        Connect to a running GLPI instance that has the webservices
        plugin enabled.

        Returns True if connection was successful.

        :type login_name: string
        :type login_password: string
        :param host: hostname of the GLPI server, has not been tested with HTTPS
        :param login_name: your GLPI username
        :param login_password: pretty obvious
        """
        self.login_name = login_name
        self.login_password = login_password

        if self.login_name != None and self.login_password != None:
            params = {'login_name':login_name,
                      'login_password': (login_password), }
            response = self.send_request("glpi.doLogin", **params)
            session_id = response.get('session')
            if session_id is not None:
                self._session = session_id
                return True
            else:
                raise Exception("Login incorrect or server down")
        else:
            logging.warning("Connected anonymously, will only be able to use non-authenticated methods")
            return self

The curry function is copied from the django project.

To access to th glpi.method, you just have to call the get_method which has been dynalicaly created on RESTClient init.

By exemple:

$ python
>>> from GLPI.GLPIClient import RESTClient
>>> rc = RESTClient(host='glpi', baseurl='/glpi_tests')
>>> rc
<GLPI.GLPIClient.RESTClient object at 0x7fa6d7aec4d0>
>>> rc.connect(login_name='<LOGIN>', login_password='<PWD>')
True
>>> rc.session
u'44h8sloakn4macki3kr0kmms42'
>> rc.connected
True
>>> rc.get_getComputer(id=100)
# Your computer !
>>> rc.listObjects(itemtype='computer', rc.get_listObjects(itemtype='computer', start=10, limit=20,)
# Your computers list !

I let you read the code and tell me what you think about it.

cordially, frague

PS : english is not my first language, so excuse for english faults...

mcphargus commented 11 years ago

At first glance this seems awesome. currying is a concept that I hadn't heard of until now. I'll check this out when I have some more time to fully understand it. Looks cool, I'll probably add it to the current codebase or set you up as a contributor, if you'd like.

I'll admit, this is the first open source project that I've maintained that has generated any interest, so I'm a little fuzzy on the best way to manage things.

And I'm sorry that I don't speak any languages other than English, I'll do my best to keep up where I can.

I'm reopening this ticket, and once this has been fully integrated into the current master branch, I'll close this issue.

frague59 commented 11 years ago

May be it will be possible to compute curryed methods from the glpi.listAllMethods method, we 'll have to check if built method exists before calling it.

This is my first real participation to an opensource project too, so I can't help you to your project managing.

I'll now look at the GLPIObject.py module, to find if there is a way to simplifiy it too.

My code now :

Cleaner version :

init.py

... Import management ...

$ cat GLPI/__init__.py
# -*- coding: utf-8 -*-
import codecs
'''
Created on 28 janv. 2013 - 13:48:56

@author: fguerin
@module GLPI.test

'''

from GLPIClient import GLPIClient
from GLPIObject import *

GLPIClient.py

... Clean up ...

$ cat GLPI/GLPIClient.py
# -*- coding:utf-8 -*-
import codecs
'''
Created on 28 janv. 2013 - 13:48:56

@author: fguerin
@module GLPI.GLPIClient

'''

import urllib, urllib2  # @UnresolvedImport
import json  # @UnresolvedImport
import logging  # @UnresolvedImport

DEBUG = False

logging.basicConfig(level=logging.DEBUG)

def curry(_curried_func, *args, **kwargs):
    def _curried(*moreargs, **morekwargs):
        return _curried_func(*(args + moreargs), **dict(kwargs, **morekwargs))
    return _curried

class GLPIClient(object):
    """
    .. note:: If any boolean arguments are defined, they're
    automatically added to the GET request, which means the
    webservices API will treat them as being true. You've been warned.
    """

    # List all methods defined in the REST API
    METHODS = dict(test='glpi.test',
                   status='glpi.status',
                   listAllMethods='glpi.listAllMethods',
                   listEntities='glpi.listEntities',
                   doLogin='glpi.doLogin',
                   listKnowBaseItems='glpi.listKnowBaseItems',
                   getKnowBaseItem='glpi.getKnowBaseItem',
                   getDocument='glpi.getDocument',
                   doLogout='glpi.doLogout',
                   getMyInfo='glpi.getMyInfo',
                   listMyProfiles='glpi.listMyProfiles',
                   setMyProfile='glpi.setMyProfile',
                   listMyEntities='glpi.listMyEntities',
                   setMyEntity='glpi.setMyEntity',
                   listDropdownValues='glpi.listDropdownValues',
                   listGroups='glpi.listGroups',
                   listHelpdeskTypes='glpi.listHelpdeskTypes',
                   listHelpdeskItems='glpi.listHelpdeskItems',
                   listTickets='glpi.listTickets',
                   listUsers='glpi.listUsers',
                   listInventoryObjects='glpi.listInventoryObjects',
                   listObjects='glpi.listObjects',
                   getObject='glpi.getObject',
                   createObjects='glpi.createObjects',
                   deleteObjects='glpi.deleteObjects',
                   updateObjects='glpi.updateObjects',
                   linkObjects='glpi.linkObjects',
                   getInfocoms='glpi.getInfocoms',
                   getContracts='glpi.getContracts',
                   getComputer='glpi.getComputer',
                   getComputerInfoComs='glpi.getComputerInfoComs',
                   getComputerContracts='glpi.getComputerContracts',
                   getNetworkports='glpi.getNetworkports',
                   listComputers='glpi.listComputers',
                   getPhones='glpi.getPhones',
                   getNetworkEquipment='glpi.getNetworkEquipment',
                   getTicket='glpi.getTicket',
                   createTicket='glpi.createTicket',
                   addTicketFollowup='glpi.addTicketFollowup',
                   addTicketDocument='glpi.addTicketDocument',
                   addTicketObserver='glpi.addTicketObserver',
                   setTicketSatisfaction='glpi.setTicketSatisfaction',
                   setTicketValidation='glpi.setTicketValidation',)

    # Attributes
    _session = None
    _url = None
    _login_name = None
    _login_password = None
    _host = None
    _baseurl = None

    @property
    def session(self):
        '''
        The session string
        '''
        return self._session

    @property
    def url(self):
        ''' The complete url '''
        # host name cleaning
        if self.host.startswith('/'):
            self._host = self._host[1:]
        if self.host.endswith('/'):
            self._host = self._host[-1:]

        # baseurl name cleaning
        if self.baseurl.startswith('/'):
            self._baseurl = self._baseurl[1:]
        if self.baseurl.endswith('/'):
            self._baseurl = self._baseurl[-1:]

        return ('http://%s/%s/plugins/webservices/rest.php?'
                % (self.host, self.baseurl,))

    @property
    def connected(self):
        '''
        tests in client is connected to the server
        '''
        return self.session is not None

    @property
    def host(self):
        return self._host

    @property
    def baseurl(self):
        return self._baseurl

    def __init__(self, host='glpi', baseurl='/glpi',):
        '''
        Initialize the GLPIClient instance, adding dynamicaly supported
        glpi-methods by currying self._glpi_method
        '''
        self._host = host
        self._baseurl = baseurl

        # Append glpi methods to self
        for method in self.METHODS.keys():
            setattr(self, method,
                    curry(self._glpi_method, self.METHODS.get(method)))

    def _glpi_method(self, method, **kwargs):
        '''
        Private method that perform the call to glpi server with the glpi method
        and providing parameters as kwargs
        @param method: GLPI method to call
        @param kwargs: parameters to send to glpi test
        '''
        response = self._send_request(method, **kwargs)
        DEBUG and logging.debug(u'GLPIClient::%s() '
                      u'response = %s'
                      % (method, response,))
        return response

    def login(self, **kwargs):
        login_name = kwargs.get('login_name') or self._login_name
        login_password = kwargs.get('login_password') or self._login_password

        decoded_json = self._glpi_method(method="glpi.doLogin",
                                         login_name=login_name,
                                         login_password=login_password)

        DEBUG and logging.debug(u'GLPIClient::login() '
                                u'decoded_json = %s'
                                % (decoded_json,))

        if self._is_fault(decoded_json):
            return False

        session_id = decoded_json.get('session')
        if session_id is not None:
            self._session = session_id
            return True
        return False

    def logout(self):
        if self.session is None:
            return True
        decoded_json = self._glpi_method(method="glpi.doLogout",)
        DEBUG and logging.debug(u'GLPIClient::login() '
                                u'decoded_json = %s'
                                % (decoded_json,))
        if self._is_fault(decoded_json):
            return False
        return True

    def connect(self, login_name=None, login_password=None):
        """
        Connect to a running GLPI instance that has the webservices
        plugin enabled.

        Returns True if connection was successful.

        :type login_name: string
        :type login_password: string
        :param host: hostname of the GLPI server, has not been tested with HTTPS
        :param login_name: your GLPI username
        :param login_password: pretty obvious
        """
        self._login_name = login_name
        self._login_password = login_password

        if self._login_name != None and self._login_password != None:
            return self.login()
        else:
            logging.warning("Connected anonymously, will only be able to use non-authenticated methods")
        return False

    # Utilities
    @staticmethod
    def _is_fault(response):
        '''
        tests if a fault has been raised by the server
        @param response: the decoded respose object
        @return: False if there is no error
        @raise Exception: if a server side error has been raised
        '''
        DEBUG and logging.debug(u'GLPIClient::_is_fault() '
                     u'response = %s'
                     % (response,))

        if (isinstance(response, (dict))):
            faultCode = response.get('faultCode')
            faultString = response.get('faultString')
            if (faultCode is not None):
                raise Exception('Fault returned by GLPI service : %s : %s' % (faultCode, faultString,))
        # There is no server-side fault.
        return False

    def _build_url(self, method, params=None):
        '''
        private method to build the final url sent to the server
        @param method: the glpi method
        @param params: params to join to the request
        @return: the final url that will be sent
        '''
        _params = {"method": method, }
        if params:
            _params.update(params)
        _url = self.url + urllib.urlencode(_params)
        DEBUG and logging.debug(u'GLPIClient::_build_url(%s, %s) '
                     u'_url = %s '
                     % (method, params, _url,))
        return _url

    def _send_request(self, method, **kwargs):
        '''
        send a request to GLPI server and returns the json-decoded response

        @param method: glpi-method
        @param kwargs: extra-parameters to pass to the method
        @return: response object json-decoded
        @raise Exception: if the server raises {'faultCode', 'faultString'}
            or if something has failed.
        '''
        DEBUG and self.connected and logging.debug(u'GLPIClient::_send_request(%s, %s) '
                                                   u'CONNECTED !'
                                                   % (method, kwargs,))
        # Prepares parameters
        _params = {'method': method, }
        if kwargs:
            _params.update(kwargs)

        # Append session to parameters
        if self.session is not None:
            _params['session'] = self.session
        url = self._build_url(method, _params)
        request = urllib2.Request(url)

        # Sends the request
        try:
            response = urllib2.urlopen(request)
            headers = response.headers
            read = response.read()
            DEBUG and logging.debug(u'GLPIClient::_send_request(%s, %s) '
                     u'response: %s /'
                     % (method, kwargs, read,))
            json_decoded = json.loads(read)
            if not GLPIClient._is_fault(json_decoded):  # An exception is raised on protocol or glpi method error
                return json_decoded
        except Exception as e:
            # Exception is re-raised
            raise e

GLPI/test.py

... some un-connected / connected tests, unittest MUST be append ...

$ cat GLPI/test.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import codecs
import logging  # @UnresolvedImport
'''
Created on 28 janv. 2013 - 13:48:56

@author: fguerin
@module GLPI.test

'''
from GLPIClient import GLPIClient
import unittest  # @UnresolvedImport

logging.basicConfig(level=logging.DEBUG,)

class GlpiTest(object):
    glpi_instance = None
    host = 'glpi'
    baseurl = 'glpi_tests'
    test_login_name = '<LOGIN>'
    test_login_password = '<PASSWD>'

    def _setUp(self):
        if not self.glpi_instance:
            self.glpi_instance = GLPIClient('glpi', 'glpi_tests')
            self.glpi_instance.connect()
            return self.glpi_instance

    def _tearDown(self):
        logging.debug(u'GlpiTest::tearDown()')
        if self.glpi_instance:
            self.glpi_instance.logout()

class GlpiTestConnected(unittest.TestCase, GlpiTest,):
    def setUp(self):
        self.glpi_instance = self._setUp()
        self.glpi_instance.login(login_name=self.test_login_name,
                                 login_password=self.test_login_password)

    def tearDown(self):
        self._tearDown()

    def test_001_test(self):
        self.assertTrue(self.glpi_instance.connected)
        test = self.glpi_instance.test()
        self.assertIsNotNone(test)
        logging.debug(u'GlpiTestConnected::test_01_test() '
                      u'test = %s \nOK'
                      % (test,))

    def test_002_status(self):

        self.assertTrue(self.glpi_instance.connected)
        test = self.glpi_instance.status()
        self.assertIsNotNone(test)
        logging.debug(u'GlpiTestConnected::test_002_get_server_status() '
                      u'test = %s \nOK'
                      % (test,))

    def test_003_listAllMethods(self):
        self.assertTrue(self.glpi_instance.connected)
        test = self.glpi_instance.listAllMethods()
        self.assertIsNotNone(test)
        logging.debug(u'GlpiTestConnected::test_003_listAllMethods() '
                      u'test = %s \nOK'
                      % (test,))

    def test_010_getComputer(self):
        self.assertTrue(self.glpi_instance.connected)

        test = self.glpi_instance.getComputer(id=374)
        self.assertIsNotNone(test)
        logging.debug(u'GlpiTestConnected::test_010_getComputer() '
                      u'test = %s \nOK'
                      % (test,))

    def test_011_getObject(self):
        self.assertTrue(self.glpi_instance.connected)
        test = self.glpi_instance.getObject(itemtype='computer',
                                            id=374,)
        self.assertIsNotNone(test)
        logging.debug(u'GlpiTestConnected::test_011_getObject() '
                      u'test = %s \nOK'
                      % (test,))

    def test_020_listComputers(self):
        self.assertTrue(self.glpi_instance.connected)
        test = self.glpi_instance.listObjects(itemtype="computer",)
        self.assertIsNotNone(test)
        logging.debug(u'GlpiTestConnected::test_011_listComputers() '
                      u'test = %s \nOK'
                      % (test,))

class GlpiTestNotConnected(unittest.TestCase, GlpiTest,):
    ''' tests sur l'intance de GLPI '''

    def setUp(self):
        self.glpi_instance = self._setUp()

    def tearDown(self):
        self._tearDown()

    def test_001_test(self):
        self.assertFalse(self.glpi_instance.connected)
        test = self.glpi_instance.test()
        self.assertIsNotNone(test)
        logging.debug(u'GlpiTestNotConnected::test_01_test() '
                      u'test = %s \nOK'
                      % (test,))

    def test_002_status(self):

        self.assertFalse(self.glpi_instance.connected)
        test = self.glpi_instance.status()
        self.assertIsNotNone(test)
        logging.debug(u'GlpiTestNotConnected::test_002_get_server_status() '
                      u'test = %s \nOK'
                      % (test,))

    def test_003_listAllMethods(self):
        self.assertFalse(self.glpi_instance.connected)
        test = self.glpi_instance.listAllMethods()
        self.assertIsNotNone(test)
        self.assertIsNotNone(test)
        logging.debug(u'GlpiTestNotConnected::test_003_listAllMethods() '
                      u'test = %s \nOK'
                      % (test,))

if __name__ == "__main__":
    unittest.main()
fsoyer commented 11 years ago

Hi guys, what about this code ? Searching for some python code to access GLPI, I found this : fine and nice ! But there is no activity since some months. @mcphargus : have you totally abandoned or is it just sleeping ? I'm not a developer, rather a technician, using python increasingly for some needs (however I've developed tons of bash scripts !), so I can help in testing code for a production use. Tell me. Thanks.

mcphargus commented 11 years ago

Not abandoned, but not a huge priority for me at the moment, since I was recently married (like last Monday recently), work full time at a startup company and am a managing partner at a really busy hacker space. Little time left for this project.

If you've any contributions or complaints, let me know or do a pull request and I'll happily check them out.

I'd imagine my job will have me back in GLPI-space heavily in the near future, but I've been spending more time in icinga (nagios) than anywhere else lately, so most of my fire has been concentrated in that direction.

fsoyer commented 11 years ago

Hi Clint, thank you for your response and first : congratulations ! Just a question here (hopping it works by email ?) then I'll post directly on Github while I test this. My problem is to know if I must use (and eventually pull requests to) your "official" code, or the code rewrited by frague59 (strangely posted as an issue, not a branch... Disappointed...).

I used the two on a CentOS5 box, python 2.4, joining a Debian test box with GLPI 0.83 :

python glpitest.py

/usr/lib/python2.4/GLPI/GLPIClient.py:61: UserWarning: Connected anonymously, will only be able to use non-authenticated methods warnings.warn("Connected anonymously, will only be able to use non-authenticated methods") {'glpi': '0.83.4', 'webservices': '1.3.1'} With your code or frague59's code, I can login and authenticate but after no commands are allowed (login return TRUE and I can print the session ID, but your code says "Not authenticated" (as issue posted by etechuganda) and frague59's code says "Command not allowed", when I try get_computer or list_tickets, for example). A code written in PHP, using xmlrpc on the same GLPI server, with the same user/password, returns correctly the informations.

Depending on your answer, I'll post this issue on etechuganda's issue or frague59's issue.

Thank's, and happiness to the newlyweds. Frank

Le 30/05/2013 02:27, Clint Grimsley a écrit :

Not abandoned, but not a huge priority for me at the moment, since I was recently married (like last Monday recently), work full time at a startup company and am a managing partner at a really busy hacker space. Little time left for this project.

If you've any contributions or complaints, let me know or do a pull request and I'll happily check them out.

I'd imagine my job will have me back in GLPI-space heavily in the near future, but I've been spending more time in icinga (nagios) than anywhere else lately, so most of my fire has been concentrated in that direction.

— Reply to this email directly or view it on GitHub https://github.com/mcphargus/python-glpi/issues/5#issuecomment-18654781.

mcphargus commented 11 years ago

@fgth feel free to open a second issue with your problem. I'll get with you more on this as I get time. I still need to bring in frague59's changes, which I'll try to do at the tail end of next month. I'd rather not keep beating on this issue with issues pertaining to another issue.