# Development on Medulla

# How to extend Medulla

<div id="bkmrk-first-of-all%2C-there-">First of all, there are a few important things to note when developing around the agent:  
  
The machine agent is automatically updated when the code on the agent does not match the agent codebase on the server. To disable this behavior, the following setting must be added to agentconf.ini and the agent restarted:</div>```
[updateagent]
updating = 0
```

<div id="bkmrk-">  
</div><div id="bkmrk-the-plugins-are-also">The plugins are also automatically updated if the version has changed. If a plugin needs to be modified, do not update the version until all tests have been completed.  
  
There are four types of plugins for the machine agent: start plugins, update plugins, action plugins, and scheduled plugins.</div>1. Start plugins are those run when the agent starts and are defined in start\_machine.ini;
2. Update plugins are used to install or update external components used by the agent;
3. Action plugins are triggered by an action received by the agent;
4. Scheduled plugins are those called at a specific time or interval.

<div id="bkmrk-each-plugin-can-have">  
Each plugin can have its own configuration file named after the plugin name and must be added to the following parameter in agentconf.ini for the configuration to be loaded:</div>```
[plugin]
pluginlist = xxxxxxx, yyyyyyy
```

<div id="bkmrk-the-scheduled-plugin">  
The scheduled plugins have their own schedule defined in the plugin under the SCHEDULE parameter. However, this can be overridden in the manage_scheduler_machine.ini file  
<figure class="attachment attachment--content attachment--horizontal-rule">---

</figure>  
  
There are three ways to extend Medulla:</div>1. interaction with the machine agent via a TCP socket
2. interaction with the machine agent via named pipes
3. machine agent action plugins
4. machine agent scheduled plugins

<div id="bkmrk--1"><figure class="attachment attachment--content attachment--horizontal-rule">---

</figure></div>##### How to interact with the agent via a TCP socket

<div id="bkmrk-a-new-action-needs-t">  
A new action must be defined in**server\_kiosk.py**within the function named**handle\_client\_connection**under the condition </div>```
if 'action' in result:
```

<div id="bkmrk-and-this-action-adde">and this action added to the JSON message sent over the TCP socket.   
Here is an example of the message sent:</div>```
{
    "action": "myNewAction", 
    "sessionid": "mysessionid", 
    "base64": false, 
    "data": {
        "date": "2020-06-24T15:45:02.000Z", 
        "family1": {
            "field1": "value1", 
            "field2": "value2"
        }, 
        "family2": {
            "field1": "value1", 
            "field2": "value2"
        }
    }
}
```

<div id="bkmrk-the-above-content-is">The above content is to be saved to a file named**json\_file**to be sent via TCP socket or added to a variable named**json\_message**in your code for sending via named pipes  
  
And its counterpart in the**handle\_client\_connection**function in the**manage\_kiosk\_message**class.  
In the following section:</div>```
try:
    _result = json.loads(minifyjsonstringrecv(msg))
```

<div id="bkmrk-add">Add</div>```
if _result['action'] == "myNewAction":
    substitute_recv = self.objectxmpp.sub_monitoring
    logging.getLogger().warning("send to %s to %s" % (_result, substitute_recv))
    self.objectxmpp.send_message(mbody=json.dumps(_result),
                                 mto=substitute_recv,
                                 mtype='chat')
    return
```

<div id="bkmrk-and-in-the-following">And in the following loop:</div>```
if 'action' in result:
    if result['action'] == "kioskinterface":
...
```

<div id="bkmrk-add-1">Add</div>```
elif result['action'] == "myNewAction":
    datasend['action'] = "myNewSubstituteAction"
    subs_recv = self.objectxmpp.sub_monitoring
    datasend['sessionid'] = getRandomName(6, "mynewsubstituteaction")
    datasend['data'] = result['data']
```

<div id="bkmrk-the-above-example-wi">  
The above example will in turn send the message to the sub_monitoring JID with a new action to be carried out: myNewSubstituteAction  
  
The listening TCP port is defined in**the agentconf.ini parameterkiosk/am\_local\_port**. The default value is 8765.  
  
