VyOS 2.0 development digest #5: doing 1.2.x and 2.0 development in parallel

There was a rather heated discussion about the 1.2.0 situation on the channel, and valid points were definitely expressed: while 2.0 is being written, 1.2.0 can't benefit from any of that work, and it's sad. We do need a working VyOS in any case, and we can't just stop doing anything about it and only work on 2.0. My original plan was to put 1.2.0 in maintenance mode once it's stabilized, but it would mean no updates at all for anyone, other than bugfixes. To make things worse, some things do need if not rewrite, but at least very deep refactoring bordering on rewrite just to make them work again, due to deep changes in the configs of e.g. StrongSWAN.

There are three main issues with reusing the old code,  as I already said: it's written in Perl, it mixes config reading and checking with logic, and it can't be tested outside VyOS. The fourth issue is that the logic for generating, writing, and applying configs is not separated in most scripts either so they don't fit the 2.0 model of more transactional commits. The question is if we can do anything about those issues to enable rewriting bits of 1.2.0 in a way that will allow reusing that code in 2.0 when the config backend and base system are ready, and what exactly should we do.

My conclusion so far is that we probably can, with some dirty hacks and extra care. Read on.

The language

I guess by now everyone agrees that Perl is a bad idea. There are few people who know it these days, and there is no justification for knowing it. The language is a minefield that lacks proper error reporting mechanism or means to convey the semantics.

If you are new to it, look at this examples:

All "error reporting" enabled, let's try to divide a string by an integer.

$ perl -e 'use strict; use warnings; print "foobar" / 42'
Argument "foobar" isn't numeric in division (/) at -e line 1.
0

A warning indeed... Didn't prevent program from producing a value though: garbage in, garbage out. And, my long time favorite: analogous issues bit me in real code a number of times!

$ perl -e 'print reverse "dog"'
dog

Even if you know that it has to do with "list context", good luck finding information about default context of this or that function in the docs. In short, if the language of VyOS 1.x wasn't Perl, a lot of bugs would be outright impossible.

Python looks like a good candidate for config scripts: it's strongly typed, the type and object system is fairly expressive, there are nice unit test libraries and template processors and other things, and it's reasonably fast. What I don't like about it and dynamically typed languages in general is that it needs a damn good test coverage because the set of errors it can detect at compile time is limited and a lot of errors make it to runtime, but there are always compromises.

