Merge "software deploy show implementation"

This commit is contained in:
Zuul 2023-12-05 16:59:43 +00:00 committed by Gerrit Code Review
commit 018eea2d51
7 changed files with 178 additions and 107 deletions

View File

@ -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',

View File

@ -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):

View File

@ -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'

View File

@ -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)

View File

@ -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 = ""

View File

@ -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:

View File

@ -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)