diff --git a/lib/puppet/provider/nova_flavor/openstack.rb b/lib/puppet/provider/nova_flavor/openstack.rb new file mode 100644 index 000000000..bf18109fb --- /dev/null +++ b/lib/puppet/provider/nova_flavor/openstack.rb @@ -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 + diff --git a/lib/puppet/type/nova_flavor.rb b/lib/puppet/type/nova_flavor.rb new file mode 100644 index 000000000..2ab50746e --- /dev/null +++ b/lib/puppet/type/nova_flavor.rb @@ -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 + diff --git a/releasenotes/notes/add_flavor_provider-a7e12b6c3e9ca80f.yaml b/releasenotes/notes/add_flavor_provider-a7e12b6c3e9ca80f.yaml new file mode 100644 index 000000000..c95f6f59e --- /dev/null +++ b/releasenotes/notes/add_flavor_provider-a7e12b6c3e9ca80f.yaml @@ -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. diff --git a/spec/acceptance/nova_wsgi_apache_spec.rb b/spec/acceptance/nova_wsgi_apache_spec.rb index b024ca706..00e6f50e7 100644 --- a/spec/acceptance/nova_wsgi_apache_spec.rb +++ b/spec/acceptance/nova_wsgi_apache_spec.rb @@ -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 diff --git a/spec/unit/provider/nova_flavor/openstack_spec.rb b/spec/unit/provider/nova_flavor/openstack_spec.rb new file mode 100644 index 000000000..5bd995740 --- /dev/null +++ b/spec/unit/provider/nova_flavor/openstack_spec.rb @@ -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 +