Browse Source

Clean up the edge cases around states

Currently show_instance returns an Instance even if the requested node
is not actually an instance (e.g. just an available node). This change
corrects it. Make list_instances consistent with it.

Also make the states a proper enum to avoid consumers from using invalid
values (I did it several times when working on this patch).

Change-Id: If9aad0d7f4d10a7119d1f0bccc1cc32a918a72e3
Dmitry Tantsur 3 months ago
parent
commit
02932096df

+ 1
- 0
lower-constraints.txt View File

@@ -1,5 +1,6 @@
1 1
 coverage==4.0
2 2
 doc8==0.6.0
3
+enum34==1.0.4
3 4
 fixtures==3.0.0
4 5
 flake8-import-order==0.13
5 6
 hacking==1.0.0

+ 2
- 1
metalsmith/__init__.py View File

@@ -15,6 +15,7 @@
15 15
 
16 16
 from metalsmith._config import InstanceConfig
17 17
 from metalsmith._instance import Instance
18
+from metalsmith._instance import InstanceState
18 19
 from metalsmith._provisioner import Provisioner
19 20
 
20
-__all__ = ['Instance', 'InstanceConfig', 'Provisioner']
21
+__all__ = ['Instance', 'InstanceConfig', 'InstanceState', 'Provisioner']

+ 2
- 1
metalsmith/_format.py View File

@@ -57,7 +57,8 @@ class DefaultFormat(object):
57 57
     def show(self, instances):
58 58
         for instance in instances:
59 59
             _print("Node %(node)s, current state is %(state)s",
60
-                   node=_utils.log_res(instance.node), state=instance.state)
60
+                   node=_utils.log_res(instance.node),
61
+                   state=instance.state.name)
61 62
 
62 63
             if instance.is_deployed:
63 64
                 ips = instance.ip_addresses()

+ 75
- 32
metalsmith/_instance.py View File

@@ -13,18 +13,74 @@
13 13
 # See the License for the specific language governing permissions and
14 14
 # limitations under the License.
15 15
 
16
+import enum
17
+import warnings
18
+
16 19
 from metalsmith import _utils
17 20
 
18 21
 
19 22
 _PROGRESS_STATES = frozenset(['deploying', 'wait call-back',
20 23
                               'deploy complete'])
21
-# NOTE(dtantsur): include available since there is a period of time between
22
-# claiming the instance and starting the actual provisioning via ironic.
23
-_DEPLOYING_STATES = _PROGRESS_STATES | {'available'}
24 24
 _ACTIVE_STATES = frozenset(['active'])
25 25
 _ERROR_STATES = frozenset(['error', 'deploy failed'])
26
+_RESERVED_STATES = frozenset(['available'])
27
+
28
+
29
+class InstanceState(enum.Enum):
30
+    """A state of an instance."""
31
+
32
+    DEPLOYING = 'deploying'
33
+    """Provisioning is in progress.
34
+
35
+    This includes the case when a node is still in the ``available`` state, but
36
+    already has an instance associated with it.
37
+    """
38
+
39
+    ACTIVE = 'active'
40
+    """The instance is provisioned."""
41
+
42
+    MAINTENANCE = 'maintenance'
43
+    """The instance is provisioned but is in the maintenance mode."""
26 44
 
27
-_HEALTHY_STATES = _PROGRESS_STATES | _ACTIVE_STATES
45
+    ERROR = 'error'
46
+    """The instance has a failure."""
47
+
48
+    UNKNOWN = 'unknown'
49
+    """The node is in an unexpected state.
50
+
51
+    It can be unprovisioned or modified by a third party.
52
+    """
53
+
54
+    @property
55
+    def is_deployed(self):
56
+        """Whether the state designates a finished deployment."""
57
+        return self in _DEPLOYED_STATES
58
+
59
+    @property
60
+    def is_healthy(self):
61
+        """Whether the state is considered healthy."""
62
+        return self in _HEALTHY_STATES
63
+
64
+    # TODO(dtantsur): remove before 1.0
65
+
66
+    def __eq__(self, other):
67
+        if isinstance(other, str):
68
+            warnings.warn("Comparing states with strings is deprecated, "
69
+                          "use InstanceState instead", DeprecationWarning)
70
+            return self.value == other
71
+        else:
72
+            return super(InstanceState, self).__eq__(other)
73
+
74
+    def __ne__(self, other):
75
+        eq = self.__eq__(other)
76
+        return NotImplemented if eq is NotImplemented else not eq
77
+
78
+    def __hash__(self):
79
+        return hash(self.value)
80
+
81
+
82
+_HEALTHY_STATES = frozenset([InstanceState.ACTIVE, InstanceState.DEPLOYING])
83
+_DEPLOYED_STATES = frozenset([InstanceState.ACTIVE, InstanceState.MAINTENANCE])
28 84
 
