Add nova flavor provider

Nova is not including flavors by default anymore so there needs to be
a way to create flavors with puppet. This change adds a nova_flavor
provider for adding and removing flavors. Additionally the provider can
be used to manage properties on flavors. Since flavor information cannot
be updated after creation time, only the properties can be managed after
creation via the nova_flavor provider.

Change-Id: Ic370ac65990d0b3a360340632ada4016231c8a93
Related-Bug: #1564928
Co-Authored-By: Alex Schultz <aschultz@mirantis.com>
This commit is contained in:
Trevor McCasland 2016-04-14 11:01:01 -05:00 committed by Alex Schultz
parent 189a662c86
commit 0986087729
5 changed files with 351 additions and 1 deletions

View File

@ -0,0 +1,122 @@
require 'puppet/provider/nova'
Puppet::Type.type(:nova_flavor).provide(
:openstack,
:parent => Puppet::Provider::Nova
) do
desc <<-EOT
Manage Nova flavor
EOT
@credentials = Puppet::Provider::Openstack::CredentialsV2_0.new
def initialize(value={})
super(value)
@property_flush = {}
end
def create
opts = [@resource[:name]]
opts << (@resource[:is_public] == :true ? '--public' : '--private')
(opts << '--id' << @resource[:id]) if @resource[:id]
(opts << '--ram' << @resource[:ram]) if @resource[:ram]
(opts << '--disk' << @resource[:disk]) if @resource[:disk]
(opts << '--ephemeral' << @resource[:ephemeral]) if @resource[:ephemeral]
(opts << '--vcpus' << @resource[:vcpus]) if @resource[:vcpus]
(opts << '--swap' << @resource[:swap]) if @resource[:swap]
(opts << '--rxtx-factor' << @resource[:rxtx_factor]) if @resource[:rxtx_factor]
@property_hash = self.class.request('flavor', 'create', opts)
if @resource[:properties]
prop_opts = [@resource[:name]]
prop_opts << props_to_s(@resource[:properties])
self.class.request('flavor', 'set', prop_opts)
end
@property_hash[:ensure] = :present
end
def exists?
@property_hash[:ensure] == :present
end
def destroy
self.class.request('flavor', 'delete', @property_hash[:id])
end
mk_resource_methods
def is_public=(value)
fail('is_public is read only')
end
def id=(value)
fail('id is read only')
end
def ram=(value)
fail('ram is read only')
end
def disk=(value)
fail('disk is read only')
end
def vcpus=(value)
fail('vcpus is read only')
end
def swap=(value)
fail('swap is read only')
end
def rxtx_factor=(value)
fail('rxtx_factor is read only')
end
def properties=(value)
@property_flush[:properties] = value
end
def self.instances
request('flavor', 'list', ['--long', '--all']).collect do |attrs|
properties = Hash[attrs[:properties].scan(/(\S+)='([^']*)'/)] rescue nil
new(
:ensure => :present,
:name => attrs[:name],
:id => attrs[:id],
:ram => attrs[:ram],
:disk => attrs[:disk],
:ephemeral => attrs[:ephemeral],
:vcpus => attrs[:vcpus],
:is_public => attrs[:is_public].downcase.chomp == 'true'? true : false,
:swap => attrs[:swap],
:rxtx_factor => attrs[:rxtx_factor],
:properties => properties
)
end
end
def self.prefetch(resources)
flavors = instances
resources.keys.each do |name|
if provider = flavors.find{ |flavor| flavor.name == name }
resources[name].provider = provider
end
end
end
def flush
unless @property_flush.empty?
opts = [@resource[:name]]
opts << props_to_s(@property_flush[:properties])
self.class.request('flavor', 'set', opts)
@property_flush.clear
end
end
private
def props_to_s(props)
props.flat_map{ |k, v| ['--property', "#{k}=#{v}"] }
end
end

View File

