Skip to content

Commit f6dc75d

Browse files
committed
Thrid Party Container Management using Sonic Package Manager - followup1
1 parent 489266a commit f6dc75d

File tree

12 files changed

+699
-952
lines changed

12 files changed

+699
-952
lines changed

setup.py

+1
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,7 @@
257257
'xmltodict==0.12.0',
258258
'lazy-object-proxy',
259259
'six==1.16.0',
260+
'scp',
260261
] + sonic_dependencies,
261262
setup_requires= [
262263
'pytest-runner',

sonic_installer/main.py

+3
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,8 @@ def migrate_sonic_packages(bootloader, binary_image_version):
337337
new_image_docker_mount = os.path.join(new_image_mount, "var", "lib", "docker")
338338
docker_default_config = os.path.join(new_image_mount, "etc", "default", "docker")
339339
docker_default_config_backup = os.path.join(new_image_mount, TMP_DIR, "docker_config_backup")
340+
custom_manifests_path = os.path.join(PACKAGE_MANAGER_DIR, "manifests")
341+
new_image_package_directory_path = os.path.join(new_image_mount, "var", "lib", "sonic-package-manager")
340342

341343
if not os.path.isdir(new_image_docker_dir):
342344
# NOTE: This codepath can be reached if the installation process did not
@@ -372,6 +374,7 @@ def migrate_sonic_packages(bootloader, binary_image_version):
372374
run_command_or_raise(["chroot", new_image_mount, DOCKER_CTL_SCRIPT, "start"])
373375
docker_started = True
374376
run_command_or_raise(["cp", packages_path, os.path.join(new_image_mount, TMP_DIR, packages_file)])
377+
run_command_or_raise(["cp", "-arf", custom_manifests_path , new_image_package_directory_path])
375378
run_command_or_raise(["touch", os.path.join(new_image_mount, "tmp", DOCKERD_SOCK)])
376379
run_command_or_raise(["mount", "--bind",
377380
os.path.join(VAR_RUN_PATH, DOCKERD_SOCK),

sonic_package_manager/main.py

+26-198
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,13 @@
1313
import paramiko
1414
import requests
1515
import getpass
16-
import shutil
1716
from natsort import natsorted
1817

1918
from sonic_package_manager.database import PackageEntry, PackageDatabase
2019
from sonic_package_manager.errors import PackageManagerError
2120
from sonic_package_manager.logger import log
2221
from sonic_package_manager.manager import PackageManager
23-
from sonic_package_manager.manifest import Manifest, DEFAULT_MANIFEST, MANIFEST_LOCATION, DEFAUT_MANIFEST_NAME, DMFILE_NAME
24-
LOCAL_TARBALL_PATH="/tmp/local_tarball.gz"
25-
LOCAL_JSON="/tmp/local_json"
22+
from sonic_package_manager.manifest import MANIFEST_LOCATION
2623

2724
BULLET_UC = '\u2022'
2825

@@ -105,8 +102,11 @@ def handle_parse_result(self, ctx, opts, args):
105102
cls=MutuallyExclusiveOption,
106103
mutually_exclusive=['from_tarball', 'package_expr']),
107104
click.option('--from-tarball',
108-
type=str,
109-
help='Fetch package from saved image tarball from local/scp/sftp/http',
105+
type=click.Path(exists=True,
106+
readable=True,
107+
file_okay=True,
108+
dir_okay=False),
109+
help='Fetch package from saved image tarball.',
110110
cls=MutuallyExclusiveOption,
111111
mutually_exclusive=['from_repository', 'package_expr']),
112112
click.argument('package-expr',
@@ -227,11 +227,6 @@ def manifest(ctx,
227227
manager: PackageManager = ctx.obj
228228

229229
try:
230-
if from_tarball:
231-
#Download the tar file from local/scp/sftp/http
232-
download_file(from_tarball, LOCAL_TARBALL_PATH)
233-
from_tarball = LOCAL_TARBALL_PATH
234-
235230
source = manager.get_package_source(package_expr,
236231
from_repository,
237232
from_tarball)
@@ -272,11 +267,6 @@ def changelog(ctx,
272267
manager: PackageManager = ctx.obj
273268

274269
try:
275-
if from_tarball:
276-
#Download the tar file from local/scp/sftp/http
277-
download_file(from_tarball, LOCAL_TARBALL_PATH)
278-
from_tarball = LOCAL_TARBALL_PATH
279-
280270
source = manager.get_package_source(package_expr,
281271
from_repository,
282272
from_tarball)
@@ -311,52 +301,13 @@ def changelog(ctx,
311301
def create(ctx, name, from_json):
312302
"""Create a new custom local manifest file."""
313303

314-
#Validation checks
315304
manager: PackageManager = ctx.obj
316-
if manager.is_installed(name):
317-
click.echo("Error: A package with the same name {} is already installed".format(name))
318-
return
319-
MFILE_NAME = os.path.join(MANIFEST_LOCATION, name)
320-
if os.path.exists(MFILE_NAME):
321-
click.echo("Error: Manifest file '{}' already exists.".format(name))
322-
return
323-
324-
#Creation of default manifest file in case the file does not exist
325-
if not os.path.exists(MANIFEST_LOCATION):
326-
os.mkdir(MANIFEST_LOCATION)
327-
if not os.path.exists(DMFILE_NAME):
328-
with open(DMFILE_NAME, 'w') as file:
329-
json.dump(DEFAULT_MANIFEST, file, indent=4)
330-
#click.echo(f"Manifest '{DEFAUT_MANIFEST_NAME}' created now.")
331-
332-
333-
#Create the manifest file in centralized location
334-
#Download the json file from scp/sftp/http to local_json_file
335-
try:
336-
if from_json:
337-
download_file(from_json, LOCAL_JSON)
338-
from_json = LOCAL_JSON
339-
data = {}
340-
with open(from_json, 'r') as file:
341-
data = json.load(file)
342-
#Validate with manifest scheme
343-
Manifest.marshal(data)
344-
345-
#Make sure the 'name' is overwritten into the dict
346-
data['package']['name'] = name
347-
data['service']['name'] = name
348-
349-
with open(MFILE_NAME, 'w') as file:
350-
json.dump(data, file, indent=4)
351-
else:
352-
shutil.copy(DMFILE_NAME, MFILE_NAME)
353-
click.echo(f"Manifest '{name}' created successfully.")
305+
try:
306+
manager.create_package_manifest(name, from_json)
354307
except Exception as e:
355308
click.echo("Error: Manifest {} creation failed - {}".format(name, str(e)))
356309
return
357310

358-
359-
360311
#At the end of sonic-package-manager install, a new manifest file is created with the name.
361312
#At the end of sonic-package-manager uninstall name, this manifest file name and name.edit will be deleted.
362313
#At the end of sonic-package-manager update, we need to mv maniests name.edit to name in case of success, else keep it as such.
@@ -365,67 +316,26 @@ def create(ctx, name, from_json):
365316
@click.pass_context
366317
@click.argument('name', type=click.Path())
367318
@click.option('--from-json', type=str, required=True)
368-
#@click.argument('--from-json', type=str, help='Specify Manifest json file')
369319
@root_privileges_required
370320
def update(ctx, name, from_json):
371321
"""Update an existing custom local manifest file with new one."""
372322

373323
manager: PackageManager = ctx.obj
374-
ORG_FILE = os.path.join(MANIFEST_LOCATION, name)
375-
if not os.path.exists(ORG_FILE):
376-
click.echo(f'Local Manifest file for {name} does not exists to update')
377-
return
378324
try:
379-
#download json file from remote/local path
380-
download_file(from_json, LOCAL_JSON)
381-
from_json = LOCAL_JSON
382-
with open(from_json, 'r') as file:
383-
data = json.load(file)
384-
385-
#Validate with manifest scheme
386-
Manifest.marshal(data)
387-
388-
#Make sure the 'name' is overwritten into the dict
389-
data['package']['name'] = name
390-
data['service']['name'] = name
391-
392-
if manager.is_installed(name):
393-
edit_name = name + '.edit'
394-
EDIT_FILE = os.path.join(MANIFEST_LOCATION, edit_name)
395-
with open(EDIT_FILE, 'w') as edit_file:
396-
json.dump(data, edit_file, indent=4)
397-
click.echo(f"Manifest '{name}' updated successfully.")
398-
else:
399-
#If package is not installed,
400-
## update the name file directly
401-
with open(ORG_FILE, 'w') as orig_file:
402-
json.dump(data, orig_file, indent=4)
403-
click.echo(f"Manifest '{name}' updated successfully.")
325+
manager.update_package_manifest(name, from_json)
404326
except Exception as e:
405327
click.echo(f"Error occurred while updating manifest '{name}': {e}")
406328
return
407329

408-
409330
@manifests.command('delete')
410331
@click.pass_context
411332
@click.argument('name', type=click.Path())
412333
@root_privileges_required
413334
def delete(ctx, name):
414335
"""Delete a custom local manifest file."""
415-
# Check if the manifest file exists
416-
mfile_name = "{}{}".format(MANIFEST_LOCATION, name)
417-
if not os.path.exists(mfile_name):
418-
click.echo("Error: Manifest file '{}' not found.".format(name))
419-
return
420-
336+
manager: PackageManager = ctx.obj
421337
try:
422-
# Confirm deletion with user input
423-
confirm = click.prompt("Are you sure you want to delete the manifest file '{}'? (y/n)".format(name), type=str)
424-
if confirm.lower() == 'y':
425-
os.remove(mfile_name)
426-
click.echo("Manifest '{}' deleted successfully.".format(name))
427-
else:
428-
click.echo("Deletion cancelled.")
338+
manager.delete_package_manifest(name)
429339
except Exception as e:
430340
click.echo("Error: Failed to delete manifest file '{}'. {}".format(name, e))
431341

@@ -436,15 +346,9 @@ def delete(ctx, name):
436346
@root_privileges_required
437347
def show_manifest(ctx, name):
438348
"""Show the contents of custom local manifest file."""
439-
mfile_name = "{}{}".format(MANIFEST_LOCATION, name)
440-
edit_file_name = "{}.edit".format(mfile_name)
349+
manager: PackageManager = ctx.obj
441350
try:
442-
if os.path.exists(edit_file_name):
443-
mfile_name = edit_file_name
444-
with open(mfile_name, 'r') as file:
445-
data = json.load(file)
446-
click.echo("Manifest file: {}".format(name))
447-
click.echo(json.dumps(data, indent=4))
351+
manager.show_package_manifest(name)
448352
except FileNotFoundError:
449353
click.echo("Manifest file '{}' not found.".format(name))
450354

@@ -453,14 +357,8 @@ def show_manifest(ctx, name):
453357
@root_privileges_required
454358
def list_manifests(ctx):
455359
"""List all custom local manifest files."""
456-
# Get all files in the manifest location
457-
manifest_files = os.listdir(MANIFEST_LOCATION)
458-
if not manifest_files:
459-
click.echo("No custom local manifest files found.")
460-
else:
461-
click.echo("Custom Local Manifest files:")
462-
for file in manifest_files:
463-
click.echo("- {}".format(file))
360+
manager: PackageManager = ctx.obj
361+
manager.list_package_manifest()
464362

465363

466364
@repository.command()
@@ -499,78 +397,6 @@ def remove(ctx, name):
499397
exit_cli(f'Failed to remove repository {name}: {err}', fg='red')
500398

501399

502-
def parse_url(url):
503-
# Parse information from URL
504-
parsed_url = urlparse(url)
505-
if parsed_url.scheme == "scp" or parsed_url.scheme == "sftp":
506-
return parsed_url.username, parsed_url.password, parsed_url.hostname, parsed_url.path
507-
elif parsed_url.scheme == "http":
508-
return None, None, parsed_url.netloc, parsed_url.path
509-
elif not parsed_url.scheme: # No scheme indicates a local file path
510-
return None, None, None, parsed_url.path
511-
else:
512-
raise ValueError("Unsupported URL scheme")
513-
514-
def validate_url_or_abort(url):
515-
# Attempt to retrieve HTTP response code
516-
try:
517-
response = requests.head(url)
518-
response_code = response.status_code
519-
except requests.exceptions.RequestException as err:
520-
response_code = None
521-
522-
if not response_code:
523-
print("Did not receive a response from remote machine. Aborting...")
524-
return
525-
else:
526-
# Check for a 4xx response code which indicates a nonexistent URL
527-
if str(response_code).startswith('4'):
528-
print("Image file not found on remote machine. Aborting...")
529-
return
530-
531-
def download_file(url, local_path):
532-
# Parse information from the URL
533-
username, password, hostname, remote_path = parse_url(url)
534-
535-
if username is not None:
536-
# If password is not provided, prompt the user for it securely
537-
if password is None:
538-
password = getpass.getpass(prompt=f"Enter password for {username}@{hostname}: ")
539-
540-
# Create an SSH client for SCP or SFTP
541-
client = paramiko.SSHClient()
542-
# Automatically add the server's host key (this is insecure and should be handled differently in production)
543-
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
544-
545-
try:
546-
# Connect to the SSH server
547-
client.connect(hostname, username=username, password=password)
548-
549-
# Open an SCP channel for SCP or an SFTP channel for SFTP
550-
with client.open_sftp() as sftp:
551-
# Download the file
552-
sftp.get(remote_path, local_path)
553-
554-
finally:
555-
# Close the SSH connection
556-
client.close()
557-
elif hostname:
558-
# Download using HTTP for URLs without credentials
559-
validate_url_or_abort(url)
560-
try:
561-
response = requests.get(url)
562-
with open(local_path, 'wb') as f:
563-
f.write(response.content)
564-
except requests.exceptions.RequestException as e:
565-
print("Download error", e)
566-
return
567-
else:
568-
if os.path.exists(remote_path):
569-
shutil.copy(remote_path, local_path)
570-
else:
571-
print(f"Error: Source file '{remote_path}' does not exist.")
572-
573-
574400
@cli.command()
575401
@click.option('--enable',
576402
is_flag=True,
@@ -592,7 +418,8 @@ def download_file(url, local_path):
592418
@click.option('--use-local-manifest',
593419
is_flag=True,
594420
default=None,
595-
help='Use locally created custom manifest file ')
421+
help='Use locally created custom manifest file. ',
422+
hidden=True)
596423
@click.option('--name',
597424
type=str,
598425
help='custom name for the package')
@@ -651,11 +478,6 @@ def install(ctx,
651478
click.echo(f'Local Manifest file for {name} does not exists to install')
652479
return
653480

654-
if from_tarball:
655-
#Download the tar file from local/scp/sftp/http
656-
download_file(from_tarball, LOCAL_TARBALL_PATH)
657-
from_tarball = LOCAL_TARBALL_PATH
658-
659481
try:
660482
manager.install(package_expr,
661483
from_repository,
@@ -671,16 +493,22 @@ def install(ctx,
671493

672494
@cli.command()
673495
@add_options(PACKAGE_COMMON_OPERATION_OPTIONS)
496+
@add_options(PACKAGE_COMMON_INSTALL_OPTIONS)
674497
@click.argument('name')
675498
@click.pass_context
676499
@root_privileges_required
677-
def update(ctx, name, force, yes):
678-
""" Update package to the updated manifest file """
500+
def update(ctx, name, force, yes, skip_host_plugins):
501+
""" Update package to the updated manifest file. """
679502

680503
manager: PackageManager = ctx.obj
681504

505+
update_opts = {
506+
'force': force,
507+
'skip_host_plugins': skip_host_plugins,
508+
'update_only': True,
509+
}
682510
try:
683-
manager.update(name, force)
511+
manager.update(name, **update_opts)
684512
except Exception as err:
685513
exit_cli(f'Failed to update package {name}: {err}', fg='red')
686514
except KeyboardInterrupt:

0 commit comments

Comments
 (0)