/usr/lib/python2.7/dist-packages/quickstart/app.py is in juju-quickstart 1.3.1-0ubuntu1.
This file is owned by root:root, with mode 0o644.
The actual contents of the file can be viewed below.
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 | # This file is part of the Juju Quickstart Plugin, which lets users set up a
# Juju environment in very few steps (https://launchpad.net/juju-quickstart).
# Copyright (C) 2013-2014 Canonical Ltd.
#
# This program is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License version 3, as published by
# the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
# SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""Juju Quickstart base application functions."""
from __future__ import (
print_function,
unicode_literals,
)
import json
import logging
import os
import sys
import time
import jujuclient
from quickstart import (
juju,
settings,
ssh,
utils,
watchers,
)
from quickstart.models import envs
class ProgramExit(Exception):
"""An error occurred while setting up the Juju environment.
Raise this exception if you want the program to exit gracefully printing
the error message to stderr.
The error message can be either a unicode or a byte string.
"""
def __init__(self, message):
if isinstance(message, unicode):
message = message.encode('utf-8')
self.message = message
def __str__(self):
return b'juju-quickstart: error: {}'.format(self.message)
def ensure_dependencies(distro_only):
"""Ensure that Juju and LXC are installed.
If the "juju" command is not found in the PATH, then install and setup
Juju, including the packages required to bootstrap local environments.
This is usually done by adding the Juju stable PPA and installing the
juju-core and juju-local packages.
If distro_only is True, the above PPA is not added to the apt sources, and
we assume Juju packages are already available in the distro repository.
Return the Juju version tuple when Juju is available.
Raise a ProgramExit if an error occurs installing packages or retrieving
the Juju version.
"""
required_packages = []
# Check if juju is installed.
try:
juju_version = utils.get_juju_version()
except ValueError:
# Juju is not installed or configured properly. To ensure everything
# is set up correctly, also install packages required to run
# environments using the local provider.
required_packages.extend(['juju-core', 'juju-local'])
juju_version = None
else:
# Check if LXC is installed.
retcode = utils.call('/usr/bin/lxc-ls')[0]
if retcode:
# Some packages (e.g. lxc and mongodb-server) are required to
# support bootstrapping environments using the local provider.
# All those packages are installed as juju-local dependencies.
required_packages.append('juju-local')
if required_packages:
if not distro_only:
try:
utils.add_apt_repository('ppa:juju/stable')
except OSError as err:
raise ProgramExit(bytes(err))
print('sudo privileges will be used for the installation of \n'
'the following packages: {}\n'
'this can take a while...'.format(', '.join(required_packages)))
retcode, _, error = utils.call(
'sudo', '/usr/bin/apt-get', 'install', '-y', *required_packages)
if retcode:
raise ProgramExit(error)
# Return the current Juju version.
if juju_version is None:
# Juju has been just installed, retrieve its version.
try:
juju_version = utils.get_juju_version()
except ValueError as err:
raise ProgramExit(bytes(err))
return juju_version
def ensure_ssh_keys():
"""Ensure that SSH keys are available.
Allow the user to let quickstart create SSH keys, or quit by raising a
ProgramExit if they would like to create the key themselves.
"""
try:
# Test to see if we have ssh-keys loaded into the ssh-agent, or if we
# can add them to the currently running ssh-agent.
if ssh.check_keys():
return
# No responsive agent was found. Start one up.
ssh.start_agent()
# And now check again.
if ssh.check_keys():
return
except OSError as err:
raise ProgramExit(bytes(err))
# At this point we have no SSH keys.
print('Warning: no SSH keys were found in ~/.ssh\n'
'To proceed and generate keys, quickstart can\n'
'[a] automatically create keys for you\n'
'[m] provide commands to manually create your keys\n\n'
'Note: ssh-keygen will prompt you for an optional\n'
'passphrase to generate your key for you.\n'
'Quickstart does not store it.\n')
try:
answer = raw_input(
'Automatically create keys [a], manually create the keys [m], '
'or cancel [c]? ').lower()
except KeyboardInterrupt:
answer = ''
try:
if answer == 'a':
ssh.create_keys()
elif answer == 'm':
ssh.watch_for_keys()
else:
sys.exit(
b'\nIf you would like to create the keys yourself,\n'
b'please run this command, follow its instructions,\n'
b'and then re-run quickstart:\n'
b' ssh-keygen -b 4096 -t rsa'
)
except OSError as err:
raise ProgramExit(bytes(err))
def bootstrap(env_name, requires_sudo=False, debug=False):
"""Bootstrap the Juju environment with the given name.
Do not bootstrap the environment if already bootstrapped.
Return a tuple (already_bootstrapped, series) in which:
- already_bootstrapped indicates whether the environment was already
bootstrapped;
- series is the bootstrap node Ubuntu series.
The is_local argument indicates whether the environment is configured to
use the local provider. If so, sudo privileges are requested in order to
bootstrap the environment.
If debug is True and the environment not bootstrapped, execute the
bootstrap command passing the --debug flag.
Raise a ProgramExit if any error occurs in the bootstrap process.
"""
already_bootstrapped = False
cmd = [settings.JUJU_CMD, 'bootstrap', '-e', env_name]
if requires_sudo:
cmd.insert(0, 'sudo')
if debug:
cmd.append('--debug')
retcode, _, error = utils.call(*cmd)
if retcode:
# XXX frankban 2013-11-13 bug 1252322: the check below is weak. We are
# relying on an error message in order to decide if the environment is
# already bootstrapped. Other possibilities include checking if the
# jenv file is present (in ~/.juju/environments/) and, if so, check the
# juju status. Unfortunately this is also prone to errors, because a
# jenv file can be there but the environment not really bootstrapped or
# functional (e.g. sync-tools was used, or a previous bootstrap failed,
# or the user terminated machines from the ec2 panel, etc.). Moreover
# jenv files seems to be an internal juju-core detail. Definitely we
# need to find a better way, but for now the "asking forgiveness"
# approach feels like the best compromise we have. Also note that,
# rather than comparing the expected error with the obtained one, we
# search in the error in order to support bootstrap --debug.
if 'environment is already bootstrapped' not in error:
# We exit if the error is not "already bootstrapped".
raise ProgramExit(error)
# Juju is bootstrapped, but we don't know if it is ready yet. Fall
# through to the next block for that check.
already_bootstrapped = True
print('reusing the already bootstrapped {} environment'.format(
env_name))
# Call "juju status" multiple times until the bootstrap node is ready.
# Exit with an error if the agent is not ready after ten minutes.
# Note: when using the local provider, calling "juju status" is very fast,
# but e.g. on ec2 the first call (right after "bootstrap") can take
# several minutes, and subsequent calls are relatively fast (seconds).
print('retrieving the environment status')
timeout = time.time() + (60*10)
while time.time() < timeout:
retcode, output, error = utils.call(
settings.JUJU_CMD, 'status', '-e', env_name, '--format', 'yaml')
if retcode:
continue
# Ensure the state server is up and the agent is started.
try:
agent_state = utils.get_agent_state(output)
except ValueError:
continue
if agent_state == 'started':
series = utils.get_bootstrap_node_series(output)
return already_bootstrapped, series
# If the agent is in an error state, there is nothing we can do, and
# it's not useful to keep trying.
if agent_state == 'error':
raise ProgramExit('state server failure:\n{}'.format(output))
details = ''.join(filter(None, [output, error])).strip()
raise ProgramExit('the state server is not ready:\n{}'.format(details))
def get_admin_secret(env_name, juju_home):
"""Read the admin-secret from the generated environment file.
At bootstrap, juju (v1.16 and later) writes the admin-secret to a
generated file located in JUJU_HOME. Return the value.
Raise a ValueError if:
- the environment file is not found;
- the environment file contents are not parsable by YAML;
- the YAML contents are not properly structured;
- the admin-secret is not found.
"""
filename = '{}.jenv'.format(env_name)
juju_env_file = os.path.expanduser(
os.path.join(juju_home, 'environments', filename))
jenv_db = envs.load_generated(juju_env_file)
try:
return jenv_db['admin-secret']
except KeyError:
msg = 'admin-secret not found in {}'.format(juju_env_file)
raise ValueError(msg.encode('utf-8'))
def get_api_url(env_name):
"""Return a Juju API URL for the given environment name.
Use the Juju CLI in a subprocess in order to retrieve the API addresses.
Return the complete URL, e.g. "wss://api.example.com:17070".
Raise a ProgramExit if any error occurs.
"""
retcode, output, error = utils.call(
settings.JUJU_CMD, 'api-endpoints', '-e', env_name, '--format', 'json')
if retcode:
raise ProgramExit(error)
# Assuming there is always at least one API address, grab the first one
# from the JSON output.
api_address = json.loads(output)[0]
return 'wss://{}'.format(api_address)
def connect(api_url, admin_secret):
"""Connect to the Juju API server and log in using the given credentials.
Return a connected and authenticated Juju Environment instance.
Raise a ProgramExit if any error occurs while establishing the WebSocket
connection or if the API returns an error response.
"""
try_count = 0
while True:
try:
env = juju.connect(api_url)
except Exception as err:
try_count += 1
msg = b'unable to connect to the Juju API server on {}: {}'.format(
api_url.encode('utf-8'), err)
if try_count >= 30:
raise ProgramExit(msg)
else:
logging.warn('Retrying: ' + msg)
time.sleep(1)
else:
break
try:
env.login(admin_secret)
except jujuclient.EnvError as err:
msg = 'unable to log in to the Juju API server on {}: {}'
raise ProgramExit(msg.format(api_url, err.message))
return env
def create_auth_token(env):
"""Return a new authentication token.
If the server does not support the request, return None. Raise any other
error."""
try:
result = env.create_auth_token()
except jujuclient.EnvError as err:
if err.message == 'unknown object type "GUIToken"':
# This is a legacy charm.
return None
else:
raise
return result['Token']
def deploy_gui(
env, service_name, machine, charm_url=None, check_preexisting=False):
"""Deploy and expose the given service, reusing the bootstrap node.
Only deploy the service if not already present in the environment.
Do not add a unit to the service if the unit is already there.
Receive an authenticated Juju Environment instance, the name of the
service, the machine where to deploy to (or None for a new machine),
the optional Juju GUI charm URL (e.g. cs:~juju-gui/precise/juju-gui-42),
and a flag (check_preexisting) that can be set to True in order to make
the function check for a pre-existing service and/or unit.
If the charm URL is not provided, and the service is not already deployed,
the function tries to retrieve it from charmworld. In this case a default
charm URL is used if charmworld is not available.
Return the name of the first running unit belonging to the given service.
Raise a ProgramExit if the API server returns an error response.
"""
service_data, unit_data = None, None
if check_preexisting:
# The service and/or the unit can be already in the environment.
try:
status = env.get_status()
except jujuclient.EnvError as err:
raise ProgramExit('bad API response: {}'.format(err.message))
service_data, unit_data = utils.get_service_info(status, service_name)
if service_data is None:
# The service does not exist in the environment.
print('requesting {} deployment'.format(service_name))
if charm_url is None:
try:
charm_url = utils.get_charm_url()
except (IOError, ValueError) as err:
msg = 'unable to retrieve the {} charm URL from the API: {}'
logging.warn(msg.format(service_name, err))
charm_url = settings.DEFAULT_CHARM_URL
utils.check_gui_charm_url(charm_url)
# Deploy the service without units.
try:
env.deploy(service_name, charm_url, num_units=0)
except jujuclient.EnvError as err:
raise ProgramExit('bad API response: {}'.format(err.message))
print('{} deployment request accepted'.format(service_name))
service_exposed = False
else:
# We already have the service in the environment.
print('service {} already deployed'.format(service_name))
utils.check_gui_charm_url(service_data['CharmURL'])
service_exposed = service_data.get('Exposed', False)
# At this point the service is surely deployed in the environment: expose
# it if necessary and add a unit if it is missing.
if not service_exposed:
print('exposing service {}'.format(service_name))
try:
env.expose(service_name)
except jujuclient.EnvError as err:
raise ProgramExit('bad API response: {}'.format(err.message))
if unit_data is None:
# Add a unit to the service.
print('requesting new unit deployment')
try:
response = env.add_unit(service_name, machine_spec=machine)
except jujuclient.EnvError as err:
raise ProgramExit('bad API response: {}'.format(err.message))
unit_name = response['Units'][0]
print('{} deployment request accepted'.format(unit_name))
else:
# A service unit is already present in the environment. Go ahead
# and try to reuse that unit.
unit_name = unit_data['Name']
print('reusing unit {}'.format(unit_name))
return unit_name
def watch(env, unit_name):
"""Start watching the given unit and the machine the unit belongs to.
Output a human readable message each time a relevant change is found.
The following changes are considered relevant for a healthy unit:
- the machine is pending;
- the unit is pending;
- the machine is started;
- the unit is reachable;
- the unit is installed;
- the unit is started.
Stop watching and return the unit public address when the unit is started.
Raise a ProgramExit if the API server returns an error response, or if
either the service unit or the machine is removed or in error.
"""
address = unit_status = machine_id = machine_status = ''
collected_machine_changes = []
watcher = env.watch_changes(watchers.unit_machine_changes)
while True:
try:
unit_changes, machine_changes = watcher.next()
except jujuclient.EnvError as err:
raise ProgramExit(
'bad API server response: {}'.format(err.message))
# Process unit changes.
for action, data in unit_changes:
if data['Name'] == unit_name:
try:
data = watchers.parse_unit_change(
action, data, unit_status, address)
except ValueError as err:
raise ProgramExit(bytes(err))
unit_status, address, machine_id = data
if address and unit_status == 'started':
# We can exit this loop.
return address
# The mega-watcher contains a single change for each specific
# unit. For this reason, we can exit the for loop here.
break
if not machine_id:
# No need to process machine changes: we don't know what machine
# the unit belongs to. However, machine changes are collected so
# that they can be parsed later.
collected_machine_changes.extend(machine_changes)
continue
# Process machine changes. Since relevant info can also be found
# in previously collected changes, add those to the current changes,
# in reverse order so that more complete info comes first.
all_machine_changes = machine_changes + list(
reversed(collected_machine_changes))
# At this point we can discard collected changes.
collected_machine_changes = []
for action, data in all_machine_changes:
if data['Id'] == machine_id:
try:
machine_status, address = watchers.parse_machine_change(
action, data, machine_status, address)
except ValueError as err:
raise ProgramExit(bytes(err))
if address and unit_status == 'started':
# We can exit this loop.
return address
# The mega-watcher contains a single change for each specific
# machine. For this reason, we can exit the for loop here.
break
def deploy_bundle(env, bundle_yaml, bundle_name, bundle_id):
"""Deploy a bundle.
Receive an API URL to a WebSocket server supporting bundle deployments, the
admin_secret to use in the authentication process, the bundle YAML encoded
contents and the bundle name to be imported.
Raise a ProgramExit if the API server returns an error response.
"""
try:
env.deploy_bundle(bundle_yaml, name=bundle_name, bundle_id=bundle_id)
except jujuclient.EnvError as err:
raise ProgramExit('bad API server response: {}'.format(err.message))
|