Support update parameters of a resource

This patch implements support to update parameters of an already
existing resource using "crm configure load update FILE"

The parameters of a resource are hashed using md5 and stored in the kv
store, when the checksum doesn't match the resource is updated,
otherwise discarded.

Change-Id: I5735eaa1309c57e3620b0a6f68ffe13ec8165592
Closes-Bug: 1753432
This commit is contained in:
Felipe Reyes 2018-06-06 19:38:01 -04:00
parent 63bb8018b4
commit 639dadb141
4 changed files with 162 additions and 10 deletions

View File

@ -14,13 +14,13 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import shutil
import os
import sys
import glob
import os
import shutil
import socket
import sys
import pcmk
import socket
from charmhelpers.core.hookenv import (
is_leader,
@ -357,6 +357,15 @@ def ha_relation_changed():
'-inf: pingd lte 0' % (res_name, res_name))
pcmk.commit(cmd)
else:
# the resource already exists so it will be updated.
code = pcmk.crm_update_resource(res_name, res_type,
resource_params.get(res_name))
if code != 0:
msg = "Cannot update pcmkr resource: {}".format(res_name)
status_set('blocked', msg)
raise Exception(msg)
log('Configuring Groups: %s' % (groups), level=DEBUG)
for grp_name, grp_params in groups.iteritems():
if not pcmk.crm_opt_exists(grp_name):

View File

@ -13,17 +13,23 @@
# limitations under the License.
import commands
import hashlib
import re
import subprocess
import socket
import tempfile
import time
import xml.etree.ElementTree as etree
from distutils.version import StrictVersion
from StringIO import StringIO
from charmhelpers.core import unitdata
from charmhelpers.core.hookenv import (
log,
ERROR,
INFO,
DEBUG,
WARNING,
)
@ -51,7 +57,7 @@ def wait_for_pcmk(retries=12, sleep=10):
def commit(cmd):
subprocess.call(cmd.split())
return subprocess.call(cmd.split())
def is_resource_present(resource):
@ -221,3 +227,60 @@ def crm_version():
raise ValueError('error parsin crm version: %s' % ver)
else:
return StrictVersion(matched.group(1))
def crm_update_resource(res_name, res_type, res_params=None, force=False):
"""Update a resource using `crm configure load update`
:param res_name: resource name
:param res_type: resource type (e.g. IPaddr2)
:param res_params: resource's parameters (e.g. "params ip=10.5.250.250")
"""
db = unitdata.kv()
res_hash = resource_checksum(res_name, res_type, res_params)
if not force and db.get('{}-{}'.format(res_name, res_type)) == res_hash:
log("Resource {} already defined and parameters haven't changed"
.format(res_name))
return 0
with tempfile.NamedTemporaryFile() as f:
f.write('primitive {} {}'.format(res_name, res_type))
if res_params:
f.write(' \\\n\t{}'.format(res_params))
else:
f.write('\n')
f.flush()
f.seek(0)
log('Updating resource {}'.format(res_name), level=INFO)
log('File content:\n{}'.format(f.read()), level=DEBUG)
cmd = "crm configure load update {}".format(f.name)
log('Update command: {}'.format(cmd))
retcode = commit(cmd)
if retcode == 0:
level = DEBUG
else:
level = WARNING
log('crm command exit code: {}'.format(retcode), level=level)
if retcode == 0:
db.set('{}-{}'.format(res_name, res_type), res_hash)
return retcode
def resource_checksum(res_name, res_type, res_params=None):
"""Create a md5 checksum of the resource parameters.
:param res_name: resource name
:param res_type: resource type (e.g. IPaddr2)
:param res_params: resource's parameters (e.g. "params ip=10.5.250.250")
"""
m = hashlib.md5()
m.update(res_type)
m.update(res_params)
return m.hexdigest()

View File

