Je suis Charlie

Autres trucs

Accueil

Seulement les RFC

Seulement les fiches de lecture

Ève

Creating measurements on RIPE Atlas through the API

First publication of this article on 16 April 2013


The network of Atlas probes managed by the RIPE-NCC exists for a long time. These small probes are installed in many places all over the world and perform periodic measurements, which are the basis of Internet health checks, interesting statistics and many papers. There have been many improvments recently, the addition of UDM (User Defined Measurements), where you can decide your own measurements via a Web interface, then an API to retrieve results in a structured format, and now an API to start measurements. It is currently beta.

Two warnings before you read further away: the UDM are not available for the general public. You need an account at RIPE-NCC and some credits earned from the Atlas system. And a second caveat: the API is in flux (and the measurement creation API is very recent) and therefore it is quite possible that the code published in this article will not work in the future. My goal is to show that it is possible, not to make the definitive documentation. So, always keep an eye on the official documentation (the measurement creation API is documented separately because it is still unstable.)

First, let's create in Python a script to run a measurement in several geographical areas (the world and four big regions). We will ping www.bortzmeyer.org with IPv6. The Atlas API is a simple REST one, taking parameters in JSON and giving back results in the same format. We will use the urllib2 package in Python's standard library and first create a Request because we will need non-standard headers:

url = "https://atlas.ripe.net/api/v1/measurement/?key=%s" % key
request = urllib2.Request(url)
request.add_header("Content-Type", "application/json")
request.add_header("Accept", "application/json")

The two HTTP headers are added because Atlas only speaks JSON. So, we need to define the parameters in JSON after reading the documentation. First, create a Python dictionary (this is Python code, not JSON, even if it is similar):

data = { "definitions": [
           { "target": "www.bortzmeyer.org", "description": "Ping my blog",
           "type": "ping", "af": 6, "is_oneoff": True} ],
         "probes": [
             { "requested": 5, "type": "area", "value": "WW" } ] }

And let's change it at each iteration:

for area in ["WW", "West", "North-East", "South-East", "North-Central", "South-Central"]:
    data["probes"][0]["value"] = area

And start the measurement with an HTTP POST request and the Python dictionary encoded as JSON as a parameter:

conn = urllib2.urlopen(request, json.dumps(data))

Atlas will send us back a JSON object giving, not the actual results, but the ID of this measurement. We will have to retrieve it later, through the Web interface or via the API, as explained in the next paragraphs. But, for the time being, let's just display the measurement ID. This requires parsing the JSON code:

results = json.load(conn) 
print("%s: measurement #%s" % (area, results["measurements"]))

And that's all. The program will display:

% python ping.py                                                      
WW: measurement #[1007970]
West: measurement #[1007971]
North-East: measurement #[1007972]
South-East: measurement #[1007973]
North-Central: measurement #[1007974]
South-Central: measurement #[1007976]

There are two things I did not explain here: the error handling and the API key. To create a measurement, you need an API key, which you get from the Web interface. In my case, I store it in $HOME/.atlas/auth and the script reads it there and adds it to the URL of the request. For the error handling, I suggest you see the actual script, atlas-ping.py.

The above script did only half of the work. It creates a measurement but does not retrieve and parse it. Let's now do that. Measurements can take a long time (in the previous example, because of the parameter is_oneoff, the measurement was done only once; if it is repeated, it lasts of course much longer) and there is no callback in Atlas, you have to poll. You get a JSON object with a member named status which is not thoroughly documented but with trial and errors and help from nice people, you can decipher it. The possible values are:

   0: Specified
   1: Scheduled
   2: Ongoing
   4: Stopped
   5: Forced to stop
   6: No suitable probes
   7: Failed
   8: Archived

Now, Let's poll:

over = False
while not over:
    request = urllib2.Request("%s/%i/?key=%s" % (url, measure, key))
    request.add_header("Accept", "application/json")
    conn = urllib2.urlopen(request)
    results = json.load(conn)
    status = results["status"]["name"]
    if status == "Ongoing" or status == "Specified":
            print("Not yet ready, sleeping...")
            time.sleep(60)
    elif  status == "Stopped":
            over = True
    else:
            print("Unknown status \"%s\"\n" % status)
            time.sleep(120)

So, for measurement 1007970, we retrieve https://atlas.ripe.net/api/v1/measurement/1007970/ and parse the JSON, looking for the status. Once the above while loop is done, the results are ready and we can get them at https://atlas.ripe.net/api/v1/measurement/1007970/result/:

request = urllib2.Request("%s/%i/result/?key=%s" % (url, measure, key))
request.add_header("Accept", "application/json")
conn = urllib2.urlopen(request)
results = json.load(conn)
total_rtt = 0
num_rtt = 0
num_error = 0
for result in results:
   ...

Here, we will do only a trivial computation, finding the average RTT of all the tests for all the probes. Just remember that some probes may fail to do the test (it is unfortunately much more common with IPv6 tests) so we have to check there is indeed a rtt field:

    for result in results:
        for test in result["result"]:
            if test.has_key("rtt"):
                total_rtt += int(test["rtt"])
                num_rtt += 1
            elif test.has_key("error"):
                num_error += 1
            else:
                raise Exception("Result has no field rtt and not field error")
...
print("%i successful tests, %i errors, average RTT: %i" % (num_rtt, num_error, total_rtt/num_rtt))

And that's all, we have a result:

Measurement #1007980, please wait the result (it may be long)
...
12 successful tests, 0 errors, average RTT: 66

The entire script is atlas-ping-retrieve.py.

Now, let's try with a different type of measurements, on the DNS, and a different programming language, Go. The preparation of the HTTP POST request is simple, using the net/http standard package:


client := &http.Client{}
data := strings.NewReader(DATA)
req, err := http.NewRequest("POST", URL + key, data)
req.Header.Add("Content-Type", "application/json")
req.Header.Add("Accept", "application/json")

And its execution also:

response, err := client.Do(req)
body, err := ioutil.ReadAll(response.Body)

But I skipped one step: what is in the DATA constant? It has to be JSON content. Go does not have a way to write a JSON-like object as simply as Python, so, in this case, we cheat, we create the JSON by hand and put it in a string (the proper way would be to create a Go map, with embedded arrays and maps, and to marshall it into JSON):

DATA string = "{ \"definitions\": [ { \"target\":
	      \"d.nic.fr\", \"query_argument\": \"fr\",
	      \"query_class\": \"IN\", \"query_type\": \"SOA\", 
              \"description\": \"DNS AFNIC\", \"type\": \"dns\",
	      \"af\": 6, \"is_oneoff\": \"True\"} ],  \"probes\": 
              [  { \"requested\": 5, \"type\": \"area\", \"value\": \"WW\" } ] }"
)

Now, we just have to parse the JSON content sent back with the standard package encoding/json. Go is a typed language and, by default, type is checked before the program is executed. In the REST/JSON world, we do not always know the complete structure of the JSON object. So we just declare the resulting object as interface{} (meaning untyped). Does it disable all type checking? No, it just postpones it until run-time. We will use type assertions to tell what we expect to find and these assertions will be checked at run-time:


err = json.Unmarshal(body, &object)
mapObject = object.(map[string]interface{})

In the code above, the type assertion is between parenthesis adter the dot: we assert that the object is actually a map indexed by strings, and storing untyped objects. We go on:

status := mapObject["status"].(map[string]interface{})["name"].(string)
if status == "Ongoing" || status == "Specified" {
			fmt.Printf("Not yet ready, be patient...\n")
			time.Sleep(60 * time.Second)
} else if status == "Stopped" {
			over = true
} else {
			fmt.Printf("Unknown status %s\n", status)
			time.Sleep(90 * time.Second)
}

There was two other type assertions above, one to say that mapObject["status"] is itself a map and one to assert that the field name contains character strings. We can now, once the polling loop is over, retrieve the result, parse it and display the result:


err = json.Unmarshal(body, &object)
arrayObject = object.([]interface{})
total_rtt := float64(0)
num_rtt := 0
num_error := 0
for i := range arrayObject {
	mapObject := arrayObject[i].(map[string]interface{})
	result, present := mapObject["result"]
	if present {
		rtt := result.(map[string]interface{})["rt"].(float64)
		num_rtt++
		total_rtt += rtt
	} else {
		num_error++
	}
}
fmt.Printf("%v successes, %v failures, average RTT %v\n", num_rtt, num_error, total_rtt/float64(num_rtt))

And it displays:

Measurement #1008096
...
4 successes, 0 failures, average RTT 53.564

And this is the (temporary) end. The Go program is atlas-dns.go.

Thanks to Daniel Quinn and Iñigo Ortiz de Urbina for their help and tricks and patience. A good tutorial on running UDM and analyzing their results is Hands-on: RIPE Atlas by Nikolay Melnikov.

Version PDF de cette page (mais vous pouvez aussi imprimer depuis votre navigateur, il y a une feuille de style prévue pour cela)

Source XML de cette page (cette page est distribuée sous les termes de la licence GFDL)