29 85
 
30 86
 class Instance(object):
@@ -57,16 +113,12 @@ class Instance(object):
57 113
     @property
58 114
     def is_deployed(self):
59 115
         """Whether the node is deployed."""
60
-        return self._node.provision_state in _ACTIVE_STATES
61
-
62
-    @property
63
-    def _is_deployed_by_metalsmith(self):
64
-        return _utils.GetNodeMixin.HOSTNAME_FIELD in self._node.instance_info
116
+        return self.state.is_deployed
65 117
 
66 118
     @property
67 119
     def is_healthy(self):
68
-        """Whether the node is not at fault or maintenance."""
69
-        return self.state in _HEALTHY_STATES and not self._node.is_maintenance
120
+        """Whether the instance is not at fault or maintenance."""
121
+        return self.state.is_healthy and not self._node.is_maintenance
70 122
 
71 123
     def nics(self):
72 124
         """List NICs for this instance.
@@ -90,32 +142,23 @@ class Instance(object):
90 142
 
91 143
     @property
92 144
     def state(self):
93
-        """Instance state.
94
-
95
-        ``deploying``
96
-            deployment is in progress
97
-        ``active``
98
-            node is provisioned
99
-        ``maintenance``
100
-            node is provisioned but is in maintenance mode
101
-        ``error``
102
-            node has a failure
103
-        ``unknown``
104
-            node in unexpected state (maybe unprovisioned or modified by
105
-            a third party)
106
-        """
145
+        """Instance state, one of :py:class:`InstanceState`."""
107 146
         prov_state = self._node.provision_state
108
-        if prov_state in _DEPLOYING_STATES:
109
-            return 'deploying'
147
+        if prov_state in _PROGRESS_STATES:
148
+            return InstanceState.DEPLOYING
149
+        # NOTE(dtantsur): include available since there is a period of time
150
+        # between claiming the instance and starting the actual provisioning.
151
+        elif prov_state in _RESERVED_STATES and self._node.instance_id:
152
+            return InstanceState.DEPLOYING
110 153
         elif prov_state in _ERROR_STATES:
111
-            return 'error'
154
+            return InstanceState.ERROR
112 155
         elif prov_state in _ACTIVE_STATES:
113 156
             if self._node.is_maintenance:
114
-                return 'maintenance'
157
+                return InstanceState.MAINTENANCE
115 158
             else:
116
-                return 'active'
159
+                return InstanceState.ACTIVE
117 160
         else:
118
-            return 'unknown'
161
+            return InstanceState.UNKNOWN
119 162
 
120 163
     def to_dict(self):
121 164
         """Convert instance to a dict."""
@@ -123,7 +166,7 @@ class Instance(object):
123 166
             'hostname': self.hostname,
124 167
             'ip_addresses': self.ip_addresses(),
125 168
             'node': self._node.to_dict(),
126
-            'state': self.state,
169
+            'state': self.state.value,
127 170
             'uuid': self._uuid,
128 171
         }
129 172
 

+ 15
- 2
metalsmith/_provisioner.py View File

@@ -419,6 +419,8 @@ class Provisioner(_utils.GetNodeMixin):
419 419
 
420 420
         :param instance_id: hostname, UUID or node name.
421 421
         :return: :py:class:`metalsmith.Instance` object.
422
+        :raises: :py:class:`metalsmith.exceptions.InvalidInstance`
423
+            if the instance is not a valid instance.
422 424
         """
423 425
         return self.show_instances([instance_id])[0]
424 426
 
@@ -431,14 +433,25 @@ class Provisioner(_utils.GetNodeMixin):
431 433
         :param instances: list of hostnames, UUIDs or node names.
432 434
         :return: list of :py:class:`metalsmith.Instance` objects in the same
433 435
             order as ``instances``.