But, we need bindings. VyConf will use sockets and protobuf messages for its API which makes writing bindings for pretty much any language trivial, but in 1.x.x it's more complicated. The C++/Perl library from VyOS backend is not really easy to follow, and not trivial to produce bindings for. However, we have cli-shell-api, which is already used in config scripts written in shell, and behaves as it should. It also produces fairly machine-friendly output, even though its error reporting is rudimantary (then again, error reporting of the C++ and Perl library isn't all that nice either). So, for a proof of concept, I decided to make a thin wrapper around cli-shell-api: later it can be rewritten as a real C++ binding if this approach shows its limitations. It will need some C++ library logic extraction and cleanup to replicate the behaviour (why the C++ library itself links against Perl interpreter library? Did you know it also links to specific version of the apt-pkg library that was never meant for end users and made no promise of API stability, for its version comparison function that it uses for soring names of nodes like eth0? That's another story though).

Anyway, I need to add the Python library to the vyatta-cfg package which I'll do soon, for the time being you can put the file to your VyOS (it works in 1.1.7 with python2.6) and play with it:  

Right now it exposes just a handful of functions: exists(), return_value(), return_values(), and list_nodes(). It also has is_leaf/is_tag/is_multi functions that it uses internally to produce somewhat better error reporting, though they are unnecessary in config scripts, since you already know that about nodes from templates. Those four functions are enough to write a config script for something like squid, dnsmasq, openvpn, or anything else that can reload its config on its own. It's programs that need fancy update logic that really need exists_orig or return_effective_value. Incidentally, a lot of components that need that rewrite to repair or could seriously benefit from serious overhaul are like that: for example. iptables is handled by manipulating individual rules right now even though iptables-restore is atomic, likewise openvpn is now managed by passing it the config in command line options while it's perfectly capable of reloading its config and this would make tunnel restarts a lot less disruptive, and strongswan, the holder of the least maintainable config script, is indeed capable of live reload too.

Which brings us to the next part...

The conventions

To avoid having to do two rewrites of the same code instead of just one, we need to make sure that at least substantial part of the code from VyOS 1.2.x can be reused in 2.0. For this we need to setup a set of conventions. I suggest the following, and let's discuss it.

Language version

Python 3 SHALL be used.

Rationale: well, how much longer can we all keep 2.x alive if 3.0 is just a cleaner and nicer implementation?

Coding standard

No single function shall SHOULD be longer than 100 lines.

Rationale: https://github.com/vyos/vyatta-cfg-vpn/blob/current/scripts/vpn-config.pl#L449-L1134 ;)

Logic separation and testability

This is the most important part. To be able to reuse anything, we need to separate assumptions about the environment from the core logic. To be able to test it in isolation and make sure most of the bugs are caught on developer workstations rather than test routers, we need to avoid dependendies on the global state whenever possible. Also, to fit the transactional commit model of VyOS 2.0 later, we need to keep consistency checking, generating configs, and restarting services separated.

For this, I suggest that we config scripts follow this blueprint:


def get_config():
    foo = vyos.config.return_value("foo bar")
    bar = vyos.config.return_value("baz quux")
    return {"foo": foo, "bar": bar} # Could be an object depending on size and complexity...

def verify(config):
    result do_some_checks(config)
    if checks_succees(result):
        return None
    else:
        raise ScaryException("Some error")

def generate(config):
    write_config_files(config)

def apply(config):
    restart_some_services(config)

if __name__ == '__main__':
    try:
       c = get_config()
       verify(c)
       generate(c)
       apply(c)
    except ScaryException:
        exit_gracefully()

This way the function that process the config can be tested outside of VyOS by creating the same stucture as get_config() would create by hand (or from file) and passing it as an argument. Likewise, in 2.0 we can call its verify(), update(), and apply() functions separately.

Let me know what you think.

14 responses
Very well written. Seems to be very logical.
Python is very flexible, fast and light - definitely the best candidate for me. I think will be better to start using python 3 instead of python 2 because is faster than version 2 also python 3 requires more strict pep coding standards, also there no need to rewrite the same code twice :)
To be fair, I'm guilty of being sceptical about the future of python3 even though I always liked the improvements it made, and my own personal standard was to maintain compatibility with 2.7 in all code I write at any cost (ok, with 2.7 the cost is not all that high, but it's non-zero). By now most of libraries that are actually used and maintained by someone got support for 3, and some distros even go as far as to ship without python2 by default, so I guess it's safe to say that new projects don't need to bother with 2, especially if you control the environment (and we do know what we include in the images). Library availability disparity is also less of a problem for VyOS anyway since most of the code is logic rather than glue, not being able to easily find a libraries for say reading FoxPro files and exporting the data to MySpace will not change much, while readily available libraries for say turning user HA requirements expressed in an abstract config into a keepalived plus conntrackd setup don't exist anyway.
I got your points and concerns. You know better than me the price of the legacy code in the big projects. For me personally the future of python 3 is stable enough and will be better , on the other hand python 2.7 will be maintained at least 10 years from the initial 2.7 release. This means there will be bugfix releases until 2020.
Yeah, by now it's safe to say that python3 succeeded after all I guess. But, I'm still guilty of thinking that it will not even in 2014 and, worse, telling beginners that they should focus on the 2 for now. Ok, to my credit, some of the major web frameworks were not 3-compatible at the time, and long term distros without python3 in the default repos were more prevalent.
DB, despite not being a programmer, this series of blog posts is interesting.
I'm a little confused: will Python replace OCaml? Or is Python just for VyOS 1.0 and OCaml for VyConf/VyOS 2.0?
WhiskeyAlpha: It's never late to add programming to your activities. ;) Mario: I guess I should have given a more detailed explanation indeed. Python will not replace OCaml in the _config backend_ code (I tried it, it doesn't work), Python is for gradually rewriting config scripts in 1.x in a saner manner. We can then move those scripts, with some modification for the new syntax and new config API, to 2.0. I think for a lot of original 2.0 scripts, Python is also a good choice. A massive issue with the current VyOS is that a lot of config generation is done through endless string concatenation, which makes the code quite hard to follow or debug, the best solution in this case is using a template processor like mako or jina2. Templates with embedded logic do not mix well with either native code or static typing though: easy and readily available templates are worth sacrificing some performance (which is not even that big in case of Python) and safety. Config scripts also rarely employ complex algorithms or datastructures, unlike the backend, their intrinsic value is in the model of translating the abstract VyOS config to application configs, and that's where templates with embedded logic shine.
To be honest, I'm still not convinced with the OCaml choice, based on your own comments "I guess by now everyone agrees that Perl is a bad idea. There are few people who know it these days, and there is no justification for knowing it. The language is a minefield that lacks proper error reporting mechanism or means to convey the semantics." If you were to tell me that OCaml adoption and thus, developers, will increase significantly in the next couple of years, I'd be very skeptical. It does have its merits and looks engaging, but I'd put my money on Go or even Python itself, which have a much bigger user base and related ecosystem. Just my 2 cents.
Juan, the fun thing is, the adoption of ML-like languages is growing. Apple's Swift is an ML in disguise, F# is an OCaml clone that even features a "compatibility mode", and Facebook is actively using OCaml itself. Go, for some reason, chose to actively resist the progress. For instance, first they said no one needs parametric polymorphism, then they went on to imlement polymorphic datastructures in the standard library (container/list etc.), through automatically casting items to void (ahem, interface {}) and having the user unsafely cast it back if they want to take it out of the datastructure. On a somewhat bitter note, the number of people who contributed to the old backend (which is in C++, a very popular language) since the first day of VyOS development is two (Kim and I). The number of people who did anything to it inside Vyatta wasn't very large either. The pool of people willing to take up a task like that is inherently small it seems, and ML people are at least invariably familiar with handling complex datastructures in a safe way. ;)
Daniil, You're right, although it has type inference, Go doesn't have generics and interface{} is basically void, like you mentioned. I like Ocaml's data immutability (a must in functional languages?), it's an asset for this task. I'll take a better look at the rose tree to better understand its complexity. I glanced over vyconfd.ml and it doesn't seem particularly difficult to follow. I don't know ocaml, but the code seems pretty straightforward. In any case, it's coming along great and I hope the best for the project.
3 visitors upvoted this post.