Skip to end of metadata
Go to start of metadata

In this tutorial we will be writing a “new hire” script to add an extension to your FreePBX using the new GraphQL API. Our script will be running on a separate client machine where other new hire processes are carried out also, although you can run it from the PBX itself if you prefer. For the client side we’ll use an off-the-shelf CentOS VM, and we’ll be writing the script using Python (it looks much better on your resume than Perl)

References

Blog post “GraphQL support in FreePBX 15” by Jared Smith : https://www.freepbx.org/graphql-support-in-freepbx-15/

GraphQL Documentation for FreePBX: GraphQL PBX APIs Documentation

GQL Library for Python: https://github.com/graphql-python/gql

Prerequisites

PBX Side: FreePBX 15 with PBX API version 15.0.3.11 or later

Client Side : CentOS 7.9

Curl 7.29.0 (“yum install -y curl” if you don’t have it)

Python 3.6.8 (“yum install -y python3 python3-pip python3-devel” if you don’t have it)

GQL Library for Python (“pip3 install --pre gql[all]”)

PBX-side basics

To start on the PBX side, go to Connectivity -> API

We need to know the “scope” for the APIs we want to use, so go to the “Scope Visualizer” tab. The APIs we want for querying and modifying extensions are under “Core”, so click on “Read/Write for Core” and the relevant scope “gql:core” will appear in the box above the tree. Note that the tick marks here don’t change any settings, they are just generating the text “gql:core” which you can reference elsewhere.

The next thing we can do is test out the fetchAllExtensions API using the “GraphQL Explorer” (let’s you try the API without worrying about authentication). Put “gql:core” in the “Paste Scopes Here” box, and hit “Reload Explorer”, then enter this into the left-hand box under “GraphiQL”:

query {
  fetchAllExtensions {
    status
    message
    totalCount
    extension {
      extensionId
    }
  }
}

And press the “Execute Query” button , you should get a response like this listing your extensions:

Here’s the text from my PBX (when there were only two extensions):

{
  "data": {
    "fetchAllExtensions": {
      "status": true,
      "message": "Extension's found successfully",
      "totalCount": 2,
      "extension": [
        {
          "extensionId": "4001"
        },
        {
          "extensionId": "7006"
        }
      ]
    }
  }
}

GraphQL explanation

Hopefully that all went well. Let’s take a step back and understand how that query was constructed:

The documentation for fetchAllExtensions can be found here:  Core Module GraphQL APIs - FetchallExtensions Details 

Under “API request” you can see some 16 parameters (most of which can occur multiple times for this API). But GraphQL lets you request only the data you’re interested in, so the query above only asks for 4 of those.  That’s because we’re just trying to get a list of existing extension IDs, to avoid a conflict when we create a new extension, the other parameters aren’t useful in this context. The query above was created by copying the full API request from the documentation and removing the parts that we don’t need.

For other purposes (for example running a weekly report listing all your extensions) you would include a lot more parameters in your request, perhaps all the parameters from the documentation would be appropriate for a detailed report.

Authentication

Now it’s time to do a real query from the GraphQL client instead of the FreePBX GUI, so we need to use authentication

We need to create an application which we will use for both of the APIs we plan to use (listing all extensions and adding a new extension). On the Applications tab, choose “Add Application” and choose “Machine-to-Machine app” from the drop-down. You can learn more about application types here: https://wiki.freepbx.org/display/FPG/API+Applications  :

Fill in the info about your application here. In order to do the full script we will need APIs from both the core and framework scopes, so our scope ends up being “gql:core gql:framework”, that’s what you get when you check both the Core and FreePBX Framework boxes in the Scope Visualizer:

Click “Add Application” and get the application details, make sure you copy Client Secret before hitting “Close”:

You can get all this info (except the secret) from the “eye” icon in the list of applications:

If you didn’t save the secret, you can hit the “Regenerate Credentials” button, which generates a new ID and secret while invalidating the old one.

Manual API access from client

