8.0.0
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
#!/usr/bin/python
|
||||
import re
|
||||
from typing import Sequence, Any, TypeVar
|
||||
|
||||
DOCUMENTATION = r'''
|
||||
---
|
||||
@@ -12,6 +14,9 @@ from ansible.module_utils.basic import env_fallback
|
||||
from ansible.module_utils.decort_utils import *
|
||||
|
||||
|
||||
DefaultT = TypeVar('DefaultT')
|
||||
|
||||
|
||||
class decort_kvmvm(DecortController):
|
||||
def __init__(self):
|
||||
# call superclass constructor first
|
||||
@@ -73,6 +78,8 @@ class decort_kvmvm(DecortController):
|
||||
|
||||
if not clone_id:
|
||||
clone_id = self.clone()
|
||||
if self.amodule.check_mode:
|
||||
self.exit()
|
||||
|
||||
self.comp_id, self.comp_info, self.rg_id = self._compute_get_by_id(
|
||||
comp_id=clone_id,
|
||||
@@ -178,8 +185,8 @@ class decort_kvmvm(DecortController):
|
||||
'hp_backed must be set to True to connect a compute '
|
||||
'to a DPDK network.'
|
||||
)
|
||||
# MTU for non-DPDK networks
|
||||
for net in aparam_nets:
|
||||
# MTU for non-DPDK networks
|
||||
if (
|
||||
net['type'] != self.VMNetType.DPDK.value
|
||||
and net['mtu'] is not None
|
||||
@@ -191,6 +198,28 @@ class decort_kvmvm(DecortController):
|
||||
' (remove parameter "mtu" for network'
|
||||
f' {net["type"]} with ID {net["id"]}).'
|
||||
)
|
||||
# MAC address
|
||||
if net['mac'] is not None:
|
||||
if net['type'] == self.VMNetType.EMPTY.value:
|
||||
check_error = True
|
||||
self.message(
|
||||
'Check for parameter "networks.mac" failed: '
|
||||
'MAC-address cannot be specified for an '
|
||||
'EMPTY type network.'
|
||||
)
|
||||
|
||||
mac_validation_result = re.match(
|
||||
'[0-9a-f]{2}([:]?)[0-9a-f]{2}(\\1[0-9a-f]{2}){4}$',
|
||||
net['mac'].lower(),
|
||||
)
|
||||
if not mac_validation_result:
|
||||
check_error = True
|
||||
self.message(
|
||||
'Check for parameter "networks.mac" failed: '
|
||||
f'MAC-address for network ID {net["id"]} must be '
|
||||
'specified in quotes and in the format '
|
||||
'"XX:XX:XX:XX:XX:XX".'
|
||||
)
|
||||
|
||||
aparam_custom_fields = self.aparams['custom_fields']
|
||||
if aparam_custom_fields is not None:
|
||||
@@ -215,6 +244,22 @@ class decort_kvmvm(DecortController):
|
||||
'the list must contain only unique elements.'
|
||||
)
|
||||
|
||||
aparam_state = self.aparams['state']
|
||||
new_state = None
|
||||
match aparam_state:
|
||||
case 'halted' | 'poweredoff':
|
||||
new_state = 'stopped'
|
||||
case 'poweredon':
|
||||
new_state = 'started'
|
||||
|
||||
if new_state:
|
||||
self.message(
|
||||
msg=f'"{aparam_state}" state is deprecated and might be '
|
||||
f'removed in newer versions. '
|
||||
f'Please use "{new_state}" instead.',
|
||||
warning=True,
|
||||
)
|
||||
|
||||
if check_error:
|
||||
self.exit(fail=True)
|
||||
|
||||
@@ -265,12 +310,40 @@ class decort_kvmvm(DecortController):
|
||||
# each of the following calls will abort if argument is missing
|
||||
self.check_amodule_argument('cpu')
|
||||
self.check_amodule_argument('ram')
|
||||
|
||||
aparam_boot = self.aparams['boot']
|
||||
validated_bdisk_size = 0
|
||||
if self.amodule.params['boot'] is not None:
|
||||
boot_mode = 'bios'
|
||||
loader_type = 'unknown'
|
||||
if aparam_boot is not None:
|
||||
validated_bdisk_size = self.amodule.params['boot'].get(
|
||||
'disk_size', 0
|
||||
)
|
||||
|
||||
if aparam_boot['mode'] is None:
|
||||
self.message(
|
||||
msg=self.MESSAGES.default_value_used(
|
||||
param_name='boot.mode',
|
||||
default_value=boot_mode
|
||||
),
|
||||
warning=True,
|
||||
)
|
||||
else:
|
||||
boot_mode = aparam_boot['mode']
|
||||
|
||||
if aparam_boot['loader_type'] is None:
|
||||
self.message(
|
||||
msg=self.MESSAGES.default_value_used(
|
||||
param_name='boot.loader_type',
|
||||
default_value=loader_type
|
||||
),
|
||||
warning=True,
|
||||
)
|
||||
|
||||
else:
|
||||
loader_type = aparam_boot['loader_type']
|
||||
|
||||
|
||||
image_id, image_facts = None, None
|
||||
if self.aparam_image:
|
||||
# either image_name or image_id must be present
|
||||
@@ -308,7 +381,7 @@ class decort_kvmvm(DecortController):
|
||||
# Once this "feature" is fixed, make sure VM is created according to the actual desired state
|
||||
#
|
||||
start_compute = False # change this once a workaround for the aforementioned libvirt "feature" is implemented
|
||||
if self.amodule.params['state'] in ('halted', 'poweredoff'):
|
||||
if self.amodule.params['state'] in ('halted', 'poweredoff', 'stopped'):
|
||||
start_compute = False
|
||||
|
||||
if self.amodule.params['ssh_key'] and self.amodule.params['ssh_key_user'] and not self.amodule.params['ci_user_data']:
|
||||
@@ -334,7 +407,6 @@ class decort_kvmvm(DecortController):
|
||||
if numa_affinity is None:
|
||||
numa_affinity = 'none'
|
||||
|
||||
|
||||
chipset = self.amodule.params['chipset']
|
||||
if chipset is None:
|
||||
chipset = 'i440fx'
|
||||
@@ -343,6 +415,28 @@ class decort_kvmvm(DecortController):
|
||||
f'default value "{chipset}" will be used.',
|
||||
warning=True,
|
||||
)
|
||||
|
||||
network_interface_naming = self.aparams['network_interface_naming']
|
||||
if network_interface_naming is None:
|
||||
network_interface_naming = 'ens'
|
||||
self.message(
|
||||
msg=self.MESSAGES.default_value_used(
|
||||
param_name='network_interface_naming',
|
||||
default_value=network_interface_naming
|
||||
),
|
||||
warning=True,
|
||||
)
|
||||
|
||||
hot_resize = self.aparams['hot_resize']
|
||||
if hot_resize is None:
|
||||
hot_resize = False
|
||||
self.message(
|
||||
msg=self.MESSAGES.default_value_used(
|
||||
param_name='hot_resize',
|
||||
default_value=hot_resize
|
||||
),
|
||||
warning=True,
|
||||
)
|
||||
# if we get through here, all parameters required to create new Compute instance should be at hand
|
||||
|
||||
# NOTE: KVM VM is created in HALTED state and must be explicitly started
|
||||
@@ -360,7 +454,11 @@ class decort_kvmvm(DecortController):
|
||||
cpu_pin=cpu_pin,
|
||||
hp_backed=hp_backed,
|
||||
numa_affinity=numa_affinity,
|
||||
preferred_cpu_cores=self.amodule.params['preferred_cpu_cores'],)
|
||||
preferred_cpu_cores=self.amodule.params['preferred_cpu_cores'],
|
||||
boot_mode=boot_mode,
|
||||
boot_loader_type=loader_type,
|
||||
network_interface_naming=network_interface_naming,
|
||||
hot_resize=hot_resize,)
|
||||
self.comp_should_exist = True
|
||||
|
||||
# Originally we would have had to re-read comp_info after VM was provisioned
|
||||
@@ -407,7 +505,7 @@ class decort_kvmvm(DecortController):
|
||||
label=self.amodule.params['affinity_label'],)
|
||||
# NOTE: see NOTE above regarding libvirt "feature" and new VMs created in HALTED state
|
||||
if self.aparam_image:
|
||||
if self.amodule.params['state'] not in ('halted', 'poweredoff'):
|
||||
if self.amodule.params['state'] in ('poweredon', 'started'):
|
||||
self.compute_powerstate(self.comp_info, 'started')
|
||||
|
||||
if self.aparams['custom_fields'] is None:
|
||||
@@ -553,14 +651,54 @@ class decort_kvmvm(DecortController):
|
||||
'description': 'desc',
|
||||
'auto_start': 'autoStart',
|
||||
'preferred_cpu_cores': 'preferredCpu',
|
||||
'boot.mode': 'bootType',
|
||||
'boot.loader_type': 'loaderType',
|
||||
'network_interface_naming': 'networkInterfaceNaming',
|
||||
'hot_resize': 'hotResize',
|
||||
}
|
||||
|
||||
def get_nested_value(
|
||||
d: dict,
|
||||
keys: Sequence[str],
|
||||
default: DefaultT | None = None,
|
||||
) -> Any | DefaultT:
|
||||
if not keys:
|
||||
raise ValueError
|
||||
|
||||
key = keys[0]
|
||||
if key not in d:
|
||||
return default
|
||||
value = d[key]
|
||||
|
||||
if len(keys) > 1:
|
||||
if isinstance(value, dict):
|
||||
nested_d = value
|
||||
return get_nested_value(
|
||||
d=nested_d,
|
||||
keys=keys[1:],
|
||||
default=default,
|
||||
)
|
||||
if value is None:
|
||||
return default
|
||||
raise ValueError(
|
||||
f'The key {key} found, but its value is not a dictionary.'
|
||||
)
|
||||
return value
|
||||
|
||||
for aparam_name, comp_field_name in params_to_check.items():
|
||||
aparam_value = self.amodule.params[aparam_name]
|
||||
if (
|
||||
aparam_value is not None
|
||||
and aparam_value != self.comp_info[comp_field_name]
|
||||
):
|
||||
result_args[aparam_name] = aparam_value
|
||||
aparam_value = get_nested_value(
|
||||
d=self.aparams,
|
||||
keys=aparam_name.split('.'),
|
||||
)
|
||||
comp_value = get_nested_value(
|
||||
d=self.comp_info,
|
||||
keys=comp_field_name.split('.'),
|
||||
)
|
||||
|
||||
if aparam_value is not None and aparam_value != comp_value:
|
||||
result_args[aparam_name.replace('.', '_')] = (
|
||||
aparam_value
|
||||
)
|
||||
|
||||
return result_args
|
||||
|
||||
@@ -679,15 +817,49 @@ class decort_kvmvm(DecortController):
|
||||
ret_dict['clones'] = self.comp_info['clones']
|
||||
ret_dict['clone_reference'] = self.comp_info['cloneReference']
|
||||
|
||||
ret_dict['boot_mode'] = self.comp_info['bootType']
|
||||
ret_dict['boot_loader_type'] = self.comp_info['loaderType']
|
||||
ret_dict['network_interface_naming'] = self.comp_info[
|
||||
'networkInterfaceNaming'
|
||||
]
|
||||
ret_dict['hot_resize'] = self.comp_info['hotResize']
|
||||
|
||||
ret_dict['pinned_to_stack'] = self.comp_info['pinnedToStack']
|
||||
|
||||
ret_dict['affinity_label'] = self.comp_info['affinityLabel']
|
||||
ret_dict['affinity_rules'] = self.comp_info['affinityRules']
|
||||
ret_dict['anti_affinity_rules'] = self.comp_info['antiAffinityRules']
|
||||
|
||||
return ret_dict
|
||||
|
||||
def check_amodule_args_for_create(self):
|
||||
check_errors = False
|
||||
# Check for unacceptable parameters for a blank Compute
|
||||
if (
|
||||
self.aparams['image_id'] is not None
|
||||
or self.aparams['image_name'] is not None
|
||||
):
|
||||
self.aparam_image = True
|
||||
for param in (
|
||||
'network_interface_naming',
|
||||
'hot_resize',
|
||||
):
|
||||
if self.aparams[param] is not None:
|
||||
check_errors = True
|
||||
self.message(
|
||||
f'Check for parameter "{param}" failed: '
|
||||
'parameter can be specified only for a blank VM.'
|
||||
)
|
||||
|
||||
if self.aparams['boot'] is not None:
|
||||
for param in ('mode', 'loader_type'):
|
||||
if self.aparams['boot'][param] is not None:
|
||||
check_errors = True
|
||||
self.message(
|
||||
f'Check for parameter "boot.{param}" failed: '
|
||||
'parameter can be specified only for a blank VM.'
|
||||
)
|
||||
|
||||
else:
|
||||
self.aparam_image = False
|
||||
if (
|
||||
@@ -696,14 +868,15 @@ class decort_kvmvm(DecortController):
|
||||
'present',
|
||||
'poweredoff',
|
||||
'halted',
|
||||
'stopped',
|
||||
)
|
||||
):
|
||||
check_errors = True
|
||||
self.message(
|
||||
'Check for parameter "state" failed: '
|
||||
'state for a blank Compute must be either '
|
||||
'"present", "poweredoff" or "halted".'
|
||||
'"present" or "stopped".'
|
||||
)
|
||||
self.exit(fail=True)
|
||||
|
||||
for parameter in (
|
||||
'ssh_key',
|
||||
@@ -711,38 +884,41 @@ class decort_kvmvm(DecortController):
|
||||
'ci_user_data',
|
||||
):
|
||||
if self.aparams[parameter] is not None:
|
||||
check_errors = True
|
||||
self.message(
|
||||
f'Check for parameter "{parameter}" failed: '
|
||||
f'"image_id" or "image_name" must be specified '
|
||||
f'to set {parameter}.'
|
||||
)
|
||||
self.exit(fail=True)
|
||||
|
||||
if (
|
||||
self.aparams['sep_id'] is not None
|
||||
and self.aparams['boot'] is None
|
||||
and self.aparams['boot']['disk_size'] is None
|
||||
):
|
||||
check_errors = True
|
||||
self.message(
|
||||
'Check for parameter "sep_id" failed: '
|
||||
'"image_id" or "image_name" or "boot.disk_size" '
|
||||
'must be specified to set sep_id.'
|
||||
)
|
||||
self.exit(fail=True)
|
||||
|
||||
if self.aparams['rollback_to'] is not None:
|
||||
check_errors = True
|
||||
self.message(
|
||||
'Check for parameter "rollback_to" failed: '
|
||||
'rollback_to can be specified only for existing compute.'
|
||||
)
|
||||
self.exit(fail=True)
|
||||
|
||||
if self.aparam_networks_has_dpdk and not self.aparams['hp_backed']:
|
||||
check_errors = True
|
||||
self.message(
|
||||
'Check for parameter "networks" failed:'
|
||||
' hp_backed must be set to True to connect a compute'
|
||||
' to a DPDK network.'
|
||||
)
|
||||
|
||||
if check_errors:
|
||||
self.exit(fail=True)
|
||||
|
||||
@property
|
||||
@@ -769,6 +945,21 @@ class decort_kvmvm(DecortController):
|
||||
disk_size=dict(
|
||||
type='int',
|
||||
),
|
||||
mode=dict(
|
||||
type='str',
|
||||
choices=[
|
||||
'bios',
|
||||
'uefi',
|
||||
],
|
||||
),
|
||||
loader_type=dict(
|
||||
type='str',
|
||||
choices=[
|
||||
'windows',
|
||||
'linux',
|
||||
'unknown',
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
sep_id=dict(
|
||||
@@ -840,6 +1031,9 @@ class decort_kvmvm(DecortController):
|
||||
mtu=dict(
|
||||
type='int',
|
||||
),
|
||||
mac=dict(
|
||||
type='str',
|
||||
),
|
||||
),
|
||||
required_if=[
|
||||
('type', 'VINS', ('id',)),
|
||||
@@ -892,6 +1086,8 @@ class decort_kvmvm(DecortController):
|
||||
'poweredoff',
|
||||
'halted',
|
||||
'poweredon',
|
||||
'stopped',
|
||||
'started',
|
||||
'present',
|
||||
],
|
||||
),
|
||||
@@ -974,6 +1170,16 @@ class decort_kvmvm(DecortController):
|
||||
),
|
||||
),
|
||||
),
|
||||
network_interface_naming=dict(
|
||||
type='str',
|
||||
choices=[
|
||||
'ens',
|
||||
'eth',
|
||||
],
|
||||
),
|
||||
hot_resize=dict(
|
||||
type='bool',
|
||||
),
|
||||
),
|
||||
supports_check_mode=True,
|
||||
required_one_of=[
|
||||
@@ -1069,12 +1275,14 @@ class decort_kvmvm(DecortController):
|
||||
|
||||
if (
|
||||
not comp_info['imageId']
|
||||
and self.amodule.params['state'] in ('poweredon', 'paused')
|
||||
and self.amodule.params['state'] in (
|
||||
'poweredon', 'paused', 'started',
|
||||
)
|
||||
):
|
||||
check_errors = True
|
||||
self.message(
|
||||
'Check for parameter "state" failed: '
|
||||
'state for a blank Compute can not be "poweredon" or "paused".'
|
||||
'state for a blank Compute can not be "started" or "paused".'
|
||||
)
|
||||
|
||||
is_vm_stopped_or_will_be_stopped = (
|
||||
@@ -1083,14 +1291,14 @@ class decort_kvmvm(DecortController):
|
||||
and (
|
||||
self.amodule.params['state'] is None
|
||||
or self.amodule.params['state'] in (
|
||||
'halted', 'poweredoff', 'present',
|
||||
'halted', 'poweredoff', 'present', 'stopped',
|
||||
)
|
||||
)
|
||||
)
|
||||
or (
|
||||
comp_info['techStatus'] != 'STOPPED'
|
||||
and self.amodule.params['state'] in (
|
||||
'halted', 'poweredoff',
|
||||
'halted', 'poweredoff', 'stopped',
|
||||
)
|
||||
)
|
||||
)
|
||||
@@ -1120,6 +1328,7 @@ class decort_kvmvm(DecortController):
|
||||
'cpu_pin': 'cpupin',
|
||||
'hp_backed': 'hpBacked',
|
||||
'numa_affinity': 'numaAffinity',
|
||||
'hot_resize': 'hotResize',
|
||||
}
|
||||
for param_name, comp_field_name in params_to_check.items():
|
||||
if (
|
||||
@@ -1159,13 +1368,13 @@ class decort_kvmvm(DecortController):
|
||||
and (
|
||||
self.amodule.params['state'] is None
|
||||
or self.amodule.params['state'] in (
|
||||
'poweredon', 'present',
|
||||
'poweredon', 'present', 'started',
|
||||
)
|
||||
)
|
||||
)
|
||||
or (
|
||||
comp_info['techStatus'] != 'STARTED'
|
||||
and self.amodule.params['state'] == 'poweredon'
|
||||
and self.amodule.params['state'] in ('poweredon', 'started')
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1179,30 +1388,55 @@ class decort_kvmvm(DecortController):
|
||||
|
||||
aparam_disks = self.aparams['disks']
|
||||
if aparam_disks is not None:
|
||||
if self.comp_info['snapSets']:
|
||||
match aparam_disks['mode']:
|
||||
case 'detach' | 'delete':
|
||||
check_errors = True
|
||||
self.message(
|
||||
f'Check for parameter "disks" failed: '
|
||||
f'cannot {aparam_disks["mode"]} disks for '
|
||||
f'Compute ID {self.comp_id} while snapshots '
|
||||
f'exist in compute.'
|
||||
)
|
||||
case 'match':
|
||||
comp_disk_ids = {
|
||||
disk['id'] for disk in self.comp_info['disks']
|
||||
}
|
||||
disks = set(aparam_disks['ids'])
|
||||
disks_to_detach = comp_disk_ids - disks
|
||||
if disks_to_detach:
|
||||
check_errors = True
|
||||
self.message(
|
||||
f'Check for parameter "disks" failed: '
|
||||
f'disks {disks_to_detach} cannot be detached '
|
||||
f'from Compute ID {self.comp_id} while '
|
||||
f'snapshots exist in compute.'
|
||||
)
|
||||
aparam_disks_ids = aparam_disks['ids']
|
||||
comp_boot_disk_id = None
|
||||
for comp_disk in self.comp_info['disks']:
|
||||
if comp_disk['type'] == 'B':
|
||||
comp_boot_disk_id = comp_disk['id']
|
||||
break
|
||||
disks_to_detach = []
|
||||
match aparam_disks['mode']:
|
||||
case 'detach' | 'delete':
|
||||
disks_to_detach = aparam_disks_ids
|
||||
case 'match':
|
||||
comp_disk_ids = {
|
||||
disk['id'] for disk in self.comp_info['disks']
|
||||
}
|
||||
disks = set(aparam_disks_ids)
|
||||
disks_to_detach = comp_disk_ids - disks
|
||||
if (
|
||||
comp_boot_disk_id is not None
|
||||
and comp_boot_disk_id in disks_to_detach
|
||||
and not is_vm_stopped_or_will_be_stopped
|
||||
):
|
||||
check_errors = True
|
||||
self.message(
|
||||
f'Check for parameter "disks" failed: '
|
||||
f'VM ID {comp_id} must be stopped to detach '
|
||||
f'boot disk ID {comp_boot_disk_id}.'
|
||||
)
|
||||
if self.comp_info['snapSets'] and disks_to_detach:
|
||||
check_errors = True
|
||||
self.message(
|
||||
f'Check for parameter "disks" failed: '
|
||||
f'cannot detach disks {disks_to_detach} from '
|
||||
f'Compute ID {self.comp_id} while snapshots exist.'
|
||||
)
|
||||
|
||||
if (
|
||||
(
|
||||
self.aparams['cpu'] is not None
|
||||
and self.aparams['cpu'] != comp_info['cpus']
|
||||
) or (
|
||||
self.aparams['ram'] is not None
|
||||
and self.aparams['ram'] != comp_info['ram']
|
||||
)
|
||||
) and not (self.aparams['hot_resize'] or comp_info['hotResize']):
|
||||
check_errors = True
|
||||
self.message(
|
||||
'Check for parameters "cpu" and "ram" failed: '
|
||||
'Hot resize must be enabled to change CPU or RAM.'
|
||||
)
|
||||
|
||||
if check_errors:
|
||||
self.exit(fail=True)
|
||||
@@ -1323,7 +1557,8 @@ def main():
|
||||
subj.destroy()
|
||||
else:
|
||||
if amodule.params['state'] in (
|
||||
'paused', 'poweredon', 'poweredoff', 'halted'
|
||||
'paused', 'poweredon', 'poweredoff',
|
||||
'halted', 'started', 'stopped',
|
||||
):
|
||||
subj.compute_powerstate(
|
||||
comp_facts=subj.comp_info,
|
||||
@@ -1331,7 +1566,7 @@ def main():
|
||||
)
|
||||
subj.modify(arg_wait_cycles=7)
|
||||
elif subj.comp_info['status'] == "DELETED":
|
||||
if amodule.params['state'] in ('present', 'poweredon'):
|
||||
if amodule.params['state'] in ('present', 'poweredon', 'started'):
|
||||
# TODO - check if restore API returns VM ID (similarly to VM create API)
|
||||
subj.compute_restore(comp_id=subj.comp_id)
|
||||
# TODO - do we need updated comp_info to manage port forwards and size after VM is restored?
|
||||
@@ -1345,10 +1580,15 @@ def main():
|
||||
# subj.nop()
|
||||
# subj.comp_should_exist = False
|
||||
subj.destroy()
|
||||
elif amodule.params['state'] in ('paused', 'poweredoff', 'halted'):
|
||||
elif amodule.params['state'] in (
|
||||
'paused', 'poweredoff', 'halted', 'stopped'
|
||||
):
|
||||
subj.error()
|
||||
elif subj.comp_info['status'] == "DESTROYED":
|
||||
if amodule.params['state'] in ('present', 'poweredon', 'poweredoff', 'halted'):
|
||||
if amodule.params['state'] in (
|
||||
'present', 'poweredon', 'poweredoff',
|
||||
'halted', 'started', 'stopped',
|
||||
):
|
||||
subj.create() # this call will also handle data disk & network connection
|
||||
elif amodule.params['state'] == 'absent':
|
||||
subj.nop()
|
||||
@@ -1363,7 +1603,10 @@ def main():
|
||||
# If requested state is 'absent' - nothing to do
|
||||
if state == 'absent':
|
||||
subj.nop()
|
||||
elif state in ('present', 'poweredon', 'poweredoff', 'halted'):
|
||||
elif state in (
|
||||
'present', 'poweredon', 'poweredoff',
|
||||
'halted', 'started', 'stopped',
|
||||
):
|
||||
subj.create() # this call will also handle data disk & network connection
|
||||
elif state == 'paused':
|
||||
subj.error()
|
||||
|
||||
Reference in New Issue
Block a user