PageRenderTime 228ms CodeModel.GetById 80ms app.highlight 78ms RepoModel.GetById 63ms app.codeStats 0ms

/silverlining/runner.py

https://bitbucket.org/ianb/silverlining/
Python | 486 lines | 475 code | 4 blank | 7 comment | 20 complexity | b687884a42385084c6a5cb2229da7330 MD5 | raw file
  1"""silverlining interface
  2
  3This implements the command-line interface of silverlining.  It includes
  4all the commands and their options, though the implementation of the
  5commands are in ``silverlining.commands.*``
  6"""
  7
  8import os
  9import sys
 10import argparse
 11from cmdutils.arg import add_verbose, create_logger
 12from cmdutils import CommandError
 13from silverlining import createconf
 14from silverlining.config import Config
 15
 16
 17## Following is a HUGE BLOCK of argument definitions:
 18
 19description = """\
 20Runs a variety of deployment-related commands
 21"""
 22
 23parser = argparse.ArgumentParser(
 24    description=description)
 25
 26parser.add_argument(
 27    '-p', '--provider',
 28    metavar='NAME',
 29    help="The [provider:NAME] section from ~/.silverlining.conf to use (default [provider:default])",
 30    default="default")
 31
 32parser.add_argument(
 33    '-y', '--yes',
 34    action='store_true',
 35    help="Answer yes to any questions")
 36
 37parser.add_argument(
 38    '--debug-libcloud', metavar='FILENAME',
 39    help="Write any libcloud interactions (HTTP request log)")
 40
 41add_verbose(parser, add_log=True)
 42
 43subcommands = parser.add_subparsers(dest="command")
 44
 45parser_list_images = subcommands.add_parser(
 46    'list-images', help="List all images available")
 47
 48parser_list_sizes = subcommands.add_parser(
 49    'list-sizes', help="List all sizes available")
 50
 51parser_list_nodes = subcommands.add_parser(
 52    'list-nodes', help="List all active nodes")
 53
 54parser_destroy = subcommands.add_parser(
 55    'destroy-node', help="Destroy a node")
 56
 57parser_destroy.add_argument(
 58    'nodes', nargs='+',
 59    metavar='HOSTNAME',
 60    help="The hostname(s) of the node to destroy")
 61
 62parser_create = subcommands.add_parser(
 63    'create-node', help="Create a new node")
 64
 65parser_create.add_argument(
 66    'node',
 67    metavar='HOSTNAME',
 68    help="The hostname of the node to create")
 69
 70parser_create.add_argument(
 71    '--image',
 72    default='name *lucid*',
 73    metavar="IMAGE",
 74    help='Image to use, can be "id 10", and can contain wildcards like "name *karmic*".  '
 75    'Default is "name *karmic*" which will select an Ubuntu Karmic image.')
 76
 77parser_create.add_argument(
 78    '--size',
 79    metavar="SIZE",
 80    default="ram 256",
 81    help='Size to use, can be "id 1", "ram 256" etc')
 82
 83parser_create.add_argument(
 84    '--setup-node', action='store_true',
 85    help="Wait for the node to be created (this means the command just "
 86    "sits for a couple minutes) and then set up the server.  It is suggested "
 87    "you also use --yes with this option.")
 88
 89parser_create.add_argument(
 90    '--wait', action='store_true',
 91    help="Wait for the node to be created, but don't setup (like "
 92    "--setup-node that just quits before it actually sets up the node)")
 93
 94parser_create.add_argument(
 95    '--if-not-exists', action='store_true',
 96    dest='if_not_exists',
 97    help="Only create the node if it does not exist (will check the cloud "
 98    "providers server list)")
 99
