Service handler

From ISPWiki

Jump to: navigation, search

You can create a custom service handler that will allow to integrate BILLmanager with a server's control panel. Locate it in /usr/local/ispmgr/sbin/ and name as cp*. Its name should contain the "." symbol, such as cpsvr.php. The handler is started with the LongTask mechanism and accepts data from BILLmanager through the command line.

Contents

Module requirements

The handler must perform the following operations:

  • Since BILLmanager starts the module with LongTask, the handler should report its results to BILLmanager. More information can be found in the article LongTask
  • The service handler must respond to BILLmanager commands (check, getconfig, open, setparam are required parameters).

Commands to pass

BILLmanager starts the module with different parameters facilitating interaction of user and server. Depending on the actions that should be performed on the server, BILLmanager starts the module with the following parameters:

  1. check IP_ADDR USERNAME PASSWD, where check is a control command, IP_ADDR is an IP-address for integration with the control panel (IP-address for accessing API functions), USERNAME is a login that is used to gain access to the control panel, PASSWD is a user password to access a control panel.
    • The command and the IP_ADDR and USERNAME parameters are passed as the command line parameters, PASSWD should be read through a standard input stream. If you connected successfully, the module will output "ok\n" (without quotation marks), if not, the module will stop working.
  2. getconfig SRV_ID, where getconfig is a control command, SRV_ID is the unique server identifier in the database.
    • The module accepts information about available packages and pass it to BILLmanager: 'Preset <package name>'. Package information should be kept in a separate line. You may pass any information you need. This information will be shown in the configuration tab of the server properties form.
    • To pass information to BILLmanager the server.setconfig function is called; the elid, which is a server identifier, and config containing information to be passed to the server are transferred.
  3. open ITEM_ID, where open is a control command, ITEM_ID is the item unique identifier in the database</ref>.
    • Consequently, a hosting service which parameters stored in the database will be set up on the server.
    • If you wish to change the status of a newly created hosting service, in BILLmanager you need to execute the <service type>.open function with the following parameters: sok containing the 'ok' value, if the service was successfully set up, elid indicating the ITEM_ID, and other parameters, if needed.
      • For the vhost service the following parameters should be passed: the password is parameter indicating the account password, ip is any of the server IP-addresses.
      • For rhost: the password parameter is the account password.
      • For vds: password is the account password, ip is the server IP-address, and slavens is the slave name servers.
      • For rvds: password is the account password.
      • For dns: password is the account password, ip is the server IP-address.
    • More information on how to call additional BILLmanager functions, for example for changing a username, can be found in BILLmanager API.
  4. setparam ITEM_ID, where setparam is a control command, ITEM_ID is the item unique identifier in the database. Perform this operation to edit a package or its details (for example, amount of RAM or disk space, etc.).
  5. suspend ITEM_ID, where suspend is a control command, ITEM_ID is the item unique identifier in the database. Perform this operation to suspend a hosting service.
  6. resume ITEM_ID, where resume is a control command, ITEM_ID is the item unique identifier in the database. You can use this function if you wish to use the hosting service again.
    • On success, the item.setstatus function should be called where elid is the service ITEM_ID, status is set to '2'.
  7. delete ITEM_ID, where delete is a control command, ITEM_ID is the item unique identifier in the database. Perform this operation to delete a hosting service.
    • If a service was successfully removed, call the item.setstatus function, where elid is the ITEM_ID of a service, elid is a service value, status is set to '4'. Delete the IP-addresses assigned to the account from the database and make other changes, if necessary.
  8. getstats SRV_ID, where getstats is the control command, SRV_ID is the unique server identifier in the database. The scheduled command is passed to the module to get resource usage statistics per user.
    • Statistical information is added into the itemstat table of the database.
    • Upon completion, execute billserver where elid is SRV_ID and date is the date when statistics was collected.
  9. getloginurl ITEM_ID SES_LEVEL --gotoserver=SERVER_ID,, where getloginurl is the control command, ITEM_ID is the item unique identifier in the database, SES_LEVEL - user access level, SERVER_ID - is the ID of the processing server in BILLmanager. This command redirects you to the hosting management server. To this end, location='<redirect URL>' is passed to STDOUT.
  10. newip ITEM_ID DOMAIN COUNT, where newip is a control command, ITEM_ID is the item unique identifier in the database, DOMAIN is a domain name, COUNT is the number of IP-addresses to be added. Use this function to allocate IP-addresses to your client.
    • for each IP-address the item.ip.done function is called, where sok is set to 'ok', elid is the item ITEM_ID and ip is the IP-address that was added.
  11. delip ITEM_ID IP, where delip is a control command, ITEM_ID is the item unique identifier in the database, IP is an IP-address you wish to delete. This function can be used to delete a client's IP-address.
    • For a remote IP-address the item.ip.done function should be called, where delip is set to yes, elid is ITEM_IP and ip is a removed IP-address.
  12. fix, where fix is a control command. The scheduled command is passed to the module to synchronize the server and BILLmanager data.

