Source

silverlining / silverlining / runner.py

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
"""silverlining interface

This implements the command-line interface of silverlining.  It includes
all the commands and their options, though the implementation of the
commands are in ``silverlining.commands.*``
"""

import os
import sys
import argparse
from cmdutils.arg import add_verbose, create_logger
from cmdutils import CommandError
from silverlining import createconf
from silverlining.config import Config


## Following is a HUGE BLOCK of argument definitions:

description = """\
Runs a variety of deployment-related commands
"""

parser = argparse.ArgumentParser(
    description=description)

parser.add_argument(
    '-p', '--provider',
    metavar='NAME',
    help="The [provider:NAME] section from ~/.silverlining.conf to use (default [provider:default])",
    default="default")

parser.add_argument(
    '-y', '--yes',
    action='store_true',
    help="Answer yes to any questions")

parser.add_argument(
    '--debug-libcloud', metavar='FILENAME',
    help="Write any libcloud interactions (HTTP request log)")

add_verbose(parser, add_log=True)

subcommands = parser.add_subparsers(dest="command")

parser_list_images = subcommands.add_parser(
    'list-images', help="List all images available")

parser_list_sizes = subcommands.add_parser(
    'list-sizes', help="List all sizes available")

parser_list_nodes = subcommands.add_parser(
    'list-nodes', help="List all active nodes")

parser_destroy = subcommands.add_parser(
    'destroy-node', help="Destroy a node")

parser_destroy.add_argument(
    'nodes', nargs='+',
    metavar='HOSTNAME',
    help="The hostname(s) of the node to destroy")

parser_create = subcommands.add_parser(
    'create-node', help="Create a new node")

parser_create.add_argument(
    'node',
    metavar='HOSTNAME',
    help="The hostname of the node to create")

parser_create.add_argument(
    '--image',
    default='name *lucid*',
    metavar="IMAGE",
    help='Image to use, can be "id 10", and can contain wildcards like "name *karmic*".  '
    'Default is "name *karmic*" which will select an Ubuntu Karmic image.')

parser_create.add_argument(
    '--size',
    metavar="SIZE",
    default="ram 256",
    help='Size to use, can be "id 1", "ram 256" etc')

parser_create.add_argument(
    '--setup-node', action='store_true',
    help="Wait for the node to be created (this means the command just "
    "sits for a couple minutes) and then set up the server.  It is suggested "
    "you also use --yes with this option.")

parser_create.add_argument(
    '--wait', action='store_true',
    help="Wait for the node to be created, but don't setup (like "
    "--setup-node that just quits before it actually sets up the node)")

parser_create.add_argument(
    '--if-not-exists', action='store_true',
    dest='if_not_exists',
    help="Only create the node if it does not exist (will check the cloud "
    "providers server list)")

parser_setup = subcommands.add_parser(
    'setup-node', help="Setup a new (fresh Ubuntu Jaunty install) server")

parser_setup.add_argument(
    'node',
    metavar='HOSTNAME',
    help="The hostname of the node to setup")

parser_clean = subcommands.add_parser(
    'clean-node', help="Clean unused application instances on a node")

parser_clean.add_argument(
    'node', nargs='?',
    metavar='HOSTNAME',
    help="Node to clean instances from")

parser_clean.add_argument(
    '-n', '--simulate',
    action='store_true',
    help="Don't actually clean anything, just show what would be done")

parser_update = subcommands.add_parser(
    'update', help="Update/deploy an application")

parser_update.add_argument(
    'dir',
    help="The directory to upload to the server")

parser_update.add_argument(
    'location', nargs='?',
    metavar="HOSTNAME[/PATH]",
    help="Place to upload to (will default to the default_location "
    "setting in app.ini)")

parser_update.add_argument(
    '--debug-single-process',
    action='store_true',
    help="Install as a 'debug' application, running in a single process with "
    "threads, so the application can be used with weberror or other debug "
    "tools.")

parser_update.add_argument(
    '--name',
    metavar="NAME",
    help="'Name' of the site; defaults to the app_name setting in app.ini")

parser_update.add_argument(
    '--node',
    metavar='NODE_HOSTNAME',
    help="The hostname of the node to upload to")

parser_update.add_argument(
    '--clear',
    action='store_true',
    help='Clear the database on update (clears all database, files, etc!)')

parser_update.add_argument(
    '--config',
    metavar='CONFIG_DIR',
    help="Configuration to use for this deployment of the application")

parser_update.add_argument(
    '--noetchosts', help="Do not try to update /etc/hosts",
    dest='update_etc_hosts', action='store_const', const=False, default=True)

