~barryprice/juju-deployer/LP1892423

« back to all changes in this revision

Viewing changes to deployer.py

  • Committer: Adam Gandelman
  • Date: 2013-09-03 20:44:14 UTC
  • mfrom: (69.3.45 darwin)
  • Revision ID: adamg@canonical.com-20130903204414-xsqqz2gp83dp5d2o
MergeĀ lp:juju-deployer/darwin.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
#!/usr/bin/python
2
 
# TODO: Add expose
3
 
import tempfile
4
 
import subprocess
5
 
import time
6
 
import os
7
 
import yaml
8
 
import optparse
9
 
import pprint
10
 
import signal
11
 
import __builtin__
12
 
 
13
 
from os.path import dirname, abspath
14
 
from pdb import *
15
 
from utils import *
16
 
 
17
 
 
18
 
def timed_out(signal, frame):
19
 
    log.error("Deployment timed out after %s sec.", opts.timeout)
20
 
    exit(1)
21
 
 
22
 
start_time = time.time()
23
 
 
24
 
parser = optparse.OptionParser()
25
 
parser.add_option('-c', '--config',
26
 
                  help=('File containing deployment(s) config. This '
27
 
                        'option can be repeated, with later files overriding '
28
 
                        'values in earlier ones.'),
29
 
                  dest='configs', action='append')
30
 
parser.add_option('-d', '--debug', help='Enable debugging to stdout',
31
 
                  dest="debug",
32
 
                  action="store_true", default=False)
33
 
parser.add_option('-L', '--local-mods',
34
 
                  help='Allow deployment of locally-modified charms',
35
 
                  dest="no_local_mods", default=True, action='store_false')
36
 
parser.add_option('-u', '--update-charms',
37
 
                  help='Update existing charm branches',
38
 
                  dest="update_charms", default=False, action="store_true")
39
 
parser.add_option('-l', '--ls', help='List available deployments',
40
 
                  dest="list_deploys", action="store_true", default=False)
41
 
parser.add_option('-D', '--destroy-services',
42
 
                  help='Destroy all services (do not terminate machines)',
43
 
                  dest="destroy_services", action="store_true",
44
 
                  default="False")
45
 
parser.add_option('-S', '--scrub-zk', action='store_true', default=False,
46
 
                  dest='scrub_zk',
47
 
                  help='Remove charm nodes from ZK after service destroy.')
48
 
parser.add_option('-T', '--terminate-machines',
49
 
                  help=('Terminate all machines but the bootstrap node.  '
50
 
                        'Destroy any services that exist on each'),
51
 
                  dest="terminate_machines", action="store_true",
52
 
                  default="False")
53
 
parser.add_option('-t', '--timeout',
54
 
                  help='Timeout (sec) for entire deployment (45min default)',
55
 
                  dest='timeout', action='store', type='int', default=2700)
56
 
parser.add_option("-f", '--find-service', action="store", type="string",
57
 
                  help='Find hostname from first unit of a specific service.',
58
 
                  dest="find_service")
59
 
parser.add_option("-m", '--max-concurrent', action="store", type="int",
60
 
                  help=("Maximum number of concurrent deployments to send "
61
 
                        " to provider. Default: no limit"),
62
 
                  dest="max_concur_deploy", default=0)
63
 
parser.add_option('-s', '--deploy-delay', action='store', type='float',
64
 
                  help=("Time in seconds to sleep between 'deploy' commands, "
65
 
                        "to allow machine provider to process requests. This "
66
 
                        "delay is also enforced between calls to"
67
 
                        "terminate_machine"),
68
 
                  dest="deploy_delay", default=0)
69
 
parser.add_option('-e', '--environment', action='store', dest='juju_env',
70
 
                  help='Deploy to a specific Juju environment.',
71
 
                  default=os.getenv('JUJU_ENV'))
72
 
parser.add_option('-o', '--override', action='append', type='string',
73
 
                  help=('Override *all* config options of the same name '
74
 
                        'across all services.  Input as key=value.'),
75
 
                  dest='overrides', default=None)
76
 