Here is an example TCP sender written in Python for the next step, which is the actual sending of the data to the TCP socket: </div>```
#!/usr/bin/env python
# -*- coding: utf-8; -*-
#
# (c) 2023 siveo, http://www.siveo.net
#
# This file is part of Medulla, http://www.siveo.net
#
# Pulse 2 is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# Pulse 2 is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Medulla; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
# MA 02110-1301, USA.
# file clientTCPcli.py

# Run python clientTCPcli.py  -p ./file.json on the client machine to 
# inject the data

from optparse import OptionParser

import socket
import sys
import os
import select
def send_message(message, host, port, timeout_in_seconds = 5):

    # Create a TCP/IP socket
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    # Connect the socket to the port where the server is listening


    try:

        server_address = (host, port)
        print >>sys.stderr, 'connecting to %s port %s' % server_address
        sock.connect(server_address)
    except socket.error, msgerror:
        print 'Bind failed on command interface ' + host + ' port ' + str(port) + ' Error Code : ' + str(msgerror[0]) + ' Message ' + msgerror[1] + '\n'
        sys.exit(str(msgerror[0]))
    try:
        # Send data
        print >>sys.stderr, 'sending "%s"' % message
        sock.sendall(message)

        # Look for the response
        amount_received = 0
        amount_expected = len(message)
        ready = select.select([sock], [], [], timeout_in_seconds)
        data = ""
        if ready[0]:
            data = sock.recv(4096)
            return 0, data
        return -1,""
    finally:
        print >>sys.stderr, 'closing socket'
        sock.close()


if __name__ == '__main__':
    optp = OptionParser()
    #optp.add_option("-h", "--help", action="store_true",
                        #dest="help", default=False,
                                #help="host")

    optp.add_option("-H", "--host", action="store_true",
                        dest="host", default="localhost",
                                help="host")

    optp.add_option("-P", "--port",
                        dest="port", default=8765,
                        help="connection port")

    optp.add_option("-T", "--timeout",
                        dest="timeout_in_seconds", default=2,
                            help="Receive timeout in seconds")

    optp.add_option("-m", "--msg",
                        dest="msg", default = "",
                            help="message to send to TCP server")

    optp.add_option("-p", "--pathfile",
                        dest="pathfile", default = None,
                            help="content file to send to TCP server")

    opts, args = optp.parse_args()
    #if opts.help:
        #print "command usage"
        #os._exit(0)
    message = ""
    if opts.pathfile is None:
        if opts.msg == "":
            print "option missing < -m | -p> "
            sys.exit(-1)
        message = opts.msg
    else:
       if os.path.exists(opts.pathfile):
            with open(opts.pathfile, 'r') as f:
                message =  f.read()

    if message !="":
        code, msg = send_message(message, opts.host, opts.port, opts.timeout_in_seconds)
    print "error code %s, server response %s"%(code,msg)
```

<div id="bkmrk-to-send-the-message%3A">  
To send the message: </div>```
python clientTCPcli.py -p <json_file>
```

<div id="bkmrk--2">  
<figure class="attachment attachment--content attachment--horizontal-rule">---

</figure></div>##### How to interact with the agent via named pipes

<div id="bkmrk-the-way-to-interact-">  
Interacting with the agent via named pipes is done the same way as interacting with the agent via a TCP socket, except for sending the message.  
  
To send the message to the named pipe, here is an example code written in Python:</div>```
import win32file

def send_message(json_message):
    fileHandle = win32file.CreateFile("\\\\.\\pipe\\interfacechang",
                                      win32file.GENERIC_READ | win32file.GENERIC_WRITE,
                                      0,
                                      None,
                                      win32file.OPEN_EXISTING,
                                      0,
                                      None)
    win32file.WriteFile(fileHandle, json_message)
    win32file.CloseHandle(fileHandle)
```

<div id="bkmrk--3">  
<figure class="attachment attachment--content attachment--horizontal-rule">---

</figure></div>##### How to write action plugins for the agent