Useful BILLmanager functions

  1. To purge the BILLmanager database table cache you can call the drop.cache function with the elid parameter indicating a table name.

BILLmanager integration

For BILLmanager integration the module may use BILLmanager API or the Mgr library. In general, to perform a function is to call the BILLmanager URL and pass the func parameter with the function value and parameters. You can specify the return type you wish to receive from the server by adding the "output" query string parameter to your API URL. If you want to receive XML, specify xml, if you wish to receive JSON, specify json.

Implementation of service handler

The following is an example implementation of the virtual hosting service handler using the Kloxo control panel that allows to perform the functions listed below:

  • Download packages
  • Set up services on the server
  • Suspend services
  • Restart services
  • Change service packages

The list of functions will be enlarged.

Module file code with comments

The heading of the module file:

  • Handler
  • libraries that can be connected
#! /usr/bin/env python
import sys, string, time, httplib2, urllib, os, logging, base64, json, MySQLdb, subprocess, random, traceback
from cookielib import debug

This function can be used to handle unknown exceptions:

def handleError(self, record):
    traceback.print_stack()
logging.Handler.handleError = handleError

Logging configuration:

  • Log file
  • Download logging configuration
  • Additional logging parameters: process id
log_file = "/usr/local/ispmgr/var/cpkloxo.log"
logging.basicConfig(filename=log_file,
                    level=logging.DEBUG,
                    format="%(asctime)s [ %(procid)-4d] %(message)s",
                    datefmt="%b %d %H:%M:%S")
d = {'procid': os.getpid()}

Logging functions:

  • debug - debugging information
  • log - general information
  • warning - warnings
  • error - errors
  • critical - critical errors
def debug(msg):
    logging.debug("\033[1;33mDEBUG %s" + msg + "\033[0m", "", extra=d)
def log(msg):
    logging.info("\033[1;32mINFO %s" + msg + "\033[0m", "", extra=d)
def warning(msg):
    logging.warning("\033[1;35mWARNING %s" + msg + "\033[0m", "", extra=d)
def error(msg):
    logging.error("\033[1;31mERROR %s" + msg + "\033[0m", "", extra=d)
def critical(msg): 
    logging.critical("\033[1;31mFATAL %s" + msg + "\033[0m", "", extra=d)

The function is used to to output usage information:

def usage():
    debug("Print usage information")
    print "Kloxo Integration module"
    print "Usage info: ..."

The function is used to send request parameters to the server and receive an association array:

def getDictResponse(url, username = "", passwd = "", params = {}):
    params.update({"login-class": "client", "login-name" : username, "login-password" : passwd, "output-type" : "json"});
    log("URL: " + str(url));
    log("Params: " + str(params));
    http_client = httplib2.Http()
    try:
        response, content = http_client.request(url + "/webcommand.php?" + urllib.urlencode(params))
    except Exception as exception:
        critical("Error while getting response: " + str(exception))
    debug("Response: \n" + str(response));
    debug("Content: \n" + str(content));
    return json.JSONDecoder().decode(content)

The function is used to connect to the MySQL server:

  • Read parameters from the configuration file
  • Connect to the server
  • Return the server connection
def sql_conn():
    try:
        sql_config = open("etc/billmgr.conf", "r")
        sql_params = {}
        for line in sql_config.readlines():
            line = string.strip(line, " \n")
            kv = line.split()
            try:
                sql_params[kv[0]] = kv[1]
            except IndexError as exception:
                debug("Config: Skip empty param")
        sql_config.close()
    except IOError as exception:
        critical("Error while reading sql config: " + str(exception))

try:

         if (!isset($sql_params["DBName"]))
              $sql_params["DBName"] = "billmgr";
        conn = MySQLdb.connect(host="localhost", user="root", passwd=sql_params["DBPassword"], db=sql_params["DBName"], unix_socket=sql_params["DBSocket"])
    except IndexError as exception:
        critical(str(exception))
    return conn.cursor()

