Introduce host registration

This patch adds a new Puppet custom type to let puppet deployers to
register one host to a MidoNet tunnel zone:

  midonet_host_registration {'host1'
    midonet_api_endpoint => '..',
    username             => '..',
    password             => '..'
  }

User and password are the credentials of the user admin who performs the
action.

If the tunnel zone (tzone0 by default) is not informed, it will create
it.

Change-Id: I5b2fbce9c75aa949053b11f35d4ca239507e950b
This commit is contained in:
Jaume Devesa 2015-06-12 16:17:33 +02:00
parent d7ee219e48
commit 66e78c3509
10 changed files with 723 additions and 11 deletions

View File

@ -16,6 +16,7 @@ group :development, :unit_tests do
gem 'puppetlabs_spec_helper', '>= 0.1.0', :require => false
gem 'puppet-lint', '>= 0.3.2', :require => false
gem 'metadata-json-lint', :require => false
gem 'faraday', :require => false
end
group :system_tests do

View File

@ -18,5 +18,4 @@ Puppet::Type.type(:midonet_client_conf).provide(
def file_path
'/root/.midonetrc'
end
end

View File

@ -0,0 +1,191 @@
require 'uri'
require 'faraday'
# Host registry type
Puppet::Type.type(:midonet_host_registry).provide(:midonet_api_caller, parent: Puppet::Provider) do
def create
define_connection(resource[:midonet_api_url])
tz = call_get_tunnelzone()
if tz.empty?
# Tunnel zone does not exist. It should. Then
# create a tunnelzone with current values. Note
# the exists? applies at the host in a given
# tunnelzone, so it is fair to create a tunnelzone
message = Hash.new
message['name'] = resource[:tunnelzone_name]
message['type'] = resource[:tunnelzone_type]
tz = call_create_tunnelzone(message)
tz_id = tz[0]['id']
else
tz_type = tz[0]['type']
if tz_type != resource[:tunnelzone_type].to_s
raise "Tunnel zone already exists in type #{tz[0]['type']} whereas you are associating a host in a type #{resource[:tunnelzone_type]}"
else
tz_id = tz[0]['id']
end
end
host = call_get_host()
if host.empty?
raise 'Midonet agent does not run on the host you are trying to register'
else
host_id = host[0]['id']
end
host_id = host[0]['id']
message = Hash.new
message['hostId'] = "#{host_id}"
message['ipAddress'] = "#{resource[:underlay_ip_address]}"
call_create_tunnelzone_host(tz_id, message)
end
def destroy
define_connection(resource[:midonet_api_url])
tz = call_get_tunnelzone()
if tz.empty?
return
end
tz_id = tz[0]['id']
host = call_get_host()
if host.empty?
return
end
host_id = host[0]['id']
reg_host = call_get_tunnelzone_host(tz_id, host_id)
if reg_host.empty?
return
end
# Delete host from tunnelzone
call_delete_tunnelzone_host(tz_id, host_id)
# We can delete the tunnelzone if no host registered left
if call_get_tunnelzone_hosts(tz_id).empty?
call_delete_tunnelzone(tz_id)
end
end
def exists?
define_connection(resource[:midonet_api_url])
tz = call_get_tunnelzone()
if tz.empty?
return false
end
tz_id = tz[0]['id']
host = call_get_host()
if host.empty?
return false
end
host_id = host[0]['id']
reg_host = call_get_tunnelzone_host(tz_id, host_id)
if reg_host.empty?
return false
end
return true
end
def define_connection(url)
@connection = Faraday.new(url: url,
ssl: { verify: false }) do |builder|
builder.request :retry, {
:max => 5,
:interval => 0.05,
:exceptions => [
Faraday::Error::TimeoutError,
Faraday::ConnectionFailed,
Errno::ETIMEDOUT,
'Timeout::Error',
],
}
builder.use Faraday::Request::BasicAuthentication, resource[:username], resource[:password]
builder.adapter :net_http
end
end
def call_get_tunnelzone()
res = @connection.get do |req|
req.url "/midonet-api/tunnel_zones"
end
output = JSON.parse(res.body)
return output.select{ |tz| tz['name'] == resource[:tunnelzone_name].to_s }
end
def call_get_host()
res = @connection.get do |req|
req.url "/midonet-api/hosts"
end
output = JSON.parse(res.body)
return output.select{ |host| host['name'] == resource[:hostname].to_s }
end
def call_create_tunnelzone(message)
res = @connection.post do |req|
req.url "/midonet-api/tunnel_zones"
req.headers['Content-Type'] = "application/vnd.org.midonet.TunnelZone-v1+json"
req.body = message.to_json
end
return call_get_tunnelzone()
end
def call_create_tunnelzone_host(tz_id, message)
res = @connection.post do |req|
req.url "/midonet-api/tunnel_zones/#{tz_id}/hosts"
req.headers['Content-Type'] = "application/vnd.org.midonet.TunnelZoneHost-v1+json"
req.body = message.to_json
end
end
def call_get_tunnelzone_hosts(tz_id)
res = @connection.get do |req|
req.url "/midonet-api/tunnel_zones/#{tz_id}/hosts"
end
return JSON.parse(res.body)
end
def call_get_tunnelzone_host(tz_id, host_id)
return call_get_tunnelzone_hosts(tz_id).select{ |host| host['id'] == host_id }
end
def call_delete_tunnelzone(tz_id)
res = @connection.delete do |req|
req.url "/midonet-api/tunnel_zones/#{tz_id}"
end
end
def call_delete_tunnelzone_host(tz_id, host_id)
res = @connection.delete do |req|
req.url "/midonet-api/tunnel_zones/#{tz_id}/hosts/#{host_id}"
end
end
private :call_create_tunnelzone,
:call_create_tunnelzone_host,
:call_delete_tunnelzone,
:call_delete_tunnelzone_host,
:call_get_host,
:call_get_tunnelzone,
:call_get_tunnelzone_host,
:call_get_tunnelzone_hosts,
:define_connection
end

View File

@ -0,0 +1,95 @@
require 'uri'
require 'facter'
Puppet::Type.newtype(:midonet_host_registry) do
@doc = %q{Register a Host to a MidoNet tunnel zone
through the MidoNet API
Example:
midonet_host_registry {'hostname':
$midonet_api_url => 'http://controller:8080',
$username => 'admin',
$password => 'admin',
$ip_address => '123.23.43.2'
}
}
ensurable
newparam(:hostname, :namevar => true) do
desc 'Hostname of the host that wants to register in MidoNet cloud'
# Regex obtained from StackOverflow question:
# http://stackoverflow.com/questions/1418423/the-hostname-regex
validate do |value|
unless value =~ /^(?=.{1,255}$)[0-9A-Za-z](?:(?:[0-9A-Za-z]|-){0,61}[0-9A-Za-z])?(?:\.[0-9A-Za-z](?:(?:[0-9A-Za-z]|-){0,61}[0-9A-Za-z])?)*\.?$/
raise ArgumentError, "'%s' is not a valid hostname" % value
end
end
end
newparam(:tunnelzone_name) do
desc 'Name of the tunnelzone. If it does not exist, it creates one'
defaultto :'tzone0'
validate do |value|
unless value =~ /\w+/
raise ArgumentError, "'%s' is not a valid tunnelzone name" % value
end
end
end
newparam(:tunnelzone_type) do
desc 'Network technology to use when creating the tunnel'
defaultto 'gre'
newvalues('gre', 'vxlan')
end
newparam(:midonet_api_url) do
desc 'MidoNet API endpoint to connect to'
validate do |value|
unless value =~ /\A#{URI::regexp(['http', 'https'])}\z/
raise ArgumentError, "'%s' is not a valid URI" % value
end
end
end
newparam(:username) do
desc 'Username of the admin user in keystone'
validate do |value|
unless value =~ /\w+/
raise ArgumentError, "'%s' is not a valid username" % value
end
end
end
newparam(:password) do
desc 'Password of the admin user in keystone'
validate do |value|
unless value =~ /\w+/
raise ArgumentError, "'%s' is not a valid password" % value
end
end
end
newparam(:tenant_name) do
desc 'Tenant name of the admin user'
defaultto :'admin'
validate do |value|
unless value =~ /\w+/
raise ArgumentError, "'%s' is not a tenant name" % value
end
end
end
newparam(:underlay_ip_address) do
desc "IP address that will be used to as the underlay layer to
create the tunnels. It will take the fact $ipaddress by
default"
defaultto Facter['ipaddress'].value
validate do |value|
unless value =~ /^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$/
raise ArgumentError, "'%s' is not a valid IPv4 address" % value
end
end
end
end

View File

@ -60,4 +60,20 @@ class midonet {
# Add midonet-cli
class {'midonet::midonet_cli':}
if ! defined(Package['faraday']) {
package { 'faraday':
ensure => present,
provider => 'gem',
before => Midonet_host_registry[$::hostname]
}
}
# Register the host
midonet_host_registry { $::hostname:
ensure => present,
midonet_api_url => 'http://127.0.0.1:8080',
username => 'admin',
password => 'admin',
require => Class['midonet::midonet_agent']
}
}

View File

@ -9,4 +9,15 @@ RSpec.configure do |c|
c.alias_it_should_behave_like_to :it_raises, 'raises'
c.module_path = File.join(fixture_path, 'modules')
c.manifest_dir = File.join(fixture_path, 'manifests')
c.mock_with :rspec do |mock_c|
mock_c = :expect
end
end
def make_site_pp(pp, path = File.join(master['puppetpath'], 'manifests'))
on master, "mkdir -p #{path}"
create_remote_file(master, File.join(path, "site.pp"), pp)
on master, "chown -R #{master['user']}:#{master['group']} #{path}"
on master, "chmod -R 0755 #{path}"
on master, "service #{master['puppetservice']} restart"
end

View File

@ -0,0 +1,248 @@
require 'spec_helper'
describe Puppet::Type.type(:midonet_host_registry).provider(:midonet_api_caller) do
let(:provider) { described_class.new(resource) }
let(:resource) { Puppet::Type.type(:midonet_host_registry).new(
{
:ensure => :present,
:hostname => 'compute.midonet',
:midonet_api_url => 'http://controller:8080',
:username => 'username',
:password => 'password',
:tunnelzone_name => 'tzone1',
:underlay_ip_address => '172.10.0.10'
}
)}
describe 'host registry happy path' do
# - Single tunnelzone zones
# - Host registered
# - Allow to be created, and deleted
# - Tunnel zone should be deleted after the host deletion
let(:tzones) {
[
{
"name" => "tzone1",
"id" => "bd69f96a-005b-4d58-9f6c-b8dd9fbb6339",
"type" => "gre"
}
]
}
let(:hosts) {
[
{
"id" => "04f7361c-4cb8-4cda-a50f-1744fd8b7851",
"name" => "compute.midonet"
}
]
}
before :each do
allow(provider).to receive(:call_get_tunnelzone).and_return(tzones)
allow(provider).to receive(:call_get_host).and_return(hosts)
allow(provider).to receive(:call_create_tunnelzone_host)
allow(provider).to receive(:call_get_tunnelzone_host).and_return(hosts)
allow(provider).to receive(:call_delete_tunnelzone_host)
allow(provider).to receive(:call_get_tunnelzone_hosts).and_return([])
allow(provider).to receive(:call_delete_tunnelzone)
end
it 'registers the host successfully' do
# Expectations over 'create' call
expect(provider).to receive(:call_create_tunnelzone_host).with(tzones[0]['id'], {'hostId' => '04f7361c-4cb8-4cda-a50f-1744fd8b7851', 'ipAddress' => '172.10.0.10'})
expect(provider).to receive(:call_get_tunnelzone)
expect(provider).not_to receive(:call_create_tunnelzone)
expect(provider).to receive(:call_get_host)
provider.create
end
it 'unregisters the host successfully' do
# Expectations over the 'destroy' call
expect(provider).to receive(:call_get_tunnelzone)
expect(provider).to receive(:call_get_host)
expect(provider).to receive(:call_get_tunnelzone_hosts).with(tzones[0]['id'])
expect(provider).to receive(:call_delete_tunnelzone)
provider.destroy
end
end
describe 'when no tunnelzones' do
let(:hosts) {
[
{
"id" => "04f7361c-4cb8-4cda-a50f-1744fd8b7851",
"name" => "compute.midonet"
}
]
}
let(:tzones) {
[
{
"name" => "tzone1",
"id" => "bd69f96a-005b-4d58-9f6c-b8dd9fbb6339",
"type" => "gre"
}
]
}
it 'creates the tunnelzone and the host' do
allow(provider).to receive(:call_get_tunnelzone).and_return([])
allow(provider).to receive(:call_create_tunnelzone).and_return(tzones)
allow(provider).to receive(:call_get_host).and_return(hosts)
allow(provider).to receive(:call_create_tunnelzone_host)
expect(provider).to receive(:call_create_tunnelzone).once
expect(provider.exists?).to eq false
provider.create
end
end
describe 'unregister not the last host in tunnelzone' do
let(:tzones) {
[
{
"name" => "tzone1",
"id" => "bd69f96a-005b-4d58-9f6c-b8dd9fbb6339",
"type" => "gre"
}
]
}
let(:host_to_unregister) {
[
{
"id" => "04f7361c-4cb8-4cda-a50f-1744fd8b7851",
"name" => "compute.midonet"
}
]
}
let(:host_left_in_tunnelzone) {
[
{
"id" => "04f7361c-4cb8-4cda-a50f-1744fd8b7852",
"name" => "compute2.midonet"
}
]
}
it 'should not call the tunnelzone deletion' do
# Preparing the rest responses
allow(provider).to receive(:call_get_tunnelzone).and_return(tzones)
allow(provider).to receive(:call_get_host).and_return(host_to_unregister)
allow(provider).to receive(:call_delete_tunnelzone_host)
allow(provider).to receive(:call_get_tunnelzone_host).and_return(host_to_unregister)
allow(provider).to receive(:call_get_tunnelzone_hosts).and_return(host_left_in_tunnelzone)
# Set the behaviour expectations
expect(provider).to receive(:call_delete_tunnelzone_host).with(tzones[0]['id'], host_to_unregister[0]['id'])
expect(provider).not_to receive(:call_delete_tunnelzone)
provider.destroy
end
end
describe 'try to register a host without midonet-agent' do
let(:tzones) {
[
{
"name" => "tzone1",
"id" => "bd69f96a-005b-4d58-9f6c-b8dd9fbb6339",
"type" => "gre"
}
]
}
it 'should raise an exception' do
# Preparing the rest responses
allow(provider).to receive(:call_get_tunnelzone).and_return(tzones)
allow(provider).to receive(:call_get_host).and_return([])
expect {
provider.create
}.to raise_error(RuntimeError)
end
end
describe 'try to register a host with wrong tunnelzone type' do
let(:tzones) {
[
{
"name" => "tzone1",
"id" => "bd69f96a-005b-4d58-9f6c-b8dd9fbb6339",
"type" => "vxlan" # Resource is 'gre' and current one is 'vxlan'
}
]
}
it 'should raise an exception' do
allow(provider).to receive(:call_get_tunnelzone).and_return(tzones)
expect {
provider.create
}.to raise_error(RuntimeError)
end
end
describe 'try to unregister a host that belongs to a tunnelzone that does not exist' do
it 'should not fail' do
allow(provider).to receive(:call_get_tunnelzone).and_return([])
expect(provider).not_to receive(:call_delete_tunnelzone_host)
provider.destroy
end
end
describe 'try to unregister a host that does not exist' do
let(:tzones) {
[
{
"name" => "tzone1",
"id" => "bd69f96a-005b-4d58-9f6c-b8dd9fbb6339",
"type" => "gre"
}
]
}
it 'should not fail' do
allow(provider).to receive(:call_get_tunnelzone).and_return(tzones)
allow(provider).to receive(:call_get_host).and_return([])
expect(provider).not_to receive(:call_delete_tunnelzone_host)
provider.destroy
end
end
describe 'try to unregister a host that does not belong to a tunnelzone' do
let(:tzones) {
[
{
"name" => "tzone1",
"id" => "bd69f96a-005b-4d58-9f6c-b8dd9fbb6339",
"type" => "gre"
}
]
}
let(:hosts) {
[
{
"id" => "04f7361c-4cb8-4cda-a50f-1744fd8b7851",
"name" => "compute.midonet"
}
]
}
it 'should not fail' do
allow(provider).to receive(:call_get_tunnelzone).and_return(tzones)
allow(provider).to receive(:call_get_host).and_return(hosts)
allow(provider).to receive(:call_get_tunnelzone_host).and_return([])
expect(provider).to receive(:call_get_tunnelzone).once
expect(provider).to receive(:call_get_host).once
expect(provider).to receive(:call_get_tunnelzone_host).once.with(tzones[0]['id'], hosts[0]['id'])
provider.destroy
end
end
end

View File

@ -0,0 +1,151 @@
require 'spec_helper'
require 'puppet'
require 'puppet/type/midonet_host_registry'
require 'facter'
describe Puppet::Type::type(:midonet_host_registry) do
context 'on default values' do
let(:resource) do
Puppet::Type.type(:midonet_host_registry).new(
:hostname => Facter['hostname'].value,
:midonet_api_url => 'http://87.23.43.2:8080/midonet-api',
:username => 'admin',
:password => 'admin')
end
it 'assign the default values' do
expect(resource[:tenant_name]).to eq :'admin'
expect(resource[:underlay_ip_address]).to eq Facter['ipaddress'].value
expect(resource[:tunnelzone_name]).to eq :'tzone0'
expect(resource[:tunnelzone_type]).to eq :'gre'
end
end
context 'on invalid hostname' do
it do
expect {
Puppet::Type.type(:midonet_host_registry).new(
:hostname => '_invalid_hostname.local',
:midonet_api_url => 'http://87.23.43.2:8080/midonet-api',
:username => 'admin',
:password => 'admin')
}.to raise_error(Puppet::ResourceError)
end
end
context 'on invalid api url' do
it do
expect {
Puppet::Type.type(:midonet_host_registry).new(
:hostname => Facter['hostname'].value,
:midonet_api_url => '87.23.43.2:8080/midonet-api',
:username => 'admin',
:password => 'admin')
}.to raise_error(Puppet::ResourceError)
end
end
context 'on tenant_name valid value' do
let(:resource) do
Puppet::Type.type(:midonet_host_registry).new(
:hostname => Facter['hostname'].value,
:midonet_api_url => 'http://87.23.43.2:8080/midonet-api',
:username => 'admin',
:password => 'admin',
:tenant_name => 'midokura')
end
it 'assign to it' do
expect(resource[:tenant_name]).to eq 'midokura'
end
end
context 'on tunnelzone valid name' do
let(:resource) do
Puppet::Type.type(:midonet_host_registry).new(
:hostname => Facter['hostname'].value,
:midonet_api_url => 'http://87.23.43.2:8080/midonet-api',
:username => 'admin',
:password => 'admin',
:tunnelzone_name => 'tzoneee')
end
it 'assign to it' do
expect(resource[:tunnelzone_name]).to eq 'tzoneee'
end
end
context 'on tunnelzone valid type gre' do
let(:resource) do
Puppet::Type.type(:midonet_host_registry).new(
:hostname => Facter['hostname'].value,
:midonet_api_url => 'http://87.23.43.2:8080/midonet-api',
:username => 'admin',
:password => 'admin',
:tunnelzone_type => 'gre')
end
it 'assign to it' do
expect(resource[:tunnelzone_type]).to eq :'gre'
end
end
context 'on tunnelzone valid type vxlan' do
let(:resource) do
Puppet::Type.type(:midonet_host_registry).new(
:hostname => Facter['hostname'].value,
:midonet_api_url => 'http://87.23.43.2:8080/midonet-api',
:username => 'admin',
:password => 'admin',
:tunnelzone_type => 'vxlan')
end
it 'assign to it' do
expect(resource[:tunnelzone_type]).to eq :'vxlan'
end
end
context 'on tunnelzone valid type foo' do
it do
expect {
Puppet::Type.type(:midonet_host_registry).new(
:hostname => Facter['hostname'].value,
:midonet_api_url => 'http://87.23.43.2:8080/midonet-api',
:username => 'admin',
:password => 'admin',
:tunnelzone_type => 'foo')
}.to raise_error(Puppet::ResourceError)
end
end
context 'on underlay_ip_address valid IP' do
let(:resource) do
Puppet::Type.type(:midonet_host_registry).new(
:hostname => Facter['hostname'].value,
:midonet_api_url => 'http://87.23.43.2:8080/midonet-api',
:username => 'admin',
:password => 'admin',
:underlay_ip_address => '76.3.176.129')
end
it 'assign it properly' do
expect(resource[:underlay_ip_address]).to eq '76.3.176.129'
end
end
context 'on underlay_ip_address invalid IP' do
it do
expect {
Puppet::Type.type(:midonet_host_registry).new(
:hostname => Facter['hostname'].value,
:midonet_api_url => 'http://87.23.43.2:8080/midonet-api',
:username => 'admin',
:password => 'admin',
:underlay_ip_address => '76.3.280.129')
}.to raise_error(Puppet::ResourceError)
end
end
end