parser_init = subcommands.add_parser(
    'init', help="Create a new application file layout")

parser_init.add_argument(
    'dir',
    metavar='DIR',
    help="A directory to initialize")

parser_init.add_argument(
    '-f', '--force',
    action='store_true',
    help="Overwrite files even if they already exist")

parser_init.add_argument(
    '--distribute',
    action='store_true',
    help="Use Distribute (instead of Setuptools)")

parser_init.add_argument(
    '--config',
    action='store_true',
    help="Put in a sample Paste Deploy config.ini")

parser_init.add_argument(
    '--main',
    action='store_true',
    help="Put in a sample main.py")

parser_serve = subcommands.add_parser(
    'serve', help="Serve up an application for development")

parser_serve.add_argument(
    'dir',
    metavar='APP_DIR',
    help='Directory holding app')

parser_serve.add_argument(
    '--port',
    metavar='PORT',
    default='8080',
    help='Port to serve on (default 8080)')

parser_serve.add_argument(
    '--host',
    metavar='IP/INTERFACE',
    default='127.0.0.1',
    help='Host/IP/interface to serve on (127.0.0.1 is private, 0.0.0.0 is public)')

parser_serve.add_argument(
    '--config',
    metavar='CONFIG_DIR',
    help="Configuration to use for the application")

## We can't handle "silver run" well with a subparser, because there's
## a bug in subparsers that they can't ignore arguments they don't
## understand.  Because there will be arguments passed to the remote
## command we need that, so instead we create an entirely separate
## parser, and we'll do a little checking to see if the run command is
## given:

parser_run = argparse.ArgumentParser(
    add_help=False,
    description="""\
Run a command for an application; this runs a script in bin/ on the
remote server.

Use it like:
    silver run LOCATION import-something --option-for-import-something

Note any arguments that point to existing files or directories will
cause those files/directories to be uploaded, and substituted with the
location of the remote name.

This will run *Python* scripts in your application's bin/ directory,
in another directory (e.g., silver run LOCATION src/myapp/script.py),
or a global command (e.g., ipython).

Use 'silver run LOCATION python' to get an interactive prompt on the
remote server.
""")

parser_run.add_argument(
    '--node',
    metavar="NODE_HOSTNAME",
    help="Node to run command on")

parser_run.add_argument(
    '-p', '--provider',
    metavar='NAME',
    help="The [provider:NAME] section from ~/.silverlining.conf to use (default [provider:default])",
    default="default")

parser_run.add_argument(
    '-y', '--yes',
    action='store_true',
    help="Answer yes to any questions")

parser_run.add_argument(
    '-i', '--interactive',
    action='store_true',
    help=("Tells ssh to force pseudo-tty allocation.  Useful when what you're"
          " running is a shell of some sort"))

#add_verbose(parser_run, add_log=True)

parser_run.add_argument(
    'location',
    help="Location where the application is running")

parser_run.add_argument(
    'script',
    help="script (in bin/) to run")

parser_run.add_argument(
    '--user', metavar='USERNAME',
    default="www-data",
    help="The user to run the command as; default is www-data.  "
    "Other options are www-mgr and root")

parser_query = subcommands.add_parser(
    'query', help="See what apps are on a node")

parser_query.add_argument(
    'node',
    metavar='HOSTNAME',
    help="Node to query")

parser_query.add_argument(
    'app-name', nargs='*',
    help="The app_name or hostname to query (wildcards allowed)")

parser_activate = subcommands.add_parser(
    'activate', help="Activate a site instance for a given domain")

parser_activate.add_argument(
    '--node',
    metavar="NODE_HOSTNAME",
    help="Node to act on")

parser_activate.add_argument(
    'location',
    help="The hostname/path to act on")

parser_activate.add_argument(
    'instance_name',
    help="The instance name to activate (can also be \"prev\")")

parser_deactivate = subcommands.add_parser(
    'deactivate', help="Deactivate a site (leaving it dangling)")

parser_deactivate.add_argument(
    '--node',
    metavar="NODE_HOSTNAME",
    help="Node to act on")

parser_deactivate.add_argument(
    'locations', nargs='+',
    help="The hostname/path to act on; if you give more than one, "
    "they must all be on the same node.  Can be a wildcard.")

parser_deactivate.add_argument(
    '--keep-prev', action='store_true',
    help="Keep the prev.* host activate (by default it is deleted)")

# This is a mock for use with -h:
parser_run_mock = subcommands.add_parser(
    'run', help="Run a command on a remote host")