I think it’s useful to use command line tools for the next “sanity check” step, it lets you see the exact URLs used and the actual format of the responses. But I also use “dir” to look at files on Windows, and that’s not everyone’s cup of tea. For those who would rather not deal with 1000-character long commands and hard-to-read output, you may prefer a GUI approach instead. The open source Insomnia client lets you do the same sanity check below with prettier formatting, less typing, and a lot of other features, like examining the API schema from inside the GUI. Feel free to skip the rest of this section and do the same query with Insomnia instead, it proves the same thing.

For those who like the command line, let’s test out authentication with curl. For “Client Credentials” flow (used for Machine-to-Machine authentication) we need “Token URL”, “ID”, and “Client Secret”. The curl command looks like this:

curl -X POST -u "<ID>:<Secret>" -d "grant_type=client_credentials&scope=<scope>" <Token URL>

You should get back a token like this:

Command:

curl -X POST -u "03cc2e2f0e77d289730b316144f1bb275fe7e840c42e57d5cd02e08f42f57595:fd84b25596588296b12df985ce1d3a96" -d "grant_type=client_credentials&scope=gql:core+gql:framework" https://2XXXXXXX.deployments.pbxact.com/admin/api/api/token

Response:

{"token_type":"Bearer","expires_in":3600,"access_token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImp0aSI6IjdlYjgxNjQ5YjI2YzQyZWQ3ZTE1ZGQ0NmEzMmY0YTllOGRlY2Y5ZDk3ZTZhOWUwYTc5MWQ5MjU1MjBmM2Q1ZDMyMDZiMWY2OTc5NTNmNzUzIn0.eyJhdWQiOiI0MGYyYTdkNzRjZjc4
OWM2MmM0NmQ1ZWQ3M2RhMDY3ZWFhOGMzMzg0NDliNzIzOTEzYmViOTllMzBiNmI0ZDE4IiwianRpIjoiN2ViODE2NDliMjZjNDJlZDdlMTVkZDQ2YTMyZjRhOWU4ZGVjZjlkOTdlNmE5ZTBhNzkxZDkyNTUyMGYzZDVkMzIwNmIxZjY5Nzk1M2Y3NTMiLCJpYXQiOjE2MTA3MDM3OTEsIm5iZiI6MTYxMDcwMzc5MSwiZXhwI
joxNjEwNzA3MzkxLCJzdWIiOiIiLCJzY29wZXMiOlsiZ3FsOmNvcmUiXX0.jr_V2AEvHXj6n1Gs640e9UWV4s3tC8vx939zZWzz4AZxiPDNX7Clnd8qm1BbUJDBG2xpS0KrmrdMKkSu9tSj8uIK7-QCAb_T9fxiiMkw91d7iTm1krMYWpZjw83RgltiDy8EPONxDmdWebSNZKTLYDk2JzZ7-Ma-ldsF-o5ezxb3MMtpSK2RLS
feodGEIwVDcieoRn1kthHj7SHDaPf49LBuVmihj4RvmK_rQUi_Q-ebaUwFb_aBwyrpzDRqgMBuIag4SEXUai0FkzyyQy9q5QOEpTCYd15mhzwDX5TJXe22yKitiB7VdaJnUvXK3eQB36GSrtWB0I7YioZ0CF5fdA"}


That 826-character string is an access token, and we’ve got an hour to use it. Oauth2 has the concept of refreshing a token but we’ll just make sure to use it within the hour. Of course you can generate a new one if this one expires, just repeat the exact same command.

You can see the newly-granted token on the “Access Tokens” tab:

Note that the abbreviated version of the token seen here can’t be used for curl or script access, we need to use the super-sized access_token above for further access.

If the above curl command gets a timeout, make sure your PBX firewall trusts your client machine.

If the above curl command produces errors about failing to validate a certificate, you need to get HTTPS properly set up for the FreePBX GUI first. See https://wiki.freepbx.org/display/FPG/Certificate+Management+User+Guide and https://wiki.freepbx.org/display/FPG/System+Admin+-+HTTPS+Setup . Another choice would be to use HTTP instead, but username and password are sent in the clear using HTTP, so this is strongly discouraged and should only be used during testing or on a trusted network.

The next part of our sanity check wins the prize for today’s longest and hardest to parse command line (also most curly braces), but should show the actual gql:core API working from our client machine :

Now the format is :

curl -X POST -H "Content-Type: application/json" -H "Authorization: Bearer <access_token>" -d '{"query":"<GraphQL Query>"}' <GraphQL URL>

Where:

<access_token> : from “access_token” returned by previous curl command

<GraphQL Query> : We’re going to query all extensions with "query { fetchAllExtensions { status message totalCount extension { extensionId } } }"

<GraphQL URL> : from “GraphQL URL” field when we created the application (should always be “https://<pbxfqdn>:/admin/api/api/gql”)

Command:

curl -X POST -H "Content-Type: application/json" \
-H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImp0aSI6IjdlYjgxNjQ5YjI2YzQyZWQ3ZTE1ZGQ0NmEzMmY0YTllOGRlY2Y5ZDk3ZTZhOWUwYTc5MWQ5MjU1MjBmM2Q1ZDMyMDZiMWY2OTc5NTNmNzUzIn0.eyJhdWQiOiI0MGYyYTdkNzRjZjc4OWM2MmM0NmQ1ZWQ3M2RhMDY3ZWFhOGMzMzg0NDliNzIzOTEzYmViOTllMzBiNmI0ZDE4IiwianRpIjoiN2ViODE2NDliMjZjNDJlZDdlMTVkZDQ2YTMyZjRhOWU4ZGVjZjlkOTdlNmE5ZTBhNzkxZDkyNTUyMGYzZDVkMzIwNmIxZjY5Nzk1M2Y3NTMiLCJpYXQiOjE2MTA3MDM3OTEsIm5iZiI
6MTYxMDcwMzc5MSwiZXhwIjoxNjEwNzA3MzkxLCJzdWIiOiIiLCJzY29wZXMiOlsiZ3FsOmNvcmUiXX0.jr_V2AEvHXj6n1Gs640e9UWV4s3tC8vx939zZWzz4AZxiPDNX7Clnd8qm1BbUJDBG2xpS0KrmrdMKkSu9tSj8uIK7-QCAb_T9fxiiMkw91d7iTm1krMYWpZjw83RgltiDy8EPONxDmdWebSNZKTLYDk2JzZ7-Ma-ldsF-o5ezxb3MMtpSK2RLSfeodGEIwVDcieoRn1kthHj7SHDaPf49LBuVmihj4RvmK_rQUi_Q-ebaUwFb_aBwyrpzDRqgMBuIag4SEXUai0FkzyyQy9q5QOEpTCYd15mhzwDX5TJXe22yKitiB7VdaJnUvXK3eQB36GSrtWB0I7YioZ0CF5fdA" \
-d '{"query":"query { fetchAllExtensions { status message totalCount extension { extensionId } } }"}' https://2xxxxxxx.deployments.pbxact.com/admin/api/api/gql

Response:

{"data":{"fetchAllExtensions":{"status":true,"message":"Extension's found successfully","totalCount":2,"extension":[{"extensionId":"4001"},{"extensionId":"7006"}]}}}

Scripting it (wasn’t this tutorial supposed to be about scripts?)

Querying for existing extensions

With authentication and a query working in curl (or Insomnia if you prefer), we will now do the exact same two operations in Python 3. Don’t forget the prerequisites at the top of this tutorial, and of course you will need to modify the parameters for your environment. First we use the Python Requests library to do the Oauth2 authentication and get back a token, then we use the GQL library to make the GQL query and parse the returned data which has already been converted into a Python dictionary:

from gql import gql, Client
from gql.transport.aiohttp import AIOHTTPTransport
import requests, sys

TOKEN_URI='https://2xxxxxxx.deployments.pbxact.com/admin/api/api/token'
API_URI='https://2xxxxxxx.deployments.pbxact.com/admin/api/api/gql'
AUTH_ID='03cc2e2f0e77d289730b316144f1bb275fe7e840c42e57d5cd02e08f42f57595'
AUTH_SECRET='fd84b25596588296b12df985ce1d3a96'
GQL_SCOPE='gql:core gql:framework'
GQL_QUERY_FETCHALL='query { fetchAllExtensions { status message totalCount extension { extensionId } } }'

#First authenticate
print('Requesting authentication token...');
token_request_data={'grant_type':'client_credentials','scope':GQL_SCOPE}
r = requests.post(TOKEN_URI, data=token_request_data, auth=(AUTH_ID, AUTH_SECRET))
if 'access_token' not in r.json():
    sys.exit('Failed to get authentication token. Exiting.')

#Now on to GraphQL
print('Querying PBX for existing extension list...');
reqHeaders = { 'Authorization': 'Bearer ' + r.json()['access_token'] }
transport = AIOHTTPTransport(url=API_URI, headers=reqHeaders)
client = Client(transport=transport, fetch_schema_from_transport=False)
result = client.execute(gql(GQL_QUERY_FETCHALL))

if not result['fetchAllExtensions']['status'] or result['fetchAllExtensions']['totalCount'] < 1:
    sys.exit('Failed to get any extension info, exiting...')
print('Existing extensions:', end='')
for ext in result['fetchAllExtensions']['extension'] :
    print(ext['extensionId'], end=' ')
print()

Here’s what it looks like in action:

$ python3 addextension.py
Requesting authentication token...
Querying PBX for existing extension list...
Existing extensions:4001 7006

Adding new extension

Now on to actually adding an extension, the first part of the script is pretty much unchanged from above, but now there’s some code to figure out the next available extension, some code to parse the command line for the user’s name, and e-mail, and finally to do two GraphQL “mutations” to actually add the extension and then apply the configuration. 

Here’s the GraphQL mutation which we use to create the extension, note that outboundCid and vmPassword are generated by the script based on the new extension, while name and email are passed in as arguments:


mutation {
    addExtension(
        input: {
            extensionId: 4017
            name: "William Robert Windsor III"
            outboundCid: "2125554012"
            email: "billybob@example.com"
            vmPassword: "4017"
            callerID: "William Robert Windsor III"
        }
    ) {
        status
        message
    }
}

The response from the PBX is simple for this mutation:

{'addExtension': {'status': True, 'message': 'Extension has been created Successfully'}}

Applying configuration

Here’s the GraphQL mutation which we use to apply the configuration:

mutation {
  doreload(input: {}) {
    message
    status
    transaction_id
  }
}


Here is the response from the PBX for this mutation:

{'doreload':
  {
    'message': 'Doreload/apply config has been initiated. Please check the status using fetchApiStatus api with the returned transaction id',
    'status': True, 'transaction_id': '2'
  }
}

Here’s what it looks like in action:

$ python3 addextension.py "William Robert Windsor III" billybob@example.com apply
Requesting authentication token...
Querying PBX for existing extension list...
Existing extensions:4001 4002 4003 4004 4005 4006 4007 4008 4009 4010 4011 4012 4013 4014 4015 4016 4017 4018 4019 4020 4021 4022 4023 7006
Adding new extension ID 4024 with name="William Robert Windsor III", email=billybob@example.com, vmPassword=4024, ouboundCid=2125554024
Result from PBX:  Extension has been created Successfully
Applying config...

Notes

  1. To just check existing extensions and find the next available extension, the script can be run without arguments (just “python3 addextension.py”)

  2. If you drop the “apply” as the last argument the script will add the extension, but won’t run the “doreload” mutation to apply the configuration

  3. The “doreload” API to apply the config is asynchronous, so when the script completes, it only indicates that the apply config operation has been started. There is another API fetchApiStatus which can be used to poll for completion (this is left as an exercise for the reader)

  4. You may prefer to use an Oauth2 library for Python (for example oauth2-client) instead of doing the Oauth2 step with the lower-level Python Requests library. For this particular use case where the simple machine-to-machine mode is used, and we request a new token every time the script is run, the Requests library was sufficient and easy to use, but other use cases would probably benefit from a library built for this purpose

Completed script

# GraphQL Add Extension Script for FreePBX 15
#
from gql import gql, Client
from gql.transport.aiohttp import AIOHTTPTransport
import requests, sys

TOKEN_URI='https://2XXXXXXX.deployments.pbxact.com/admin/api/api/token'
API_URI='https://2XXXXXXX.deployments.pbxact.com/admin/api/api/gql'
AUTH_ID='03cc2e2f0e77d289730b316144f1bb275fe7e840c42e57d5cd02e08f42f57595'
AUTH_SECRET='fd84b25596588296b12df985ce1d3a96'
GQL_SCOPE='gql:core gql:framework'
GQL_QUERY_FETCHALL='query { fetchAllExtensions { status message totalCount extension { extensionId } } }'
OUTBOUND_CID_PREFIX="212555" # Extension 4567 will get Outbound CID 2125554567

# First authenticate
print('Requesting authentication token...');
token_request_data={'grant_type':'client_credentials','scope':GQL_SCOPE}
r = requests.post(TOKEN_URI, data=token_request_data, auth=(AUTH_ID, AUTH_SECRET))
if 'access_token' not in r.json():
    sys.exit('Failed to get authentication token. Exiting.')

# Now on to GraphQL, fetch current extension list
print('Querying PBX for existing extension list...');
reqHeaders = { 'Authorization': 'Bearer ' + r.json()['access_token'] }
transport = AIOHTTPTransport(url=API_URI, headers=reqHeaders)
client = Client(transport=transport, fetch_schema_from_transport=False)
result = client.execute(gql(GQL_QUERY_FETCHALL))

# Get the list of existing extensions into a sorted numerical list
existingExtensions=[]
if not result['fetchAllExtensions']['status'] or result['fetchAllExtensions']['totalCount'] < 1:
    sys.exit('Failed to get any extension info, exiting...')
for ext in result['fetchAllExtensions']['extension']:
    existingExtensions.append(int(ext['extensionId']));
existingExtensions.sort()
print('Existing extensions:', end='')
for extId in existingExtensions:
    print(extId, end=' ')
print()

# Find the new extension, start at the lowest-defined extension and look for the next available number
lastCheckedExtension=existingExtensions[0]-1;
for checkExt in existingExtensions:
    if checkExt != lastCheckedExtension+1: break
    lastCheckedExtension = checkExt
newExt = lastCheckedExtension+1;

if(len(sys.argv)!=3 and len(sys.argv)!=4):
    exit('Next extension to use is '+str(newExt)+'. To add new user, format is: "python3 ' + sys.argv[0] + ' <username> <email>" or "python3 ' + sys.argv[0] + ' <username> <email> apply" to also apply the config')
outboundcid='{}{:04d}'.format(OUTBOUND_CID_PREFIX,newExt)

# Add new user using GraphQL
gqlAddUser = """
mutation {{
    addExtension(
        input: {{
            extensionId: {extensionId}
            name: "{name}"
            outboundCid: "{outboundCid}"
            email: "{email}"
            vmPassword: "{vmPassword}"
            callerID: "{callerID}"
        }}
    ) {{
        status
        message
    }}
}}
""".format(extensionId=newExt, name=sys.argv[1], outboundCid=outboundcid, email=sys.argv[2], vmPassword=newExt, callerID=sys.argv[1] )

print('Adding new extension ID ', newExt, ' with name="'+sys.argv[1]+'", email='+sys.argv[2]+', vmPassword=', newExt, ', ouboundCid='+outboundcid, sep="")
result = client.execute(gql(gqlAddUser))
print( 'Result from PBX: ' , result['addExtension']['message'])

# Apply config using GraphQL
gqlApplyConfig = """
mutation {
  doreload(input: {}) {
    message
    status
    transaction_id
  }
}
"""

if(len(sys.argv)==4 and sys.argv[3].lower()=='apply'):
    if(not result['addExtension']['status']): sys.exit('addExtension failed, not applying config')
    print('Applying config...')
    result = client.execute(gql(gqlApplyConfig))
  • No labels