Merge ~smoser/curtin:feature/bug-1746348-fsimage-support into curtin:master

Proposed by Scott Moser
Status: Merged
Merge reported by: Ryan Harper
Merged at revision: f0f1bcf227b1022c361f53b1b27fb35c0b802b93
Proposed branch: ~smoser/curtin:feature/bug-1746348-fsimage-support
Merge into: curtin:master
Diff against target: 583 lines (+256/-81)
8 files modified
curtin/commands/extract.py (+59/-31)
curtin/url_helper.py (+94/-4)
curtin/util.py (+8/-1)
doc/topics/config.rst (+3/-0)
tests/vmtests/__init__.py (+49/-35)
tests/vmtests/releases.py (+13/-0)
tests/vmtests/test_uefi_basic.py (+14/-0)
tools/launch (+16/-10)
Reviewer Review Type Date Requested Status
Lee Trager (community) Approve
Server Team CI bot continuous-integration Approve
Ryan Harper (community) Approve
Review via email: mp+336872@code.launchpad.net

Commit message

Add support for installing sources that are a filesystem image.

This adds support for sources that are a 'filesystem image' via local
file path or via http or https. It will work for any filesystem
image type that is mountable by the running kernel.

The end result is that this works:
  sudo ./bin/curtin install --config=my.yaml \
    http://cloud-images.ubuntu.com/..../some.squashfs

To accomplish this:
 a.) if the source is a http/https url, then download it to temporary file.
 b.) mount the file loopback (mount -o loop,ro).
 c.) copy the contents to the target
 d.) unmount
 e.) if downloaded remove the file.

In order to do this we needed some mechanism for downloading a url
in pieces rather than all into memory as 'geturl' uses. So I've
added UrlReader and 'download'.

Also here, a fix for the default headers to include the actual curtin
version per version.version_string() rather than '.1'.

MAAS 2.3 and 2.4 plan to use this functionality to install Ubuntu 12.04
while booted in 16.04. There are two tests added that excercise that
path that will be enabled later. To support testing hwe here,
also have added explicit setting of target_kernel_package in the class.

LP: #1746348

Description of the change

I was able to test with the proposed precise squashfs stream:
 https://code.launchpad.net/~smoser/curtin/+git/curtin/+merge/337080

$ export IMAGE_DIR=/tmp/my-image-dir
$ python3 tests/vmtests/image_sync.py mirror --max=1 --verbose --arches=amd64 --source=http://people.canonical.com/~rcj/mpmirror/streams/v1/index.sjson --keyring=/usr/share/keyrings/ubuntu-cloudimage-keyring.gpg "$IMAGE_DIR" "release~(precise|xenial)"
$ ./tools/jenkins-runner tests/vmtests/test_uefi_basic.py

To post a comment you must log in.
Revision history for this message
Scott Moser (smoser) wrote :

no tests yet, but tested installation using both url and file from xenial squashfs image on http://cloud-images.ubuntu.com as described in
https://gist.github.com/smoser/375123ef1ef098be23cc856a5772c5c8

Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Approve (continuous-integration)
fb9128f... by Scott Moser

tools/launch: support --publish to a subdir

This allows:
  --publish=/my/path/to/image:somedir/somefile

Before you could only publish to the top level PUBDIR.
That issue would be exposed if you did:
  --publish=/path/to/boot/squashfs
  --publish=/path/to/target/squashfs

the second would silently override the first.
It still does, but now you can put into subdirs.

88bc44c... by Scott Moser

vmtest: Enablement of precise installations using xenial boot.

This works a bit on vmtest base class to more cleanly support
installations of one OS while booted into another.

Also enables some one test of Precise to verify.

Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Approve (continuous-integration)
Revision history for this message
Scott Moser (smoser) wrote :

The goal of this merge is to enable installation of a Precise squashfs
while booting either trusty or xenial. Since we do not currently have
a precise squashfs I instead worked on installing a trusty squashfs image
while booted in xenial.

The test I re-enabled is called PreciseUefiTestBasic.

It currently installs fine but fails boot.
The issue can be seen in the boot log at
  http://paste.ubuntu.com/26498529/ .
| [ 3.055946] SGI XFS with ACLs, security attributes, realtime, large block/inode numbers, no debug enabled
| [ 3.062047] XFS (vda3): Version 5 superblock detected. This kernel has EXPERIMENTAL support enabled!
| [ 3.062047] Use of these features in this kernel is at your own risk!
| [ 3.063697] XFS (vda3): Superblock has unknown read-only compatible features (0x1) enabled.
| [ 3.064568] XFS (vda3): Attempted to mount read-only compatible filesystem read-write.
| [ 3.064568] Filesystem can only be safely mounted read only.
| [ 3.068021] XFS (vda3): SB validate failed with error 22.

The issue here I believe is that Xenial's mkfs.xfs enabled some features
by default that were not supported in the Trusty (3.13) kernel. So the
install works fine, but when the target tries to boot it fails. We are
shielded from such mismatch issues currently as we booting and installing
the same release, so the default mkfs options match.

I can't be sure yet whether or not this would be a problem when booting
trusty and installing precise, but it is a general issue that could
definitely show itself.

To validate, the following "fixes" the boot problem:

--- a/examples/tests/uefi_basic.yaml
+++ b/examples/tests/uefi_basic.yaml
@@ -49,7 +49,7 @@ storage:
   - id: id_home_format
     label: home
     type: format
- fstype: xfs
+ fstype: ext4
     volume: id_disk0_part3

Revision history for this message
Scott Moser (smoser) wrote :

another option...
 http://paste.ubuntu.com/26498603/

basically just allowing the storage_config to pass an array of options taht will go into the mkfs command.

that also produced actually booting system.

a85f8b7... by Scott Moser

move downloading to a url_helper.download method that adds speed info.

Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Approve (continuous-integration)
Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Approve (continuous-integration)
Revision history for this message
Scott Moser (smoser) wrote :

OK, I'm happy with the current state of this branch for landing.
It doesn't handle the issue discussed above, but that is really
not related to support for installing a filesystem image.

The 'download' currently gives no status, but at the end does have a nice
message like:

[ 51.934046] cloud-init[1254]: Downloaded 287375360 bytes from http://10.245.1
68.20:35549/target/squashfs to /tmp/tmpgqud66pl.img in 1.57s (174.35Mbps)

Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Approve (continuous-integration)
Revision history for this message
Chad Smith (chad.smith) :
Revision history for this message
Chad Smith (chad.smith) :
Revision history for this message
Ryan Harper (raharper) wrote :

The extract logic currently is like:

dd-
  continue

cp://
  copy_to_target

isfile:
  extract_root_tgz_file

file://
  extract_root_tgz_file

http[s]://
  extract_root_tgz_url

Can we just remove _tgz_ from the method name,
and add the squashfs handling in there? Something like

  extract_root_from_file
  extract_root_from_url

Then we could push some of the logic for source and format hanlding into those two functions.
Something like this:

    if source['type'].startswith('dd-'):
        continue
    if source['uri'].startswith("cp://"):
        copy_to_target(source['uri'], target)
    elif os.path.isfile(source['uri']) or source['uri'].startswith("file://"):
        extract_root_from_file(source['uri'], fmt=source['type'],
                                target=target)
    elif (source['uri'].startswith("http://") or
          source['uri'].startswith("https://")):
        extract_root_from_url(source['uri'], fmt=source['type'],
                              target=target)
    else:
        raise TypeError("do not know how to extract '%s'" % source['uri'])

def extract_root_from_file(source, fmt=None, target=None):
    if source.startswith('file://'):
        source = source[len('file://'):]
    if fmt == "fsimage":
        return extract_root_fsimage(source, target)
    else:
        curtin.util.subp(args=['tar', '-C', target] +
                         tar_xattr_opts() +
                         ['-Sxpzf', source, '--numeric-owner'])

def extract_root_from_url(source, fmt=None, target=None):
    if fmt == "fsimage":
        return extract_root_fsimage_url(source, target)
    else:
        curtin.util.subp(args=['sh', '-cf',
                           ('wget "$1" --progress=dot:mega -O - |'
                            'smtar -C "$2" ' + ' '.join(tar_xattr_opts()) +
                            ' ' + '-Sxpf - --numeric-owner'),
                           '--', source, target])

Revision history for this message
Scott Moser (smoser) :
Revision history for this message
Scott Moser (smoser) :
3541263... by Scott Moser

address some feedback from chad.

20e2684... by Scott Moser

explicitly set some things in PreciseBase

Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Approve (continuous-integration)
Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Approve (continuous-integration)
8af54f1... by Scott Moser

add returns to extract_root_tgz calls

Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Needs Fixing (continuous-integration)
dd224f0... by Scott Moser

support specifying kernel package.

The get_kernel_config didn't work for precise kernel names.
  linux-generic .. linux-generic-lts-trusty
So just specify them manually.

Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Approve (continuous-integration)
1613ee0... by Scott Moser

address feedback / simplify

e900c89... by Scott Moser

add urls that are just path, not file://<path> .

Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Approve (continuous-integration)
Revision history for this message
Scott Moser (smoser) wrote :

I've addressed the feedback i think. i haven't tested, but plealse re-review.

f0525d8... by Scott Moser

disable PreciseUefi tests. Later enable when they work.

Revision history for this message
Ryan Harper (raharper) wrote :

This looks great, just a few questions/comments in line.

Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Approve (continuous-integration)
Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Approve (continuous-integration)
Revision history for this message
Lee Trager (ltrager) wrote :

The Precise image currently in the stream is a gzip compressed ext4 image. It seems this supports SquashFS files by mounting them and coping the files. Why not support mounting the Precise ext4 image so the streams don't have to be modified to add a SquashFS image?

If Curtin is supporting SquashFS why not use unsquashfs and skip mounting it?

review: Needs Information
Revision history for this message
Scott Moser (smoser) wrote :

> The Precise image currently in the stream is a gzip compressed ext4 image. It
> seems this supports SquashFS files by mounting them and coping the files. Why
> not support mounting the Precise ext4 image so the streams don't have to be
> modified to add a SquashFS image?
>

We could support that, but I'd rather not. We'd have to download the
image (possibly decompressing as we read) and then mount it. That image
is 1.4G so we'd then be using 1.4G of tmpfs space. It seems
generally superior to make the squashfs image availalbe in the
stream.. Then we have
 - consistency for all releases in v3 stream
 - less memory required