The class that implements integration with the Kloxo control panel:

class Panel:

This method is used to redefine account limits according to BILLmanager data (not available in the current version):

    def setdetails(self, item_id):
        debug("Set item details")

The method to check Kloxo connection:

  • Send a test request
  • If a success, output OK
    def check(self, url, username, passwd = ""):
        debug("Check action")
        log("Check connection. Server address: "+url+", username: "+username)
        params = {"action": "simplelist", 
                  "resource" : "resourceplan"}
        data = getDictResponse(url, username, passwd, params)
        try:
            if data["return"] == "error":
                log("Error: " + data["message"])
                sys.stdout.write("")
            else:
                sys.stdout.write("OK\n")
        except KeyError:
            error("Can't connect to server")
            sys.stdout.write("")

The method to get server configuration:

  • Request existing packages from the server
  • Send the information to BILLmanager
    def getconfig(self, srv_id):
        debug("Getconfig action")
        cursor = sql_conn()
        cursor.execute ("select s.url, s.username, s.password "
                        "from server s "
                        "where s.id = " + srv_id)
        row = cursor.fetchone()
        params = {"action": "simplelist", 
                  "resource" : "resourceplan"}
        data = getDictResponse(row[0], row[1], row[2], params)
        try:
            if data["return"] == "error":
                log("Error: " + data["message"])
            else:
                srvconfig = "Ver 1.0\n"
                plans = data["result"].items()
                for plankey, plan in plans:
                     srvconfig = srvconfig + "Preset " + plankey + "\n"  
                subprocess.Popen(["/usr/local/ispmgr/sbin/mgrctl", 
                                  "-m", "billmgr", 
                                  "-o", "xml", 
                                  "server.setconfig", 
                                  "elid=" + srv_id, 
                                  "config=" + srvconfig]);
        except KeyError:
            error("Can't connect to server")
        cursor.close()

The method to set up a service on the server:

  • Get server parameters from the base
  • Request to the server to create a customer account
  • Call the method to create an account
    def open(self, item_id):
        debug("Open action")
        cursor = sql_conn()
        cursor.execute("select s.url, s.username, s.password, v.domain, v.username, p.internalname "
                       "from item i left join user u on i.account=u.account "
                       "left join vhost v on v.pid=i.id "
                       "left join account a on i.account=a.id "
                       "left join pricelist p on i.price=p.id "
                       "left join server s on i.server = s.id "
                       "where i.id = " + item_id)
        row = cursor.fetchone()
        size = 8;
        passwd = "".join([random.choice(string.letters + string.digits) for i in range(size)])
        params = {"action" : "add", 
                  "class" : "client", 
                  "name" : row[4], 
                  "v-password" : passwd, 
                  "v-type" : "customer", 
                  "v-plan_name" : row[5], 
                  "v-domain_name" : row[3], 
                  "v-dnstemplate_name" : "DNS Default.dnst"}
        data = getDictResponse(row[0], row[1], row[2], params)
        try:
            if data["return"] == "error":
                log("Error: " + data["message"])
            else:
                subprocess.Popen(["/usr/local/ispmgr/sbin/mgrctl", 
                                  "-m", "billmgr", 
                                  "-o", "xml", 
                                  "vhost.open", 
                                  "elid=" + item_id, 
                                  "password=" + passwd, 
                                  "sok=ok"]);
        except KeyError:
            error("Can't connect to server")
            
        self.setdetails(item_id)

