Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fb_networkmanager: new cookbook to manage NetworkManager #127

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cookbooks/fb_init_sample/metadata.rb
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
'fb_mlocate',
'fb_modprobe',
'fb_motd',
'fb_networkmanager',
'fb_nscd',
'fb_nsswitch',
'fb_postfix',
Expand Down
1 change: 1 addition & 0 deletions cookbooks/fb_init_sample/recipes/default.rb
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@
include_recipe 'fb_sysctl'
# HERE: networking
include_recipe 'fb_users'
include_recipe 'fb_networkmanager'
if node.centos?
# We turn this off because the override causes intermittent failures in
# Travis when rsyslog is restarted
Expand Down
225 changes: 225 additions & 0 deletions cookbooks/fb_networkmanager/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
fb_networkmanager Cookbook
============================
An attribute-driven API to configure Network Manager

Requirements
------------

Attributes
----------
* node['fb_networkmanager']['enable']
* node['fb_networkmanager']['system_connections']
* node['fb_networkmanager']['system_connections'][$NAME]['_migrate_from']
* node['fb_networkmanager']['system_connections'][$NAME]['_defaults']
* node['fb_networkmanager']['config']

Usage
-----
### Config

The global config (`/etc/NetworkManager/NetworkManager.conf`), is controlled
by the `config` hash. It's a two-level hash where the top-level is INI section
names and the second level is key-value pairs for the options in that section.

For example:

```ruby
node.default['fb_networkmanager']['config']['main']['foo'] = 'bar'
```

would render as:

```text
[main]
foo=bar
```

The default config is based on the Ubuntu config but should be safe for all
distros.

### System Connections

Network Manager unfortunately uses the files in the `system-connections` folder
as a data store about those networks. This means they can change out from under
you as it addds its own information. For example, for WiFi entries, it can add
BSSIDs it has seen to the file.

As such using the desired config to populate a template is not sufficient - this
would both lose data that Network Manager wants and also cause a lot of
unnecessary resource firing.

To work around this scenario, this cookbook loads in the existing file, merges
in the desired config, and then checks to see if the resulting contents are
different from just the loaded file. If the values would not change, then we do
not write out the new config. If the values are different, then we write out
the merged config. Since Network Manager can write out values in a different
order or with different spacing, we dont' compare the actual files, but instead
the parsed data. We leverage IniParse for reading and writing INI files since
it is bundled with Chef.

All that said, the system_connections hash works a lot like the `config` hash,
except there's an extra level for each connection. For example:

```ruby
node.default['fb_networkmanager']['system-connections']['mywifi'] = {
'connection' => {
'type' => 'wifi',
'id' => 'Cool Wifi',
'uuid' => '...',
},
'wifi' => {
'mode' => 'infrastructure',
'ssid' => 'Cool Wifi',
},
'wifi-security' => {
'auth-alg' => 'open',
'key-mgmt' => 'wpa-psk',
'psk' => 'SuperS3kr1t',
},
}
```

Would create `/etc/NetworkManager/system-connections/fb_networkmanager_mywifi`
with this content:

```text
[connection]
id=Cool Wifi
uuid=...
type=wifi

[wifi]
mode=infrastructure
ssid=Cool Wifi

[wifi-security]
auth-alg=open
key-mgmt=wpa-psk
psk=SuperS3kr1t
```

Note that all files we make are prefixed with `fb_networkmanager`, so that we
can cleanup files we created that are no longer in the config.

### A note on booleans

It is worth noting that various plugins and parts of the config expect
different kinds of booleans - some `true` and `false`, others `yes` and `no`.
Normally, an FB Attribute API cookbook would take a true ruby boolean and
convert it to the appropriate string for a system, but since it's not
consistent across NM, we leave it to the user to specify the right one for the
right value. This is true both in `config` and in `system_connections`.

### A note on UUIDs