parser_run_mock.add_argument(
    'host',
    help="Host where the application is running")

parser_run_mock.add_argument(
    'script',
    help="script (in bin/) to run")

parser_run_mock.add_argument(
    '--user', metavar='USERNAME',
    default="www-data",
    help="The user to run the command as; default is www-data.  "
    "Other options are www-mgr and root")

parser_backup = subcommands.add_parser(
    'backup', help="Backup a deployed application's data")

parser_backup.add_argument(
    'location', metavar="LOCATION",
    help="The location to back up (i.e., the server and application)")

parser_backup.add_argument(
    'destination', metavar='DESTINATION',
    help="Destination to back up to.  Can be a local file, remote:filename, "
    "site://location, ssh://hostname/location rsync://hostname/location")

parser_restore = subcommands.add_parser(
    'restore', help="Restore a backup")

parser_restore.add_argument(
    'backup', metavar="BACKUP",
    help="The backup to restore (file or archive)")

parser_restore.add_argument(
    'location', metavar="LOCATION",
    help="Location (hostname[/path]) to restore to")

parser_clear = subcommands.add_parser(
    'clear', help="Clear the data from an app")

parser_clear.add_argument(
    'location', metavar="LOCATION",
    help="Location (hostname[/path]) to clear")

parser_diff = subcommands.add_parser(
    'diff', help="Diff a local app and a deployed app")

parser_diff.add_argument(
    'dir', metavar='DIR',
    help="The application to diff")

parser_diff.add_argument(
    'location', metavar='HOST', nargs='?',
    help="the host to diff with")

parser_diff.add_argument(
    '--app-name', metavar='NAME',
    help="A name besides app.ini:app_name")

parser_diff.add_argument(
    '--instance-name', metavar='APP_NAME.DATE_VERSION',
    help="Diff with a specific instance (not the active one)")

parser_create_config = subcommands.add_parser(
    'create-config', help="Create configuration for an application")

parser_create_config.add_argument(
    'dir', metavar='DIR',
    help="The application to generate configuration for")

parser_create_config.add_argument(
    'output', nargs='?', metavar='DIR',
    help="The place to put the new configuration")

parser_create_config.add_argument(
    'variable', nargs='*',
    help="Assign a variable to use for generating the configuration")

parser_create_config.add_argument(
    '--info', action='store_true',
    help="Show information about how the configuration is created")

parser_disable = subcommands.add_parser(
    'disable', help="Temporarily disable an application")

parser_enable = subcommands.add_parser(
    'enable', help="Re-enable a disabled application")

for subparser in (parser_disable, parser_enable):
    group = subparser.add_mutually_exclusive_group(required=True)
    group.add_argument('--by-name', metavar="APPNAME",
                       help="Identify the application by its name")
    group.add_argument('--by-location', metavar="LOCATION",
                       help="Identify the application by its location")
    subparser.add_argument('--node', metavar="NODE_HOSTNAME",
                           help="Node to act on")

for subparser in subcommands._name_parser_map.values():
    subparser.add_argument(
        '-p', '--provider',
        metavar='NAME',
        help="The [provider:NAME] section from ~/.silverlining.conf to use (default [provider:default])",
        default="default")

    subparser.add_argument(
        '-y', '--yes',
        action='store_true',
        help="Answer yes to any questions")

    add_verbose(subparser, add_log=True)


def catch_error(func):
    """Catch CommandError and turn it into an error message"""
    def decorated(*args, **kw):
        try:
            return func(*args, **kw)
        except CommandError, e:
            print e
            sys.exit(2)
    return decorated


@catch_error
def main():
    if not os.path.exists(createconf.silverlining_conf):
        print "%s doesn't exists; let's create it" % createconf.silverlining_conf
        createconf.create_conf()
        return
    if len(sys.argv) > 1 and sys.argv[1] == 'run':
        # Special case for silver run:
        if sys.argv[2:] in [['-h'], ['--help']]:
            # Don't generally catch this, as you might want to pass this option
            print parser_run.format_help()
            return 0
        args, unknown_args = parser_run.parse_known_args(sys.argv[2:])
        args.unknown_args = unknown_args
        args.command = 'run'
    else:
        args = parser.parse_args()
    create_logger(args)
    config = Config.from_config_file(
        createconf.silverlining_conf, 'provider:'+args.provider,
        args)
    name = args.command.replace('-', '_')
    mod_name = 'silverlining.commands.%s' % name
    __import__(mod_name)
    mod = sys.modules[mod_name]
    func = getattr(mod, 'command_%s' % name)
    return func(config)


if __name__ == '__main__':
    main()