The method to edit the account propertiesе:

  • Check the package
  • If the package should be changed, send the corresponding request to the server
    • If the package was successfully changed, call BILLmanager to confirm the change of the package, if not, call the item.getbackprice function to cancel the package
  • Call a method to configure the account:
    def setparam(self, item_id):
        debug("Setparam action")
        cursor = sql_conn()
        cursor.execute("select i.lastprice "
                       "from pricelist p left join item i on i.price=p.id "
                       "where i.id=" + item_id)
        row = cursor.fetchone()
        old_plan = row[0]
        cursor.execute("select s.url, s.username, s.password, v.domain, v.username, p.internalname, p.id "
                       "from item i left join user u on i.account=u.account "
                       "left join vhost v on v.pid=i.id "
                       "left join account a on i.account=a.id "
                       "left join pricelist p on i.price=p.id "
                       "left join server s on i.server = s.id "
                       "where i.id = " + item_id)
        row = cursor.fetchone()
        if str(old_plan) == "None":
            log("No need change price")
        elif old_plan != row[6]:
            log("Change price from: " + str(old_plan) + ", to: " + str(row[6]))
            params = {"action" : "update",
                      "subaction" : "change_plan", 
                      "class" : "client", 
                      "name" : row[4],
                      "v-resourceplan_name" : row[5]}
            try:
                data = getDictResponse(row[0], row[1], row[2], params)
                if data["return"] == "error":
                    error("Error: " + data["message"])
                    log("Rollback price")
                    subprocess.Popen(["/usr/local/ispmgr/sbin/mgrctl", 
                                      "-m", "billmgr", 
                                      "-o", "xml", 
                                      "item.getbackprice", 
                                      "elid=" + item_id]);
                else:
                    subprocess.Popen(["/usr/local/ispmgr/sbin/mgrctl", 
                                      "-m", "billmgr", 
                                      "-o", "xml", 
                                      "item.commitprice", 
                                      "elid=" + item_id]);
            except Exception as exception:
                error("Error: " + str(exception))
                log("Rollback price")
                subprocess.Popen(["/usr/local/ispmgr/sbin/mgrctl", 
                                  "-m", "billmgr", 
                                  "-o", "xml", 
                                  "item.getbackprice", 
                                  "elid=" + item_id]);
                    
        self.setdetails(item_id)

The method to suspend an account on the server:

  • Send a request to the server to suspend the account
  • If a success, change the order status in BILLmanager
    def suspend(self, item_id):
        debug("Suspend action")
        cursor = sql_conn()
        cursor.execute("select s.url, s.username, s.password, v.username "
                       "from item i left join vhost v on v.pid=i.id "
                       "left join server s on i.server = s.id "
                       "where i.id = " + item_id)
        row = cursor.fetchone()
        params = {"action" : "update", 
                  "class" : "client", 
                  "name" : row[3], 
                  "subaction" : "disable"}
        data = getDictResponse(row[0], row[1], row[2], params)
        try:
            if data["return"] == "error":
                log("Error: " + data["message"])
            else:
                subprocess.Popen(["/usr/local/ispmgr/sbin/mgrctl", 
                                  "-m", "billmgr", 
                                  "-o", "xml", 
                                  "item.setstatus", 
                                  "elid=" + item_id, 
                                  "status=3"]);
        except KeyError:
            error("Can't connect to server")

The method to restart the account on the server:

  • Send a request to the server to restart the account
  • If a success, change the order status in BILLmanager
    def resume(self, item_id):
        debug("Resume action")
        cursor = sql_conn()
        cursor.execute("select s.url, s.username, s.password, v.username "
                       "from item i left join vhost v on v.pid=i.id "
                       "left join server s on i.server = s.id "
                       "where i.id = " + item_id)
        row = cursor.fetchone()
        params = {"action" : "update", 
                  "class" : "client", 
                  "name" : row[3], 
                  "subaction" : "enable"}
        data = getDictResponse(row[0], row[1], row[2], params)
        try:
            if data["return"] == "error":
                log("Error: " + data["message"])
            else:
                subprocess.Popen(["/usr/local/ispmgr/sbin/mgrctl", 
                                  "-m", "billmgr", 
                                  "-o", "xml", 
                                  "item.setstatus", 
                                  "elid=" + item_id, 
                                  "status=2"]);
        except KeyError:
            error("Can't connect to server")

The method to delete a server's order:

  • Send a request to delete the account
  • If the account has been successfully deleted from the server, change the order status in BILLmanager
    def delete(self, item_id):
        debug("Delete action")
        cursor = sql_conn()
        cursor.execute("select s.url, s.username, s.password, v.username "
                       "from item i left join vhost v on v.pid=i.id "
                       "left join server s on i.server = s.id "
                       "where i.id = " + item_id)
        row = cursor.fetchone()
        params = {"action" : "delete", 
                  "class" : "client", 
                  "name" : row[3]}
        data = getDictResponse(row[0], row[1], row[2], params)
        try:
            if data["return"] == "error":
               log("Error: " + data["message"])
            else:
                subprocess.Popen(["/usr/local/ispmgr/sbin/mgrctl", 
                                  "-m", "billmgr", 
                                  "-o", "xml", 
                                  "item.setstatus", 
                                  "elid=" + item_id, 
                                  "status=4"]);
                cursor.execute("delete from itemip "
                                "where item = " + item_id)
                subprocess.Popen(["/usr/local/ispmgr/sbin/mgrctl", 
                                  "-m", "billmgr", 
                                  "-o", "xml", 
                                  "drop.cache", 
                                  "elid=itemip"]);
        except KeyError:
            error("Can't connect to server")