<div id="bkmrk-below-is-a-template-">  
Below is a template that can be used for writing action plugins:</div>```
import logging
import json

plugin = {"VERSION": "1.0", "NAME": "mynewaction",  "TYPE": "machine"}

logger = logging.getLogger()

def action( objectxmpp, action, sessionid, data, message, dataerreur):
    logger.debug("###################################################")
    logger.debug("call %s from %s session id %s" % (plugin, message['from'], sessionid))
    logger.debug("###################################################")
    datasend = {"action" : "myNewSubstituteAction",
                "data" : data,
                "sessionid": sessionid,
                "ret": 0,
                "base64": False
                }
    objectxmpp.send_message(mto=objectxmpp.sub_monitoring,
                            mbody=json.dumps(datasend),
                            mtype='chat')
```

<div id="bkmrk-please-note-the-foll">  
Please note the following: </div>- NAME must match the name of the plugin file. Here the file will be named plugin\_mynewaction.py
- TYPE must be set to machine, relayserver, or all, depending on its target
- The action function will be the code executed by default. The example above will send the message to the sub\_monitoring JID with a new action to be performed: myNewSubstituteAction

<div id="bkmrk--4"><figure class="attachment attachment--content attachment--horizontal-rule">---

</figure></div>##### How to write scheduled plugins for the agent

<div id="bkmrk-below-is-a-template--1">  
Below is a template that can be used for writing scheduled plugins:</div>```
import logging
import json
import os
import ConfigParser
from pulse_xmpp_agent.lib.agentconffile import directoryconffile
from pulse_xmpp_agent.lib.utils import file_put_contents

plugin = {"VERSION": "1.0", "NAME": "scheduling_mynewscheduledaction", "TYPE": "machine", "SCHEDULED": True}

SCHEDULE = {"schedule": "*/15 * * * *", "nb": -1}

logger = logging.getLogger()

def schedule_main(xmppobject):
    logger.debug("===================================================")
    logger.debug(plugin)
    logger.debug("===================================================")
    if xmppobject.num_call_scheduling_mynewscheduledaction == 0:
        __read_conf(xmppobject)
        
    if xmppobject.config.mynewscheduledaction_enable:
        data = {}
        data['family1'] = {}
        data['family1']['field1'] = "value1"
        data['family1']['field2'] = "value2"
        data['family2'] = {}
        data['family2']['field1'] = "value1"
        data['family2']['field2'] = "value2"
    
    if xmppobject.config.mynewscheduledaction_forward:
        datasend = {"action" : "myNewSubstituteAction",
                    "data": data,
                    "sessionid": "mysessionid",
                    "base64": False
                    }
        objectxmpp.send_message(mto=objectxmpp.sub_monitoring,
                                mbody=json.dumps(datasend),
                                mtype='chat')

def __read_conf(xmppobject):
    """
        Read the plugin configuration
    """
    configfilename = os.path.join(directoryconffile(), "%s.ini" % plugin['NAME'])
    logger.debug("Reading configuration in file %s" % configfilename)
    
    #default parameters
    xmppobject.config.mynewscheduledaction_enable = True
    xmppobject.config.mynewscheduledaction_forward = False
    
    if not os.path.isfile(configfilename):
        logger.warning("Plugin %s configuration file %s missing" % (plugin['NAME'], configfilename))
        logger.warning("The missing configuration file will be created automatically.")
        file_put_contents(configfilename,
                          "[mynewscheduledaction]\n" \
                          "enable = 1\n" \
                          "forward = 0\n")
                            
    # Load configuration from file
    Config = ConfigParser.ConfigParser()
    Config.read(configfilename)
    if os.path.exists(configfilename + ".local"):
        Config.read(configfilename + ".local")
    if Config.has_section("mynewscheduledaction"):
        if Config.has_option("mynewscheduledaction", "enable"):
            xmppobject.config.mynewscheduledaction_enable = Config.getboolean('mynewscheduledaction','enable')
        if Config.has_option("mynewscheduledaction", "forward"):
            xmppobject.config.mynewscheduledaction_forward = Config.getboolean('mynewscheduledaction','forward')
```