parser.add_option('-w', '--relation-wait', action='store', dest='rel_wait',
77
 
                  default='60',
78
 
                  help=('Number of seconds to wait before checking for '
79
 
                        'relation errors after all relations have been added '
80
 
                        'and subordinates started. (default: 60)'))
81
 
(opts, args) = parser.parse_args()
82
 
 
83
 
if not opts.configs:
84
 
    opts.configs = ['deployments.cfg']
85
 
update_charms = opts.update_charms
86
 
 
87
 
# temporarily abuse __builtin__ till this is setup properly
88
 
__builtin__.juju_log = juju_log = open("juju.log", "w")
89
 
__builtin__.juju_cmds = []
90
 
 
91
 
init_logging("debug.log", opts.debug)
92
 
 
93
 
ORIGCWD = os.getcwd()
94
 
 
95
 
if opts.destroy_services is True or opts.terminate_machines is True:
96
 
    destroy_all(juju_status(opts.juju_env), opts.juju_env,
97
 
                terminate_machines=opts.terminate_machines,
98
 
                scrub_zk=opts.scrub_zk,
99
 
                delay=opts.deploy_delay)
100
 
    exit(0)
101
 
 
102
 
if opts.find_service is not None:
103
 
    rc = find_service(juju_status(opts.juju_env), opts.find_service)
104
 
    exit(rc)
105
 
 
106
 
# load the configuration for possible deployments.
107
 
missing_configs = [c for c in opts.configs if not os.path.exists(c)]
108
 
if missing_configs:
109
 
    log.error("Configuration not found: {}".format(", ".join(missing_configs)))
110
 
    exit(1)
111
 
 
112
 
debug_msg("Loading deployments from {}".format(", ".join(opts.configs)))
113
 
cfg, include_dirs = load_config(opts.configs)
114
 
 
115
 
if opts.list_deploys:
116
 
    display_deploys(cfg)
117
 
    exit(0)
118
 
 
119
 
if not args:
120
 
    log.error("You must specify a deployment.")
121
 
    display_deploys(cfg)
122
 
    exit(1)
123
 
 
124
 
deployment = args[0]
125
 
SERIES, CHARMS, RELATIONS, overrides = load_deployment(cfg, deployment)
126
 
series_store = "%s/%s" % (ORIGCWD, SERIES)
127
 
 
128
 
# series store ends up being the local juju charm repository
129
 
if not os.path.exists(series_store):
130
 
    debug_msg("Creating series charm store: %s" % series_store)
131
 
    os.mkdir(series_store)
132
 
else:
133
 
    debug_msg("Series charm store already exists: %s" % series_store)
134
 
 
135
 
# either clone all charms if we dont have them or update branches
136
 
for k in CHARMS.keys():
137
 
    charm_path = "%s/%s" % (series_store, k)
138
 
    debug_msg("Charm '%s' - using charm path '%s'" % (k, charm_path))
139
 
    (branch, sep, revno) = CHARMS[k].get("branch", '').partition('@')
140
 
    needs_build = update_charms
141
 
    if branch:
142
 
        debug_msg("Branch: {}, revision: {}".format(branch, revno))
143
 
    else:
144
 
        debug_msg("No remote branch specified")
145
 
        needs_build = False
146
 
    if os.path.exists(charm_path):
147
 
        if opts.no_local_mods:
148
 
            with cd(charm_path):
149
 
                # is there a better way to check for changes?
150
 
                bzrstatus = subprocess.check_output(['bzr', 'st']).strip()
151
 
                if bzrstatus not in (
152
 
                    "", "working tree is out of date, run 'bzr update'"):
153
 
                    log.error("Charm is locally modified: {}".format(
154
 
                        charm_path))
155
 
                    log.error("Aborting")
156
 
                    exit(1)
157
 
        debug_msg("Charm path exists @ %s." % charm_path)
158
 
        if update_charms and branch:
159
 
            debug_msg("Updating charm branch '%s'" % k)
160
 
            code = subprocess.call(
161
 
                ["bzr", "pull", "-d", charm_path, '--remember', branch])
162
 
            if code != 0:
163
 
                log.error("Could not update branch at {} from {}".format(
164
 
                    charm_path, branch))
