-
Notifications
You must be signed in to change notification settings - Fork 684
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
ThirdPartyContainerManagement(TPCM)_in_SonicPackageManager #2815
Conversation
2560a9c
to
cf60307
Compare
ebf186a
to
5247f6b
Compare
@venkatmahalingam @stepanblyschak pls review the Code PR |
sonic_package_manager/main.py
Outdated
if manager.is_installed(name): | ||
click.echo("Error: A package with the same name {} is already installed".format(name)) | ||
return | ||
MFILE_NAME = os.path.join(MANIFEST_LOCATION, name) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
MFILE_NAME -> mfile_name since not a global constant
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
sonic_package_manager/main.py
Outdated
if not os.path.exists(DMFILE_NAME): | ||
with open(DMFILE_NAME, 'w') as file: | ||
json.dump(DEFAULT_MANIFEST, file, indent=4) | ||
#click.echo(f"Manifest '{DEFAUT_MANIFEST_NAME}' created now.") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
sonic_package_manager/main.py
Outdated
@click.pass_context | ||
@click.argument('name', type=click.Path()) | ||
@click.option('--from-json', type=str, required=True) | ||
#@click.argument('--from-json', type=str, help='Specify Manifest json file') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
sonic_package_manager/main.py
Outdated
"""Update an existing custom local manifest file with new one.""" | ||
|
||
manager: PackageManager = ctx.obj | ||
ORG_FILE = os.path.join(MANIFEST_LOCATION, name) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does org mean "original"? if so I prefer longer variable name
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
def test_get_manifest_from_local_file2(capsys): | ||
metadata_resolver = MetadataResolver(None, None) # Replace None with appropriate mocks | ||
|
||
with patch('os.path.exists', return_value=True), \ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This isn't needed. To simplify mocking FS operations there is a sonic_fs fixture based on pyfakefs
sonic_package_manager/main.py
Outdated
|
||
#Validation checks | ||
manager: PackageManager = ctx.obj | ||
if manager.is_installed(name): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What if manifest is created before the package with the same name is installed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed the logic such a way that, it first checks if a manifests with name "name" exists and use it. Else creates a new manifest with the name "name" populated from default manifest.
sonic_package_manager/metadata.py
Outdated
if labels is None: | ||
raise MetadataError('No manifest found in image labels') | ||
|
||
raise MetadataError('No manifest found in image labels and also could not create locally') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix indent
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed
@@ -105,20 +105,22 @@ def __init__(self, | |||
tarball_path: str, | |||
database: PackageDatabase, | |||
docker: DockerApi, | |||
metadata_resolver: MetadataResolver): | |||
metadata_resolver: MetadataResolver, | |||
use_local_manifest: bool = False, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There seems to be no point in passing in use_local_manifest through all the objects.
The logic is - if manifest with the name name
exists use it unless docker has one, otherwise use what's inside the docker. This also means you don't need to modify MetadataResolver as it is relevant only for the second case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The logic is now maintained as suggested.
To first use the manifest inside docker image. If that does not exist and If the custom local manifest with name "name" exists, use it.
The option to prioritize the local manifest over the docker image built-in manifest is hidden.
@@ -378,17 +642,50 @@ def install(ctx, | |||
if allow_downgrade is not None: | |||
install_opts['allow_downgrade'] = allow_downgrade | |||
|
|||
if use_local_manifest: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please disallow the use of custom user manifest on dockers that provide their own. This might create a lot of irrelevant complains from customers trying to do that and breaking the package functionality. Only allow to use custom manifest on dockers that don't have them.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For now, I have made this option(use_local_manifest) to be hidden. Have kept this code as such in case if there is a requirement in future to override the built-in manifest.
sonic_package_manager/manager.py
Outdated
@@ -838,10 +813,10 @@ def migrate_package(old_package_entry, | |||
package_source = self.get_package_source(package_ref=new_package_ref) | |||
package = package_source.get_package() | |||
new_package_default_version = package.manifest['package']['version'] | |||
if old_package.version > new_package_default_version: | |||
if old_package.version >= new_package_default_version: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For instance, Third party container debian:latest with name MYDEB was downloaded to the switch( default_version will be 1.0.0 and default_reference will be latest)
Now, on attempt to migrate, the new_package_default_reference will also be 1.0.0.
In this case as well, we could migrate the MYDEB tpcm docker using image save technique of migrate_package.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@sg893052 If I understand correctly, package is migrated even if the it is of the same version in the new image?
If you installed a package MYDEB in the old life, why it should be present in the new life and have the same version, even then, why we should migrate?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@stepanblyschak Please suggest if the below is OK.
MYDEB package (debian:latest) is installed in the current image.
Now, on sonic-sonic image upgrade, as part of new image, MYDEB package is not installed.
Hence as part of migrate_packages(), "elif new_package.default_reference is not None:" is hit and finally the version comparison occurs where the old_package.version [1.0.0] and new_package_default_version are same [1.0.0].
As per the original code, calling self.install("MYDEB=1.0.0") in the else case would fail as below
Failed to migrate packages Failed to retrieve manifest for library/debian:1.0.0: code: 404 details: {
'errors': [
{
'code': 'MANIFEST_UNKNOWN',
'message': 'manifest unknown',
'detail': 'unknown tag=1.0.0'
}
]
}
For this to work, we could change the else case code to below (if migration is not supposed to be called in version equal case)
else:
#self.install(f'{new_package.name}={new_package_default_version}')
repo_tag_formed="{}:{}".format(new_package.repository, new_package.default_reference)
self.install(None,repo_tag_formed,name=new_package.name)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@sg893052 Could you please check why you have default_reference set to 1.0.0? In your case I would expect the following will execute:
else:
# No default version and package is not installed.
# Migrate old package same version.
new_package.version = old_package.version
migrate_package(old_package, new_package)
The default_reference
is a tag or digest of Docker image that will be a default installation candidate. 1.0.0 does not exists for debian docker image, therefore you get this error.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@stepanblyschak In the migrate_package function, the following condition cases are defined, but there is still some confusion that requires clarification:
Case 1: if new_package.installed.
Case 2: else if new_package.default_reference is not None.
Case 3: else.
In the specific example of the MYDEB package (debian:latest), where "latest" is set as the new_package.default_reference, Case 2 will be triggered because new_package.default_reference is not None. However, Case 3 will not be triggered in this scenario.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@sg893052 The error message you shared before implies you had '1.0.0' as the default_reference "Failed to migrate packages Failed to retrieve manifest for library/debian:1.0.0:". What is the problem if default_reference is set to 'latest'?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@stepanblyschak Firstly, new_package.default_reference is "latest" which makes it to hit the case2 .
Subsequently, new_package_default_version = package.manifest['package']['version'] --> new_package_default_version is derived from package's manifest package version which is 1.0.0 .
Hence the new_package_default_version variable is set to 1.0.0 here
Log:
package: Package(entry=PackageEntry(name='MYDEB', repository='debian', description='', default_reference='latest', version=None, installed=False, built_in=False, image_id=None), metadata=Metadata(manifest={'version': Version('1.0.0'), 'package': {'version': Version('1.0.0'), 'name': 'MYDEB', 'description': '', 'base-os': ComponentConstraints(components={}), 'depends': [], 'breaks': [], 'init-cfg': {}, 'changelog': {}, 'debug-dump': ''}, 'service': {'name': 'MYDEB', 'requires': [], 'requisite': [], 'wanted-by': [], 'after': [], 'before': [], 'dependent': [], 'dependent-of': [], 'post-start-action': '', 'pre-shutdown-action': '', 'asic-service': False, 'host-service': True, 'delayed': False, 'check_up_status': False, 'warm-shutdown': {'after': [], 'before': []}, 'fast-shutdown': {'after': [], 'before': []}, 'syslog': {'support-rate-limit': False}}, 'container': {'privileged': False, 'volumes': [], 'mounts': [], 'environment': {}, 'tmpfs': []}, 'processes': [], 'cli': {'mandatory': False, 'show': [], 'config': [], 'clear': [], 'auto-generate-show': False, 'auto-generate-config': False, 'auto-generate-show-source-yang-modules': [], 'auto-generate-config-source-yang-modules': []}}, components={}, yang_modules=[]))
migrate -- old_package.version:1.0.0 new_package_default_version: 1.0.0
==================================================================
Code snippet:
elif new_package.default_reference is not None:
new_package_ref = PackageReference(new_package.name, new_package.default_reference)
package_source = self.get_package_source(package_ref=new_package_ref)
package = package_source.get_package()
print("package:{}".format(package))
new_package_default_version = package.manifest['package']['version']
print("migrate -- old_package.version:{} new_package_default_version: {}".format(old_package.version, new_package_default_version))
if old_package.version > new_package_default_version:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@sg893052 Thanks for all the details.
I agree with your following suggestion:
For this to work, we could change the else case code to below (if migration is not supposed to be called in version equal case)
else:
#self.install(f'{new_package.name}={new_package_default_version}')
repo_tag_formed="{}:{}".format(new_package.repository, new_package.default_reference)
self.install(None,repo_tag_formed,name=new_package.name)
If the logic found new_package.default_reference is not None
and decided to install from default_reference it should use default_reference instead of version. The code currently assumes default_reference is the same as default_version, however it's not always the case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks! Incorporated it in latest commit
f6dc75d
to
c6457a4
Compare
setup.py
Outdated
@@ -257,6 +257,7 @@ | |||
'xmltodict==0.12.0', | |||
'lazy-object-proxy', | |||
'six==1.16.0', | |||
'scp', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pin this package to a certain version tested with like other dependencies are.
sonic_package_manager/main.py
Outdated
if not name: | ||
click.echo(f'name argument is not provided to use local manifest') | ||
return | ||
ORG_FILE = os.path.join(MANIFEST_LOCATION, name) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rename to lower case to indicate this is not a global constant
sonic_package_manager/manager.py
Outdated
@@ -430,7 +433,7 @@ def install_from_source(self, | |||
self.service_creator.generate_shutdown_sequence_files, | |||
self.get_installed_packages()) | |||
) | |||
|
|||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@sg893052 Could you please check again, I see spaces in the diff
sonic_package_manager/manager.py
Outdated
@@ -838,10 +813,10 @@ def migrate_package(old_package_entry, | |||
package_source = self.get_package_source(package_ref=new_package_ref) | |||
package = package_source.get_package() | |||
new_package_default_version = package.manifest['package']['version'] | |||
if old_package.version > new_package_default_version: | |||
if old_package.version >= new_package_default_version: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@sg893052 If I understand correctly, package is migrated even if the it is of the same version in the new image?
If you installed a package MYDEB in the old life, why it should be present in the new life and have the same version, even then, why we should migrate?
sonic_package_manager/manifest.py
Outdated
} | ||
#DEFAULT_MANIFEST = {'version': '1.0.0', 'package': {'version': '1.0.0', 'depends': [], 'name': 'default_manifest'}, 'service': {'name': 'default_manifest', 'requires': ['docker'], 'after': ['docker'], 'before': [], 'dependent-of': [], 'asic-service': False, 'host-service': False, 'warm-shutdown': {'after': [], 'before': []}, 'fast-shutdown': | ||
#{'after': [], 'before': []}, 'syslog': {'support-rate-limit': False}}, 'container': {'privileged': False, 'volumes': [], 'tmpfs': [], 'entrypoint': ''}, 'cli': { 'mandatory': False, 'config': [], 'show': [], 'clear': []}} | ||
MANIFEST_LOCATION = "/var/lib/sonic-package-manager/manifests/" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@sg893052 Could you please double check if the rename was commited?
e9db7a1
to
37cb008
Compare
@@ -257,6 +257,7 @@ | |||
'xmltodict==0.12.0', | |||
'lazy-object-proxy', | |||
'six==1.16.0', | |||
'scp==0.14.5', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed the version number as per suggestion
sonic_package_manager/manifest.py
Outdated
"auto-generate-config-source-yang-modules": [] | ||
} | ||
} | ||
MANIFESTS_LOCATION = "/var/lib/sonic-package-manager/manifests/" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
updated the variable from MANIFEST_LOCATION to MANIFESTS_LOCATION
@stepanblyschak please help review the changes from Senthil again, this is a planned feature for 202405 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One open question regarding changes in the package migration logic
sonic_package_manager/manifest.py
Outdated
'sonic': { | ||
'manifest': json_str | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please fix formatting
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Taken care of in latest commit
sonic_package_manager/manifest.py
Outdated
if not os.path.exists(MANIFESTS_LOCATION): | ||
os.mkdir(MANIFESTS_LOCATION) | ||
if not os.path.exists(DEFAULT_MANIFEST_FILE): | ||
with open(DEFAULT_MANIFEST_FILE, 'w') as file: | ||
json.dump(DEFAULT_MANIFEST, file, indent=4) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems to duplicate the code from create_package_manifest, why we need to create a directory and default manifest file here in get_manifest_from_local_file ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
create_package_manifest is invoked as part of "sonic_package_manager manifests create" command.
If the user does not invoke this CLI first but rather directly executes "sonic_package_manager install" command, then these directory & default manifest file are to be created on the fly for the first time.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@sg893052 It seems like you can prepare the directory and the default manifest at build time without the need to create it in runtime every time it is required
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For this, creation of directory and default manifest is taken care in sonic-buidimage repo similar to packages.json file.
https://github.com/sonic-net/sonic-buildimage/pull/14917/files
sonic_package_manager/manager.py
Outdated
@@ -838,10 +813,10 @@ def migrate_package(old_package_entry, | |||
package_source = self.get_package_source(package_ref=new_package_ref) | |||
package = package_source.get_package() | |||
new_package_default_version = package.manifest['package']['version'] | |||
if old_package.version > new_package_default_version: | |||
if old_package.version >= new_package_default_version: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@sg893052 Could you please check why you have default_reference set to 1.0.0? In your case I would expect the following will execute:
else:
# No default version and package is not installed.
# Migrate old package same version.
new_package.version = old_package.version
migrate_package(old_package, new_package)
The default_reference
is a tag or digest of Docker image that will be a default installation candidate. 1.0.0 does not exists for debian docker image, therefore you get this error.
ThirdPartyContainerManagement(TPCM) support in SonicPackageManager allows third party dockers to be installed on the sonic system. The Manifest file is generated from a local default file. The Manifest file could be updated through "sonic-package-manager manifests update" command and later the running package could be updated with the new manifest file through "sonic-package-manager update"
c66679c
to
73250ca
Compare
@adyeung Reviewer @stepanblyschak has approved the changes. |
@qiluo-msft please help review and merge, this is needed for 202405 |
…#2815) ThirdPartyContainerManagement(TPCM) support in SonicPackageManager allows third party dockers to be installed on the sonic system. The Manifest file is generated from a custom local default file. The Manifest file could be updated through "sonic-package-manager manifests update" command and later the running package could be updated with the new manifest file through "sonic-package-manager update" #### What I did There are many Third Party application dockers, that can be used in SONiC to provision, manage and monitor SONiC devices. The dockers need not be compatible with SONiC, but can almost work independently with minimal SONiC interfaces. These are extensions to SONiC and require additional capabilities to seamlessly integrate with SONiC. These are related to installation, upgrade, and configuration. This change is an enhancement to the SONiC Application Extension Infrastructure to enable integrating a Third Party Application in the form of dockers with SONiC. Moreover, the process of downloading image tarballs for the dockers (packages) supports SCP, SFTP, and URL before installing them. #### How I did it The Sonic-package-manager framework has been enhanced to support ThirdPartyContainerManagement (TPCM). In case no manifest is found in the image labels, the framework treats it as a TPCM package and creates a default manifest for it. During installation, a new manifest file is created with a specified name using the --name option. Users can use the "sonic-package-manager manifests create/update/delete" commands to modify or delete the manifest file. The location for custom local package manifest files is set to "/var/lib/sonic-package-manager/manifests/". Finally, the "sonic-package-manager update" command can be used to apply the updated manifest file to the running TPCM docker. #### How to verify it sonic-package-manager install --from-repository <package without manifest, say httpd> --name mytpcm sonic-package manager install --from-tarball <local tar/scp tar/sftp tar/http tar> --name <> --use-local-manifest Manifests Commands(tpcm): sonic-package-manager manifests create <> --from-json <> sonic-package-manager manifests update <> --from-json <> sonic-package-manager manifests list sonic-package-manager manifests show <> sonic-package-manager manifests delete <> sonic-package manager update <package>
HLD : https://github.com/sonic-net/SONiC/blob/master/doc/sonic-application-extension/tpcm_app_ext.md
ThirdPartyContainerManagement(TPCM) support in SonicPackageManager allows third party dockers to be installed on the sonic system. The Manifest file is generated from a custom local default file. The Manifest file could be updated through "sonic-package-manager manifests update" command and later the running package could be updated with the new manifest file through "sonic-package-manager update"
What I did
There are many Third Party application dockers, that can be used in SONiC to provision, manage and monitor SONiC devices. The dockers need not be compatible with SONiC, but can almost work independently with minimal SONiC interfaces. These are extensions to SONiC and require additional capabilities to seamlessly integrate with SONiC. These are related to installation, upgrade, and configuration. This change is an enhancement to the SONiC Application Extension Infrastructure to enable integrating a Third Party Application in the form of dockers with SONiC.
Moreover, the process of downloading image tarballs for the dockers (packages) supports SCP, SFTP, and URL before installing them.
How I did it
The Sonic-package-manager framework has been enhanced to support ThirdPartyContainerManagement (TPCM). In case no manifest is found in the image labels, the framework treats it as a TPCM package and creates a default manifest for it. During installation, a new manifest file is created with a specified name using the --name option. Users can use the "sonic-package-manager manifests create/update/delete" commands to modify or delete the manifest file. The location for custom local package manifest files is set to "/var/lib/sonic-package-manager/manifests/". Finally, the "sonic-package-manager update" command can be used to apply the updated manifest file to the running TPCM docker.
How to verify it
sonic-package-manager install --from-repository <package without manifest, say httpd> --name mytpcm
sonic-package manager install --from-tarball <local tar/scp tar/sftp tar/http tar> --name <> --use-local-manifest
Manifests Commands(tpcm):
sonic-package-manager manifests create <> --from-json <>
sonic-package-manager manifests update <> --from-json <>
sonic-package-manager manifests list
sonic-package-manager manifests show <>
sonic-package-manager manifests delete <>
sonic-package manager update
Previous command output (if the output of a command-line utility has changed)
New command output (if the output of a command-line utility has changed)