<div id="bkmrk-please-note-the-foll-1">Please note the following: </div>- NAME must match the name of the plugin file. Here the file will be named plugin\_mynewaction.py
- TYPE must be set to machine, relayserver, or all, depending on its target
- SCHEDULE notation is similar to cron notation. The additional parameter nb defines how many times the plugin must run. If set to -1, it will run indefinitely
- The schedule\_main function will be the code executed by default. The example above will read a configuration file or create it if it does not exist, and send a message to the sub\_monitoring JID with a new action to be performed: myNewSubstituteAction

# How to use the Medulla XML-RPC API

<section class="push_double--top" id="bkmrk-this-will-guide-the-"><div class="formatted_content formatted_content--large"><div>This guide will help users utilize Medulla's XML-RPC API: [Medulla's XML-RPC API.php](https://docs.medulla-tech.io/books/development-on-medulla/page/medullas-xml-rpc-apiphp "Medulla's XML-RPC API.php")  
  
</div></div>##### 1: Configuration

<div class="formatted_content formatted_content--large"><div>Check the following:</div>- SSL Certificate: Download the certificate from the Medulla server:[http://&lt;medulla\_server&gt;/downloads/medulla-ca-chain.cert.pem](/downloads/medulla-ca-chain.cert.pem)
- Install the certificate on the server that will be querying the API 
    - For Debian-based systems, copy the file to*/usr/local/share/ca-certificates*and change its extension to crt, then import the certificate by running the*update-ca-certificates*command
    - For RedHat-based systems, copy the file to*/etc/pki/ca-trust/source/anchors*and change its extension to crt, then import the certificate by running the*update-ca-trust extract*command
- If necessary, add the IP address and hostname of the XML-RPC server to /etc/hosts

<div>  
</div></div>##### 2: Using the API

<div class="formatted_content formatted_content--large"><div>First authenticate the user and retrieve the session cookie:</div></div>```
/**
 * Executes an XML-RPC request.
 *
 * @param string $method The name of the XML-RPC method to call.
 * @param array $params The parameters to pass to the method.
 * @param bool $includeCookie Indicates whether the session cookie should be included in the request.
 * @return array An array containing the HTTP headers and the body of the response.
 * @throws <em>Exception</em> If an error occurs while connecting or sending the request.
 */
function executeRequest($method, $params, $includeCookie = false) {
    $agentInfo = $_SESSION["XMLRPC_agent"];
    $requestXml = xmlrpc_encode_request($method, $params, ['output_type' => 'php', 'verbosity' => 'pretty', 'encoding' => 'UTF-8']);

    // Set the HTTP headers
    $url = "/";
    $httpQuery  = "POST " . $url . " HTTP/1.0\r\n";
    $httpQuery .= "User-Agent: MMC web interface\r\n";
    $httpQuery .= "Host: " . $agentInfo["host"] . ":" . $agentInfo["port"] . "\r\n";
    $httpQuery .= "Content-Type: text/xml\r\n";
    $httpQuery .= "Content-Length: " . strlen($requestXml) . "\r\n";
    // Add the cookie if necessary
    if ($includeCookie) {
        $httpQuery .= "Cookie: " . $_SESSION['cookie'] . "\r\n";
    }
    $httpQuery .= "Authorization: Basic " . base64_encode($agentInfo["login"] . ":" . $agentInfo["password"]) . "\r\n\r\n";
    $httpQuery .= $requestXml;

    // Configure the SSL context
    // 'allow_self_signed' is set to false to accept only certificates signed by a recognized certificate authority
    // 'verify_peer' is set to true to verify the server's SSL certificate
    $context = stream_context_create();
    $proto = $agentInfo["scheme"] == "https" ? "ssl://" : "";
    if ($proto) {
        stream_context_set_option($context, "ssl", "allow_self_signed", false);
        stream_context_set_option($context, "ssl", "verify_peer", true);
    }

    // Open the connection to the server
    $socket = stream_socket_client($proto . $agentInfo["host"] . ":" . $agentInfo["port"], $errno, $errstr, 30, STREAM_CLIENT_CONNECT, $context);
    if (!$socket) {
        throw new <em>Exception</em>("Unable to connect to XML-RPC server: $errstr ($errno)");
    }

    // Set a 60-second timeout for reading
    stream_set_timeout($socket, 60);

    if (!fwrite($socket, $httpQuery)) {
        throw new <em>Exception</em>("Unable to send data to XML-RPC server");
    }

    $responseXml = '';
    while (!feof($socket)) {
        $ret = fgets($socket, 128);
        $responseXml .= $ret;
    }
    fclose($socket);

    // Separate the HTTP headers from the response body
    list($headers, $body) = explode("\r\n\r\n", $responseXml, 2);
    return [$headers, $body];
}

/**
 * Authenticates the user and retrieves the session cookie.
 * @param string $method The name of the XML-RPC method to call.
 * @param array $params The parameters to pass to the method.
 * @return string The session cookie.
 * @throws <em>Exception</em> If an error occurs during authentication.
 */
function authenticateAndGetCookie($method, $params) {
    list($headers, $body) = executeRequest($method, $params);

    // Parse the HTTP headers to extract the session cookie
    $headers_array = array();
    $header_lines = explode("\r\n", $headers);
    foreach ($header_lines as $header) {
        $parts = explode(': ', $header, 2);
        if (count($parts) == 2) {
            $headers_array[$parts[0]] = $parts[1];
        }
    }

    // Using the parsed headers
    if (isset($headers_array['Set-Cookie'])) {
        $cookie = $headers_array['Set-Cookie'];
        $_SESSION['cookie'] = $cookie;
    } else {
        throw new <em>Exception</em>('Authentication failed, no cookie received');
    }

    return $cookie;
}
```

<div class="formatted_content formatted_content--large"><div>  
</div></div>##### 3: Examples of requests

<div class="formatted_content formatted_content--large">- Get the list of all packages

</div>```
/**
 * Sends an XML-RPC request and returns the response only if authenticated by cookie.
 * @param string $method The name of the XML-RPC method to call.
 * @param array $params The parameters to pass to the method.
 * @return array The XML-RPC response.
 * @throws <em>Exception</em> If an error occurs while sending the request.
 */
function sendXmlRpcRequest($method, $params) {
    list($headers, $body) = executeRequest($method, $params, true);
    $responseXml = substr($body, strpos($body, '<?xml'));
    $response = xmlrpc_decode($responseXml, 'UTF-8');
    if (is_array($response) && xmlrpc_is_fault($response)) {
        throw new <em>Exception</em>("XML-RPC fault: {$response['faultString']} ({$response['faultCode']})");
    }
    return $response;
}

    $method = "pkgs.get_all_packages";
    $params = [
        'root', // login
        false,  // sharing_activated
        0,      // start
        10,      // end
        [
            'filter' => 'hostname', // example name of package
        ] // ctx
    ];

    try {
        $responseXml = sendXmlRpcRequest($method, $params);

    } catch (<em>Exception</em> $e) {
        echo "Error: " . $e->getMessage();
    }
```

<div class="formatted_content formatted_content--large"><div>*Tips: To get the package UUID: $responseXml\['datas'\]\['uuid'\]\[$key\]*   
</div>- Get the list of all machines

</div>```
$method = "xmppmaster.get_machines_list";
$params = [
     0, // start
     20, // end
     [
        'filter' => '',
        'field' => 'allchamp',
        'computerpresence' => 'presence', // Specify whether to include machines that are present
        // 'computerpresence' => 'no_presence', // Specify whether to include non-present machines
        'location' => "UUID0", // glpi_id - entity
    ] // ctx
];

try {
    $responseXml = sendXmlRpcRequest($method, $params);
} catch (<em>Exception</em> $e) {
    echo "Error: " . $e->getMessage();
}
```

<div class="formatted_content formatted_content--large"><div>  
</div>- Get a machine's details

</div>```
$method = "glpi.getLastMachineInventoryPart";
$params = [
     $uuid,
     'Summary',
      0, // minbound
      0, // maxbound
      "", // filter
      [
           "hide_win_updates" => false,
           "history_delta" => false
       ], // options
];

try {
    $responseXml = sendXmlRpcRequest($method, $params);
} catch (<em>Exception</em> $e) {
    echo "Error: " . $e->getMessage();
}
```

<div class="formatted_content formatted_content--large"><div>*Tips: To get the UUID of the machine: $responseXml\['data'\]\['uuid\_inventorymachine'\]\[$key\]. This also corresponds to the GLPI Machine ID prefixed by the UUID*   
</div>- Deploy a specific package on a target machine

</div>```
$method = "msc.add_command_api";
$pid = "96982fce-hostname_vq918j7wtnwzm610by"; // pid - package_id
$target = "UUID1"; // target - machine_uuid

$params = [
    $pid,  // pid - package_id
    $target,  // target - machine_uuid
    array(  // params
    "name" => "devdemo-win-1",
    "hostname" => "devdemo-win-1",
    "uuid" => $target,
    "gid" => NULL,
    "from" => "base|computers|msctabs|tablogs",
    "pid" => $pid,
    // "title" => "DEPLOYMENT TITLE",
    "create_directory" => "on",
    "start_script" => "on",
    "clean_on_success" => "on",
    "do_reboot" => "",
    "do_wol" => "",
    "do_inventory" => "on",
    "next_connection_delay" => "60",
    "max_connection_attempt" => "3",
    "maxbw" => "0",
    "deployment_intervals" => "",
    "tab" => "tablaunch",
    "issue_halt_to" => array(),
    ),
    "push",  // mode
    NULL,  // gid
    array(),  // proxy 
    0  // cmd_type
];

    try {
        $responseXml = sendXmlRpcRequest($method, $params);

        $commandId = $responseXml; // Retrieve the command ID

        $method2 = "xmppmaster.addlogincommand";
        $params2 = [
            'root', // login
            $commandId, // commandid
            '', // grpid
            '', // nb_machine_in_grp
            '', // instructions_nb_machine_for_exec
            '', // instructions_datetime_for_exec
            '', // parameterspackage
            0, // rebootrequired
            0, // shutdownrequired
            0, // bandwidth
            0, // syncthing
            [] // params
        ];

        try {
    $responseXml2 = sendXmlRpcRequest($method2, $params2);

    } catch (<em>Exception</em> $e) {
        echo "Error: " . $e->getMessage();
    }

    } catch (<em>Exception</em> $e) {
        echo "Error: " . $e->getMessage();
}
```

<div class="formatted_content formatted_content--large"><div>  
</div>- Get deployment logs (Audit) from sessionname

</div>```
$method = "xmppmaster.getlinelogssession";
$params = [
    "command63bb5ee8fc834eae89" // sessionname
];

try {
    $responseXml = sendXmlRpcRequest($method, $params);
} catch (<em>Exception</em> $e) {
    echo "Error: " . $e->getMessage();
}
```

<div class="formatted_content formatted_content--large"><div>*Tips: To get the deployment session name: $responseXml\['tabdeploy'\]*['sessionid'][$key]  
  
</div>- Get all deployment logs by User and/or target

</div>```
$method = "xmppmaster.get_deploy_by_user_with_interval";
$params = [
    "root",       // login_user
    "",           // state_deploy
    86400,        // intervalsearch
    0,            // start_pagination
    "20",         // end_pagination
    "spo-win-1",  // filt - hostname_machine
    "command"     // typedeploy
];

try {
    $responseXml = sendXmlRpcRequest($method, $params);
} catch (<em>Exception</em> $e) {
    echo "Error: " . $e->getMessage();
}
```

</section><div class="push--top u-hide-on-template" id="bkmrk-new-boost"><div class="boosts boosts--small metadata push--bottom boosts--empty"><div class="boost-form__link-container">[<span class="a-for-screen-reader">New boost</span>](https://3.basecamp.com/4759565/buckets/18027007/boosts/new?boost%5Bboostable_gid%5D=Z2lkOi8vYmMzL1JlY29yZGluZy84NDYzMTQ5NjAx)</div></div></div>