436
+        :raises: :py:class:`metalsmith.exceptions.InvalidInstance`
437
+            if one of the instances are not valid instances.
434 438
         """
435 439
         with self._cache_node_list_for_lookup():
436
-            return [
440
+            result = [
437 441
                 _instance.Instance(
438 442
                     self.connection,
439 443
                     self._get_node(inst, accept_hostname=True))
440 444
                 for inst in instances
441 445
             ]
446
+        # NOTE(dtantsur): do not accept node names as valid instances if they
447
+        # are not deployed or being deployed.
448
+        missing = [inst for (res, inst) in zip(result, instances)
449
+                   if res.state == _instance.InstanceState.UNKNOWN]
450
+        if missing:
451
+            raise exceptions.InvalidInstance(
452
+                "Node(s)/instance(s) %s are not valid instances"
453
+                % ', '.join(map(str, missing)))
454
+        return result
442 455
 
443 456
     def list_instances(self):
444 457
         """List instances deployed by metalsmith.
@@ -449,5 +462,5 @@ class Provisioner(_utils.GetNodeMixin):
449 462
         instances = [i for i in
450 463
                      (_instance.Instance(self.connection, node)
451 464
                       for node in nodes)
452
-                     if i._is_deployed_by_metalsmith]
465
+                     if i.state != _instance.InstanceState.UNKNOWN]
453 466
         return instances

+ 4
- 0
metalsmith/exceptions.py View File

@@ -120,3 +120,7 @@ class DeploymentFailure(Error):
120 120
     def __init__(self, message, nodes):
121 121
         self.nodes = nodes
122 122
         super(DeploymentFailure, self).__init__(message)
123
+
124
+
125
+class InvalidInstance(Error):
126
+    """The node(s) does not have a metalsmith instance associated."""

+ 20
- 19
metalsmith/test/test_cmd.py View File

@@ -76,7 +76,7 @@ class TestDeploy(testtools.TestCase):
76 76
         instance.create_autospec(_instance.Instance)
77 77
         instance.node.name = None
78 78
         instance.node.id = '123'
79
-        instance.state = 'active'
79
+        instance.state = _instance.InstanceState.ACTIVE
80 80
         instance.is_deployed = True
81 81
         instance.ip_addresses.return_value = {'private': ['1.2.3.4']}
82 82
 
@@ -99,7 +99,7 @@ class TestDeploy(testtools.TestCase):
99 99
             mock_log.getLogger.mock_calls)
100 100
 
101 101
         self.mock_print.assert_has_calls([
102
-            mock.call(mock.ANY, node='123', state='active'),
102
+            mock.call(mock.ANY, node='123', state='ACTIVE'),
103 103
             mock.call(mock.ANY, ips='private=1.2.3.4')
104 104
         ])
105 105
 
@@ -132,14 +132,14 @@ class TestDeploy(testtools.TestCase):
132 132
         instance.ip_addresses.return_value = {}
133 133
         instance.node.name = None
134 134
         instance.node.id = '123'
135
-        instance.state = 'active'
135
+        instance.state = _instance.InstanceState.ACTIVE
136 136
 
137 137
         args = ['deploy', '--network', 'mynet', '--image', 'myimg',
138 138
                 '--resource-class', 'compute']
139 139
         self._check(mock_pr, args, {}, {})
140 140
 
141 141
         self.mock_print.assert_called_once_with(mock.ANY, node='123',
142
-                                                state='active'),
142
+                                                state='ACTIVE'),
143 143
 
144 144
     def test_not_deployed_no_ips(self, mock_pr):
145 145
         instance = mock_pr.return_value.provision_node.return_value
@@ -147,14 +147,14 @@ class TestDeploy(testtools.TestCase):
147 147
         instance.is_deployed = False
148 148
         instance.node.name = None
149 149
         instance.node.id = '123'
150
-        instance.state = 'deploying'
150
+        instance.state = _instance.InstanceState.DEPLOYING
151 151
 
152 152
         args = ['deploy', '--network', 'mynet', '--image', 'myimg',
153 153
                 '--resource-class', 'compute']
154 154
         self._check(mock_pr, args, {}, {})
155 155
 
156 156
         self.mock_print.assert_called_once_with(mock.ANY, node='123',
157
-                                                state='deploying'),
157
+                                                state='DEPLOYING'),
158 158
 
159 159
     @mock.patch.object(_cmd.LOG, 'info', autospec=True)
160 160
     def test_no_logs_not_deployed(self, mock_log, mock_pr):
@@ -581,11 +581,12 @@ class TestShowWait(testtools.TestCase):
581 581
             'metalsmith._format._print', autospec=True))
582 582
         self.mock_print = self.print_fixture.mock
