Skip to content

Commit

Permalink
Add two subcommands(snap, classic) support in command line.
Browse files Browse the repository at this point in the history
1.Common arguments shared between both subcommands.
2.For backwards compatibility, whenever ubuntu-image is called
  without a subcommand issue a warning and default to 'ubuntu-image snap'.
3.Supported 'ubuntu-image classic' with a bunch of options.
  • Loading branch information
adglkh committed Jul 31, 2017
1 parent 32fb00d commit 23ddd19
Show file tree
Hide file tree
Showing 3 changed files with 185 additions and 33 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Expand Up @@ -11,3 +11,5 @@ stage
coverage.xml
diffcov.html
demo/model.assertion
*.model
.tox
212 changes: 180 additions & 32 deletions ubuntu_image/__main__.py
@@ -1,6 +1,7 @@
"""Allows the package to be run with `python3 -m ubuntu_image`."""

import os
import re
import sys
import logging
import argparse
Expand All @@ -12,6 +13,7 @@
from ubuntu_image.helpers import as_size
from ubuntu_image.i18n import _
from ubuntu_image.parser import GadgetSpecificationError
from subprocess import Popen, PIPE


_logger = logging.getLogger('ubuntu-image')
Expand All @@ -20,6 +22,46 @@
PROGRAM = 'ubuntu-image'


class SimpleHelpFormatter(argparse.HelpFormatter):
def add_usage(self, usage, actions, groups, prefix=None):
# only show main usage when no subcommand is provided.
if prefix is None:
prefix = 'Usage: '
if len(actions) != 0:
usage="""
{prog} [OPTIONS] COMMAND [ARGS]...
{prog} COMMAND --help """. format(prog=PROGRAM)
else:
usage="""
{prog} [OPTIONS] """. format(prog=PROGRAM)

return super(SimpleHelpFormatter, self).add_usage(
usage, actions, groups, prefix)
def _format_action(self, action):
if type(action) == argparse._SubParsersAction:
# calculate the subcommand max length
subactions = action._get_subactions()
invocations = [self._format_action_invocation(a) for a in subactions]
self._subcommand_max_length = max(len(i) for i in invocations)

if type(action) == argparse._SubParsersAction._ChoicesPseudoAction:
# format subcommand help line
subcommand = self._format_action_invocation(action)
help_text = ""
if action.help:
help_text = self._expand_help(action)
return " {:{width}}\t\t{} \n".format(subcommand, help_text,
width=self._subcommand_max_length)
elif type(action) == argparse._SubParsersAction:
# eliminate subcommand choices line {cmd1, cmd2}
msg = ''
for subaction in action._get_subactions():
msg += self._format_action(subaction)
return msg
else:
return super(SimpleHelpFormatter, self)._format_action(action)


class SizeAction(argparse.Action):
def __call__(self, parser, namespace, values, option_string=None):
sizes = {}
Expand Down Expand Up @@ -56,22 +98,71 @@ def __call__(self, parser, namespace, values, option_string=None):
# For display purposes.
namespace.given_image_size = values

def get_host_arch():
process = Popen(["dpkg", "--print-architecture"], stdout=PIPE)
(output, err) = process.communicate()
exit_code = process.wait()
return output.decode('ascii').strip() if exit_code == 0 else None

def get_modified_arguments(self, default_subcommand, argv):
subparser_found = False
for arg in argv:
# skip global help option and state machine resume option, no need
# to provide model_assertion file if -r option is not specified

if arg in ['-h', '--help',
'-r', '--resume']:
break
else:
all_args=' '.join(argv)
for x in self._subparsers._actions:
if not isinstance(x, argparse._SubParsersAction):
continue
for sp_name in x._name_parser_map.keys():
if sp_name in argv:
subparser_found = True # if default subcommand is provided.
if not subparser_found:
print('Warning: for backwards compatibility, `ubuntu-image` fallbacks to '
'`ubuntu-image snap` if no subcommand is provided',
file=sys.stderr)

# re-arrange snap specific args for backwards compatibility.
snap_args_pattern=r'(?:--channel|-c|--extra-snaps|--cloud-init)\s{1}\S*'
match_args=re.findall(snap_args_pattern, all_args)
snap_args = [arg for arg_pair in match_args for arg in arg_pair.split()]

# remove match args and only keep global options.
all_args=re.sub(snap_args_pattern, '', all_args)

# and insert 'snap' subcommand at proper position.
# put snap specific args after `snap` subcommand.
new_argv = all_args.split()
insert_pos = len(new_argv)
new_argv.insert(insert_pos - 1, default_subcommand)
new_argv[insert_pos:insert_pos] = snap_args

return new_argv
return argv

def parseargs(argv=None):
parser = argparse.ArgumentParser(
prog=PROGRAM,
description=_('Generate a bootable disk image.'),
)
formatter_class=SimpleHelpFormatter)

parser.add_argument(
'--version', action='version',
version='{} {}'.format(PROGRAM, __version__))
# Common options.

# create two subcommands, "snap" and "classic"
subparser = parser.add_subparsers(title=_('Command'), dest='cmd')
snap_cmd = subparser.add_parser('snap',
help=_("""Create snap-based Ubuntu Core image."""))
classic_cmd = subparser.add_parser('classic',
help=_("""Create debian-based Ubuntu Classic image."""))
argv = parser.get_modified_arguments('snap', argv)