100parser_setup = subcommands.add_parser(
101    'setup-node', help="Setup a new (fresh Ubuntu Jaunty install) server")
102
103parser_setup.add_argument(
104    'node',
105    metavar='HOSTNAME',
106    help="The hostname of the node to setup")
107
108parser_clean = subcommands.add_parser(
109    'clean-node', help="Clean unused application instances on a node")
110
111parser_clean.add_argument(
112    'node', nargs='?',
113    metavar='HOSTNAME',
114    help="Node to clean instances from")
115
116parser_clean.add_argument(
117    '-n', '--simulate',
118    action='store_true',
119    help="Don't actually clean anything, just show what would be done")
120
121parser_update = subcommands.add_parser(
122    'update', help="Update/deploy an application")
123
124parser_update.add_argument(
125    'dir',
126    help="The directory to upload to the server")
127
128parser_update.add_argument(
129    'location', nargs='?',
130    metavar="HOSTNAME[/PATH]",
131    help="Place to upload to (will default to the default_location "
132    "setting in app.ini)")
133
134parser_update.add_argument(
135    '--debug-single-process',
136    action='store_true',
137    help="Install as a 'debug' application, running in a single process with "
138    "threads, so the application can be used with weberror or other debug "
139    "tools.")
140
141parser_update.add_argument(
142    '--name',
143    metavar="NAME",
144    help="'Name' of the site; defaults to the app_name setting in app.ini")
145
146parser_update.add_argument(
147    '--node',
148    metavar='NODE_HOSTNAME',
149    help="The hostname of the node to upload to")
150
151parser_update.add_argument(
152    '--clear',
153    action='store_true',
154    help='Clear the database on update (clears all database, files, etc!)')
155
156parser_update.add_argument(
157    '--config',
158    metavar='CONFIG_DIR',
159    help="Configuration to use for this deployment of the application")
160
161parser_update.add_argument(
162    '--noetchosts', help="Do not try to update /etc/hosts",
163    dest='update_etc_hosts', action='store_const', const=False, default=True)
164
165parser_init = subcommands.add_parser(
166    'init', help="Create a new application file layout")
167
168parser_init.add_argument(
169    'dir',
170    metavar='DIR',
171    help="A directory to initialize")
172
173parser_init.add_argument(
174    '-f', '--force',
175    action='store_true',
176    help="Overwrite files even if they already exist")
177
178parser_init.add_argument(
179    '--distribute',
180    action='store_true',
181    help="Use Distribute (instead of Setuptools)")
182
183parser_init.add_argument(
184    '--config',
185    action='store_true',
186    help="Put in a sample Paste Deploy config.ini")
187
188parser_init.add_argument(
189    '--main',
190    action='store_true',
191    help="Put in a sample main.py")
192
193parser_serve = subcommands.add_parser(
194    'serve', help="Serve up an application for development")
195
196parser_serve.add_argument(
197    'dir',
198    metavar='APP_DIR',
199    help='Directory holding app')
200
201parser_serve.add_argument(
202    '--port',
203    metavar='PORT',
204    default='8080',
205    help='Port to serve on (default 8080)')
206
207parser_serve.add_argument(
208    '--host',
209    metavar='IP/INTERFACE',
210    default='127.0.0.1',
211    help='Host/IP/interface to serve on (127.0.0.1 is private, 0.0.0.0 is public)')
212
213parser_serve.add_argument(
214    '--config',
215    metavar='CONFIG_DIR',
216    help="Configuration to use for the application")
217
218## We can't handle "silver run" well with a subparser, because there's
219## a bug in subparsers that they can't ignore arguments they don't
220## understand.  Because there will be arguments passed to the remote
221## command we need that, so instead we create an entirely separate
222## parser, and we'll do a little checking to see if the run command is
223## given:
224
225parser_run = argparse.ArgumentParser(
226    add_help=False,
227    description="""\
228Run a command for an application; this runs a script in bin/ on the
229remote server.
230
231Use it like:
232    silver run LOCATION import-something --option-for-import-something
233
234Note any arguments that point to existing files or directories will
235cause those files/directories to be uploaded, and substituted with the
236location of the remote name.
237
238This will run *Python* scripts in your application's bin/ directory,
239in another directory (e.g., silver run LOCATION src/myapp/script.py),
240or a global command (e.g., ipython).
241
242Use 'silver run LOCATION python' to get an interactive prompt on the
243remote server.
244""")
245
246parser_run.add_argument(
247    '--node',
248    metavar="NODE_HOSTNAME",
249    help="Node to run command on")
250
251parser_run.add_argument(
252    '-p', '--provider',
253    metavar='NAME',
254    help="The [provider:NAME] section from ~/.silverlining.conf to use (default [provider:default])",
255    default="default")
256
257parser_run.add_argument(
258    '-y', '--yes',
259    action='store_true',
260    help="Answer yes to any questions")
261
262parser_run.add_argument(
263    '-i', '--interactive',
264    action='store_true',
265    help=("Tells ssh to force pseudo-tty allocation.  Useful when what you're"
266          " running is a shell of some sort"))
267
268#add_verbose(parser_run, add_log=True)
269
270parser_run.add_argument(
271    'location',
272    help="Location where the application is running")
273
274parser_run.add_argument(
275    'script',
276    help="script (in bin/) to run")
277
278parser_run.add_argument(
279    '--user', metavar='USERNAME',
280    default="www-data",
281    help="The user to run the command as; default is www-data.  "
282    "Other options are www-mgr and root")
283
284parser_query = subcommands.add_parser(
285    'query', help="See what apps are on a node")
286
287parser_query.add_argument(
288    'node',
289    metavar='HOSTNAME',
290    help="Node to query")
291
292parser_query.add_argument(
293    'app-name', nargs='*',
294    help="The app_name or hostname to query (wildcards allowed)")
295
296parser_activate = subcommands.add_parser(
297    'activate', help="Activate a site instance for a given domain")
298
299parser_activate.add_argument(
300    '--node',
301    metavar="NODE_HOSTNAME",
302    help="Node to act on")
303
304parser_activate.add_argument(
305    'location',
306    help="The hostname/path to act on")
307
308parser_activate.add_argument(
309    'instance_name',
310    help="The instance name to activate (can also be \"prev\")")
311
312parser_deactivate = subcommands.add_parser(
313    'deactivate', help="Deactivate a site (leaving it dangling)")
314
315parser_deactivate.add_argument(
316    '--node',
317    metavar="NODE_HOSTNAME",
318    help="Node to act on")
319
320parser_deactivate.add_argument(
321    'locations', nargs='+',
322    help="The hostname/path to act on; if you give more than one, "
323    "they must all be on the same node.  Can be a wildcard.")
324
325parser_deactivate.add_argument(
326    '--keep-prev', action='store_true',
327    help="Keep the prev.* host activate (by default it is deleted)")
328
329# This is a mock for use with -h:
330parser_run_mock = subcommands.add_parser(
331    'run', help="Run a command on a remote host")
332
333parser_run_mock.add_argument(
334    'host',
335    help="Host where the application is running")
336
337parser_run_mock.add_argument(
338    'script',
339    help="script (in bin/) to run")
340
341parser_run_mock.add_argument(
342    '--user', metavar='USERNAME',
343    default="www-data",
344    help="The user to run the command as; default is www-data.  "
345    "Other options are www-mgr and root")
346
347parser_backup = subcommands.add_parser(
348    'backup', help="Backup a deployed application's data")
349
350parser_backup.add_argument(
351    'location', metavar="LOCATION",
352    help="The location to back up (i.e., the server and application)")
353
354parser_backup.add_argument(
355    'destination', metavar='DESTINATION',
356    help="Destination to back up to.  Can be a local file, remote:filename, "
357    "site://location, ssh://hostname/location rsync://hostname/location")
358
359parser_restore = subcommands.add_parser(
360    'restore', help="Restore a backup")
361
362parser_restore.add_argument(
363    'backup', metavar="BACKUP",
364    help="The backup to restore (file or archive)")
365
366parser_restore.add_argument(
367    'location', metavar="LOCATION",
368    help="Location (hostname[/path]) to restore to")
369
370parser_clear = subcommands.add_parser(
371    'clear', help="Clear the data from an app")
372
373parser_clear.add_argument(
374    'location', metavar="LOCATION",
375    help="Location (hostname[/path]) to clear")
376
377parser_diff = subcommands.add_parser(
378    'diff', help="Diff a local app and a deployed app")
379
380parser_diff.add_argument(
381    'dir', metavar='DIR',
382    help="The application to diff")
383
384parser_diff.add_argument(
385    'location', metavar='HOST', nargs='?',
386    help="the host to diff with")
387
388parser_diff.add_argument(
389    '--app-name', metavar='NAME',
390    help="A name besides app.ini:app_name")
391
392parser_diff.add_argument(
393    '--instance-name', metavar='APP_NAME.DATE_VERSION',
394    help="Diff with a specific instance (not the active one)")
395
396parser_create_config = subcommands.add_parser(
397    'create-config', help="Create configuration for an application")
398
399parser_create_config.add_argument(
400    'dir', metavar='DIR',
401    help="The application to generate configuration for")
402
403parser_create_config.add_argument(
404    'output', nargs='?', metavar='DIR',
405    help="The place to put the new configuration")
406
407parser_create_config.add_argument(
408    'variable', nargs='*',
409    help="Assign a variable to use for generating the configuration")
410
411parser_create_config.add_argument(
412    '--info', action='store_true',
413    help="Show information about how the configuration is created")
414
415parser_disable = subcommands.add_parser(
416    'disable', help="Temporarily disable an application")
417
418parser_enable = subcommands.add_parser(
419    'enable', help="Re-enable a disabled application")
420
421for subparser in (parser_disable, parser_enable):
422    group = subparser.add_mutually_exclusive_group(required=True)
423    group.add_argument('--by-name', metavar="APPNAME",
424                       help="Identify the application by its name")
425    group.add_argument('--by-location', metavar="LOCATION",
426                       help="Identify the application by its location")
427    subparser.add_argument('--node', metavar="NODE_HOSTNAME",
428                           help="Node to act on")
429
430for subparser in subcommands._name_parser_map.values():
431    subparser.add_argument(
432        '-p', '--provider',
433        metavar='NAME',
434        help="The [provider:NAME] section from ~/.silverlining.conf to use (default [provider:default])",
435        default="default")
436
437    subparser.add_argument(
438        '-y', '--yes',
439        action='store_true',
440        help="Answer yes to any questions")
441
442    add_verbose(subparser, add_log=True)
443
444
445def catch_error(func):
446    """Catch CommandError and turn it into an error message"""
447    def decorated(*args, **kw):
448        try:
449            return func(*args, **kw)
450        except CommandError, e:
451            print e
452            sys.exit(2)
453    return decorated
454
455
456@catch_error
457def main():
458    if not os.path.exists(createconf.silverlining_conf):
459        print "%s doesn't exists; let's create it" % createconf.silverlining_conf
460        createconf.create_conf()
461        return
462    if len(sys.argv) > 1 and sys.argv[1] == 'run':
463        # Special case for silver run:
464        if sys.argv[2:] in [['-h'], ['--help']]:
465            # Don't generally catch this, as you might want to pass this option
466            print parser_run.format_help()
467            return 0
468        args, unknown_args = parser_run.parse_known_args(sys.argv[2:])
469        args.unknown_args = unknown_args
470        args.command = 'run'
471    else:
472        args = parser.parse_args()
473    create_logger(args)
474    config = Config.from_config_file(
475        createconf.silverlining_conf, 'provider:'+args.provider,
476        args)
477    name = args.command.replace('-', '_')
478    mod_name = 'silverlining.commands.%s' % name
479    __import__(mod_name)
480    mod = sys.modules[mod_name]
481    func = getattr(mod, 'command_%s' % name)
482    return func(config)
483
484
485if __name__ == '__main__':
486    main()