diff --git a/.travis.yml b/.travis.yml index 6b4d5c0..619765d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,20 +2,13 @@ language: python python: - "2.7" - - "3.3" - "3.4" + - "3.5" cache: directories: - $HOME/.cache/pip -# Set up our environment -env: - NOSE_WITH_XUNIT: 1 - NOSE_WITH_COVERAGE: 1 - NOSE_COVER_BRANCHES: 1 - NOSE_COVER_INCLUSIVE: 1 - # So that we get a docker container sudo: false @@ -23,6 +16,7 @@ sudo: false install: - pip install -U pip - pip install -U wheel + - pip install -U twine - pip install -U -e .[testing] ## Customize test commands @@ -37,18 +31,30 @@ script: # Only build artifacts on success after_success: - coveralls - - export STACKDIO_VERSION=`python setup.py --version` - python setup.py sdist - python setup.py bdist_wheel deploy: - provider: releases - api_key: - secure: T4jI1aZQ+wDJBgGxcbdrtLz3zpXA9yZwmrsm8d3GqEGxApMtkKLWq0uqf86C8VkqaY6p4Nm1a/PTApV1isbuSoJbdeMVJA1MlYB/G7QMK7eI8nFqkw7Q4jzuOdEC0D1CPZx7ZWBn0bYxSRTcSeQSnGeGDy2KxekGSZFfIxe4APo= - file: - - dist/stackdio-${STACKDIO_VERSION}.tar.gz - - dist/stackdio-${STACKDIO_VERSION}-py2.py3-none-any.whl - skip_cleanup: true - on: - tags: true - repo: stackdio/stackdio-python-client + - provider: releases + api_key: + secure: WDRJ+QYPfAMuH8sEFPTTEHabaEtfvLWvHiXi69NA3lruIlKr0Id5gpF/Bqr5VfHiz9jdHuBRdVLgYRYVXAVsRkw13N1YlHgR4j4oi61fMugwDTC820Jnf8EDpuvXys8TPiPRh7Xe2XTGc4HMO0moGz6gp9gH4OAsxGgLPNLmiDA= + file: + - dist/stackdio-${TRAVIS_TAG}.tar.gz + - dist/stackdio-${TRAVIS_TAG}-py2.py3-none-any.whl + skip_cleanup: true + on: + tags: true + repo: stackdio/stackdio-python-client + python: "2.7" + + # Upload to pypi. Do this instead of the pypi provider so that we + # ensure the exact same artifact is uploaded to github and pypi. + # The pypi provider will re-build the 2 artifacts, which is not ideal. + # This requires setting TWINE_USERNAME and TWINE_PASSWORD in travis. + - provider: script + script: twine upload dist/stackdio-${TRAVIS_TAG}.tar.gz dist/stackdio-${TRAVIS_TAG}-py2.py3-none-any.whl + skip_cleanup: true + on: + tags: true + repo: stackdio/stackdio-python-client + python: "2.7" diff --git a/setup.py b/setup.py index 3c460a0..f8869f0 100644 --- a/setup.py +++ b/setup.py @@ -15,7 +15,6 @@ # limitations under the License. # -import os import sys from setuptools import setup, find_packages @@ -25,9 +24,9 @@ def test_python_version(): major = sys.version_info[0] minor = sys.version_info[1] micro = sys.version_info[2] - if float('%d.%d' % (major, minor)) < 2.6: + if (major, minor) < (2, 7): err_msg = ('Your Python version {0}.{1}.{2} is not supported.\n' - 'stackdio-server requires Python 2.6 or newer.\n'.format(major, minor, micro)) + 'stackdio-server requires Python 2.7 or newer.\n'.format(major, minor, micro)) sys.stderr.write(err_msg) sys.exit(1) @@ -85,7 +84,6 @@ def test_python_version(): 'console_scripts': [ 'stackdio-cli=stackdio.cli:main', 'blueprint-generator=stackdio.cli.blueprints:main', - 'stackdio-config-convert=stackdio.client.config:main', ], }, classifiers=[ @@ -99,8 +97,8 @@ def test_python_version(): 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', 'Topic :: System :: Clustering', 'Topic :: System :: Distributed Computing', ] diff --git a/stackdio/cli/blueprints/generator.py b/stackdio/cli/blueprints/generator.py index efef425..b59c935 100644 --- a/stackdio/cli/blueprints/generator.py +++ b/stackdio/cli/blueprints/generator.py @@ -1,15 +1,15 @@ -from __future__ import print_function +from __future__ import print_function, unicode_literals -import sys -import os import json +import os +import sys import click import yaml from jinja2 import Environment, FileSystemLoader, StrictUndefined, meta from jinja2.exceptions import TemplateNotFound, TemplateSyntaxError, UndefinedError -from jinja2.nodes import Assign, Block, Const, If, Not from jinja2.filters import do_replace, evalcontextfilter +from jinja2.nodes import Assign, Block, Const, If, Not class BlueprintException(Exception): @@ -232,12 +232,12 @@ def generate(self, template_file, var_files=(), variables=None, if prompt: # Prompt for missing vars for var in sorted(missing_vars): - context[var] = self.prompt('{0}: '.format(var)) + context[var] = self.prompt('{}: '.format(var)) else: # Print an error error_str = 'Missing variables:\n' for var in sorted(missing_vars): - error_str += ' {0}\n'.format(var) + error_str += ' {}\n'.format(var) self.error_exit(error_str, 0) # Find the set of optional variables (ones inside of a 'if is not undefined' @@ -259,14 +259,14 @@ def generate(self, template_file, var_files=(), variables=None, if null_vars and not suppress_warnings: warn_str = '\nWARNING: Null variables (replaced with empty string):\n' for var in null_vars: - warn_str += ' {0}\n'.format(var) + warn_str += ' {}\n'.format(var) self.warning(warn_str, 0) # Print a warning if there's unset optional variables if optional_vars and not suppress_warnings: warn_str = '\nWARNING: Missing optional variables:\n' for var in sorted(optional_vars): - warn_str += ' {0}\n'.format(var) + warn_str += ' {}\n'.format(var) self.warning(warn_str, 0) # Generate the blueprint @@ -276,24 +276,31 @@ def generate(self, template_file, var_files=(), variables=None, set_vars.update(context) context = set_vars - template_json = template.render(**context) + rendered_template = template.render(**context) if debug: click.echo('\n') - click.echo(template_json) + click.echo(rendered_template) click.echo('\n') - # Return a dict object rather than a string - return json.loads(template_json) + template_extension = template_file.split('.')[-1] + + if template_extension in ('json',): + # Return a dict object rather than a string + return json.loads(rendered_template) + elif template_extension in ('yaml', 'yml'): + return yaml.safe_load(rendered_template) + else: + self.error_exit('Template extension {} is not valid.'.format(template_extension)) except TemplateNotFound: - self.error_exit('Your template file {0} was not found.'.format(template_file)) + self.error_exit('Your template file {} was not found.'.format(template_file)) except TemplateSyntaxError as e: - self.error_exit('Invalid template error at line {0}:\n{1}'.format( + self.error_exit('Invalid template error at line {}:\n{}'.format( e.lineno, str(e) )) except UndefinedError as e: - self.error_exit('Missing variable: {0}'.format(str(e))) + self.error_exit('Missing variable: {}'.format(str(e))) # except ValueError: # self.error_exit('Invalid JSON. Check your template file.') diff --git a/stackdio/cli/mixins/blueprints.py b/stackdio/cli/mixins/blueprints.py index d9c8138..6f18ceb 100644 --- a/stackdio/cli/mixins/blueprints.py +++ b/stackdio/cli/mixins/blueprints.py @@ -192,7 +192,7 @@ def create_all_blueprints(client): def get_blueprint_id(client, blueprint_title): - found_blueprints = client.search_blueprints(title=blueprint_title) + found_blueprints = client.list_blueprints(title=blueprint_title) if len(found_blueprints) == 0: raise click.Abort('Blueprint "{0}" does not exist'.format(blueprint_title)) diff --git a/stackdio/cli/mixins/formulas.py b/stackdio/cli/mixins/formulas.py index 2fcb748..ec4d47c 100644 --- a/stackdio/cli/mixins/formulas.py +++ b/stackdio/cli/mixins/formulas.py @@ -43,7 +43,7 @@ def import_formula(client, uri, username, password): def get_formula_id(client, formula_uri): - found_formulas = client.search_formulas(uri=formula_uri) + found_formulas = client.list_formulas(uri=formula_uri) if len(found_formulas) == 0: raise click.Abort('Formula "{0}" does not exist'.format(formula_uri)) diff --git a/stackdio/cli/mixins/stacks.py b/stackdio/cli/mixins/stacks.py index a1fa3f1..cea0c30 100644 --- a/stackdio/cli/mixins/stacks.py +++ b/stackdio/cli/mixins/stacks.py @@ -52,7 +52,7 @@ def launch_stack(client, blueprint_title, stack_title): def get_stack_id(client, stack_title): - found_stacks = client.search_stacks(title=stack_title) + found_stacks = client.list_stacks(title=stack_title) if len(found_stacks) == 0: raise click.Abort('Stack "{0}" does not exist'.format(stack_title)) @@ -73,7 +73,7 @@ def stack_history(client, stack_title, length): stack_id = get_stack_id(client, stack_title) history = client.get_stack_history(stack_id) for event in history[0:min(length, len(history))]: - click.echo('[{created}] {level} // {event} // {status}'.format(**event)) + click.echo('[{created}] {message}'.format(**event)) @stacks.command(name='hostnames') diff --git a/stackdio/client/__init__.py b/stackdio/client/__init__.py index 43daf08..e982ebb 100644 --- a/stackdio/client/__init__.py +++ b/stackdio/client/__init__.py @@ -15,8 +15,11 @@ # limitations under the License. # +from __future__ import unicode_literals + import logging +from pkg_resources import parse_version from .account import AccountMixin from .blueprint import BlueprintMixin from .config import StackdioConfig @@ -38,40 +41,6 @@ logger.addHandler(logging.NullHandler()) -def _get_server_version_info(version_str): - basic_info = version_str.split('.') - - major = int(basic_info[0]) - minor = int(basic_info[1]) - - version_type = 'final' - extra_id = 0 - - try: - patch_v = int(basic_info[2]) - except ValueError: - for vtype in ('a', 'b', 'rc'): - if vtype in basic_info[2]: - version_type = vtype - idx = basic_info[2].find(vtype) - patch_v = int(basic_info[:idx]) - extra_id = int(basic_info[2][idx + len(vtype):]) - - if version_type == 'final': - raise ValueError('Invalid version: {}'.format(version_str)) - - if len(basic_info) > 3: - for vtype in ('dev', 'post'): - if basic_info[3].startswith(vtype): - version_type = vtype - extra_id = int(basic_info[3][len(vtype):]) - - if version_type == 'final': - raise ValueError('Invalid version: {}'.format(version_str)) - - return major, minor, patch_v, version_type, extra_id - - class StackdioClient(BlueprintMixin, FormulaMixin, AccountMixin, ImageMixin, RegionMixin, StackMixin, SettingsMixin, SnapshotMixin, HttpMixin): @@ -95,12 +64,12 @@ def __init__(self, url=None, username=None, password=None, verify=None, cfg_file if self.usable(): try: raw_version = self.get_version(raise_for_status=False) - self.version = _get_server_version_info(raw_version) + self.version = parse_version(raw_version) except MissingUrlException: raw_version = None self.version = None - if self.version and (self.version[0] != 0 or self.version[1] != 7): + if self.version and (int(self.version[0]) != 0 or int(self.version[1]) != 8): raise IncompatibleVersionException( 'Server version {0} not supported. Please upgrade ' 'stackdio-cli to {1}.{2}.0 or higher.'.format(raw_version, *self.version) diff --git a/stackdio/client/account.py b/stackdio/client/account.py index 928105f..f43b522 100644 --- a/stackdio/client/account.py +++ b/stackdio/client/account.py @@ -21,15 +21,10 @@ class AccountMixin(HttpMixin): @get('cloud/providers/', paginate=True) - def list_providers(self): + def list_providers(self, **kwargs): """List all providers""" pass - @get('cloud/providers/', paginate=True) - def search_providers(self, **kwargs): - """Search for a provider""" - pass - @post('cloud/accounts/') def create_account(self, **kwargs): """Create an account""" @@ -53,7 +48,7 @@ def create_account(self, **kwargs): return form_data @get('cloud/accounts/', paginate=True) - def list_accounts(self): + def list_accounts(self, **kwargs): """List all account""" pass @@ -62,11 +57,6 @@ def get_account(self, account_id): """Return the account that matches the given id""" pass - @get('cloud/accounts/') - def search_accounts(self, **kwargs): - """List all accounts""" - pass - @delete('cloud/accounts/{account_id}/') def delete_account(self, account_id): """List all accounts""" diff --git a/stackdio/client/blueprint.py b/stackdio/client/blueprint.py index a48807c..a0fcc7e 100644 --- a/stackdio/client/blueprint.py +++ b/stackdio/client/blueprint.py @@ -56,17 +56,13 @@ def create_blueprint(self, blueprint): return blueprint @get('blueprints/', paginate=True) - def list_blueprints(self): + def list_blueprints(self, **kwargs): pass @get('blueprints/{blueprint_id}/') def get_blueprint(self, blueprint_id): pass - @get('blueprints/', paginate=True) - def search_blueprints(self, **kwargs): - pass - @delete('blueprints/{blueprint_id}/') def delete_blueprint(self, blueprint_id): pass diff --git a/stackdio/client/formula.py b/stackdio/client/formula.py index 5e30935..3d819b7 100644 --- a/stackdio/client/formula.py +++ b/stackdio/client/formula.py @@ -37,7 +37,7 @@ def import_formula(self, formula_uri, git_username=None, git_password=None, acce return data @get('formulas/', paginate=True) - def list_formulas(self): + def list_formulas(self, **kwargs): """Return all formulas""" pass @@ -50,11 +50,6 @@ def get_formula(self, formula_id): def list_components_for_version(self, formula_id, version): pass - @get('formulas/', paginate=True) - def search_formulas(self, **kwargs): - """Get a formula with matching id""" - pass - @delete('formulas/{formula_id}/') def delete_formula(self, formula_id): """Delete formula with matching id""" diff --git a/stackdio/client/http.py b/stackdio/client/http.py index 4eb3d4d..35e2bf7 100644 --- a/stackdio/client/http.py +++ b/stackdio/client/http.py @@ -95,10 +95,7 @@ def __init__(self, dfunc=None, rfunc=None, quiet=False): self.obj = None self.data_func = dfunc - self.response_func = rfunc - - if self.response_func is None: - self.response_func = default_response + self.response_func = rfunc or default_response self.quiet = quiet diff --git a/stackdio/client/image.py b/stackdio/client/image.py index 8af4f15..80402f2 100644 --- a/stackdio/client/image.py +++ b/stackdio/client/image.py @@ -32,7 +32,7 @@ def create_image(self, title, image_id, ssh_user, cloud_provider, default_instan } @get('cloud/images/', paginate=True) - def list_images(self): + def list_images(self, **kwargs): """List all images""" pass @@ -41,11 +41,6 @@ def get_image(self, image_id): """Return the image that matches the given id""" pass - @get('cloud/images/', paginate=True) - def search_images(self, **kwargs): - """List all images""" - pass - @delete('cloud/images/{image_id}/') def delete_image(self, image_id): """Delete the image with the given id""" diff --git a/stackdio/client/region.py b/stackdio/client/region.py index 2689d8b..e69237a 100644 --- a/stackdio/client/region.py +++ b/stackdio/client/region.py @@ -20,25 +20,17 @@ class RegionMixin(HttpMixin): @get('cloud/providers/{provider_name}/regions/', paginate=True) - def list_regions(self, provider_name): + def list_regions(self, provider_name, **kwargs): pass @get('cloud/providers/{provider_name}/regions/{region_id}/') def get_region(self, provider_name, region_id): pass - @get('cloud/providers/{provider_name}/regions/', paginate=True) - def search_regions(self, provider_name, **kwargs): - pass - @get('cloud/providers/{provider_name}/zones/', paginate=True) - def list_zones(self): + def list_zones(self, provider_name, **kwargs): pass @get('cloud/providers/{provider_name}/zones/{zone_id}') def get_zone(self, provider_name, zone_id): pass - - @get('cloud/providers/{provider_name}/zones/', paginate=True) - def search_zones(self, provider_name, **kwargs): - pass diff --git a/stackdio/client/snapshot.py b/stackdio/client/snapshot.py index 5cda87a..6264f8b 100644 --- a/stackdio/client/snapshot.py +++ b/stackdio/client/snapshot.py @@ -26,17 +26,13 @@ def create_snapshot(self, snapshot): return snapshot @get('cloud/snapshots/', paginate=True) - def list_snapshots(self): + def list_snapshots(self, **kwargs): pass @get('cloud/snapshots/{snapshot_id}/') def get_snapshot(self, snapshot_id): pass - @get('cloud/snapshots/', paginate=True) - def search_snapshots(self, **kwargs): - pass - @delete('cloud/snapshots/{snapshot_id}/') def delete_snapshot(self, snapshot_id): pass diff --git a/stackdio/client/stack.py b/stackdio/client/stack.py index 0308f25..396fe3d 100644 --- a/stackdio/client/stack.py +++ b/stackdio/client/stack.py @@ -33,7 +33,7 @@ def create_stack(self, stack_data): return stack_data @get('stacks/', paginate=True) - def list_stacks(self): + def list_stacks(self, **kwargs): """Return a list of all stacks""" pass @@ -42,11 +42,6 @@ def get_stack(self, stack_id): """Get stack info""" pass - @get('stacks/', paginate=True) - def search_stacks(self, **kwargs): - """Search for stacks that match the given criteria""" - pass - @delete('stacks/{stack_id}/') def delete_stack(self, stack_id): """Destructively delete a stack forever.""" @@ -100,6 +95,10 @@ def get_stack_history(self, stack_id): """Get stack info""" pass + @get_stack_history.response + def get_stack_history(self, resp): + return reversed(resp) + @get('stacks/{stack_id}/hosts/', paginate=True) def get_stack_hosts(self, stack_id): """Get a list of all stack hosts""" diff --git a/stackdio/client/version.py b/stackdio/client/version.py index c98fad7..73cb64f 100644 --- a/stackdio/client/version.py +++ b/stackdio/client/version.py @@ -27,7 +27,7 @@ except Exception: pass -VERSION = (0, 7, 4, 'dev', 0) +VERSION = (0, 8, 0, 'dev', 0) def get_version(version):