|
1 | 1 | #!/usr/bin/env python |
2 | 2 |
|
3 | | -from __future__ import print_function |
4 | | - |
5 | | -import argparse |
6 | | -import json |
7 | 3 | import os |
8 | | -import sys |
9 | | - |
10 | | -from cmd2 import Cmd |
11 | | -import keyring |
12 | | -from requests import ConnectionError |
13 | | - |
14 | | - |
15 | | -from stackdio.client import StackdIO |
16 | | - |
17 | | -from stackdio.cli import mixins |
18 | | - |
19 | | - |
20 | | -class StackdioShell( |
21 | | - Cmd, |
22 | | - mixins.bootstrap.BootstrapMixin, |
23 | | - mixins.stacks.StackMixin, |
24 | | - mixins.formulas.FormulaMixin, |
25 | | - mixins.blueprints.BlueprintMixin): |
26 | | - |
27 | | - CFG_DIR = os.path.expanduser(os.getenv('STACKDIO_CFG_DIR',"~/.stackdio-cli/")) |
28 | | - CFG_FILE = os.path.join(CFG_DIR, os.getenv('STACKDIO_CFG_FILE',"config.json")) |
29 | | - BOOTSTRAP_FILE = os.path.join(CFG_DIR, "bootstrap.yaml") |
30 | | - KEYRING_SERVICE = "stackdio_cli" |
31 | | - PROMPT = "\n{username} @ {url}\n> " |
32 | | - HELP_CMDS = [ |
33 | | - "account_summary", |
34 | | - "stacks", "blueprints", "formulas", |
35 | | - "initial_setup", "bootstrap", |
36 | | - "help", "exit", "quit", |
37 | | - ] |
38 | | - |
39 | | - Cmd.intro = """ |
40 | | -###################################################################### |
41 | | - s t a c k d . i o |
42 | | -###################################################################### |
43 | | -""" |
44 | | - |
45 | | - def __init__(self): |
46 | | - Cmd.__init__(self) |
47 | | - mixins.bootstrap.BootstrapMixin.__init__(self) |
48 | | - self._load_config() |
49 | | - if 'url' in self.config and self.config['url']: |
50 | | - self._init_stacks() |
51 | | - self._validate_auth() |
52 | | - |
53 | | - def preloop(self): |
54 | | - self._setprompt() |
55 | | - |
56 | | - def precmd(self, line): |
57 | | - self._setprompt() |
58 | | - return line |
59 | | - |
60 | | - def postloop(self): |
61 | | - print("\nGoodbye!") |
62 | | - |
63 | | - def get_names(self): |
64 | | - if self.validated: |
65 | | - return ["do_%s" % c for c in self.HELP_CMDS] |
66 | | - else: |
67 | | - return ["do_initial_setup", "do_help"] |
68 | | - |
69 | | - def _init_stacks(self): |
70 | | - """Instantiate a StackdIO object""" |
71 | | - self.stacks = StackdIO( |
72 | | - base_url=self.config["url"], |
73 | | - auth=( |
74 | | - self.config["username"], |
75 | | - keyring.get_password(self.KEYRING_SERVICE, self.config.get("username") or "") |
76 | | - ), |
77 | | - verify=self.config.get('verify', True) |
78 | | - ) |
79 | | - |
80 | | - def _load_config(self): |
81 | | - """Attempt to load config file, otherwise fallback to DEFAULT_CONFIG""" |
82 | | - |
83 | | - try: |
84 | | - self.config = json.loads(open(self.CFG_FILE).read()) |
85 | | - self.config['blueprint_dir'] = os.path.expanduser(self.config.get('blueprint_dir', '')) |
86 | | - |
87 | | - except ValueError: |
88 | | - print(self.colorize( |
89 | | - "What happened?! The config file is not valid JSON. A " |
90 | | - "re-install is likely the easiest fix.", "red")) |
91 | | - raise |
92 | | - except IOError: |
93 | | - self.config = { |
94 | | - 'url': None, |
95 | | - 'username': None, |
96 | | - } |
97 | | - print(self.colorize( |
98 | | - "It seems like this is your first time using the CLI. Please run " |
99 | | - "'initial_setup' to configure.", "green")) |
100 | | - # print(self.colorize( |
101 | | - # "What happened?! Unable to find a config file. A re-install " |
102 | | - # "is likely the easiest fix.", "red")) |
103 | | - # raise |
104 | | - |
105 | | - self.has_public_key = None |
106 | | - |
107 | | - def _validate_auth(self): |
108 | | - """Verify that we can connect successfully to the api""" |
109 | | - |
110 | | - # If there is no config, just force the user to go through initial setup |
111 | | - if self.config['url'] is None: |
112 | | - return |
113 | | - |
114 | | - try: |
115 | | - self.stacks.get_root() |
116 | | - status_code = 200 |
117 | | - self.validated = (200 <= status_code <= 299) |
118 | | - except ConnectionError: |
119 | | - print(self.colorize( |
120 | | - "Unable to connect to {0}".format(self.config["url"]), |
121 | | - "red")) |
122 | | - raise |
123 | | - |
124 | | - if self.validated: |
125 | | - print(self.colorize( |
126 | | - "Config loaded and validated", "blue")) |
127 | | - self.has_public_key = self.stacks.get_public_key() |
128 | | - else: |
129 | | - print(self.colorize( |
130 | | - "ERROR: Unable to validate config", "red")) |
131 | | - self.has_public_key = None |
132 | | - |
133 | | - def _setprompt(self): |
134 | | - |
135 | | - Cmd.prompt = self.colorize( |
136 | | - self.PROMPT.format(**self.config), |
137 | | - "blue") |
138 | | - |
139 | | - if not self.validated and self.config['url'] is not None: |
140 | | - print(self.colorize(""" |
141 | | -## |
142 | | -## Unable to validate connection - one of several possibilities exist: |
143 | | -## If this is the first time you've fired this up, you need to run |
144 | | -## 'initial_setup' to configure your account details. If you've already |
145 | | -## done that, there could be a network connection issue anywhere between |
146 | | -## your computer and your stackd.io instance, |
147 | | -## or your password may be incorrect, or ... etc. |
148 | | -## |
149 | | - """, |
150 | | - "green")) |
151 | | - |
152 | | - if self.validated and not self.has_public_key: |
153 | | - print(self.colorize( |
154 | | - "## Your account is missing the public key, run 'bootstrap' to fix", |
155 | | - "red")) |
156 | | - |
157 | | - def _print_summary(self, title, components): |
158 | | - num_components = len(components) |
159 | | - print("## {0} {1}{2}".format( |
160 | | - num_components, |
161 | | - title, |
162 | | - "s" if num_components == 0 or num_components > 1 else "")) |
163 | | - |
164 | | - for item in components: |
165 | | - print("- Title: {0}\n Description: {1}".format( |
166 | | - item.get("title"), item.get("description"))) |
167 | | - |
168 | | - if "status_detail" in item: |
169 | | - print(" Status Detail: {0}\n".format( |
170 | | - item.get("status_detail"))) |
171 | | - else: |
172 | | - print("") |
173 | | - |
174 | | - def do_account_summary(self, args=None): |
175 | | - """Get a summary of your account.""" |
176 | | - sys.stdout.write("Polling {0} ... ".format(self.config["url"])) |
177 | | - sys.stdout.flush() |
178 | | - |
179 | | - public_key = self.stacks.get_public_key() |
180 | | - formulas = self.stacks.list_formulas() |
181 | | - blueprints = self.stacks.list_blueprints() |
182 | | - stacks = self.stacks.list_stacks() |
183 | | - |
184 | | - sys.stdout.write("done\n") |
185 | | - |
186 | | - print("## Username: {0}".format(self.config["username"])) |
187 | | - print("## Public Key:\n{0}".format(public_key)) |
188 | | - |
189 | | - self._print_summary("Formula", formulas) |
190 | | - self._print_summary("Blueprint", blueprints) |
191 | | - self._print_summary("Stack", stacks) |
192 | 4 |
|
| 5 | +import click |
| 6 | +import click_shell |
193 | 7 |
|
194 | | -def main(): |
| 8 | +from stackdio.cli.mixins import blueprints, formulas, stacks |
| 9 | +from stackdio.cli.utils import pass_client |
| 10 | +from stackdio.client import StackdioClient |
| 11 | +from stackdio.client.config import CFG_FILE |
| 12 | +from stackdio.client.version import __version__ |
| 13 | + |
| 14 | + |
| 15 | +CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help']) |
| 16 | + |
| 17 | +HIST_FILE = os.path.join(os.path.expanduser('~'), '.stackdio-cli', 'history') |
| 18 | + |
| 19 | + |
| 20 | +@click_shell.shell(context_settings=CONTEXT_SETTINGS, prompt='stackdio > ', |
| 21 | + intro='stackdio-cli, v{0}'.format(__version__), hist_file=HIST_FILE) |
| 22 | +@click.version_option(__version__, '-v', '--version') |
| 23 | +@click.option('-c', '--config-file', help='The config file to use.', |
| 24 | + type=click.Path(dir_okay=False, file_okay=True), default=CFG_FILE, |
| 25 | + envvar='STACKDIO_CLI_CONFIG_FILE') |
| 26 | +@click.pass_context |
| 27 | +def stackdio(ctx, config_file): |
| 28 | + # Create a client instance |
| 29 | + client = StackdioClient(cfg_file=config_file) |
| 30 | + |
| 31 | + # Throw an error if we're not configured already |
| 32 | + if ctx.invoked_subcommand not in ('configure', None) and not client.usable(): |
| 33 | + raise click.UsageError('It looks like you haven\'t used this CLI before. Please run ' |
| 34 | + '`stackdio-cli configure`') |
| 35 | + |
| 36 | + # Put the client in the obj so other commands can pick it up |
| 37 | + ctx.obj = client |
195 | 38 |
|
196 | | - parser = argparse.ArgumentParser( |
197 | | - description="Invoke the stackdio cli") |
198 | | - parser.add_argument("--debug", action="store_true", help="Enable debugging output") |
199 | | - args = parser.parse_args() |
200 | | - |
201 | | - # an ugly hack to work around the fact that cmd2 is using optparse to parse |
202 | | - # arguments for the commands; not sure what the "right" fix is, but as long |
203 | | - # as we assume that we don't want any of our arguments to get passed into |
204 | | - # the cmdloop this seems ok |
205 | | - sys.argv = sys.argv[0:1] |
206 | | - |
207 | | - shell = StackdioShell() |
208 | | - if args.debug: |
209 | | - shell.debug = True |
210 | | - shell.cmdloop() |
| 39 | + |
| 40 | +@stackdio.command(name='configure') |
| 41 | +@pass_client |
| 42 | +def configure(client): |
| 43 | + """ |
| 44 | + Configure the client |
| 45 | + """ |
| 46 | + client.config.prompt_for_config() |
| 47 | + |
| 48 | + |
| 49 | +@stackdio.command(name='server-version') |
| 50 | +@pass_client |
| 51 | +def server_version(client): |
| 52 | + """ |
| 53 | + Print the version of the server |
| 54 | + """ |
| 55 | + click.echo('stackdio-server, version {0}'.format(client.get_version())) |
| 56 | + |
| 57 | + |
| 58 | +# Add all our other commands |
| 59 | +stackdio.add_command(blueprints.blueprints) |
| 60 | +stackdio.add_command(stacks.stacks) |
| 61 | +stackdio.add_command(formulas.formulas) |
| 62 | + |
| 63 | + |
| 64 | +def main(): |
| 65 | + # Just run our CLI tool |
| 66 | + stackdio() |
211 | 67 |
|
212 | 68 |
|
213 | 69 | if __name__ == '__main__': |
|
0 commit comments