We generally recommend coming up with a static UUID per connection you want
to rollout. For example, generate a UUID (for example using `uuidgen`, or
by `cat /proc/sys/kernel/random/uuid`), and then associate that with given
connection, statically in your config. You must use a different UUID for each
connection (obviously), but using the same UUID for the same connection across
machines makes debugging easier.

However, if you want truly unique UUIDs, one option is to just not specify a
UUID and let Network Manager fill one in. However, not all versions of NM
support this, and some will just ignore that connections.

You can't just generate UUIDs in the recipe, as they'll change on every run. So
here's one way to solve that problem: build each UUID seeded with the hostname
and the connection name so they stay the same across runs:

```ruby
node.default['fb_networkmanager']['system-connections']['mywifi'] = {
'connection' => {
'type' => 'wifi',
'id' => 'Cool Wifi',
'uuid' => UUIDTools::UUID.sha1_create(
UUIDTools::UUID_DNS_NAMESPACE,
"#{node['fqdn']}/Cool Wifi",
),
},
...
}
```

#### Migrating from existing configs

Migrating to this cookbook could potentially pose a problem: you want all the
information from the existing connection, but you don't want a duplicate
connection.

We provide a `_migrate_from` key. When populated, we'll use that as our base
config the first time, merge any data provided in the node, and then delete
the old config.

This provides seemless transition - it will preserve the UUID, which will
keep network manager from thinking any connections went away, and ensure
in-use connections don't drop.

For example, let's say you had droped a file
`/etc/NetworkManager/system-connections/OurCorpWifi` that you had dropped off
with `cookbook_file`, or a script, or even that you had was built through
manually setting it up in the NM GUI. You could then do:

```ruby
node.default['fb_networkmanager']['system-connections']['our_corp_wifi'] = {
'_migrate_from' => 'OurCorpWifi',
'connection' => {
'type' => 'wifi',
'id' => 'OurCorpWifi',
},
'wifi' => {
'mode' => 'infrastructure',
'ssid' => 'OurCorpWifi',
},
'wifi-security' => {
'auth-alg' => 'open',
'key-mgmt' => 'wpa-psk',
'psk' => 'SuperS3kr1t',
},
}
```

Then anything not specified here will be pulled in from the existing
`OurCorpWifi` file. Note that any settings that you care about should be
specified in the node to ensure that on new setups, you're not missing critical
configuration.

Note that if the original service file isn't there, Chef will just create a new
connection file (though it will warn). Also note that once Chef has created one,
it stop pulling in the old file (and will remove it).

#### Providing defaults

In general, the Chef config wins over the user config as described above.
However it is often desirable to specify a default config in case the user does
not specify anything. In that case you can use the `_defaults`. For example:

```ruby
node.default['fb_networkmanager']['system-connections']['our_corp_wifi'] = {
'_migrate_from' => 'OurCorpWifi',
'_defaults' => {
'connection' => {
'autoconnect-priority' => '100',
}
},
'connection' => {
'type' => 'wifi',
'id' => 'OurCorpWifi',
},
'wifi' => {
'mode' => 'infrastructure',
'ssid' => 'OurCorpWifi',
},
'wifi-security' => {
'auth-alg' => 'open',
'key-mgmt' => 'wpa-psk',
'psk' => 'SuperS3kr1t',
},
}
```

The rendered config here will set `connection.autoconnect-priority` to 100
if there is no value for it found in the existing file, but will use the value
in the file if it exists. The logic here is quite simple:

defaults < user config < chef config
46 changes: 46 additions & 0 deletions cookbooks/fb_networkmanager/attributes/default.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
#
# Cookbook:: fb_networkmanager
# Recipe:: default
#
# Copyright (c) 2020-present, Vicarious, Inc.
# Copyright (c) 2020-present, Facebook, Inc.
# All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

