Men&Mice Web Service: JSON-RPC
Micetro by Men&Mice is a comprehensive DDI solution that allows you to manage your DNS, DHCP and IP Addresses in a simplified, flexible manner. In order to maximize this flexibility the Men&Mice Suite provides a rich set of commands accessible through its web service interface. These commands are used internally for almost all requests between the user interface and the Central service, so if something can be done in the user interface it can also be done through the web service interface.
Initially the web service interface provided a SOAP API and a WSDL service. Starting with version 6.6, the Men&Mice Web Service also provides a JSON-RPC service that complies with the JSON-RPC 2.0 specifications. The main reason for this decision to provide JSON-RPC along with SOAP is that JSON is well supported in popular programming languages such as JavaScript and Python, and working with JSON-like data instead of XML is much easier and less error prone, not to mention that JSON packets are smaller than XML packets and more human readable.
If you are interested in the SOAP service there is an excellent introduction to it on our blog.
JSON-RPC
The JSON-RPC service supports all the same commands as the SOAP API. You can find a complete list of the SOAP commands available here. The same code is used internally in Micetro so when a new SOAP command is added, it will be readily available as a JSON-RPC method. As with SOAP, our main focus is to provide JSON-RPC over HTTP/HTTPS.
No special magic is needed to send a JSON-RPC request. When our web service receives a packet it will determine from the content of the packet whether this is a SOAP or a JSON-RPC request. You can even mix those two and the web service will simply respond with a JSON if this was a JSON-RPC request or an XML if this was a SOAP request. The general format of a Men&Mice Web Service JSON-RPC request and a response is as follows:
--> {"jsonrpc": "2.0", "method": "theMethodName", "params": theParams, "id": 1} <-- {"jsonrpc": "2.0", "result": theResult, "id": 1}
The jsonrpc member should always be “2.0”. The method member should contain a string with the name of the method to be executed. The params member contains data that should be passed to the method. The member id is optional, but if it exists the response will contain the same ID as provided in the request. If successful the response will include a member result that will contain the result from the RPC method that was executed. If an error occurs the response will not include a result but instead another member named error that will include information about what happened.
An example:
--> {"jsonrpc": "2.0", "method": "Login", "params": {"password": "secretPassword", "loginName": "administrator", "server": "10.5.0.6"}, "id": 1} <-- {"jsonrpc": "2.0", "result": {"session":"J95A5uh9obM6KjCtbtHZ"}, "id": 1} <-- {"jsonrpc": "2.0", "method": "GetDNSServers", "params": {"session": "J95A5uh9obM6KjCtbtHZ"}, "id": 2} --> {"jsonrpc": "2.0", "result": {"dnsServers":[{"ref":"{#2-#10}","name":"caching1.demo.","resolvedAddress":"10.5.0.27","port":1337,"type":"Unbound","state":"OK","customProperties":[],"subtype":"Unbound"},{"ref":"{#2-#11}","name":"caching2.demo.","resolvedAddress":"10.5.0.30","port":1337,"type":"Unbound","state":"OK","customProperties":[],"subtype":"Unbound"}],"totalResults":2}, "id": 2}
JSON-RPC and Python
Python is powerful as a scripting language. The standard libraries are pretty comprehensive and if you need something more specific then there are plenty of additional libraries that can be found. One of the strengths of Python is its dynamic type system and “duck typing” that allows for the creation of complex objects without writing too much code.
We have created a small, light-weight Python module that uses the request library to simplify session handling and data conversion from Python dictionaries to JSON requests. The class overloads the __getattr__ method, so that all unknown functions are assumed to be RPC requests and all the arguments passed are assumed to be a RPC data.
Download the latest version of the Python module, extract the downloaded file, go into the subdirectory and run:
$ python setup.py install
Before we can use any of the commands we need to create a JSONClient object and log in to our Central Service. Let’s start Python and type in the following commands.
$ python >>> import mmJSONClient >>> client = mmJSONClient.JSONClient()
The basics
The Login method accepts the following arguments: proxy, server, username and password. Proxy is the name or IP address of a server running the Men&Mice Web Service. If you do not provide the proxy argument, then the Login method assumes the web service is running on the same machine as the Central Service. The server argument should contain the name or address of the machine that is running the Men&Mice Central Service. The username argument is the name of the user that we want to log in as and password is his password. Note that in order for this user to be able to use the web service he has to have permissions to use the web user interface. For optimal security, it is best to create a new dedicated user account that only has access to the objects the script needs to function.
Now let’s log in to the server. In this example I’m logging in to a server named ‘central.demo’ with the user name ‘a_user’ and the password ‘secret’. Since the web service is also running on ‘central.demo’ I don’t need to provide the proxy argument.
>>> client.Login(server='central.demo', username='a_user', password='secret')
As we mentioned earlier, we can use all the commands that are available for the SOAP API, and there are a lot (around 200 at last count). A list can be found here, along with descriptions of what arguments they need and what data they return.
Let’s start with something simple, like the GetDNSServers method. As the name implies, the method will return all the DNS servers available to the user. Note that it might not return all the DNS servers available, but instead only those the user has permission to see. If you look at the description of GetDNSServers, you will see the only argument required is the session ID; everything else is optional. The session ID is a random string that the web service returned from the Login method. If you have successfully executed the Login method, you can see what your temporary session ID is by doing:
>>> print(client.sessionid) VFr79UQMhFfOq4Q8l0Qi
Or if you are using Python 2.7, use print without parentheses:
>>> print client.sessionid VFr79UQMhFfOq4Q8l0Qi
The rest of this article assumes use of Python 3.x. When using earlier versions of Python the syntax may have to be adjusted accordingly.
Fortunately when using the JSONClient class you don’t have to worry about the session ID, as the class handles that automatically so if you want to list all the DNS servers available, you can simply type:
>>> print(client.GetDNSServers()) {u'totalResults': 2, u'dnsServers': [{u'name': u'caching1.demo.', u'resolvedAddress': u'10.5.0.27', u'ref': u'{#2-#10}', u'subtype': u'Unbound', u'state': u'OK', u'customProperties': [], u'type': u'Unbound', u'port': 1337}, {u'name': u'caching2.demo.', u'resolvedAddress': u'10.5.0.30', u'ref': u'{#2-#11}', u'subtype': u'Unbound', u'state': u'OK', u'customProperties': [], u'type': u'Unbound', u'port': 1337}]}
The result from GetDNSServers is a Python dictionary that contains two members: totalResults and dnsServers. As stated in the SOAP API documentation, the totalResults member contains the number of DNS Servers we have access to, and the member dnsServers contains an array of DNS servers. To refer to a member in a Python dictionary we can use brackets – for example, the following can be used to print out the total number of DNS servers:
>>> print(client.GetDNSServers()['totalResults']) 2
Providing arguments to a method is simple. GetDNSServers has a number of optional arguments, for example the filter argument. Filter is powerful argument that is provided with many of the Men&Mice SOAP API commands. It allows you to limit the result to only the items that you want to see. You can use wildcards and regular expressions with a filter. For more information, look at the description of filtering in the SOAP API. Let’s say that we wanted to get all the servers that contain the number 2 in their name. We can simply do:
>>> print(client.GetDNSServers(filter='name:2')) {u'totalResults': 1, u'dnsServers': [{u'name': u'caching2.demo.', u'resolvedAddress': u'10.5.0.30', u'ref': u'{#2-#11}', u'subtype': u'Unbound', u'state': u'OK', u'customProperties': [], u'type': u'Unbound', u'port': 1337}]}
Or let’s say we wanted to print out all A records from the zone ‘applepie.ak.is’ that start with the name ‘apple’:
>>> print( '\n'.join(['{}\t{}\t{}'.format(r['name'],r['type'],r['data']) for r in client.GetDNSRecords(dnsZoneRef= 'applepie.ak.is.', filter='type:^A$ name:^apple')['dnsRecords']]) ) apple1 A 10.50.0.1 apple2 A 10.50.0.2 apple3 A 10.50.0.3
In the example below we have created a Python script that logs on to a server, prints out all of its zones and then the first 10 records of a single zone. It assumes that we are running Men&Mice Web Service and Men&Mice Central on a server named ‘central.demo’. It also assumes there is a zone named ‘applepie.ak.is’ on one of the servers:
import mmJSONClient client = mmJSONClient.JSONClient() client.Login(server='central.demo.', username='a_user', password='secret') result= client.GetDNSZones() print('\nTotal number of zones: ' + str(result['totalResults'])) for zone in result['dnsZones']: print(zone['name']) myZone = myZone= 'applepie.ak.is.' result = client.GetDNSRecords(dnsZoneRef= myZone, limit= 10) print('\nRecords in ' + myZone) for record in result['dnsRecords']: print(record['name'] + "\t" + record['ttl'] + "\t" + record['type'] + "\t" + record['data'])
Manipulating DNS zones
Now let’s do something a little bit more complicated. Let’s create a zone and add some records to it. If we look at the list of SOAP API commands we can find AddDNSZone. AddDNSZone requires at least two arguments: a session argument we don’t have to worry about since JSONClient will handle that for us, and a dnsZone argument which should be of type DNSZone. The DNSZone type requires at least two members (or arguments): name and type. The name argument is a string, and the type argument is an enumeration which can be Master, Slave, Hint, Stub or Forward. If the call is successful AddDNSZone will return a reference to the newly created zone.
We also have to specify where we want to create the zone. Notice that we don’t have any possible DNS server reference when creating a zone. The reason is that each DNS server can have one or more views that in turn can contain a number of zones. In most cases DNS servers will only have one view, called the default view or the empty view (”). When referring to any object in the Men&Mice Suite, you can either use its globally unique identifier (GUID), that was returned from the Central Server or you can pass on a string that will uniquely identify the object, in this case using its name (for a more detailed description on how this works, see Referencing Objects in the SOAP API). To reference a default view on a server you can simply use its fully qualified name and add a double column at the end.
As an example, let’s say we want to send a reference to the default view as an argument on the DNS server dnsserver1.demo. Here we can simply pass the string ‘dnsserver1.demo.:’ as a DNS view reference.
>>> zoneRef= client.AddDNSZone(dnsZone={'dnsViewRef':'dnsserver1.demo.:', 'name':'test1.com', 'type':'Master'} , saveComment='A zone created using Python') >>> print(zoneRef) {u'ref': u'{#4-#2054}'}
Note that we also added a save comment when creating the zone as it is sensible to always add a comment when you are making changes to the system. Also notice the return value from AddDNSZone. As before it is a Python dictionary, this time with only the one member, ‘ref’, that contains a reference to the newly created zone. Now let’s create few records:
>>> for i in range(100,105): ... client.AddDNSRecord(dnsRecord={'dnsZoneRef':zoneRef['ref'], 'name':'test{0}'.format(i), 'ttl':'', 'type':'A', 'data':'10.50.0.{0}'.format(i), 'enabled':1}) ...
In this example, since I’m creating multiple records, I might instead use the SOAP command AddDNSRecords. AddDNSRecords will accept multiple records and update the zone just once instead of multiple times:
>>> client.AddDNSRecords(dnsRecords= [{'dnsZoneRef':zoneRef['ref'], 'name':'test{0}'.format(i), 'ttl':'', 'type':'A', 'data':'10.50.0.{0}'.format(i), 'enabled':1} for i in range(100,105)]) {'errors': [], 'objRefs': ['{#13-#9009}', '{#13-#9010}', '{#13-#9011}', '{#13-#9012}', '{#13-#9013}']}
As a final example, let’s create a script that will check if all the PTR records for a DNS server are in order. The script will read through all the forward and reverse zones and report if there are any PTR records missing or if there are any orphaned PTR records:
import mmJSONClient def qualifyName(recName, domainName): if len(recName) == 0: return domainName elif recName[len(recName) - 1] == '.': return recName + domainName return recName + '.' + domainName mmServer = 'central.demo.' mmUser = 'a_user' mmPassword = 'secret' dnsServer = 'b-centos6-32.demo.' try: print('Checking reverse zone on server [{}] ...'.format(dnsServer)) client = mmJSONClient.JSONClient() client.Login(server= mmServer, username= mmUser, password= mmPassword) viewRef = client.GetDNSView(dnsViewRef= dnsServer + ':') # Get all forward and reverse zones zoneList = client.GetDNSZones(filter= 'dnsViewRef:' + viewRef['dnsView']['ref'])['dnsZones'] forwardZoneList = [z for z in zoneList if (not z['name'].endswith('in-addr.arpa.') and not z['name'].endswith('ip6.arpa.'))] revZoneList = [z for z in zoneList if z['name'].endswith('in-addr.arpa.')] print('Found {} forward-zone(s) and {} reverse-zone(s)'.format(len(forwardZoneList), len(revZoneList))) # Collect all PTR records as a map of ip -> [record, zone, data] # and give the user a warning if there are duplicates ptrRecordsMap = {} for z in revZoneList: print('Scanning reverse-zone [{}]'.format(z['name'])) try: records = client.GetDNSRecords(dnsZoneRef= z['ref'], filter= 'type:^PTR$')['dnsRecords'] except Exception as e: print(e) for r in records: domain = qualifyName(r['name'], z['name']) label = domain.split('.', 4) ip = '{}.{}.{}.{}'.format(label[3], label[2], label[1], label[0]) if ip in ptrRecordsMap: print( ('Warning: Ignoring reverse record [{} in {} {}] for the IP address [{}]:\n' + '\talready found [{} in {} {}]').format( r['name'], z['name'], r['data'], ip , ptrRecordsMap[ip][0], ptrRecordsMap[ip][1], ptrRecordsMap[ip][2])) else: ptrRecordsMap[ip] = [r['name'], z['name'], r['data']] # Collect all A records as a map of ip -> [record, zone] # and give the user a warning if there are duplicates aRecordsMap = {} aMatchedRecordsMap = {} # contains all record that found a match for z in forwardZoneList: print('Scanning forward-zone [{}]'.format(z['name'])) records= [] try: records = client.GetDNSRecords(dnsZoneRef= z['ref'], filter= 'type:^A$')['dnsRecords'] except Exception as e: print(e) for r in records: ip = r['data'] if ip in aRecordsMap: print( ('Warning: Ignoring the A record [{} in {} {}]:\n' + '\talready found [{} in {} {}]').format( r['name'], z['name'], r['data'] , aRecordsMap[ip][0], aRecordsMap[ip][1], ip)) elif ip in ptrRecordsMap: # We already have this as a PTR record but do the data match? if qualifyName(r['name'], z['name']) != ptrRecordsMap[ip][2]: print( ('Warning: Record [{} in {} {}] has an incorrect reverse reference:\n' + '\talready found a PTR record [{} in {} {}]').format( r['name'], z['name'], r['data'] , ptrRecordsMap[ip][0], ptrRecordsMap[ip][1], ptrRecordsMap[ip][2])) aMatchedRecordsMap[ip] = [r['name'], z['name']] del ptrRecordsMap[ip] else: # the ip wasn't found in the reverse list but it might already been matched if ip in aMatchedRecordsMap: print( ('Warning: Ignoring the A record [{} in {} {}]:\n' + '\talready found [{} in {} {}]').format( r['name'], z['name'], r['data'] , aMatchedRecordsMap[ip][0], aMatchedRecordsMap[ip][1], ip)) else: aRecordsMap[ip] = [r['name'], z['name']] print ('\nFound {} missing PTR-Record(s)...'.format(len(aRecordsMap))) for ip in aRecordsMap: print ('{} in {} {}'.format(aRecordsMap[ip][0], aRecordsMap[ip][1], ip)) print ('\nFound {} orphan PTR-Record(s)...'.format(len(ptrRecordsMap))) for ip in ptrRecordsMap: print ('{} in {} {}'.format(ptrRecordsMap[ip][0], ptrRecordsMap[ip][1], ptrRecordsMap[ip][2])) except Exception as e: print(e)
Summary
The primary focus of the Men&Mice development team has always been to simplify the complex task of administering a DDI environment while still being flexible, as different environments can have very different needs. To achieve this flexibility Micetro provides rich set of commands accessible through a web service interface.
As of version 6.6 both SOAP/WSDL and JSON-RPC services are supported. Both of these services have their advantages but JSON-RPC is more light-weight than SOAP/WSDL and a better fit for many programming languages.