Use openstack cli to manage neutron routers
... and cleans up unused implementations from the base neutron provider. Closes-Bug: #1808317 Change-Id: I257d55be0acfecc529f86d5fe734c6c142f64e5f
This commit is contained in:
parent
541aa3ba43
commit
75ca125cfa
@ -13,7 +13,6 @@ class Puppet::Provider::Neutron < Puppet::Provider::Openstack
|
||||
extend Puppet::Provider::Openstack::Auth
|
||||
|
||||
initvars
|
||||
commands :neutron => 'neutron'
|
||||
|
||||
def self.request(service, action, properties=nil)
|
||||
begin
|
||||
@ -42,20 +41,6 @@ class Puppet::Provider::Neutron < Puppet::Provider::Openstack
|
||||
'/etc/neutron/neutron.conf'
|
||||
end
|
||||
|
||||
def self.withenv(hash, &block)
|
||||
saved = ENV.to_hash
|
||||
hash.each do |name, val|
|
||||
ENV[name.to_s] = val
|
||||
end
|
||||
|
||||
yield
|
||||
ensure
|
||||
ENV.clear
|
||||
saved.each do |name, val|
|
||||
ENV[name] = val
|
||||
end
|
||||
end
|
||||
|
||||
def self.neutron_conf
|
||||
return @neutron_conf if @neutron_conf
|
||||
@neutron_conf = Puppet::Util::IniConfig::File.new
|
||||
@ -113,58 +98,6 @@ class Puppet::Provider::Neutron < Puppet::Provider::Openstack
|
||||
@auth_endpoint ||= get_auth_endpoint
|
||||
end
|
||||
|
||||
def self.auth_neutron(*args)
|
||||
q = neutron_credentials
|
||||
authenv = {
|
||||
:OS_AUTH_URL => self.auth_endpoint,
|
||||
:OS_USERNAME => q['username'],
|
||||
:OS_PROJECT_NAME => q['project_name'],
|
||||
:OS_PASSWORD => q['password'],
|
||||
:OS_PROJECT_DOMAIN_NAME => q['project_domain_name'],
|
||||
:OS_USER_DOMAIN_NAME => q['user_domain_name']
|
||||
}
|
||||
if q.key?('region_name')
|
||||
authenv[:OS_REGION_NAME] = q['region_name']
|
||||
end
|
||||
rv = nil
|
||||
timeout = 10
|
||||
end_time = Time.now.to_i + timeout
|
||||
loop do
|
||||
begin
|
||||
withenv authenv do
|
||||
rv = neutron(args)
|
||||
end
|
||||
break
|
||||
rescue Puppet::ExecutionFailure => e
|
||||
if ! e.message =~ /(\(HTTP\s+400\))|
|
||||
(400-\{\'message\'\:\s+\'\'\})|
|
||||
(\[Errno 111\]\s+Connection\s+refused)|
|
||||
(503\s+Service\s+Unavailable)|
|
||||
(504\s+Gateway\s+Time-out)|
|
||||
(\:\s+Maximum\s+attempts\s+reached)|
|
||||
(Unauthorized\:\s+bad\s+credentials)|
|
||||
(Max\s+retries\s+exceeded)/
|
||||
raise(e)
|
||||
end
|
||||
current_time = Time.now.to_i
|
||||
if current_time > end_time
|
||||
break
|
||||
else
|
||||
wait = end_time - current_time
|
||||
notice("Unable to complete neutron request due to non-fatal error: \"#{e.message}\". Retrying for #{wait} sec.")
|
||||
end
|
||||
sleep(2)
|
||||
# Note(xarses): Don't remove, we know that there is one of the
|
||||
# Recoverable erros above, So we will retry a few more times
|
||||
end
|
||||
end
|
||||
return rv
|
||||
end
|
||||
|
||||
def auth_neutron(*args)
|
||||
self.class.auth_neutron(args)
|
||||
end
|
||||
|
||||
def self.reset
|
||||
@neutron_conf = nil
|
||||
@neutron_credentials = nil
|
||||
@ -194,82 +127,12 @@ class Puppet::Provider::Neutron < Puppet::Provider::Openstack
|
||||
end
|
||||
end
|
||||
|
||||
def self.list_neutron_resources(type)
|
||||
ids = []
|
||||
list = cleanup_csv_with_id(auth_neutron("#{type}-list", '--format=csv',
|
||||
'--column=id', '--quote=none'))
|
||||
if list.nil?
|
||||
raise(Puppet::ExecutionFailure, "Can't retrieve #{type}-list because Neutron or Keystone API is not available.")
|
||||
end
|
||||
|
||||
(list.split("\n")[1..-1] || []).compact.collect do |line|
|
||||
ids << line.strip
|
||||
end
|
||||
return ids
|
||||
end
|
||||
|
||||
def self.get_neutron_resource_attrs(type, id)
|
||||
attrs = {}
|
||||
net = auth_neutron("#{type}-show", '--format=shell', id)
|
||||
if net.nil?
|
||||
raise(Puppet::ExecutionFailure, "Can't retrieve #{type}-show because Neutron or Keystone API is not available.")
|
||||
end
|
||||
|
||||
last_key = nil
|
||||
(net.split("\n") || []).compact.collect do |line|
|
||||
if line.include? '='
|
||||
k, v = line.split('=', 2)
|
||||
attrs[k] = v.gsub(/\A"|"\Z/, '')
|
||||
last_key = k
|
||||
else
|
||||
# Handle the case of a list of values
|
||||
v = line.gsub(/\A"|"\Z/, '')
|
||||
attrs[last_key] = [attrs[last_key], v].flatten
|
||||
end
|
||||
end
|
||||
return attrs
|
||||
end
|
||||
|
||||
def self.get_tenant_id(catalog, name, domain='Default')
|
||||
instance_type = 'keystone_tenant'
|
||||
instance = catalog.resource("#{instance_type.capitalize!}[#{name}]")
|
||||
if ! instance
|
||||
instance = Puppet::Type.type(instance_type).instances.find do |i|
|
||||
# We need to check against the Default domain name because of
|
||||
# https://review.opendev.org/#/c/226919/ which changed the naming
|
||||
# format for the tenant to include <Domain name>. This should be
|
||||
# removed when we drop the resource without a domain name.
|
||||
# TODO(aschultz): remove ::domain lookup as part of M-cycle
|
||||
i.provider.name == name || i.provider.name == "#{name}::#{domain}"
|
||||
end
|
||||
end
|
||||
if instance
|
||||
return instance.provider.id
|
||||
def self.parse_availability_zone_hint(value)
|
||||
hints = JSON.parse(value.gsub(/\\"/,'"').gsub('u\'', '"').gsub('\'','"'))
|
||||
if hints.length > 1
|
||||
hints
|
||||
else
|
||||
fail("Unable to find #{instance_type} for name #{name}")
|
||||
hints.first
|
||||
end
|
||||
end
|
||||
|
||||
def self.parse_creation_output(data)
|
||||
hash = {}
|
||||
data.split("\n").compact.each do |line|
|
||||
if line.include? '='
|
||||
hash[line.split('=').first] = line.split('=', 2)[1].gsub(/\A"|"\Z/, '')
|
||||
end
|
||||
end
|
||||
hash
|
||||
end
|
||||
|
||||
def self.cleanup_csv(text)
|
||||
# Ignore warnings - assume legitimate output starts with a double quoted
|
||||
# string. Errors will be caught and raised prior to this
|
||||
text = text.split("\n").drop_while { |line| line !~ /^\".*\"/ }.join("\n")
|
||||
"#{text}\n"
|
||||
end
|
||||
|
||||
def self.cleanup_csv_with_id(text)
|
||||
return nil if text.nil?
|
||||
text = text.split("\n").drop_while { |line| line !~ /^\s*id$/ }.join("\n")
|
||||
"#{text}\n"
|
||||
end
|
||||
end
|
||||
|
@ -167,15 +167,6 @@ Puppet::Type.type(:neutron_network).provide(
|
||||
@property_hash[:ensure] = :absent
|
||||
end
|
||||
|
||||
def self.parse_availability_zone_hint(value)
|
||||
hints = JSON.parse(value.gsub(/\\"/,'"').gsub('u\'', '"').gsub('\'','"'))
|
||||
if hints.length > 1
|
||||
hints
|
||||
else
|
||||
hints.first
|
||||
end
|
||||
end
|
||||
|
||||
[
|
||||
:admin_state_up,
|
||||
:shared,
|
||||
|
@ -1,207 +0,0 @@
|
||||
require File.join(File.dirname(__FILE__), '..','..','..',
|
||||
'puppet/provider/neutron')
|
||||
|
||||
Puppet::Type.type(:neutron_router).provide(
|
||||
:neutron,
|
||||
:parent => Puppet::Provider::Neutron
|
||||
) do
|
||||
desc <<-EOT
|
||||
Neutron provider to manage neutron_router type.
|
||||
|
||||
Assumes that the neutron service is configured on the same host.
|
||||
EOT
|
||||
|
||||
mk_resource_methods
|
||||
|
||||
def self.do_not_manage
|
||||
@do_not_manage
|
||||
end
|
||||
|
||||
def self.do_not_manage=(value)
|
||||
@do_not_manage = value
|
||||
end
|
||||
|
||||
def self.instances
|
||||
self.do_not_manage = true
|
||||
list = list_neutron_resources('router').collect do |id|
|
||||
attrs = get_neutron_resource_attrs('router', id)
|
||||
new(
|
||||
:ensure => :present,
|
||||
:name => attrs['name'],
|
||||
:id => attrs['id'],
|
||||
:admin_state_up => attrs['admin_state_up'],
|
||||
:external_gateway_info => attrs['external_gateway_info'],
|
||||
:status => attrs['status'],
|
||||
:distributed => attrs['distributed'],
|
||||
:ha => attrs['ha'],
|
||||
:tenant_id => attrs['tenant_id'],
|
||||
:availability_zone_hint => attrs['availability_zone_hint']
|
||||
)
|
||||
end
|
||||
self.do_not_manage = false
|
||||
list
|
||||
end
|
||||
|
||||
def self.prefetch(resources)
|
||||
instances_ = instances
|
||||
resources.keys.each do |name|
|
||||
if provider = instances_.find{ |instance| instance.name == name }
|
||||
resources[name].provider = provider
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def exists?
|
||||
@property_hash[:ensure] == :present
|
||||
end
|
||||
|
||||
def create
|
||||
if self.class.do_not_manage
|
||||
fail("Not managing Neutron_router[#{@resource[:name]}] due to earlier Neutron API failures.")
|
||||
end
|
||||
|
||||
opts = Array.new
|
||||
|
||||
if @resource[:admin_state_up] == 'False'
|
||||
opts << '--admin-state-down'
|
||||
end
|
||||
|
||||
if @resource[:tenant_name]
|
||||
tenant_id = self.class.get_tenant_id(@resource.catalog,
|
||||
@resource[:tenant_name])
|
||||
opts << "--tenant_id=#{tenant_id}"
|
||||
elsif @resource[:tenant_id]
|
||||
opts << "--tenant_id=#{@resource[:tenant_id]}"
|
||||
end
|
||||
|
||||
if @resource[:distributed]
|
||||
opts << "--distributed=#{@resource[:distributed]}"
|
||||
end
|
||||
|
||||
if @resource[:ha]
|
||||
opts << "--ha=#{@resource[:ha]}"
|
||||
end
|
||||
|
||||
if @resource[:availability_zone_hint]
|
||||
opts << "--availability-zone-hint=#{@resource[:availability_zone_hint]}"
|
||||
end
|
||||
|
||||
results = auth_neutron("router-create", '--format=shell',
|
||||
opts, resource[:name])
|
||||
|
||||
attrs = self.class.parse_creation_output(results)
|
||||
@property_hash = {
|
||||
:ensure => :present,
|
||||
:name => resource[:name],
|
||||
:id => attrs['id'],
|
||||
:admin_state_up => attrs['admin_state_up'],
|
||||
:external_gateway_info => attrs['external_gateway_info'],
|
||||
:status => attrs['status'],
|
||||
:tenant_id => attrs['tenant_id'],
|
||||
:availability_zone_hint => attrs['availability_zone_hint']
|
||||
}
|
||||
|
||||
if @resource[:gateway_network_name]
|
||||
results = auth_neutron('router-gateway-set',
|
||||
@resource[:name],
|
||||
@resource[:gateway_network_name])
|
||||
attrs = self.class.get_neutron_resource_attrs('router',
|
||||
@resource[:name])
|
||||
@property_hash[:external_gateway_info] = \
|
||||
attrs['external_gateway_info']
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
if self.class.do_not_manage
|
||||
fail("Not managing Neutron_router[#{@resource[:name]}] due to earlier Neutron API failures.")
|
||||
end
|
||||
auth_neutron('router-delete', name)
|
||||
@property_hash[:ensure] = :absent
|
||||
end
|
||||
|
||||
def gateway_network_name
|
||||
if @gateway_network_name == nil and gateway_network_id
|
||||
Puppet::Type.type('neutron_network').instances.each do |instance|
|
||||
if instance.provider.id == gateway_network_id
|
||||
@gateway_network_name = instance.provider.name
|
||||
end
|
||||
end
|
||||
end
|
||||
@gateway_network_name
|
||||
end
|
||||
|
||||
def gateway_network_name=(value)
|
||||
if self.class.do_not_manage
|
||||
fail("Not managing Neutron_router[#{@resource[:name]}] due to earlier Neutron API failures.")
|
||||
end
|
||||
if value == ''
|
||||
auth_neutron('router-gateway-clear', name)
|
||||
else
|
||||
auth_neutron('router-gateway-set', name, value)
|
||||
end
|
||||
end
|
||||
|
||||
def parse_gateway_network_id(external_gateway_info_)
|
||||
match_data = /\{"network_id": "(.*?)"/.match(external_gateway_info_.gsub(/\\"/,'"'))
|
||||
if match_data
|
||||
match_data[1]
|
||||
else
|
||||
''
|
||||
end
|
||||
end
|
||||
|
||||
def gateway_network_id
|
||||
@gateway_network_id ||= parse_gateway_network_id(external_gateway_info)
|
||||
end
|
||||
|
||||
def admin_state_up=(value)
|
||||
if self.class.do_not_manage
|
||||
fail("Not managing Neutron_router[#{@resource[:name]}] due to earlier Neutron API failures.")
|
||||
end
|
||||
set_admin_state_up(value)
|
||||
end
|
||||
|
||||
def set_admin_state_up(value)
|
||||
auth_neutron('router-update', "--admin-state-up=#{value}", name)
|
||||
end
|
||||
|
||||
def distributed=(value)
|
||||
if self.class.do_not_manage
|
||||
fail("Not managing Neutron_router[#{@resource[:name]}] due to earlier Neutron API failures.")
|
||||
end
|
||||
results = auth_neutron("router-show", '--format=shell', resource[:name])
|
||||
attrs = self.class.parse_creation_output(results)
|
||||
set_admin_state_up(false)
|
||||
auth_neutron('router-update', "--distributed=#{value}", name)
|
||||
if attrs['admin_state_up'] == 'True'
|
||||
set_admin_state_up(true)
|
||||
end
|
||||
end
|
||||
|
||||
def ha=(value)
|
||||
if self.class.do_not_manage
|
||||
fail("Not managing Neutron_router[#{@resource[:name]}] due to earlier Neutron API failures.")
|
||||
end
|
||||
results = auth_neutron("router-show", '--format=shell', resource[:name])
|
||||
attrs = self.class.parse_creation_output(results)
|
||||
set_admin_state_up(false)
|
||||
auth_neutron('router-update', "--ha=#{value}", name)
|
||||
if attrs['admin_state_up'] == 'True'
|
||||
set_admin_state_up(true)
|
||||
end
|
||||
end
|
||||
|
||||
def availability_zone_hint=(value)
|
||||
if self.class.do_not_manage
|
||||
fail("Not managing Neutron_router[#{@resource[:name]}] due to earlier Neutron API failures.")
|
||||
end
|
||||
results = auth_neutron("router-show", '--format=shell', resource[:name])
|
||||
attrs = self.class.parse_creation_output(results)
|
||||
set_admin_state_up(false)
|
||||
auth_neutron('router-update', "--availability-zone-hint=#{value}", name)
|
||||
if attrs['admin_state_up'] == 'True'
|
||||
set_admin_state_up(true)
|
||||
end
|
||||
end
|
||||
end
|
218
lib/puppet/provider/neutron_router/openstack.rb
Normal file
218
lib/puppet/provider/neutron_router/openstack.rb
Normal file
@ -0,0 +1,218 @@
|
||||
require File.join(File.dirname(__FILE__), '..','..','..',
|
||||
'puppet/provider/neutron')
|
||||
|
||||
Puppet::Type.type(:neutron_router).provide(
|
||||
:openstack,
|
||||
:parent => Puppet::Provider::Neutron
|
||||
) do
|
||||
desc <<-EOT
|
||||
Neutron provider to manage neutron_router type.
|
||||
|
||||
Assumes that the neutron service is configured on the same host.
|
||||
EOT
|
||||
|
||||
@credentials = Puppet::Provider::Openstack::CredentialsV3.new
|
||||
|
||||
mk_resource_methods
|
||||
|
||||
def initialize(value={})
|
||||
super(value)
|
||||
@property_flush = {}
|
||||
end
|
||||
|
||||
def self.do_not_manage
|
||||
@do_not_manage
|
||||
end
|
||||
|
||||
def self.do_not_manage=(value)
|
||||
@do_not_manage = value
|
||||
end
|
||||
|
||||
def self.instances
|
||||
self.do_not_manage = true
|
||||
list = request('router', 'list').collect do |attrs|
|
||||
router = request('router', 'show', attrs[:id])
|
||||
new(
|
||||
:ensure => :present,
|
||||
:name => attrs[:name],
|
||||
:id => attrs[:id],
|
||||
:admin_state_up => router[:admin_state_up],
|
||||
:external_gateway_info => router[:external_gateway_info],
|
||||
:status => router[:status],
|
||||
:distributed => router[:distributed],
|
||||
:ha => router[:ha],
|
||||
:tenant_id => router[:tenant_id],
|
||||
:availability_zone_hint => parse_availability_zone_hint(router[:availability_zone_hints])
|
||||
)
|
||||
end
|
||||
self.do_not_manage = false
|
||||
list
|
||||
end
|
||||
|
||||
def self.prefetch(resources)
|
||||
routers = instances
|
||||
resources.keys.each do |name|
|
||||
if provider = routers.find{ |router| router.name == name }
|
||||
resources[name].provider = provider
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def exists?
|
||||
@property_hash[:ensure] == :present
|
||||
end
|
||||
|
||||
def create
|
||||
if self.class.do_not_manage
|
||||
fail("Not managing Neutron_router[#{@resource[:name]}] due to earlier Neutron API failures.")
|
||||
end
|
||||
|
||||
opts = [@resource[:name]]
|
||||
|
||||
if @resource[:admin_state_up] == 'False'
|
||||
opts << '--disable'
|
||||
end
|
||||
|
||||
if @resource[:tenant_name]
|
||||
opts << "--project=#{@resource[:tenant_name]}"
|
||||
elsif @resource[:tenant_id]
|
||||
opts << "--project=#{@resource[:tenant_id]}"
|
||||
end
|
||||
|
||||
if @resource[:distributed]
|
||||
if @resource[:distributed] == 'False'
|
||||
opts << '--centralized'
|
||||
else
|
||||
opts << '--distributed'
|
||||
end
|
||||
end
|
||||
|
||||
if @resource[:ha]
|
||||
if @resource[:ha] == 'False'
|
||||
opts << '--no-ha'
|
||||
else
|
||||
opts << '--ha'
|
||||
end
|
||||
end
|
||||
|
||||
if @resource[:availability_zone_hint]
|
||||
opts << \
|
||||
"--availability-zone-hint=#{@resource[:availability_zone_hint]}"
|
||||
end
|
||||
|
||||
router = self.class.request('router', 'create', opts)
|
||||
|
||||
if @resource[:gateway_network_id]
|
||||
self.class.request('router', 'set',
|
||||
[@resource[:name],
|
||||
"--external-gateway=#{@resource[:gateway_network_id]}"])
|
||||
router = self.class.request('router', 'show', [@resource[:name]])
|
||||
elsif @resource[:gateway_network_name]
|
||||
self.class.request('router', 'set',
|
||||
[@resource[:name],
|
||||
"--external-gateway=#{@resource[:gateway_network_name]}"])
|
||||
router = self.class.request('router', 'show', [@resource[:name]])
|
||||
end
|
||||
|
||||
@property_hash = {
|
||||
:ensure => :present,
|
||||
:name => router[:name],
|
||||
:id => router[:id],
|
||||
:admin_state_up => router[:admin_state_up],
|
||||
:external_gateway_info => router[:external_gateway_info],
|
||||
:status => router[:status],
|
||||
:distributed => router[:distributed],
|
||||
:ha => router[:ha],
|
||||
:tenant_id => router[:tenant_id],
|
||||
:availability_zone_hint => self.class.parse_availability_zone_hint(router[:availability_zone_hints])
|
||||
}
|
||||
end
|
||||
|
||||
def flush
|
||||
if !@property_flush.empty?
|
||||
opts = [@resource[:name]]
|
||||
clear_opts = [@resource[:name]]
|
||||
|
||||
if @property_flush.has_key?(:admin_state_up)
|
||||
if @property_flush[:admin_state_up] == 'False'
|
||||
opts << '--disable'
|
||||
else
|
||||
opts << '--enable'
|
||||
end
|
||||
end
|
||||
|
||||
if @property_flush.has_key?(:distributed)
|
||||
if @property_flush[:distributed] == 'False'
|
||||
opts << '--centralized'
|
||||
else
|
||||
opts << '--distributed'
|
||||
end
|
||||
end
|
||||
|
||||
if @property_flush.has_key?(:gateway_network_id)
|
||||
if @property_flush[:gateway_network_id] == ''
|
||||
clear_opts << '--external-gateway'
|
||||
else
|
||||
opts << "--external-gateway=#{@property_flush[:gateway_network_id]}"
|
||||
end
|
||||
elsif @property_flush.has_key?(:gateway_network_name)
|
||||
if @property_flush[:gateway_network_name] == ''
|
||||
clear_opts << '--external-gateway'
|
||||
else
|
||||
opts << "--external-gateway=#{@property_flush[:gateway_network_name]}"
|
||||
end
|
||||
end
|
||||
|
||||
if @property_flush.has_key?(:ha)
|
||||
if @property_flush[:ha] == 'False'
|
||||
opts << '--no-ha'
|
||||
else
|
||||
opts << '--ha'
|
||||
end
|
||||
end
|
||||
|
||||
if clear_opts.length > 1
|
||||
self.class.request('router', 'unset', clear_opts)
|
||||
end
|
||||
if opts.length > 1
|
||||
self.class.request('router', 'set', opts)
|
||||
end
|
||||
@property_flush.clear
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
if self.class.do_not_manage
|
||||
fail("Not managing Neutron_router[#{@resource[:name]}] due to earlier Neutron API failures.")
|
||||
end
|
||||
self.class.request('router', 'delete', @resource[:name])
|
||||
@property_flush.clear
|
||||
@property_flush[:ensure] = :absent
|
||||
end
|
||||
|
||||
[
|
||||
:admin_state_up,
|
||||
:gateway_network_id,
|
||||
:gateway_network_name,
|
||||
:distributed,
|
||||
:ha,
|
||||
].each do |attr|
|
||||
define_method(attr.to_s + "=") do |value|
|
||||
if self.class.do_not_manage
|
||||
fail("Not managing Neutron_router[#{@resource[:name]}] due to earlier Neutron API failures.")
|
||||
end
|
||||
@property_flush[attr] = value
|
||||
end
|
||||
end
|
||||
|
||||
[
|
||||
:availability_zone_hint,
|
||||
:tenant_id,
|
||||
:tenant_name,
|
||||
].each do |attr|
|
||||
define_method(attr.to_s + "=") do |value|
|
||||
fail("Property #{attr.to_s} does not support being updated")
|
||||
end
|
||||
end
|
||||
|
||||
end
|
@ -1,114 +0,0 @@
|
||||
require 'puppet'
|
||||
require 'spec_helper'
|
||||
require 'puppet/provider/neutron_router/neutron'
|
||||
|
||||
provider_class = Puppet::Type.type(:neutron_router).provider(:neutron)
|
||||
klass = Puppet::Provider::Neutron
|
||||
|
||||
describe provider_class do
|
||||
|
||||
let :router_name do
|
||||
'router1'
|
||||
end
|
||||
|
||||
let :router_attrs do
|
||||
{
|
||||
:name => router_name,
|
||||
:ensure => 'present',
|
||||
:admin_state_up => 'True',
|
||||
:distributed => 'True',
|
||||
:ha => 'False',
|
||||
:tenant_id => '60f9544eb94c42a6b7e8e98c2be981b1',
|
||||
:availability_zone_hint => 'zone1',
|
||||
}
|
||||
end
|
||||
|
||||
let :resource do
|
||||
Puppet::Type::Neutron_router.new(router_attrs)
|
||||
end
|
||||
|
||||
let :provider do
|
||||
provider_class.new(resource)
|
||||
end
|
||||
|
||||
describe 'when creating a router' do
|
||||
|
||||
it 'should call router-create with appropriate command line options' do
|
||||
provider.class.stubs(:get_tenant_id).returns(router_attrs[:tenant_id])
|
||||
|
||||
output = 'Created a new router:
|
||||
admin_state_up="True"
|
||||
external_gateway_info=""
|
||||
id="c5f799fa-b3e0-47ca-bdb7-abeff209b816"
|
||||
name="router1"
|
||||
status="ACTIVE"
|
||||
distributed="True"
|
||||
ha="False"
|
||||
tenant_id="60f9544eb94c42a6b7e8e98c2be981b1"
|
||||
availability-zone-hint="zone1"'
|
||||
|
||||
provider.expects(:auth_neutron).with('router-create',
|
||||
'--format=shell', ["--tenant_id=#{router_attrs[:tenant_id]}",
|
||||
"--distributed=#{router_attrs[:distributed]}",
|
||||
"--ha=#{router_attrs[:ha]}",
|
||||
"--availability-zone-hint=#{router_attrs[:availability_zone_hint]}"],
|
||||
router_name).returns(output)
|
||||
|
||||
provider.create
|
||||
end
|
||||
end
|
||||
|
||||
describe 'when updating a router' do
|
||||
|
||||
it 'should call router-update to change admin_state_up' do
|
||||
provider.expects(:auth_neutron).with('router-update',
|
||||
'--admin-state-up=False',
|
||||
router_name)
|
||||
provider.admin_state_up=('False')
|
||||
end
|
||||
|
||||
it 'should call router-gateway-clear for an empty network name' do
|
||||
provider.expects(:auth_neutron).with('router-gateway-clear',
|
||||
router_name)
|
||||
provider.gateway_network_name=('')
|
||||
end
|
||||
|
||||
it 'should call router-gateway-set to configure an external network' do
|
||||
provider.expects(:auth_neutron).with('router-gateway-set',
|
||||
router_name,
|
||||
'net1')
|
||||
provider.gateway_network_name=('net1')
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
describe 'when parsing an external gateway info' do
|
||||
let :resource do
|
||||
Puppet::Type::Neutron_router.new(router_attrs)
|
||||
end
|
||||
|
||||
let :provider do
|
||||
provider_class.new(resource)
|
||||
end
|
||||
|
||||
after :each do
|
||||
klass.reset
|
||||
end
|
||||
|
||||
it 'should detect a gateway net id' do
|
||||
klass.stubs(:auth_neutron).returns(
|
||||
'external_gateway_info="{\"network_id\": \"1b-b1\", \"enable_snat\": true, \"external_fixed_ips\": [{\"subnet_id\": \"1b-b1\", \"ip_address\": \"1.1.1.1\"}]}"'
|
||||
)
|
||||
result = klass.get_neutron_resource_attrs 'foo', nil
|
||||
expect(provider.parse_gateway_network_id(result['external_gateway_info'])).to eql('1b-b1')
|
||||
end
|
||||
|
||||
it 'should return empty value, if there is no net id found' do
|
||||
klass.stubs(:auth_neutron).returns('external_gateway_info="{}"')
|
||||
result = klass.get_neutron_resource_attrs 'foo', nil
|
||||
expect(provider.parse_gateway_network_id(result['external_gateway_info'])).to eql('')
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
352
spec/unit/provider/neutron_router/openstack_spec.rb
Normal file
352
spec/unit/provider/neutron_router/openstack_spec.rb
Normal file
@ -0,0 +1,352 @@
|
||||
require 'puppet'
|
||||
require 'spec_helper'
|
||||
require 'puppet/provider/neutron_router/openstack'
|
||||
|
||||
provider_class = Puppet::Type.type(:neutron_router).provider(:openstack)
|
||||
|
||||
describe provider_class do
|
||||
|
||||
let(:set_env) do
|
||||
ENV['OS_USERNAME'] = 'test'
|
||||
ENV['OS_PASSWORD'] = 'abc123'
|
||||
ENV['OS_PROJECT_NAME'] = 'test'
|
||||
ENV['OS_AUTH_URL'] = 'http://127.0.0.1:5000'
|
||||
end
|
||||
|
||||
describe 'manage routers' do
|
||||
let :router_name do
|
||||
'router1'
|
||||
end
|
||||
|
||||
let :router_attrs do
|
||||
{
|
||||
:name => router_name,
|
||||
:ensure => 'present',
|
||||
}
|
||||
end
|
||||
|
||||
let :resource do
|
||||
Puppet::Type::Neutron_router.new(router_attrs)
|
||||
end
|
||||
|
||||
let :provider do
|
||||
provider_class.new(resource)
|
||||
end
|
||||
|
||||
before :each do
|
||||
set_env
|
||||
end
|
||||
|
||||
describe '#create' do
|
||||
context 'with defaults' do
|
||||
it 'creates router' do
|
||||
provider_class.expects(:openstack)
|
||||
.with('router', 'create', '--format', 'shell',
|
||||
['router1'])
|
||||
.returns('admin_state_up="True"
|
||||
availability_zone_hints="[]"
|
||||
distributed="False"
|
||||
external_gateway_info="None"
|
||||
ha="True"
|
||||
id="d73f453a-77ca-4843-977a-3af0fda8dfcb"
|
||||
name="router1"
|
||||
project_id="60f9544eb94c42a6b7e8e98c2be981b1"
|
||||
project_id="60f9544eb94c42a6b7e8e98c2be981b1"
|
||||
status="ACTIVE"')
|
||||
provider.create
|
||||
expect(provider.exists?).to be_truthy
|
||||
expect(provider.admin_state_up).to eq('True')
|
||||
expect(provider.ha).to eq('True')
|
||||
expect(provider.distributed).to eq('False')
|
||||
expect(provider.status).to eq('ACTIVE')
|
||||
end
|
||||
end
|
||||
|
||||
context 'with admin_state_up' do
|
||||
let :router_attrs do
|
||||
{
|
||||
:name => router_name,
|
||||
:ensure => 'present',
|
||||
:admin_state_up => 'False',
|
||||
}
|
||||
end
|
||||
it 'creates router' do
|
||||
provider_class.expects(:openstack)
|
||||
.with('router', 'create', '--format', 'shell',
|
||||
['router1', '--disable'])
|
||||
.returns('admin_state_up="False"
|
||||
availability_zone_hints="[]"
|
||||
distributed="False"
|
||||
external_gateway_info="None"
|
||||
ha="True"
|
||||
id="d73f453a-77ca-4843-977a-3af0fda8dfcb"
|
||||
name="router1"
|
||||
project_id="60f9544eb94c42a6b7e8e98c2be981b1"
|
||||
status="ACTIVE"')
|
||||
provider.create
|
||||
expect(provider.exists?).to be_truthy
|
||||
expect(provider.admin_state_up).to eq('False')
|
||||
end
|
||||
end
|
||||
|
||||
context 'with centralized' do
|
||||
let :router_attrs do
|
||||
{
|
||||
:name => router_name,
|
||||
:ensure => 'present',
|
||||
:distributed => 'False',
|
||||
}
|
||||
end
|
||||
it 'creates router' do
|
||||
provider_class.expects(:openstack)
|
||||
.with('router', 'create', '--format', 'shell',
|
||||
['router1', '--centralized'])
|
||||
.returns('admin_state_up="True"
|
||||
availability_zone_hints="[]"
|
||||
distributed="False"
|
||||
external_gateway_info="None"
|
||||
ha="True"
|
||||
id="d73f453a-77ca-4843-977a-3af0fda8dfcb"
|
||||
name="router1"
|
||||
project_id="60f9544eb94c42a6b7e8e98c2be981b1"
|
||||
status="ACTIVE"')
|
||||
provider.create
|
||||
expect(provider.exists?).to be_truthy
|
||||
expect(provider.distributed).to eq('False')
|
||||
end
|
||||
end
|
||||
|
||||
context 'with distributed' do
|
||||
let :router_attrs do
|
||||
{
|
||||
:name => router_name,
|
||||
:ensure => 'present',
|
||||
:distributed => 'True',
|
||||
}
|
||||
end
|
||||
it 'creates router' do
|
||||
provider_class.expects(:openstack)
|
||||
.with('router', 'create', '--format', 'shell',
|
||||
['router1', '--distributed'])
|
||||
.returns('admin_state_up="True"
|
||||
availability_zone_hints="[]"
|
||||
distributed="True"
|
||||
external_gateway_info="None"
|
||||
ha="True"
|
||||
id="d73f453a-77ca-4843-977a-3af0fda8dfcb"
|
||||
name="router1"
|
||||
project_id="60f9544eb94c42a6b7e8e98c2be981b1"
|
||||
status="ACTIVE"')
|
||||
provider.create
|
||||
expect(provider.exists?).to be_truthy
|
||||
expect(provider.distributed).to eq('True')
|
||||
end
|
||||
end
|
||||
|
||||
context 'with ha' do
|
||||
let :router_attrs do
|
||||
{
|
||||
:name => router_name,
|
||||
:ensure => 'present',
|
||||
:ha => 'True',
|
||||
}
|
||||
end
|
||||
it 'creates router' do
|
||||
provider_class.expects(:openstack)
|
||||
.with('router', 'create', '--format', 'shell',
|
||||
['router1', '--ha'])
|
||||
.returns('admin_state_up="True"
|
||||
availability_zone_hints="[]"
|
||||
distributed="False"
|
||||
external_gateway_info="None"
|
||||
ha="True"
|
||||
id="d73f453a-77ca-4843-977a-3af0fda8dfcb"
|
||||
name="router1"
|
||||
project_id="60f9544eb94c42a6b7e8e98c2be981b1"
|
||||
status="ACTIVE"')
|
||||
provider.create
|
||||
expect(provider.exists?).to be_truthy
|
||||
expect(provider.ha).to eq('True')
|
||||
end
|
||||
end
|
||||
|
||||
context 'with non-ha' do
|
||||
let :router_attrs do
|
||||
{
|
||||
:name => router_name,
|
||||
:ensure => 'present',
|
||||
:ha => 'False',
|
||||
}
|
||||
end
|
||||
it 'creates router' do
|
||||
provider_class.expects(:openstack)
|
||||
.with('router', 'create', '--format', 'shell',
|
||||
['router1', '--no-ha'])
|
||||
.returns('admin_state_up="True"
|
||||
availability_zone_hints="[]"
|
||||
distributed="False"
|
||||
external_gateway_info="None"
|
||||
ha="False"
|
||||
id="d73f453a-77ca-4843-977a-3af0fda8dfcb"
|
||||
name="router1"
|
||||
project_id="60f9544eb94c42a6b7e8e98c2be981b1"
|
||||
status="ACTIVE"')
|
||||
provider.create
|
||||
expect(provider.exists?).to be_truthy
|
||||
expect(provider.ha).to eq('False')
|
||||
end
|
||||
end
|
||||
|
||||
context 'with gateway_network_name' do
|
||||
let :router_attrs do
|
||||
{
|
||||
:name => router_name,
|
||||
:ensure => 'present',
|
||||
:gateway_network_name => 'net1',
|
||||
}
|
||||
end
|
||||
it 'creates router' do
|
||||
provider_class.expects(:openstack)
|
||||
.with('router', 'create', '--format', 'shell',
|
||||
['router1'])
|
||||
.returns('admin_state_up="True"
|
||||
availability_zone_hints="[]"
|
||||
distributed="False"
|
||||
external_gateway_info="None"
|
||||
ha="False"
|
||||
id="d73f453a-77ca-4843-977a-3af0fda8dfcb"
|
||||
name="router1"
|
||||
project_id="60f9544eb94c42a6b7e8e98c2be981b1"
|
||||
status="ACTIVE"')
|
||||
provider_class.expects(:openstack)
|
||||
.with('router', 'set', ['router1', '--external-gateway=net1'])
|
||||
provider_class.expects(:openstack)
|
||||
.with('router', 'show', '--format', 'shell',
|
||||
['router1'])
|
||||
.returns('admin_state_up="True"
|
||||
availability_zone_hints="[]"
|
||||
distributed="False"
|
||||
external_gateway_info="{\'network_id\': \'076520cc-b783-4cf5-a4a9-4cb5a5e93a9b\'}"
|
||||
ha="False"
|
||||
id="d73f453a-77ca-4843-977a-3af0fda8dfcb"
|
||||
name="router1"
|
||||
project_id="60f9544eb94c42a6b7e8e98c2be981b1"
|
||||
status="ACTIVE"')
|
||||
provider.create
|
||||
expect(provider.exists?).to be_truthy
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#destroy' do
|
||||
it 'removes router' do
|
||||
provider_class.expects(:openstack)
|
||||
.with('router', 'delete', 'router1')
|
||||
provider.destroy
|
||||
expect(provider.exists?).to be_falsey
|
||||
end
|
||||
end
|
||||
|
||||
describe '#flush' do
|
||||
context '.admin_state_up' do
|
||||
it 'updates router' do
|
||||
provider_class.expects(:openstack)
|
||||
.with('router', 'set', ['router1', '--disable'])
|
||||
provider.admin_state_up = 'False'
|
||||
provider.flush
|
||||
provider_class.expects(:openstack)
|
||||
.with('router', 'set', ['router1', '--enable'])
|
||||
provider.admin_state_up = 'True'
|
||||
provider.flush
|
||||
end
|
||||
end
|
||||
context '.distributed' do
|
||||
it 'updates router' do
|
||||
provider_class.expects(:openstack)
|
||||
.with('router', 'set', ['router1', '--distributed'])
|
||||
provider.distributed = 'True'
|
||||
provider.flush
|
||||
provider_class.expects(:openstack)
|
||||
.with('router', 'set', ['router1', '--centralized'])
|
||||
provider.distributed = 'False'
|
||||
provider.flush
|
||||
end
|
||||
end
|
||||
context '.ha' do
|
||||
it 'updates router' do
|
||||
provider_class.expects(:openstack)
|
||||
.with('router', 'set', ['router1', '--ha'])
|
||||
provider.ha = 'True'
|
||||
provider.flush
|
||||
provider_class.expects(:openstack)
|
||||
.with('router', 'set', ['router1', '--no-ha'])
|
||||
provider.ha = 'False'
|
||||
provider.flush
|
||||
end
|
||||
end
|
||||
context '.gateway_network_name' do
|
||||
it 'updates router' do
|
||||
provider_class.expects(:openstack)
|
||||
.with('router', 'set', ['router1', '--external-gateway=net1'])
|
||||
provider.gateway_network_name = 'net1'
|
||||
provider.flush
|
||||
provider_class.expects(:openstack)
|
||||
.with('router', 'unset', ['router1', '--external-gateway'])
|
||||
provider.gateway_network_name = ''
|
||||
provider.flush
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#instances' do
|
||||
it 'lists router' do
|
||||
provider_class.expects(:openstack)
|
||||
.with('router', 'list', '--quiet', '--format', 'csv', [])
|
||||
.returns('"ID","Name","Status","State","Project","Distributed","HA"
|
||||
"d73f453a-77ca-4843-977a-3af0fda8dfcb","router1","ACTIVE","True","60f9544eb94c42a6b7e8e98c2be981b1",True,False
|
||||
"c3e93a5b-45ee-4029-b3a3-3331cb3e3f2a","router2","DOWN","False","60f9544eb94c42a6b7e8e98c2be981b1",False,True
|
||||
')
|
||||
provider_class.expects(:openstack)
|
||||
.with('router', 'show', '--format', 'shell', 'd73f453a-77ca-4843-977a-3af0fda8dfcb')
|
||||
.returns('admin_state_up="True"
|
||||
availability_zone_hints="[]"
|
||||
distributed="False"
|
||||
external_gateway_info="None"
|
||||
ha="True"
|
||||
id="d73f453a-77ca-4843-977a-3af0fda8dfcb"
|
||||
name="router1"
|
||||
project_id="60f9544eb94c42a6b7e8e98c2be981b1"
|
||||
status="ACTIVE"')
|
||||
provider_class.expects(:openstack)
|
||||
.with('router', 'show', '--format', 'shell', 'c3e93a5b-45ee-4029-b3a3-3331cb3e3f2a')
|
||||
.returns('admin_state_up="False"
|
||||
availability_zone_hints="[]"
|
||||
distributed="True"
|
||||
external_gateway_info="None"
|
||||
ha="False"
|
||||
id="c3e93a5b-45ee-4029-b3a3-3331cb3e3f2a"
|
||||
name="router2"
|
||||
project_id="60f9544eb94c42a6b7e8e98c2be981b1"
|
||||
project_id="60f9544eb94c42a6b7e8e98c2be981b1"
|
||||
status="DOWN"')
|
||||
|
||||
instances = provider_class.instances
|
||||
expect(instances.length).to eq(2)
|
||||
|
||||
expect(instances[0].id).to eq('d73f453a-77ca-4843-977a-3af0fda8dfcb')
|
||||
expect(instances[0].name).to eq('router1')
|
||||
expect(instances[0].admin_state_up).to eq('True')
|
||||
expect(instances[0].ha).to eq('True')
|
||||
expect(instances[0].distributed).to eq('False')
|
||||
expect(instances[0].status).to eq('ACTIVE')
|
||||
|
||||
expect(instances[1].id).to eq('c3e93a5b-45ee-4029-b3a3-3331cb3e3f2a')
|
||||
expect(instances[1].name).to eq('router2')
|
||||
expect(instances[1].admin_state_up).to eq('False')
|
||||
expect(instances[1].ha).to eq('False')
|
||||
expect(instances[1].distributed).to eq('True')
|
||||
expect(instances[1].status).to eq('DOWN')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
@ -57,148 +57,5 @@ describe Puppet::Provider::Neutron do
|
||||
klass.neutron_credentials
|
||||
end.to raise_error(Puppet::Error, credential_error)
|
||||
end
|
||||
|
||||
|
||||
end
|
||||
|
||||
describe 'when invoking the neutron cli' do
|
||||
|
||||
it 'should set auth credentials in the environment' do
|
||||
authenv = {
|
||||
:OS_AUTH_URL => credential_hash['auth_url'],
|
||||
:OS_USERNAME => credential_hash['username'],
|
||||
:OS_PROJECT_NAME => credential_hash['project_name'],
|
||||
:OS_PASSWORD => credential_hash['password'],
|
||||
:OS_PROJECT_DOMAIN_NAME => credential_hash['project_domain_name'],
|
||||
:OS_USER_DOMAIN_NAME => credential_hash['user_domain_name'],
|
||||
}
|
||||
klass.expects(:get_neutron_credentials).with().returns(credential_hash)
|
||||
klass.expects(:withenv).with(authenv)
|
||||
klass.auth_neutron('test_retries')
|
||||
end
|
||||
|
||||
it 'should set region in the environment if needed' do
|
||||
authenv = {
|
||||
:OS_AUTH_URL => credential_hash['auth_url'],
|
||||
:OS_USERNAME => credential_hash['username'],
|
||||
:OS_PROJECT_NAME => credential_hash['project_name'],
|
||||
:OS_PASSWORD => credential_hash['password'],
|
||||
:OS_REGION_NAME => 'REGION_NAME',
|
||||
:OS_PROJECT_DOMAIN_NAME => credential_hash['project_domain_name'],
|
||||
:OS_USER_DOMAIN_NAME => credential_hash['user_domain_name'],
|
||||
}
|
||||
|
||||
cred_hash = credential_hash.merge({'region_name' => 'REGION_NAME'})
|
||||
klass.expects(:get_neutron_credentials).with().returns(cred_hash)
|
||||
klass.expects(:withenv).with(authenv)
|
||||
klass.auth_neutron('test_retries')
|
||||
end
|
||||
|
||||
['[Errno 111] Connection refused',
|
||||
'400-{\'message\': \'\'}',
|
||||
'(HTTP 400)',
|
||||
'503 Service Unavailable',
|
||||
'504 Gateway Time-out',
|
||||
'Maximum attempts reached',
|
||||
'Unauthorized: bad credentials',
|
||||
'Max retries exceeded'].reverse.each do |valid_message|
|
||||
it "should retry when neutron cli returns with error #{valid_message}" do
|
||||
klass.expects(:get_neutron_credentials).with().returns({})
|
||||
klass.expects(:sleep).with(2).returns(nil)
|
||||
klass.expects(:neutron).twice.with(['test_retries']).raises(
|
||||
Puppet::ExecutionFailure, valid_message).then.returns('')
|
||||
klass.auth_neutron('test_retries')
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
describe 'when listing neutron resources' do
|
||||
|
||||
it 'should exclude the column header' do
|
||||
output = <<-EOT
|
||||
id
|
||||
net1
|
||||
net2
|
||||
EOT
|
||||
klass.expects(:auth_neutron).returns(output)
|
||||
result = klass.list_neutron_resources('foo')
|
||||
expect(result).to eql(['net1', 'net2'])
|
||||
end
|
||||
|
||||
it 'should return empty list when there are no neutron resources' do
|
||||
output = <<-EOT
|
||||
EOT
|
||||
klass.stubs(:auth_neutron).returns(output)
|
||||
result = klass.list_neutron_resources('foo')
|
||||
expect(result).to eql([])
|
||||
end
|
||||
|
||||
it 'should fail if resources list is nil' do
|
||||
klass.stubs(:auth_neutron).returns(nil)
|
||||
expect do
|
||||
klass.list_neutron_resources('foo')
|
||||
end.to raise_error(Puppet::Error, exec_error)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
describe 'when retrieving attributes for neutron resources' do
|
||||
|
||||
it 'should parse single-valued attributes into a key-value pair' do
|
||||
klass.expects(:auth_neutron).returns('admin_state_up="True"')
|
||||
result = klass.get_neutron_resource_attrs('foo', 'id')
|
||||
expect(result).to eql({"admin_state_up" => 'True'})
|
||||
end
|
||||
|
||||
it 'should parse multi-valued attributes into a key-list pair' do
|
||||
output = <<-EOT
|
||||
subnets="subnet1
|
||||
subnet2
|
||||
subnet3"
|
||||
EOT
|
||||
klass.expects(:auth_neutron).returns(output)
|
||||
result = klass.get_neutron_resource_attrs('foo', 'id')
|
||||
expect(result).to eql({"subnets" => ['subnet1', 'subnet2', 'subnet3']})
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
describe 'when parsing creation output' do
|
||||
|
||||
it 'should parse valid output into a hash' do
|
||||
data = <<-EOT
|
||||
Created a new network:
|
||||
admin_state_up="True"
|
||||
id="5f9cbed2-d31c-4e9c-be92-87229acb3f69"
|
||||
name="foo"
|
||||
tenant_id="3056a91768d948d399f1d79051a7f221"
|
||||
EOT
|
||||
expected = {
|
||||
'admin_state_up' => 'True',
|
||||
'id' => '5f9cbed2-d31c-4e9c-be92-87229acb3f69',
|
||||
'name' => 'foo',
|
||||
'tenant_id' => '3056a91768d948d399f1d79051a7f221',
|
||||
}
|
||||
expect(klass.parse_creation_output(data)).to eq(expected)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
describe 'garbage in the csv output' do
|
||||
it '#list_neutron_resources' do
|
||||
output = <<-EOT
|
||||
/usr/lib/python2.7/dist-packages/urllib3/util/ssl_.py:90: InsecurePlatformWarning: A true SSLContext object is not available. This prevents urllib3 from configuring SSL appropriately and may cause certain SSL connections to fail. For more information, see https://urllib3.readthedocs.org/en/latest/security.html#insecureplatformwarning.
|
||||
InsecurePlatformWarning
|
||||
id
|
||||
4a305398-d806-46c5-a6aa-dcd6a4a99330
|
||||
EOT
|
||||
klass.expects(:auth_neutron).
|
||||
with('subnet-list', '--format=csv', '--column=id', '--quote=none').
|
||||
returns(output)
|
||||
expected = ['4a305398-d806-46c5-a6aa-dcd6a4a99330']
|
||||
result = klass.list_neutron_resources('subnet')
|
||||
expect(result).to eql(expected)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
Loading…
Reference in New Issue
Block a user