default['fb_networkmanager'] = {
'enable' => false,
'manage_packages' => true,
'system_connections' => {},
'config' => {
'main' => {
'plugins' => [
'ifupdown',
'keyfile',
],
},
'ifupdown' => {
# yup... this boolean does NOT take true/false like others,
# but instead yes/no. Since there's no programmatic way to know
# when true/false is wanted vs yes/no, we leave it up to the user
# to specify the right one at the right time.
#
# NetworkManager is the worst.
'managed' => 'no',
},
'device' => {
'wifi.scan-rand-mac-address' => false,
},
},
}
57 changes: 57 additions & 0 deletions cookbooks/fb_networkmanager/libraries/default.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
#
# Cookbook:: fb_networkmanager
# Recipe:: default
#
# Copyright (c) 2020-present, Vicarious, Inc.
# Copyright (c) 2020-present, Facebook, Inc.
# All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

require 'iniparse'

module FB
class Networkmanager
def self.active_connections
return {} unless ::File.exist?('/usr/bin/nmcli')

s = Mixlib::ShellOut.new('nmcli -t conn show --active').run_command
return {} if s.error?

cons = {}
s.stdout.each_line do |line|
name, uuid, type, device = line.strip.split(':')
cons[name] = {
'uuid' => uuid,
'type' => type,
'device' => device,
}
end
cons
end

def self.to_ini(content)
IniParse.gen do |doc|
content.each_pair do |sect, opts|
doc.section(sect, :option_sep => '=') do |section|
opts.each_pair do |opt, val|
v = [val].flatten.join(',')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NetworkManager connection files (but not NetworkManager.conf, because consistency!) use semicolons for multi-value separators, and requires a trailing semicolon for multi-value fields. Using a semicolon in a value makes IniParse wrap the value in quotes, which breaks NM parsing the file.

When I wrote a NetworkManager cookbook, I ended up having to manually generate the not-quite-INI text myself. You can imagine how impressed I was (still am) about having to do that when NM could have just used the proper INI format.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you give me an example to test? I had eap=peap; and dropped it to eap=peap and it worked fine.

Copy link
Collaborator Author

@jaymzh jaymzh May 14, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jgoguen I don't think the trailing semicolon is necessary, and here's some data:

[phil@ldx-vulturus ~]$ sudo grep ^eap /etc/NetworkManager/system-connections/fb_networkmanager_vicarious_wifi_hq
eap=peap
[phil@ldx-vulturus ~]$ nmcli c show 6f1f4851-15a3-321f-beec-fbfc873c9f85 | grep 802-1x.eap
802-1x.eap:                             peap

vs

root@ldx-vulturus:~# sudo grep ^eap /etc/NetworkManager/system-connections/fb_networkmanager_vicarious_wifi_hq
eap=peap;
root@ldx-vulturus:~# nmcli c reload
root@ldx-vulturus:~# nmcli c show 6f1f4851-15a3-321f-beec-fbfc873c9f85 | grep 802-1x.eap
802-1x.eap:                             peap

They're identical.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting, what version of NetworkManager? I have 1.20 and even for connections manually created fresh it's still forcing semicolon-separate lists with a trailing semicolon.

I have 1.22 on another laptop, I can check what it does later tonight.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1.10.6

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder then if this INI-incompatible change is the new format. I created new connections on both 1.20 and 1.22, both forced trailing semicolons for multi-value fiends. It would work if I remove the trailing semicolon, if I restart NetworkManager itself not just reload connections, but as soon as anything modified the connection file it put the trailing semicolons back.

Where it uses semicolons as list item separators, I wonder if IniParse would see the first value and then discard the rest treating it as a comment…

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

certainly it does like to add them back. I just didn't find any problem to not having them.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, I'm going to assume there was something borked on my system. So as long as this can output semicolon-delimited lists for the connection files and comma-delimited lists for NetworkManager.conf it's good.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doesn't today. I'll add that and tests. Thanks!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh huh, I didn't get to this... I'll come back to this

section.option(opt, v)
end
end
end
end.to_s
end
end
end
Loading