Creating OpenStack Client Instances in Python

So you're writing a Python application and you want to talk to an OpenStack service. Or maybe even multiple services. A quick Google turns up something like python-novaclient, which provides a way to create an instance of a Nova client in Python. Great! We're done here, right? Not so fast, my friend.

While it is true that you can directly use python-novaclient to talk to Nova, it's not necessarily your best move. Why? Well, there's a dirty little secret about the python-*client projects for OpenStack: They were mostly written for the services to talk to each other, not for users to talk to the services. True, they can be used for the latter, but as we'll discuss here there are some pretty serious usability issues with doing that.

The Problems

The biggest problem is Keystone v3. With Keystone v2 you could get away with directly instantiating client instances. There were only a few required auth parameters and it was pretty easy to figure out which ones you needed to provide (username, password, auth_url, and tenant, essentially). Not so bad, at least for novaclient. Other clients were not so nice, but more on that later.

Enter Keystone v3. Now you have domains to worry about in addition to all the above auth parameters. But that's okay, we just need to make sure we provide the extra parameters and everything will be fine, right? Nope. This was the approach I tried to take when adding Keystone v3 support to OpenStack Virtual Baremetal, and it went badly enough to prompt this blog post.

The problem is that apparently not every cloud needs exactly the same Keystone v3 auth parameters. I don't actually understand why that is the case, but in my experience it is. And on top of that there's no apparent sanity in why some parameters are needed but others are not.

For example, against my local Devstack-based cloud I needed to provide the following auth parameters: username, password, auth_url, project, user_domain, and project_domain. Fair enough. I added those configuration options and thought my work was done. It was not. Against RDO Cloud (a cloud being run by the RDO admins to provide development resources for RDO and its related projects), those auth parameters did not work. I don't have the exact error message anymore, but it was something about expecting to find the user in the domain. Or maybe the domain in the user. I can't remember and it's not relevant because the point of this blog post is to help you avoid it in the first place. :-)

Ultimately I discovered that RDO cloud needed the following parameters: username, password, auth_url, project_name, project_id (yes, both), and user_domain_name. But strangely, not project_domain_name. In fact, the rc file provided did not even set project_domain_name. But it did set both project_name and project_id, and if I tried to remove the id (because previously I had been passing it by name) it caused my client calls to fail.

I haven't the faintest clue why these different configurations are needed, and I have even less idea how to predict what a particular cloud is going to require and include logic that can just do the right thing. If I've convinced you that you don't want to deal with this yourself, you can skip below to The Solution. But I'm going to continue and discuss another drawback of creating your own clients from scratch, for historical reference if nothing else.

The other drawback relates to my earlier point about the clients being primarily used to talk between services. Within a service, you probably have a number of things in place already. Keystone sessions and tokens, for example. When starting from scratch in your own application you don't. Some clients require a Keystone session or token to be passed in on client creation, which makes your simple "instantiate a client" call turn into multiple calls to different services.

One particularly fun example is the Heat client. It requires a Keystone token and the Heat endpoint in order to construct a client instance. So you get to retrieve a token and then pull the id and endpoint out of that data. But wait, there's more! The token data format changed from v2 to v3, so I had to special-case the endpoint retrieval logic based on the version. There may have been a better way to do that, but if so I wasn't aware of it at the time. And the data was in the token content, so why wouldn't I just read it from there?

Heat is hardly the only client that requires excessive effort just to create a client instance though. Novaclient gets a special mention here too for breaking compatibility from v6 to v7. It was necessary to fix problems with its own Keystone v3 support, but it was still a headache for users of the project.

If you're not convinced by now that you don't want to deal with all this hassle, then more power to you. I threw up my hands and made it someone else's problem. Read on for details.

The Solution

TLDR: Use os-client-config.

Or Shade. I went with os-client-config because it was a drop-in replacement since I was already calling the clients directly. YMMV.

Honestly, the only reason I didn't do this in the first place is because my initial introduction to os-client-config was as a way to add clouds.yaml support to OVB. But I only work with a few clouds, so I don't use clouds.yaml. I just have an rc file for each cloud I use.

Good news! os-client-config supports that too! There's a special value that you can pass in to the 'cloud' parameter in its make_client function that will cause it to read the authentication data from the environment, just like I was doing manually when calling the clients directly. That special value is cleverly named "envvars". I actually found that passing cloud=None also resulted in the correct behavior, but that may vary depending on whether you have a clouds.yaml or not. I believe that makes it use the "default" cloud, which apparently is "envvars" if you have no clouds.yaml at all.

Here's an example of creating a client with os-client-config:

nova = os_client_config.make_client('compute', cloud='envvars')

The OVB build-nodes-json command needs to create 3 different OpenStack clients: nova, neutron, and glance. The code I used to have for that was around 30 lines and those 30 lines called into dozens of additional lines of code in authentication helper functions. The os-client-config version is three lines, the one above and two more for neutron and glance. That's it.

Conclusion

The change to os-client-config has made OVB much less complex and more maintainable. I'm hopeful that documenting my struggles with this may help someone else avoid them altogether since my mistakes were almost entirely due to ignorance. Had I known all of this ahead of time I'd have made much different choices. Better choices. I guess the main takeaway from this post is "make better choices than I did." :-)