@ -0,0 +1,144 @@
# nova_flavor type
#
# == Parameters
# [*name*]
# Name for the flavor
# Required
#
# [*id*]
# Unique ID (integer or UUID) for the flavor.
# Optional
#
# [*ram*]
# Amount of RAM to use (in megabytes).
# Optional
#
# [*disk*]
# Amount of disk space (in gigabytes) to use for the root (/) partition.
# Optional
#
# [*vcpus*]
# Number of virtual CPUs to use.
# Optional
#
# [*ephemeral*]
# Amount of disk space (in gigabytes) to use for the ephemeral partition.
# Optional
#
# [*swap*]
# Amount of swap space (in megabytes) to use.
# Optional
#
# [*rxtx_factor*]
# The slice of bandwidth that the instances with this flavor can use
# (through the Virtual Interface (vif) creation in the hypervisor)
# Optional
#
# [*is_public*]
# A boolean to indicate visibility
# Optional
#
# [*properties*]
# A key => value hash used to set the properties for the flavor. This is
# the only parameter that can be updated after the creation of the flavor.
# Optional
require 'puppet'
Puppet::Type.newtype(:nova_flavor) do
@doc = "Manage creation of nova flavors."
ensurable
autorequire(:nova_config) do
['auth_uri', 'admin_tenant_name', 'admin_user', 'admin_password']
end
# Require the nova-api service to be running
autorequire(:service) do
['nova-api']
end
newparam(:name, :namevar => true) do
desc 'Name for the flavor'
validate do |value|
if not value.is_a? String
raise ArgumentError, "name parameter must be a String"
end
unless value =~ /^[a-zA-Z0-9\-\._]+$/
raise ArgumentError, "#{value} is not a valid name"
end
end
end
newparam(:id) do
desc 'Unique ID (integer or UUID) for the flavor.'
end
newparam(:ram) do
desc 'Amount of RAM to use (in megabytes).'
end
newparam(:disk) do
desc 'Amount of disk space (in gigabytes) to use for the root (/) partition.'
end
newparam(:vcpus) do
desc 'Number of virtual CPUs to use.'
end
newparam(:ephemeral) do
desc 'Amount of disk space (in gigabytes) to use for the ephemeral partition.'
end
newparam(:swap) do
desc 'Amount of swap space (in megabytes) to use.'
end
newparam(:rxtx_factor) do
desc 'The slice of bandiwdth that the instances with this flavor can use (through the Virtual Interface (vif) creation in the hypervisor)'
end
newparam(:is_public) do
desc "Whether the image is public or not. Default true"
newvalues(/(y|Y)es/, /(n|N)o/, /(t|T)rue/, /(f|F)alse/, true, false)
defaultto(true)
munge do |v|
if v =~ /^(y|Y)es$/
:true
elsif v =~ /^(n|N)o$/
:false
else
v.to_s.downcase.to_sym
end
end
end
newproperty(:properties) do
desc "The set of flavor properties"
munge do |value|
return value if value.is_a? Hash
# wrap property value in commas
value.gsub!(/=(\w+)/, '=\'\1\'')
Hash[value.scan(/(\S+)='([^']*)'/)]
end
validate do |value|
return true if value.is_a? Hash
value.split(',').each do |property|
raise ArgumentError, "Key/value pairs should be separated by an =" unless property.include?('=')
end
end
end
validate do
unless self[:name]
raise(ArgumentError, 'Name must be set')
end
end
end

View File

@ -0,0 +1,6 @@
---
features:
- Added flavor provider and type for creating and deleting
because nova no longer comes with flavors by default.
The provider can be used to add/delete flavors and manage
flavor properties.

View File

@ -30,7 +30,7 @@ describe 'basic nova' do
# Nova resources
class { '::nova':
database_connection => 'mysql+pymysql://nova:a_big_secret@127.0.0.1/nova?charset=utf8',
api_database_connection => 'mysql+pymysql://nova_api:a_big_secret@127.0.0.1/nova_api?charset=utf8',
api_database_connection => 'mysql+pymysql://nova_api:a_big_secret@127.0.0.1/nova_api?charset=utf8',
rabbit_userid => 'nova',
rabbit_password => 'an_even_bigger_secret',
image_service => 'nova.image.glance.GlanceImageService',
@ -78,6 +78,16 @@ describe 'basic nova' do
require => Class['nova::api'],
}
nova_flavor { 'test_flavor':
ensure => present,
name => 'test_flavor',
id => '9999',
ram => '512',
disk => '1',
vcpus => '1',
require => [ Class['nova::api'], Class['nova::keystone::auth'] ],
}
# TODO: networking with neutron
EOS
@ -111,5 +121,13 @@ describe 'basic nova' do
end
end
end
describe 'nova flavor' do
it 'should create new flavor' do
shell('openstack --os-username nova --os-password a_big_secret --os-tenant-name services --os-auth-url http://127.0.0.1:5000/v2.0 flavor list') do |r|
expect(r.stdout).to match(/test_flavor/)
expect(r.stderr).to be_empty
end
end
end
end
end

View File

@ -0,0 +1,60 @@
require 'puppet'
require 'spec_helper'
require 'puppet/provider/nova_flavor/openstack'
provider_class = Puppet::Type.type(:nova_flavor).provider(:openstack)
describe provider_class do
describe 'managing flavors' do
let(:flavor_attrs) do
{
:name => 'example',
:id => '1',
:ram => '512',
:disk => '1',
:vcpus => '1',
:ensure => 'present',
}
end
let :resource do
Puppet::Type::Nova_flavor.new(flavor_attrs)
end
let(:provider) do
provider_class.new(resource)
end
describe '#create' do
it 'creates flavor' do
provider.class.stubs(:openstack)
.with('flavor', 'list', ['--long', '--all'])
.returns('"ID", "Name", "RAM", "Disk", "Ephemeral", "VCPUs", "Is Public", "Swap", "RXTX Factor", "Properties"')
provider.class.stubs(:openstack)
.with('flavor', 'create', 'shell', ['example', '--public', '--id', '1', '--ram', '512', '--disk', '1', '--vcpus', '1'])
.returns('os-flv-disabled:disabled="False"
os-flv-ext-data:ephemeral="0"
disk="1"
id="1"
name="example"
os-flavor-access:is_public="True"
ram="512"
rxtx_factor="1.0"
swap=""
vcpus="1"')
end
end
describe '#destroy' do
it 'removes flavor' do
provider_class.expects(:openstack)
.with('flavor', 'delete', '1')
provider.instance_variable_set(:@property_hash, flavor_attrs)
provider.destroy
expect(provider.exists?).to be_falsey
end
end
end
end