Coverage for watcher/decision_engine/model/model_root.py: 91%
474 statements
« prev ^ index » next coverage.py v7.8.2, created at 2025-06-17 12:22 +0000
« prev ^ index » next coverage.py v7.8.2, created at 2025-06-17 12:22 +0000
1# -*- encoding: utf-8 -*-
2# Copyright (c) 2016 Intel Innovation and Research Ireland Ltd.
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15"""
16Openstack implementation of the cluster graph.
17"""
19import ast
20from lxml import etree # nosec: B410
21import networkx as nx
22from oslo_concurrency import lockutils
23from oslo_log import log
25from watcher._i18n import _
26from watcher.common import exception
27from watcher.decision_engine.model import base
28from watcher.decision_engine.model import element
30LOG = log.getLogger(__name__)
33class ModelRoot(nx.DiGraph, base.Model):
34 """Cluster graph for an Openstack cluster."""
36 def __init__(self, stale=False):
37 super(ModelRoot, self).__init__()
38 self.stale = stale
40 def __nonzero__(self):
41 return not self.stale
43 __bool__ = __nonzero__
45 @staticmethod
46 def assert_node(obj):
47 if not isinstance(obj, element.ComputeNode):
48 raise exception.IllegalArgumentException(
49 message=_("'obj' argument type is not valid: %s") % type(obj))
51 @staticmethod
52 def assert_instance(obj):
53 if not isinstance(obj, element.Instance):
54 raise exception.IllegalArgumentException(
55 message=_("'obj' argument type is not valid"))
57 @lockutils.synchronized("model_root")
58 def add_node(self, node):
59 self.assert_node(node)
60 super(ModelRoot, self).add_node(node.uuid, attr=node)
62 @lockutils.synchronized("model_root")
63 def remove_node(self, node):
64 self.assert_node(node)
65 try:
66 super(ModelRoot, self).remove_node(node.uuid)
67 except nx.NetworkXError as exc:
68 LOG.exception(exc)
69 raise exception.ComputeNodeNotFound(name=node.uuid)
71 @lockutils.synchronized("model_root")
72 def add_instance(self, instance):
73 self.assert_instance(instance)
74 try:
75 super(ModelRoot, self).add_node(instance.uuid, attr=instance)
76 except nx.NetworkXError as exc:
77 LOG.exception(exc)
78 raise exception.InstanceNotFound(name=instance.uuid)
80 @lockutils.synchronized("model_root")
81 def remove_instance(self, instance):
82 self.assert_instance(instance)
83 super(ModelRoot, self).remove_node(instance.uuid)
85 @lockutils.synchronized("model_root")
86 def map_instance(self, instance, node):
87 """Map a newly created instance to a node
89 :param instance: :py:class:`~.instance.Instance` object or instance
90 UUID
91 :type instance: str or :py:class:`~.instance.Instance`
92 :param node: :py:class:`~.node.ComputeNode` object or node UUID
93 :type node: str or :py:class:`~.instance.Instance`
94 """
95 if isinstance(instance, str): 95 ↛ 96line 95 didn't jump to line 96 because the condition on line 95 was never true
96 instance = self.get_instance_by_uuid(instance)
97 if isinstance(node, str): 97 ↛ 98line 97 didn't jump to line 98 because the condition on line 97 was never true
98 node = self.get_node_by_uuid(node)
99 self.assert_node(node)
100 self.assert_instance(instance)
102 self.add_edge(instance.uuid, node.uuid)
104 @lockutils.synchronized("model_root")
105 def unmap_instance(self, instance, node):
106 if isinstance(instance, str): 106 ↛ 107line 106 didn't jump to line 107 because the condition on line 106 was never true
107 instance = self.get_instance_by_uuid(instance)
108 if isinstance(node, str): 108 ↛ 109line 108 didn't jump to line 109 because the condition on line 108 was never true
109 node = self.get_node_by_uuid(node)
111 self.remove_edge(instance.uuid, node.uuid)
113 def delete_instance(self, instance, node=None):
114 self.assert_instance(instance)
115 self.remove_instance(instance)
117 @lockutils.synchronized("model_root")
118 def migrate_instance(self, instance, source_node, destination_node):
119 """Migrate single instance from source_node to destination_node
121 :param instance:
122 :param source_node:
123 :param destination_node:
124 :return:
125 """
126 self.assert_instance(instance)
127 self.assert_node(source_node)
128 self.assert_node(destination_node)
130 if source_node == destination_node:
131 return False
133 # unmap
134 self.remove_edge(instance.uuid, source_node.uuid)
135 # map
136 self.add_edge(instance.uuid, destination_node.uuid)
137 return True
139 @lockutils.synchronized("model_root")
140 def get_all_compute_nodes(self):
141 return {uuid: cn['attr'] for uuid, cn in self.nodes(data=True)
142 if isinstance(cn['attr'], element.ComputeNode)}
144 @lockutils.synchronized("model_root")
145 def get_node_by_uuid(self, uuid):
146 try:
147 return self._get_by_uuid(uuid)
148 except exception.ComputeResourceNotFound:
149 raise exception.ComputeNodeNotFound(name=uuid)
151 @lockutils.synchronized("model_root")
152 def get_node_by_name(self, name):
153 try:
154 node_list = [cn['attr'] for uuid, cn in self.nodes(data=True)
155 if (isinstance(cn['attr'], element.ComputeNode) and
156 cn['attr']['hostname'] == name)]
157 if node_list:
158 return node_list[0]
159 else:
160 raise exception.ComputeNodeNotFound(name=name)
161 except exception.ComputeResourceNotFound:
162 raise exception.ComputeNodeNotFound(name=name)
164 @lockutils.synchronized("model_root")
165 def get_instance_by_uuid(self, uuid):
166 try:
167 return self._get_by_uuid(uuid)
168 except exception.ComputeResourceNotFound:
169 raise exception.InstanceNotFound(name=uuid)
171 def _get_by_uuid(self, uuid):
172 try:
173 return self.nodes[uuid]['attr']
174 except Exception as exc:
175 LOG.exception(exc)
176 raise exception.ComputeResourceNotFound(name=uuid)
178 @lockutils.synchronized("model_root")
179 def get_node_by_instance_uuid(self, instance_uuid):
180 instance = self._get_by_uuid(instance_uuid)
181 for node_uuid in self.neighbors(instance.uuid):
182 node = self._get_by_uuid(node_uuid)
183 if isinstance(node, element.ComputeNode): 183 ↛ 181line 183 didn't jump to line 181 because the condition on line 183 was always true
184 return node
185 raise exception.InstanceNotMapped(uuid=instance_uuid)
187 @lockutils.synchronized("model_root")
188 def get_all_instances(self):
189 return {uuid: inst['attr'] for uuid, inst in self.nodes(data=True)
190 if isinstance(inst['attr'], element.Instance)}
192 @lockutils.synchronized("model_root")
193 def get_node_instances(self, node):
194 self.assert_node(node)
195 node_instances = []
196 for instance_uuid in self.predecessors(node.uuid):
197 instance = self._get_by_uuid(instance_uuid)
198 if isinstance(instance, element.Instance): 198 ↛ 196line 198 didn't jump to line 196 because the condition on line 198 was always true
199 node_instances.append(instance)
201 return node_instances
203 def get_node_used_resources(self, node):
204 vcpu_used = 0
205 memory_used = 0
206 disk_used = 0
207 for instance in self.get_node_instances(node):
208 vcpu_used += instance.vcpus
209 memory_used += instance.memory
210 disk_used += instance.disk
212 return dict(vcpu=vcpu_used, memory=memory_used, disk=disk_used)
214 def get_node_free_resources(self, node):
215 resources_used = self.get_node_used_resources(node)
216 vcpu_free = node.vcpu_capacity-resources_used.get('vcpu')
217 memory_free = node.memory_mb_capacity-resources_used.get('memory')
218 disk_free = node.disk_gb_capacity-resources_used.get('disk')
220 return dict(vcpu=vcpu_free, memory=memory_free, disk=disk_free)
222 def to_string(self):
223 return self.to_xml()
225 def to_xml(self):
226 root = etree.Element("ModelRoot")
227 # Build compute node tree
228 for cn in sorted(self.get_all_compute_nodes().values(),
229 key=lambda cn: cn.uuid):
230 compute_node_el = cn.as_xml_element()
232 # Build mapped instance tree
233 node_instances = self.get_node_instances(cn)
234 for instance in sorted(node_instances, key=lambda x: x.uuid):
235 instance_el = instance.as_xml_element()
236 compute_node_el.append(instance_el)
238 root.append(compute_node_el)
240 # Build unmapped instance tree (i.e. not assigned to any compute node)
241 for instance in sorted(self.get_all_instances().values(),
242 key=lambda inst: inst.uuid):
243 try:
244 self.get_node_by_instance_uuid(instance.uuid)
245 except exception.ComputeResourceNotFound:
246 root.append(instance.as_xml_element())
248 return etree.tostring(root, pretty_print=True).decode('utf-8')
250 def to_list(self):
251 ret_list = []
252 for cn in sorted(self.get_all_compute_nodes().values(),
253 key=lambda cn: cn.uuid):
254 in_dict = {}
255 for field in cn.fields:
256 new_name = "node_"+str(field)
257 in_dict[new_name] = cn[field]
258 node_instances = self.get_node_instances(cn)
259 if not node_instances:
260 deep_in_dict = in_dict.copy()
261 ret_list.append(deep_in_dict)
262 continue
263 for instance in sorted(node_instances, key=lambda x: x.uuid):
264 for field in instance.fields:
265 new_name = "server_"+str(field)
266 in_dict[new_name] = instance[field]
267 if in_dict != {}: 267 ↛ 263line 267 didn't jump to line 263 because the condition on line 267 was always true
268 deep_in_dict = in_dict.copy()
269 ret_list.append(deep_in_dict)
270 return ret_list
272 @classmethod
273 def from_xml(cls, data):
274 model = cls()
276 root = etree.fromstring(data)
277 for cn in root.findall('.//ComputeNode'):
278 node = element.ComputeNode(**cn.attrib)
279 model.add_node(node)
281 for inst in root.findall('.//Instance'):
282 instance = element.Instance(**inst.attrib)
283 instance.watcher_exclude = ast.literal_eval(
284 inst.attrib["watcher_exclude"])
285 model.add_instance(instance)
287 parent = inst.getparent()
288 if parent.tag == 'ComputeNode':
289 node = model.get_node_by_uuid(parent.get('uuid'))
290 model.map_instance(instance, node)
291 else:
292 model.add_instance(instance)
294 return model
296 @classmethod
297 def is_isomorphic(cls, G1, G2):
298 def node_match(node1, node2):
299 return node1['attr'].as_dict() == node2['attr'].as_dict()
300 return nx.algorithms.isomorphism.isomorph.is_isomorphic(
301 G1, G2, node_match=node_match)
304class StorageModelRoot(nx.DiGraph, base.Model):
305 """Cluster graph for an Openstack cluster."""
307 def __init__(self, stale=False):
308 super(StorageModelRoot, self).__init__()
309 self.stale = stale
311 def __nonzero__(self):
312 return not self.stale
314 __bool__ = __nonzero__
316 @staticmethod
317 def assert_node(obj):
318 if not isinstance(obj, element.StorageNode):
319 raise exception.IllegalArgumentException(
320 message=_("'obj' argument type is not valid: %s") % type(obj))
322 @staticmethod
323 def assert_pool(obj):
324 if not isinstance(obj, element.Pool):
325 raise exception.IllegalArgumentException(
326 message=_("'obj' argument type is not valid: %s") % type(obj))
328 @staticmethod
329 def assert_volume(obj):
330 if not isinstance(obj, element.Volume):
331 raise exception.IllegalArgumentException(
332 message=_("'obj' argument type is not valid: %s") % type(obj))
334 @lockutils.synchronized("storage_model")
335 def add_node(self, node):
336 self.assert_node(node)
337 super(StorageModelRoot, self).add_node(node.host, attr=node)
339 @lockutils.synchronized("storage_model")
340 def add_pool(self, pool):
341 self.assert_pool(pool)
342 super(StorageModelRoot, self).add_node(pool.name, attr=pool)
344 @lockutils.synchronized("storage_model")
345 def remove_node(self, node):
346 self.assert_node(node)
347 try:
348 super(StorageModelRoot, self).remove_node(node.host)
349 except nx.NetworkXError as exc:
350 LOG.exception(exc)
351 raise exception.StorageNodeNotFound(name=node.host)
353 @lockutils.synchronized("storage_model")
354 def remove_pool(self, pool):
355 self.assert_pool(pool)
356 try:
357 super(StorageModelRoot, self).remove_node(pool.name)
358 except nx.NetworkXError as exc:
359 LOG.exception(exc)
360 raise exception.PoolNotFound(name=pool.name)
362 @lockutils.synchronized("storage_model")
363 def map_pool(self, pool, node):
364 """Map a newly created pool to a node
366 :param pool: :py:class:`~.node.Pool` object or pool name
367 :param node: :py:class:`~.node.StorageNode` object or node host
368 """
369 if isinstance(pool, str): 369 ↛ 370line 369 didn't jump to line 370 because the condition on line 369 was never true
370 pool = self.get_pool_by_pool_name(pool)
371 if isinstance(node, str): 371 ↛ 372line 371 didn't jump to line 372 because the condition on line 371 was never true
372 node = self.get_node_by_name(node)
373 self.assert_node(node)
374 self.assert_pool(pool)
376 self.add_edge(pool.name, node.host)
378 @lockutils.synchronized("storage_model")
379 def unmap_pool(self, pool, node):
380 """Unmap a pool from a node
382 :param pool: :py:class:`~.node.Pool` object or pool name
383 :param node: :py:class:`~.node.StorageNode` object or node name
384 """
385 if isinstance(pool, str): 385 ↛ 386line 385 didn't jump to line 386 because the condition on line 385 was never true
386 pool = self.get_pool_by_pool_name(pool)
387 if isinstance(node, str): 387 ↛ 388line 387 didn't jump to line 388 because the condition on line 387 was never true
388 node = self.get_node_by_name(node)
390 self.remove_edge(pool.name, node.host)
392 @lockutils.synchronized("storage_model")
393 def add_volume(self, volume):
394 self.assert_volume(volume)
395 super(StorageModelRoot, self).add_node(volume.uuid, attr=volume)
397 @lockutils.synchronized("storage_model")
398 def remove_volume(self, volume):
399 self.assert_volume(volume)
400 try:
401 super(StorageModelRoot, self).remove_node(volume.uuid)
402 except nx.NetworkXError as exc:
403 LOG.exception(exc)
404 raise exception.VolumeNotFound(name=volume.uuid)
406 @lockutils.synchronized("storage_model")
407 def map_volume(self, volume, pool):
408 """Map a newly created volume to a pool
410 :param volume: :py:class:`~.volume.Volume` object or volume UUID
411 :param pool: :py:class:`~.node.Pool` object or pool name
412 """
413 if isinstance(volume, str): 413 ↛ 414line 413 didn't jump to line 414 because the condition on line 413 was never true
414 volume = self.get_volume_by_uuid(volume)
415 if isinstance(pool, str): 415 ↛ 416line 415 didn't jump to line 416 because the condition on line 415 was never true
416 pool = self.get_pool_by_pool_name(pool)
417 self.assert_pool(pool)
418 self.assert_volume(volume)
420 self.add_edge(volume.uuid, pool.name)
422 @lockutils.synchronized("storage_model")
423 def unmap_volume(self, volume, pool):
424 """Unmap a volume from a pool
426 :param volume: :py:class:`~.volume.Volume` object or volume UUID
427 :param pool: :py:class:`~.node.Pool` object or pool name
428 """
429 if isinstance(volume, str): 429 ↛ 430line 429 didn't jump to line 430 because the condition on line 429 was never true
430 volume = self.get_volume_by_uuid(volume)
431 if isinstance(pool, str): 431 ↛ 432line 431 didn't jump to line 432 because the condition on line 431 was never true
432 pool = self.get_pool_by_pool_name(pool)
434 self.remove_edge(volume.uuid, pool.name)
436 def delete_volume(self, volume):
437 self.assert_volume(volume)
438 self.remove_volume(volume)
440 @lockutils.synchronized("storage_model")
441 def get_all_storage_nodes(self):
442 return {host: cn['attr'] for host, cn in self.nodes(data=True)
443 if isinstance(cn['attr'], element.StorageNode)}
445 @lockutils.synchronized("storage_model")
446 def get_node_by_name(self, name):
447 try:
448 return self._get_by_name(name.split("#")[0])
449 except exception.StorageResourceNotFound:
450 raise exception.StorageNodeNotFound(name=name)
452 @lockutils.synchronized("storage_model")
453 def get_pool_by_pool_name(self, name):
454 try:
455 return self._get_by_name(name)
456 except exception.StorageResourceNotFound:
457 raise exception.PoolNotFound(name=name)
459 @lockutils.synchronized("storage_model")
460 def get_volume_by_uuid(self, uuid):
461 try:
462 return self._get_by_uuid(uuid)
463 except exception.StorageResourceNotFound:
464 raise exception.VolumeNotFound(name=uuid)
466 def _get_by_uuid(self, uuid):
467 try:
468 return self.nodes[uuid]['attr']
469 except Exception as exc:
470 LOG.exception(exc)
471 raise exception.StorageResourceNotFound(name=uuid)
473 def _get_by_name(self, name):
474 try:
475 return self.nodes[name]['attr']
476 except Exception as exc:
477 LOG.exception(exc)
478 raise exception.StorageResourceNotFound(name=name)
480 @lockutils.synchronized("storage_model")
481 def get_node_by_pool_name(self, pool_name):
482 pool = self._get_by_name(pool_name)
483 for node_name in self.neighbors(pool.name): 483 ↛ 487line 483 didn't jump to line 487 because the loop on line 483 didn't complete
484 node = self._get_by_name(node_name)
485 if isinstance(node, element.StorageNode): 485 ↛ 483line 485 didn't jump to line 483 because the condition on line 485 was always true
486 return node
487 raise exception.StorageNodeNotFound(name=pool_name)
489 @lockutils.synchronized("storage_model")
490 def get_node_pools(self, node):
491 self.assert_node(node)
492 node_pools = []
493 for pool_name in self.predecessors(node.host):
494 pool = self._get_by_name(pool_name)
495 if isinstance(pool, element.Pool): 495 ↛ 493line 495 didn't jump to line 493 because the condition on line 495 was always true
496 node_pools.append(pool)
498 return node_pools
500 @lockutils.synchronized("storage_model")
501 def get_pool_by_volume(self, volume):
502 self.assert_volume(volume)
503 volume = self._get_by_uuid(volume.uuid)
504 for p in self.neighbors(volume.uuid):
505 pool = self._get_by_name(p)
506 if isinstance(pool, element.Pool): 506 ↛ 504line 506 didn't jump to line 504 because the condition on line 506 was always true
507 return pool
508 raise exception.PoolNotFound(name=volume.uuid)
510 @lockutils.synchronized("storage_model")
511 def get_all_volumes(self):
512 return {name: vol['attr'] for name, vol in self.nodes(data=True)
513 if isinstance(vol['attr'], element.Volume)}
515 @lockutils.synchronized("storage_model")
516 def get_pool_volumes(self, pool):
517 self.assert_pool(pool)
518 volumes = []
519 for vol in self.predecessors(pool.name):
520 volume = self._get_by_uuid(vol)
521 if isinstance(volume, element.Volume): 521 ↛ 519line 521 didn't jump to line 519 because the condition on line 521 was always true
522 volumes.append(volume)
524 return volumes
526 def to_string(self):
527 return self.to_xml()
529 def to_xml(self):
530 root = etree.Element("ModelRoot")
531 # Build storage node tree
532 for cn in sorted(self.get_all_storage_nodes().values(),
533 key=lambda cn: cn.host):
534 storage_node_el = cn.as_xml_element()
535 # Build mapped pool tree
536 node_pools = self.get_node_pools(cn)
537 for pool in sorted(node_pools, key=lambda x: x.name):
538 pool_el = pool.as_xml_element()
539 storage_node_el.append(pool_el)
540 # Build mapped volume tree
541 pool_volumes = self.get_pool_volumes(pool)
542 for volume in sorted(pool_volumes, key=lambda x: x.uuid):
543 volume_el = volume.as_xml_element()
544 pool_el.append(volume_el)
546 root.append(storage_node_el)
548 # Build unmapped volume tree (i.e. not assigned to any pool)
549 for volume in sorted(self.get_all_volumes().values(),
550 key=lambda vol: vol.uuid):
551 try:
552 self.get_pool_by_volume(volume)
553 except (exception.VolumeNotFound, exception.PoolNotFound):
554 root.append(volume.as_xml_element())
556 return etree.tostring(root, pretty_print=True).decode('utf-8')
558 @classmethod
559 def from_xml(cls, data):
560 model = cls()
562 root = etree.fromstring(data)
563 for cn in root.findall('.//StorageNode'):
564 ndata = {}
565 for attr, val in cn.items():
566 ndata[attr] = val
567 volume_type = ndata.get('volume_type')
568 if volume_type: 568 ↛ 570line 568 didn't jump to line 570 because the condition on line 568 was always true
569 ndata['volume_type'] = [volume_type]
570 node = element.StorageNode(**ndata)
571 model.add_node(node)
573 for p in root.findall('.//Pool'):
574 pool = element.Pool(**p.attrib)
575 model.add_pool(pool)
577 parent = p.getparent()
578 if parent.tag == 'StorageNode': 578 ↛ 582line 578 didn't jump to line 582 because the condition on line 578 was always true
579 node = model.get_node_by_name(parent.get('host'))
580 model.map_pool(pool, node)
581 else:
582 model.add_pool(pool)
584 for vol in root.findall('.//Volume'):
585 volume = element.Volume(**vol.attrib)
586 model.add_volume(volume)
588 parent = vol.getparent()
589 if parent.tag == 'Pool':
590 pool = model.get_pool_by_pool_name(parent.get('name'))
591 model.map_volume(volume, pool)
592 else:
593 model.add_volume(volume)
595 return model
597 @classmethod
598 def is_isomorphic(cls, G1, G2):
599 return nx.algorithms.isomorphism.isomorph.is_isomorphic(
600 G1, G2)
603class BaremetalModelRoot(nx.DiGraph, base.Model):
605 """Cluster graph for an Openstack cluster: Baremetal Cluster."""
607 def __init__(self, stale=False):
608 super(BaremetalModelRoot, self).__init__()
609 self.stale = stale
611 def __nonzero__(self):
612 return not self.stale
614 __bool__ = __nonzero__
616 @staticmethod
617 def assert_node(obj):
618 if not isinstance(obj, element.IronicNode):
619 raise exception.IllegalArgumentException(
620 message=_("'obj' argument type is not valid: %s") % type(obj))
622 @lockutils.synchronized("baremetal_model")
623 def add_node(self, node):
624 self.assert_node(node)
625 super(BaremetalModelRoot, self).add_node(node.uuid, attr=node)
627 @lockutils.synchronized("baremetal_model")
628 def remove_node(self, node):
629 self.assert_node(node)
630 try:
631 super(BaremetalModelRoot, self).remove_node(node.uuid)
632 except nx.NetworkXError as exc:
633 LOG.exception(exc)
634 raise exception.IronicNodeNotFound(uuid=node.uuid)
636 @lockutils.synchronized("baremetal_model")
637 def get_all_ironic_nodes(self):
638 return {uuid: cn['attr'] for uuid, cn in self.nodes(data=True)
639 if isinstance(cn['attr'], element.IronicNode)}
641 @lockutils.synchronized("baremetal_model")
642 def get_node_by_uuid(self, uuid):
643 try:
644 return self._get_by_uuid(uuid)
645 except exception.BaremetalResourceNotFound:
646 raise exception.IronicNodeNotFound(uuid=uuid)
648 def _get_by_uuid(self, uuid):
649 try:
650 return self.nodes[uuid]['attr']
651 except Exception as exc:
652 LOG.exception(exc)
653 raise exception.BaremetalResourceNotFound(name=uuid)
655 def to_string(self):
656 return self.to_xml()
658 def to_xml(self):
659 root = etree.Element("ModelRoot")
660 # Build Ironic node tree
661 for cn in sorted(self.get_all_ironic_nodes().values(),
662 key=lambda cn: cn.uuid):
663 ironic_node_el = cn.as_xml_element()
664 root.append(ironic_node_el)
666 return etree.tostring(root, pretty_print=True).decode('utf-8')
668 @classmethod
669 def from_xml(cls, data):
670 model = cls()
672 root = etree.fromstring(data)
673 for cn in root.findall('.//IronicNode'):
674 node = element.IronicNode(**cn.attrib)
675 model.add_node(node)
677 return model
679 @classmethod
680 def is_isomorphic(cls, G1, G2):
681 return nx.algorithms.isomorphism.isomorph.is_isomorphic(
682 G1, G2)