The method to collect traffic usage statistics per account (example is not implemented in the current version):

    def getstat(self, srv_id):
        debug("Getstat action")
        warning("unsupport action")

The method to get a URL to gain access to the account on the server:

  • Form the URL address
  • Output the URL address
    def getloginurl(self, item_id):
        debug("Getloginurl action")
        cursor = sql_conn()
        cursor.execute("select s.url "
                       "from item i left join server s on i.server = s.id "
                       "where i.id = " + item_id)
        row = cursor.fetchone()
        sys.stdout.write("location='" + row[0] + "'")

The method to add IP addresses to the account (in the current version the example is not implemented):

    def newip(self, item_id, domain, count):
        debug("Newip action")
        warning("unsupport action")

The method to delete IP addresses from the account (IP addresses are passed in the line with the ', ' separator)(in the current version the example is not implemented):

    def delip(self, item_ip, ip):
        debug("Delip action")
        warning("unsupport action")

The method to synchronize accounts on the server with their parameters in BILLmanager (in the current version the example is not implemented):

    def fix(self):
        debug("Fix action")
        warning("unsupport action")

Create the panel class example:

kloxo = Panel()
log("Start module work");

Handle the console commands:

  • Check the number of received arguments
  • Call the corresponding method
  • Output usage information, if the incorrect control command was received
try:
    if len(sys.argv) < 2:
        usage()
    elif len(sys.argv) < 3:
        if sys.argv[1] == "fix":
            kloxo.fix()
        else:
            usage()
    elif len(sys.argv) < 4:
        if sys.argv[1] == "getconfig":
            kloxo.getconfig(sys.argv[2])
        elif sys.argv[1] == "open":
            kloxo.open(sys.argv[2])
        elif sys.argv[1] == "setparam":
            kloxo.setparam(sys.argv[2])
        elif sys.argv[1] == "suspend":
            kloxo.suspend(sys.argv[2])
        elif sys.argv[1] == "resume":
            kloxo.resume(sys.argv[2])
        elif sys.argv[1] == "delete":
            kloxo.delete(sys.argv[2])
        elif sys.argv[1] == "getstat":
            kloxo.getstat(sys.argv[2])
        else:
            usage()
    elif len(sys.argv) < 5:
        if sys.argv[1] == "check":
            passwd = sys.stdin.read();
            kloxo.check(sys.argv[2], sys.argv[3], passwd)
        elif sys.argv[1] == "delip":
            kloxo.delip(sys.argv[2], sys.argv[3])
        elif sys.argv[1] == "getloginurl":
            kloxo.getloginurl(sys.argv[2])
        else:
            usage()
    elif len(sys.argv) < 6:
        if sys.argv[1] == "newip":
            kloxo.newip(sys.argv[2], sys.argv[3], sys.argv[4])
        else:
            usage()
    else:
        usage()

Handle successful completion of the module:

  • Check whether the MGR_LT_PID parameter was received in the environment variables parameters
  • If yes, inform BILLmanager that the module has successfully finished
    MGR_LT_PID = str(os.getenv("MGR_LT_PID"))
    log("MGR_LT_PID: " + MGR_LT_PID)
    if MGR_LT_PID == "None":
        log("Standalong. No finish action")
    else:
        subprocess.Popen(["/usr/local/ispmgr/sbin/mgrctl", 
                          "-m", "billmgr", 
                          "-o", "xml", 
                          "longtask.finish", 
                          "elid=" + MGR_LT_PID, 
                          "status=ok"]);  

If not:

  • Check whether the MGR_LT_PID parameter was received in the environment variables parameters
  • If yes, report to BILLmanager that the module failed.
except Exception as exception:
    critical(str(exception))   
    MGR_LT_PID = str(os.getenv("MGR_LT_PID"))
    log("MGR_LT_PID: " + MGR_LT_PID)
    if MGR_LT_PID == "None":
        log("Standalong. No finish action")
    else:
        subprocess.Popen(["/usr/local/ispmgr/sbin/mgrctl", 
                          "-m", "billmgr", 
                          "-o", "xml", 
                          "longtask.finish", 
                          "elid=" + MGR_LT_PID, 
                          "status=err", 
                          "errmsg=" + urllib.quote(str(""))]);
del kloxo
Was this helpful? Yes | No
Personal tools