583 583
         self.instances = [
584
-            mock.Mock(spec=_instance.Instance, hostname=hostname,
585
-                      uuid=hostname[-1], is_deployed=(hostname[-1] == '1'),
586
-                      state=('active' if hostname[-1] == '1' else 'deploying'),
587
-                      **{'ip_addresses.return_value': {'private':
588
-                                                       ['1.2.3.4']}})
584
+            mock.Mock(
585
+                spec=_instance.Instance, hostname=hostname,
586
+                uuid=hostname[-1], is_deployed=(hostname[-1] == '1'),
587
+                state=_instance.InstanceState.ACTIVE
588
+                if hostname[-1] == '1' else _instance.InstanceState.DEPLOYING,
589
+                **{'ip_addresses.return_value': {'private': ['1.2.3.4']}})
589 590
             for hostname in ['hostname1', 'hostname2']
590 591
         ]
591 592
         for inst in self.instances:
@@ -599,9 +600,9 @@ class TestShowWait(testtools.TestCase):
599 600
         _cmd.main(args)
600 601
 
601 602
         self.mock_print.assert_has_calls([
602
-            mock.call(mock.ANY, node='name-1 (UUID 1)', state='active'),
603
+            mock.call(mock.ANY, node='name-1 (UUID 1)', state='ACTIVE'),
603 604
             mock.call(mock.ANY, ips='private=1.2.3.4'),
604
-            mock.call(mock.ANY, node='name-2 (UUID 2)', state='deploying'),
605
+            mock.call(mock.ANY, node='name-2 (UUID 2)', state='DEPLOYING'),
605 606
         ])
606 607
         mock_pr.return_value.show_instances.assert_called_once_with(
607 608
             ['uuid1', 'hostname2'])
@@ -612,9 +613,9 @@ class TestShowWait(testtools.TestCase):
612 613
         _cmd.main(args)
613 614
 
614 615
         self.mock_print.assert_has_calls([
615
-            mock.call(mock.ANY, node='name-1 (UUID 1)', state='active'),
616
+            mock.call(mock.ANY, node='name-1 (UUID 1)', state='ACTIVE'),
616 617
             mock.call(mock.ANY, ips='private=1.2.3.4'),
617
-            mock.call(mock.ANY, node='name-2 (UUID 2)', state='deploying'),
618
+            mock.call(mock.ANY, node='name-2 (UUID 2)', state='DEPLOYING'),
618 619
         ])
619 620
         mock_pr.return_value.list_instances.assert_called_once_with()
620 621
 
@@ -625,9 +626,9 @@ class TestShowWait(testtools.TestCase):
625 626
         _cmd.main(args)
626 627
 
627 628
         self.mock_print.assert_has_calls([
628
-            mock.call(mock.ANY, node='name-1 (UUID 1)', state='active'),
629
+            mock.call(mock.ANY, node='name-1 (UUID 1)', state='ACTIVE'),
629 630
             mock.call(mock.ANY, ips='private=1.2.3.4'),
630
-            mock.call(mock.ANY, node='name-2 (UUID 2)', state='deploying'),
631
+            mock.call(mock.ANY, node='name-2 (UUID 2)', state='DEPLOYING'),
631 632
         ])
632 633
         mock_pr.return_value.wait_for_provisioning.assert_called_once_with(
633 634
             ['uuid1', 'hostname2'], timeout=None)
@@ -639,9 +640,9 @@ class TestShowWait(testtools.TestCase):
639 640
         _cmd.main(args)
640 641
 
641 642
         self.mock_print.assert_has_calls([
642
-            mock.call(mock.ANY, node='name-1 (UUID 1)', state='active'),
643
+            mock.call(mock.ANY, node='name-1 (UUID 1)', state='ACTIVE'),
643 644
             mock.call(mock.ANY, ips='private=1.2.3.4'),
644
-            mock.call(mock.ANY, node='name-2 (UUID 2)', state='deploying'),
645
+            mock.call(mock.ANY, node='name-2 (UUID 2)', state='DEPLOYING'),
645 646
         ])
646 647
         mock_pr.return_value.wait_for_provisioning.assert_called_once_with(
647 648
             ['uuid1', 'hostname2'], timeout=42)

+ 38
- 8
metalsmith/test/test_instance.py View File

@@ -14,6 +14,7 @@
14 14
 # limitations under the License.
15 15
 
16 16
 import mock
17
+import six
17 18
 
18 19
 from metalsmith import _instance
