~ubuntu-branches/ubuntu/vivid/crmsh/vivid

« back to all changes in this revision

Viewing changes to modules/ra.py

  • Committer: Package Import Robot
  • Author(s): Martin Loschwitz
  • Date: 2013-08-05 05:21:29 UTC
  • mfrom: (1.1.2)
  • Revision ID: package-import@ubuntu.com-20130805052129-aooqqanxrctmy93m
Tags: 1.2.5+hg1034-1
New upstream checkout

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
1
# Copyright (C) 2008-2011 Dejan Muhamedagic <dmuhamedagic@suse.de>
2
 
 
2
#
3
3
# This program is free software; you can redistribute it and/or
4
4
# modify it under the terms of the GNU General Public
5
5
# License as published by the Free Software Foundation; either
6
6
# version 2.1 of the License, or (at your option) any later version.
7
 
 
7
#
8
8
# This software is distributed in the hope that it will be useful,
9
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
11
11
# General Public License for more details.
12
 
 
12
#
13
13
# You should have received a copy of the GNU General Public
14
14
# License along with this library; if not, write to the Free Software
15
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
23
23
import glob
24
24
from cache import WCache
25
25
from vars import Vars, getpwdent
26
 
from utils import *
27
 
from msg import *
 
26
from utils import stdout2list, is_program, is_process, add_sudo
 
27
from utils import os_types_list, get_stdout, find_value
 
28
from utils import crm_msec, crm_time_cmp
 
29
from msg import common_debug, common_err, common_warn, common_info
 
30
from msg import user_prefs
28
31
 
29
32
#
30
33
# Resource Agents interface (meta-data, parameters, etc)
31
34
#
32
35
lrmadmin_prog = "lrmadmin"
 
36
 
 
37
 
33
38
class RaLrmd(object):
34
39
    '''
35
40
    Getting information from the resource agents.
36
41
    '''
37
42
    def __init__(self):
38
43
        self.good = self.is_lrmd_accessible()
 
44
 
39
45
    def lrmadmin(self, opts, xml=False):
40
46
        '''
41
47
        Get information directly from lrmd using lrmadmin.
42
48
        '''
43
49
        rc, l = stdout2list("%s %s" % (lrmadmin_prog, opts))
44
50
        if l and not xml:
45
 
            l = l[1:] # skip the first line
 
51
            l = l[1:]  # skip the first line
46
52
        return l
 
53
 
47
54
    def is_lrmd_accessible(self):
48
55
        if not (is_program(lrmadmin_prog) and is_process("lrmd")):
49
56
            return False