165
 
                exit(code)
166
 
    elif branch:
167
 
        print "- Cloning %s from %s" % (k, branch)
168
 
        subprocess.call(["bzr", "branch", branch, charm_path])
169
 
        needs_build = True
170
 
    if revno:
171
 
        cmd = ["bzr", "update", charm_path]
172
 
        revno != 'tip' and cmd.extend(['-r', revno])
173
 
        code = subprocess.call(cmd)
174
 
        if code != 0:
175
 
            log.error("Unable to check out branch revision {}".format(revno))
176
 
            exit(code)
177
 
    if CHARMS[k].get("build") is not None and needs_build:
178
 
        cmd = CHARMS[k]["build"]
179
 
        debug_msg("Running build command at {}...".format(charm_path))
180
 
        with cd(charm_path):
181
 
            code = subprocess.call(cmd)
182
 
        if code != 0:
183
 
            log.error("Failed to build charm {}".format(k))
184
 
            exit(code)
185
 
    # load charms metadata
186
 
    if not os.path.isdir(charm_path):
187
 
        print "Branch for {} does not exist ({})".format(k, charm_path)
188
 
        exit(1)
189
 
    mdf = open("%s/metadata.yaml" % charm_path, "r")
190
 
    debug_msg("Loading metadata from %s/metadata.yaml" % charm_path)
191
 
    CHARMS[k]["metadata"] = yaml.load(mdf)
192
 
    mdf.close()
193
 
    # load charms config.yaml if it has one
194
 
    if os.path.exists("%s/config.yaml" % charm_path):
195
 
        debug_msg("Loading config.yaml from %s/config.yaml" % charm_path)
196
 
        conf = open("%s/config.yaml" % charm_path)
197
 
        CHARMS[k]["config"] = yaml.safe_load(conf)["options"]
198
 
        conf.close()
199
 
    if "units" not in CHARMS[k].keys():
200
 
        CHARMS[k]["units"] = 1
201
 
 
202
 
if opts.overrides:
203
 
    for override in opts.overrides:
204
 
        spl = override.split('=')
205
 
        key = spl[0]
206
 
        value = '='.join(spl[1:])
207
 
        overrides[key] = value
208
 
 
209
 
# apply overrides to relevant charms
210
 
for k, v in overrides.iteritems():
211
 
    for svc in CHARMS:
212
 
        if k in CHARMS[svc]['config']:
213
 
            if 'options' not in CHARMS[svc]:
214
 
                CHARMS[svc]['options'] = {}
215
 
 
216
 
            CHARMS[svc]['options'][k] = v
217
 
 
218
 
# create a temporary deploy-time config yaml
219
 
temp = tempfile.NamedTemporaryFile()
220
 
deploy_config = temp.name
221
 
CONFIG = generate_deployment_config(temp, CHARMS, include_dirs)
222
 
log.debug("Using the following config:\n%s", pprint.pformat(CONFIG))
223
 
 
224
 
# make sure we're bootstrapped
225
 
status = juju_status(opts.juju_env)
226
 
if status == 1:
227
 
    log.error("Is juju bootstrapped?")
228
 
    exit(1)
229
 
if (status["machines"][0]["instance-state"] != "provisioned" and
230
 
    status["machines"][0]["agent-state"] != "running"):
231
 
    log.error("Bootstrap node not running?")
232
 
    exit(1)
233
 
 
234
 
debug_msg("Deploying with timeout %s sec." % opts.timeout)
235
 
signal.signal(signal.SIGALRM, timed_out)
236
 
signal.alarm(opts.timeout)
237
 
 
238
 
# figure out what needs to be done
239
 
to_deploy = []
240
 
for c in CHARMS.keys():
241
 
    if c not in status["services"].keys():
242
 
        to_deploy.append(c)
243
 
    else:
244
 
        print "* Services '%s' already deployed. Skipping" % c
245
 
 
246
 
if (len(to_deploy) < opts.max_concur_deploy or opts.max_concur_deploy == 0):
247
 
    groups_of = len(to_deploy)
248
 
else:
249
 
    groups_of = opts.max_concur_deploy
250
 
 
251
 
