openshift-python-wrapper 4.11.61__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ocp_resources/__init__.py +0 -0
- ocp_resources/api_service.py +9 -0
- ocp_resources/backup.py +52 -0
- ocp_resources/benchmark.py +92 -0
- ocp_resources/catalog_source.py +63 -0
- ocp_resources/catalog_source_config.py +86 -0
- ocp_resources/cdi.py +13 -0
- ocp_resources/cdi_config.py +52 -0
- ocp_resources/chaos_engine.py +30 -0
- ocp_resources/chaos_result.py +11 -0
- ocp_resources/cluster_operator.py +5 -0
- ocp_resources/cluster_role.py +70 -0
- ocp_resources/cluster_role_binding.py +36 -0
- ocp_resources/cluster_service_version.py +8 -0
- ocp_resources/cluster_version.py +5 -0
- ocp_resources/configmap.py +37 -0
- ocp_resources/console_cli_download.py +9 -0
- ocp_resources/console_plugin.py +9 -0
- ocp_resources/console_quick_start.py +9 -0
- ocp_resources/constants.py +26 -0
- ocp_resources/csidriver.py +9 -0
- ocp_resources/custom_resource_definition.py +5 -0
- ocp_resources/daemonset.py +61 -0
- ocp_resources/data_import_cron.py +100 -0
- ocp_resources/data_source.py +59 -0
- ocp_resources/datavolume.py +275 -0
- ocp_resources/deployment.py +65 -0
- ocp_resources/destination_rule.py +11 -0
- ocp_resources/event.py +114 -0
- ocp_resources/forklift_controller.py +10 -0
- ocp_resources/gateway.py +11 -0
- ocp_resources/hook.py +52 -0
- ocp_resources/host.py +64 -0
- ocp_resources/hostpath_provisioner.py +50 -0
- ocp_resources/hyperconverged.py +43 -0
- ocp_resources/image.py +9 -0
- ocp_resources/image_content_source_policy.py +9 -0
- ocp_resources/image_stream.py +48 -0
- ocp_resources/imagestreamtag.py +9 -0
- ocp_resources/infrastructure.py +18 -0
- ocp_resources/installplan.py +8 -0
- ocp_resources/job.py +65 -0
- ocp_resources/kube_descheduler.py +65 -0
- ocp_resources/kubevirt.py +5 -0
- ocp_resources/kubevirt_common_templates_bundle.py +5 -0
- ocp_resources/kubevirt_metrics_aggregation.py +5 -0
- ocp_resources/kubevirt_node_labeller_bundle.py +5 -0
- ocp_resources/kubevirt_template_validaotr.py +5 -0
- ocp_resources/logger.py +76 -0
- ocp_resources/machine.py +50 -0
- ocp_resources/machine_config.py +5 -0
- ocp_resources/machine_config_pool.py +9 -0
- ocp_resources/machine_health_check.py +76 -0
- ocp_resources/machine_set.py +189 -0
- ocp_resources/migration.py +63 -0
- ocp_resources/migration_policy.py +66 -0
- ocp_resources/mtv.py +171 -0
- ocp_resources/mutating_webhook_config.py +9 -0
- ocp_resources/namespace.py +40 -0
- ocp_resources/network.py +5 -0
- ocp_resources/network_addons_config.py +9 -0
- ocp_resources/network_attachment_definition.py +24 -0
- ocp_resources/network_map.py +64 -0
- ocp_resources/network_policy.py +9 -0
- ocp_resources/nmstate.py +9 -0
- ocp_resources/node.py +41 -0
- ocp_resources/node_health_check.py +10 -0
- ocp_resources/node_maintenance.py +40 -0
- ocp_resources/node_network_configuration_enactment.py +20 -0
- ocp_resources/node_network_configuration_policy.py +472 -0
- ocp_resources/node_network_state.py +121 -0
- ocp_resources/oauth.py +9 -0
- ocp_resources/operator_condition.py +5 -0
- ocp_resources/operator_group.py +37 -0
- ocp_resources/operator_hub.py +5 -0
- ocp_resources/operator_source.py +50 -0
- ocp_resources/package_manifest.py +5 -0
- ocp_resources/peer_authentication.py +18 -0
- ocp_resources/persistent_volume.py +27 -0
- ocp_resources/persistent_volume_claim.py +103 -0
- ocp_resources/plan.py +143 -0
- ocp_resources/pod.py +175 -0
- ocp_resources/pod_disruption_budget.py +53 -0
- ocp_resources/priority_class.py +48 -0
- ocp_resources/project.py +60 -0
- ocp_resources/prometheus.py +9 -0
- ocp_resources/prometheus_rule.py +9 -0
- ocp_resources/provider.py +59 -0
- ocp_resources/replicaset.py +11 -0
- ocp_resources/resource.py +1383 -0
- ocp_resources/restore.py +52 -0
- ocp_resources/role.py +11 -0
- ocp_resources/role_binding.py +67 -0
- ocp_resources/route.py +76 -0
- ocp_resources/secret.py +75 -0
- ocp_resources/security_context_constraints.py +9 -0
- ocp_resources/service.py +16 -0
- ocp_resources/service_account.py +9 -0
- ocp_resources/service_mesh_control_plane.py +11 -0
- ocp_resources/service_mesh_member_roll.py +14 -0
- ocp_resources/service_monitor.py +9 -0
- ocp_resources/sriov_network.py +54 -0
- ocp_resources/sriov_network_node_policy.py +67 -0
- ocp_resources/sriov_network_node_state.py +44 -0
- ocp_resources/ssp.py +9 -0
- ocp_resources/storage_class.py +47 -0
- ocp_resources/storage_map.py +67 -0
- ocp_resources/storage_profile.py +7 -0
- ocp_resources/subscription.py +66 -0
- ocp_resources/tekton_tasks.py +9 -0
- ocp_resources/template.py +74 -0
- ocp_resources/upload_token_request.py +39 -0
- ocp_resources/utils.py +278 -0
- ocp_resources/validating_webhook_config.py +9 -0
- ocp_resources/virtual_machine.py +177 -0
- ocp_resources/virtual_machine_Instance_replica_set.py +9 -0
- ocp_resources/virtual_machine_cluster_flavor.py +5 -0
- ocp_resources/virtual_machine_flavor.py +5 -0
- ocp_resources/virtual_machine_import.py +307 -0
- ocp_resources/virtual_machine_instance.py +322 -0
- ocp_resources/virtual_machine_instance_migration.py +36 -0
- ocp_resources/virtual_machine_instance_preset.py +9 -0
- ocp_resources/virtual_machine_restore.py +99 -0
- ocp_resources/virtual_machine_snapshot.py +96 -0
- ocp_resources/virtual_service.py +11 -0
- ocp_resources/vm_import_config.py +9 -0
- ocp_resources/volume_snapshot.py +9 -0
- ocp_resources/volume_snapshot_class.py +9 -0
- openshift_python_wrapper-4.11.61.dist-info/LICENSE +201 -0
- openshift_python_wrapper-4.11.61.dist-info/METADATA +188 -0
- openshift_python_wrapper-4.11.61.dist-info/RECORD +132 -0
- openshift_python_wrapper-4.11.61.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
from ocp_resources.constants import TIMEOUT_4MINUTES
|
|
2
|
+
from ocp_resources.mtv import MTV
|
|
3
|
+
from ocp_resources.resource import NamespacedResource
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class NetworkMap(NamespacedResource, MTV):
|
|
7
|
+
"""
|
|
8
|
+
Migration Toolkit For Virtualization (MTV) NetworkMap object.
|
|
9
|
+
|
|
10
|
+
Args:
|
|
11
|
+
source_provider_name (str): MTV Source Provider CR name.
|
|
12
|
+
source_provider_namespace (str): MTV Source Provider CR namespace.
|
|
13
|
+
destination_provider_name (str): MTV Destination Provider CR name.
|
|
14
|
+
destination_provider_namespace (str): MTV Destination Provider CR namespace.
|
|
15
|
+
mapping (dict): Storage Resources Mapping
|
|
16
|
+
Exaple:
|
|
17
|
+
[ { "destination" : { "type": "pod",
|
|
18
|
+
"source" : { "id": "network-13" }},
|
|
19
|
+
|
|
20
|
+
{ "destination" : { "name": "nad_cr_name",
|
|
21
|
+
"namespace": "nad_cr_namespace",
|
|
22
|
+
"type": "multus"},
|
|
23
|
+
"source" : { "name": "VM Netowrk" }},
|
|
24
|
+
|
|
25
|
+
]
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
api_group = NamespacedResource.ApiGroup.FORKLIFT_KONVEYOR_IO
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
name=None,
|
|
33
|
+
namespace=None,
|
|
34
|
+
mapping=None,
|
|
35
|
+
source_provider_name=None,
|
|
36
|
+
source_provider_namespace=None,
|
|
37
|
+
destination_provider_name=None,
|
|
38
|
+
destination_provider_namespace=None,
|
|
39
|
+
client=None,
|
|
40
|
+
teardown=True,
|
|
41
|
+
yaml_file=None,
|
|
42
|
+
delete_timeout=TIMEOUT_4MINUTES,
|
|
43
|
+
**kwargs,
|
|
44
|
+
):
|
|
45
|
+
super().__init__(
|
|
46
|
+
name=name,
|
|
47
|
+
namespace=namespace,
|
|
48
|
+
client=client,
|
|
49
|
+
teardown=teardown,
|
|
50
|
+
yaml_file=yaml_file,
|
|
51
|
+
delete_timeout=delete_timeout,
|
|
52
|
+
**kwargs,
|
|
53
|
+
)
|
|
54
|
+
self.mapping = mapping
|
|
55
|
+
self.source_provider_name = source_provider_name
|
|
56
|
+
self.source_provider_namespace = source_provider_namespace
|
|
57
|
+
self.destination_provider_name = destination_provider_name
|
|
58
|
+
self.destination_provider_namespace = destination_provider_namespace
|
|
59
|
+
self.condition_message_ready = self.ConditionMessage.NETWORK_MAP_READY
|
|
60
|
+
|
|
61
|
+
def to_dict(self):
|
|
62
|
+
super().to_dict()
|
|
63
|
+
if not self.yaml_file:
|
|
64
|
+
self.res.update(self.map_to_dict)
|
ocp_resources/nmstate.py
ADDED
ocp_resources/node.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from ocp_resources.resource import Resource
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Node(Resource):
|
|
5
|
+
"""
|
|
6
|
+
Node object, inherited from Resource.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
api_version = Resource.ApiVersion.V1
|
|
10
|
+
|
|
11
|
+
class Status(Resource.Status):
|
|
12
|
+
READY = "Ready"
|
|
13
|
+
SCHEDULING_DISABLED = "Ready,SchedulingDisabled"
|
|
14
|
+
|
|
15
|
+
@property
|
|
16
|
+
def kubelet_ready(self):
|
|
17
|
+
return any(
|
|
18
|
+
stat["reason"] == "KubeletReady"
|
|
19
|
+
and stat["status"] == self.Condition.Status.TRUE
|
|
20
|
+
for stat in self.instance.status.conditions
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def machine_name(self):
|
|
25
|
+
return self.instance.metadata.annotations[
|
|
26
|
+
f"{self.ApiGroup.MACHINE_OPENSHIFT_IO}/machine"
|
|
27
|
+
].split("/")[-1]
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def internal_ip(self):
|
|
31
|
+
for addr in self.instance.status.addresses:
|
|
32
|
+
if addr.type == "InternalIP":
|
|
33
|
+
return addr.address
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def hostname(self):
|
|
37
|
+
return self.labels["kubernetes.io/hostname"]
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def taints(self):
|
|
41
|
+
return self.instance.get("spec", {}).get("taints")
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from ocp_resources.resource import NamespacedResource
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class NodeHealthCheck(NamespacedResource):
|
|
5
|
+
"""
|
|
6
|
+
NodeHealthCheck object.
|
|
7
|
+
Reference : https://github.com/medik8s/node-healthcheck-operator
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
api_group = NamespacedResource.ApiGroup.REMEDIATION_MEDIK8S_IO
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from ocp_resources.constants import TIMEOUT_4MINUTES
|
|
2
|
+
from ocp_resources.resource import Resource
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class NodeMaintenance(Resource):
|
|
6
|
+
"""
|
|
7
|
+
Node Maintenance object, inherited from Resource.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
api_group = Resource.ApiGroup.NODEMAINTENANCE_KUBEVIRT_IO
|
|
11
|
+
|
|
12
|
+
def __init__(
|
|
13
|
+
self,
|
|
14
|
+
name=None,
|
|
15
|
+
client=None,
|
|
16
|
+
node=None,
|
|
17
|
+
reason="TEST Reason",
|
|
18
|
+
teardown=True,
|
|
19
|
+
timeout=TIMEOUT_4MINUTES,
|
|
20
|
+
yaml_file=None,
|
|
21
|
+
delete_timeout=TIMEOUT_4MINUTES,
|
|
22
|
+
**kwargs,
|
|
23
|
+
):
|
|
24
|
+
super().__init__(
|
|
25
|
+
name=name,
|
|
26
|
+
client=client,
|
|
27
|
+
teardown=teardown,
|
|
28
|
+
timeout=timeout,
|
|
29
|
+
yaml_file=yaml_file,
|
|
30
|
+
delete_timeout=delete_timeout,
|
|
31
|
+
**kwargs,
|
|
32
|
+
)
|
|
33
|
+
self.node = node
|
|
34
|
+
self.reason = reason
|
|
35
|
+
|
|
36
|
+
def to_dict(self):
|
|
37
|
+
super().to_dict()
|
|
38
|
+
if not self.yaml_file:
|
|
39
|
+
assert self.node, "node is mandatory for create"
|
|
40
|
+
self.res["spec"] = {"nodeName": self.node.name, "reason": self.reason}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from ocp_resources.resource import Resource
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class NodeNetworkConfigurationEnactment(Resource):
|
|
5
|
+
api_group = Resource.ApiGroup.NMSTATE_IO
|
|
6
|
+
|
|
7
|
+
class Conditions:
|
|
8
|
+
class Type:
|
|
9
|
+
FAILING = "Failing"
|
|
10
|
+
AVAILABLE = "Available"
|
|
11
|
+
PROGRESSING = "Progressing"
|
|
12
|
+
PENDING = "Pending"
|
|
13
|
+
ABORTED = "Aborted"
|
|
14
|
+
|
|
15
|
+
class Reason:
|
|
16
|
+
CONFIGURATION_PROGRESSING = "ConfigurationProgressing"
|
|
17
|
+
SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
|
|
18
|
+
FAILED_TO_CONFIGURE = "FailedToConfigure"
|
|
19
|
+
CONFIGURATION_ABORTED = "ConfigurationAborted"
|
|
20
|
+
MAX_UNAVAILABLE_LIMIT_REACHED = "MaxUnavailableLimitReached"
|
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
3
|
+
from openshift.dynamic.exceptions import ConflictError
|
|
4
|
+
|
|
5
|
+
from ocp_resources.constants import TIMEOUT_4MINUTES
|
|
6
|
+
from ocp_resources.node import Node
|
|
7
|
+
from ocp_resources.node_network_configuration_enactment import (
|
|
8
|
+
NodeNetworkConfigurationEnactment,
|
|
9
|
+
)
|
|
10
|
+
from ocp_resources.node_network_state import NodeNetworkState
|
|
11
|
+
from ocp_resources.resource import Resource, ResourceEditor
|
|
12
|
+
from ocp_resources.utils import TimeoutExpiredError, TimeoutSampler
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
IPV4_STR = "ipv4"
|
|
16
|
+
IPV6_STR = "ipv6"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class NNCPConfigurationFailed(Exception):
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class NodeNetworkConfigurationPolicy(Resource):
|
|
24
|
+
api_group = Resource.ApiGroup.NMSTATE_IO
|
|
25
|
+
|
|
26
|
+
class Conditions:
|
|
27
|
+
class Type:
|
|
28
|
+
DEGRADED = "Degraded"
|
|
29
|
+
AVAILABLE = "Available"
|
|
30
|
+
|
|
31
|
+
class Reason:
|
|
32
|
+
CONFIGURATION_PROGRESSING = "ConfigurationProgressing"
|
|
33
|
+
SUCCESSFULLY_CONFIGURED = "SuccessfullyConfigured"
|
|
34
|
+
FAILED_TO_CONFIGURE = "FailedToConfigure"
|
|
35
|
+
NO_MATCHING_NODE = "NoMatchingNode"
|
|
36
|
+
|
|
37
|
+
def __init__(
|
|
38
|
+
self,
|
|
39
|
+
name=None,
|
|
40
|
+
client=None,
|
|
41
|
+
capture=None,
|
|
42
|
+
node_selector=None,
|
|
43
|
+
node_selector_labels=None,
|
|
44
|
+
teardown_absent_ifaces=True,
|
|
45
|
+
teardown=True,
|
|
46
|
+
mtu=None,
|
|
47
|
+
ports=None,
|
|
48
|
+
ipv4_enable=False,
|
|
49
|
+
ipv4_dhcp=False,
|
|
50
|
+
ipv4_auto_dns=True,
|
|
51
|
+
ipv4_addresses=None,
|
|
52
|
+
ipv6_enable=False,
|
|
53
|
+
ipv6_dhcp=False,
|
|
54
|
+
ipv6_auto_dns=True,
|
|
55
|
+
ipv6_addresses=None,
|
|
56
|
+
dns_resolver=None,
|
|
57
|
+
routes=None,
|
|
58
|
+
yaml_file=None,
|
|
59
|
+
set_ipv4=True,
|
|
60
|
+
set_ipv6=True,
|
|
61
|
+
max_unavailable=None,
|
|
62
|
+
state=None,
|
|
63
|
+
success_timeout=480,
|
|
64
|
+
delete_timeout=TIMEOUT_4MINUTES,
|
|
65
|
+
**kwargs,
|
|
66
|
+
):
|
|
67
|
+
"""
|
|
68
|
+
ipv4_addresses should be sent in this format:
|
|
69
|
+
[{"ip": <ip1-string>, "prefix-length": <prefix-len1-int>},
|
|
70
|
+
{"ip": <ip2-string>, "prefix-length": <prefix-len2-int>}, ...]
|
|
71
|
+
For example:
|
|
72
|
+
[{"ip": "10.1.2.3", "prefix-length": 24},
|
|
73
|
+
{"ip": "10.4.5.6", "prefix-length": 24},
|
|
74
|
+
{"ip": "10.7.8.9", "prefix-length": 23}]
|
|
75
|
+
"""
|
|
76
|
+
super().__init__(
|
|
77
|
+
name=name,
|
|
78
|
+
client=client,
|
|
79
|
+
teardown=teardown,
|
|
80
|
+
yaml_file=yaml_file,
|
|
81
|
+
delete_timeout=delete_timeout,
|
|
82
|
+
node_selector=node_selector,
|
|
83
|
+
node_selector_labels=node_selector_labels,
|
|
84
|
+
**kwargs,
|
|
85
|
+
)
|
|
86
|
+
self.desired_state = {"interfaces": []}
|
|
87
|
+
self.mtu = mtu
|
|
88
|
+
self.capture = capture
|
|
89
|
+
self.mtu_dict = {}
|
|
90
|
+
self.ports = ports or []
|
|
91
|
+
self.iface = None
|
|
92
|
+
self.ipv4_enable = ipv4_enable
|
|
93
|
+
self.ipv4_dhcp = ipv4_dhcp
|
|
94
|
+
self.ipv4_auto_dns = ipv4_auto_dns
|
|
95
|
+
self.ipv4_addresses = ipv4_addresses or []
|
|
96
|
+
self.ipv4_iface_state = {}
|
|
97
|
+
self.ipv6_enable = ipv6_enable
|
|
98
|
+
self.ipv6_dhcp = ipv6_dhcp
|
|
99
|
+
self.ipv6_autoconf = self.ipv6_dhcp
|
|
100
|
+
self.ipv6_auto_dns = ipv6_auto_dns
|
|
101
|
+
self.ipv6_addresses = ipv6_addresses
|
|
102
|
+
self.dns_resolver = dns_resolver
|
|
103
|
+
self.routes = routes
|
|
104
|
+
self.state = state or self.Interface.State.UP
|
|
105
|
+
self.set_ipv4 = set_ipv4
|
|
106
|
+
self.set_ipv6 = set_ipv6
|
|
107
|
+
self.success_timeout = success_timeout
|
|
108
|
+
self.max_unavailable = max_unavailable
|
|
109
|
+
self.ipv4_ports_backup_dict = {}
|
|
110
|
+
self.ipv6_ports_backup_dict = {}
|
|
111
|
+
self.nodes = self._nodes()
|
|
112
|
+
self.teardown_absent_ifaces = teardown_absent_ifaces
|
|
113
|
+
|
|
114
|
+
def _nodes(self):
|
|
115
|
+
if self.node_selector:
|
|
116
|
+
return list(Node.get(dyn_client=self.client, name=self.node_selector))
|
|
117
|
+
if self.node_selector_labels:
|
|
118
|
+
node_labels = ",".join(
|
|
119
|
+
[
|
|
120
|
+
f"{label_key}={label_value}"
|
|
121
|
+
for label_key, label_value in self.node_selector_labels.items()
|
|
122
|
+
]
|
|
123
|
+
)
|
|
124
|
+
return list(Node.get(dyn_client=self.client, label_selector=node_labels))
|
|
125
|
+
|
|
126
|
+
def set_interface(self, interface):
|
|
127
|
+
if not self.res:
|
|
128
|
+
super().to_dict()
|
|
129
|
+
# First drop the interface if it's already in the list
|
|
130
|
+
interfaces = [
|
|
131
|
+
iface
|
|
132
|
+
for iface in self.desired_state["interfaces"]
|
|
133
|
+
if iface["name"] != interface["name"]
|
|
134
|
+
]
|
|
135
|
+
# Add the interface
|
|
136
|
+
interfaces.append(interface)
|
|
137
|
+
self.desired_state["interfaces"] = interfaces
|
|
138
|
+
self.res.setdefault("spec", {}).setdefault("desiredState", {})[
|
|
139
|
+
"interfaces"
|
|
140
|
+
] = self.desired_state["interfaces"]
|
|
141
|
+
|
|
142
|
+
def to_dict(self):
|
|
143
|
+
super().to_dict()
|
|
144
|
+
if not self.yaml_file:
|
|
145
|
+
if self.dns_resolver or self.routes or self.iface:
|
|
146
|
+
self.res.setdefault("spec", {}).setdefault("desiredState", {})
|
|
147
|
+
|
|
148
|
+
if self.node_selector_spec:
|
|
149
|
+
self.res.setdefault("spec", {}).setdefault(
|
|
150
|
+
"nodeSelector", self.node_selector_spec
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
if self.capture:
|
|
154
|
+
self.res["spec"]["capture"] = self.capture
|
|
155
|
+
|
|
156
|
+
if self.dns_resolver:
|
|
157
|
+
self.res["spec"]["desiredState"]["dns-resolver"] = self.dns_resolver
|
|
158
|
+
|
|
159
|
+
if self.routes:
|
|
160
|
+
self.res["spec"]["desiredState"]["routes"] = self.routes
|
|
161
|
+
|
|
162
|
+
if self.max_unavailable:
|
|
163
|
+
self.res.setdefault("spec", {}).setdefault(
|
|
164
|
+
"maxUnavailable", self.max_unavailable
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
if self.iface:
|
|
168
|
+
"""
|
|
169
|
+
It's the responsibility of the caller to verify the desired configuration they send.
|
|
170
|
+
For example: "ipv4.dhcp.enabled: false" without specifying any static IP address
|
|
171
|
+
is a valid desired state and therefore not blocked in the code, but nmstate would
|
|
172
|
+
reject it. Such configuration might be used for negative tests.
|
|
173
|
+
"""
|
|
174
|
+
self.res = self.add_interface(
|
|
175
|
+
iface=self.iface,
|
|
176
|
+
state=self.state,
|
|
177
|
+
set_ipv4=self.set_ipv4,
|
|
178
|
+
ipv4_enable=self.ipv4_enable,
|
|
179
|
+
ipv4_dhcp=self.ipv4_dhcp,
|
|
180
|
+
ipv4_auto_dns=self.ipv4_auto_dns,
|
|
181
|
+
ipv4_addresses=self.ipv4_addresses,
|
|
182
|
+
set_ipv6=self.set_ipv6,
|
|
183
|
+
ipv6_enable=self.ipv6_enable,
|
|
184
|
+
ipv6_dhcp=self.ipv6_dhcp,
|
|
185
|
+
ipv6_auto_dns=self.ipv6_auto_dns,
|
|
186
|
+
ipv6_addresses=self.ipv6_addresses,
|
|
187
|
+
ipv6_autoconf=self.ipv6_autoconf,
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
def add_interface(
|
|
191
|
+
self,
|
|
192
|
+
iface=None,
|
|
193
|
+
name=None,
|
|
194
|
+
type_=None,
|
|
195
|
+
state=None,
|
|
196
|
+
set_ipv4=True,
|
|
197
|
+
ipv4_enable=False,
|
|
198
|
+
ipv4_dhcp=False,
|
|
199
|
+
ipv4_auto_dns=True,
|
|
200
|
+
ipv4_addresses=None,
|
|
201
|
+
set_ipv6=True,
|
|
202
|
+
ipv6_enable=False,
|
|
203
|
+
ipv6_dhcp=False,
|
|
204
|
+
ipv6_auto_dns=True,
|
|
205
|
+
ipv6_addresses=None,
|
|
206
|
+
ipv6_autoconf=False,
|
|
207
|
+
):
|
|
208
|
+
# If self.res is already defined (from to_dict()), don't call it again.
|
|
209
|
+
if not self.res:
|
|
210
|
+
self.to_dict()
|
|
211
|
+
|
|
212
|
+
self.res.setdefault("spec", {}).setdefault("desiredState", {})
|
|
213
|
+
if not iface:
|
|
214
|
+
iface = {
|
|
215
|
+
"name": name,
|
|
216
|
+
"type": type_,
|
|
217
|
+
"state": state,
|
|
218
|
+
}
|
|
219
|
+
if set_ipv4:
|
|
220
|
+
if isinstance(set_ipv4, str):
|
|
221
|
+
iface[IPV4_STR] = set_ipv4
|
|
222
|
+
|
|
223
|
+
else:
|
|
224
|
+
iface[IPV4_STR] = {
|
|
225
|
+
"enabled": ipv4_enable,
|
|
226
|
+
"dhcp": ipv4_dhcp,
|
|
227
|
+
"auto-dns": ipv4_auto_dns,
|
|
228
|
+
}
|
|
229
|
+
if ipv4_addresses:
|
|
230
|
+
iface[IPV4_STR]["address"] = ipv4_addresses
|
|
231
|
+
|
|
232
|
+
if set_ipv6:
|
|
233
|
+
if isinstance(set_ipv6, str):
|
|
234
|
+
iface[IPV6_STR] = set_ipv6
|
|
235
|
+
|
|
236
|
+
else:
|
|
237
|
+
iface[IPV6_STR] = {
|
|
238
|
+
"enabled": ipv6_enable,
|
|
239
|
+
"dhcp": ipv6_dhcp,
|
|
240
|
+
"auto-dns": ipv6_auto_dns,
|
|
241
|
+
"autoconf": ipv6_autoconf,
|
|
242
|
+
}
|
|
243
|
+
if ipv6_addresses:
|
|
244
|
+
iface[IPV6_STR]["address"] = ipv6_addresses
|
|
245
|
+
|
|
246
|
+
self.set_interface(interface=iface)
|
|
247
|
+
return self.res
|
|
248
|
+
|
|
249
|
+
def _get_port_from_nns(self, port_name):
|
|
250
|
+
if not self.nodes:
|
|
251
|
+
return None
|
|
252
|
+
|
|
253
|
+
nns = NodeNetworkState(name=self.nodes[0].name)
|
|
254
|
+
_port = [_iface for _iface in nns.interfaces if _iface["name"] == port_name]
|
|
255
|
+
return _port[0] if _port else None
|
|
256
|
+
|
|
257
|
+
def _ports_backup(self, ip_family):
|
|
258
|
+
for port in self.ports:
|
|
259
|
+
_port = self._get_port_from_nns(port_name=port)
|
|
260
|
+
if _port:
|
|
261
|
+
if ip_family == IPV4_STR:
|
|
262
|
+
self.ipv4_ports_backup_dict[port] = _port[ip_family]
|
|
263
|
+
elif ip_family == IPV6_STR:
|
|
264
|
+
self.ipv6_ports_backup_dict[port] = _port[ip_family]
|
|
265
|
+
else:
|
|
266
|
+
raise ValueError(
|
|
267
|
+
f"'ip_family' must be either '{IPV4_STR}' or '{IPV6_STR}'"
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
def ipv4_ports_backup(self):
|
|
271
|
+
self._ports_backup(ip_family=IPV4_STR)
|
|
272
|
+
|
|
273
|
+
def ipv6_ports_backup(self):
|
|
274
|
+
self._ports_backup(ip_family=IPV6_STR)
|
|
275
|
+
|
|
276
|
+
def add_ports(self):
|
|
277
|
+
for port in self.ports:
|
|
278
|
+
_port = self._get_port_from_nns(port_name=port)
|
|
279
|
+
if _port:
|
|
280
|
+
ipv4_backup = self.ipv4_ports_backup_dict.get(port)
|
|
281
|
+
ipv6_backup = self.ipv6_ports_backup_dict.get(port)
|
|
282
|
+
if ipv4_backup or ipv6_backup:
|
|
283
|
+
iface = {
|
|
284
|
+
"name": port,
|
|
285
|
+
"type": _port["type"],
|
|
286
|
+
"state": _port["state"],
|
|
287
|
+
}
|
|
288
|
+
if ipv4_backup:
|
|
289
|
+
iface[IPV4_STR] = ipv4_backup
|
|
290
|
+
|
|
291
|
+
if ipv6_backup:
|
|
292
|
+
iface[IPV6_STR] = ipv6_backup
|
|
293
|
+
|
|
294
|
+
self.set_interface(interface=iface)
|
|
295
|
+
|
|
296
|
+
def apply(self, resource=None):
|
|
297
|
+
if not resource:
|
|
298
|
+
super().to_dict()
|
|
299
|
+
resource = self.res
|
|
300
|
+
samples = TimeoutSampler(
|
|
301
|
+
wait_timeout=3,
|
|
302
|
+
sleep=1,
|
|
303
|
+
exceptions_dict={ConflictError: []},
|
|
304
|
+
func=self.update,
|
|
305
|
+
resource_dict=resource,
|
|
306
|
+
)
|
|
307
|
+
self.logger.info(f"Applying {resource}")
|
|
308
|
+
for _ in samples:
|
|
309
|
+
return
|
|
310
|
+
|
|
311
|
+
def deploy(self, wait=False):
|
|
312
|
+
self.ipv4_ports_backup()
|
|
313
|
+
self.ipv6_ports_backup()
|
|
314
|
+
|
|
315
|
+
self.create(wait=wait)
|
|
316
|
+
try:
|
|
317
|
+
self.wait_for_status_success()
|
|
318
|
+
return self
|
|
319
|
+
except Exception as exp:
|
|
320
|
+
self.logger.error(exp)
|
|
321
|
+
super().__exit__(exception_type=None, exception_value=None, traceback=None)
|
|
322
|
+
raise
|
|
323
|
+
|
|
324
|
+
def clean_up(self):
|
|
325
|
+
if self.teardown_absent_ifaces:
|
|
326
|
+
try:
|
|
327
|
+
self._absent_interface()
|
|
328
|
+
self.wait_for_status_success()
|
|
329
|
+
except Exception as exp:
|
|
330
|
+
self.logger.error(exp)
|
|
331
|
+
|
|
332
|
+
super().clean_up()
|
|
333
|
+
|
|
334
|
+
def _absent_interface(self):
|
|
335
|
+
for _iface in self.desired_state["interfaces"]:
|
|
336
|
+
_iface["state"] = self.Interface.State.ABSENT
|
|
337
|
+
self.set_interface(interface=_iface)
|
|
338
|
+
|
|
339
|
+
if self.ports:
|
|
340
|
+
self.add_ports()
|
|
341
|
+
|
|
342
|
+
ResourceEditor(
|
|
343
|
+
patches={
|
|
344
|
+
self: {
|
|
345
|
+
"spec": {
|
|
346
|
+
"desiredState": {"interfaces": self.desired_state["interfaces"]}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
).update()
|
|
351
|
+
|
|
352
|
+
@property
|
|
353
|
+
def status(self):
|
|
354
|
+
for condition in self.instance.status.conditions:
|
|
355
|
+
if condition["type"] == self.Conditions.Type.AVAILABLE:
|
|
356
|
+
return condition["reason"]
|
|
357
|
+
|
|
358
|
+
def wait_for_configuration_conditions_unknown_or_progressing(self, wait_timeout=30):
|
|
359
|
+
samples = TimeoutSampler(
|
|
360
|
+
wait_timeout=wait_timeout,
|
|
361
|
+
sleep=1,
|
|
362
|
+
func=lambda: self.instance.status.conditions,
|
|
363
|
+
)
|
|
364
|
+
for sample in samples:
|
|
365
|
+
if (
|
|
366
|
+
sample
|
|
367
|
+
and sample[0]["type"] == self.Conditions.Type.AVAILABLE
|
|
368
|
+
and (
|
|
369
|
+
sample[0]["status"] == self.Condition.Status.UNKNOWN
|
|
370
|
+
or sample[0]["reason"]
|
|
371
|
+
== self.Conditions.Reason.CONFIGURATION_PROGRESSING
|
|
372
|
+
)
|
|
373
|
+
):
|
|
374
|
+
return sample
|
|
375
|
+
|
|
376
|
+
def _process_failed_status(self, failed_condition_reason):
|
|
377
|
+
last_err_msg = None
|
|
378
|
+
for failed_nnce in self._get_failed_nnce():
|
|
379
|
+
nnce_name = failed_nnce.instance.metadata.name
|
|
380
|
+
nnce_dict = failed_nnce.instance.to_dict()
|
|
381
|
+
for cond in nnce_dict["status"]["conditions"]:
|
|
382
|
+
err_msg = self._get_nnce_error_msg(
|
|
383
|
+
nnce_name=nnce_name, nnce_condition=cond
|
|
384
|
+
)
|
|
385
|
+
if err_msg:
|
|
386
|
+
last_err_msg = err_msg
|
|
387
|
+
|
|
388
|
+
raise NNCPConfigurationFailed(
|
|
389
|
+
f"Reason: {failed_condition_reason}\n{last_err_msg}"
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
def wait_for_status_success(self):
|
|
393
|
+
failed_condition_reason = self.Conditions.Reason.FAILED_TO_CONFIGURE
|
|
394
|
+
no_match_node_condition_reason = self.Conditions.Reason.NO_MATCHING_NODE
|
|
395
|
+
|
|
396
|
+
# if we get here too fast there are no conditions, we need to wait.
|
|
397
|
+
self.wait_for_configuration_conditions_unknown_or_progressing()
|
|
398
|
+
|
|
399
|
+
samples = TimeoutSampler(
|
|
400
|
+
wait_timeout=self.success_timeout, sleep=1, func=lambda: self.status
|
|
401
|
+
)
|
|
402
|
+
try:
|
|
403
|
+
for sample in samples:
|
|
404
|
+
if sample == self.Conditions.Reason.SUCCESSFULLY_CONFIGURED:
|
|
405
|
+
self.logger.info(f"NNCP {self.name} configured Successfully")
|
|
406
|
+
return sample
|
|
407
|
+
|
|
408
|
+
elif sample == no_match_node_condition_reason:
|
|
409
|
+
raise NNCPConfigurationFailed(
|
|
410
|
+
f"{self.name}. Reason: {no_match_node_condition_reason}"
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
elif sample == failed_condition_reason:
|
|
414
|
+
self._process_failed_status(
|
|
415
|
+
failed_condition_reason=failed_condition_reason
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
except (TimeoutExpiredError, NNCPConfigurationFailed):
|
|
419
|
+
self.logger.error(
|
|
420
|
+
f"Unable to configure NNCP {self.name} "
|
|
421
|
+
f"{f'nodes: {[node.name for node in self.nodes]}' if self.nodes else ''}"
|
|
422
|
+
)
|
|
423
|
+
raise
|
|
424
|
+
|
|
425
|
+
@property
|
|
426
|
+
def nnces(self):
|
|
427
|
+
nnces = []
|
|
428
|
+
for nnce in NodeNetworkConfigurationEnactment.get(dyn_client=self.client):
|
|
429
|
+
if nnce.name.endswith(f".{self.name}"):
|
|
430
|
+
nnces.append(nnce)
|
|
431
|
+
return nnces
|
|
432
|
+
|
|
433
|
+
def node_nnce(self, node_name):
|
|
434
|
+
nnce = [
|
|
435
|
+
nnce for nnce in self.nnces if nnce.labels["nmstate.io/node"] == node_name
|
|
436
|
+
]
|
|
437
|
+
return nnce[0] if nnce else None
|
|
438
|
+
|
|
439
|
+
@staticmethod
|
|
440
|
+
def _get_nnce_error_msg(nnce_name, nnce_condition):
|
|
441
|
+
err_msg = ""
|
|
442
|
+
nnce_prefix = f"NNCE {nnce_name}"
|
|
443
|
+
nnce_msg = nnce_condition.get("message")
|
|
444
|
+
if not nnce_msg:
|
|
445
|
+
return err_msg
|
|
446
|
+
|
|
447
|
+
errors = nnce_msg.split("->")
|
|
448
|
+
if errors:
|
|
449
|
+
err_msg += f"{nnce_prefix}: {errors[0]}"
|
|
450
|
+
if len(errors) > 1:
|
|
451
|
+
err_msg += errors[-1]
|
|
452
|
+
|
|
453
|
+
libnmstate_err = re.findall(r"libnmstate.error.*", nnce_msg)
|
|
454
|
+
if libnmstate_err:
|
|
455
|
+
err_msg += f"{nnce_prefix}: {libnmstate_err[0]}"
|
|
456
|
+
|
|
457
|
+
return err_msg
|
|
458
|
+
|
|
459
|
+
def _get_failed_nnce(self):
|
|
460
|
+
for nnce in self.nnces:
|
|
461
|
+
try:
|
|
462
|
+
nnce.wait_for_conditions()
|
|
463
|
+
except TimeoutExpiredError:
|
|
464
|
+
self.logger.error(f"Failed to get NNCE {nnce.name} status")
|
|
465
|
+
continue
|
|
466
|
+
|
|
467
|
+
for nnce_cond in nnce.instance.status.conditions:
|
|
468
|
+
if (
|
|
469
|
+
nnce_cond.type == "Failing"
|
|
470
|
+
and nnce_cond.status == Resource.Condition.Status.TRUE
|
|
471
|
+
):
|
|
472
|
+
yield nnce
|