Add Berkshelf support

Add Berkshelf support in murano-agent. If Berkshelf support is enabled
(Execution plan with "Type": "Chef" and "useBerkshelf": True), then the
image is expected to contain Berkshelf.

Change-Id: I036b5b9b7e1202d26fa2fd16591b680755cbfbc1
Partial-Implements: blueprint support-chef-berkshelf
This commit is contained in:
Olivier Lemasle 2015-11-02 09:06:41 +01:00
parent d075358882
commit ec60c7139e
5 changed files with 288 additions and 9 deletions

View File

@ -33,7 +33,7 @@ from muranoagent import execution_result as ex_result
CONF = config.CONF
LOG = logging.getLogger(__name__)
max_format_version = semantic_version.Spec('<=2.1.0')
max_format_version = semantic_version.Spec('<=2.2.0')
class MuranoAgent(service.Service):
@ -212,7 +212,7 @@ class MuranoAgent(service.Service):
2, 'Script {0} misses entry point {1}'.format(
name, script['EntryPoint']))
if plan_format_version in semantic_version.Spec('==2.1.0'):
if plan_format_version in semantic_version.Spec('>=2.1.0'):
if script['Type'] not in ('Application', 'Chef', 'Puppet'):
raise exc.IncorrectFormat(
2, 'Script has not a valid type {0}'.format(
@ -229,6 +229,18 @@ class MuranoAgent(service.Service):
'executors. :: needed'.format(name,
script['EntryPoint']))
for option in script['Options']:
if option in ('useBerkshelf', 'berksfilePath'):
if plan_format_version in semantic_version.Spec('<2.2.0'):
raise exc.IncorrectFormat(
2, 'Script has an option {0} invalid '
'for version {1}'.format(option,
plan_format_version))
elif script['Type'] != 'Chef':
raise exc.IncorrectFormat(
2, 'Script has an option {0} invalid '
'for type {1}'.format(option, script['Type']))
for additional_file in script.get('Files', []):
mns_error = ('Script {0} misses file {1}'.
format(name, additional_file))

View File

@ -31,6 +31,11 @@ LOG = logging.getLogger(__name__)
@executors.executor('Chef')
class ChefExecutor(chef_puppet_executor_base.ChefPuppetExecutorBase):
def load(self, path, options):
super(ChefExecutor, self).load(path, options)
self._use_berkshelf = options.get('useBerkshelf', False)
self._berksfile_path = options.get('berksfilePath', None)
def run(self, function, recipe_attributes=None, input=None):
"""It runs the chef executor.
@ -40,8 +45,10 @@ class ChefExecutor(chef_puppet_executor_base.ChefPuppetExecutorBase):
"""
self._valid_module_name()
cookbook_path = self._create_cookbook_path(self.module_name)
try:
self._configure_chef()
self._configure_chef(cookbook_path)
self._generate_manifest(self.module_name,
self.module_recipe, recipe_attributes)
except Exception as e:
@ -61,14 +68,54 @@ class ChefExecutor(chef_puppet_executor_base.ChefPuppetExecutorBase):
result = self._execute_command(command)
return bunch.Bunch(result)
def _configure_chef(self):
def _create_cookbook_path(self, cookbook_name):
"""It defines a path where all required cookbooks are located."""
path = os.path.abspath(self._path)
if self._use_berkshelf:
LOG.debug('Using Berkshelf')
# Get Berksfile
if self._berksfile_path is None:
self._berksfile_path = cookbook_name + '/Berksfile'
berksfile = os.path.join(path, self._berksfile_path)
if not os.path.isfile(berksfile):
msg = "Berskfile {0} not found".format(berksfile)
LOG.debug(msg)
raise muranoagent.exceptions.CustomException(
0,
message=msg,
additional_data=None)
# Create cookbooks path
cookbook_path = os.path.join(path, "berks-cookbooks")
if not os.path.isdir(cookbook_path):
os.makedirs(cookbook_path)
# Vendor cookbook and its dependencies to cookbook_path
command = 'berks vendor --berksfile={0} {1}'.format(
berksfile,
cookbook_path)
result = self._execute_command(command)
if result['exitCode'] != 0:
raise muranoagent.exceptions.CustomException(
0,
message='Berks returned error code',
additional_data=result)
return cookbook_path
else:
return path
def _configure_chef(self, cookbook_path):
"""It generates the chef files for configuration."""
solo_file = os.path.join(self._path, 'solo.rb')
if not os.path.exists(solo_file):
if not os.path.isdir(self._path):
os.makedirs(self._path)
with open(solo_file, "w+") as f:
f.write('cookbook_path \"' + self._path + '\"')
f.write('cookbook_path \"' + cookbook_path + '\"')
def _generate_manifest(self, cookbook_name,
cookbook_recipe, recipe_attributes):

View File

@ -189,3 +189,115 @@ class PuppetExPlanDownloable(fixtures.Fixture):
Version='1.0.0'
)
self.addCleanup(delattr, self, 'execution_plan')
class ExPlanBerkshelf(fixtures.Fixture):
def setUp(self):
super(ExPlanBerkshelf, self).setUp()
self.execution_plan = bunch.Bunch(
Action='Execute',
Body='return deploy(args.appName).stdout\n',
Files={
'ID1': {
'Name': 'tomcat.git',
'Type': 'Downloadable',
'URL': 'https://github.com/tomcat.git'
}
},
FormatVersion='2.2.0',
ID='ID',
Name='Deploy Chef',
Parameters={},
Scripts={
'deploy': {
'EntryPoint': 'cookbook::recipe',
'Files': [
'ID1'
],
'Options': {
'captureStderr': True,
'captureStdout': True,
'useBerkshelf': True
},
'Type': 'Chef',
'Version': '1.0.0'
}
},
Version='1.0.0'
)
self.addCleanup(delattr, self, 'execution_plan')
class ExPlanCustomBerskfile(fixtures.Fixture):
def setUp(self):
super(ExPlanCustomBerskfile, self).setUp()
self.execution_plan = bunch.Bunch(
Action='Execute',
Body='return deploy(args.appName).stdout\n',
Files={
'ID1': {
'Name': 'tomcat.git',
'Type': 'Downloadable',
'URL': 'https://github.com/tomcat.git'
}
},
FormatVersion='2.2.0',
ID='ID',
Name='Deploy Chef',
Parameters={},
Scripts={
'deploy': {
'EntryPoint': 'cookbook::recipe',
'Files': [
'ID1'
],
'Options': {
'captureStderr': True,
'captureStdout': True,
'useBerkshelf': True,
'berksfilePath': 'custom/customFile'
},
'Type': 'Chef',
'Version': '1.0.0'
}
},
Version='1.0.0'
)
self.addCleanup(delattr, self, 'execution_plan')
class ExPlanBerkWrongVersion(fixtures.Fixture):
def setUp(self):
super(ExPlanBerkWrongVersion, self).setUp()
self.execution_plan = bunch.Bunch(
Action='Execute',
Body='return deploy(args.appName).stdout\n',
Files={
'ID1': {
'Name': 'tomcat.git',
'Type': 'Downloadable',
'URL': 'https://github.com/tomcat.git'
}
},
FormatVersion='2.1.0',
ID='ID',
Name='Deploy Chef',
Parameters={},
Scripts={
'deploy': {
'EntryPoint': 'cookbook::recipe',
'Files': [
'ID1'
],
'Options': {
'captureStderr': True,
'captureStdout': True,
'useBerkshelf': True
},
'Type': 'Chef',
'Version': '1.0.0'
}
},
Version='1.0.0'
)
self.addCleanup(delattr, self, 'execution_plan')