common_group = parser.add_argument_group(_('Common options'))
common_group.add_argument(
'model_assertion', nargs='?',
help=_("""Path to the model assertion file. This argument must be
given unless the state machine is being resumed, in which case it
cannot be given."""))
common_group.add_argument(
'-d', '--debug',
default=False, action='store_true',
Expand Down Expand Up @@ -107,24 +198,7 @@ def parseargs(argv=None):
disk image file. If not given, the image will be put in a file called
disk.img in the working directory (in which case, you probably want to
specify -w)."""))
# Snap-based image options.
snap_group = parser.add_argument_group(
_('Image contents options'),
_("""Additional options for defining the contents of snap-based
images."""))
snap_group.add_argument(
'--extra-snaps',
default=None, action='append',
help=_("""Extra snaps to install. This is passed through to `snap
prepare-image`."""))
snap_group.add_argument(
'--cloud-init',
default=None, metavar='USER-DATA-FILE',
help=_('cloud-config data to be copied to the image'))
snap_group.add_argument(
'-c', '--channel',
default=None,
help=_('The snap channel to use'))

# State machine options.
inclusive_state_group = parser.add_argument_group(
_('State machine options'),
Expand Down Expand Up @@ -158,15 +232,80 @@ def parseargs(argv=None):
default=False, action='store_true',
help=_("""Continue the state machine from the previously saved state.
It is an error if there is no previous state."""))

# Snap-based image options.
snap_cmd.add_argument(
'model_assertion', nargs='?',
help=_("""Path to the model assertion file. This argument must be
given unless the state machine is being resumed, in which case it
cannot be given."""))
snap_cmd.add_argument(
'--extra-snaps',
default=None, action='append',
help=_("""Extra snaps to install. This is passed through to `snap
prepare-image`."""))
snap_cmd.add_argument(
'--cloud-init',
default=None, metavar='USER-DATA-FILE',
help=_('cloud-config data to be copied to the image'))
snap_cmd.add_argument(
'-c', '--channel',
default=None,
help=_('The snap channel to use'))

# Classic-based image options.
classic_cmd.add_argument(
'project', nargs='?', metavar='PROJECT',
help=_("""Project name to be specified to livecd-rootfs."""))
classic_cmd.add_argument(
'suite', nargs='?', metavar='SUITE',
help=_("""Distribution name to be specified to livecd-rootfs."""))
classic_cmd.add_argument(
'gadget-tree', nargs='?', metavar='GADGET-TREE',
help=_("""Gadget tree"""))
classic_cmd.add_argument(
'-a', '--arch',
default=get_host_arch(), metavar='CPU-ARCHITECTURE',
help=_("""CPU architecture to be specified to livecd-rootfs.
default value is builder arch."""))
classic_cmd.add_argument(
'-s', '--subarch',
default=None, metavar='SUBARCH',
help=_("""Sub architecture to be specified to livecd-rootfs."""))
classic_cmd.add_argument(
'-p', '--proposed',
default=None, metavar='PROPOSED',
help=_("""Proposed repo to install, This is passed through to
livecd-rootfs."""))
classic_cmd.add_argument(
'--subproject',
default=None, metavar='SUBPROJECT',
help=_("""Sub project name to be specified to livecd-rootfs."""))
classic_cmd.add_argument(
'--image-format',
default='img',
help=_("""Image format to be specified to livecd-rootfs."""))
classic_cmd.add_argument(
'--extra-ppas',
default=None, action='append',
help=_("""Extra ppas to install. This is passed through to
livecd-rootfs."""))

args = parser.parse_args(argv)

if args.debug:
logging.basicConfig(level=logging.DEBUG)
# The model assertion argument is required unless --resume is given, in
# which case it cannot be given.
if args.resume and args.model_assertion:
parser.error('model assertion is not allowed with --resume')
if not args.resume and args.model_assertion is None:
parser.error('model assertion is required')

if args.cmd == 'snap' :
# The model assertion argument is required unless --resume is given, in
# which case it cannot be given.
if args.resume and args.model_assertion:
parser.error('model assertion is not allowed with --resume')
if not args.resume and args.model_assertion is None:
parser.error('model assertion is required')
elif args.cmd == 'classic' :
print ('classic:: ', args.arch)

if args.resume and args.workdir is None:
parser.error('--resume requires --workdir')
# --until and --thru can take an int.
Expand All @@ -180,8 +319,12 @@ def parseargs(argv=None):
file=sys.stderr)
return args

argparse.ArgumentParser.get_modified_arguments = get_modified_arguments

def main(argv=None):
if argv == None:
argv = sys.argv[1:]

args = parseargs(argv)
if args.workdir:
os.makedirs(args.workdir, exist_ok=True)
Expand All @@ -192,8 +335,13 @@ def main(argv=None):
with open(pickle_file, 'rb') as fp:
state_machine = load(fp)
state_machine.workdir = args.workdir
else:
elif args.cmd == 'snap':
state_machine = ModelAssertionBuilder(args)
'''
elif args.cmd == 'classic':
sys.exit(0)
state_machine = ClassicBuilder(args)
'''
# Run the state machine, either to the end or thru/until the named state.
try:
if args.thru:
Expand Down
4 changes: 3 additions & 1 deletion ubuntu_image/tests/test_main.py
Expand Up @@ -121,8 +121,10 @@ def test_help(self):
main(('--help',))
self.assertEqual(cm.exception.code, 0)
lines = self._stdout.getvalue().splitlines()
self.assertTrue(lines[0].startswith('usage: ubuntu-image'),
self.assertTrue(lines[0].startswith('Usage'),
lines[0])
self.assertTrue(lines[1].startswith(' ubuntu-image'),
lines[1])

def test_debug(self):
with ExitStack() as resources:
Expand Down

0 comments on commit 23ddd19

Please sign in to comment.