- Allow full customization of ODL RPM repo
2017-11-17 Release 6.1.0
- Configure websocket address
+2017-01-02 Release 6.2.0
+- Adds SSL/TLS support
Websocket address can be configured to the IP of ODL rather than default 0.0.0.0. This IP will
be defined by `odl_bind_ip`.
+### Enabling TLS with OpenDaylight
+
+It is possible to enable TLS encrypted communication for OpenDaylight Northbound REST
+along with Southbound OVSDB/OpenFlow communication with Open vSwitch. To enable
+TLS, use the `enable_tls` flag. This option will create two keystores in OpenDaylight
+which are stored in '/opt/opendaylight/configuration/ssl'. The first keystore
+is the controller keystore, which will hold the private key and ODL certificate,
+along with the Certificate Authority (CA) certificate if provided. The second
+keystore is the trust keystore, which will hold the trusted OVS switch certificates.
+
+In order to enable TLS, it is required to provide the `tls_keystore_password`
+parameter. This represents the password to use for the controller and truststore
+keystores. With only providing these parameters, ODL will generate the
+controller keystore with a random private key and self-signed certficate.
+
+Additionally the `tls_key_file` and `tls_cert_file` parameters may be provided.
+These represent ODL's private key file and certificate file to be used when building
+the controller keystore. Optionally the `tls_ca_cert_file` may be provided which
+will chain the CA certificate to the keystore for client validation.
+
+`tls_trusted_certs` may be provided as an array of trusted certificates to be
+added to the trusted keystore. This allows OpenDaylight to identify trusted
+clients which may connect to ODL Southbound and Northbound.
+
## Reference
### Classes
--- /dev/null
+#********************************************************************************************
+# Boot Time Configuration *
+# Config knob changes will require controller restart *
+#********************************************************************************************
+
+#Ovsdb plugin's (OVS, HwVtep) support both active and passive connections. OVSDB library by
+#default listens on port 6640 for switch initiated connection. Please use following config
+#knob for changing this default port.
+ovsdb-listener-port = 6640
+
+#This flag will be enforced across all the connection's (passive and active) if set to true
+use-ssl = true
+
+#Set Json Rpc decoder max frame length value. If the OVSDB node contains large configurations
+#that can cause connection related issue while reading the configuration from the OVSDB node
+#database. Increasing the max frame lenge helps resolve the issue. Please see following bug
+#report for more details ( https://bugs.opendaylight.org/show_bug.cgi?id=2732 &
+#https://bugs.opendaylight.org/show_bug.cgi?id=2487). Default value set to 100000.
+json-rpc-decoder-max-frame-length = 100000
+
+
+#********************************************************************************************
+# Run Time Configuration *
+# Config knob changes doesn't require controller resart *
+#********************************************************************************************
+#Timeout value (in millisecond) after which OVSDB rpc task will be cancelled.Default value is
+#set to 1000ms, please uncomment and override the value if requires.Changing the value don't
+#require controller restart.
+ovsdb-rpc-task-timeout = 1000
--- /dev/null
+Puppet::Functions.create_function(:convert_cert_to_string) do
+ dispatch :convert_cert_to_string do
+ param 'String', :cert_file
+ end
+
+ def convert_cert_to_string(cert_file)
+ unless File.file?(cert_file)
+ raise puppet::ParseError, "Certificate file not found: #{cert_file}"
+ end
+ text=File.readlines(cert_file)
+ cert_string = ''
+ text.each do |line|
+ unless line.include? '-----'
+ cert_string += line.strip
+ end
+ end
+ return cert_string
+ end
+end
--- /dev/null
+Puppet::Type.type(:odl_keystore).provide(:jks) do
+ commands :keytool => 'keytool'
+
+ require 'fileutils'
+ require 'openssl'
+
+ def remove_p12_ks
+ keystore_dir = File.dirname(@resource[:keystore_path])
+ if File.file?("#{keystore_dir}/ctl.p12")
+ FileUtils.rm("#{keystore_dir}/ctl.p12")
+ end
+ end
+
+ def create
+ keystore_dir = File.dirname(@resource[:keystore_path])
+ unless File.directory?(keystore_dir)
+ FileUtils.mkdir_p(keystore_dir, :mode => 0755)
+ FileUtils.chown('odl', 'odl', keystore_dir)
+ end
+ # create p12 keystore
+ key = OpenSSL::PKey::RSA.new File.read(@resource[:key_file])
+ raw_cert = File.read(@resource[:cert_file])
+ certificate = OpenSSL::X509::Certificate.new(raw_cert)
+ if @resource[:ca_file]
+ p12_ks = OpenSSL::PKCS12.create(@resource[:password], @resource[:name], \
+ key, certificate, [@resource[:ca_file]])
+ else
+ p12_ks = OpenSSL::PKCS12.create(@resource[:password], @resource[:name], \
+ key, certificate)
+ end
+ open "#{keystore_dir}/ctl1.p12", 'w', 0644 do |io|
+ io.write p12_ks.to_der()
+ end
+ # convert to jks
+ keytool('-importkeystore', '-deststorepass', @resource[:password], \
+ '-destkeypass', @resource[:password], '-destkeystore', \
+ @resource[:keystore_path], '-srckeystore', "#{keystore_dir}/ctl1.p12", \
+ '-srcstoretype', 'PKCS12', '-srcstorepass', @resource[:password], \
+ '-alias', @resource[:name])
+ remove_p12_ks
+ unless File.file?(@resource[:keystore_path])
+ raise Puppet::Error, 'JKS keystore creation failed'
+ end
+ FileUtils.chown('odl', 'odl', @resource[:keystore_path])
+ end
+
+ def destroy
+ FileUtils.rm(@resource[:keystore_path])
+ end
+
+ def exists?
+ return File.file?(@resource[:keystore_path])
+ end
+
+ def key_file
+ return @resource[:key_file]
+ end
+
+ def key_file=(key_file)
+ destroy
+ create
+ end
+
+ def cert_file
+ return @resource[:cert_file]
+ end
+
+ def cert_file=(cert_file)
+ destroy
+ create
+ end
+
+ def ca_file
+ return @resource[:ca_file]
+ end
+
+ def ca_file=(ca_file)
+ destroy
+ create
+ end
+
+ def keystore_path
+ if exists?
+ return @resource[:keystore_path]
+ end
+ end
+
+ def keystore_path=(keystore_path)
+ destroy
+ create
+ end
+
+ def password
+ begin
+ keytool('-list', '-keystore', @resource[:keystore_path], '-storepass', \
+ @resource[:password])
+ return @resource[:password]
+ rescue
+ return false
+ end
+ end
+
+ def password=(password)
+ destroy
+ create
+ end
+end
--- /dev/null
+Puppet::Type.newtype(:odl_keystore) do
+
+ ensurable
+
+ newparam(:name, :namevar => true) do
+ desc "Name of the keystore"
+ newvalues(/^\w+$/)
+ end
+
+ newproperty(:ca_file) do
+ desc "CA authority certificate file path"
+ validate do |value|
+ if value
+ if !value.is_a?(String)
+ raise ArgumentError, "CA cert file path must be a string"
+ end
+ unless File.file?(value)
+ raise ArgumentError, "CA cert file not found: #{value}"
+ end
+ end
+ end
+ end
+
+ newproperty(:password) do
+ desc "Password for the keystore"
+ validate do |value|
+ if !value.is_a?(String)
+ raise ArgumentError, "Passwords must be a string"
+ end
+
+ if value.length < 6
+ raise ArgumentError, "Password must be at least 6 characters"
+ end
+ end
+
+ def change_to_s(current, desire)
+ "Keystore Password changed"
+ end
+ end
+
+ newproperty(:cert_file) do
+ desc "Certificate filepath"
+ validate do |value|
+ if !value.is_a?(String)
+ raise ArgumentError, "Certificate file path must be a string"
+ end
+ unless File.file?(value)
+ raise ArgumentError, "Certificate file not found: #{value}"
+ end
+ end
+ end
+
+ newproperty(:key_file) do
+ desc "Private key file path"
+ validate do |value|
+ if !value.is_a?(String)
+ raise ArgumentError, "Key file path must be a string"
+ end
+ unless File.file?(value)
+ raise ArgumentError, "Key file not found: #{value}"
+ end
+ end
+ end
+
+ newproperty(:keystore_path) do
+ desc "Filepath for the keystore"
+ defaultto '/opt/opendaylight/configuration/ssl/ctl.jks'
+ validate do |value|
+ if !value.is_a?(String)
+ raise ArgumentError, "Keystore file path must be a string"
+ end
+ end
+ end
+end
match => '^featuresBoot=.*$',
}
+ file { 'org.ops4j.pax.web.cfg':
+ ensure => file,
+ path => '/opt/opendaylight/etc/org.ops4j.pax.web.cfg',
+ # Set user:group owners
+ owner => 'odl',
+ group => 'odl',
+ }
+
+ $ha_node_count = count($::opendaylight::ha_node_ips)
+ if $::opendaylight::enable_ha and $ha_node_count < 2 {
+ fail("Number of HA nodes less than 2: ${ha_node_count} and HA Enabled")
+ }
+
# Configuration of ODL NB REST port to listen on
- augeas {'ODL REST Port':
- incl => '/opt/opendaylight/etc/jetty.xml',
- context => '/files/opt/opendaylight/etc/jetty.xml/Configure',
- lens => 'Xml.lns',
- changes => [
- "set Call[2]/Arg/New/Set[#attribute[name='port']]/Property/#attribute/default ${opendaylight::odl_rest_port}"]
+ if $opendaylight::enable_tls {
+
+ if $::opendaylight::tls_keystore_password == undef {
+ fail('Enabling TLS requires setting a TLS password for the ODL keystore')
+ }
+
+ if $::opendaylight::tls_key_file or $::opendaylight::tls_cert_file {
+ if $::opendaylight::tls_key_file and $::opendaylight::tls_cert_file {
+ odl_keystore { 'controller':
+ password => $::opendaylight::tls_keystore_password,
+ cert_file => $::opendaylight::tls_cert_file,
+ key_file => $::opendaylight::tls_key_file,
+ ca_file => $::opendaylight::tls_ca_cert_file,
+ require => File['/opt/opendaylight/configuration/ssl']
+ }
+ } else {
+ fail('Must specify both TLS key file path AND certificate file path')
+ }
+ }
+
+ augeas {'Remove HTTP ODL REST Port':
+ incl => '/opt/opendaylight/etc/jetty.xml',
+ context => '/files/opt/opendaylight/etc/jetty.xml/Configure',
+ lens => 'Xml.lns',
+ changes => ["rm Call[2]/Arg/New/Set[#attribute[name='port']]"]
+ }
+
+ augeas {'ODL SSL REST Port':
+ incl => '/opt/opendaylight/etc/jetty.xml',
+ context => '/files/opt/opendaylight/etc/jetty.xml/Configure',
+ lens => 'Xml.lns',
+ changes => ["set New[2]/Set[#attribute[name='securePort']]/Property/#attribute/default ${opendaylight::odl_rest_port}"]
+ }
+
+ file_line { 'set pax TLS port':
+ ensure => present,
+ path => '/opt/opendaylight/etc/org.ops4j.pax.web.cfg',
+ line => "org.osgi.service.http.port.secure = ${opendaylight::odl_rest_port}",
+ match => '^#?org.osgi.service.http.port.secure.*$',
+ require => File['org.ops4j.pax.web.cfg']
+ }
+
+ file_line { 'enable pax TLS':
+ ensure => present,
+ path => '/opt/opendaylight/etc/org.ops4j.pax.web.cfg',
+ line => 'org.osgi.service.http.secure.enabled = true',
+ match => '^#?org.osgi.service.http.secure.enabled.*$',
+ require => File['org.ops4j.pax.web.cfg']
+ }
+
+ file {'aaa-cert-config.xml':
+ ensure => file,
+ path => '/opt/opendaylight/etc/opendaylight/datastore/initial/config/aaa-cert-config.xml',
+ owner => 'odl',
+ group => 'odl',
+ content => template('opendaylight/aaa-cert-config.xml.erb'),
+ }
+
+ file_line {'set pax TLS keystore location':
+ ensure => present,
+ path => '/opt/opendaylight/etc/org.ops4j.pax.web.cfg',
+ line => 'org.ops4j.pax.web.ssl.keystore = configuration/ssl/ctl.jks',
+ match => '^#?org.ops4j.pax.web.ssl.keystore.*$',
+ require => File['org.ops4j.pax.web.cfg']
+ }
+ file_line {'set pax TLS keystore integrity password':
+ ensure => present,
+ path => '/opt/opendaylight/etc/org.ops4j.pax.web.cfg',
+ line => "org.ops4j.pax.web.ssl.password = ${opendaylight::tls_keystore_password}",
+ match => '^#?org.ops4j.pax.web.ssl.password.*$',
+ require => File['org.ops4j.pax.web.cfg']
+ }
+
+ file_line {'set pax TLS keystore password':
+ ensure => present,
+ path => '/opt/opendaylight/etc/org.ops4j.pax.web.cfg',
+ line => "org.ops4j.pax.web.ssl.keypassword = ${opendaylight::tls_keystore_password}",
+ match => '^#?org.ops4j.pax.web.ssl.keypassword.*$',
+ require => File['org.ops4j.pax.web.cfg']
+ }
+
+ # Enable TLS with OVSDB
+ file { 'org.opendaylight.ovsdb.library.cfg':
+ ensure => file,
+ path => '/opt/opendaylight/etc/org.opendaylight.ovsdb.library.cfg',
+ owner => 'odl',
+ group => 'odl',
+ source => 'puppet:///modules/opendaylight/org.opendaylight.ovsdb.library.cfg',
+ }
+
+ # Configure OpenFlow plugin to use TLS
+ $transport_protocol = 'TLS'
+ } else {
+ $transport_protocol = 'TCP'
+ augeas { 'ODL REST Port':
+ incl => '/opt/opendaylight/etc/jetty.xml',
+ context => '/files/opt/opendaylight/etc/jetty.xml/Configure',
+ lens => 'Xml.lns',
+ changes => [
+ "set Call[2]/Arg/New/Set[#attribute[name='port']]/Property/#attribute/default
+ ${opendaylight::odl_rest_port}"]
+ }
+ }
+ # Configure OpenFlow plugin to use TCP/TLS
+ file { 'default-openflow-connection-config.xml':
+ ensure => file,
+ path => '/opt/opendaylight/etc/opendaylight/datastore/initial/config/default-openflow-connection-config.xml',
+ # Set user:group owners
+ owner => 'odl',
+ group => 'odl',
+ content => template('opendaylight/default-openflow-connection-config.xml.erb'),
}
$initial_config_dir = '/opt/opendaylight/configuration/initial'
"set Call[2]/Arg/New/Set[#attribute[name='host']]/Property/#attribute/default ${opendaylight::odl_bind_ip}"]
}
- file { 'org.ops4j.pax.web.cfg':
- ensure => file,
- path => '/opt/opendaylight/etc/org.ops4j.pax.web.cfg',
- # Set user:group owners
- owner => 'odl',
- group => 'odl',
- }
- -> file_line { 'org.ops4j.pax.web.cfg':
- ensure => present,
- path => '/opt/opendaylight/etc/org.ops4j.pax.web.cfg',
- line => "org.ops4j.pax.web.listening.addresses = ${opendaylight::odl_bind_ip}"
+ file_line { 'set pax bind IP':
+ ensure => present,
+ path => '/opt/opendaylight/etc/org.ops4j.pax.web.cfg',
+ line => "org.ops4j.pax.web.listening.addresses = ${opendaylight::odl_bind_ip}",
+ require => File['org.ops4j.pax.web.cfg']
}
}
}
# Configure ODL HA if enabled
- $ha_node_count = count($::opendaylight::ha_node_ips)
if $::opendaylight::enable_ha {
- if $ha_node_count >= 2 {
- # Configure ODL OSVDB Clustering
-
- file {'akka.conf':
- ensure => file,
- path => "${initial_config_dir}/akka.conf",
- owner => 'odl',
- group => 'odl',
- content => template('opendaylight/akka.conf.erb'),
- require => File[$initial_config_dir]
- }
+ # Configure ODL OSVDB Clustering
- file {'modules.conf':
- ensure => file,
- path => "${initial_config_dir}/modules.conf",
- owner => 'odl',
- group => 'odl',
- content => template('opendaylight/modules.conf.erb'),
- require => File[$initial_config_dir]
- }
+ file {'akka.conf':
+ ensure => file,
+ path => "${initial_config_dir}/akka.conf",
+ owner => 'odl',
+ group => 'odl',
+ content => template('opendaylight/akka.conf.erb'),
+ require => File[$initial_config_dir]
+ }
- file {'module-shards.conf':
- ensure => file,
- path => "${initial_config_dir}/module-shards.conf",
- owner => 'odl',
- group => 'odl',
- content => template('opendaylight/module-shards.conf.erb'),
- require => File[$initial_config_dir]
- }
+ file {'modules.conf':
+ ensure => file,
+ path => "${initial_config_dir}/modules.conf",
+ owner => 'odl',
+ group => 'odl',
+ content => template('opendaylight/modules.conf.erb'),
+ require => File[$initial_config_dir]
+ }
- } else {
- fail("Number of HA nodes less than 2: ${ha_node_count} and HA Enabled")
+ file {'module-shards.conf':
+ ensure => file,
+ path => "${initial_config_dir}/module-shards.conf",
+ owner => 'odl',
+ group => 'odl',
+ content => template('opendaylight/module-shards.conf.erb'),
+ require => File[$initial_config_dir]
}
}
'/opt/opendaylight/etc/opendaylight/datastore',
'/opt/opendaylight/etc/opendaylight/datastore/initial',
'/opt/opendaylight/etc/opendaylight/datastore/initial/config',
+ '/opt/opendaylight/configuration/ssl'
]
file { $odl_dirs:
# Maxium number of OpenDaylight log rollovers to keep.
# [*snat_mechanism*]
# Sets the mechanism to be used for SNAT (conntrack, controller)
-#
+# [*enable_tls*]
+# (Boolean) Enables TLS for REST and OpenFlow/OVSDB with OpenDaylight.
+# Defaults to false
+# [*tls_keystore_password*]
+# TLS keystore password. Required when enabling TLS.
+# [*tls_trusted_certs*]
+# An array of cert files to be added to OpenDaylight's trusted keystore.
+# Optional. Defaults to None.
+# [*tls_key_file*]
+# Full path to a private key file to be used for OpenDaylight.
+# Optional. Defaults to undef. Requires setting tls_cert_file.
+# [*tls_cert_file*]
+# Full path to a public certificate file to be used for OpenDaylight.
+# Optional. Defaults to undef. Requires setting tls_key_file.
+# [*tls_ca_cert_file*]
+# Full path to a public CA authority certificate file which signed
+# OpenDaylight's certificate. Not needed if ODL certificate is self-signed.
+# Optional. Defaults to undef.
# === Deprecated Parameters
#
# [*ha_node_index*]
# Index of ha_node_ips for this node.
#
class opendaylight (
- $default_features = $::opendaylight::params::default_features,
- $extra_features = $::opendaylight::params::extra_features,
- $odl_rest_port = $::opendaylight::params::odl_rest_port,
- $odl_bind_ip = $::opendaylight::params::odl_bind_ip,
- $rpm_repo = $::opendaylight::params::rpm_repo,
- $deb_repo = $::opendaylight::params::deb_repo,
- $log_levels = $::opendaylight::params::log_levels,
- $enable_ha = $::opendaylight::params::enable_ha,
- $ha_node_ips = $::opendaylight::params::ha_node_ips,
- $ha_node_index = $::opendaylight::params::ha_node_index,
- $ha_db_modules = $::opendaylight::params::ha_db_modules,
- $vpp_routing_node = $::opendaylight::params::vpp_routing_node,
- $java_opts = $::opendaylight::params::java_opts,
- $manage_repositories = $::opendaylight::params::manage_repositories,
- $username = $::opendaylight::params::username,
- $password = $::opendaylight::params::password,
- $log_max_size = $::opendaylight::params::log_max_size,
- $log_max_rollover = $::opendaylight::params::log_max_rollover,
- $snat_mechanism = $::opendaylight::params::snat_mechanism
+ $default_features = $::opendaylight::params::default_features,
+ $extra_features = $::opendaylight::params::extra_features,
+ $odl_rest_port = $::opendaylight::params::odl_rest_port,
+ $odl_bind_ip = $::opendaylight::params::odl_bind_ip,
+ $rpm_repo = $::opendaylight::params::rpm_repo,
+ $deb_repo = $::opendaylight::params::deb_repo,
+ $log_levels = $::opendaylight::params::log_levels,
+ $enable_ha = $::opendaylight::params::enable_ha,
+ $ha_node_ips = $::opendaylight::params::ha_node_ips,
+ $ha_node_index = $::opendaylight::params::ha_node_index,
+ $ha_db_modules = $::opendaylight::params::ha_db_modules,
+ $vpp_routing_node = $::opendaylight::params::vpp_routing_node,
+ $java_opts = $::opendaylight::params::java_opts,
+ $manage_repositories = $::opendaylight::params::manage_repositories,
+ $username = $::opendaylight::params::username,
+ $password = $::opendaylight::params::password,
+ $log_max_size = $::opendaylight::params::log_max_size,
+ $log_max_rollover = $::opendaylight::params::log_max_rollover,
+ $snat_mechanism = $::opendaylight::params::snat_mechanism,
+ $enable_tls = $::opendaylight::params::enable_tls,
+ $tls_keystore_password = $::opendaylight::params::tls_keystore_password,
+ $tls_trusted_certs = $::opendaylight::params::tls_trusted_certs,
+ $tls_key_file = $::opendaylight::params::tls_key_file,
+ $tls_cert_file = $::opendaylight::params::tls_cert_file,
+ $tls_ca_cert_file = $::opendaylight::params::tls_ca_cert_file
) inherits ::opendaylight::params {
# Validate OS family
class { '::opendaylight::install': }
-> class { '::opendaylight::config': }
~> class { '::opendaylight::service': }
+ -> class { '::opendaylight::post_config': }
-> Class['::opendaylight']
}
$log_max_size = '10GB'
$log_max_rollover = 2
$snat_mechanism = 'controller'
+ $enable_tls = false
+ $tls_keystore_password = undef
+ $tls_key_file = undef
+ $tls_cert_file = undef
+ $tls_ca_cert_file = undef
+ $tls_trusted_certs = []
}
--- /dev/null
+# == Class opendaylight::post_config
+#
+# This class handles ODL config changes after ODL has come up.
+# These configuration changes do not require restart of ODL.
+# It's called from the opendaylight class.
+#
+class opendaylight::post_config {
+ # Add trusted certs to ODL keystore
+ $curl_post = "curl -k -X POST -o /dev/null --fail --silent -H 'Content-Type: application/json' -H 'Cache-Control: no-cache'"
+ $cert_rest_url = "https://${opendaylight::odl_bind_ip}:${opendaylight::odl_rest_port}/restconf/operations/aaa-cert-rpc:setNodeCertifcate"
+ if $opendaylight::enable_tls {
+ if !empty($opendaylight::tls_trusted_certs) {
+ $opendaylight::tls_trusted_certs.each |$idx, $cert| {
+ $cert_data = convert_cert_to_string($cert)
+ $rest_data = @("END":json/L)
+ {\
+ "aaa-cert-rpc:input": {\
+ "aaa-cert-rpc:node-alias": "node${idx}",\
+ "aaa-cert-rpc:node-cert": "${cert_data}"\
+ }\
+ }
+ |-END
+
+ exec { "Add trusted cert: ${cert}":
+ command => "${curl_post} -u ${opendaylight::username}:${
+ opendaylight::password} -d '${rest_data}' ${cert_rest_url}",
+ tries => 5,
+ try_sleep => 30,
+ path => '/usr/sbin:/usr/bin:/sbin:/bin',
+ }
+ }
+ }
+ }
+}
{
"name": "opendaylight-opendaylight",
- "version": "6.1.0",
+ "version": "6.2.0",
"author": "Daniel Farrell",
"summary": "Puppet module that installs and configures the OpenDaylight SDN controller",
"license": "BSD-2-Clause",
websocket_address_validations(odl_bind_ip: '127.0.0.1')
end
end
+
+ describe 'testing enabling TLS' do
+ context 'with self-signed key/cert creation' do
+ install_odl(enable_tls: true, tls_keystore_password: '123456')
+
+ # Call specialized helper fn for TLS config validations
+ tls_validations(enable_tls: true, tls_keystore_password: '123456')
+ end
+ end
end
# SNAT Mechanism tests
describe 'SNAT mechanism tests' do
- # Non-OS-type tests assume CentO
+ # Non-OS-type tests assume CentOS 7
# See issue #43 for reasoning:
# https://github.com/dfarrell07/puppet-opendaylight/issues/43#issue-57343159
osfamily = 'RedHat'
# SFC tests
describe 'SFC tests' do
- # Non-OS-type tests assume CentO
+ # Non-OS-type tests assume CentOS 7
# See issue #43 for reasoning:
# https://github.com/dfarrell07/puppet-opendaylight/issues/43#issue-57343159
osfamily = 'RedHat'
# websocket address tests
describe 'ODL websocket address tests' do
- # Non-OS-type tests assume CentO
+ # Non-OS-type tests assume CentOS 7
# See issue #43 for reasoning:
# https://github.com/dfarrell07/puppet-opendaylight/issues/43#issue-57343159
osfamily = 'RedHat'
odl_websocket_address_tests(odl_bind_ip: '127.0.0.1')
end
end
+
+ # TLS tests
+ describe 'ODL TLS tests' do
+ # Non-OS-type tests assume CentOS 7
+ # See issue #43 for reasoning:
+ # https://github.com/dfarrell07/puppet-opendaylight/issues/43#issue-57343159
+ osfamily = 'RedHat'
+ operatingsystem = 'CentOS'
+ operatingsystemmajrelease = '7'
+ context 'enabling TLS without required keystore password (negative test)' do
+ let(:facts) {{
+ :osfamily => osfamily,
+ :operatingsystem => operatingsystem,
+ :operatingsystemmajrelease => operatingsystemmajrelease,
+ }}
+
+ let(:params) {{
+ :enable_tls => :true
+ }}
+
+ # Run test that specialize in checking TLS
+ # Note that this function is defined in spec_helper
+ odl_tls_tests(enable_tls:true)
+ end
+ context 'enabling TLS with required params' do
+ let(:facts) {{
+ :osfamily => osfamily,
+ :operatingsystem => operatingsystem,
+ :operatingsystemmajrelease => operatingsystemmajrelease,
+ }}
+
+ let(:params) {{
+ :enable_tls => true,
+ :tls_keystore_password => '123456',
+ }}
+
+ # Run shared tests applicable to all supported OSs
+ # Note that this function is defined in spec_helper
+ generic_tests
+
+ # Run test that specialize in checking TLS
+ # Note that this function is defined in spec_helper
+ odl_tls_tests(enable_tls:true, tls_keystore_password:'123456')
+ end
+ end
end
it { should contain_class('opendaylight::params') }
it { should contain_class('opendaylight::install') }
it { should contain_class('opendaylight::config') }
+ it { should contain_class('opendaylight::post_config') }
it { should contain_class('opendaylight::service') }
# Confirm relationships between classes
it { should contain_class('opendaylight::config').that_notifies('Class[opendaylight::service]') }
it { should contain_class('opendaylight::service').that_subscribes_to('Class[opendaylight::config]') }
it { should contain_class('opendaylight::service').that_comes_before('Class[opendaylight]') }
+ it { should contain_class('opendaylight::post_config').that_requires('Class[opendaylight::service]') }
+ it { should contain_class('opendaylight::post_config').that_comes_before('Class[opendaylight]') }
it { should contain_class('opendaylight').that_requires('Class[opendaylight::service]') }
# Confirm presence of generic resources
if not odl_bind_ip.eql? '0.0.0.0'
it {
should contain_augeas('ODL REST IP')
- should contain_file_line('org.ops4j.pax.web.cfg').with(
- 'path' => '/opt/opendaylight/etc/org.ops4j.pax.web.cfg',
- 'line' => "org.ops4j.pax.web.listening.addresses = #{odl_bind_ip}",
+ should contain_file_line('set pax bind IP').with(
+ 'ensure' => 'present',
+ 'path' => '/opt/opendaylight/etc/org.ops4j.pax.web.cfg',
+ 'line' => "org.ops4j.pax.web.listening.addresses = #{odl_bind_ip}",
+ 'require' => 'File[org.ops4j.pax.web.cfg]'
)
}
else
)
}
end
+
+def odl_tls_tests(options = {})
+ enable_tls = options.fetch(:enable_tls, false)
+ tls_keystore_password = options.fetch(:tls_keystore_password, nil)
+ tls_trusted_certs = options.fetch(:tls_trusted_certs, [])
+ tls_keystore_password = options.fetch(:tls_keystore_password, nil)
+ tls_key_file = options.fetch(:tls_key_file, nil)
+ tls_cert_file = options.fetch(:tls_cert_file, nil)
+ tls_ca_cert_file = options.fetch(:tls_ca_cert_file, nil)
+ odl_rest_port = options.fetch(:odl_rest_port, 8080)
+
+ if enable_tls
+ if tls_keystore_password.nil?
+ it { expect { should contain_class('opendaylight::config') }.to raise_error(Puppet::PreformattedError) }
+ return
+ end
+
+ if tls_key_file or tls_cert_file
+ if tls_key_file and tls_cert_file
+ it {
+ should contain_odl_keystore('controller')
+ }
+ else
+ it { expect { should contain_class('opendaylight::config') }.to raise_error(Puppet::PreformattedError) }
+ end
+ end
+ it {
+ should contain_augeas('Remove HTTP ODL REST Port')
+ should contain_augeas('ODL SSL REST Port')
+ should contain_file_line('set pax TLS port').with(
+ 'path' => '/opt/opendaylight/etc/org.ops4j.pax.web.cfg',
+ 'line' => "org.osgi.service.http.port.secure = #{odl_rest_port}",
+ 'match' => '^#?org.osgi.service.http.port.secure.*$',
+ )
+ should contain_file_line('set pax TLS keystore location').with(
+ 'path' => '/opt/opendaylight/etc/org.ops4j.pax.web.cfg',
+ 'line' => 'org.ops4j.pax.web.ssl.keystore = configuration/ssl/ctl.jks',
+ 'match' => '^#?org.ops4j.pax.web.ssl.keystore.*$',
+ )
+ should contain_file_line('set pax TLS keystore integrity password').with(
+ 'path' => '/opt/opendaylight/etc/org.ops4j.pax.web.cfg',
+ 'line' => "org.ops4j.pax.web.ssl.password = #{tls_keystore_password}",
+ 'match' => '^#?org.ops4j.pax.web.ssl.password.*$',
+ )
+ should contain_file_line('set pax TLS keystore password').with(
+ 'path' => '/opt/opendaylight/etc/org.ops4j.pax.web.cfg',
+ 'line' => "org.ops4j.pax.web.ssl.keypassword = #{tls_keystore_password}",
+ 'match' => '^#?org.ops4j.pax.web.ssl.keypassword.*$',
+ )
+ should contain_file('aaa-cert-config.xml').with(
+ 'ensure' => 'file',
+ 'path' => '/opt/opendaylight/etc/opendaylight/datastore/initial/config/aaa-cert-config.xml',
+ 'owner' => 'odl',
+ 'group' => 'odl',
+ )
+ should contain_file('org.opendaylight.ovsdb.library.cfg').with(
+ 'ensure' => 'file',
+ 'path' => '/opt/opendaylight/etc/org.opendaylight.ovsdb.library.cfg',
+ 'owner' => 'odl',
+ 'group' => 'odl',
+ 'source' => 'puppet:///modules/opendaylight/org.opendaylight.ovsdb.library.cfg'
+ )
+ should contain_file('/opt/opendaylight/configuration/ssl').with(
+ 'ensure' => 'directory',
+ 'path' => '/opt/opendaylight/configuration/ssl',
+ 'owner' => 'odl',
+ 'group' => 'odl',
+ 'mode' => '0755'
+ )
+ should contain_file_line('enable pax TLS').with(
+ 'ensure' => 'present',
+ 'path' => '/opt/opendaylight/etc/org.ops4j.pax.web.cfg',
+ 'line' => 'org.osgi.service.http.secure.enabled = true',
+ 'match' => '^#?org.osgi.service.http.secure.enabled.*$',
+ )
+ should contain_file('org.ops4j.pax.web.cfg').with(
+ 'ensure' => 'file',
+ 'path' => '/opt/opendaylight/etc/org.ops4j.pax.web.cfg',
+ 'owner' => 'odl',
+ 'group' => 'odl',
+ )
+ should contain_file('default-openflow-connection-config.xml').with(
+ 'ensure' => 'file',
+ 'path' => '/opt/opendaylight/etc/opendaylight/datastore/initial/config/default-openflow-connection-config.xml',
+ 'owner' => 'odl',
+ 'group' => 'odl',
+ 'content' => /<transport-protocol>TLS<\/transport-protocol>/
+ )
+ }
+ end
+end
log_max_size = options.fetch(:log_max_size, '10GB')
log_max_rollover = options.fetch(:log_max_rollover, 2)
snat_mechanism = options.fetch(:snat_mechanism, 'controller')
+ enable_tls = options.fetch(:enable_tls, false)
+ tls_keystore_password = options.fetch(:tls_keystore_password, 'dummypass')
# Build script for consumption by Puppet apply
it 'should work idempotently with no errors' do
log_max_size => '#{log_max_size}',
log_max_rollover => #{log_max_rollover},
snat_mechanism => #{snat_mechanism},
+ enable_tls => #{enable_tls},
+ tls_keystore_password => #{tls_keystore_password},
}
EOS
its(:content) { should match /<websocket-address>#{odl_bind_ip}<\/websocket-address>/ }
end
end
+
+def tls_validations(options = {})
+ # NB: This param default should match the one used by the opendaylight
+ # class, which is defined in opendaylight::params
+ # TODO: Remove this possible source of bugs^^
+ tls_keystore_password = options.fetch(:tls_keystore_password)
+ odl_rest_port = options.fetch(:odl_rest_port, 8080)
+
+ describe file('/opt/opendaylight/etc/org.ops4j.pax.web.cfg') do
+ it { should be_file }
+ it { should be_owned_by 'odl' }
+ it { should be_grouped_into 'odl' }
+ its(:content) { should match /org.osgi.service.http.port.secure = #{odl_rest_port}/ }
+ its(:content) { should match /org.ops4j.pax.web.ssl.keystore = configuration\/ssl\/ctl.jks/ }
+ its(:content) { should match /org.ops4j.pax.web.ssl.password = #{tls_keystore_password}/ }
+ its(:content) { should match /org.ops4j.pax.web.ssl.keypassword = #{tls_keystore_password}/ }
+ its(:content) { should match /org.osgi.service.http.secure.enabled = true/ }
+ end
+
+ describe file('/opt/opendaylight/etc/org.opendaylight.ovsdb.library.cfg') do
+ it { should be_file }
+ it { should be_owned_by 'odl' }
+ it { should be_grouped_into 'odl' }
+ its(:content) { should match /use-ssl = true/ }
+ end
+
+ describe file('/opt/opendaylight/etc/opendaylight/datastore/initial/config/default-openflow-connection-config.xml') do
+ it { should be_file }
+ it { should be_owned_by 'odl' }
+ it { should be_grouped_into 'odl' }
+ its(:content) { should match /<keystore-password>#{tls_keystore_password}<\/keystore-password>/ }
+ its(:content) { should match /<truststore-password>#{tls_keystore_password}<\/truststore-password>/ }
+ its(:content) { should match /<transport-protocol>TLS<\/transport-protocol>/ }
+ end
+
+ describe file('/opt/opendaylight/etc/opendaylight/datastore/initial/config/aaa-cert-config.xml') do
+ it { should be_file }
+ it { should be_owned_by 'odl' }
+ it { should be_grouped_into 'odl' }
+ its(:content) { should match /<store-password>#{tls_keystore_password}<\/store-password>/ }
+ its(:content) { should match /<use-mdsal>false<\/use-mdsal>/ }
+ end
+
+ describe file('/opt/opendaylight/etc/jetty.xml') do
+ it { should be_file }
+ it { should be_owned_by 'odl' }
+ it { should be_grouped_into 'odl' }
+ its(:content) { should match /<Property name="jetty.secure.port" default="#{odl_rest_port}" \/>/ }
+ end
+end
--- /dev/null
+require 'puppet'
+require 'puppet/provider/odl_keystore/jks'
+require 'spec_helper'
+require 'fileutils'
+
+File.any_instance.stubs(:file?).returns(true)
+
+provider_class = Puppet::Type.type(:odl_keystore).provider(:jks)
+
+describe 'Puppet::Type.type(:odl_keystore).provider(:jks)' do
+
+ before(:all) do
+ FileUtils.stubs(:mkdir_p).returns(true)
+ FileUtils.stubs(:chown)
+ FileUtils.stubs(:rm)
+ end
+
+ let :odl_attrs do
+ {
+ :name => 'controller',
+ :ensure => 'present',
+ :password => 'dummypassword'
+ }
+ end
+
+ let :resource do
+ Puppet::Type::Odl_keystore.new(odl_attrs)
+ end
+
+ let :provider do
+ provider_class.new(resource)
+ end
+
+ describe "when changing cert_file" do
+ it 'should recreate keystore' do
+ File.stubs(:file?).returns(true)
+ provider.expects(:destroy)
+ provider.expects(:create)
+ provider.cert_file = '/tmp/blah.pem'
+ end
+ end
+
+ describe "when changing key_file" do
+ it 'should recreate keystore' do
+ File.stubs(:file?).returns(true)
+ provider.expects(:destroy)
+ provider.expects(:create)
+ provider.key_file = '/tmp/blah.pem'
+ end
+ end
+
+ describe "when adding a CA cert" do
+ it 'should recreate keystore' do
+ provider.expects(:destroy)
+ provider.expects(:create)
+ provider.ca_file = '/tmp/blah.pem'
+ end
+ end
+
+ describe "when keystore path" do
+ it 'should recreate keystore' do
+ provider.expects(:destroy)
+ provider.expects(:create)
+ provider.keystore_path = '/tmp/blah.jks'
+ end
+ end
+
+ describe "when changing password" do
+ it 'should change password' do
+ provider.expects(:destroy)
+ provider.expects(:create)
+ provider.password = 'admin123'
+ end
+ end
+end
--- /dev/null
+require 'puppet'
+require 'puppet/type/odl_keystore'
+require 'spec_helper'
+
+describe 'Puppet::Type.type(:odl_keystore)' do
+
+ it 'should accept name, password, tls options' do
+ File.stubs(:file?).returns(true)
+ Puppet::Type.type(:odl_keystore).new(
+ :name => 'admin',
+ :password => 'admin12345',
+ :cert_file => '/tmp/dummy.txt',
+ :key_file => '/tmp/dummy.txt',
+ :ca_file => '/tmp/dummy.txt')
+ end
+
+ it 'should fail with password less than 6 chars' do
+ File.stubs(:file?).returns(true)
+ expect{Puppet::Type.type(:odl_keystore).new(
+ :name => 'admin',
+ :password => 'admin',
+ :cert_file => '/tmp/dummy.txt',
+ :key_file => '/tmp/dummy.txt',
+ :ca_file => '/tmp/dummy.txt')}.to raise_error(Puppet::ResourceError)
+ end
+end
--- /dev/null
+<aaa-cert-service-config xmlns="urn:opendaylight:yang:aaa:cert">
+ <use-config>true</use-config>
+ <use-mdsal><%= scope.lookupvar('opendaylight::enable_ha') %></use-mdsal>
+ <bundle-name>opendaylight</bundle-name>
+ <ctlKeystore>
+ <name>ctl.jks</name>
+ <alias>controller</alias>
+ <store-password><%= scope.lookupvar('opendaylight::tls_keystore_password') %></store-password>
+ <dname>CN=ODL, OU=Dev, O=LinuxFoundation, L=QC Montreal, C=CA</dname>
+ <validity>365</validity>
+ <key-alg>RSA</key-alg>
+ <sign-alg>SHA1WithRSAEncryption</sign-alg>
+ <keysize>1024</keysize>
+ <tls-protocols />
+ <cipher-suites>
+ <suite-name />
+ </cipher-suites>
+ </ctlKeystore>
+ <trustKeystore>
+ <name>truststore.jks</name>
+ <store-password><%= scope.lookupvar('opendaylight::tls_keystore_password') %></store-password>
+ </trustKeystore>
+</aaa-cert-service-config>
--- /dev/null
+<switch-connection-config xmlns="urn:opendaylight:params:xml:ns:yang:openflow:switch:connection:config">
+ <instance-name>openflow-switch-connection-provider-default-impl</instance-name>
+ <port>6653</port>
+ <transport-protocol><%= scope.lookupvar('opendaylight::config::transport_protocol') %></transport-protocol>
+ <tls>
+ <keystore>configuration/ssl/ctl.jks</keystore>
+ <keystore-type>JKS</keystore-type>
+ <keystore-path-type>PATH</keystore-path-type>
+ <keystore-password><%= scope.lookupvar('opendaylight::tls_keystore_password') %></keystore-password>
+ <truststore>configuration/ssl/truststore.jks</truststore>
+ <truststore-type>JKS</truststore-type>
+ <truststore-path-type>PATH</truststore-path-type>
+ <truststore-password><%= scope.lookupvar('opendaylight::tls_keystore_password') %></truststore-password>
+ <certificate-password><%= scope.lookupvar('opendaylight::tls_keystore_password') %></certificate-password>
+ </tls>
+</switch-connection-config>