View File

@ -16,6 +16,8 @@ import bunch
import fixtures
import json
import mock
from mock import ANY
import os
from muranoagent.common import config as cfg
from muranoagent import exceptions as ex
@ -49,13 +51,16 @@ class TestChefExecutor(base.MuranoAgentTestCase, fixtures.TestWithFixtures):
@mock.patch('subprocess.Popen')
@mock.patch('__builtin__.open')
@mock.patch('os.path.exists')
def test_cookbook(self, mock_exist, open_mock, mock_subproc_popen):
@mock.patch('os.path.isdir')
def test_cookbook(self, mock_isdir, mock_exist, open_mock,
mock_subproc_popen):
"""It tests chef executor."""
self._open_mock(open_mock)
mock_exist.return_value = True
mock_isdir.return_value = True
process_mock = mock.Mock()
attrs = {'communicate.return_value': ('ouput', 'ok'),
attrs = {'communicate.return_value': ('output', 'ok'),
'poll.return_value': 0}
process_mock.configure_mock(**attrs)
mock_subproc_popen.return_value = process_mock
@ -68,13 +73,16 @@ class TestChefExecutor(base.MuranoAgentTestCase, fixtures.TestWithFixtures):
@mock.patch('subprocess.Popen')
@mock.patch('__builtin__.open')
@mock.patch('os.path.exists')
def test_cookbook_error(self, mock_exist, open_mock, mock_subproc_popen):
@mock.patch('os.path.isdir')
def test_cookbook_error(self, mock_isdir, mock_exist, open_mock,
mock_subproc_popen):
"""It tests chef executor with error in the request."""
self._open_mock(open_mock)
mock_exist.return_value = True
mock_isdir.return_value = True
process_mock = mock.Mock()
attrs = {'communicate.return_value': ('ouput', 'error'),
attrs = {'communicate.return_value': ('output', 'error'),
'poll.return_value': 2}
process_mock.configure_mock(**attrs)
mock_subproc_popen.return_value = process_mock
@ -91,6 +99,97 @@ class TestChefExecutor(base.MuranoAgentTestCase, fixtures.TestWithFixtures):
self.assertRaises(ex.CustomException, chef_executor.run,
'test')
def test_chef_no_berkshelf(self):
"""It tests the cookbook path if Berkshelf is not enabled"""
template = self.useFixture(ep.ExPlanDownloable()).execution_plan
self.chef_executor.load('path',
template['Scripts'].values()[0]['Options'])
cookbook_path = self.chef_executor._create_cookbook_path('cookbook')
self.assertEqual(cookbook_path, os.path.abspath('path'))
@mock.patch('subprocess.Popen')
@mock.patch('os.path.isfile')
def test_chef_berkshelf_default_berksfile(self, mock_isfile,
mock_subproc_popen):
"""It tests Berkshelf usage if no Berksfile path is provided"""
mock_isfile.return_value = True
process_mock = mock.Mock()
attrs = {'communicate.return_value': ('output', 'ok'),
'poll.return_value': 0}
process_mock.configure_mock(**attrs)
mock_subproc_popen.return_value = process_mock
template = self.useFixture(ep.ExPlanBerkshelf()).execution_plan
self.chef_executor.load('path',
template['Scripts'].values()[0]['Options'])
self.chef_executor.module_name = 'test'
cookbook_path = self.chef_executor._create_cookbook_path('cookbook')
self.assertEqual(cookbook_path,
os.path.abspath('path/berks-cookbooks'))
expected_command = 'berks vendor --berksfile={0} {1}'.format(
os.path.abspath('path/cookbook/Berksfile'),
cookbook_path)
mock_subproc_popen.assert_called_once_with(expected_command,
cwd=ANY,
shell=ANY,
stdout=ANY,
stderr=ANY,
universal_newlines=ANY)
@mock.patch('subprocess.Popen')
@mock.patch('os.path.isfile')
def test_chef_berkshelf_custom_berksfile(self, mock_isfile,
mock_subproc_popen):
"""It tests Berkshelf usage if a custom Berksfile is provided"""
mock_isfile.return_value = True
process_mock = mock.Mock()
attrs = {'communicate.return_value': ('output', 'ok'),
'poll.return_value': 0}
process_mock.configure_mock(**attrs)
mock_subproc_popen.return_value = process_mock
template = self.useFixture(ep.ExPlanCustomBerskfile()).execution_plan
self.chef_executor.load('path',
template['Scripts'].values()[0]['Options'])
self.chef_executor.module_name = 'test'
cookbook_path = self.chef_executor._create_cookbook_path('cookbook')
self.assertEqual(cookbook_path,
os.path.abspath('path/berks-cookbooks'))
expected_command = 'berks vendor --berksfile={0} {1}'.format(
os.path.abspath('path/custom/customFile'),
cookbook_path)
mock_subproc_popen.assert_called_once_with(expected_command,
cwd=ANY,
shell=ANY,
stdout=ANY,
stderr=ANY,
universal_newlines=ANY)
@mock.patch('subprocess.Popen')
@mock.patch('os.path.isfile')
def test_chef_berkshelf_error(self, mock_isfile,
mock_subproc_popen):
"""It tests if Berkshelf throws an error"""
mock_isfile.return_value = True
process_mock = mock.Mock()
attrs = {'communicate.return_value': ('output', 'error'),
'poll.return_value': 2}
process_mock.configure_mock(**attrs)
mock_subproc_popen.return_value = process_mock
template = self.useFixture(ep.ExPlanBerkshelf()).execution_plan
self.chef_executor.load('path',
template['Scripts'].values()[0]['Options'])
self.chef_executor.module_name = 'test'
self.assertRaises(ex.CustomException,
self.chef_executor._create_cookbook_path,
'cookbook')
def _open_mock(self, open_mock):
context_manager_mock = mock.Mock()
open_mock.return_value = context_manager_mock

View File

@ -71,3 +71,12 @@ class TestApp(base.MuranoAgentTestCase, fixtures.FunctionFixture):
template = self.useFixture(ep.ExPlanDownloableNoFiles()).execution_plan
self.assertRaises(exc.IncorrectFormat,
self.agent._verify_plan, template)
def test_verify_execution_plan_berkshelf(self):
template = self.useFixture(ep.ExPlanBerkshelf()).execution_plan
self.agent._verify_plan(template)
def test_verify_execution_plan_berkshelf_wrong_version(self):
template = self.useFixture(ep.ExPlanBerkWrongVersion()).execution_plan
self.assertRaises(exc.IncorrectFormat,
self.agent._verify_plan, template)