@ -14,6 +14,7 @@
import mock
import os
import shutil
import sys
import tempfile
import unittest
@ -32,6 +33,11 @@ class TestCorosyncConf(unittest.TestCase):
def setUp(self):
self.tmpdir = tempfile.mkdtemp()
self.tmpfile = tempfile.NamedTemporaryFile(delete=False)
def tearDown(self):
shutil.rmtree(self.tmpdir)
os.remove(self.tmpfile.name)
@mock.patch.object(hooks, 'write_maas_dns_address')
@mock.patch('pcmk.wait_for_pcmk')
@ -56,7 +62,13 @@ class TestCorosyncConf(unittest.TestCase):
configure_cluster_global, configure_corosync,
is_leader, crm_opt_exists,
wait_for_pcmk, write_maas_dns_address):
crm_opt_exists.return_value = False
def fake_crm_opt_exists(res_name):
# res_ubuntu will take the "update resource" route
return res_name == "res_ubuntu"
crm_opt_exists.side_effect = fake_crm_opt_exists
commit.return_value = 0
is_leader.return_value = True
related_units.return_value = ['ha/0', 'ha/1', 'ha/2']
get_cluster_nodes.return_value = ['10.0.3.2', '10.0.3.3', '10.0.3.4']
@ -75,8 +87,10 @@ class TestCorosyncConf(unittest.TestCase):
'groups': {'grp_foo': 'res_foo'},
'colocations': {'co_foo': 'inf: grp_foo cl_foo'},
'resources': {'res_foo': 'ocf:heartbeat:IPaddr2',
'res_bar': 'ocf:heartbear:IPv6addr'},
'resource_params': {'res_foo': 'params bar'},
'res_bar': 'ocf:heartbear:IPv6addr',
'res_ubuntu': 'IPaddr2'},
'resource_params': {'res_foo': 'params bar',
'res_ubuntu': 'params ubuntu=42'},
'ms': {'ms_foo': 'res_foo meta notify=true'},
'orders': {'foo_after': 'inf: res_foo ms_foo'}}
@ -85,7 +99,10 @@ class TestCorosyncConf(unittest.TestCase):
parse_data.side_effect = fake_parse_data
hooks.ha_relation_changed()
with mock.patch.object(tempfile, "NamedTemporaryFile",
side_effect=lambda: self.tmpfile):
hooks.ha_relation_changed()
relation_set.assert_any_call(relation_id='hanode:1', ready=True)
configure_stonith.assert_called_with()
configure_monitor_host.assert_called_with()
@ -101,7 +118,11 @@ class TestCorosyncConf(unittest.TestCase):
('ms', 'ms'),
('order', 'orders')]:
for name, params in rel_get_data[key].items():
if name in rel_get_data['resource_params']:
if name == "res_ubuntu":
commit.assert_any_call(
'crm configure load update %s' % self.tmpfile.name)
elif name in rel_get_data['resource_params']:
res_params = rel_get_data['resource_params'][name]
commit.assert_any_call(
'crm -w -F configure %s %s %s %s' % (kw, name, params,

View File

@ -14,8 +14,11 @@
import mock
import pcmk
import os
import tempfile
import unittest
from distutils.version import StrictVersion
from charmhelpers.core import unitdata
CRM_CONFIGURE_SHOW_XML = '''<?xml version="1.0" ?>
@ -73,6 +76,12 @@ CRM_CONFIGURE_SHOW_XML_MAINT_MODE_TRUE = '''<?xml version="1.0" ?>
class TestPcmk(unittest.TestCase):
def setUp(self):
self.tmpfile = tempfile.NamedTemporaryFile(delete=False)
def tearDown(self):
os.remove(self.tmpfile.name)
@mock.patch('commands.getstatusoutput')
def test_crm_res_running_true(self, getstatusoutput):
getstatusoutput.return_value = (0, ("resource res_nova_consoleauth is "
@ -167,3 +176,53 @@ class TestPcmk(unittest.TestCase):
mock_check_output.assert_called_with(['crm', 'configure', 'property',
'maintenance-mode=false'],
universal_newlines=True)
@mock.patch('subprocess.call')
def test_crm_update_resource(self, mock_call):
mock_call.return_value = 0
with mock.patch.object(tempfile, "NamedTemporaryFile",
side_effect=lambda: self.tmpfile):
pcmk.crm_update_resource('res_test', 'IPaddr2',
('params ip=1.2.3.4 '
'cidr_netmask=255.255.0.0'))
mock_call.assert_any_call(['crm', 'configure', 'load',
'update', self.tmpfile.name])
with open(self.tmpfile.name, 'r') as f:
self.assertEqual(f.read(),
('primitive res_test IPaddr2 \\\n'
'\tparams ip=1.2.3.4 cidr_netmask=255.255.0.0'))
@mock.patch('subprocess.call')
def test_crm_update_resource_exists_in_kv(self, mock_call):
db = unitdata.kv()
db.set('res_test-IPaddr2', 'ef395293b1b7c29c5bf1c99774f75cf4')
pcmk.crm_update_resource('res_test', 'IPaddr2',
'params ip=1.2.3.4 cidr_netmask=255.0.0.0')
mock_call.assert_called_once_with([
'juju-log',
"Resource res_test already defined and parameters haven't changed"
])
@mock.patch('subprocess.call')
def test_crm_update_resource_exists_in_kv_force_true(self, mock_call):
db = unitdata.kv()
db.set('res_test-IPaddr2', 'ef395293b1b7c29c5bf1c99774f75cf4')
with mock.patch.object(tempfile, "NamedTemporaryFile",
side_effect=lambda: self.tmpfile):
pcmk.crm_update_resource('res_test', 'IPaddr2',
('params ip=1.2.3.4 '
'cidr_netmask=255.0.0.0'),
force=True)
mock_call.assert_any_call(['crm', 'configure', 'load',
'update', self.tmpfile.name])
def test_resource_checksum(self):
r = pcmk.resource_checksum('res_test', 'IPaddr2',
'params ip=1.2.3.4 cidr_netmask=255.0.0.0')
self.assertEqual(r, 'ef395293b1b7c29c5bf1c99774f75cf4')