Replace defines for managing cinder types with providers

We have define classes, which allow to manage Cinder types and their
properties. This patch switches using of define classes to puppet
providers, based on openstack auth from openstacklib.

related blueprint use-openstackclient-in-module-resources

Change-Id: I4f7e8137fa3e1ad3e141c58eaba110b12101d22c
This commit is contained in:
Denis Egorenko 2016-01-28 15:52:38 +03:00
parent 253599e6f3
commit 9bc49efba8
14 changed files with 465 additions and 200 deletions

View File

@ -131,22 +131,14 @@ cinder::backend::rbd {'rbd-images':
rbd_user => 'images',
}
# Cinder::Type requires keystone credentials
Cinder::Type {
os_password => 'admin',
os_tenant_name => 'admin',
os_username => 'admin',
os_auth_url => 'http://127.0.0.1:5000/v2.0/',
cinder_type {'iscsi':
ensure => present,
properties => ['volume_backend_name=iscsi,iscsi1,iscsi2'],
}
cinder::type {'iscsi':
set_key => 'volume_backend_name',
set_value => ['iscsi1', 'iscsi2', 'iscsi']
}
cinder::type {'rbd':
set_key => 'volume_backend_name',
set_value => 'rbd-images',
cinder_type {'rbd-images':
ensure => present,
properties => ['volume_backend_name=rbd-images'],
}
class { 'cinder::backends':
@ -157,13 +149,14 @@ class { 'cinder::backends':
Note: that the name passed to any backend resource must be unique accross all
backends otherwise a duplicate resource will be defined.
** Using type and type_set **
** Using cinder_type **
Cinder allows for the usage of type to set extended information that can be
used for various reasons. We have resource provider for ``type`` and
``type_set`` Since types are rarely defined with out also setting attributes
with it, the resource for ``type`` can also call ``type_set`` if you pass
``set_key`` and ``set_value``
used for various reasons. We have resource provider for ``cinder_type``
and if you want create some cinder type, you should set ensure to absent.
Properties field is optional and should be an array. All items of array
should match pattern key=value1[,value2 ...]. In case when you want to
delete some type - set ensure to absent.
Implementation

View File

@ -0,0 +1,76 @@
require 'puppet/util/inifile'
require 'puppet/provider/openstack'
require 'puppet/provider/openstack/auth'
require 'puppet/provider/openstack/credentials'
class Puppet::Provider::Cinder < Puppet::Provider::Openstack
extend Puppet::Provider::Openstack::Auth
def self.conf_filename
'/etc/cinder/cinder.conf'
end
def self.cinder_conf
return @cinder_conf if @cinder_conf
@cinder_conf = Puppet::Util::IniConfig::File.new
@cinder_conf.read(conf_filename)
@cinder_conf
end
def self.request(service, action, properties=nil)
begin
super
rescue Puppet::Error::OpenstackAuthInputError, Puppet::Error::OpenstackUnauthorizedError => error
cinder_request(service, action, error, properties)
end
end
def self.cinder_request(service, action, error, properties=nil)
properties ||= []
@credentials.username = cinder_credentials['admin_user']
@credentials.password = cinder_credentials['admin_password']
@credentials.project_name = cinder_credentials['admin_tenant_name']
@credentials.auth_url = auth_endpoint
raise error unless @credentials.set?
Puppet::Provider::Openstack.request(service, action, properties, @credentials)
end
def self.cinder_credentials
@cinder_credentials ||= get_cinder_credentials
end
def cinder_credentials
self.class.cinder_credentials
end
def self.get_cinder_credentials
auth_keys = ['auth_uri', 'admin_tenant_name', 'admin_user',
'admin_password']
conf = cinder_conf
if conf and conf['keystone_authtoken'] and
auth_keys.all?{|k| !conf['keystone_authtoken'][k].nil?}
creds = Hash[ auth_keys.map \
{ |k| [k, conf['keystone_authtoken'][k].strip] } ]
return creds
else
raise(Puppet::Error, "File: #{conf_filename} does not contain all " +
"required sections. Cinder types will not work if cinder is not " +
"correctly configured.")
end
end
def self.get_auth_endpoint
q = cinder_credentials
"#{q['auth_uri']}"
end
def self.auth_endpoint
@auth_endpoint ||= get_auth_endpoint
end
def self.reset
@cinder_conf = nil
@cinder_credentials = nil
end
end

View File

@ -0,0 +1,70 @@
require 'puppet/provider/cinder'
Puppet::Type.type(:cinder_type).provide(
:openstack,
:parent => Puppet::Provider::Cinder
) do
desc 'Provider for cinder types.'
@credentials = Puppet::Provider::Openstack::CredentialsV2_0.new
mk_resource_methods
def create
properties = []
resource[:properties].each do |item|
properties << '--property' << item
end
properties << name
self.class.request('volume type', 'create', properties)
@property_hash[:ensure] = :present
@property_hash[:properties] = resource[:properties]
@property_hash[:name] = name
end
def destroy
self.class.request('volume type', 'delete', name)
@property_hash.clear
end
def exists?
@property_hash[:ensure] == :present
end
def properties=(value)
properties = []
(value - @property_hash[:properties]).each do |item|
properties << '--property' << item
end
unless properties.empty?
self.class.request('volume type', 'set', [properties, name])
@property_hash[:properties] = value
end
end
def self.instances
list = request('volume type', 'list', '--long')
list.collect do |type|
new({
:name => type[:name],
:ensure => :present,
:id => type[:id],
:properties => string2array(type[:properties])
})
end
end
def self.prefetch(resources)
types = instances
resources.keys.each do |name|
if provider = types.find{ |type| type.name == name }
resources[name].provider = provider
end
end
end
def self.string2array(input)
return input.delete("'").split(/,\s/)
end
end

View File

@ -0,0 +1,26 @@
Puppet::Type.newtype(:cinder_type) do
desc 'Type for managing cinder types.'
ensurable
newparam(:name, :namevar => true) do
newvalues(/\S+/)
end
newproperty(:properties, :array_matching => :all) do
desc 'The properties of the cinder type. Should be an array, all items should match pattern <key=value1[,value2 ...]>'
def insync?(is)
return false unless is.is_a? Array
is.sort == should.sort
end
validate do |value|
raise ArgumentError, "Properties doesn't match" unless value.match(/^\s*[^=\s]+=[^=\s]+$/)
end
end
autorequire(:service) do
'cinder-api'
end
end

View File

@ -181,7 +181,7 @@ class cinder::keystone::auth (
if $configure_user_role {
Keystone_user_role["${auth_name}@${tenant}"] ~> Service <| name == 'cinder-api' |>
Keystone_user_role["${auth_name}@${tenant}"] -> Cinder::Type <| |>
Keystone_user_role["${auth_name}@${tenant}"] -> Cinder_type <| |>
}
}

View File

@ -1,6 +1,7 @@
# == Define: cinder::type
#
# Creates cinder type and assigns backends.
# Deprecated class.
#
# === Parameters
#
@ -17,73 +18,55 @@
# passed to type_set
# Defaults to 'undef'.
#
# === DEPRECATED PARAMETERS
#
# [*os_tenant_name*]
# (Optional) The keystone tenant name.
# Defaults to 'admin'.
# Defaults to undef.
#
# [*os_username*]
# (Optional) The keystone user name.
# Defaults to 'admin.
# Defaults to undef.
#
# [*os_auth_url*]
# (Optional) The keystone auth url.
# Defaults to 'http://127.0.0.1:5000/v2.0/'.
# Defaults to undef.
#
# [*os_region_name*]
# (Optional) The keystone region name.
# Default is unset.
# Default is undef.
#
# Author: Andrew Woodward <awoodward@mirantis.com>
#
define cinder::type (
$os_password,
$set_key = undef,
$set_value = undef,
$os_tenant_name = 'admin',
$os_username = 'admin',
$os_auth_url = 'http://127.0.0.1:5000/v2.0/',
# DEPRECATED PARAMETERS
$os_password = undef,
$os_tenant_name = undef,
$os_username = undef,
$os_auth_url = undef,
$os_region_name = undef,
) {
$volume_name = $name
# TODO: (xarses) This should be moved to a ruby provider so that among other
# reasons, the credential discovery magic can occur like in neutron.
$cinder_env = [
"OS_TENANT_NAME=${os_tenant_name}",
"OS_USERNAME=${os_username}",
"OS_PASSWORD=${os_password}",
"OS_AUTH_URL=${os_auth_url}",
]
if $os_region_name {
$region_env = ["OS_REGION_NAME=${os_region_name}"]
}
else {
$region_env = []
}
exec {"cinder type-create ${volume_name}":
command => "cinder type-create ${volume_name}",
unless => "cinder type-list | grep -qP '\\s${volume_name}\\s'",
environment => concat($cinder_env, $region_env),
require => Package['python-cinderclient'],
path => ['/usr/bin', '/bin'],
tries => '2',
try_sleep => '5',
if $os_password or $os_region_name or $os_tenant_name or $os_username or $os_auth_url {
warning('Parameters $os_password/$os_region_name/$os_tenant_name/$os_username/$os_auth_url are not longer required')
warning('Auth creds will be used from env or /root/openrc file or cinder.conf')
}
if ($set_value and $set_key) {
Exec["cinder type-create ${volume_name}"] ->
cinder::type_set { $set_value:
type => $volume_name,
key => $set_key,
os_password => $os_password,
os_tenant_name => $os_tenant_name,
os_username => $os_username,
os_auth_url => $os_auth_url,
os_region_name => $os_region_name,
if is_array($set_value) {
$value = join($set_value, ',')
} else {
$value = $set_value
}
cinder_type { $name:
ensure => present,
properties => ["${set_key}=${value}"],
}
} else {
cinder_type { $name:
ensure => present,
}
}
}

View File

@ -1,12 +1,10 @@
# ==Define: cinder::type_set
#
# Assigns keys after the volume type is set.
# Deprecated class.
#
# === Parameters
#
# [*os_password*]
# (required) The keystone tenant:username password.
#
# [*type*]
# (required) Accepts single name of type to set.
#
@ -16,54 +14,45 @@
# [*value*]
# the value that we are setting. Defaults to content of namevar.
#
# === Deprecated parameters
#
# [*os_password*]
# (optional) DEPRECATED: The keystone tenant:username password.
# Defaults to undef.
#
# [*os_tenant_name*]
# (optional) The keystone tenant name. Defaults to 'admin'.
# (optional) DEPRECATED: The keystone tenant name. Defaults to undef.
#
# [*os_username*]
# (optional) The keystone user name. Defaults to 'admin.
# (optional) DEPRECATED: The keystone user name. Defaults to undef.
#
# [*os_auth_url*]
# (optional) The keystone auth url. Defaults to 'http://127.0.0.1:5000/v2.0/'.
# (optional) DEPRECATED: The keystone auth url. Defaults to undef.
#
# [*os_region_name*]
# (optional) The keystone region name. Default is unset.
# (optional) DEPRECATED: The keystone region name. Default is undef.
#
# Author: Andrew Woodward <awoodward@mirantis.com>
#
define cinder::type_set (
$type,
$key,
$os_password,
$os_tenant_name = 'admin',
$os_username = 'admin',
$os_auth_url = 'http://127.0.0.1:5000/v2.0/',
$os_region_name = undef,
$value = $name,
# DEPRECATED PARAMETERS
$os_password = undef,
$os_tenant_name = undef,
$os_username = undef,
$os_auth_url = undef,
$os_region_name = undef,
) {
# TODO: (xarses) This should be moved to a ruby provider so that among other
# reasons, the credential discovery magic can occur like in neutron.
$cinder_env = [
"OS_TENANT_NAME=${os_tenant_name}",
"OS_USERNAME=${os_username}",
"OS_PASSWORD=${os_password}",
"OS_AUTH_URL=${os_auth_url}",
]
if $os_region_name {
$region_env = ["OS_REGION_NAME=${os_region_name}"]
}
else {
$region_env = []
if $os_password or $os_region_name or $os_tenant_name or $os_username or $os_auth_url {
warning('Parameters $os_password/$os_region_name/$os_tenant_name/$os_username/$os_auth_url are not longer required.')
warning('Auth creds will be used from env or /root/openrc file or cinder.conf')
}
exec {"cinder type-key ${type} set ${key}=${value}":
path => ['/usr/bin', '/bin'],
command => "cinder type-key ${type} set ${key}=${value}",
unless => "cinder extra-specs-list | grep -Eq '\\b${type}\\b.*\\b${key}\\b.*\\b${value}\\b'",
environment => concat($cinder_env, $region_env),
require => Package['python-cinderclient']
cinder_type { $type:
ensure => present,
properties => ["${key}=${value}"],
}
}

View File

@ -1,56 +1,59 @@
# == Define: cinder::vmware
# == Class: cinder::vmware
#
# Creates vmdk specific disk file type & clone type.
#
# === Parameters
#
# [*os_password*]
# (Required) The keystone tenant:username password.
# DEPRECATED. The keystone tenant:username password.
# Defaults to undef.
#
# [*os_tenant_name*]
# (Optional) The keystone tenant name.
# Defaults to 'admin'.
# DEPRECATED. The keystone tenant name.
# Defaults to undef.
#
# [*os_username*]
# (Optional) The keystone user name.
# Defaults to 'admin.
# DEPRECATED. The keystone user name.
# Defaults to undef.
#
# [*os_auth_url*]
# (Optional) The keystone auth url.
# Defaults to 'http://127.0.0.1:5000/v2.0/'.
# DEPRECATED. The keystone auth url.
# Defaults to undef.
#
class cinder::vmware (
$os_password,
$os_tenant_name = 'admin',
$os_username = 'admin',
$os_auth_url = 'http://127.0.0.1:5000/v2.0/'
$os_password = undef,
$os_tenant_name = undef,
$os_username = undef,
$os_auth_url = undef
) {
Cinder::Type {
os_password => $os_password,
os_tenant_name => $os_tenant_name,
os_username => $os_username,
os_auth_url => $os_auth_url
if $os_password or $os_tenant_name or $os_username or $os_auth_url {
warning('Parameters $os_password/$os_tenant_name/$os_username/$os_auth_url are not longer required.')
warning('Auth creds will be used from env or /root/openrc file or cinder.conf')
}
cinder::type {'vmware-thin':
set_value => 'thin',
set_key => 'vmware:vmdk_type'
cinder_type { 'vmware-thin':
ensure => present,
properties => ['vmware:vmdk_type=thin']
}
cinder::type {'vmware-thick':
set_value => 'thick',
set_key => 'vmware:vmdk_type'
cinder_type { 'vmware-thick':
ensure => present,
properties => ['vmware:vmdk_type=thick']
}
cinder::type {'vmware-eagerZeroedThick':
set_value => 'eagerZeroedThick',
set_key => 'vmware:vmdk_type'
cinder_type { 'vmware-eagerZeroedThick':
ensure => present,
properties => ['vmware:vmdk_type=eagerZeroedThick']
}
cinder::type {'vmware-full':
set_value => 'full',
set_key => 'vmware:clone_type'
cinder_type { 'vmware-full':
ensure => present,
properties => ['vmware:clone_type=full']
}
cinder::type {'vmware-linked':
set_value => 'linked',
set_key => 'vmware:clone_type'
cinder_type { 'vmware-linked':
ensure => present,
properties => ['vmware:clone_type=linked']
}
}

View File

@ -2,34 +2,27 @@ require 'spec_helper'
describe 'cinder::vmware' do
let :params do
{:os_password => 'asdf',
:os_tenant_name => 'admin',
:os_username => 'admin',
:os_auth_url => 'http://127.127.127.1:5000/v2.0/'}
end
describe 'with defaults' do
it 'should create vmware special types' do
is_expected.to contain_cinder__type('vmware-thin').with(
:set_key => 'vmware:vmdk_type',
:set_value => 'thin')
is_expected.to contain_cinder_type('vmware-thin').with(
:ensure => :present,
:properties => ['vmware:vmdk_type=thin'])
is_expected.to contain_cinder__type('vmware-thick').with(
:set_key => 'vmware:vmdk_type',
:set_value => 'thick')
is_expected.to contain_cinder_type('vmware-thick').with(
:ensure => :present,
:properties => ['vmware:vmdk_type=thick'])
is_expected.to contain_cinder__type('vmware-eagerZeroedThick').with(
:set_key => 'vmware:vmdk_type',
:set_value => 'eagerZeroedThick')
is_expected.to contain_cinder_type('vmware-eagerZeroedThick').with(
:ensure => :present,
:properties => ['vmware:vmdk_type=eagerZeroedThick'])
is_expected.to contain_cinder__type('vmware-full').with(
:set_key => 'vmware:clone_type',
:set_value => 'full')
is_expected.to contain_cinder_type('vmware-full').with(
:ensure => :present,
:properties => ['vmware:clone_type=full'])
is_expected.to contain_cinder__type('vmware-linked').with(
:set_key => 'vmware:clone_type',
:set_value => 'linked')
is_expected.to contain_cinder_type('vmware-linked').with(
:ensure => :present,
:properties => ['vmware:clone_type=linked'])
end
end
end

View File

@ -9,25 +9,13 @@ describe 'cinder::type_set' do
let :default_params do {
:type => 'sith',
:key => 'monchichi',
:os_password => 'asdf',
:os_tenant_name => 'admin',
:os_username => 'admin',
:os_auth_url => 'http://127.127.127.1:5000/v2.0/',
}
end
describe 'by default' do
let(:params){ default_params }
it 'should have its execs' do
is_expected.to contain_exec('cinder type-key sith set monchichi=hippo').with(
:command => 'cinder type-key sith set monchichi=hippo',
:unless => "cinder extra-specs-list | grep -Eq '\\bsith\\b.*\\bmonchichi\\b.*\\bhippo\\b'",
:environment => [
'OS_TENANT_NAME=admin',
'OS_USERNAME=admin',
'OS_PASSWORD=asdf',
'OS_AUTH_URL=http://127.127.127.1:5000/v2.0/'],
:require => 'Package[python-cinderclient]')
it 'should create type with properties' do
should contain_cinder_type('sith').with(:ensure => :present, :properties => ['monchichi=hippo'])
end
end
@ -35,16 +23,8 @@ describe 'cinder::type_set' do
let(:params){
default_params.merge({:value => 'hippi'})
}
it 'should have its execs' do
is_expected.to contain_exec('cinder type-key sith set monchichi=hippi').with(
:command => 'cinder type-key sith set monchichi=hippi',
:unless => "cinder extra-specs-list | grep -Eq '\\bsith\\b.*\\bmonchichi\\b.*\\bhippi\\b'",
:environment => [
'OS_TENANT_NAME=admin',
'OS_USERNAME=admin',
'OS_PASSWORD=asdf',
'OS_AUTH_URL=http://127.127.127.1:5000/v2.0/'],
:require => 'Package[python-cinderclient]')
it 'should create type with properties' do
should contain_cinder_type('sith').with(:ensure => :present, :properties => ['monchichi=hippi'])
end
end
end

View File

@ -6,29 +6,20 @@ describe 'cinder::type' do
let(:title) {'hippo'}
context 'default creation' do
it 'should create type basic' do
should contain_cinder_type('hippo').with(:ensure => :present)
end
end
context 'creation with properties' do
let :params do {
:set_value => ['name1','name2'],
:set_key => 'volume_backend_name',
:os_password => 'asdf',
:os_tenant_name => 'admin',
:os_username => 'admin',
:os_auth_url => 'http://127.127.127.1:5000/v2.0/',
}
end
it 'should have its execs' do
is_expected.to contain_exec('cinder type-create hippo').with(
:command => 'cinder type-create hippo',
:environment => [
'OS_TENANT_NAME=admin',
'OS_USERNAME=admin',
'OS_PASSWORD=asdf',
'OS_AUTH_URL=http://127.127.127.1:5000/v2.0/'],
:unless => "cinder type-list | grep -qP '\\shippo\\s'",
:tries => '2',
:try_sleep => '5',
:require => 'Package[python-cinderclient]')
is_expected.to contain_exec('cinder type-key hippo set volume_backend_name=name1')
is_expected.to contain_exec('cinder type-key hippo set volume_backend_name=name2')
it 'should create type with properties' do
should contain_cinder_type('hippo').with(:ensure => :present, :properties => ['volume_backend_name=name1,name2'])
end
end
end

View File

@ -0,0 +1,46 @@
require 'puppet'
require 'spec_helper'
require 'puppet/provider/cinder'
require 'tempfile'
klass = Puppet::Provider::Cinder
describe Puppet::Provider::Cinder do
after :each do
klass.reset
end
describe 'when retrieving the auth credentials' do
it 'should fail if no auth params are passed and the glance config file does not have the expected contents' do
mock = {}
Puppet::Util::IniConfig::File.expects(:new).returns(mock)
mock.expects(:read).with('/etc/cinder/cinder.conf')
expect do
klass.cinder_credentials
end.to raise_error(Puppet::Error, /Cinder types will not work/)
end
it 'should read conf file with all sections' do
creds_hash = {
'auth_uri' => 'https://192.168.56.210:35357/v2.0/',
'admin_tenant_name' => 'admin_tenant',
'admin_user' => 'admin',
'admin_password' => 'password',
}
mock = {
'keystone_authtoken' => {
'auth_uri' => 'https://192.168.56.210:35357/v2.0/',
'admin_tenant_name' => 'admin_tenant',
'admin_user' => 'admin',
'admin_password' => 'password',
}
}
Puppet::Util::IniConfig::File.expects(:new).returns(mock)
mock.expects(:read).with('/etc/cinder/cinder.conf')
expect(klass.cinder_credentials).to eq(creds_hash)
end
end
end

View File

@ -0,0 +1,83 @@
require 'puppet'
require 'puppet/provider/cinder_type/openstack'
provider_class = Puppet::Type.type(:cinder_type).provider(:openstack)
describe provider_class do
let(:set_creds_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
let(:type_attributes) do
{
:name => 'Backend_1',
:ensure => :present,
:properties => ['key=value', 'new_key=new_value'],
}
end
let(:resource) do
Puppet::Type::Cinder_type.new(type_attributes)
end
let(:provider) do
provider_class.new(resource)
end
before(:each) { set_creds_env }
after(:each) do
Puppet::Type.type(:cinder_type).provider(:openstack).reset
provider_class.reset
end
describe 'managing type' do
describe '#create' do
it 'creates a type' do
provider_class.expects(:openstack)
.with('volume type', 'create', '--format', 'shell', ['--property', 'key=value', '--property', 'new_key=new_value', 'Backend_1'])
.returns('id="90e19aff-1b35-4d60-9ee3-383c530275ab"
name="Backend_1"
properties="key=\'value\', new_key=\'new_value\'"
')
provider.create
expect(provider.exists?).to be_truthy
end
end
describe '#destroy' do
it 'destroys a type' do
provider_class.expects(:openstack)
.with('volume type', 'delete', 'Backend_1')
provider.destroy
expect(provider.exists?).to be_falsey
end
end
describe '#instances' do
it 'finds types' do
provider_class.expects(:openstack)
.with('volume type', 'list', '--quiet', '--format', 'csv', '--long')
.returns('"ID","Name","Properties"
"28b632e8-6694-4bba-bf68-67b19f619019","type-1","key1=\'value1\'"
"4f992f69-14ec-4132-9313-55cc06a6f1f6","type-2","key2=\'value2\'"
')
instances = provider_class.instances
expect(instances.count).to eq(2)
expect(instances[0].name).to eq('type-1')
expect(instances[1].name).to eq('type-2')
end
end
describe '#string2array' do
it 'should return an array with key-value' do
s = "key='value', key2='value2'"
expect(provider_class.string2array(s)).to eq(['key=value', 'key2=value2'])
end
end
end
end

View File

@ -0,0 +1,32 @@
require 'puppet'
require 'puppet/type/cinder_type'
describe Puppet::Type.type(:cinder_type) do
before :each do
Puppet::Type.rmtype(:cinder_type)
end
it 'should reject an invalid propertie value' do
incorrect_input = {
:name => 'test_type',
:properties => ['some_key1 = some_value2']
}
expect { Puppet::Type.type(:cinder_type).new(incorrect_input) }.to raise_error(Puppet::ResourceError, /Parameter properties failed/)
end
it 'should autorequire cinder-api service' do
catalog = Puppet::Resource::Catalog.new
service = Puppet::Type.type(:service).new(:name => 'cinder-api')
correct_input = {
:name => 'test_type',
:properties => ['some_key1=value', 'some_key2=value1,value2']
}
cinder_type = Puppet::Type.type(:cinder_type).new(correct_input)
catalog.add_resource service, cinder_type
dependency = cinder_type.autorequire
expect(dependency.size).to eq(1)
expect(dependency[0].target).to eq(cinder_type)
expect(dependency[0].source).to eq(service)
end
end