> If Curtin is supporting SquashFS why not use unsquashfs and skip
> mounting it?

a.) That would certainly be one path, however it then requires a package
to do what the kernel can do just as well. We are guaranteed root to
run curtin so this seems more straight forward. We also then are able
to re-use the 'cp://' handler as we're mounting and copying from
an existing directory.

b.) the non-unsquashfs path supports *any* filesystem image that the
kernel booted can mount.

What is wrong with mounting an image?

If squashfs was "streamable" then you'd have a much better argument.
But to my knowledge it is not. Ie, you have to download the whole
image before you can start 'unsquashfs'. If we could
 wget <url> -O - | unsquashfs --cd /target/dir

then that'd be nice. I dont think the squashfs filesystem format
would really allow it though.

Revision history for this message
Scott Moser (smoser) wrote :

I put up
 https://code.launchpad.net/~smoser/curtin/+git/curtin/+merge/337192
which removes xfs from the uefi basic test.
that will solve/avoid the problems i've discussed above.

the goal is to later enable a 'FileSystemStress' sort of test that
would verify support for each filesystem type and could be run
with precise installs from xenial.

fe3fe29... by Scott Moser

disable PreciseUefi tests. Later enable when they work.

Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Approve (continuous-integration)
Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Approve (continuous-integration)
1e53905... by Scott Moser

change installation of PreciseBase to use precise.

e7d6656... by Scott Moser

fix class name

7bb6fa1... by Scott Moser

sanitize_source: do not ever call 'squashfs' an image type

Revision history for this message
Ryan Harper (raharper) wrote :

I think the commit message should clarify fsimage means squasfs-based image; especially regarding the comment about other image types.

If we want to keep fsimage as generic (it *could* be used to install the ext4 root-image) then maybe we should add a test (even if that's not what we'd use in practice). Thoughts?

Some more questions and items inline.

Revision history for this message
Scott Moser (smoser) wrote :

fsimage does not mean 'squashfs-based image'.
it means filesystem image.

I'd like to keep it 'fsimage' as that is what is implemented.

Revision history for this message
Chad Smith (chad.smith) :
e685e5a... by Scott Moser

mention other file system types than squash

51b13c9... by Scott Moser

avoid valueerror if precise was not in the list

31c1664... by Scott Moser

update download per feedback.

e4cf526... by Scott Moser

raise OSerror rather than runtimeerror.

f0f1bcf... by Scott Moser

address suggested change for _path_from_file usage.

Revision history for this message
Ryan Harper (raharper) wrote :

One more question on the curtin.util source sanitize w.r.t auto converting squash to fsimage types.

Revision history for this message
Scott Moser (smoser) :
Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Approve (continuous-integration)
Revision history for this message
Ryan Harper (raharper) wrote :

I'm ok with leaving it in.

review: Approve
Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Approve (continuous-integration)
Revision history for this message
Lee Trager (ltrager) wrote :