19 20
 from metalsmith.test import test_provisioner
@@ -57,47 +58,68 @@ class TestInstanceStates(test_provisioner.Base):
57 58
 
58 59
     def test_state_deploying(self):
59 60
         self.node.provision_state = 'wait call-back'
60
-        self.assertEqual('deploying', self.instance.state)
61
+        self.assertEqual(_instance.InstanceState.DEPLOYING,
62
+                         self.instance.state)
61 63
         self.assertFalse(self.instance.is_deployed)
62 64
         self.assertTrue(self.instance.is_healthy)
65
+        self.assertTrue(self.instance.state.is_healthy)
66
+        self.assertFalse(self.instance.state.is_deployed)
63 67
 
64 68
     def test_state_deploying_when_available(self):
65 69
         self.node.provision_state = 'available'
66
-        self.assertEqual('deploying', self.instance.state)
70
+        self.node.instance_id = 'abcd'
71
+        self.assertEqual(_instance.InstanceState.DEPLOYING,
72
+                         self.instance.state)
67 73
         self.assertFalse(self.instance.is_deployed)
68 74
         self.assertTrue(self.instance.is_healthy)
69 75
 
76
+    def test_state_unknown_when_available(self):
77
+        self.node.provision_state = 'available'
78
+        self.node.instance_id = None
79
+        self.assertEqual(_instance.InstanceState.UNKNOWN, self.instance.state)
80
+        self.assertFalse(self.instance.is_deployed)
81
+        self.assertFalse(self.instance.is_healthy)
82
+        self.assertFalse(self.instance.state.is_healthy)
83
+
70 84
     def test_state_deploying_maintenance(self):
71 85
         self.node.is_maintenance = True
72 86
         self.node.provision_state = 'wait call-back'
73
-        self.assertEqual('deploying', self.instance.state)
87
+        self.assertEqual(_instance.InstanceState.DEPLOYING,
88
+                         self.instance.state)
74 89
         self.assertFalse(self.instance.is_deployed)
75 90
         self.assertFalse(self.instance.is_healthy)
91
+        # The state itself is considered healthy
92
+        self.assertTrue(self.instance.state.is_healthy)
76 93
 
77 94
     def test_state_active(self):
78 95
         self.node.provision_state = 'active'
79
-        self.assertEqual('active', self.instance.state)
96
+        self.assertEqual(_instance.InstanceState.ACTIVE, self.instance.state)
80 97
         self.assertTrue(self.instance.is_deployed)
81 98
         self.assertTrue(self.instance.is_healthy)
99
+        self.assertTrue(self.instance.state.is_deployed)
82 100
 
83 101
     def test_state_maintenance(self):
84 102
         self.node.is_maintenance = True
85 103
         self.node.provision_state = 'active'
86
-        self.assertEqual('maintenance', self.instance.state)
104
+        self.assertEqual(_instance.InstanceState.MAINTENANCE,
105
+                         self.instance.state)
87 106
         self.assertTrue(self.instance.is_deployed)
88 107
         self.assertFalse(self.instance.is_healthy)
108
+        self.assertFalse(self.instance.state.is_healthy)
89 109
 
90 110
     def test_state_error(self):
91 111
         self.node.provision_state = 'deploy failed'
92
-        self.assertEqual('error', self.instance.state)
112
+        self.assertEqual(_instance.InstanceState.ERROR, self.instance.state)
93 113
         self.assertFalse(self.instance.is_deployed)
94 114
         self.assertFalse(self.instance.is_healthy)
115
+        self.assertFalse(self.instance.state.is_healthy)
95 116
 
96 117
     def test_state_unknown(self):
97 118
         self.node.provision_state = 'enroll'
98
-        self.assertEqual('unknown', self.instance.state)
119
+        self.assertEqual(_instance.InstanceState.UNKNOWN, self.instance.state)
99 120
         self.assertFalse(self.instance.is_deployed)
100 121
         self.assertFalse(self.instance.is_healthy)
122
+        self.assertFalse(self.instance.state.is_healthy)
101 123
 
102 124
     @mock.patch.object(_instance.Instance, 'ip_addresses', autospec=True)
103 125
     def test_to_dict(self, mock_ips):
@@ -106,9 +128,17 @@ class TestInstanceStates(test_provisioner.Base):
106 128
         self.node.instance_info = {'metalsmith_hostname': 'host'}
107 129
         mock_ips.return_value = {'private': ['1.2.3.4']}
108 130
 
