Merge ~ltrager/maas-images:version_remove_copy into maas-images:master

Proposed by Lee Trager
Status: Merged
Merge reported by: Lee Trager
Merged at revision: 0c3ba47197755743bd936360f60c07c43ce41624
Proposed branch: ~ltrager/maas-images:version_remove_copy
Merge into: maas-images:master
Diff against target: 268 lines (+184/-9)
5 files modified
doc/copying-product-version.txt (+36/-0)
doc/removing-product-version.txt (+51/-0)
meph2/commands/flags.py (+23/-4)
meph2/commands/meph2_util.py (+65/-0)
meph2/util.py (+9/-5)
Reviewer Review Type Date Requested Status
Newell Jensen (community) Approve
Review via email: mp+374577@code.launchpad.net

Commit message

Add the ability to remove or copy versions from a stream.

Description of the change

Example - remove a version from all Bionic images
./maas-images/bin/meph2-util remove-version ~/remove-version/ 20191004 release=bionic

Example - copy a version for all amd64 Bionic images
/maas-images/bin/meph2-util copy-version ~/copy-version/ 20191004 20191022 release=bionic arch=amd64

To post a comment you must log in.
Revision history for this message
Newell Jensen (newell-jensen) wrote :

LGTM

review: Approve
0c3ba47... by Lee Trager

Add documentation

Revision history for this message
Robert C Jennings (rcj) wrote :

Lee, the docs look good, thank you. Should we copy an image forward from $serial to $serial.1 to test this out? Or at least, how did you test this?

Revision history for this message
Lee Trager (ltrager) wrote :