50
 
        return subprocess.call(\
51
 
            add_sudo(">/dev/null 2>&1 %s -C" % lrmadmin_prog), \
 
57
        return subprocess.call(
 
58
            add_sudo(">/dev/null 2>&1 %s -C" % lrmadmin_prog),
52
59
            shell=True) == 0
 
60
 
53
61
    def meta(self, ra_class, ra_type, ra_provider):
54
 
        return self.lrmadmin("-M %s %s %s"%(ra_class, ra_type, ra_provider), True)
 
62
        return self.lrmadmin("-M %s %s %s" % (ra_class, ra_type, ra_provider), True)
 
63
 
55
64
    def providers(self, ra_type, ra_class="ocf"):
56
65
        'List of providers for a class:type.'
57
66
        return self.lrmadmin("-P %s %s" % (ra_class, ra_type), True)
 
67
 
58
68
    def classes(self):
59
69
        'List of classes.'
60
70
        return self.lrmadmin("-C")
 
71
 
61
72
    def types(self, ra_class="ocf", ra_provider=""):
62
73
        'List of types for a class.'
63
74
        return self.lrmadmin("-T %s" % ra_class)
64
75
 
 
76
 
65
77
class RaOS(object):
66
78
    '''
67
79
    Getting information from the resource agents (direct).
68
80
    '''
69
81
    def __init__(self):
70
82
        self.good = True
 
83
 
71
84
    def meta(self, ra_class, ra_type, ra_provider):
72
85
        l = []
73
86
        if ra_class == "ocf":
74
 
            rc, l = stdout2list("%s/resource.d/%s/%s meta-data" % \
75
 
                (os.environ["OCF_ROOT"], ra_provider, ra_type))
 
87
            rc, l = stdout2list("%s/resource.d/%s/%s meta-data" %
 
88
                                (os.environ["OCF_ROOT"], ra_provider, ra_type))
76
89
        elif ra_class == "stonith":
77
 
            if ra_type.startswith("fence_") and os.path.exists("/usr/sbin/%s" %  ra_type):
 
90
            if ra_type.startswith("fence_") and os.path.exists("/usr/sbin/%s" % ra_type):
78
91
                rc, l = stdout2list("/usr/sbin/%s -o metadata" % ra_type)
79
92
            else:
80
93
                rc, l = stdout2list("stonith -m -t %s" % ra_type)
81
94
        elif ra_class == "nagios":
82
 
            rc, l = stdout2list("%s/check_%s --metadata" % \
83
 
                (vars.nagios_dir, ra_type))
 
95
            rc, l = stdout2list("%s/check_%s --metadata" %
 
96
                                (vars.nagios_dir, ra_type))
84
97
        return l
 
98
 
85
99
    def providers(self, ra_type, ra_class="ocf"):
86
100
        'List of providers for a class:type.'
87
101
        l = []
91
105
                if len(a) == 7:
92
106
                    l.append(a[5])
93
107
        return l
 
108
 
94
109
    def classes(self):
95
110
        'List of classes.'
96
111
        return "heartbeat lsb nagios ocf stonith".split()
 
112
 
97
113
    def types(self, ra_class="ocf", ra_provider=""):
98
114
        'List of types for a class.'
99
115
        l = []
111
127
            l.extend(os_types_list("/usr/sbin/fence_*"))
112
128
        elif ra_class == "nagios":
113
129
            l = os_types_list("%s/check_*" % vars.nagios_dir)
114
 
            l = [x.replace("check_","") for x in l]
 
130
            l = [x.replace("check_", "") for x in l]
115
131
        l = list(set(l))
116
132
        l.sort()
117
133
        if ra_class == "stonith":
120
136
                    l.remove(ra)
121
137
        return l
122
138
 
 
139
 
123
140
class RaCrmResource(object):
124
141
    '''
125
142
    Getting information from the resource agents via new crm_resource.
126
143
    '''
127
144
    def __init__(self):
128
145
        self.good = True
 
146
 
129
147
    def crm_resource(self, opts):
130
148
        '''
131
149
        Get information from crm_resource.
134
152
        # not clear when/why crm_resource exits with non-zero
135
153
        # code
136
154
        if rc != 0:
137
 
            common_debug("crm_resource %s exited with code %d" % \
138
 
                (opts, rc))
 
155
            common_debug("crm_resource %s exited with code %d" %
 
156
                         (opts, rc))
139
157
        return s
 
158
 
140
159
    def meta(self, ra_class, ra_type, ra_provider):
141
 
        return self.crm_resource("--show-metadata %s:%s:%s"%(ra_class, ra_provider, ra_type))
 
160
        return self.crm_resource("--show-metadata %s:%s:%s" % (ra_class, ra_provider, ra_type))
 
161
 
142
162
    def providers(self, ra_type, ra_class="ocf"):
143
163
        'List of providers for OCF:type.'
144
164
        if ra_class != "ocf":
145
165
            common_err("no providers for class %s" % ra_class)
146
166
            return []
147
167
        return self.crm_resource("--list-ocf-alternatives %s" % ra_type)
 
168
 
148
169
    def classes(self):
149
170
        'List of classes.'
150
171
        l = self.crm_resource("--list-standards")
151
172
        return l
 
173
 
152
174
    def types(self, ra_class="ocf", ra_provider=""):
153
175
        'List of types for a class.'
154
176
        if ra_provider:
156
178
        else:
157
179
            return self.crm_resource("--list-agents %s" % ra_class)
158
180
 
 
181
 
159
182
def can_use_lrmadmin():
160
183
    from distutils import version
161
184
    # after this glue release all users can get meta-data and
162
185
    # similar from lrmd
163
186
    minimum_glue = "1.0.10"
164
 
    rc, glue_ver = get_stdout("%s -v" % lrmadmin_prog, stderr_on = False)
165
 
    if not glue_ver: #lrmadmin probably not found
 
187
    rc, glue_ver = get_stdout("%s -v" % lrmadmin_prog, stderr_on=False)
 
188
    if not glue_ver:  # lrmadmin probably not found
166
189
        return False
167
190
    v_min = version.LooseVersion(minimum_glue)
168
191
    v_this = version.LooseVersion(glue_ver)
169
192
    return v_this >= v_min or \
170
193
        (getpwdent()[0] in ("root", vars.crm_daemon_user))
 
194
 
 
195
 
171
196
def crm_resource_support():
172
 
    rc, s = get_stdout("crm_resource --list-standards", stderr_on = False)
 
197
    rc, s = get_stdout("crm_resource --list-standards", stderr_on=False)
173
198
    return s != ""
 
199
 
 
200
 
174
201
def ra_if():
175
202
    if vars.ra_if:
176
203
        return vars.ra_if
182
209
        vars.ra_if = RaOS()
183
210
    return vars.ra_if
184
211
 
 
212
 
185
213
def ra_classes():
186
214
    '''
187
215
    List of RA classes.
191
219
    l = ra_if().classes()
192
220
    l.sort()
193
221
    return wcache.store("ra_classes", l)
 
222
 
 
223
 
194
224
def ra_providers(ra_type, ra_class="ocf"):
195
225
    'List of providers for a class:type.'
196
226
    id = "ra_providers-%s-%s" % (ra_class, ra_type)
199
229
    l = ra_if().providers(ra_type, ra_class)
200
230
    l.sort()
201
231
    return wcache.store(id, l)
 
232
 
 
233
 
202
234
def ra_providers_all(ra_class="ocf"):
203
235
    '''
204
236
    List of providers for a class.
213
245
            l.append(s)
214
246
    l.sort()
215
247
    return wcache.store(id, l)
 
248
 
 
249
 
216
250
def ra_types(ra_class="ocf", ra_provider=""):
217
251
    '''
218
252
    List of RA type for a class.
224
258
        return wcache.retrieve(id)
225
259
    list = []
226
260
    for ra in ra_if().types(ra_class):
227
 
        if (not ra_provider or \
 
261
        if (not ra_provider or
228
262
                ra_provider in ra_providers(ra, ra_class)) \
229
263
                and ra not in list:
230
264
            list.append(ra)
231
265
    list.sort()
232
266
    return wcache.store(id, list)
233
267
 
 
268
 
234
269
def get_pe_meta():
235
270
    if not vars.pe_metadata:
236
 
        vars.pe_metadata = RAInfo("pengine","metadata")
 
271
        vars.pe_metadata = RAInfo("pengine", "metadata")
237
272
    return vars.pe_metadata
 
273
 
 
274
 
238
275
def get_crmd_meta():
239
276
    if not vars.crmd_metadata:
240
 
        vars.crmd_metadata = RAInfo("crmd","metadata")
 
277
        vars.crmd_metadata = RAInfo("crmd", "metadata")
241
278
        vars.crmd_metadata.set_advanced_params(vars.crmd_advanced)
242
279
    return vars.crmd_metadata
 
280
 
 
281
 
243
282
def get_stonithd_meta():
244
283
    if not vars.stonithd_metadata:
245
 
        vars.stonithd_metadata = RAInfo("stonithd","metadata")
 
284
        vars.stonithd_metadata = RAInfo("stonithd", "metadata")
246
285
    return vars.stonithd_metadata
 
286
 
 
287
 
247
288
def get_cib_meta():
248
289
    if not vars.cib_metadata:
249
 
        vars.cib_metadata = RAInfo("cib","metadata")
 
290
        vars.cib_metadata = RAInfo("cib", "metadata")
250
291
    return vars.cib_metadata
 
292
 
 
293
 
251
294
def get_properties_meta():
252
295
    if not vars.crm_properties_metadata:
253
296
        get_pe_meta()
257
300
        vars.crm_properties_metadata.add_ra_params(vars.pe_metadata)
258
301
        vars.crm_properties_metadata.add_ra_params(vars.cib_metadata)
259
302
    return vars.crm_properties_metadata
 
303
 
 
304
 
260
305
def get_properties_list():
261
306
    try:
262
307
        return get_properties_meta().params().keys()
263
308
    except:
264
309
        return []
265
310
 
 
311
 
266
312
def prog_meta(prog):
267
313
    '''
268
314
    Do external program metadata.
274
320
            common_debug("%s metadata exited with code %d" % (prog, rc))
275
321
            l = []
276
322
    return l
 
323
 
 
324
 
277
325
def get_nodes_text(n, tag):
278
 
    try: return n.findtext(tag).strip()
279
 
    except: return ''
 
326
    try:
 
327
        return n.findtext(tag).strip()
 
328
    except:
 
329
        return ''
 
330
 
280
331
 
281
332
def mk_monitor_name(role, depth):
282
333
    depth = depth != "0" and ("_%s" % depth) or ""
283
334
    return role and role != "Started" and \
284
335
        "monitor_%s%s" % (role, depth) or \
285
336
        "monitor%s" % depth
 
337
 
 
338
 
286
339
def monitor_name_node(node):
287
340
    depth = node.get("depth") or '0'
288
341
    role = node.get("role")
289
342
    return mk_monitor_name(role, depth)
 
343
 
 
344
 
290
345
def monitor_name_pl(pl):
291
346
    depth = find_value(pl, "depth") or '0'
292
347
    role = find_value(pl, "role")
293
348
    return mk_monitor_name(role, depth)
294
349
 
 
350
 
295
351
class RAInfo(object):
296
352
    '''
297
353
    A resource agent and whatever's useful about it.
300
356
    required_ops = ("start", "stop")
301
357
    skip_ops = ("meta-data", "validate-all")
302
358
    skip_op_attr = ("name", "depth", "role")
 
359
 
303
360
    def __init__(self, ra_class, ra_type, ra_provider="heartbeat"):
304
361
        self.advanced_params = []
305
362
        self.ra_class = ra_class
308
365
        if not self.ra_provider:
309
366
            self.ra_provider = "heartbeat"
310
367
        self.ra_elem = None
 
368
 
311
369
    def ra_string(self):
312
370
        return self.ra_class == "ocf" and \
313
371
            "%s:%s:%s" % (self.ra_class, self.ra_provider, self.ra_type) or \
314
372
            "%s:%s" % (self.ra_class, self.ra_type)
 
373
 
315
374
    def error(self, s):
316
375
        common_err("%s: %s" % (self.ra_string(), s))
 
376
 
317
377
    def warn(self, s):
318
378
        common_warn("%s: %s" % (self.ra_string(), s))
 
379
 
319
380
    def info(self, s):
320
381
        common_info("%s: %s" % (self.ra_string(), s))
 
382
 
321
383
    def debug(self, s):
322
384
        common_debug("%s: %s" % (self.ra_string(), s))
 
385
 
323
386
    def set_advanced_params(self, l):
324
387
        self.advanced_params = l
 
388
 
325
389
    def filter_crmd_attributes(self):
326
390
        for p in self.ra_elem.xpath("//parameters/parameter"):
327
391
            if not p.get("name") in vars.crmd_user_attributes:
328
392
                self.ra_elem.remove(p)
 
393
 
329
394
    def add_ra_params(self, ra):
330
395
        '''
331
396
        Add parameters from another RAInfo instance.
341
406
            params_node = etree.SubElement(self.ra_elem, "parameters")
342
407
        for n in ra.ra_elem.xpath("//parameters/parameter"):
343
408
            params_node.append(copy.deepcopy(n))
 
409
 
344
410
    def mk_ra_node(self):
345
411
        '''
346
412
        Return the resource_agent node.
359
425
        if self.ra_class == "stonith":
360
426
            self.add_ra_params(get_stonithd_meta())
361
427
        return self.ra_elem
 
428
 
362
429
    def param_type_default(self, n):
363
430
        try:
364
431
            content = n.find("content")
367
434
            return type, default
368
435
        except:
369
436
            return None, None
 
437
 
370
438
    def params(self):
371
439
        '''
372
440
        Construct a dict of dicts: parameters are keys and
392
460
                "default": default,
393
461
            }
394
462
        return wcache.store(id, d)
 
463
 
395
464
    def completion_params(self):
396
465
        '''
397
466
        Extra method for completion, for we want to filter some
400
469
        if self.mk_ra_node() is None:
401
470
            return None
402
471
        return [c.get("name")
403
 
            for c in self.ra_elem.xpath("//parameters/parameter")
 
472
                for c in self.ra_elem.xpath("//parameters/parameter")
404
473
                if c.get("name")
405
 
                and c.get("name") not in self.advanced_params
406
 
        ]
 
474
                and c.get("name") not in self.advanced_params]
 
475
 
407
476
    def actions(self):
408
477
        '''
409
478
        Construct a dict of dicts: actions are keys and
438
507
                    d2[norole_op] = d[op]
439
508
        d.update(d2)
440
509
        return wcache.store(id, d)
 
510
 
441
511
    def reqd_params_list(self):
442
512
        '''
443
513
        List of required parameters.
444
514
        '''
445
515
        d = self.params()
446
 
        if not d: return []
 
516
        if not d:
 
517
            return []
447
518
        return [x for x in d if d[x]["required"] == '1']
 
519
 
448
520
    def param_default(self, pname):
449
521
        '''
450
522
        Parameter's default.
451
523
        '''
452
524
        d = self.params()
453
 
        try: return d[pname]["default"]
454
 
        except: return None
 
525
        try:
 
526
            return d[pname]["default"]
 
527
        except:
 
528
            return None
 
529
 
455
530
    def unreq_param(self, p):
456
531
        '''
457
532
        Allow for some exceptions.
458
 
        
 
533
 
459
534
        - the rhcs stonith agents sometimes require "action" (in
460
535
          the meta-data) and "port", but they're automatically
461
536
          supplied by stonithd
462
537
        '''
463
538
        if self.ra_class == "stonith" and \
464
 
            (self.ra_type.startswith("rhcs/") or \
465
 
            self.ra_type.startswith("fence_")):
 
539
            (self.ra_type.startswith("rhcs/") or
 
540
             self.ra_type.startswith("fence_")):
466
541
            if p in ("action", "port"):
467
542
                return True
468
543
        return False
 
544
 
469
545
    def sanity_check_params(self, id, pl, existence_only=False):
470
546
        '''
471
547
        pl is a list of (attribute, value) pairs.
491
567
                common_err("%s: parameter %s does not exist" % (id, p))
492
568
                rc |= user_prefs.get_check_rc()
493
569
        return rc
 
570
 
494
571
    def get_adv_timeout(self, op, node=None):
495
572
        if node is not None and op == "monitor":
496
573
            name = monitor_name_node(node)
500
577
            return self.actions()[name]["timeout"]
501
578
        except:
502
579
            return None
 
580
 
503
581
    def sanity_check_ops(self, id, ops, default_timeout):
504
582
        '''
505
583
        ops is a list of operations
530
608
                v = n_ops[op]["interval"]
531
609
                v_msec = crm_msec(v)
532
610
                if op in ("start", "stop") and v_msec != 0:
533
 
                    common_warn("%s: Specified interval for %s is %s, it must be 0" %(id, op, v))
 
611
                    common_warn("%s: Specified interval for %s is %s, it must be 0" % (id, op, v))
534
612
                    rc |= 1
535
613
                if op.startswith("monitor") and v_msec != 0:
536
614
                    if v_msec not in intervals:
551
629
            if crm_msec(v) < 0:
552
630
                continue
553
631
            if crm_time_cmp(adv_timeout, v) > 0:
554
 
                common_warn("%s: %s %s for %s is smaller than the advised %s" % \
555
 
                    (id, timeout_string, v, op, adv_timeout))
 
632
                common_warn("%s: %s %s for %s is smaller than the advised %s" %
 
633
                            (id, timeout_string, v, op, adv_timeout))
556
634
                rc |= 1
557
635
        return rc
 
636
 
558
637
    def meta(self):
559
638
        '''
560
639
        RA meta-data as raw xml.
568
647
            l = ra_if().meta(self.ra_class, self.ra_type, self.ra_provider)
569
648
        self.debug("read and cached meta-data")
570
649
        return wcache.store(id, l)
 
650
 
571
651
    def meta_pretty(self):
572
652
        '''
573
653
        Print the RA meta-data in a human readable form.
577
657
        l = []
578
658
        title = self.meta_title()
579
659
        l.append(title)
580
 
        longdesc = get_nodes_text(self.ra_elem,"longdesc")
 
660
        longdesc = get_nodes_text(self.ra_elem, "longdesc")
581
661
        if longdesc:
582
662
            l.append(longdesc)
583
663
        if self.ra_class != "heartbeat":
588
668
        if actions:
589
669
            l.append(actions)
590
670
        return '\n\n'.join(l)
 
671
 
591
672
    def get_shortdesc(self, n):
592
673
        name = n.get("name")
593
 
        shortdesc = get_nodes_text(n,"shortdesc")
594
 
        longdesc = get_nodes_text(n,"longdesc")
 
674
        shortdesc = get_nodes_text(n, "shortdesc")
 
675
        longdesc = get_nodes_text(n, "longdesc")
595
676
        if shortdesc and shortdesc not in (name, longdesc, self.ra_type):
596
677
            return shortdesc
597
678
        return ''
 
679
 
598
680
    def meta_title(self):
599
681
        s = self.ra_string()
600
682
        shortdesc = self.get_shortdesc(self.ra_elem)
601
683
        if shortdesc:
602
684
            s = "%s (%s)" % (shortdesc, s)
603
685
        return s
 
686
 
604
687
    def meta_param_head(self, n):
605
688
        name = n.get("name")
606
689
        if not name:
616
699
        shortdesc = self.get_shortdesc(n)
617
700
        s = "%s: %s" % (s, shortdesc)
618
701
        return s
 
702
 
619
703
    def format_parameter(self, n):
620
704
        l = []
621
705
        head = self.meta_param_head(n)
623
707
            self.error("no name attribute for parameter")
624
708
            return ""
625
709
        l.append(head)
626
 
        longdesc = get_nodes_text(n,"longdesc")
 
710
        longdesc = get_nodes_text(n, "longdesc")
627
711
        if longdesc:
628
 
            longdesc = self.ra_tab + longdesc.replace("\n", "\n"+self.ra_tab) + '\n'
 
712
            longdesc = self.ra_tab + longdesc.replace("\n", "\n" + self.ra_tab) + '\n'
629
713
            l.append(longdesc)
630
714
        return '\n'.join(l)
 
715
 
631
716
    def meta_parameter(self, param):
632
717
        if self.mk_ra_node() is None:
633
718
            return ''
634
719
        for c in self.ra_elem.xpath("//parameters/parameter"):
635
720
            if c.get("name") == param:
636
721
                return self.format_parameter(c)
 
722
 
637
723
    def meta_parameters(self):
638
724
        if self.mk_ra_node() is None:
639
725
            return ''
644
730
                l.append(s)
645
731
        if l:
646
732
            return "Parameters (* denotes required, [] the default):\n\n" + '\n'.join(l)
 
733
 
647
734
    def meta_action_head(self, n):
648
735
        name = n.get("name")
649
736
        if not name:
660
747
            if v:
661
748
                s = "%s %s=%s" % (s, a, v)
662
749
        return s
 
750
 
663
751
    def meta_actions(self):
664
752
        l = []
665
753
        for c in self.ra_elem.xpath("//actions/action"):
669
757
        if l:
670
758
            return "Operations' defaults (advisory minimum):\n\n" + '\n'.join(l)
671
759
 
 
760
 
672
761
def get_ra(el):
673
762
    ra_type = el.get("type")
674
763
    ra_class = el.get("class")
675
764
    ra_provider = el.get("provider")
676
765
    return RAInfo(ra_class, ra_type, ra_provider)
677
766
 
 
767
 
678
768
#
679
769
# resource type definition
680
770
#
683
773
    Only ocf ra class supports providers.
684
774
    '''
685
775
    if not rsc_type:
686
 
        common_err("bad resource type specification %s"%s)
 
776
        common_err("bad resource type specification %s" % s)
687
777
        return False
688
778
    if ra_class == "ocf":
689
779
        if not provider:
690
 
            common_err("provider could not be determined for %s"%s)
 
780
            common_err("provider could not be determined for %s" % s)
691
781
            return False
692
782
    else:
693
783
        if provider:
694
 
            common_warn("ra class %s does not support providers"%ra_class)
 
784
            common_warn("ra class %s does not support providers" % ra_class)
695
785
            return True
696
786
    return True
 
787
 
 
788
 
697
789
def disambiguate_ra_type(s):
698
790
    '''
699
791
    Unravel [class:[provider:]]type