start_groups = []
252
 
if to_deploy:
253
 
    # go through to_deploy in chunks of group_by
254
 
    start_groups = [to_deploy[i:i + groups_of]
255
 
                    for i in range(0, len(to_deploy), groups_of)]
256
 
    for group_num in range(0, len(start_groups)):
257
 
        for c in start_groups[group_num]:
258
 
            print ("- Deploying %s in group %d/%d" %
259
 
                   (c, group_num + 1, len(start_groups)))
260
 
            cmd = "deploy"
261
 
            if "units" in CHARMS[c]:
262
 
                cmd += " -n %s" % CHARMS[c]["units"]
263
 
            if "constraints" in CHARMS[c]:
264
 
                cmd += " --constraints=%s" % CHARMS[c]["constraints"]
265
 
            if c in CONFIG.keys():
266
 
                cmd += " --config=%s" % deploy_config
267
 
            cmd += " --repository=%s local:%s %s" % (
268
 
                ORIGCWD, CHARMS[c]["metadata"]["name"], c)
269
 
            if opts.juju_env:
270
 
                cmd += " -e %s" % opts.juju_env
271
 
            juju_call(cmd)
272
 
            if opts.deploy_delay > 0:
273
 
                debug_msg("Delaying %s sec. between deployment" %
274
 
                          opts.deploy_delay)
275
 
                time.sleep(opts.deploy_delay)
276
 
        wait_for_started(opts.debug, opts.juju_env, sleep=3.0,
277
 
                         msg="- Waiting for started: %s" %
278
 
                         start_groups[group_num])
279
 
 
280
 
if len(start_groups) != 0:
281
 
    status = juju_status(opts.juju_env)
282
 
 
283
 
# add additional units to any services that want/need them
284
 
# TODO: need max_concur_deploy support here also
285
 
for c in CHARMS.keys():
286
 
    if CHARMS[c]["units"] > 1:
287
 
        if len(status["services"][c]["units"]) < CHARMS[c]["units"]:
288
 
            needed_units = (int(CHARMS[c]["units"]) -
289
 
                            len(status["services"][c]["units"].keys()))
290
 
            if needed_units > 0:
291
 
                print "- Adding %d more units to %s" % (needed_units, c)
292
 
                cmd = "add-unit --num-units %d %s" % (needed_units, c)
293
 
                juju_call(cmd)
294
 
        else:
295
 
            debug_msg("Service '%s' does not need any more units added." % c)
296
 
 
297
 
# poll juju status until all services report strated. fail on any error
298
 
wait_for_started(opts.debug, opts.juju_env,
299
 
                 "- Waiting for all service units to reach 'started' state.")
300
 
 
301
 
# add all relations, ordered by weight
302
 
if RELATIONS:
303
 
    print "- Adding relations:"
304
 
    for w in sorted(RELATIONS, reverse=True):
305
 
        for r in RELATIONS[w]:
306
 
            print "  -> Relation: %s <-> %s" % (r[0], r[1])
307
 
            cmd = "add-relation %s %s" % (r[0], r[1])
308
 
            if opts.juju_env:
309
 
                cmd += " -e %s" % opts.juju_env
310
 
            juju_call(cmd, ignore_failure=True)
311
 
            # to be safe
312
 
            time.sleep(5)
313
 
 
314
 
# Subordinates units spawn after relations have been added.
315
 
# Ensure they're started, if they exist.
316
 
wait_for_subordinates_started(opts.debug, opts.juju_env)
317
 
 
318
 
if RELATIONS:
319
 
    print "- Sleeping for %s before ensuring relation state." % opts.rel_wait
320
 
    # give all relations a minute to settle down, and make sure no errors are
321
 
    # reported.
322
 
    time.sleep(float(opts.rel_wait))
323
 
 
324
 
if not ensure_relations_up(juju_status(opts.juju_env)):
325
 
    exit(1)
326
 
 
327
 
print ("- Deployment complete in %d seconds.\n\n" %
328
 
       (int(time.time() - start_time)))
329
 
 
330
 
print "- Juju command log:"
331
 
for c in __builtin__.juju_cmds:
332
 
    print c