From mboxrd@z Thu Jan 1 00:00:00 1970 From: rmccabe@sourceware.org Date: 25 Jun 2007 16:11:33 -0000 Subject: [Cluster-devel] conga luci/cluster/form-macros luci/cluster/in ... Message-ID: <20070625161133.27221.qmail@sourceware.org> List-Id: To: cluster-devel.redhat.com MIME-Version: 1.0 Content-Type: text/plain; charset="us-ascii" Content-Transfer-Encoding: 7bit CVSROOT: /cvs/cluster Module name: conga Changes by: rmccabe at sourceware.org 2007-06-25 16:11:31 Modified files: luci/cluster : form-macros index_html resource_form_handlers.js validate_fence.js luci/plone-custom: conga.js luci/site/luci/Extensions: LuciClusterActions.py LuciClusterInfo.py cluster_adapters.py conga_constants.py ricci/modules/storage: VG.cpp parted_wrapper.cpp Log message: Fixes from the RHEL5 branch Patches: http://sourceware.org/cgi-bin/cvsweb.cgi/conga/luci/cluster/form-macros.diff?cvsroot=cluster&r1=1.199&r2=1.200 http://sourceware.org/cgi-bin/cvsweb.cgi/conga/luci/cluster/index_html.diff?cvsroot=cluster&r1=1.33&r2=1.34 http://sourceware.org/cgi-bin/cvsweb.cgi/conga/luci/cluster/resource_form_handlers.js.diff?cvsroot=cluster&r1=1.35&r2=1.36 http://sourceware.org/cgi-bin/cvsweb.cgi/conga/luci/cluster/validate_fence.js.diff?cvsroot=cluster&r1=1.6&r2=1.7 http://sourceware.org/cgi-bin/cvsweb.cgi/conga/luci/plone-custom/conga.js.diff?cvsroot=cluster&r1=1.4&r2=1.5 http://sourceware.org/cgi-bin/cvsweb.cgi/conga/luci/site/luci/Extensions/LuciClusterActions.py.diff?cvsroot=cluster&r1=1.2&r2=1.3 http://sourceware.org/cgi-bin/cvsweb.cgi/conga/luci/site/luci/Extensions/LuciClusterInfo.py.diff?cvsroot=cluster&r1=1.2&r2=1.3 http://sourceware.org/cgi-bin/cvsweb.cgi/conga/luci/site/luci/Extensions/cluster_adapters.py.diff?cvsroot=cluster&r1=1.256&r2=1.257 http://sourceware.org/cgi-bin/cvsweb.cgi/conga/luci/site/luci/Extensions/conga_constants.py.diff?cvsroot=cluster&r1=1.40&r2=1.41 http://sourceware.org/cgi-bin/cvsweb.cgi/conga/ricci/modules/storage/VG.cpp.diff?cvsroot=cluster&r1=1.12&r2=1.13 http://sourceware.org/cgi-bin/cvsweb.cgi/conga/ricci/modules/storage/parted_wrapper.cpp.diff?cvsroot=cluster&r1=1.10&r2=1.11 --- conga/luci/cluster/form-macros 2007/06/25 16:03:37 1.199 +++ conga/luci/cluster/form-macros 2007/06/25 16:11:30 1.200 @@ -3117,7 +3117,11 @@ + + @@ -3129,6 +3133,7 @@ @@ -3537,6 +3542,7 @@ --- conga/luci/cluster/index_html 2007/06/25 16:03:37 1.33 +++ conga/luci/cluster/index_html 2007/06/25 16:11:30 1.34 @@ -38,7 +38,14 @@ resmap python:here.getClusterOS(ri_agent); global isVirtualized resmap/isVirtualized | nothing; global os_version resmap/os | nothing; - global isBusy python:here.isClusterBusy(request)" /> + global isBusy python:here.isClusterBusy(request); + global firsttime request/busyfirst | nothing" /> + + + + + --- conga/luci/cluster/resource_form_handlers.js 2007/06/25 16:03:37 1.35 +++ conga/luci/cluster/resource_form_handlers.js 2007/06/25 16:11:30 1.36 @@ -500,12 +500,12 @@ if (res_type == 'hidden' || res_type == 'text' || res_type == 'password') { - temp += ''; + temp += ''; } else if (res_type == 'checkbox' || res_type == 'radio') { if (input_elem[j].checked) { - temp += ''; + temp += ''; } - form_xml += '
' + temp + '
'; + form_xml += '
' + temp + '
'; } if (!svc_name) --- conga/luci/cluster/validate_fence.js 2007/02/16 23:25:27 1.6 +++ conga/luci/cluster/validate_fence.js 2007/06/25 16:11:30 1.7 @@ -218,27 +218,27 @@ if (res_type == 'hidden' || res_type == 'text' || res_type == 'password') { - temp += ''; + temp += ''; } else if (res_type == 'checkbox' || res_type == 'radio') { if (input_elem[j].checked) { - temp += ''; + temp += ''; } } } var select_elem = form[i].getElementsByTagName('select'); for (var j = 0 ; j < select_elem.length ; j++) { - temp += ''; + temp += ''; } - form_xml += '
' + temp + '
'; + form_xml += '
' + temp + '
'; } master_form.fence_xml.value = '' + form_xml + ''; --- conga/luci/plone-custom/conga.js 2006/11/03 21:47:27 1.4 +++ conga/luci/plone-custom/conga.js 2007/06/25 16:11:30 1.5 @@ -5,6 +5,12 @@ return (0); } +function escapeXML(str) { + if (!str) + return ''; + return str.replace('&', '&').replace('<', '<').replace('>', '>').replace('"', '"').replace('\'', '''); +} + function popup_window(url, width_percent, height_percent) { var width = window.innerWidth * (width_percent / 100); var height = window.innerHeight * (height_percent / 100); --- conga/luci/site/luci/Extensions/LuciClusterActions.py 2007/06/25 16:03:38 1.2 +++ conga/luci/site/luci/Extensions/LuciClusterActions.py 2007/06/25 16:11:30 1.3 @@ -17,7 +17,7 @@ CLUSTER_NODE_NEED_AUTH from conga_constants import CLUSTER_CONFIG, LUCI_DEBUG_MODE, \ - NODE_DELETE, CLUSTER_DELETE, CLUSTERLIST, \ + NODE_DELETE, NODE_FORCE_DELETE, CLUSTER_DELETE, CLUSTERLIST, \ NODE_FENCE, NODE_JOIN_CLUSTER, NODE_LEAVE_CLUSTER, NODE_REBOOT, \ RESOURCE_ADD, RESOURCE_CONFIG, RESOURCE_REMOVE, \ SERVICE_DELETE, SERVICE_RESTART, SERVICE_START, SERVICE_STOP @@ -283,6 +283,64 @@ % (nodename_resolved, e, str(e))) return True +def NodeForceDeleteFromCluster(self, model, clustername, nodename, nodename_resolved): + rc = getRicciAgent(self, clustername, + exclude_names=[ nodename_resolved, nodename ], exclude_busy=True) + + if rc is None: + rc = getRicciAgent(self, clustername, + exclude_names=[ nodename_resolved, nodename ]) + + if rc is None: + if LUCI_DEBUG_MODE is True: + luci_log.debug_verbose('NFDFC0: no agent to delete node %s "%s"' \ + % (nodename_resolved, clustername)) + return None + + try: + model.deleteNodeByName(nodename.lower()) + except Exception, e: + if LUCI_DEBUG_MODE is True: + luci_log.debug_verbose('NFDFC1: deleteNode %s: %r %s' \ + % (nodename, e, str(e))) + return None + + try: + model.setModified(True) + str_buf = str(model.exportModelAsString()) + if not str_buf: + raise Exception, 'model string is blank' + except Exception, e: + if LUCI_DEBUG_MODE is True: + luci_log.debug_verbose('NFDFC2: exportModelAsString: %r %s' \ + % (e, str(e))) + return None + + batch_number, result = rq.setClusterConf(rc, str_buf) + if batch_number is None or result is None: + if LUCI_DEBUG_MODE is True: + luci_log.debug_verbose('NFDFC3: batch number is None') + return None + + try: + ret = delClusterSystem(self, clustername, nodename_resolved) + if ret is not None: + raise Exception, ret + except Exception, e: + if LUCI_DEBUG_MODE is True: + luci_log.debug_verbose('NFDFC4: error deleting %s: %r %s' \ + % (nodename_resolved, e, str(e))) + + try: + set_node_flag(self, clustername, rc.hostname(), + str(batch_number), NODE_FORCE_DELETE, + 'Forcing the deletion of node "%s"' % nodename) + except Exception, e: + if LUCI_DEBUG_MODE is True: + luci_log.debug_verbose('NFDFC5: failed to set flags: %r %s' \ + % (e, str(e))) + return True + def NodeDeleteFromCluster( self, rc, model, @@ -354,7 +412,7 @@ batch_number, result = rq.setClusterConf(rc2, str_buf) if batch_number is None: if LUCI_DEBUG_MODE is True: - luci_log.debug_verbose('ND8: batch number is None after del node in NTP') + luci_log.debug_verbose('ND8: batch number is None') return None try: --- conga/luci/site/luci/Extensions/LuciClusterInfo.py 2007/06/25 16:03:38 1.2 +++ conga/luci/site/luci/Extensions/LuciClusterInfo.py 2007/06/25 16:11:30 1.3 @@ -17,7 +17,7 @@ from conga_constants import CLUSTER_CONFIG, CLUSTER_DELETE, \ CLUSTER_PROCESS, CLUSTER_RESTART, CLUSTER_START, CLUSTER_STOP, \ - FDOM, FENCEDEV, NODE, NODE_ACTIVE, \ + NODE_FORCE_DELETE, FDOM, FENCEDEV, NODE, NODE_ACTIVE, \ NODE_ACTIVE_STR, NODE_DELETE, NODE_FENCE, NODE_INACTIVE, \ NODE_INACTIVE_STR, NODE_JOIN_CLUSTER, NODE_LEAVE_CLUSTER, \ NODE_PROCESS, NODE_REBOOT, NODE_UNKNOWN, NODE_UNKNOWN_STR, \ @@ -149,18 +149,10 @@ if not doc: try: from LuciDB import getClusterStatusDB + fvars = GetReqVars(request, [ 'clustername' ]) - clustername = cluname + clustername = fvars['clustername'] if clustername is None: - try: - clustername = request['clustername'] - except: - try: - clustername = request.form['clustername'] - except: - pass - - if not clustername: raise Exception, 'unable to determine cluster name' cinfo = getClusterStatusDB(self, clustername) @@ -860,6 +852,8 @@ else: infohash['fence_url'] = '%s?pagetype=%s&task=%s&nodename=%s&clustername=%s' \ % (baseurl, NODE_PROCESS, NODE_FENCE, nodename, clustername) + infohash['force_delete_url'] = '%s?pagetype=%s&task=%s&nodename=%s&clustername=%s' \ + % (baseurl, NODE_PROCESS, NODE_FORCE_DELETE, nodename, clustername) # figure out current services running on this node svc_dict_list = list() @@ -1021,6 +1015,8 @@ else: nl_map['fence_it_url'] = '%s?pagetype=%s&task=%s&nodename=%s&clustername=%s' \ % (baseurl, NODE_PROCESS, NODE_FENCE, name, clustername) + nl_map['force_delete_url'] = '%s?pagetype=%s&task=%s&nodename=%s&clustername=%s' \ + % (baseurl, NODE_PROCESS, NODE_FORCE_DELETE, name, clustername) # figure out current services running on this node svc_dict_list = list() --- conga/luci/site/luci/Extensions/cluster_adapters.py 2007/06/25 16:03:39 1.256 +++ conga/luci/site/luci/Extensions/cluster_adapters.py 2007/06/25 16:11:30 1.257 @@ -36,12 +36,12 @@ DISABLE_SVC_TASK, ENABLE_SVC_TASK, FDOM, FDOM_ADD, FENCEDEV, \ FENCEDEV_NODE_CONFIG, FENCEDEVS, FLAG_DESC, INSTALL_TASK, \ LAST_STATUS, LUCI_DEBUG_MODE, NODE, NODE_ADD, NODE_DELETE, \ - NODE_FENCE, NODE_JOIN_CLUSTER, NODE_LEAVE_CLUSTER, NODE_REBOOT, \ - NODES, POSSIBLE_REBOOT_MESSAGE, PRE_CFG, PRE_INSTALL, PRE_JOIN, \ - REBOOT_TASK, REDIRECT_MSG, RESOURCES, RICCI_CONNECT_FAILURE, \ + NODE_FENCE, NODE_FORCE_DELETE, NODE_JOIN_CLUSTER, NODE_LEAVE_CLUSTER, \ + NODE_REBOOT, NODES, POSSIBLE_REBOOT_MESSAGE, PRE_CFG, PRE_INSTALL, \ + PRE_JOIN, REBOOT_TASK, REDIRECT_MSG, RESOURCES, RICCI_CONNECT_FAILURE, \ RICCI_CONNECT_FAILURE_MSG, SEND_CONF, SERVICE_ADD, SERVICE_CONFIG, \ SERVICE_LIST, SERVICES, START_NODE, TASKTYPE, VM_ADD, VM_CONFIG, \ - REDIRECT_SEC + REDIRECT_SEC, LUCI_CLUSTER_BASE_URL from FenceHandler import validateNewFenceDevice, \ validateFenceDevice, validate_fenceinstance, \ @@ -719,7 +719,7 @@ errors = list() try: - form_xml = request['form_xml'] + form_xml = request['form_xml'].strip() if not form_xml: raise KeyError, 'form_xml must not be blank' except Exception, e: @@ -741,7 +741,7 @@ doc = minidom.parseString(form_xml) forms = doc.getElementsByTagName('form') if len(forms) < 1: - raise + raise Exception, 'invalid XML' except Exception, e: if LUCI_DEBUG_MODE is True: luci_log.debug_verbose('vSA1: error: %r %s' % (e, str(e))) @@ -1682,7 +1682,7 @@ errors = list() try: - form_xml = request['fence_xml'] + form_xml = request['fence_xml'].strip() if not form_xml: raise KeyError, 'form_xml must not be blank' except Exception, e: @@ -2639,7 +2639,9 @@ return getRicciAgent(self, clustername) def clusterTaskProcess(self, model, request): - fvar = GetReqVars(request, [ 'task', 'clustername' ]) + fvar = GetReqVars(request, [ 'task', 'clustername', 'URL' ]) + + baseurl = fvar['URL'] or LUCI_CLUSTER_BASE_URL task = fvar['task'] if task is None: @@ -2648,7 +2650,7 @@ return 'No cluster task was given' if not model: - cluname = fvar['cluname'] + cluname = fvar['clustername'] if cluname is None: if LUCI_DEBUG_MODE is True: luci_log.debug('CTP1: no cluster name') @@ -2686,7 +2688,7 @@ response = request.RESPONSE response.redirect('%s?pagetype=%s&clustername=%s&busyfirst=true' \ - % (request['URL'], redirect_page, model.getClusterName())) + % (baseurl, redirect_page, model.getClusterName())) def nodeTaskProcess(self, model, request): fvar = GetReqVars(request, [ 'task', 'clustername', 'nodename', 'URL' ]) @@ -2694,6 +2696,7 @@ task = fvar['task'] clustername = fvar['clustername'] nodename = fvar['nodename'] + baseurl = fvar['URL'] or LUCI_CLUSTER_BASE_URL if clustername is None: if LUCI_DEBUG_MODE is True: @@ -2713,10 +2716,9 @@ nodename_resolved = resolve_nodename(self, clustername, nodename) response = request.RESPONSE - if task != NODE_FENCE: - # Fencing is the only task for which we don't - # want to talk to the node on which the action is - # to be performed. + if task != NODE_FENCE and task != NODE_FORCE_DELETE: + # Fencing and forced deletion are the only tasks + # for which we don't want to talk to the target node. try: rc = RicciCommunicator(nodename_resolved) if not rc: @@ -2775,7 +2777,7 @@ return (False, {'errors': [ 'Node "%s" failed to leave cluster "%s"' % (nodename_resolved, clustername) ]}) response.redirect('%s?pagetype=%s&clustername=%s&busyfirst=true' \ - % (request['URL'], NODES, clustername)) + % (baseurl, NODES, clustername)) elif task == NODE_JOIN_CLUSTER: from LuciClusterActions import NodeJoinCluster if NodeJoinCluster(self, rc, clustername, nodename_resolved) is None: @@ -2784,7 +2786,7 @@ return (False, {'errors': [ 'Node "%s" failed to join cluster "%s"' % (nodename_resolved, clustername) ]}) response.redirect('%s?pagetype=%s&clustername=%s&busyfirst=true' \ - % (request['URL'], NODES, clustername)) + % (baseurl, NODES, clustername)) elif task == NODE_REBOOT: from LuciClusterActions import NodeReboot if NodeReboot(self, rc, clustername, nodename_resolved) is None: @@ -2794,7 +2796,7 @@ % nodename_resolved ]}) response.redirect('%s?pagetype=%s&clustername=%s&busyfirst=true' \ - % (request['URL'], NODES, clustername)) + % (baseurl, NODES, clustername)) elif task == NODE_FENCE: from LuciClusterActions import NodeFence if NodeFence(self, clustername, nodename, nodename_resolved) is None: @@ -2803,7 +2805,7 @@ return (False, {'errors': [ 'Fencing of node "%s" failed' \ % nodename_resolved]}) response.redirect('%s?pagetype=%s&clustername=%s&busyfirst=true' \ - % (request['URL'], NODES, clustername)) + % (baseurl, NODES, clustername)) elif task == NODE_DELETE: from LuciClusterActions import NodeDeleteFromCluster if NodeDeleteFromCluster(self, rc, model, clustername, nodename, nodename_resolved) is None: @@ -2812,7 +2814,16 @@ return (False, {'errors': [ 'Deletion of node "%s" from cluster "%s" failed' % (nodename_resolved, clustername) ]}) response.redirect('%s?pagetype=%s&clustername=%s&busyfirst=true' \ - % (request['URL'], NODES, clustername)) + % (baseurl, NODES, clustername)) + elif task == NODE_FORCE_DELETE: + from LuciClusterActions import NodeForceDeleteFromCluster + if NodeForceDeleteFromCluster(self, model, clustername, nodename, nodename_resolved) is None: + if LUCI_DEBUG_MODE is True: + luci_log.debug_verbose('NTP13: nodeForceDelete failed') + return (False, {'errors': [ 'Deletion of node "%s" from cluster "%s" failed' % (nodename_resolved, clustername) ]}) + + response.redirect('%s?pagetype=%s&clustername=%s&busyfirst=true' \ + % (baseurl, NODES, clustername)) def isClusterBusy(self, req): items = None @@ -3180,11 +3191,13 @@ fvars = GetReqVars(req, [ 'clustername', 'servicename', 'nodename', 'URL' ]) + baseurl = fvars['URL'] or LUCI_CLUSTER_BASE_URL + ret = RestartCluSvc(self, rc, fvars) if ret is None: response = req.RESPONSE response.redirect('%s?pagetype=%s&clustername=%s&busyfirst=true' \ - % (fvars['URL'], SERVICE_LIST, fvars['clustername'])) + % (baseurl, SERVICE_LIST, fvars['clustername'])) else: return ret @@ -3193,11 +3206,13 @@ fvars = GetReqVars(req, [ 'clustername', 'servicename', 'nodename', 'URL' ]) + baseurl = fvars['URL'] or LUCI_CLUSTER_BASE_URL + ret = StopCluSvc(self, rc, fvars) if ret is None: response = req.RESPONSE response.redirect('%s?pagetype=%s&clustername=%s&busyfirst=true' \ - % (fvars['URL'], SERVICE_LIST, fvars['clustername'])) + % (baseurl, SERVICE_LIST, fvars['clustername'])) else: return ret @@ -3206,11 +3221,13 @@ fvars = GetReqVars(req, [ 'clustername', 'servicename', 'nodename', 'URL' ]) + baseurl = fvars['URL'] or LUCI_CLUSTER_BASE_URL + ret = StartCluSvc(self, rc, fvars) if ret is None: response = req.RESPONSE response.redirect('%s?pagetype=%s&clustername=%s&busyfirst=true' \ - % (fvars['URL'], SERVICE_LIST, fvars['clustername'])) + % (baseurl, SERVICE_LIST, fvars['clustername'])) else: return ret @@ -3219,6 +3236,8 @@ fvars = GetReqVars(req, [ 'clustername', 'servicename', 'nodename', 'URL' ]) + baseurl = fvars['URL'] or LUCI_CLUSTER_BASE_URL + try: model = LuciExtractCluModel(self, req, cluster_name=fvars['clustername']) @@ -3231,7 +3250,7 @@ if ret is None: response = req.RESPONSE response.redirect('%s?pagetype=%s&clustername=%s&busyfirst=true' \ - % (fvars['URL'], SERVICES, fvars['clustername'])) + % (baseurl, SERVICES, fvars['clustername'])) else: return ret @@ -3240,11 +3259,13 @@ fvars = GetReqVars(req, [ 'clustername', 'servicename', 'nodename', 'URL' ]) + baseurl = fvars['URL'] or LUCI_CLUSTER_BASE_URL + ret = MigrateCluSvc(self, rc, fvars) if ret is None: response = req.RESPONSE response.redirect('%s?pagetype=%s&clustername=%s&busyfirst=true' \ - % (fvars['URL'], SERVICE_LIST, fvars['clustername'])) + % (baseurl, SERVICE_LIST, fvars['clustername'])) else: return ret @@ -3253,6 +3274,8 @@ fvars = GetReqVars(req, [ 'clustername', 'resourcename', 'nodename', 'URL' ]) + baseurl = fvars['URL'] or LUCI_CLUSTER_BASE_URL + try: model = LuciExtractCluModel(self, req, cluster_name=fvars['clustername']) @@ -3267,12 +3290,14 @@ if ret is None: response = req.RESPONSE response.redirect('%s?pagetype=%s&clustername=%s&busyfirst=true' \ - % (fvars['URL'], RESOURCES, fvars['clustername'])) + % (baseurl, RESOURCES, fvars['clustername'])) else: return ret def resourceAdd(self, req, model, res): from LuciClusterActions import AddResource, EditResource + fvars = GetReqVars(req, [ 'URL' ]) + baseurl = fvars['URL'] or LUCI_CLUSTER_BASE_URL try: cluname = model.getClusterName() @@ -3293,7 +3318,7 @@ if ret is None: response = req.RESPONSE response.redirect('%s?pagetype=%s&clustername=%s&busyfirst=true' \ - % (req['URL'], RESOURCES, cluname)) + % (baseurl, RESOURCES, cluname)) else: return ret --- conga/luci/site/luci/Extensions/conga_constants.py 2007/06/25 16:03:39 1.40 +++ conga/luci/site/luci/Extensions/conga_constants.py 2007/06/25 16:11:30 1.41 @@ -59,17 +59,18 @@ SYS_SERVICE_UPDATE = '91' # Cluster tasks -CLUSTER_STOP = '1000' -CLUSTER_START = '1001' -CLUSTER_RESTART = '1002' -CLUSTER_DELETE = '1003' +CLUSTER_STOP = '1000' +CLUSTER_START = '1001' +CLUSTER_RESTART = '1002' +CLUSTER_DELETE = '1003' # Node tasks -NODE_LEAVE_CLUSTER = '100' -NODE_JOIN_CLUSTER = '101' -NODE_REBOOT = '102' -NODE_FENCE = '103' -NODE_DELETE = '104' +NODE_LEAVE_CLUSTER = '100' +NODE_JOIN_CLUSTER = '101' +NODE_REBOOT = '102' +NODE_FENCE = '103' +NODE_DELETE = '104' +NODE_FORCE_DELETE = '105' # General tasks BASECLUSTER = '201' --- conga/ricci/modules/storage/VG.cpp 2007/03/30 14:40:24 1.12 +++ conga/ricci/modules/storage/VG.cpp 2007/06/25 16:11:30 1.13 @@ -354,10 +354,15 @@ props.set(Variable("extent_size", 4 * 1024 * 1024 /* 4 MB */, ext_sizes)); - + // clustered - props.set(Variable("clustered", false, true)); - + bool use_clustered = false; + try { + LVM::check_locking(); + use_clustered = true; + } catch ( ... ) { } + props.set(Variable("clustered", use_clustered, true)); + // new sources VG unused(VG_PREFIX); for (list >::iterator iter = unused.sources.begin(); --- conga/ricci/modules/storage/parted_wrapper.cpp 2007/03/20 15:31:20 1.10 +++ conga/ricci/modules/storage/parted_wrapper.cpp 2007/06/25 16:11:30 1.11 @@ -680,7 +680,7 @@ else if (s[s.size()-2] == 'g') multiplier = 1000 * 1000 * 1000; else if (s[s.size()-2] == 't') - multiplier = 1000 * 1000 * 1000 * 1000; + multiplier = (long long) 1000 * 1000 * 1000 * 1000; } return (long long) utils::to_float(s) * multiplier;