Merge "software deploy show implementation"
This commit is contained in:
commit
018eea2d51
|
@ -308,7 +308,7 @@ def release_is_available_req(args):
|
||||||
print("An internal error has occurred. Please check /var/log/software.log for details")
|
print("An internal error has occurred. Please check /var/log/software.log for details")
|
||||||
else:
|
else:
|
||||||
print("Error: %s has occurred. %s" % (req.status_code, req.reason))
|
print("Error: %s has occurred. %s" % (req.status_code, req.reason))
|
||||||
|
|
||||||
return rc
|
return rc
|
||||||
|
|
||||||
|
|
||||||
|
@ -930,9 +930,40 @@ def deploy_complete_req(args):
|
||||||
return check_rc(req)
|
return check_rc(req)
|
||||||
|
|
||||||
|
|
||||||
def deploy_list_req(args):
|
def deploy_show_req(args):
|
||||||
print(args.deployment)
|
url = "http://%s/software/deploy_show" % api_addr
|
||||||
return 1
|
headers = {}
|
||||||
|
append_auth_token_if_required(headers)
|
||||||
|
req = requests.get(url, headers=headers)
|
||||||
|
|
||||||
|
if req.status_code >= 500:
|
||||||
|
print("An internal error has occurred. Please check /var/log/software.log for details")
|
||||||
|
return 1
|
||||||
|
elif req.status_code >= 400:
|
||||||
|
print("Respond code %d. Error: %s" % (req.status_code, req.reason))
|
||||||
|
return 1
|
||||||
|
|
||||||
|
data = json.loads(req.text)
|
||||||
|
if not data:
|
||||||
|
print("No deploy in progress.\n")
|
||||||
|
else:
|
||||||
|
data["reboot_required"] = "Yes" if data.get("reboot_required") else "No"
|
||||||
|
data_list = [[k, v] for k, v in data.items()]
|
||||||
|
transposed_data_list = list(zip(*data_list))
|
||||||
|
|
||||||
|
transposed_data_list[0] = [s.title().replace('_', ' ') for s in transposed_data_list[0]]
|
||||||
|
# Find the longest header string in each column
|
||||||
|
header_lengths = [len(str(x)) for x in transposed_data_list[0]]
|
||||||
|
# Find the longest content string in each column
|
||||||
|
content_lengths = [len(str(x)) for x in transposed_data_list[1]]
|
||||||
|
# Find the max of the two for each column
|
||||||
|
col_lengths = [(x if x > y else y) for x, y in zip(header_lengths, content_lengths)]
|
||||||
|
|
||||||
|
print(' '.join(f"{x.center(col_lengths[i])}" for i, x in enumerate(transposed_data_list[0])))
|
||||||
|
print(' '.join('=' * length for length in col_lengths))
|
||||||
|
print(' '.join(f"{x.center(col_lengths[i])}" for i, x in enumerate(transposed_data_list[1])))
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
def deploy_host_req(args):
|
def deploy_host_req(args):
|
||||||
|
@ -1161,8 +1192,8 @@ def register_deploy_commands(commands):
|
||||||
- activate
|
- activate
|
||||||
- complete
|
- complete
|
||||||
non root/sudo users can run:
|
non root/sudo users can run:
|
||||||
- list
|
|
||||||
- query-hosts
|
- query-hosts
|
||||||
|
- show
|
||||||
Deploy commands are region_restricted, which means
|
Deploy commands are region_restricted, which means
|
||||||
that they are not permitted to be run in DC
|
that they are not permitted to be run in DC
|
||||||
"""
|
"""
|
||||||
|
@ -1249,13 +1280,13 @@ def register_deploy_commands(commands):
|
||||||
cmd.add_argument('deployment',
|
cmd.add_argument('deployment',
|
||||||
help='Deployment ID to complete')
|
help='Deployment ID to complete')
|
||||||
|
|
||||||
# --- software deploy list ---------------------------
|
# --- software deploy show ---------------------------
|
||||||
cmd = sub_cmds.add_parser(
|
cmd = sub_cmds.add_parser(
|
||||||
'list',
|
'show',
|
||||||
help='List the software deployments and their states'
|
help='Show the software deployments states'
|
||||||
)
|
)
|
||||||
cmd.set_defaults(cmd='list')
|
cmd.set_defaults(cmd='show')
|
||||||
cmd.set_defaults(func=deploy_list_req)
|
cmd.set_defaults(func=deploy_show_req)
|
||||||
cmd.set_defaults(restricted=False) # can run non root
|
cmd.set_defaults(restricted=False) # can run non root
|
||||||
# --deployment is an optional argument
|
# --deployment is an optional argument
|
||||||
cmd.add_argument('--deployment',
|
cmd.add_argument('--deployment',
|
||||||
|
|
|
@ -127,6 +127,16 @@ class SoftwareAPIController(object):
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
@expose('json')
|
||||||
|
@expose('query.xml', content_type='application/xml')
|
||||||
|
def deploy_show(self):
|
||||||
|
try:
|
||||||
|
result = sc.software_deploy_show_api()
|
||||||
|
except SoftwareError as e:
|
||||||
|
return dict(error="Error: %s" % str(e))
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
@expose('json')
|
@expose('json')
|
||||||
@expose('query.xml', content_type='application/xml')
|
@expose('query.xml', content_type='application/xml')
|
||||||
def install_local(self):
|
def install_local(self):
|
||||||
|
|
|
@ -4,7 +4,7 @@ Copyright (c) 2023 Wind River Systems, Inc.
|
||||||
SPDX-License-Identifier: Apache-2.0
|
SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
from enum import Enum
|
||||||
import os
|
import os
|
||||||
try:
|
try:
|
||||||
# The tsconfig module is only available at runtime
|
# The tsconfig module is only available at runtime
|
||||||
|
@ -114,3 +114,21 @@ SOFTWARE_JSON_FILE = "/opt/software/software.json"
|
||||||
|
|
||||||
WORKER_SUMMARY_DIR = "%s/summary" % SOFTWARE_STORAGE_DIR
|
WORKER_SUMMARY_DIR = "%s/summary" % SOFTWARE_STORAGE_DIR
|
||||||
WORKER_DATETIME_FORMAT = "%Y%m%dT%H%M%S%f"
|
WORKER_DATETIME_FORMAT = "%Y%m%dT%H%M%S%f"
|
||||||
|
UNKNOWN_SOFTWARE_VERSION = "0.0.0"
|
||||||
|
|
||||||
|
|
||||||
|
class DEPLOY_STATES(Enum):
|
||||||
|
ACTIVATING = 'activating'
|
||||||
|
ACTIVATED = 'activated'
|
||||||
|
ACTIVATION_FAILED = 'activation-failed'
|
||||||
|
DATA_MIGRATION_FAILED = 'data-migration-failed'
|
||||||
|
DATA_MIGRATION = 'data-migration'
|
||||||
|
DEPLOYING = 'deploying'
|
||||||
|
DEPLOYED = 'deployed'
|
||||||
|
PRESTAGING = 'prestaging'
|
||||||
|
PRESTAGED = 'prestaged'
|
||||||
|
PRESTAGING_FAILED = 'prestaging-failed'
|
||||||
|
UPGRADE_CONTROLLERS = 'upgrade-controllers'
|
||||||
|
UPGRADE_CONTROLLER_FAILED = 'upgrade-controller-failed'
|
||||||
|
UPGRADE_HOSTS = 'upgrade-hosts'
|
||||||
|
UNKNOWN = 'unknown'
|
||||||
|
|
|
@ -1,10 +1,13 @@
|
||||||
from software.software_entities import DeployHandler
|
from software.software_entities import DeployHandler
|
||||||
from software.software_entities import DeployHostHandler
|
from software.software_entities import DeployHostHandler
|
||||||
|
from software.constants import DEPLOY_STATES
|
||||||
|
|
||||||
|
|
||||||
def get_instance():
|
def get_instance():
|
||||||
"""Return a Software API instance."""
|
"""Return a Software API instance."""
|
||||||
return SoftwareAPI()
|
return SoftwareAPI()
|
||||||
|
|
||||||
|
|
||||||
class SoftwareAPI:
|
class SoftwareAPI:
|
||||||
_instance = None
|
_instance = None
|
||||||
|
|
||||||
|
@ -17,20 +20,17 @@ class SoftwareAPI:
|
||||||
self.deploy_handler = DeployHandler()
|
self.deploy_handler = DeployHandler()
|
||||||
self.deploy_host_handler = DeployHostHandler()
|
self.deploy_host_handler = DeployHostHandler()
|
||||||
|
|
||||||
def create_deploy(self, from_release, to_release, reboot_required):
|
def create_deploy(self, from_release, to_release, reboot_required: bool):
|
||||||
self.deploy_handler.create(from_release, to_release, reboot_required)
|
self.deploy_handler.create(from_release, to_release, reboot_required)
|
||||||
|
|
||||||
def get_deploy(self, from_release, to_release):
|
def get_deploy(self):
|
||||||
return self.deploy_handler.query(from_release, to_release)
|
return self.deploy_handler.query()
|
||||||
|
|
||||||
def get_deploy_all(self):
|
def update_deploy(self, state: DEPLOY_STATES):
|
||||||
return self.deploy_handler.query_all()
|
self.deploy_handler.update(state)
|
||||||
|
|
||||||
def update_deploy(self, from_release, to_release, reboot_required, state):
|
def delete_deploy(self):
|
||||||
self.deploy_handler.update(from_release, to_release, reboot_required, state)
|
self.deploy_handler.delete()
|
||||||
|
|
||||||
def delete_deploy(self, from_release, to_release):
|
|
||||||
self.deploy_handler.delete(from_release, to_release)
|
|
||||||
|
|
||||||
def create_deploy_host(self, hostname, software_release, target_release):
|
def create_deploy_host(self, hostname, software_release, target_release):
|
||||||
self.deploy_host_handler.create(hostname, software_release, target_release)
|
self.deploy_host_handler.create(hostname, software_release, target_release)
|
||||||
|
|
|
@ -59,6 +59,8 @@ from software.release_verify import verify_files
|
||||||
import software.config as cfg
|
import software.config as cfg
|
||||||
import software.utils as utils
|
import software.utils as utils
|
||||||
|
|
||||||
|
from software.db.api import get_instance
|
||||||
|
|
||||||
import software.messages as messages
|
import software.messages as messages
|
||||||
import software.constants as constants
|
import software.constants as constants
|
||||||
|
|
||||||
|
@ -603,6 +605,8 @@ class PatchController(PatchService):
|
||||||
self.hosts = {}
|
self.hosts = {}
|
||||||
self.controller_neighbours = {}
|
self.controller_neighbours = {}
|
||||||
|
|
||||||
|
self.db_api_instance = get_instance()
|
||||||
|
|
||||||
# interim_state is used to track hosts that have not responded
|
# interim_state is used to track hosts that have not responded
|
||||||
# with fresh queries since a patch was applied or removed, on
|
# with fresh queries since a patch was applied or removed, on
|
||||||
# a per-patch basis. This allows the patch controller to move
|
# a per-patch basis. This allows the patch controller to move
|
||||||
|
@ -2258,6 +2262,10 @@ class PatchController(PatchService):
|
||||||
|
|
||||||
return dict(info=msg_info, warning=msg_warning, error=msg_error)
|
return dict(info=msg_info, warning=msg_warning, error=msg_error)
|
||||||
|
|
||||||
|
def software_deploy_show_api(self):
|
||||||
|
# Retrieve deploy state from db
|
||||||
|
return self.db_api_instance.get_deploy()
|
||||||
|
|
||||||
def software_deploy_host_api(self, host_ip, force, async_req=False):
|
def software_deploy_host_api(self, host_ip, force, async_req=False):
|
||||||
msg_info = ""
|
msg_info = ""
|
||||||
msg_warning = ""
|
msg_warning = ""
|
||||||
|
|
|
@ -11,8 +11,15 @@ from enum import Enum
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
from software import constants
|
from software import constants
|
||||||
from software.exceptions import DeployDoNotExist, DeployAlreadyExist
|
from software.exceptions import DeployDoNotExist
|
||||||
from software.utils import check_instances, check_state, save_to_json_file, get_software_filesystem_data
|
from software.exceptions import DeployAlreadyExist
|
||||||
|
from software.utils import check_instances
|
||||||
|
from software.utils import check_state
|
||||||
|
from software.utils import save_to_json_file
|
||||||
|
from software.utils import get_software_filesystem_data
|
||||||
|
from software.utils import validate_versions
|
||||||
|
|
||||||
|
from software.constants import DEPLOY_STATES
|
||||||
|
|
||||||
LOG = logging.getLogger('main_logger')
|
LOG = logging.getLogger('main_logger')
|
||||||
|
|
||||||
|
@ -122,17 +129,11 @@ class CompatibleRelease(ABC):
|
||||||
|
|
||||||
|
|
||||||
class Deploy(ABC):
|
class Deploy(ABC):
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.states = Enum('States', 'activate-failed activated '
|
pass
|
||||||
'data-migration-failed data-migration '
|
|
||||||
'activating prestaged prestaging '
|
|
||||||
'prestaging-failed '
|
|
||||||
'upgrade-controller-failed upgrade-controllers '
|
|
||||||
'upgrade-hosts')
|
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def create(self, from_release: str, to_release: str, reboot_required:bool, state: str):
|
def create(self, from_release: str, to_release: str, reboot_required: bool, state: DEPLOY_STATES):
|
||||||
"""
|
"""
|
||||||
Create a new deployment entry.
|
Create a new deployment entry.
|
||||||
|
|
||||||
|
@ -142,51 +143,32 @@ class Deploy(ABC):
|
||||||
:param state: The state of the deployment.
|
:param state: The state of the deployment.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
instances = [from_release, to_release]
|
validate_versions([from_release, to_release])
|
||||||
if state:
|
|
||||||
check_state(state, self.states)
|
|
||||||
instances.append(state)
|
|
||||||
check_instances([reboot_required], bool)
|
check_instances([reboot_required], bool)
|
||||||
check_instances(instances, str)
|
check_instances([state], DEPLOY_STATES)
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def query(self, from_release: str, to_release: str):
|
def query(self):
|
||||||
"""
|
"""
|
||||||
Get deployments based on source and target release versions.
|
Get deployments based on source and target release versions.
|
||||||
|
|
||||||
:param from_release: The source release version.
|
|
||||||
:param to_release: The target release version.
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
check_instances([from_release, to_release], str)
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def update(self, from_release: str, to_release: str, reboot_required:bool, state: str):
|
def update(self, new_state: DEPLOY_STATES):
|
||||||
"""
|
"""
|
||||||
Update a deployment entry.
|
Update a deployment entry.
|
||||||
|
|
||||||
:param from_release: The source release version.
|
|
||||||
:param to_release: The target release version.
|
|
||||||
:param reboot_required: If is required to do host reboot.
|
|
||||||
:param state: The state of the deployment.
|
:param state: The state of the deployment.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
check_instances([from_release, to_release, state], str)
|
check_instances([new_state], DEPLOY_STATES)
|
||||||
check_instances([reboot_required], bool)
|
|
||||||
check_state(state, self.states)
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def delete(self, from_release: str, to_release: str):
|
def delete(self):
|
||||||
"""
|
"""
|
||||||
Delete a deployment entry based on source and target release versions.
|
Delete a deployment entry based on source and target release versions.
|
||||||
|
|
||||||
:param from_release: The source release version.
|
|
||||||
:param to_release: The target release version.
|
|
||||||
"""
|
"""
|
||||||
check_instances([from_release, to_release], str)
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@ -273,67 +255,74 @@ class DeployHosts(ABC):
|
||||||
check_instances([hostname, software_release, target_release], str)
|
check_instances([hostname, software_release, target_release], str)
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class DeployHandler(Deploy):
|
class DeployHandler(Deploy):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.data = get_software_filesystem_data()
|
self.data = get_software_filesystem_data()
|
||||||
|
|
||||||
def create(self, from_release, to_release, reboot_required, state=None):
|
def create(self, from_release, to_release, reboot_required, state=DEPLOY_STATES.DEPLOYING):
|
||||||
|
"""
|
||||||
|
Create a new deploy with given from and to release version
|
||||||
|
:param from_release: The source release version.
|
||||||
|
:param to_release: The target release version.
|
||||||
|
:param reboot_required: If is required to do host reboot.
|
||||||
|
:param state: The state of the deployment.
|
||||||
|
"""
|
||||||
super().create(from_release, to_release, reboot_required, state)
|
super().create(from_release, to_release, reboot_required, state)
|
||||||
deploy = self.query(from_release, to_release)
|
deploy = self.query()
|
||||||
if deploy:
|
if deploy:
|
||||||
raise DeployAlreadyExist("Error to create. Deploy already exist.")
|
raise DeployAlreadyExist("Error to create. Deploy already exists.")
|
||||||
new_deploy = {
|
new_deploy = {
|
||||||
"from_release": from_release,
|
"from_release": from_release,
|
||||||
"to_release": to_release,
|
"to_release": to_release,
|
||||||
"reboot_required": reboot_required,
|
"reboot_required": reboot_required,
|
||||||
"state": state
|
"state": state.value
|
||||||
}
|
}
|
||||||
deploy_data = self.data.get("deploy", [])
|
|
||||||
if not deploy_data:
|
|
||||||
deploy_data = {
|
|
||||||
"deploy": []
|
|
||||||
}
|
|
||||||
deploy_data["deploy"].append(new_deploy)
|
|
||||||
self.data.update(deploy_data)
|
|
||||||
else:
|
|
||||||
deploy_data.append(new_deploy)
|
|
||||||
save_to_json_file(constants.SOFTWARE_JSON_FILE, self.data)
|
|
||||||
|
|
||||||
def query(self, from_release, to_release):
|
try:
|
||||||
super().query(from_release, to_release)
|
self.data["deploy"] = new_deploy
|
||||||
for deploy in self.data.get("deploy", []):
|
save_to_json_file(constants.SOFTWARE_JSON_FILE, self.data)
|
||||||
if deploy.get("from_release") == from_release and deploy.get("to_release") == to_release:
|
except Exception:
|
||||||
return deploy
|
self.data["deploy"] = {}
|
||||||
return None
|
|
||||||
|
|
||||||
def query_all(self):
|
def query(self):
|
||||||
return self.data.get("deploy", [])
|
"""
|
||||||
|
Query deploy based on from and to release version
|
||||||
|
:return: A deploy dictionary
|
||||||
|
"""
|
||||||
|
super().query()
|
||||||
|
return self.data.get("deploy", {})
|
||||||
|
|
||||||
def update(self, from_release, to_release, reboot_required, state):
|
def update(self, new_state: DEPLOY_STATES):
|
||||||
super().update(from_release, to_release, reboot_required, state)
|
"""
|
||||||
deploy = self.query(from_release, to_release)
|
Update deploy state based on from and to release version
|
||||||
|
:param new_state: The new state
|
||||||
|
"""
|
||||||
|
super().update(new_state)
|
||||||
|
deploy = self.query()
|
||||||
if not deploy:
|
if not deploy:
|
||||||
raise DeployDoNotExist("Error to update. Deploy do not exist.")
|
raise DeployDoNotExist("Error to update deploy state. No deploy in progress.")
|
||||||
deploy_data = {
|
|
||||||
"deploy": []
|
|
||||||
}
|
|
||||||
deploy_data["deploy"].append({
|
|
||||||
"from_release": from_release,
|
|
||||||
"to_release": to_release,
|
|
||||||
"reboot_required": reboot_required,
|
|
||||||
"state": state
|
|
||||||
})
|
|
||||||
self.data.update(deploy_data)
|
|
||||||
save_to_json_file(constants.SOFTWARE_JSON_FILE, self.data)
|
|
||||||
|
|
||||||
def delete(self, from_release, to_release):
|
try:
|
||||||
super().delete(from_release, to_release)
|
self.data["deploy"]["state"] = new_state.value
|
||||||
deploy = self.query(from_release, to_release)
|
save_to_json_file(constants.SOFTWARE_JSON_FILE, self.data)
|
||||||
|
except Exception:
|
||||||
|
self.data["deploy"] = deploy
|
||||||
|
|
||||||
|
def delete(self):
|
||||||
|
"""
|
||||||
|
Delete a deploy based on given from and to release version
|
||||||
|
"""
|
||||||
|
super().delete()
|
||||||
|
deploy = self.query()
|
||||||
if not deploy:
|
if not deploy:
|
||||||
raise DeployDoNotExist("Error to delete. Deploy do not exist.")
|
raise DeployDoNotExist("Error to delete deploy state. No deploy in progress.")
|
||||||
self.data.get("deploy").remove(deploy)
|
try:
|
||||||
save_to_json_file(constants.SOFTWARE_JSON_FILE, self.data)
|
self.data["deploy"] = {}
|
||||||
|
save_to_json_file(constants.SOFTWARE_JSON_FILE, self.data)
|
||||||
|
except Exception:
|
||||||
|
self.data["deploy"] = deploy
|
||||||
|
|
||||||
|
|
||||||
class DeployHostHandler(DeployHosts):
|
class DeployHostHandler(DeployHosts):
|
||||||
|
@ -349,11 +338,11 @@ class DeployHostHandler(DeployHosts):
|
||||||
raise DeployAlreadyExist("Error to create. Deploy host already exist.")
|
raise DeployAlreadyExist("Error to create. Deploy host already exist.")
|
||||||
|
|
||||||
new_deploy_host = {
|
new_deploy_host = {
|
||||||
"hostname": hostname,
|
"hostname": hostname,
|
||||||
"software_release": software_release,
|
"software_release": software_release,
|
||||||
"target_release": target_release,
|
"target_release": target_release,
|
||||||
"state": state
|
"state": state
|
||||||
}
|
}
|
||||||
|
|
||||||
deploy_data = self.data.get("deploy_host", [])
|
deploy_data = self.data.get("deploy_host", [])
|
||||||
if not deploy_data:
|
if not deploy_data:
|
||||||
|
|
|
@ -6,6 +6,7 @@ SPDX-License-Identifier: Apache-2.0
|
||||||
"""
|
"""
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
from netaddr import IPAddress
|
from netaddr import IPAddress
|
||||||
import os
|
import os
|
||||||
|
@ -260,6 +261,7 @@ def save_to_json_file(file, data):
|
||||||
json.dump(data, f)
|
json.dump(data, f)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
LOG.error("Problem saving file %s: %s", file, e)
|
LOG.error("Problem saving file %s: %s", file, e)
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
def load_from_json_file(file):
|
def load_from_json_file(file):
|
||||||
|
@ -297,9 +299,22 @@ def check_instances(params: list, instance):
|
||||||
LOG.exception(msg)
|
LOG.exception(msg)
|
||||||
raise ValueError(msg)
|
raise ValueError(msg)
|
||||||
|
|
||||||
|
|
||||||
def get_software_filesystem_data():
|
def get_software_filesystem_data():
|
||||||
if os.path.exists(constants.SOFTWARE_JSON_FILE):
|
if os.path.exists(constants.SOFTWARE_JSON_FILE):
|
||||||
data = load_from_json_file(constants.SOFTWARE_JSON_FILE)
|
return load_from_json_file(constants.SOFTWARE_JSON_FILE)
|
||||||
else:
|
else:
|
||||||
data = {}
|
return {}
|
||||||
return data
|
|
||||||
|
|
||||||
|
def validate_versions(versions):
|
||||||
|
"""
|
||||||
|
Validate a list of versions
|
||||||
|
:param versions: list of versions
|
||||||
|
:raise: ValueError if version is invalid
|
||||||
|
"""
|
||||||
|
for version in versions:
|
||||||
|
if not re.match(r'[0-9]+\.[0-9]+(\.[0-9]+)?$', version):
|
||||||
|
msg = "Invalid version: %s" % version
|
||||||
|
LOG.exception(msg)
|
||||||
|
raise ValueError(msg)
|
||||||
|
|
Loading…
Reference in New Issue