For development and testing I created a local mirror of the images and ran remove-version and copy-version on it. I verified with diff that the modifications happened as expected.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
diff --git a/doc/copying-product-version.txt b/doc/copying-product-version.txt
0new file mode 1006440new file mode 100644
index 0000000..6008d0b
--- /dev/null
+++ b/doc/copying-product-version.txt
@@ -0,0 +1,36 @@
1In the case that a bad image is published it may be quicker to copy a working
2version to a new version than wait for a fixed version to appear at
3cloud-images.ubuntu.com. This process is safer than just removing a broken
4version as it mimics a new version being added to the stream. This guarantees
5all clients, not just MAAS, will get the fixed image.
6
7It is also suggested to create a backup of the stream metadata in
8maas-v3-images/streams/v1 so you can use diff to verify only versions you wish
9to remove were removed.
10
11Basic usage:
12meph2-util copy-version data_d from_version to_version
13
14data_d - The path to the directory containing the stream you wish to modify.
15from_version - The version you wish to copy from
16to_version - The version you wish to copy to
17
18Example - Copy 20191004 to 20191022 on all products
19meph2-util copy-version /path/to/stream 20191004 20191022
20
21Filters:
22You may also add filters to the copy-version command. Only products matching
23those filters will have the specified version copied. Filters may be any field
24described in the product.
25
26Example - Copy the version 20191004 to 20191022 on all AMD64 Bionic and Xenial
27products.
28meph2-util copy-version /path/to/stream 20191004 20191022 \
29 'release~(bionic|xenial)' arch=amd64
30
31Optional Arguments:
32 -n, --dry-run - Only show what will be copied, do not modify the stream.
33 -u, --no-sign - Do not sign the stream when done. A stream can be signed later
34 with the meph2-util sign command.
35 --keyring - Specify the keyring to use when verifying the stream. Defaults
36 to /usr/share/keyrings/ubuntu-cloudimage-keyring.gpg
diff --git a/doc/removing-product-version.txt b/doc/removing-product-version.txt
0new file mode 10064437new file mode 100644
index 0000000..df8dfe6
--- /dev/null
+++ b/doc/removing-product-version.txt
@@ -0,0 +1,51 @@
1In the case that a bad image is published it may be quicker to remove it from
2the stream than wait for a fixed version to appear at cloud-images.ubuntu.com.
3MAAS will import the latest version available from the stream, if a version is
4removed the previous version will be automatically retrieved.
5
6Before beginning make sure any import process which imports from
7cloud-images.ubuntu.com is disabled so the image isn't recreated.
8
9It is also suggested to create a backup of the stream metadata in
10maas-v3-images/streams/v1 so you can use diff to verify only versions you wish
11to remove were removed.
12
13Basic usage:
14meph2-util remove-version data_d version
15
16data_d - The path to the directory containing the stream you wish to modify.
17version - The version of the product you wish to remove.
18
19Example - Remove the version 20191021 from all products
20meph2-util remove-version /path/to/stream 20191021
21
22Filters:
23You may also add filters to the remove-version command. Only products matching
24those filters will have the specified version removed. Filters may be any field
25described in the product.
26
27Example - Remove the version 20191021 from all AMD64 Bionic and Xenial
28products.
29meph2-util remove-version /path/to/stream 20191021 \
30 'release~(bionic|xenial)' arch=amd64
31
32Optional Arguments:
33 -n, --dry-run - Only show what will be removed, do not modify the stream.
34 -u, --no-sign - Do not sign the stream when done. A stream can be signed later
35 with the meph2-util sign command.
36 --keyring - Specify the keyring to use when verifying the stream. Defaults
37 to /usr/share/keyrings/ubuntu-cloudimage-keyring.gpg
38
39Clean up:
40meph2-util remove-version only modifies the stream metadata. The image files
41themselves are left on the filesystem. To automatically remove them run the
42following commands
43
44Find all orphaned files and write the list to /tmp/orphans:
45meph2-util find-orphans /tmp/orphans /path/to/stream
46
47Delete all orphaned files:
48meph2-util reap-orphans /tmp/orphans /path/to/stream --older 0
49
50Remove orphan file:
51rm /tmp/orphans
diff --git a/meph2/commands/flags.py b/meph2/commands/flags.py
index e2cffa5..d0c3d61 100644
--- a/meph2/commands/flags.py
+++ b/meph2/commands/flags.py
@@ -24,6 +24,8 @@ COMMON_FLAGS = {
24 'keyring': (('--keyring',),24 'keyring': (('--keyring',),
25 {'help': 'gpg keyring to check sjson',25 {'help': 'gpg keyring to check sjson',
26 'default': DEF_KEYRING}),26 'default': DEF_KEYRING}),
27 'filters': ('filters', {'nargs': '*', 'default': []}),
28 'version': ('version', {'help': 'the version_id to promote.'}),
27}29}
2830
29SUBCOMMANDS = {31SUBCOMMANDS = {
@@ -33,7 +35,7 @@ SUBCOMMANDS = {
33 COMMON_FLAGS['dry-run'], COMMON_FLAGS['no-sign'],35 COMMON_FLAGS['dry-run'], COMMON_FLAGS['no-sign'],
34 COMMON_FLAGS['keyring'],36 COMMON_FLAGS['keyring'],
35 COMMON_FLAGS['src'], COMMON_FLAGS['target'],37 COMMON_FLAGS['src'], COMMON_FLAGS['target'],
36 ('filters', {'nargs': '*', 'default': []}),38 COMMON_FLAGS['filters'],
37 ]39 ]
38 },40 },
39 'import': {41 'import': {
@@ -68,8 +70,7 @@ SUBCOMMANDS = {
68 {'help': 'do not copy files, only metadata [TEST_ONLY]',70 {'help': 'do not copy files, only metadata [TEST_ONLY]',
69 'action': 'store_true', 'default': False}),71 'action': 'store_true', 'default': False}),
70 COMMON_FLAGS['src'], COMMON_FLAGS['target'],72 COMMON_FLAGS['src'], COMMON_FLAGS['target'],
71 ('version', {'help': 'the version_id to promote.'}),73 COMMON_FLAGS['version'], COMMON_FLAGS['filters'],
72 ('filters', {'nargs': '+', 'default': []}),
73 ]74 ]
74 },75 },
75 'clean-md': {76 'clean-md': {
@@ -78,7 +79,7 @@ SUBCOMMANDS = {
78 COMMON_FLAGS['dry-run'], COMMON_FLAGS['no-sign'],79 COMMON_FLAGS['dry-run'], COMMON_FLAGS['no-sign'],
79 COMMON_FLAGS['keyring'],80 COMMON_FLAGS['keyring'],
80 ('max', {'type': int}), ('target', {}),81 ('max', {'type': int}), ('target', {}),
81 ('filters', {'nargs': '*', 'default': []}),82 COMMON_FLAGS['filters'],
82 ]83 ]
83 },84 },
84 'find-orphans': {85 'find-orphans': {
@@ -106,6 +107,24 @@ SUBCOMMANDS = {
106 COMMON_FLAGS['data_d'], COMMON_FLAGS['no-sign'],107 COMMON_FLAGS['data_d'], COMMON_FLAGS['no-sign'],
107 ],108 ],
108 },109 },
110 'remove-version': {
111 'help': 'Remove a version from a product',
112 'opts': [
113 COMMON_FLAGS['dry-run'], COMMON_FLAGS['no-sign'],
114 COMMON_FLAGS['keyring'], COMMON_FLAGS['data_d'],
115 COMMON_FLAGS['version'], COMMON_FLAGS['filters'],
116 ],
117 },
118 'copy-version': {
119 'help': 'Copy a version of a product to a new version',
120 'opts': [
121 COMMON_FLAGS['dry-run'], COMMON_FLAGS['no-sign'],
122 COMMON_FLAGS['keyring'], COMMON_FLAGS['data_d'],
123 ('from_version', {'help': 'the version_id to copy from.'}),
124 ('to_version', {'help': 'the version_id to copy to.'}),
125 COMMON_FLAGS['filters']
126 ],
127 },
109}128}
110129
111# vi: ts=4 expandtab syntax=python130# vi: ts=4 expandtab syntax=python
diff --git a/meph2/commands/meph2_util.py b/meph2/commands/meph2_util.py
index 4109634..0b114dc 100755
--- a/meph2/commands/meph2_util.py
+++ b/meph2/commands/meph2_util.py
@@ -403,6 +403,71 @@ def main_sign(args):
403 return 0403 return 0
404404
405405
406def main_remove_version(args):
407 filter_list = filters.get_filters(args.filters)
408 product_streams = util.load_product_streams(args.data_d)
409 resign = False
410
411 for product_stream in product_streams:
412 product_stream_path = os.path.join(args.data_d, product_stream)
413 content = util.load_content(product_stream_path)
414 products = content['products']
415 write_stream = False
416 for product, data in products.items():
417 if (
418 filters.filter_dict(filter_list, data) and
419 args.version in data['versions']):
420 print('Removing %s from %s' % (args.version, product))
421 if not args.dry_run:
422 del data['versions'][args.version]
423 resign = write_stream = True
424 if write_stream:
425 with open(product_stream_path, 'wb') as f:
426 f.write(util.dump_data(content).strip())
427 if resign:
428 util.gen_index_and_sign(args.data_d, not args.no_sign)
429 return 0
430
431
432def main_copy_version(args):
433 filter_list = filters.get_filters(args.filters)
434 product_streams = util.load_product_streams(args.data_d)
435 resign = False
436
437 for product_stream in product_streams:
438 product_stream_path = os.path.join(args.data_d, product_stream)
439 content = util.load_content(product_stream_path)
440 products = content['products']
441 write_stream = False
442 for product, data in products.items():
443 if (
444 filters.filter_dict(filter_list, data) and
445 args.from_version in data['versions']):
446 print('Copying %s to %s in %s' % (
447 args.from_version, args.to_version, product))
448 if not args.dry_run:
449 new_version = copy.deepcopy(
450 data['versions'][args.from_version])
451 for item in new_version['items'].values():
452 old_path = os.path.join(args.data_d, item['path'])
453 item['path'] = item['path'].replace(
454 args.from_version, args.to_version)
455 new_path = os.path.join(args.data_d, item['path'])
456 if not os.path.exists(new_path):
457 os.makedirs(
458 os.path.dirname(new_path), exist_ok=True)
459 shutil.copy(
460 old_path, new_path, follow_symlinks=False)
461 data['versions'][args.to_version] = new_version
462 resign = write_stream = True
463 if write_stream:
464 with open(product_stream_path, 'wb') as f:
465 f.write(util.dump_data(content).strip())
466 if resign:
467 util.gen_index_and_sign(args.data_d, not args.no_sign)
468 return 0
469
470
406def main_import(args):471def main_import(args):
407 """meph2-util import wraps the preferred command 'meph2-import'.472 """meph2-util import wraps the preferred command 'meph2-import'.
408473
diff --git a/meph2/util.py b/meph2/util.py
index 017e5ef..f990eb9 100644
--- a/meph2/util.py
+++ b/meph2/util.py
@@ -264,15 +264,19 @@ def dump_data(data, end_cr=True):
264 return bytestr264 return bytestr
265265
266266
267def load_content(path):
268 if not os.path.exists(path):
269 return {}
270 with scontentsource.UrlContentSource(path) as tcs:
271 return sutil.load_content(tcs.read())
272
273
267def load_products(path, product_streams):274def load_products(path, product_streams):
268 products = {}275 products = {}
269 for product_stream in product_streams:276 for product_stream in product_streams:
270 product_stream_path = os.path.join(path, product_stream)277 product_stream_path = os.path.join(path, product_stream)
271 if os.path.exists(product_stream_path):278 product_listing = load_content(product_stream_path)
272 with scontentsource.UrlContentSource(279 products.update(product_listing['products'])
273 product_stream_path) as tcs:
274 product_listing = sutil.load_content(tcs.read())
275 products.update(product_listing['products'])
276 return products280 return products
277281
278282

Subscribers

People subscribed via source and target branches