131
+        to_dict = self.instance.to_dict()
109 132
         self.assertEqual({'hostname': 'host',
110 133
                           'ip_addresses': {'private': ['1.2.3.4']},
111 134
                           'node': {'node': 'dict'},
112 135
                           'state': 'deploying',
113 136
                           'uuid': self.node.id},
114
-                         self.instance.to_dict())
137
+                         to_dict)
138
+        # States are converted to strings
139
+        self.assertIsInstance(to_dict['state'], six.string_types)
140
+
141
+    def test_deprecated_comparisons(self):
142
+        for item in _instance.InstanceState:
143
+            self.assertEqual(item, item.value)
144
+            self.assertFalse(item != item.value)  # noqa

+ 9
- 5
metalsmith/test/test_provisioner.py View File

@@ -1305,6 +1305,10 @@ class TestUnprovisionNode(Base):
1305 1305
 
1306 1306
 
1307 1307
 class TestShowInstance(Base):
1308
+    def setUp(self):
1309
+        super(TestShowInstance, self).setUp()
1310
+        self.node.provision_state = 'active'
1311
+
1308 1312
     def test_show_instance(self):
1309 1313
         self.mock_get_node.side_effect = lambda n, *a, **kw: self.node
1310 1314
         inst = self.pr.show_instance('id1')
@@ -1315,7 +1319,7 @@ class TestShowInstance(Base):
1315 1319
         self.assertIs(inst.uuid, self.node.id)
1316 1320
 
1317 1321
     def test_show_instances(self):
1318
-        self.mock_get_node.side_effect = [self.node, mock.Mock()]
1322
+        self.mock_get_node.side_effect = [self.node, self.node]
1319 1323
         result = self.pr.show_instances(['1', '2'])
1320 1324
         self.mock_get_node.assert_has_calls([
1321 1325
             mock.call(self.pr, '1', accept_hostname=True),
@@ -1343,16 +1347,16 @@ class TestListInstances(Base):
1343 1347
         super(TestListInstances, self).setUp()
1344 1348
         self.nodes = [
1345 1349
             mock.Mock(spec=NODE_FIELDS, provision_state=state,
1346
-                      instance_info={'metalsmith_hostname': '1234'})
1350
+                      instance_id='1234')
1347 1351
             for state in ('active', 'active', 'deploying', 'wait call-back',
1348
-                          'deploy failed', 'available')
1352
+                          'deploy failed', 'available', 'available', 'enroll')
1349 1353
         ]
1350
-        del self.nodes[-1].instance_info['metalsmith_hostname']
1354
+        self.nodes[6].instance_id = None
1351 1355
         self.api.baremetal.nodes.return_value = self.nodes
1352 1356
 
1353 1357
     def test_list(self):
1354 1358
         instances = self.pr.list_instances()
1355 1359
         self.assertTrue(isinstance(i, _instance.Instance) for i in instances)
1356
-        self.assertEqual(self.nodes[:5], [i.node for i in instances])
1360
+        self.assertEqual(self.nodes[:6], [i.node for i in instances])
1357 1361
         self.api.baremetal.nodes.assert_called_once_with(associated=True,
1358 1362
                                                          details=True)

+ 17
- 0
releasenotes/notes/states-79b593683c0783d5.yaml View File

@@ -0,0 +1,17 @@
1
+---
2
+features:
3
+  - |
4
+    The ``Instance.state`` value is now a proper enumeration of type
5
+    ``metalsmith.InstanceState``.
6
+  - |
7
+    The ``list_instances`` call now returns any valid instances, not only ones
8
+    created by metalsmith. This is consistent with the ``show_instance(s)``
9
+    behavior.
10
+deprecations:
11
+  - |
12
+    Comparing an instance state with strings is deprecated, use enumeration
13
+    values instead.
14
+fixes:
15
+  - |
16
+    Fixes the ``show_instance(s)`` calls to return an exception for nodes that
17
+    are not valid instances (state == ``UNKNOWN``).

+ 1
- 0
requirements.txt View File

@@ -5,3 +5,4 @@ pbr!=2.1.0,>=2.0.0 # Apache-2.0
5 5
 openstacksdk>=0.22.0 # Apache-2.0
6 6
 requests>=2.18.4 # Apache-2.0
7 7
 six>=1.10.0 # MIT
8
+enum34>=1.0.4;python_version=='2.7' or python_version=='2.6' or python_version=='3.3' # BSD

Loading…
Cancel
Save