Thanks for the explanation!

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
diff --git a/curtin/commands/extract.py b/curtin/commands/extract.py
index b9f1baf..69a9d18 100644
--- a/curtin/commands/extract.py
+++ b/curtin/commands/extract.py
@@ -2,12 +2,14 @@
22
3import os3import os
4import sys4import sys
5import tempfile
56
6import curtin.config7import curtin.config
7from curtin.log import LOG8from curtin.log import LOG
8import curtin.util9from curtin import util
9from curtin.futil import write_files10from curtin.futil import write_files
10from curtin.reporter import events11from curtin.reporter import events
12from curtin import url_helper
1113
12from . import populate_one_subcmd14from . import populate_one_subcmd
1315
@@ -31,27 +33,56 @@ def tar_xattr_opts(cmd=None):
31 if isinstance(cmd, str):33 if isinstance(cmd, str):
32 cmd = [cmd]34 cmd = [cmd]
3335
34 (out, _err) = curtin.util.subp(cmd + ['--help'], capture=True)36 (out, _err) = util.subp(cmd + ['--help'], capture=True)
3537
36 if "xattr" in out:38 if "xattr" in out:
37 return ['--xattrs', '--xattrs-include=*']39 return ['--xattrs', '--xattrs-include=*']
38 return []40 return []
3941
4042
41def extract_root_tgz_url(source, target):43def extract_root_tgz_url(url, target):
42 # extract a -root.tar.gz url in the 'target' directory44 # extract a -root.tar.gz url in the 'target' directory
43 #45 path = _path_from_file_url(url)
44 # Uses smtar to avoid specifying the compression type46 if path != url or os.path.isfile(path):
45 curtin.util.subp(args=['sh', '-cf',47 util.subp(args=['tar', '-C', target] + tar_xattr_opts() +
46 ('wget "$1" --progress=dot:mega -O - |'48 ['-Sxpzf', path, '--numeric-owner'])
47 'smtar -C "$2" ' + ' '.join(tar_xattr_opts()) +49 return
48 ' ' + '-Sxpf - --numeric-owner'),
49 '--', source, target])
50
5150
52def extract_root_tgz_file(source, target):51 # Uses smtar to avoid specifying the compression type
53 curtin.util.subp(args=['tar', '-C', target] +52 util.subp(args=['sh', '-cf',
54 tar_xattr_opts() + ['-Sxpzf', source, '--numeric-owner'])53 ('wget "$1" --progress=dot:mega -O - |'
54 'smtar -C "$2" ' + ' '.join(tar_xattr_opts()) +
55 ' ' + '-Sxpf - --numeric-owner'),
56 '--', url, target])
57
58
59def extract_root_fsimage_url(url, target):
60 path = _path_from_file_url(url)
61 if path != url or os.path.isfile(path):
62 return _extract_root_fsimage(path(url), target)
63
64 wfp = tempfile.NamedTemporaryFile(suffix=".img", delete=False)
65 wfp.close()
66 try:
67 url_helper.download(url, wfp.name)
68 return _extract_root_fsimage(wfp.name, target)
69 finally:
70 os.unlink(wfp.name)
71
72
73def _extract_root_fsimage(path, target):
74 mp = tempfile.mkdtemp()
75 try:
76 util.subp(['mount', '-o', 'loop,ro', path, mp], capture=True)
77 except util.ProcessExecutionError as e:
78 LOG.error("Failed to mount '%s' for extraction: %s", path, e)
79 os.rmdir(mp)
80 raise e
81 try:
82 return copy_to_target(mp, target)
83 finally:
84 util.subp(['umount', mp])
85 os.rmdir(mp)
5586
5687
57def copy_to_target(source, target):88def copy_to_target(source, target):
@@ -59,17 +90,21 @@ def copy_to_target(source, target):
59 source = source[5:]90 source = source[5:]
60 source = os.path.abspath(source)91 source = os.path.abspath(source)
6192
62 curtin.util.subp(args=['sh', '-c',93 util.subp(args=['sh', '-c',
63 ('mkdir -p "$2" && cd "$2" && '94 ('mkdir -p "$2" && cd "$2" && '
64 'rsync -aXHAS --one-file-system "$1/" .'),95 'rsync -aXHAS --one-file-system "$1/" .'),
65 '--', source, target])96 '--', source, target])
97
98
99def _path_from_file_url(url):
100 return url[7:] if url.startswith("file://") else url
66101
67102
68def extract(args):103def extract(args):
69 if not args.target:104 if not args.target:
70 raise ValueError("Target must be defined or set in environment")105 raise ValueError("Target must be defined or set in environment")
71106
72 state = curtin.util.load_command_environment()107 state = util.load_command_environment()
73 cfg = curtin.config.load_command_config(args, state)108 cfg = curtin.config.load_command_config(args, state)
74109
75 sources = args.sources110 sources = args.sources
@@ -82,6 +117,8 @@ def extract(args):
82 if isinstance(sources, dict):117 if isinstance(sources, dict):
83 sources = [sources[k] for k in sorted(sources.keys())]118 sources = [sources[k] for k in sorted(sources.keys())]
84119
120 sources = [util.sanitize_source(s) for s in sources]
121
85 LOG.debug("Installing sources: %s to target at %s" % (sources, target))122 LOG.debug("Installing sources: %s to target at %s" % (sources, target))
86 stack_prefix = state.get('report_stack_prefix', '')123 stack_prefix = state.get('report_stack_prefix', '')
87124
@@ -94,19 +131,10 @@ def extract(args):
94 continue131 continue
95 if source['uri'].startswith("cp://"):132 if source['uri'].startswith("cp://"):
96 copy_to_target(source['uri'], target)133 copy_to_target(source['uri'], target)
97 elif os.path.isfile(source['uri']):134 elif source['type'] == "fsimage":
98 extract_root_tgz_file(source['uri'], target)135 extract_root_fsimage_url(source['uri'], target=target)
99 elif source['uri'].startswith("file://"):
100 extract_root_tgz_file(
101 source['uri'][len("file://"):],
102 target)
103 elif (source['uri'].startswith("http://") or
104 source['uri'].startswith("https://")):
105 extract_root_tgz_url(source['uri'], target)
106 else:136 else:
107 raise TypeError(137 extract_root_tgz_url(source['uri'], target=target)
108 "do not know how to extract '%s'" %
109 source['uri'])
110138
111 if cfg.get('write_files'):139 if cfg.get('write_files'):
112 LOG.info("Applying write_files from config.")140 LOG.info("Applying write_files from config.")
diff --git a/curtin/url_helper.py b/curtin/url_helper.py
index 75ea633..0221ad5 100644
--- a/curtin/url_helper.py
+++ b/curtin/url_helper.py
@@ -9,6 +9,8 @@ import time
9import uuid9import uuid
10from functools import partial10from functools import partial
1111
12from curtin import version
13
12try:14try:
13 from urllib import request as _u_re # pylint: disable=no-name-in-module15 from urllib import request as _u_re # pylint: disable=no-name-in-module
14 from urllib import error as _u_e # pylint: disable=no-name-in-module16 from urllib import error as _u_e # pylint: disable=no-name-in-module
@@ -25,6 +27,8 @@ from .log import LOG
2527
26error = urllib_error28error = urllib_error
2729
30DEFAULT_HEADERS = {'User-Agent': 'Curtin/' + version.version_string()}
31
2832
29class _ReRaisedException(Exception):33class _ReRaisedException(Exception):
30 exc = None34 exc = None
@@ -34,14 +38,100 @@ class _ReRaisedException(Exception):
34 self.exc = exc38 self.exc = exc
3539
3640
37def _geturl(url, headers=None, headers_cb=None, exception_cb=None, data=None):41class UrlReader(object):
38 def_headers = {'User-Agent': 'Curtin/0.1'}42 fp = None
43
44 def __init__(self, url, headers=None, data=None):
45 headers = _get_headers(headers)
46 self.url = url
47 try:
48 req = urllib_request.Request(url=url, data=data, headers=headers)
49 self.fp = urllib_request.urlopen(req)
50 except urllib_error.HTTPError as exc:
51 raise UrlError(exc, code=exc.code, headers=exc.headers, url=url,
52 reason=exc.reason)
53 except Exception as exc:
54 raise UrlError(exc, code=None, headers=None, url=url,
55 reason="unknown")
56
57 self.info = self.fp.info()
58 self.size = self.info.get('content-length', -1)
59
60 def read(self, buflen):
61 try:
62 return self.fp.read(buflen)
63 except urllib_error.HTTPError as exc:
64 raise UrlError(exc, code=exc.code, headers=exc.headers,
65 url=self.url, reason=exc.reason)
66 except Exception as exc:
67 raise UrlError(exc, code=None, headers=None, url=self.url,
68 reason="unknown")
69
70 def close(self):
71 if not self.fp:
72 return
73 try:
74 self.fp.close()
75 finally:
76 self.fp = None
3977
78 def __enter__(self):
79 return self
80
81 def __exit__(self, etype, value, trace):
82 self.close()
83
84
85def download(url, path, reporthook=None, data=None):
86 """Download url to path.
87
88 reporthook is compatible with py3 urllib.request.urlretrieve.
89 urlretrieve does not exist in py2."""
90
91 buflen = 8192
92 wfp = open(path, "wb")
93
94 try:
95 buf = None
96 blocknum = 0
97 fsize = 0
98 start = time.time()
99 with UrlReader(url) as rfp:
100 if reporthook:
101 reporthook(blocknum, buflen, rfp.size)
102
103 while True:
104 buf = rfp.read(buflen)
105 if not buf:
106 break
107 blocknum += 1
108 if reporthook:
109 reporthook(blocknum, buflen, rfp.size)
110 rlen = len(buf)
111 wlen = wfp.write(buf)
112 if rlen != wlen:
113 raise OSError(
114 "Short write to %s. Tried write of %d bytes "
115 "but wrote only %d" % (path, rlen, wlen))
116 fsize += rlen
117 timedelta = time.time() - start
118 LOG.debug("Downloaded %d bytes from %s to %s in %.2fs (%.2fMbps)",
119 fsize, url, path, timedelta, fsize / timedelta / 1024 / 1024)
120 return path, rfp.info
121 finally:
122 wfp.close()
123
124
125def _get_headers(headers=None):
126 allheaders = DEFAULT_HEADERS.copy()
40 if headers is not None:127 if headers is not None:
41 def_headers.update(headers)128 allheaders.update(headers)
129 return allheaders
42130
43 headers = def_headers
44131
132def _geturl(url, headers=None, headers_cb=None, exception_cb=None, data=None):
133
134 headers = _get_headers(headers)
45 if headers_cb:135 if headers_cb:
46 headers.update(headers_cb(url))136 headers.update(headers_cb(url))
47137
diff --git a/curtin/util.py b/curtin/util.py
index d343339..12a5446 100644
--- a/curtin/util.py
+++ b/curtin/util.py
@@ -1084,13 +1084,20 @@ def sanitize_source(source):
1084 # already sanitized?1084 # already sanitized?
1085 return source1085 return source
1086 supported = ['tgz', 'dd-tgz', 'dd-tbz', 'dd-txz', 'dd-tar', 'dd-bz2',1086 supported = ['tgz', 'dd-tgz', 'dd-tbz', 'dd-txz', 'dd-tar', 'dd-bz2',
1087 'dd-gz', 'dd-xz', 'dd-raw']1087 'dd-gz', 'dd-xz', 'dd-raw', 'fsimage']
1088 deftype = 'tgz'1088 deftype = 'tgz'
1089 for i in supported:1089 for i in supported:
1090 prefix = i + ":"1090 prefix = i + ":"
1091 if source.startswith(prefix):1091 if source.startswith(prefix):
1092 return {'type': i, 'uri': source[len(prefix):]}1092 return {'type': i, 'uri': source[len(prefix):]}
10931093
1094 # translate squashfs: to fsimage type.
1095 if source.startswith("squashfs:"):
1096 return {'type': 'fsimage', 'uri': source[len("squashfs:")]}
1097
1098 if source.endswith("squashfs") or source.endswith("squash"):
1099 return {'type': 'fsimage', 'uri': source}
1100
1094 LOG.debug("unknown type for url '%s', assuming type '%s'", source, deftype)1101 LOG.debug("unknown type for url '%s', assuming type '%s'", source, deftype)
1095 # default to tgz for unknown types1102 # default to tgz for unknown types
1096 return {'type': deftype, 'uri': source}1103 return {'type': deftype, 'uri': source}
diff --git a/doc/topics/config.rst b/doc/topics/config.rst
index 0f11b53..fdc524f 100644
--- a/doc/topics/config.rst
+++ b/doc/topics/config.rst
@@ -435,6 +435,9 @@ configures the method used to copy the image to the target system.
435- **cp://**: Use ``rsync`` command to copy source directory to target.435- **cp://**: Use ``rsync`` command to copy source directory to target.
436- **file://**: Use ``tar`` command to extract source to target.436- **file://**: Use ``tar`` command to extract source to target.
437- **http[s]://**: Use ``wget | tar`` commands to extract source to target.437- **http[s]://**: Use ``wget | tar`` commands to extract source to target.
438- **fsimage://**: mount filesystem image and copy contents to target.
439 Local file or url are supported. Filesystem can be any filesystem type
440 mountable by the running kernel.
438441
439**Example Cloud-image**::442**Example Cloud-image**::
440443
diff --git a/tests/vmtests/__init__.py b/tests/vmtests/__init__.py
index f6515af..60882a2 100644
--- a/tests/vmtests/__init__.py
+++ b/tests/vmtests/__init__.py
@@ -392,6 +392,7 @@ class VMBaseClass(TestCase):
392 target_release = None392 target_release = None
393 target_krel = None393 target_krel = None
394 target_ftype = "squashfs"394 target_ftype = "squashfs"
395 target_kernel_package = None
395396
396 _debian_packages = None397 _debian_packages = None
397398
@@ -414,31 +415,32 @@ class VMBaseClass(TestCase):
414 subarch=cls.subarch if cls.subarch else None,415 subarch=cls.subarch if cls.subarch else None,
415 sync=CURTIN_VMTEST_IMAGE_SYNC,416 sync=CURTIN_VMTEST_IMAGE_SYNC,
416 ftypes=('boot-initrd', 'boot-kernel', cls.ephemeral_ftype))417 ftypes=('boot-initrd', 'boot-kernel', cls.ephemeral_ftype))
417 logger.debug("Install Image %s\n, ftypes: %s\n",418
418 eph_img_verstr, ftypes)
419 if not cls.target_krel and cls.krel:419 if not cls.target_krel and cls.krel:
420 cls.target_krel = cls.krel420 cls.target_krel = cls.krel
421421
422 # get local absolute filesystem paths for the OS tarball to be422 tftype = cls.target_ftype
423 # installed423 if tftype in ["root-image.xz"]:
424 if cls.target_ftype in ["vmtest.root-tgz", "squashfs"]:424 logger.info('get-testfiles UC16 hack!')
425 target_img_verstr, found = get_images(425 target_ftypes = {'root-image.xz': UC16_IMAGE}
426 target_img_verstr = "UbuntuCore 16"
427 elif cls.target_release == cls.release:
428 target_ftypes = ftypes.copy()
429 target_img_verstr = eph_img_verstr
430 else:
431 target_img_verstr, target_ftypes = get_images(
426 IMAGE_SRC_URL, IMAGE_DIR,432 IMAGE_SRC_URL, IMAGE_DIR,
427 cls.target_distro if cls.target_distro else cls.distro,433 cls.target_distro if cls.target_distro else cls.distro,
428 cls.target_release if cls.target_release else cls.release,434 cls.target_release if cls.target_release else cls.release,
429 cls.arch, subarch=cls.subarch if cls.subarch else None,435 cls.arch, subarch=cls.subarch if cls.subarch else None,
430 kflavor=cls.kflavor if cls.kflavor else None,436 kflavor=cls.kflavor if cls.kflavor else None,
431 krel=cls.target_krel, sync=CURTIN_VMTEST_IMAGE_SYNC,437 krel=cls.target_krel, sync=CURTIN_VMTEST_IMAGE_SYNC,
432 ftypes=(cls.target_ftype,))438 ftypes=(tftype,))
433 logger.debug("Target Tarball %s\n, ftypes: %s\n",439
434 target_img_verstr, found)440 ftypes["target/%s" % tftype] = target_ftypes[tftype]
435 elif cls.target_ftype in ["root-image.xz"]:441 logger.debug(
436 logger.info('get-testfiles UC16 hack!')442 "Install Image Version = %s\n, Target Image Version = %s\n"
437 found = {'root-image.xz': UC16_IMAGE}443 "ftypes: %s", eph_img_verstr, target_img_verstr, ftypes)
438 target_img_verstr = "UbuntuCore 16"
439 logger.info("Ephemeral Image Version:[%s] Target Image Version:[%s]",
440 eph_img_verstr, target_img_verstr)
441 ftypes.update(found)
442 return ftypes444 return ftypes
443445
444 @classmethod446 @classmethod
@@ -621,6 +623,9 @@ class VMBaseClass(TestCase):
621 @classmethod623 @classmethod
622 def get_kernel_package(cls):624 def get_kernel_package(cls):
623 """ Return the kernel package name for this class """625 """ Return the kernel package name for this class """
626 if cls.target_kernel_package:
627 return cls.target_kernel_package
628
624 package = 'linux-image-' + cls.kflavor629 package = 'linux-image-' + cls.kflavor
625 if cls.subarch is None or cls.subarch.startswith('ga'):630 if cls.subarch is None or cls.subarch.startswith('ga'):
626 return package631 return package
@@ -634,6 +639,9 @@ class VMBaseClass(TestCase):
634639
635 Returns: config dictionary only for hwe, or edge subclass640 Returns: config dictionary only for hwe, or edge subclass
636 """641 """
642 if cls.target_kernel_package:
643 return {'kernel': {'package': cls.target_kernel_package}}
644
637 if cls.subarch is None or cls.subarch.startswith('ga'):645 if cls.subarch is None or cls.subarch.startswith('ga'):
638 return None646 return None
639647
@@ -689,12 +697,12 @@ class VMBaseClass(TestCase):
689 cmd.extend(["--append=" + cls.extra_kern_args])697 cmd.extend(["--append=" + cls.extra_kern_args])
690698
691 ftypes = cls.get_test_files()699 ftypes = cls.get_test_files()
700 root_pubpath = "root/" + cls.ephemeral_ftype
692 # trusty can't yet use root=URL due to LP:#1735046701 # trusty can't yet use root=URL due to LP:#1735046
693 if cls.release in ['trusty']:702 if cls.release in ['trusty']:
694 root_url = "/dev/disk/by-id/virtio-boot-disk"703 root_url = "/dev/disk/by-id/virtio-boot-disk"
695 else:704 else:
696 root_url = "squash:PUBURL/%s" % (705 root_url = "squash:PUBURL/" + root_pubpath
697 os.path.basename(ftypes[cls.ephemeral_ftype]))
698 # configure ephemeral boot environment706 # configure ephemeral boot environment
699 cmd.extend([707 cmd.extend([
700 "--root-arg=root=%s" % root_url,708 "--root-arg=root=%s" % root_url,
@@ -710,16 +718,17 @@ class VMBaseClass(TestCase):
710 cmd.extend(["--append=iscsi_auto"])718 cmd.extend(["--append=iscsi_auto"])
711719
712 # publish the ephemeral image (used in root=URL)720 # publish the ephemeral image (used in root=URL)
713 cmd.append("--publish=%s" % ftypes[cls.ephemeral_ftype])721 cmd.append("--publish=%s:%s" % (ftypes[cls.ephemeral_ftype],
722 root_pubpath))
714 logger.info("Publishing ephemeral image as %s", cmd[-1])723 logger.info("Publishing ephemeral image as %s", cmd[-1])
715 # publish the target image
716 cmd.append("--publish=%s" % ftypes[cls.target_ftype])
717 logger.info("Publishing target image as %s", cmd[-1])
718724
719 # set curtin install source725 # set curtin install source
720 install_src = cls.get_install_source(ftypes)726 install_src, publishes = cls.get_install_source(ftypes)
721
722 logger.info("Curtin install source URI: %s", install_src)727 logger.info("Curtin install source URI: %s", install_src)
728 if len(publishes):
729 cmd.extend(['--publish=%s' % p for p in publishes])
730 logger.info("Publishing install sources: %s",
731 cmd[-len(publishes):])
723732
724 # check for network configuration733 # check for network configuration
725 cls.network_state = curtin_net.parse_net_config(cls.conf_file)734 cls.network_state = curtin_net.parse_net_config(cls.conf_file)
@@ -1082,19 +1091,21 @@ class VMBaseClass(TestCase):
10821091
1083 @classmethod1092 @classmethod
1084 def get_install_source(cls, ftypes):1093 def get_install_source(cls, ftypes):
1085 if cls.target_ftype == 'squashfs':1094 """Return install uri and a list of files needed to be published."""
1086 # If we're installing from squashfs source then direct1095 # if release (install environment) is the same as target
1087 # curtin to install from the read-only undermount1096 # target (thing to install) then install via cp://
1097 if cls.release == cls.target_release:
1088 install_src = "cp:///media/root-ro"1098 install_src = "cp:///media/root-ro"
1089 else:1099 return install_src, []
1090 if cls.target_ftype == 'root-image.xz':
1091 stype = "dd-xz"
1092 else:
1093 stype = "tgz"
1094 src = os.path.basename(ftypes[cls.target_ftype])
1095 install_src = "%s:PUBURL/%s" % (stype, src)
10961100
1097 return install_src1101 # publish the file to target/<ftype>
1102 # and set the install source to <type>:PUBURL/target/<ftype>
1103 ttype2stype = {'root-image.xz': 'dd-xz', 'squashfs': 'fsimage'}
1104 ftype = cls.target_ftype
1105 pubpath = "target/" + ftype
1106 src = "%s:PUBURL/%s" % (ttype2stype.get(ftype, 'tgz'), pubpath)
1107 publishes = [ftypes["target/" + ftype] + ":" + pubpath]
1108 return src, publishes
10981109
1099 @classmethod1110 @classmethod
1100 def tearDownClass(cls):1111 def tearDownClass(cls):
@@ -1711,6 +1722,9 @@ def is_unsupported_ubuntu(release):
1711 elif util.which(udi):1722 elif util.which(udi):
1712 _UNSUPPORTED_UBUNTU = util.subp(1723 _UNSUPPORTED_UBUNTU = util.subp(
1713 [udi, '--unsupported'], capture=True)[0].splitlines()1724 [udi, '--unsupported'], capture=True)[0].splitlines()
1725 # precise ESM support.
1726 if 'precise' in _UNSUPPORTED_UBUNTU:
1727 _UNSUPPORTED_UBUNTU.remove('precise')
1714 else:1728 else:
1715 # no way to tell.1729 # no way to tell.
1716 return None1730 return None
diff --git a/tests/vmtests/releases.py b/tests/vmtests/releases.py
index f365824..7c133db 100644
--- a/tests/vmtests/releases.py
+++ b/tests/vmtests/releases.py
@@ -46,6 +46,17 @@ class _Centos66FromXenialBase(_CentosFromUbuntuBase):
46 target_release = "centos66"46 target_release = "centos66"
4747
4848
49class _PreciseBase(_UbuntuBase):
50 release = "xenial"
51 target_release = "precise"
52 target_distro = "ubuntu"
53 target_ftype = "squashfs"
54
55
56class _PreciseHWET(_PreciseBase):
57 target_kernel_package = 'linux-generic-lts-trusty'
58
59
49class _TrustyBase(_UbuntuBase):60class _TrustyBase(_UbuntuBase):
50 release = "trusty"61 release = "trusty"
5162
@@ -105,6 +116,8 @@ class _BionicBase(_UbuntuBase):
105116
106class _Releases(object):117class _Releases(object):
107 trusty = _TrustyBase118 trusty = _TrustyBase
119 precise = _PreciseBase
120 precise_hwe_t = _PreciseHWET
108 trusty_hwe_u = _TrustyHWEU121 trusty_hwe_u = _TrustyHWEU
109 trusty_hwe_v = _TrustyHWEV122 trusty_hwe_v = _TrustyHWEV
110 trusty_hwe_w = _TrustyHWEW123 trusty_hwe_w = _TrustyHWEW
diff --git a/tests/vmtests/test_uefi_basic.py b/tests/vmtests/test_uefi_basic.py
index 712075c..d6a58eb 100644
--- a/tests/vmtests/test_uefi_basic.py
+++ b/tests/vmtests/test_uefi_basic.py
@@ -78,6 +78,20 @@ class TestBasicAbs(VMBaseClass):
78 self.assertEqual(self.disk_block_size, size)78 self.assertEqual(self.disk_block_size, size)
7979
8080
81class PreciseUefiTestBasic(relbase.precise, TestBasicAbs):
82 __test__ = False
83
84 def test_ptable(self):
85 print("test_ptable does not work for Precise")
86
87 def test_dname(self):
88 print("test_dname does not work for Precise")
89
90
91class PreciseHWETUefiTestBasic(relbase.precise_hwe_t, PreciseUefiTestBasic):
92 __test__ = False
93
94
81class TrustyUefiTestBasic(relbase.trusty, TestBasicAbs):95class TrustyUefiTestBasic(relbase.trusty, TestBasicAbs):
82 __test__ = True96 __test__ = True
8397
diff --git a/tools/launch b/tools/launch
index 179dd82..b273de4 100755
--- a/tools/launch
+++ b/tools/launch
@@ -624,7 +624,12 @@ main() {
624 debug 1 "added $ip to existing no_proxy (${no_proxy})";;624 debug 1 "added $ip to existing no_proxy (${no_proxy})";;
625 esac625 esac
626626
627 local tok tok_split src pub fpath627 start_http "${TEMP_D}" "$ip" "$http_port" "${HTTP_TRIES}" </dev/null ||
628 { error "failed to start http service"; return 1; }
629 http_port=$_RET
630 burl="http://$ip:${http_port}"
631
632 local tok tok_split src pub fpath rdir
628 # tok in pubs looks like file[:pubname]633 # tok in pubs looks like file[:pubname]
629 # link them into the temp dir for publishing634 # link them into the temp dir for publishing
630 for tok in "${pubs[@]}"; do635 for tok in "${pubs[@]}"; do
@@ -633,19 +638,20 @@ main() {
633 pub=${tok_split[1]}638 pub=${tok_split[1]}
634 fpath=$(readlink -f "$src") ||639 fpath=$(readlink -f "$src") ||
635 { error "'$src': failed to get path"; return 1; }640 { error "'$src': failed to get path"; return 1; }
636 if [ -z "$pub" ]; then641 if [ -n "$pub" ]; then
642 rdir=$(dirname "$pub")
643 [ -d "${TEMP_D}/$rdir" ] || mkdir -p "${TEMP_D}/$rdir" || {
644 error "Failed to make <pubdir>/$rdir for publish of $pub";
645 return 1;
646 }
647 else
637 pub="${src##*/}"648 pub="${src##*/}"
638 fi649 fi
639 ln -sf "$fpath" "${TEMP_D}/${pub}"650 ln -sf "$fpath" "${TEMP_D}/${pub}" ||
640 debug 1 "publishing: $fpath to ${TEMP_D}/${pub}"651 { error "failed to link $fpath to <pubdir>/$pub"; return 1; }
652 debug 1 "publishing: $fpath to $burl/${pub}"
641 done653 done
642654
643 start_http "${TEMP_D}" "$ip" "$http_port" "${HTTP_TRIES}" </dev/null ||
644 { error "failed to start http service"; return 1; }
645 http_port=$_RET
646 burl="http://$ip:${http_port}"
647
648
649 local addargs="" f=""655 local addargs="" f=""
650 addargs=( )656 addargs=( )
651 for f in "${addfiles[@]}"; do657 for f in "${addfiles[@]}"; do

Subscribers

People subscribed via source and target branches