cloud-installer-proper/0000755000175000017500000000000012323645516014061 5ustar adamadamcloud-installer-proper/docs/0000755000175000017500000000000012323631201014773 5ustar adamadamcloud-installer-proper/docs/_build/0000755000175000017500000000000012323641573016246 5ustar adamadamcloud-installer-proper/docs/developers.rst0000644000175000017500000000012212323623210017671 0ustar adamadamDeveloper Guide =============== .. toctree:: :maxdepth: 2 developer.setup cloud-installer-proper/docs/modules.rst0000644000175000017500000000011112323623210017167 0ustar adamadamcloudinstall ============ .. toctree:: :maxdepth: 4 cloudinstall cloud-installer-proper/docs/log.rst0000644000175000017500000000025412323623210016310 0ustar adamadam``cloudinstall.log`` --- Log Interface ======================================== .. automodule:: cloudinstall.log :members: :undoc-members: :show-inheritance: cloud-installer-proper/docs/cloudinstall.juju.rst0000644000175000017500000000044012323623210021175 0ustar adamadam``cloudinstall.juju`` --- Juju interface ======================================== .. automodule:: cloudinstall.juju :noindex: :members: :undoc-members: :show-inheritance: .. automodule:: cloudinstall.juju.client :members: :undoc-members: :show-inheritance: cloud-installer-proper/docs/pegasus.rst0000644000175000017500000000027712323623210017203 0ustar adamadam``cloudinstall.pegasus`` --- GUI helpers ======================================== .. automodule:: cloudinstall.pegasus :noindex: :members: :undoc-members: :show-inheritance: cloud-installer-proper/docs/conf.py0000644000175000017500000002057512323630746016320 0ustar adamadam# -*- coding: utf-8 -*- # # Ubuntu Cloud Installer documentation build configuration file, created by # sphinx-quickstart on Mon Apr 7 14:07:45 2014. # # This file is execfile()d with the current directory set to its containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import sys, os class Mock(object): __all__ = [] def __init__(self, *args, **kwargs): pass def __call__(self, *args, **kwargs): return Mock() @classmethod def __getattr__(cls, name): if name in ('__file__', '__path__'): return '/dev/null' elif name[0] == name[0].upper(): mockType = type(name, (), {}) mockType.__module__ = __name__ return mockType else: return Mock() MOCK_MODULES = ['yaml', 'ws4py'] for mod_name in MOCK_MODULES: sys.modules[mod_name] = Mock() # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. sys.path.insert(0, os.path.abspath('..')) import cloudinstall # -- General configuration ----------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. #needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = ['sphinx.ext.autodoc', 'sphinx.ext.todo', 'sphinx.ext.coverage', 'sphinx.ext.ifconfig', 'sphinx.ext.viewcode'] autodoc_docstring_signature = False todo_include_todos = True # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The encoding of source files. #source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = u'Ubuntu Cloud Installer' copyright = u'2014, Canonical Ltd' version = release = cloudinstall.__version__ # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. #language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. #today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ['_build'] # The reST default role (used for this markup: `text`) to use for all documents. #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). #add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'perldoc' # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] # -- Options for HTML output --------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = 'default' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. #html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. #html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". #html_title = None # A shorter title for the navigation bar. Default is the same as html_title. html_short_title = "Ubuntu Cloud Installer" # The name of an image file (relative to this directory) to place at the top # of the sidebar. #html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. #html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. #html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. #html_domain_indices = True # If false, no index is generated. #html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, links to the reST sources are added to the pages. #html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. html_show_sphinx = False # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. #html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = 'UbuntuCloudInstallerdoc' # -- Options for LaTeX output -------------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). #'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). #'pointsize': '10pt', # Additional stuff for the LaTeX preamble. #'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ ('index', 'UbuntuCloudInstaller.tex', u'Ubuntu Cloud Installer Documentation', u'Solutions Engineering', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. #latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False # If true, show page references after internal links. #latex_show_pagerefs = False # If true, show URL addresses after external links. #latex_show_urls = False # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_domain_indices = True # -- Options for manual page output -------------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ('cloud-install', 'cloud-install', u'Ubuntu Cloud Installer Documentation', [u'Canonical Solutions Engineering'], 1), ('cloud-status', 'cloud-status', u'Ubuntu Cloud Status Documentation', [u'Canonical Solutions Engineering'], 1) ] # If true, show URL addresses after external links. #man_show_urls = False # -- Options for Texinfo output ------------------------------------------------ # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ('index', 'UbuntuCloudInstaller', u'Ubuntu Cloud Installer Documentation', u'Solutions Engineering', 'UbuntuCloudInstaller', 'One line description of project.', 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. #texinfo_appendices = [] # If false, no module index is generated. #texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. #texinfo_show_urls = 'footnote' cloud-installer-proper/docs/Makefile0000644000175000017500000001276412323623210016446 0ustar adamadam# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: -rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/UbuntuCloudInstaller.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/UbuntuCloudInstaller.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/UbuntuCloudInstaller" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/UbuntuCloudInstaller" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." cloud-installer-proper/docs/multi-installer.guide.rst0000644000175000017500000000175712323623210021761 0ustar adamadamMulti Installer Guide ===================== .. todo:: * Discuss a MaaS setup * Outline hardware resources needed for a multi install Pre-requisites ^^^^^^^^^^^^^^ Add the `cloud-installer` ppa to your system. .. code:: $ sudo apt-add-repository ppa:cloud-installer/ppa .. note:: Adding the ppa is only necessary until an official release to the archives has been announced. Installation ^^^^^^^^^^^^ Install the cloud-installer via `apt-get` .. code:: $ sudo apt-get install cloud-installer Start the installation ^^^^^^^^^^^^^^^^^^^^^^ To start the installation run the following command .. code:: $ sudo cloud-install An initial dialog box will appear asking you to select which type of install, choose **Multi-system**. Next Steps ^^^^^^^^^^ The installer will run through a series of steps starting with making sure the necessary bits are available for a single system installation and ending with a `juju` bootstrapped system. .. todo:: Finish this guide. cloud-installer-proper/docs/cloud-install.rst0000644000175000017500000000103112323631201020272 0ustar adamadam .. code:: cloud-install [-ihf] Create an Ubuntu Cloud! (requires root privileges) Options: -i install only (don't invoke cloud-status) -h print this message -f bypass any sanity checks DESCRIPTION =========== Ubuntu Cloud installer is a metal to cloud image that provides an extremely simple way to install, deploy and scale an openstack cloud on top of Ubuntu server. Initial configurations are available for single physical system deployments as well as multiple physical system deployments. cloud-installer-proper/docs/cloudinstall.rst0000644000175000017500000000242112323623210020222 0ustar adamadamcloudinstall Package ==================== :mod:`cloudinstall` Package --------------------------- .. automodule:: cloudinstall.__init__ :members: :undoc-members: :show-inheritance: :mod:`gui` Module ----------------- .. automodule:: cloudinstall.gui :members: :undoc-members: :show-inheritance: :mod:`log` Module ----------------- .. automodule:: cloudinstall.log :noindex: :members: :undoc-members: :show-inheritance: :mod:`machine` Module --------------------- .. automodule:: cloudinstall.machine :members: :undoc-members: :show-inheritance: :mod:`pegasus` Module --------------------- .. automodule:: cloudinstall.pegasus :members: :undoc-members: :show-inheritance: :mod:`service` Module --------------------- .. automodule:: cloudinstall.service :members: :undoc-members: :show-inheritance: :mod:`utils` Module ------------------- .. automodule:: cloudinstall.utils :members: :undoc-members: :show-inheritance: :mod:`juju` Module ------------------- .. automodule:: cloudinstall.juju :members: :undoc-members: :show-inheritance: :mod:`maas` Module ------------------- .. automodule:: cloudinstall.maas :noindex: :members: :undoc-members: :show-inheritance: cloud-installer-proper/docs/cloudinstall.maas.rst0000644000175000017500000000056412323623210021150 0ustar adamadam``cloudinstall.maas`` --- Maas interface ========================================= .. automodule:: cloudinstall.maas :members: :undoc-members: :show-inheritance: .. automodule:: cloudinstall.maas.auth :members: :undoc-members: :show-inheritance: .. automodule:: cloudinstall.maas.client :members: :undoc-members: :show-inheritance: cloud-installer-proper/docs/developer.setup.rst0000644000175000017500000000332512323623210020655 0ustar adamadamDeveloper Guide - Setup ======================= The document walks you through installing the necessary packages and environment preparations in order to build the cloud installer. Base system ^^^^^^^^^^^ Development and testing is done on Ubuntu and using a release of **Trusty** or later. Needed packages ^^^^^^^^^^^^^^^ * debhelper * dh-python * python3-all * python3-mock * python3-nose * python3-oauthlib * python3-passlib * python3-requests * python3-requests-oauthlib * python3-setuptools * python3-urwid * python3-ws4py * python3-yaml Building cloud installer ^^^^^^^^^^^^^^^^^^^^^^^^ **Sbuild** is the preferred way for building the package set. Please refer to this `wiki page `_ on setting up sbuild. Just like the base system the sbuild chroots need to be `Trusty` or later. .. note:: The architecture of the chroots do not matter. Once **sbuild** is configured, checkout the source code of the installer .. code:: $ git clone https://github.com/Ubuntu-Solutions-Engineering/cloud-installer.git ~/cloud-installer $ cd cloud-installer From here you can build the entire package set by running: .. code:: $ make sbuild Once finished your packages will be stored in the top level directory where your cloud-installer project is kept. .. code:: $ ls ../*.deb Building documentation ^^^^^^^^^^^^^^^^^^^^^^ Documentation will be built in **docs/_build/html**, and requires **Sphinx** to build. .. code:: $ cd docs && make html Running Tests ^^^^^^^^^^^^^ Tests can be ran against a set of exported data(**default**) or a live machine. In order to test against live data the following environment variable is used. .. code:: $ JUJU_LIVE=1 nosetests3 test cloud-installer-proper/docs/service.rst0000644000175000017500000000031312323623210017163 0ustar adamadam``cloudinstall.service`` --- Service Interface ============================================== .. automodule:: cloudinstall.service :noindex: :members: :undoc-members: :show-inheritance: cloud-installer-proper/docs/gui.rst0000644000175000017500000000027212323623210016313 0ustar adamadam``cloudinstall.gui`` --- GUI Interface ======================================== .. automodule:: cloudinstall.gui :noindex: :members: :undoc-members: :show-inheritance: cloud-installer-proper/docs/machine.rst0000644000175000017500000000035112323623210017131 0ustar adamadam``cloudinstall.machine`` --- Maas/Juju machine representation ============================================================= .. automodule:: cloudinstall.machine :noindex: :members: :undoc-members: :show-inheritance: cloud-installer-proper/docs/index.rst0000644000175000017500000000115012323623210016632 0ustar adamadamUbuntu Cloud Installer ================================================== `Github project page `_ .. todo:: This is work in progress and some information will be missing. Please consult the source code or file a bug at the Github project page. Contributions are welcomed. Guides ^^^^^^ .. toctree:: :maxdepth: 1 developers single-installer.guide multi-installer.guide Reference ^^^^^^^^^^ .. toctree:: :maxdepth: 2 cloudinstall.juju cloudinstall.maas gui log machine pegasus service utils cloud-installer-proper/docs/single-installer.guide.rst0000644000175000017500000000254412323623210022103 0ustar adamadamSingle Installer Guide ====================== Pre-requisites ^^^^^^^^^^^^^^ Add the `cloud-installer` ppa to your system. .. code:: $ sudo apt-add-repository ppa:cloud-installer/ppa .. note:: Adding the ppa is only necessary until an official release to the archives has been announced. Installation ^^^^^^^^^^^^ Install the cloud-installer via `apt-get` .. code:: $ sudo apt-get install cloud-installer Start the installation ^^^^^^^^^^^^^^^^^^^^^^ To start the installation run the following command .. code:: $ sudo cloud-install An initial dialog box will appear asking you to select which type of install, choose **Single system**. Next Steps ^^^^^^^^^^ The installer will run through a series of steps starting with making sure the necessary bits are available for a single system installation and ending with a `juju` bootstrapped system. When the bootstrapping has finished it will immediately load the status screen. From there you can see the nodes listed along with the deployed charms necessary to start your private openstack cloud. Adding additional compute nodes, block storage, object storage, and controllers can be done by pressing `F6` and making the selection on the dialog box. Finally, once those nodes are displayed and the charms deployed the horizon dashboard will be available to you for managing your openstack cloud. cloud-installer-proper/docs/utils.rst0000644000175000017500000000030112323623210016660 0ustar adamadam``cloudinstall.utils`` --- Utility helpers ========================================== .. automodule:: cloudinstall.utils :noindex: :members: :undoc-members: :show-inheritance: cloud-installer-proper/docs/cloud-status.rst0000644000175000017500000000053312323631106020161 0ustar adamadamUbuntu Cloud installer is a metal to cloud image that provides an extremely simple way to install, deploy and scale an openstack cloud on top of Ubuntu server. Initial configurations are available for single physical system deployments as well as multiple physical system deployments. This is the user interface for managing the deployed cloud. cloud-installer-proper/CONTRIBUTING.txt0000644000175000017500000000127512323623210016521 0ustar adamadamGuidelines to follow when writing new code or submitting patches. ## Syntax * No ending whitespace on any line. ### Functions Function names with opening/closing braces on newlines. funcname() { return; } ### Style #### Shell scripts - Tabs (8 characters), not spaces. - Line continuations are tabs up to the previous lines columns start and then an additional 4 spaces. #### Python - Follow PEP-8 style guide. http://www.python.org/dev/peps/pep-0008/ - Coding guidelines based off with a few differences http://google-styleguide.googlecode.com/svn/trunk/pyguide.html - Documenting code differences We use the default sphinx style for documenting classes, functions, methods. cloud-installer-proper/requirements.txt0000644000175000017500000000022612323623210017330 0ustar adamadamPyYAML==3.10 oauthlib==0.6.1 requests==2.2.1 requests-oauthlib==0.4.0 ws4py==0.3.2 passlib mock setuptools urwid nose nose-cov readthedocs-sphinx-ext cloud-installer-proper/setup.py0000644000175000017500000000363112323631503015565 0ustar adamadam#!/usr/bin/env python3 # -*- mode: python; -*- # # setup.py - MAAS distutils setup # # Copyright 2014 Canonical, Ltd. # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This package is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . from setuptools import setup import os import sys import cloudinstall if sys.argv[-1] == 'clean': print("Cleaning up ...") os.system('rm -rf cloud_installer.egg-info build dist') sys.exit() setup(name='cloud-installer', version=cloudinstall.__version__, description="""Ubuntu Cloud installer is a metal to cloud image that provides an extremely simple way to install, deploy and scale an openstack cloud on top of Ubuntu server. Initial configurations are available for single physical system deployments as well as multiple physical system deployments. """, author='Robert Ayres', author_email='robert.ayres@ubuntu.com', url='https://launchpad.net/cloud-installer', license="AGPLv3+", scripts=['bin/cloud-install', 'bin/cloud-status'], packages=['cloudinstall', 'cloudinstall.maas', 'cloudinstall.juju'], requires=['PyYAML', 'six', 'urwid', 'requests_oauthlib', 'requests'], data_files=[ ('share/man/man1', ['man/en/cloud-status.1', 'man/en/cloud-install.1']) ], ) cloud-installer-proper/README.md0000644000175000017500000000152612323623210015327 0ustar adamadam# Ubuntu Cloud Installer [![Build Status](https://travis-ci.org/Ubuntu-Solutions-Engineering/cloud-installer.svg?branch=master)](https://travis-ci.org/Ubuntu-Solutions-Engineering/cloud-installer) ## Developers * [Read the developers guide](http://ubuntu-cloud-installer.readthedocs.org/en/latest/developers.html) ## Users * [Single Installer guide](http://ubuntu-cloud-installer.readthedocs.org/en/latest/single-installer.guide.html) * [Multi Installer guide](http://ubuntu-cloud-installer.readthedocs.org/en/latest/multi-installer.guide.html) # Copyright Copyright 2014 Canonical, Ltd. # License This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. cloud-installer-proper/cloudinstall/0000755000175000017500000000000012323645516016556 5ustar adamadamcloud-installer-proper/cloudinstall/pegasus.py0000644000175000017500000001426112323641263020576 0ustar adamadam# # pegasus.py - GUI interface to Cloud Installer # # Copyright 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 as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY 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 . from io import StringIO from collections import defaultdict import os from os.path import expanduser, exists from subprocess import check_call, DEVNULL from textwrap import dedent import tempfile import re import urllib from cloudinstall import utils from cloudinstall.log import logger from cloudinstall.maas import MaasState from cloudinstall.maas.auth import MaasAuth from cloudinstall.juju import JujuState from cloudinstall.maas.client import MaasClient log = logger(__name__) NOVA_CLOUD_CONTROLLER = "nova-cloud-controller" MYSQL = 'mysql' RABBITMQ_SERVER = 'rabbitmq-server' GLANCE = 'glance' KEYSTONE = 'keystone' OPENSTACK_DASHBOARD = 'openstack-dashboard' NOVA_COMPUTE = 'nova-compute' SWIFT = 'swift-storage' CEPH = 'ceph' CONTROLLER = "Controller" COMPUTE = "Compute" OBJECT_STORAGE = "Object Storage" BLOCK_STORAGE = "Block Storage" ALLOCATION = { NOVA_CLOUD_CONTROLLER: CONTROLLER, NOVA_COMPUTE: COMPUTE, SWIFT: OBJECT_STORAGE, CEPH: BLOCK_STORAGE, } CONTROLLER_CHARMS = [ NOVA_CLOUD_CONTROLLER, MYSQL, RABBITMQ_SERVER, GLANCE, KEYSTONE, OPENSTACK_DASHBOARD, ] RELATIONS = { KEYSTONE: [MYSQL], NOVA_CLOUD_CONTROLLER: [MYSQL, RABBITMQ_SERVER, GLANCE, KEYSTONE], NOVA_COMPUTE: [MYSQL, RABBITMQ_SERVER, GLANCE, NOVA_CLOUD_CONTROLLER], GLANCE: [MYSQL, KEYSTONE], OPENSTACK_DASHBOARD: [KEYSTONE], } class MaasLoginFailure(Exception): MESSAGE = "Could not read login credentials. Please run: " \ "maas-get-user-creds root > ~/.cloud-install/maas-creds" def get_charm_relations(charm): """ Return a list of (relation, command) of relations to add. """ for rel in RELATIONS.get(charm, []): if charm == NOVA_COMPUTE and rel == RABBITMQ_SERVER: c, r = (NOVA_COMPUTE + ":amqp", RABBITMQ_SERVER + ":amqp") else: c, r = (charm, rel) cmd = "juju add-relation {charm} {relation}" yield (r, cmd.format(charm=c, relation=r)) PASSWORD_FILE = expanduser('~/.cloud-install/openstack.passwd') try: with open(PASSWORD_FILE) as f: OPENSTACK_PASSWORD = f.read().strip() except IOError: OPENSTACK_PASSWORD = None # This is kind of a hack. juju deploy $foo rejects foo if it doesn't have a # config or there aren't any options in the declared config. So, we have to # babysit it and not tell it about configs when there aren't any. _OMIT_CONFIG = [ MYSQL, RABBITMQ_SERVER, ] # TODO: Use trusty + icehouse CONFIG_TEMPLATE = dedent("""\ glance: openstack-origin: cloud:precise-grizzly keystone: openstack-origin: cloud:precise-grizzly admin-password: {password} nova-cloud-controller: openstack-origin: cloud:precise-grizzly nova-compute: openstack-origin: cloud:precise-grizzly openstack-dashboard: openstack-origin: cloud:precise-grizzly """).format(password=OPENSTACK_PASSWORD) SINGLE_SYSTEM = exists(expanduser('~/.cloud-install/single')) MULTI_SYSTEM = exists(expanduser('~/.cloud-install/multi')) def juju_config_arg(charm): """ Query configuration parameters for openstack charms :param charm: name of charm :type charm: str :return: path of openstack configuration :rtype: str """ path = os.path.join(tempfile.gettempdir(), "openstack.yaml") if not exists(path): with open(path, 'wb') as f: f.write(bytes(CONFIG_TEMPLATE, 'utf-8')) config = "" if charm in _OMIT_CONFIG else "--config {path}" return config.format(path=path) def poll_state(): """ Polls current state of Juju and MAAS """ # Capture Juju state juju = utils._run('juju status') if not juju: raise Exception("Juju State is empty!") juju = JujuState(StringIO(juju.decode('ascii'))) maas = None if MULTI_SYSTEM: # Login to MAAS auth = MaasAuth() auth.get_api_key('root') auth.login() # Load Client routines c = MaasClient(auth) # Capture Maas state maas = MaasState(c.nodes) c.tag_fpi(maas) c.nodes_accept_all() c.tag_name(maas) return parse_state(juju, maas), juju def parse_state(juju, maas=None): """ Parses the current state of juju containers and maas nodes :param juju: juju polled state :type juju: JujuState() :param maas: maas polled state :type mass: MaasState() :return: nodes/containers :rtype: list """ results = [] for machine in juju.machines(): if machine.is_machine_0: continue # Query our maas machine to capture some of the # hardware specifications. # FIXME: why isn't storage being applied here? if maas: maas_machine = maas.machine(machine.instance_id) machine.mem = maas_machine.mem machine.cpu_cores = maas_machine.cpu_cores machine.storage = maas_machine.storage machine.tag = maas_machine.tag log.debug("Updated machine properties: %s" % (machine,)) results.append(machine) return results def wait_for_services(): """ Wait for services to be in ready state .. todo:: Is this still needed? """ services = [ 'maas-region-celery', 'maas-cluster-celery', 'maas-pserv', 'maas-txlongpoll', 'juju-db', 'jujud-machine-0', ] for service in services: check_call(['start', 'wait-for-state', 'WAITER=cloud-install-status', 'WAIT_FOR=%s' % service, 'WAIT_STATE=running']) cloud-installer-proper/cloudinstall/__init__.py0000644000175000017500000000141512323623210020653 0ustar adamadam# # __init__.py - init # # Copyright 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 as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY 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 . """ Ubuntu Cloud Installer """ __version__ = "0.13+git20140410" cloud-installer-proper/cloudinstall/log.py0000644000175000017500000000322512323640352017704 0ustar adamadam# # log.py - Logger # # Copyright 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 as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY 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 . """ Logging interface Simply exports `logger` variable """ import logging import logging.handlers import os def logger(name='ubuntu-cloud-installer'): """ setup logging Overridding the default log level(**debug**) can be done via an environment variable `UCI_LOGLEVEL` Available levels: * CRITICAL * ERROR * WARNING * INFO * DEBUG .. code:: # Running cloud-status from cli $ UCI_LOGLEVEL=INFO cloud-status :params str name: logger name :returns: a log object """ LOGFILE = os.path.expanduser('~/.cloud-install/commands.log') commandslog = logging.FileHandler(LOGFILE, 'w') commandslog.setFormatter(logging.Formatter( '%(asctime)s %(pathname)s [%(process)d] * ' \ '%(levelname)s %(name)s - %(message)s')) logger = logging.getLogger(name) env = os.environ.get('UCI_LOGLEVEL', 'DEBUG') logger.setLevel(env) logger.addHandler(commandslog) return logger cloud-installer-proper/cloudinstall/gui.py0000644000175000017500000006115612323645023017716 0ustar adamadam# # gui.py - Cloud install gui components # # Copyright 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 as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY 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 . """ Pegasus - gui interface to Ubuntu Cloud Installer """ from collections import deque from errno import ENOENT from os import write, close from os.path import expanduser from subprocess import check_call, Popen, PIPE, STDOUT from time import strftime from traceback import format_exc import re import threading import urwid from cloudinstall.log import logger from cloudinstall.machine import Machine from cloudinstall import pegasus from cloudinstall import utils log = logger(__name__) TITLE_TEXT = "Ubuntu Cloud Installer (q to quit)" #- Properties ----------------------------------------------------------------- IS_TTY = re.match('/dev/tty[0-9]', utils.get_command_output('tty')[1]) # Time to lock in seconds LOCK_TIME = 120 NODE_FORMAT = "|".join(["{fqdn:<20}", "{cpu_count:>6}", "{memory:>10}", "{storage:>12}", "{agent_state:<12}", "{tags}"]) NODE_HEADER = "|".join(["{fqdn:<20}", "{cpu_count:<6}", "{memory:<10}", "{storage:<12}", "{agent_state:<12}", "{tags}"]).format(fqdn="Hostname", cpu_count="# CPUs", memory="RAM (MB)", storage="Storage (GB)", agent_state="State", tags="Charms") STYLES = [ ('body', 'white', 'black',), ('border', 'brown', 'dark magenta'), ('focus', 'black', 'dark green'), ('dialog', 'black', 'dark cyan'), ('list_title', 'black', 'light gray',), ('error', 'white', 'dark red'), ] RADIO_STATES = list(pegasus.ALLOCATION.values()) def _allocation_for_charms(charms): als = [pegasus.ALLOCATION.get(c, '') for c in charms] return list(filter(lambda x: x, als)) class TextOverlay(urwid.Overlay): def __init__(self, text, underlying): w = urwid.LineBox(urwid.Filler(urwid.Text(text))) w = urwid.AttrWrap(w, "dialog") urwid.Overlay.__init__(self, w, underlying, 'center', 60, 'middle', 5) class ControllerOverlay(TextOverlay): PXE_BOOT = "You need one node to act as the cloud controller. " \ "Please PXE boot the node you would like to use." NODE_WAIT = "Please wait while the cloud controller is installed on your " \ "host system." NODE_SETUP = "Your node has been correctly detected. " \ "Please wait until setup is complete " def __init__(self, underlying, command_runner): self.underlying = underlying self.allocated = None self.command_runner = command_runner self.done = False self.start_text = self.NODE_WAIT if pegasus.SINGLE_SYSTEM else self.PXE_BOOT TextOverlay.__init__(self, self.start_text, self.underlying) def process(self, data): """ Process a node list. Returns True if the overlay still needs to be shown, false otherwise. """ if self.done: return False # Wait until the command runner is done to do any more processing # steps. if len(self.command_runner.to_run) > 0: return True continue_ = self._process(data) if not continue_: self.done = True return continue_ def _process(self, data): allocated = list(data.machines_allocated()) log.debug("Allocated machines: {machines}".format(machines=allocated)) unallocated = list(data.machines_unallocated()) log.debug("Unallocated machines: {machines}".format(machines=unallocated)) for machine in allocated: if pegasus.NOVA_CLOUD_CONTROLLER in machine.charms: return False # Regardless of install type (single, multi) we always # create at least 1 machine to deploy our cloud-controller # on if len(allocated) == 0: if pegasus.MULTI_SYSTEM and len(unallocated) > 0: self.command_runner.add_machine() elif pegasus.SINGLE_SYSTEM: self.command_runner.add_machine() elif len(allocated) > 0: machine = allocated[0] pending = set(pegasus.CONTROLLER_CHARMS) - set(machine.charms) if len(pending) == 0: return False for charm in pending: # If multi system install into lxc containers on machine if pegasus.MULTI_SYSTEM: id_ = 'lxc:{machine_id}'.format(machine_id=machine.machine_id) else: id_ = machine.machine_id # Deploy any remaining charms onto machine self.command_runner.deploy(charm, id=id_) else: TextOverlay(self.NODE_SETUP, self.underlying) return True def _wrap_focus(widgets, unfocused=None): try: return [urwid.AttrMap(w, unfocused, "focus") for w in widgets] except TypeError: return urwid.AttrMap(widgets, unfocused, "focus") class ChangeStateDialog(urwid.Overlay): def __init__(self, underlying, machine, on_success, on_cancel): self.boxes = [] start_states = [] log.debug("ChangeStateDialog.__init__: " \ "{machine}".format(machine=machine)) if machine.charms: start_states = _allocation_for_charms(machine.charms) self.boxes = [] first_index = 0 for i, txt in enumerate(RADIO_STATES): if txt in start_states and not first_index: first_index = i r = urwid.CheckBox(txt, state=txt in start_states) r.text_label = txt self.boxes.append(r) wrapped_boxes = _wrap_focus(self.boxes) def ok(button): states = map(lambda b: b.get_state(), self.boxes) selected = filter(lambda r: r.get_state(), self.boxes) on_success([s.text_label for s in selected]) def cancel(button): on_cancel() bs = [urwid.Button("Ok", ok), urwid.Button("Cancel", cancel)] wrapped_buttons = _wrap_focus(bs) self.buttons = urwid.Columns(wrapped_buttons) self.items = urwid.ListBox(wrapped_boxes) self.items.set_focus(first_index) ba = urwid.BoxAdapter(self.items, height=len(wrapped_boxes)) self.lb = urwid.ListBox([ba, urwid.Text(""), self.buttons]) root = urwid.LineBox(self.lb, title="Select new charm") root = urwid.AttrMap(root, "dialog") urwid.Overlay.__init__(self, root, underlying, 'center', 30, 'middle', len(wrapped_boxes) + 4) def keypress(self, size, key): if key == 'tab': if self.lb.get_focus()[0] == self.buttons: self.keypress(size, 'page up') else: self.keypress(size, 'page down') return urwid.Overlay.keypress(self, size, key) class Node(urwid.Text): """ A single ui node representation """ def __init__(self, machine, open_dialog): """ Initialize Node :param machine: juju machine state :type machine: Machine() """ urwid.Text.__init__(self, "") self.machine = machine self.open_dialog = open_dialog self.allocated = self.machine.charms if self.allocated: self._selectable = self.machine.charms not in pegasus.CONTROLLER_CHARMS else: self._selectable = True self.set_text(NODE_FORMAT.format(tags=', '.join(self.machine.charms), fqdn=self.machine.dns_name, cpu_count=self.machine.cpu_cores, memory=self.machine.mem, storage=self.machine.storage, agent_state=self.machine.agent_state)) @property def is_horizon(self): return self.allocated and \ pegasus.OPENSTACK_DASHBOARD in self.machine.charms @property def is_compute(self): return self.allocated and \ pegasus.NOVA_COMPUTE in self.machine.charms def keypress(self, size, key): """ Signal binding for Node Keys: * Enter - Opens node state change dialog * F6 - Opens charm deployments dialog """ # can't change node state on single system # FIXME: Can we make F6 apply to both single and # multi install systems? # if key == 'enter' and not pegasus.SINGLE_SYSTEM: # self.open_dialog(self.machine) if key == 'f6': self.open_dialog(self.machine) return key class ListWithHeader(urwid.Frame): def __init__(self, header_text): header = urwid.AttrMap(urwid.Text(header_text), "list_title") self._contents = urwid.SimpleListWalker([]) body = urwid.ListBox(self._contents) urwid.Frame.__init__(self, header=header, body=body) def selectable(self): return len(self._contents) > 0 def update(self, nodes): self._contents[:] = _wrap_focus(nodes) class CommandRunner(urwid.ListBox): def __init__(self): self._contents = urwid.SimpleListWalker([]) urwid.ListBox.__init__(self, self._contents) self.to_run = deque() self.running = None self.services = set() self.to_add = [] def keypress(self, size, key): if key.lower() == "ctrl u": key = 'page up' if key.lower() == "ctrl d": key = 'page down' return urwid.ListBox.keypress(self, size, key) def _add(self, command, output): def add_to_f8(command, output): txt = "{time}> {cmd}\n{output}".format(time=utils.time(), cmd=command, output=output) self._contents.append(urwid.Text(txt)) self._contents[:] = self._contents[:200] return txt txt = add_to_f8(command, output) def _run(self, command): self.to_run.append(command) self._next() def _next(self): if not self.running and len(self.to_run) > 0: cmd = self.to_run.popleft() try: self.running = Popen(cmd.split(), stdout=PIPE, stderr=STDOUT) self.running.command = cmd except (IOError, OSError) as e: self.running = None self._add(cmd, str(e)) def add_machine(self): self._run("juju add-machine") def deploy(self, charm, id=None, tag=None): config = pegasus.juju_config_arg(charm) to = "" if id is None else "--to " + str(id) constraints = "" if tag is None else "--constraints tags=" + tag cmd = "juju deploy {config} {to} {constraints} {charm}".format( config=config, to=to, constraints=constraints, charm=charm) self._run(cmd) self.services.add(charm) self.to_add.extend(pegasus.get_charm_relations(charm)) remaining = [] for (relation, cmd) in self.to_add: if relation in self.services: self._run(cmd) else: remaining.append((relation, cmd)) self.to_add = remaining def change_allocation(self, new_states, machine): log.debug("CommandRunner.change_allocation: " \ "new_states: {states}".format(states=new_states)) try: for charm, unit in zip(machine.charms, machine.units): if charm not in new_states: self._run("juju remove-unit {unit}".format(unit=unit)) except KeyError: pass if len(new_states) == 0: cmd = "juju terminate-machine {id}".format(id=machine.machine_id) log.debug("Terminating machine: {cmd}".format(cmd=cmd)) self._run(cmd) state_to_charm = {v: k for k, v in pegasus.ALLOCATION.items()} for state in set(new_states) - set(machine.charms): charm = state_to_charm[state] new_service = charm not in self.services if new_service: if pegasus.SINGLE_SYSTEM: self.deploy(charm, id=machine.machine_id) else: self.deploy(charm, tag=machine.tag) else: # TODO: we should make this a bit nicer, and do the # SINGLE_SYSTEM test a little higher up. if pegasus.SINGLE_SYSTEM: cmd = "juju add-unit --to 1 " \ "{charm}".format(charm=charm) log.debug("Adding unit: {cmd}".format(cmd=cmd)) self._run(cmd) else: constraints = "juju set-constraints --service " \ "{charm} tags={{tag}}".format(charm=charm, tag=machine.tag) log.debug("Setting constraints: " \ "{constraints}".format(constraints=constraints)) self._run(constraints.format(tag=machine.tag)) cmd = "juju add-unit {charm}".format(charm=charm) log.debug("Adding unit: {cmd}".format(cmd=cmd)) self._run(cmd) self._run(constraints.format(tag='')) def update(self, juju_state): self.services = set(juju_state.services) log.debug("Services: {services}".format(services=self.services)) def poll(self): if self.running and self.running.poll() is not None: out = self.running.stdout.read().decode('ascii') self._add(self.running.command, out) self.running = None self._next() def _make_header(rest): header = urwid.Text("{title} {rest}".format(title=TITLE_TEXT, rest=rest)) return urwid.AttrWrap(header, "border") # TODO: This and CommandRunner should really be merged class ConsoleMode(urwid.Frame): def __init__(self): header = _make_header("(f8 switches to node view mode)") self.command_runner = CommandRunner() urwid.Frame.__init__(self, header=header, body=self.command_runner) def tick(self): self.command_runner.poll() class NodeViewMode(urwid.Frame): def __init__(self, loop, get_data, command_runner): f6 = ', f6 to add another node' if pegasus.SINGLE_SYSTEM else '' header = _make_header("(f8 switches to console mode{f6})".format(f6=f6)) self.timer = urwid.Text("", align="right") self.url = urwid.Text("") footer = urwid.Columns([self.url, self.timer]) footer = urwid.AttrWrap(footer, "border") self.poll_interval = 10 self.ticks_left = 0 self.get_data = get_data self.nodes = ListWithHeader(NODE_HEADER) self.loop = loop self.cr = command_runner urwid.Frame.__init__(self, header=header, body=self.nodes, footer=footer) self.controller_overlay = ControllerOverlay(self, self.cr) self._target = self.controller_overlay # TODO: get rid of this shim. @property def target(self): return self._target @target.setter def target(self, val): self._target = val # Don't switch from command runner back to us "randomly" (i.e. when # the setup is complete and the overlay goes away). if isinstance(self.loop.widget, ConsoleMode): return # don't accidentally unlock if not isinstance(self.loop.widget, LockScreen): self.loop.widget = val # FIXME: what is this used for? def total_nodes(self): return len(self.nodes._contents) def open_dialog(self, machine): def destroy(): self.loop.widget = self def ok(new_states): if pegasus.SINGLE_SYSTEM: compute, other = utils.partition( lambda c: c == pegasus.NOVA_COMPUTE, new_states) if compute: # Here we just boot the KVM, the polling process will # automatically allocate new kvms to nova-compute. self.cr.add_machine() self.cr.change_allocation(other, machine) else: self.cr.change_allocation(new_states, machine) destroy() self.loop.widget = ChangeStateDialog(self, machine, ok, destroy) def refresh_states(self): """ Refresh states Make a call to refresh both juju and maas machine states :returns: data from the polling of services and the juju state :rtype: tuple (parse_state(), Machine()) """ return self.get_data() def do_update(self, machines): """ Updating node states :params list machines: list of known machines """ log.debug(machines) nodes, juju = machines nodes = [Node(t, self.open_dialog) for t in nodes] if self.target == self.controller_overlay and \ not self.controller_overlay.process(juju): self.target = self for n in nodes: if n.is_horizon: url = "Access your dashboard: http://{name}/horizon" self.url.set_text(url.format(name=n.machine.dns_name)) if pegasus.SINGLE_SYSTEM: # For single installs, all new 'unallocated' nodes are # automatically allocated to nova-compute. We process the rest of # the nodes normally. unallocated_nodes, allocated_nodes = utils.partition( lambda n: n.is_compute, nodes) for node in unallocated_nodes: self.cr.change_allocation([pegasus.NOVA_COMPUTE], node.machine) self.nodes.update(nodes) self.cr.update(juju) def tick(self): if self.ticks_left == 0: self.ticks_left = self.poll_interval def update_and_redraw(data): self.do_update(data) self.loop.draw_screen() self.loop.run_async(self.refresh_states, update_and_redraw) self.timer.set_text("Poll in {secs} seconds " \ "({t_count}) ".format(secs=self.ticks_left, t_count=threading.active_count())) self.ticks_left = self.ticks_left - 1 def keypress(self, size, key): """ Signal binding for NodeViewMode Keys: * F5 - Refreshes the node list """ if key == 'f5': self.ticks_left = 0 return urwid.Frame.keypress(self, size, key) class LockScreen(urwid.Overlay): LOCKED = "The screen is locked. Please enter a password (this is the " \ "password you entered for OpenStack during installation). " INVALID = ("error", "Invalid password.") IOERROR = ("error", "Problem accessing {pwd}. Please make sure " \ "it contains exactly one line that is the lock " \ "password.".format(pwd=pegasus.PASSWORD_FILE)) def __init__(self, underlying, unlock): self.unlock = unlock self.password = urwid.Edit("Password: ", mask='*') self.invalid = urwid.Text("") w = urwid.ListBox([urwid.Text(self.LOCKED), self.invalid, self.password]) w = urwid.LineBox(w) w = urwid.AttrWrap(w, "dialog") urwid.Overlay.__init__(self, w, underlying, 'center', 60, 'middle', 8) def keypress(self, size, key): if key == 'enter': if pegasus.OPENSTACK_PASSWORD is None: self.invalid.set_text(self.IOERROR) elif pegasus.OPENSTACK_PASSWORD == self.password.get_edit_text(): self.unlock() else: self.invalid.set_text(self.INVALID) self.password.set_edit_text("") else: return urwid.Overlay.keypress(self, size, key) class PegasusGUI(urwid.MainLoop): def __init__(self, get_data): self.console = ConsoleMode() self.node_view = NodeViewMode(self, get_data, self.console.command_runner) self.lock_ticks = 0 # start in a locked state self.locked = False urwid.MainLoop.__init__(self, self.node_view.target, STYLES, unhandled_input=self._header_hotkeys) def _key_pressed(self, keys, raw): # We use this as an 'input filter' just to hook when keys are pressed; # we don't actually filter any input here. self.lock_ticks = LOCK_TIME return keys def _header_hotkeys(self, key): # if we are locked, don't do anything if isinstance(self.widget, LockScreen): return None if key == 'f8': if self.widget == self.console: self.widget = self.node_view.target else: self.widget = self.console if key in ['q', 'Q']: raise urwid.ExitMainLoop() def tick(self, unused_loop=None, unused_data=None): # FIXME: Build problems with nonlocal keyword # see comment under unlock() # Only lock when we are in TTY mode. if not self.locked and IS_TTY: if self.lock_ticks == 0: self.locked = True old = {'res' : self.widget} def unlock(): # If the controller overlay finished its work while we were # locked, bypass it. # FIXME: syntax error complains in debian building # probably has something to do with the mixture of # py2 and py3 in our stack. # nonlocal old if isinstance(old['res'], ControllerOverlay) and old['res'].done: old['res'] = self.node_view self.widget = old['res'] self.lock_ticks = LOCK_TIME self.locked = False self.widget = LockScreen(old['res'], unlock) else: self.lock_ticks = self.lock_ticks - 1 self.console.tick() self.node_view.tick() self.set_alarm_in(1.0, self.tick) def run(self): self.tick() with utils.console_blank(): urwid.MainLoop.run(self) def run_async(self, f, callback): """ This is a little bit goofy. The urwid API is based on select(), and can't actually run python functions asynchronously. So, if we want to run a long-running function which should update the UI, we have to get a fd to have urwid watch for us, and then we send data to it when it's done. FIXME: Once https://github.com/wardi/urwid/pull/57 is implemented. """ result = {'res' : None} # Here again things are a little weird: we own write_fd, but the urwid # API makes things a bit awkward since we end up needing mutually # recursive values, so we abuse python's scoping rules. def done(unused): try: callback(result['res']) except Exception as e: self.console.command_runner._add("Status thread:", format_exc()) finally: self.remove_watch_pipe(write_fd) close(write_fd) write_fd = self.watch_pipe(done) def run_f(): # FIXME: Because we are putting a dependency on python2 # for whatever reason using nonlocal is turning into a # syntax error. I can only assume it has to do with the # packaging somehow. #nonlocal result try: result['res'] = f() except Exception as e: self.console.command_runner._add("Status thread:", format_exc()) write(write_fd, bytes('done', 'ascii')) threading.Thread(target=run_f).start() cloud-installer-proper/cloudinstall/juju/0000755000175000017500000000000012323645154017531 5ustar adamadamcloud-installer-proper/cloudinstall/juju/__init__.py0000644000175000017500000000714112323645154021645 0ustar adamadam# # __init__.py - Juju state # # Copyright 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 as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY 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 . """ Represents a juju status """ import yaml import itertools from collections import defaultdict from cloudinstall.machine import Machine from cloudinstall.service import Service from cloudinstall.log import logger log = logger(__name__) class JujuState: """ Represents a global Juju state """ def __init__(self, raw_yaml): """ Builds a JujuState from a file-like object containing the raw output from __juju status__ :param raw_yaml: YAML object """ self._yaml = yaml.load(raw_yaml) self.valid_states = ['pending', 'started', 'down'] def __validate_allocation(self, machine): """ Private function to test if machine is in an allocated state. """ return not machine.is_machine_0 and \ machine.agent_state in self.valid_states or \ any(c for c in machine.charms) def machine(self, instance_id): """ Return single machine state :param str instance_id: machine instance_id :returns: machine :rtype: cloudinstall.machine.Machine() """ return next(filter(lambda x: x.instance_id == instance_id, self.machines())) or Machine(-1, {}) def machines(self): """ Machines property :returns: machines known to juju :rtype: generator """ for machine_id, machine in self._yaml['machines'].items(): if '0' in machine_id: continue # Add units for machine machine['units'] = [] for svc in self.services: for unit in svc.units: if machine_id == unit.machine_id: machine['units'].append(unit) yield Machine(machine_id, machine) def machines_allocated(self): """ Machines allocated property :returns: Machines in an allocated state :rtype: iter """ return filter(self.__validate_allocation, self.machines()) def machines_unallocated(self): """ Machines unallocated property :returns: Machines in an unallocated state :rtype: iter """ return itertools.filterfalse(self.__validate_allocation, self.machines()) def service(self, name): """ Return a single service entry :param str name: service/charm name :returns: a service entry or None :rtype: Service() """ try: return next(filter(lambda s: s.service_name == name, self.services)) except StopIteration: return None @property def services(self): """ Juju services property :returns: Service() of all loaded services :rtype: generator """ for name, service in self._yaml.get('services', {}).items(): yield Service(name, service) cloud-installer-proper/cloudinstall/juju/client.py0000644000175000017500000002532512323623210021355 0ustar adamadam# # client.py - Juju api client # # Copyright 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 as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY 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 . from ws4py.client.threadedclient import WebSocketClient import json import os import time class JujuWS(WebSocketClient): def opened(self): creds = {'Type': 'Admin', 'Request': 'Login', 'RequestId': 1, 'Params' : { 'AuthTag' : 'user-admin', 'Password' : self.juju_pass}} self.send(json.dumps(creds)) def received_message(self, m): json.loads(m.data.decode('utf-8')) @property def juju_pass(self): return self._juju_pass @juju_pass.setter def juju_pass(self, password): self._juju_pass = password class JujuClient: """ Juju client class """ def __init__(self, url='wss://juju-bootstrap.maas:17070/'): self.conn = JujuWS(url, protocols=['https-only']) self._request_id = 1 self.is_connected = False def login(self, password): """ Login to juju :param password: password of juju websocket api server """ self.conn.daemon = False self.conn.juju_pass = password if not self.is_connected: self.conn.connect() time.sleep(1) self.is_connected = True def close(self): """ Closes connection to juju websocket """ self.conn.close() def call(self, params): """ Get json data from juju api daemon :params params: Additional params to be passed into request :type params: dict """ self._request_id = self._request_id + 1 params['RequestId'] = self._request_id self.conn.send(json.dumps(params)) def info(self): """ Returns Juju environment state """ return self.call(dict(Type="Client", Request="EnvironmentInfo")) def add_charm(self, charm_url): """ Adds charm """ return self.call(dict(Type="Client", Request="AddCharm", Params=dict(URL=charm_url))) def get_charm(self, charm_url): """ Get charm """ return self.call(dict(Type='Client', Request='CharmInfo', Params=dict(CharmURL=charm_url))) def get_env_constraints(self): """ Get environment constraints """ return self.call(dict(Type="Client", Request="GetEnvironmentConstraints")) def set_env_constraints(self, constraints): """ Set environment constraints """ return self.call(dict(Type="Client", Request="SetEnvironmentConstraints", Params=constraints)) def get_env_config(self): """ Get environment config """ return self.call(dict(Type="Client", Request="EnvironmentGet")) def set_env_config(self, config): """ Sets environment config variables """ return self.call(dict(Type="Client", Request="EnvironmentSet", Params=dict(Config=config))) def add_machine(self, machine): """ Allocate new machine """ return self.add_machines(machine) def add_machines(self, machines): """ Add machines """ return self.call(dict(Type="Client", Request="AddMachines", Params=dict(MachineParams=machines))) def add_relation(self, endpoint_a, endpoint_b): """ Adds relation between units """ return self.call(dict(Type="Client", Request="AddRelation", Params=dict(Endpoints=[endpoint_a, endpoint_b]))) def remove_relation(self, endpoint_a, endpoint_b): """ Removes relation """ return self.call(dict(Type="Client", Request="DestroyRelaiton", Params=dict(Endpoints=[endpoint_a, endpoint_b]))) def deploy(self, service_name, charm_url, num_units=1, config=None, constraints=None, machine_spec=None): return self.call(dict(Type="Client", Request="ServiceDeploy", Params=dict(ServiceName=service_name, CharmURL=charm_url, NumUnits=num_units, Config=config, Constraints=constraints, ToMachineSpec=machine_spec))) def set_config(self, service_name, config_keys): """ Sets machine config """ return self.call(dict(Type="Client", Request="ServiceSet", Params=dict(ServiceName=service_name, Options=config_keys))) def unset_config(self, service_name, config_keys): """ Unsets machine config """ return self.call(dict(Type="Client", Request="ServiceUnset", Params=dict(ServiceName=service_name, Options=config_keys))) def set_charm(self, service_name, charm_url, force=0): return self.call(dict(Type="Client", Request="ServiceSetCharm", Params=dict(ServiceName=service_name, CharmUrl=charm_url, Force=force))) def get_service(self, service_name): """ Get charm, config, constraits for srevice""" return self.call(dict(Type="Client", Request="ServiceGet", Params=dict(ServiceName=service_name))) def get_config(self, service_name): """ Get service configuration """ svc = self.get_service(service_name) return svc['Config'] def get_constraints(self, service_name): """ Get service constraints """ return self.call(dict(Type="Client", Request="GetServiceConstraints", Params=dict(ServiceName=service_name))) def set_constraints(self, service_name, constraints): """ Sets service level constraints """ return self.call(dict(Type="Client", Request="SetServiceConstraints", Params=dict(ServiceName=service_name, Constraints=constraints))) def update_service(self, service_name, charm_url, force_charm_url=0, min_units=1, settings={}, constraints={}): """ Update service """ return self.call(dict(Type="Client", Request="SetServiceConstraints", Params=dict(ServiceName=service_name, CharmUrl=charm_url, MinUnits=min_units, SettingsStrings=settings, Constraints=constraints))) def destroy_service(self, service_name): """ Destroy a service """ return self.call(dict(Type="Client", Request="ServiceDestroy", Params=dict(ServiceName=service_name))) def expose(self, service_name): """ Expose a service """ return self.call(dict(Type="Client", Request="ServiceExpose", Params=dict(ServiceName=service_name))) def unexpose(self, service_name): """ Unexpose service """ return self.call(dict(Type="Client", Request="ServiceUnexpose", Params=dict(ServiceName=service_name))) def valid_relation_name(self, service_name): """ All possible relation names for service """ return self.call(dict(Type="Client", Request="ServiceCharmRelations", Params=dict(ServiceName=service_name))) def add_units(self, service_name, num_units=1): """ Add units """ return self.call(dict(Type="Client", Request="AddServiceUnits", Params=dict(ServiceName=service_name, NumUnits=num_units))) def add_unit(self, service_name, machine_spec=0): """ Add unit """ return self.call(dict(Type="Client", Request="AddServiceUnits", Params=dict(MachineSpec=machine_spec))) def remove_unit(self, unit_names): """ Removes unit """ return self.call(dict(Type="Client", Request="DestroyServiceUnits", Params=dict(UnitNames=unit_names))) def resolved(self, unit_name, retry=0): """ Resolved """ return self.call(dict(Type="Client", Request="Resolved", Params=dict(UnitName=unit_name, Retry=retry))) def get_public_address(self, target): """ Gets public address of instance """ return self.call(dict(Type="Client", Request="PublicAddress", Params=dict(Target=target))) def set_annontation(self, entity, entity_type, annotation): """ Sets annontation """ return self.call(dict(Type="Client", Request="SetAnnotations", Params=dict(Tag="%-%s" % (entity_type, entity), Pairs=annotation))) def get_annotation(self, entity, entity_type): """ Gets annotation """ return self.call(dict(Type="Client", Request="GetAnnotation", Params=dict(Tag="%-s%" % (entity_type, entity)))) cloud-installer-proper/cloudinstall/utils.py0000644000175000017500000001133512323623210020256 0ustar adamadam# # utils.py - Helper utilies for cloud installer # # Copyright 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 as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY 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 . from subprocess import Popen, PIPE, DEVNULL, call, STDOUT from contextlib import contextmanager import os import re import string import random from time import strftime # String with number of minutes, or None. blank_len = None def get_command_output(command, timeout=300): """ Execute command through system shell :param command: command to run :type command: str :returns: (returncode, stdout, 0) :rtype: tuple .. code:: # Get output of juju status ret, out, rtime = utils.get_command_output('juju status') """ cmd_env = os.environ.copy() # set consistent locale cmd_env['LC_ALL'] = 'C' if timeout: command = "timeout %ds %s" % (timeout, command) p = Popen(command, shell=True, stdout=PIPE, stderr=STDOUT, bufsize=-1, env=cmd_env, close_fds=True) stdout, stderr = p.communicate() return (p.returncode, stdout.decode('utf-8'), 0) def get_network_interface(iface): """ Get network interface properties :param iface: Interface to query (ex. eth0) :type iface: str :return: interface properties or empty if none :rtype: dict .. code:: # Get address, broadcast, and netmask of eth0 iface = utils.get_network_interface('eth0') """ (status, output, runtime) = get_command_output('ifconfig %s' % (iface,)) line = output.split('\n')[1:2][0].lstrip() regex = re.compile('^inet addr:([0-9]+(?:\.[0-9]+){3})\s+Bcast:([0-9]+(?:\.[0-9]+){3})\s+Mask:([0-9]+(?:\.[0-9]+){3})') match = re.match(regex, line) if match: return {'address': match.group(1), 'broadcast': match.group(2), 'netmask': match.group(3)} return {} def get_network_interfaces(): """ Get network interfaces :returns: available interfaces and their properties :rtype: generator """ (status, output, runtime) = get_command_output('ifconfig -s') _ifconfig = output.split('\n')[1:-1] for i in _ifconfig: name = i.split(' ')[0] if 'lo' not in name: yield {name: get_network_interface(name)} def partition(pred, iterable): """ Returns tuple of allocated and unallocated systems :param pred: status predicate :type pred: function :param iterable: machine data :type iterable: list :returns: ([allocated], [unallocated]) :rtype: tuple .. code:: def is_allocated(d): allocated_states = ['started', 'pending', 'down'] return 'charms' in d or d['agent_state'] in allocated_states allocated, unallocated = utils.partition(is_allocated, [{state: 'pending'}]) """ yes, no = [], [] for i in iterable: (yes if pred(i) else no).append(i) return (yes, no) # TODO: replace with check_output() def _run(cmd): return Popen(cmd.split(), stdout=PIPE, stderr=DEVNULL).communicate()[0] def reset_blanking(): global blank_len if blank_len is not None: call(('setterm', '-blank', blank_len)) @contextmanager def console_blank(): global blank_len try: with open('/sys/module/kernel/parameters/consoleblank') as f: blank_len = f.read() except (IOError, FileNotFoundError): blank_len = None else: # Cannot use anything that captures stdout, because it is needed # by the setterm command to write to the console. call(('setterm', '-blank', '0')) # Convert the interval from seconds to minutes. blank_len = str(int(blank_len)//60) yield reset_blanking() def randomString(size=6, chars=string.ascii_uppercase + string.digits): """ Generate a random string :param size: number of string characters :type size: int :param chars: range of characters (optional) :type chars: str :returns: a random string :rtype: str """ return ''.join(random.choice(chars) for x in range(size)) def time(): """ Time helper :returns: formatted current time string :rtype: str """ return strftime('%Y-%m-%d %H:%M') cloud-installer-proper/cloudinstall/__pycache__/0000755000175000017500000000000012323645516020766 5ustar adamadamcloud-installer-proper/cloudinstall/service.py0000644000175000017500000000643512323623210020563 0ustar adamadam# # service.py - Juju Services and Units # # Copyright 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 as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY 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 . """ Represents a Juju service """ class Unit: """ Unit class """ def __init__(self, unit_name, unit): self.unit_name = unit_name self.unit = unit @property def agent_state(self): """ Unit's agent state :returns: agent state :rtype: str """ return self.unit.get('agent-state', 'unknown') @property def machine_id(self): """ Associate machine for unit :returns: machine id :rtype: str """ return self.unit.get('machine', '-1') @property def public_address(self): """ Public address of unit :returns: address of unit :rtype: str """ return self.unit.get('public-address', '0.0.0.0') def __repr__(self): return "".format(name=self.unit_name, machine=self.machine_id) class Relation: """ Relation class """ def __init__(self, relation_name, charms): self.relation_name = relation_name self.charms = charms class Service: """ Service class """ def __init__(self, service_name, service): self.service_name = service_name self.service = service @property def charm(self): """ Charm :returns: Charm Path :rtype: str """ return self.service.get('charm', '') @property def exposed(self): """ Exposed :returns: if service is exposed :rtype: bool """ def unit(self, name): """ Single unit entry :params str name: name of unit :returns: a Unit entry :rtype: Unit() """ u = list(filter(lambda u: u.unit_name == name, self.units))[0] if u: return u return Unit('unknown', []) @property def units(self): """ Service units :returns: list associated units for service :rtype: Unit() """ for unit_name, units in self.service.get('units', {}).items(): yield Unit(unit_name, units) @property def relations(self): """ Service relations :returns: list of relations for service :rtype: Relation() """ for relation_name, relation in \ self.service.get('relations', {}).items(): yield Relation(relation_name, relation) def __repr__(self): return "".format(name=self.service_name, units=list(self.units)) cloud-installer-proper/cloudinstall/maas/0000755000175000017500000000000012323631312017464 5ustar adamadamcloud-installer-proper/cloudinstall/maas/__init__.py0000644000175000017500000001264112323623210021577 0ustar adamadam# # __init__.py - MAAS instance state # # Copyright 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 as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY 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 . from cloudinstall.machine import Machine class MaasMachine(Machine): """ Single maas machine """ @property def hostname(self): """ Query hostname reported by MaaS :returns: hostname :rtype: str """ return self.machine.get('hostname', '') @property def status(self): """ Status of machine state Those statuses are defined as follows: DECLARED = 0 COMMISSIONING = 1 FAILED_TESTS = 2 MISSING = 3 READY = 4 RESERVED = 5 ALLOCATED = 6 RETIRED = 7 :returns: status :rtype: int """ return self.machine.get('status', 0) @property def zone(self): """ Zone information :returns: zone information :rtype: dict """ return self.machine.get('zone', {}) @property def cpu_cores(self): """ Returns number of cpu-cores :returns: number of cpus :rtype: str """ return self.machine.get('cpu_count', '0') @property def storage(self): """ Return storage :returns: storage size :rtype: str """ try: _storage_in_gb = int(self.machine.get('storage')) / 1024 except ValueError: return "N/A" return "{size}G".format(size=str(_storage_in_gb)) @property def arch(self): """ Return architecture :returns: architecture type :rtype: str """ return self.machine.get('architecture') @property def mem(self): """ Return memory :returns: memory size :rtype: str """ try: _mem = int(self.machine.get('memory')) except ValueError: return "N/A" if _mem > 1024: _mem = _mem / 1024 return "{size}G".format(size=str(_mem)) else: return "{size}M".format(size=str(_mem)) @property def power_type(self): """ Machine power type :returns: machines power type :rtype: str """ return self.machine.get('power_type', 'None') @property def instance_id(self): """ Returns instance-id of a machine :returns: instance-id of machine :rtype: str """ return self.machine.get('resource_uri', '') @property def system_id(self): """ Returns system id of a maas machine :returns: system id of machine :rtype: str """ return self.machine.get('system_id', '') @property def ip_addresses(self): """ Ip addresses for machine :returns: ip addresses :rtype: list """ return self.machine.get('ip_addresses', []) @property def mac_address(self): """ Macaddress set of maas machine :returns: mac_address and resource_uri :rtype: dict """ return self.machine.get('macaddress_set', {}) @property def tag_names(self): """ Tag names for machine :returns: tags associated with machine :rtype: list """ return self.machine.get('tag_names', []) @property def tag(self): """ Machine tag :returns: tag defined :rtype: str """ return self.machine.get('tag', '') @property def owner(self): """ Machine owner :returns: owner :rtype: str """ return self.machine.get('owner', 'root') class MaasState: """ Represents global MaaS state """ DECLARED = 0 COMMISSIONING = 1 FAILED_TESTS = 2 MISSING = 3 READY = 4 RESERVED = 5 ALLOCATED = 6 RETIRED = 7 def __init__(self, maas): self.maas = maas def __iter__(self): return iter(self.maas) def machine(self, instance_id): """ Return single machine state :param str instance_id: machine instance_id :returns: machine :rtype: cloudinstall.maas.MaasMachine """ for m in self.machines(): if m.instance_id == instance_id: return m def machines(self): """ Maas Machines :returns: machines known to maas :rtype: generator """ for machine in self.maas: if 'juju-bootstrap.maas' in machine['hostname']: continue yield MaasMachine(-1, machine) def num_in_state(self, state): """ Number of machines in a particular state :param str state: a machine state :returns: number of machines in `status` :rtype: int """ return len(list(filter(lambda m: int(m.status) == state, self.machines()))) cloud-installer-proper/cloudinstall/maas/client.py0000644000175000017500000002142712323623210021320 0ustar adamadam# # client.py - Client routines for MAAS API # # Copyright 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 as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY 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 . from cloudinstall.maas import MaasState from requests_oauthlib import OAuth1 import requests import json class MaasClient: """ Client Class """ def __init__(self, auth): """ Entry point to client routines for interfacing with MAAS api. :param auth: MAAS Authorization class (required) """ self.auth = auth def _oauth(self): """ Generates OAuth attributes for protected resources :returns: OAuth class """ oauth = OAuth1(self.auth.consumer_key, client_secret=self.auth.consumer_secret, resource_owner_key=self.auth.token_key, resource_owner_secret=self.auth.token_secret, signature_method='PLAINTEXT', signature_type='query') return oauth def get(self, url, params=None): """ Performs a authenticated GET against a MAAS endpoint :param url: MAAS endpoint :param params: extra data sent with the HTTP request """ return requests.get(url=self.auth.api_url + url, auth=self._oauth(), params=params) def post(self, url, params=None): """ Performs a authenticated POST against a MAAS endpoint :param url: MAAS endpoint :param params: extra data sent with the HTTP request """ return requests.post(url=self.auth.api_url + url, auth=self._oauth(), data=params) def delete(self, url, params=None): """ Performs a authenticated DELETE against a MAAS endpoint :param url: MAAS endpoint :param params: extra data sent with the HTTP request """ return requests.delete(url=self.auth.api_url + url, auth=self._oauth()) ########################################################################### # Node API ########################################################################### @property def nodes(self): """ Nodes managed by MAAS :returns: managed nodes :rtype: list """ res = self.get('/nodes/', dict(op='list')) if res.ok: return json.loads(res.text) return [] def nodes_accept_all(self): """ Accept all commissioned nodes :returns: Status :rtype: bool """ res = self.post('/nodes/', dict(op='accept_all')) if res.ok: return True return False def node_commission(self, system_id): """ (Re)commission a node :param system_id: machine identification :returns: True on success False on failure """ res = self.post('/nodes/%s' % (system_id,), dict(op='commission')) if res.ok: return True return False def node_start(self, system_id): """ Power up a node :param system_id: machine identification :returns: True on success False on failure """ res = self.post('/nodes/%s' % (system_id,), dict(op='start')) if res.ok: return True return False def node_stop(self, system_id): """ Shutdown a node :param system_id: machine identification :returns: True on success False on failure """ res = self.post('/nodes/%s' % (system_id,), dict(op='stop')) if res.ok: return True return False def node_remove(self, system_id): """ Delete a node :param system_id: machine identification :returns: True and success False on failure """ res = self.delete('/nodes/%s' % (system_id,)) if res.ok: return True return False ########################################################################### # Tag API ########################################################################### @property def tags(self): """ List tags known to MAAS :returns: List of tags or empty list """ res = self.get('/tags/', dict(op='list')) if res.ok: return json.loads(res.text) return [] def tag_new(self, tag): """ Create tag if it doesn't exist. :param tag: Tag name :returns: Success/Fail boolean """ tags = {tagmd['name'] for tagmd in self.tags} if tag not in tags: res = self.post('/tags/', dict(op='new',name=tag)) return res.ok return False def tag_delete(self, tag): """ Delete a tag :param tag: tag id :type tag: str :returns: True on success False on failure :rtype: bool """ res = self.delete('/tags/%s' % (tag,)) if res.ok: return True return False def tag_machine(self, tag, system_id): """ Tag the machine with the specified tag. :param tag: Tag name :type tag: str :param system_id: ID of node :type system_id: str :returns: Success or Fail :rtype: bool """ # Make use of rest api res = self.post('/tags/%s/' % (tag,), dict(op='update_nodes', add=system_id)) if res.ok: return True return False def tag_name(self, maas): """ Tag each node as its hostname. This is a bit ugly. Since we want to be able to juju deploy to a particular node that the user has selected, we use juju's constraints support for maas. Unfortunately, juju didn't implement maas-name directly, we have to tag each node with its hostname for now so that we can pass that tag as a constraint to juju. :param maas: MAAS object representing all managed nodes """ for machine in maas.machines(): tag = machine.system_id if 'tag_names' not in machine.tag_names or tag not in machine.tag_names: self.tag_new(tag) self.tag_machine(tag, tag) def tag_fpi(self, maas): """ Tag each DECLARED host with the FPI tag. Also a little strange: we could define a tag with 'definition=true()' and automatically tag each node. However, each time we un-tag a node, maas evaluates the xpath expression again and re-tags it. So, we do it once, manually, when the machine is in the DECLARED state (also to avoid re-tagging things that have already been tagged). :param maas: MAAS object representing all managed nodes """ FPI_TAG = 'use-fastpath-installer' self.tag_new(FPI_TAG) for machine in maas: if machine['status'] == MaasState.DECLARED: self.tag_machine(FPI_TAG, machine['system_id']) ########################################################################### # Users API ########################################################################### @property def users(self): """ List users on MAAS :returns: List of registered users or an empty list """ res = self.get('/users/') if res.ok: return json.loads(res.text) return [] ########################################################################### # Zone API ########################################################################### @property def zones(self): """ List physical zones :returns: List of managed zones or empty list """ res = self.get('/zones/') if res.ok: return json.loads(res.text) return [] def zone_new(self, name, description="Zone created by API"): """ Create a physical zone :param name: Name of the zone :param description: Description of zone. :returns: True on success False on failure """ res = self.post('/zones/', dict(name=name, description=description)) if res.ok: return True return False cloud-installer-proper/cloudinstall/maas/auth.py0000644000175000017500000001017212323623210020776 0ustar adamadam# # auth.py - MAAS Authentication # # Copyright 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 as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY 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 . from subprocess import check_output, check_call, DEVNULL import os import requests import yaml import sys from cloudinstall.log import logger log = logger(__name__) class MaasAuth: """ MAAS Authorization class """ def __init__(self): """ Initialize with optional OAuth credentials """ self.api_url = 'http://localhost/MAAS/api/1.0' self.api_key = None self.consumer_secret = '' @property def is_logged_in(self): """ Checks if we are logged into the MAAS api :rtype: bool """ return True if self.api_key else False @property def consumer_key(self): """ Maas consumer key :rtype: str """ return self.api_key.split(':')[0] if self.api_key else None @property def token_key(self): """ Maas oauth token key :rtype: str """ return self.api_key.split(':')[1] if self.api_key else None @property def token_secret(self): """ Maas oauth token secret :rtype: str """ return self.api_key.split(':')[2] if self.api_key else None def get_api_key(self, username='root'): """ MAAS api key :param username: (optional) MAAS user to query for credentials :type username: str """ maas_creds_file = os.path.expanduser('~/.cloudinstall/maas-creds') if os.path.isfile(maas_creds_file): with open(maas_creds_file, 'r') as f: self.api_key = f.read().rstrip('\n') else: log.debug("Could not find credentials, attempting to login.") self.api_key = check_output(['sudo', 'maas-region-admin', 'apikey', '--username', username]).decode('ascii').rstrip('\n') def read_config(self, url, creds): """Read cloud-init config from given `url` into `creds` dict. Updates any keys in `creds` that are None with their corresponding values in the config. Important keys include `metadata_url`, and the actual OAuth credentials. :param url: cloud-init config URL :type url: str :param creds: MAAS user credentials :type creds: dict """ if url.startswith("http://") or url.startswith("https://"): cfg_str = requests.get(url=url).content else: if url.startswith("file://"): url = url[7:] cfg_str = open(url, "r").read() cfg = yaml.safe_load(cfg_str) # Support reading cloud-init config for MAAS datasource. if 'datasource' in cfg: cfg = cfg['datasource']['MAAS'] for key in creds.keys(): if key in cfg and creds[key] is None: creds[key] = cfg[key] def login(self): """ Login to MAAS api server .. todo:: Deprecate once MAAS api matures (http://pad.lv/1058137) """ if not self.api_key: raise Exception('No api_key was found, please run ' '`cloud-install maas-creds -u root`') sys.exit(1) check_call('maas login maas http://localhost/MAAS/api/1.0 ' '%s' % (self.api_key,), shell=True, stderr=DEVNULL, stdout=DEVNULL) cloud-installer-proper/cloudinstall/machine.py0000644000175000017500000001350412323641230020524 0ustar adamadam# # machine.py - Machine # # Copyright 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 as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY 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 . class Machine: """ Base machine class """ def __init__(self, machine_id, machine): self.machine_id = machine_id self.machine = machine self._cpu_cores = self.hardware('cpu-cores') self._storage = self.hardware('root-disk') self._mem = self.hardware('memory') @property def is_machine_0(self): """ Checks if machine is bootstrapped node :rtype: bool """ return "0" in self.machine_id @property def is_machine_1(self): """ Checks if machine is first machine This holds the openstack services needed to manage your cloud. Everything except nova-compute should be deployed here. :rtype: bool """ return "1" in self.machine_id @property def cpu_cores(self): """ Return number of cpu-cores :returns: number of cpus :rtype: str """ return self._cpu_cores @cpu_cores.setter def cpu_cores(self, val): self._cpu_cores = val @property def arch(self): """ Return architecture :returns: architecture type :rtype: str """ return self.hardware('arch') @property def storage(self): """ Return storage :returns: storage size :rtype: str """ try: _storage_in_gb = int(self._storage[:-1]) / 1024 except ValueError: return "N/A" return "{size}G".format(size=str(_storage_in_gb)) @storage.setter def storage(self, val): self._storage = val @property def mem(self): """ Return memory :returns: memory size :rtype: str """ try: _mem = int(self._mem[:-1]) except ValueError: return "N/A" if _mem > 1024: _mem = _mem / 1024 return "{size}G".format(size=str(_mem)) else: return "{size}M".format(size=str(_mem)) @mem.setter def mem(self, val): self._mem = val def hardware(self, spec): """ Get hardware information :param spec: a hardware specification :type spec: str :returns: hardware of spec :rtype: str """ _machine = self.machine.get('hardware', None) if _machine: _hardware_list = _machine.split(' ') for item in _hardware_list: k, v = item.split('=') if k in spec: return v return 'N/A' @property def instance_id(self): """ Returns instance-id of a machine :returns: instance-id of machine :rtype: str """ return self.machine.get('instance-id', '') @property def dns_name(self): """ Returns dns-name :rtype: str """ return self.machine.get('dns-name', '') @property def agent_state(self): """ Returns agent-state :rtype: str """ return self.machine.get('agent-state', '') @property def charms(self): """ Returns charms for machine :returns: charms for machine :rtype: generator """ def charm_name(charm): return charm.split("/")[0] for unit in self.units: yield charm_name(unit.unit_name) @property def units(self): """ Return units for machine :rtype: list """ return self.machine.get('units', []) @property def containers(self): """ Return containers for machine :rtype: generator """ _containers = self.machine.get('containers', {}).items() for container_id, container in _containers: yield Machine(container_id, container) def container(self, container_id): """ Inspect a container :param container_id: lxc container id :type container_id: int :returns: Returns a dictionary of the container information for specific machine and lxc id. :rtype: dict """ for m in self.containers: if m.machine_id == container_id: return m return Machine('0/lxc/0', {'agent-state': 'unallocated', 'dns-name': 'unallocated'}) def __str__(self): return "id: {machine_id}, state: {state}, " \ "dns-name: {dns_name}, mem: {mem}, " \ "storage: {storage}, " \ "cpus: {cpus}".format(machine_id=self.machine_id, dns_name=self.dns_name, state=self.agent_state, mem=self.mem, storage=self.storage, cpus=self.cpu_cores) def __repr__(self): return "".format(dns_name=self.dns_name, state=self.agent_state, mem=self.mem, storage=self.storage, cpus=self.cpu_cores) cloud-installer-proper/Makefile0000644000175000017500000000246112323623210015507 0ustar adamadam# # Makefile for cloud-install # NAME = cloud-installer TOPDIR := $(shell basename `pwd`) GIT_REV := $(shell git log --oneline -n1| cut -d" " -f1) VERSION := $(shell ./tools/version) $(NAME)_$(VERSION).orig.tar.gz: clean cd .. && tar czf $(NAME)_$(VERSION).orig.tar.gz $(TOPDIR) --exclude-vcs --exclude=debian tarball: $(NAME)_$(VERSION).orig.tar.gz .PHONY: install-dependencies install-dependencies: sudo apt-get install devscripts equivs sudo mk-build-deps -i debian/control clean: @debian/rules clean @rm -rf debian/cloud-install @rm -rf docs/_build/* @rm -rf ../cloud-*.deb ../cloud-*.tar.gz ../cloud-*.dsc ../cloud-*.changes \ ../cloud-*.build deb-src: clean update_version tarball @debuild -S -us -uc deb: clean update_version tarball @debuild -us -uc -i sbuild: clean update_version tarball @sbuild -d trusty-amd64 -j4 current_version: @echo $(VERSION) git_rev: @echo $(GIT_REV) update_version: wrap-and-sort @sed -i -r "s/(^__version__\s=\s)(.*)/\1\"$(VERSION)\"/" cloudinstall/__init__.py status: PYTHONPATH=$(shell pwd):$(PYTHONPATH) bin/cloud-status # sudo make run type=multi proxy=http://localhost:3128/ .PHONY: run run: deb -dpkg -i ../cloud-installer*deb -dpkg -i ../cloud-install-${type}*deb apt-get install -f MAAS_HTTP_PROXY=${proxy} cloud-install all: deb cloud-installer-proper/.travis.yml0000644000175000017500000000036112323623210016155 0ustar adamadamlanguage: python python: - 3.3 notifications: email: false install: - "pip install -r requirements.txt --use-mirrors" - "python setup.py install" script: - "nosetests -v --with-cover --cover-package=cloudinstall --cover-html test" cloud-installer-proper/bin/0000755000175000017500000000000012323640001014611 5ustar adamadamcloud-installer-proper/bin/ip_range.py0000755000175000017500000000503412323632603016765 0ustar adamadam#!/usr/bin/env python3 # # ip_range.py - Cloud install ip range utility # # Copyright 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 as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY 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 . from ipaddress import ip_address, ip_network def ip_range(network): """Return tuple of low, high IP address for given network""" num_addresses = network.num_addresses if num_addresses == 1: host = network[0] return host, host elif num_addresses == 2: return network[0], network[-1] else: return network[1], network[-2] def ip_range_max(network, exclude): """Return tuple of low, high IP address for largest IP address range within the given network. Accepts a list of IP addresses to exclude. """ if (network.num_addresses <= 2) or (len(exclude) == 0): return ip_range(network) current = range(0, 0) remaining = range(int(network[1]), int(network[-1])) excluded = sorted(set(exclude)) for ex in excluded: e = int(ex) if e in remaining: index = remaining.index(e) if index != 0: r = remaining[:index] if len(r) > len(current): current = r index += 1 if index < len(remaining): remaining = remaining[index:] else: remaining = range(0, 0) break length = len(current) if length < len(remaining): current = remaining elif length == 0: return ip_range(network) return ip_address(current[0]), ip_address(current[-1]) if __name__ == "__main__": import sys from sys import argv args = len(argv) if args == 1: print("Missing arguments") sys.exit(1) network = ip_network(argv[1], strict=False) ip_low, ip_high = ip_range_max(network, [ip_address(arg) for arg in argv[2:]]) \ if args >= 3 else ip_range(network) print(str(ip_low) + "-" + str(ip_high)) cloud-installer-proper/bin/cloud-install0000755000175000017500000000546112323640001017317 0ustar adamadam#!/bin/sh -e # # cloud-install - Cloud installer # # Copyright 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 as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY 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 . . /usr/share/cloud-installer/common/display.sh . /usr/share/cloud-installer/common/common.sh . /usr/share/cloud-installer/common/maas.sh . /usr/share/cloud-installer/common/juju.sh . /usr/share/cloud-installer/common/configure.sh . /usr/share/cloud-installer/common/multi.sh . /usr/share/cloud-installer/common/single.sh . /usr/share/cloud-installer/common/landscape.sh OPT_HELP=h OPT_INSTALL=i OPT_FORCE=f OPTS=:${OPT_INSTALL}${OPT_HELP}${OPT_FORCE} USAGE="\ cloud-install [-${OPT_INSTALL}${OPT_HELP}${OPT_FORCE}] Create an Ubuntu Cloud! (requires root privileges) Options: -$OPT_INSTALL install only (don't invoke cloud-status) -$OPT_HELP print this message -$OPT_FORCE bypass any sanity checks" usage() { echo "$USAGE" } usageError() { echo "$1" >&2 usage >&2 } while getopts $OPTS opt; do case $opt in $OPT_INSTALL) install=true ;; $OPT_HELP) usage exit 0 ;; $OPT_FORCE) force_install=true ;; \?) usageError "Unknown argument: $OPTARG" exit 1 ;; esac done shift $((OPTIND - 1)) if [ $(id -u) -ne 0 ]; then usageError "Installing a cloud requires root privileges. Rerun with sudo." exit 1 fi createTempDir startLog trap exitInstall EXIT trap "" PIPE # TODO add better logging set -x #disableBlank if [ -d /home/$INSTALL_USER/.juju ] && [ -z "$force_install" ]; then echo "You've already got juju configured! Aborting..." && exit 0 fi if [ ! -e /etc/.cloud-installed ]; then configureInstall mkdir -m 0700 "/home/$INSTALL_USER/.cloud-install" || true echo "$openstack_password" > "/home/$INSTALL_USER/.cloud-install/openstack.passwd" chmod 0600 "/home/$INSTALL_USER/.cloud-install/openstack.passwd" chown -R "$INSTALL_USER:$INSTALL_USER" "/home/$INSTALL_USER/.cloud-install" case $install_type in Multi-system) multiInstall ;; "Single system") singleInstall ;; "Landscape managed") landscapeInstall ;; *) # install cancelled by user exit 0 ;; esac touch /etc/.cloud-installed else echo "Cloud already installed." fi if [ -z "$install" ]; then exitInstall cd "/home/$INSTALL_USER"; exec sudo -H -u "$INSTALL_USER" cloud-status fi cloud-installer-proper/bin/wait-for-landscape0000755000175000017500000000403012323623210020217 0ustar adamadam#!/usr/bin/env python3 # -*- mode: python; -*- # # cloud-status - Displays status of all managed nodes # # Copyright 2014 Canonical, Ltd. # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This package is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . from cloudinstall.juju.state import JujuState from subprocess import check_output import requests from requests.exceptions import ConnectionError import time MAGIC_OK_STRING = 'New user - Landscape' def get_landscape_host(): """ Assuming landscape has been deployed in landscape-dense-maas form, find the "dns-name" of the landscape web server. """ juju = JujuState(check_output(['juju', 'status'])) for container, (charms, units) in juju.containers.items(): machine_no, _, lxc_id = container.split('/') if 'apache2' in charms: return juju.container(machine_no, lxc_id)['dns-name'] raise Exception("Landscape not found!") def main(): host = get_landscape_host() while True: try: # Landscape generates a self signed cert for each install. r = requests.get('http://%s/' % host, verify=False) if MAGIC_OK_STRING in r.text: # now do an API call to make sure the API is up (it gives 503 # for a while) r = requests.get('http://%s/api' % host, verify=False) if r.status_code == 200: break except ConnectionError: pass time.sleep(10) print(host) if __name__ == '__main__': main() cloud-installer-proper/bin/cloud-status0000755000175000017500000000251212323623210017171 0ustar adamadam#!/usr/bin/env python3 # -*- mode: python; -*- # # cloud-status - Displays status of all managed nodes # # Copyright 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 as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY 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 . import signal import sys import os from cloudinstall import gui from cloudinstall import pegasus from cloudinstall import utils # TODO: Why does this crash? # pegasus.wait_for_services() def sig_handler(signum, frame): utils.reset_blanking() sys.exit(1) for sig in (signal.SIGTERM, signal.SIGQUIT, signal.SIGINT, signal.SIGHUP): signal.signal(sig, sig_handler) def get_data(): """ Starts the polling routine. """ return pegasus.poll_state() if __name__ == '__main__': gui = gui.PegasusGUI(get_data) sys.exit(gui.run()) cloud-installer-proper/bin/maas-report-boot-images0000755000175000017500000000135212323623210021201 0ustar adamadam#!/usr/bin/env python2 # This must be run as root because I can't figure out how to switch the # effective django user during database access. However, it should bootstrap # the rest of itself. import os import sys sys.path.extend(['/etc/maas', '/usr/share/maas']) os.environ['MAAS_URL'] = 'http://localhost/MAAS' os.environ['DJANGO_SETTINGS_MODULE'] = 'maas.settings' from maasserver.models.nodegroup import NodeGroup from provisioningserver import cache, auth, tasks cluster = NodeGroup.objects.first() os.environ['CLUSTER_UUID'] = cluster.uuid creds = '%s:%s:%s' % ( cluster.api_token.consumer.key, cluster.api_token.key, cluster.api_token.secret) cache.initialize() auth.record_api_credentials(creds) tasks.report_boot_images() cloud-installer-proper/test/0000755000175000017500000000000012323623210015023 5ustar adamadamcloud-installer-proper/test/test_maasstate.py0000644000175000017500000000056212323623210020421 0ustar adamadamimport sys sys.path.append('../cloudinstall') from cloudinstall.pegasus import MaasState import helpers load_status = lambda f: helpers.load_status(f, MaasState) # @load_status('test/maas-output/twonodes.out') # def test_twonodes(s): # assert s.machines == 2 # assert s.num_in_state(MaasState.ALLOCATED) == 1 # assert s.num_in_state(MaasState.READY) == 1 cloud-installer-proper/test/helpers.py0000644000175000017500000000140012323623210017032 0ustar adamadamimport contextlib from cloudinstall import pegasus def load_status(fname, cons): def wrap(f): def new_f(): with open(fname) as inp: return f(cons(inp)) # copy the name to make nose happy new_f.__name__ = f.__name__ return new_f return wrap # def parse_output(name): # with open('juju-output/%s.out' % name) as juju_out: # with open('maas-output/%s.out' % name) as maas_out: # maas = MaasState(maas_out) # juju = JujuState(juju_out.read()) # return pegasus.parse_state(juju, maas) @contextlib.contextmanager def set_single_system(setting): old = pegasus.SINGLE_SYSTEM pegasus.SINGLE_SYSTEM = setting yield pegasus.SINGLE_SYSTEM = old cloud-installer-proper/test/test_jujuclient.py0000644000175000017500000000361512323623210020615 0ustar adamadam#!/usr/bin/env python # # test_jujuclient.py - Juju Api Tests # # Copyright 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 as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY 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 . # # Usage: # juju bootstrap # nose test import unittest import os import sys sys.path.insert(0, '../cloudinstall') from cloudinstall.juju.client import JujuClient from cloudinstall.utils import randomString JUJU_PASS = os.environ.get('JUJU_PASS', randomString()) JUJU_URL = os.environ.get('JUJU_URL', 'wss://juju-bootstrap.master:17070/') JUJU_INSTALLED = os.path.exists(os.path.join(os.path.expanduser('~'), '.juju/environments.yaml')) @unittest.skipIf(not JUJU_INSTALLED, "Juju is not installed") class JujuClientTest(unittest.TestCase): def setUp(self): self.c = JujuClient(url=JUJU_URL) def tearDown(self): self.c.close() def test_login(self): self.c.login(JUJU_PASS) self.assertTrue(self.c.is_connected) @unittest.skipIf(not JUJU_INSTALLED, "Juju is not installed") class JujuApiTest(unittest.TestCase): def setUp(self): self.c = JujuClient(url=JUJU_URL) self.c.login(JUJU_PASS) def tearDown(self): self.c.close() def test_info(self): ret = self.c.info() self.assertTrue(ret) if __name__ == '__main__': unittest.main() cloud-installer-proper/test/juju-output/0000755000175000017500000000000012323623210017336 5ustar adamadamcloud-installer-proper/test/juju-output/pending.out0000644000175000017500000000512412323623210021515 0ustar adamadamenvironment: maas machines: "0": agent-state: started agent-version: 1.16.3 instance-id: /MAAS/api/1.0/nodes/node-juju-bootstrap/ instance-state: missing series: saucy "1": agent-state: started agent-version: 1.16.3 dns-name: juju-host.master instance-id: /MAAS/api/1.0/nodes/node-ed4672a0-539e-11e3-888b-525400f40f5c/ series: precise containers: 1/lxc/0: instance-id: pending series: precise 1/lxc/1: instance-id: pending series: precise 1/lxc/2: instance-id: pending series: precise 1/lxc/3: instance-id: pending series: precise 1/lxc/4: instance-id: pending series: precise 1/lxc/5: instance-id: pending series: precise services: glance: charm: cs:precise/glance-26 exposed: false relations: cluster: - glance identity-service: - keystone image-service: - nova-cloud-controller shared-db: - mysql units: glance/0: agent-state: pending machine: 1/lxc/0 keystone: charm: cs:precise/keystone-23 exposed: false relations: cluster: - keystone identity-service: - glance - nova-cloud-controller - openstack-dashboard shared-db: - mysql units: keystone/0: agent-state: pending machine: 1/lxc/5 mysql: charm: cs:precise/mysql-29 exposed: false relations: cluster: - mysql shared-db: - glance - keystone - nova-cloud-controller units: mysql/0: agent-state: pending machine: 1/lxc/1 nova-cloud-controller: charm: cs:precise/nova-cloud-controller-19 exposed: false relations: amqp: - rabbitmq-server cluster: - nova-cloud-controller identity-service: - keystone image-service: - glance shared-db: - mysql units: nova-cloud-controller/0: agent-state: pending machine: 1/lxc/4 openstack-dashboard: charm: cs:precise/openstack-dashboard-11 exposed: false relations: cluster: - openstack-dashboard identity-service: - keystone units: openstack-dashboard/0: agent-state: pending machine: 1/lxc/3 rabbitmq-server: charm: cs:precise/rabbitmq-server-16 exposed: false relations: amqp: - nova-cloud-controller cluster: - rabbitmq-server units: rabbitmq-server/0: agent-state: pending machine: 1/lxc/2 cloud-installer-proper/test/juju-output/lxc-controller-deployed.out0000644000175000017500000000502012323623210024636 0ustar adamadamenvironment: local machines: "0": agent-state: started agent-version: 1.15.1.1 dns-name: 10.0.3.1 instance-id: localhost series: raring "1": agent-state: started agent-version: 1.15.1.1 instance-id: tycho-local-machine-1 instance-state: missing series: precise containers: 1/lxc/0: instance-id: pending series: precise 1/lxc/1: instance-id: pending series: precise 1/lxc/2: instance-id: pending series: precise 1/lxc/3: instance-id: pending series: precise 1/lxc/4: instance-id: pending series: precise 1/lxc/5: instance-id: pending series: precise services: glance: charm: cs:precise/glance-19 exposed: false relations: cluster: - glance identity-service: - keystone shared-db: - mysql units: glance/0: agent-state: pending machine: 1/lxc/2 keystone: charm: cs:precise/keystone-19 exposed: false relations: cluster: - keystone identity-service: - glance - openstack-dashboard shared-db: - mysql units: keystone/0: agent-state: pending machine: 1/lxc/3 mysql: charm: cs:precise/mysql-27 exposed: false relations: cluster: - mysql shared-db: - glance - keystone units: mysql/0: agent-state: pending machine: 1/lxc/1 nova-cloud-controller: charm: cs:precise/nova-cloud-controller-14 exposed: false relations: cluster: - nova-cloud-controller units: nova-cloud-controller/0: agent-state: pending machine: 1/lxc/0 openstack-dashboard: charm: cs:precise/openstack-dashboard-9 exposed: false relations: cluster: - openstack-dashboard identity-service: - keystone units: openstack-dashboard/0: agent-state: pending machine: 1/lxc/5 rabbitmq-server: charm: cs:precise/rabbitmq-server-14 exposed: false relations: cluster: - rabbitmq-server units: rabbitmq-server/0: agent-state: pending machine: 1/lxc/4 wordpress: charm: cs:precise/wordpress-19 exposed: false relations: loadbalancer: - wordpress units: wordpress/0: agent-state: started agent-version: 1.15.1.1 machine: "1" open-ports: - 80/tcp public-address: 10.0.3.225 cloud-installer-proper/test/juju-output/one-pending.out0000644000175000017500000000032112323623210022266 0ustar adamadammachines: 0: agent-state: not-started dns-name: slave1.local instance-id: /MAAS/api/1.0/nodes/node-4c49e73e-e8b8-11e2-ac16-5254002cb1d6/ instance-state: unknown 1: instance-id: pending cloud-installer-proper/test/juju-output/service-pending.out0000644000175000017500000000104512323623210023151 0ustar adamadammachines: 0: agent-state: not-started dns-name: slave1.local instance-id: /MAAS/api/1.0/nodes/node-4c49e73e-e8b8-11e2-ac16-5254002cb1d6/ instance-state: unknown 3: agent-state: not-started instance-id: /MAAS/api/1.0/nodes/node-5fb74ba0-e8c1-11e2-b109-5254002cb1d6/ dns-name: slave2.local instance-state: unknown services: mysql: charm: cs:precise/mysql-25 relations: cluster: - mysql units: mysql/1: agent-state: not-started machine: 3 public-address: null cloud-installer-proper/test/juju-output/initial-install-config.out0000644000175000017500000000061012323623210024424 0ustar adamadamenvironment: maas machines: "0": agent-state: started agent-version: 1.16.3 instance-id: /MAAS/api/1.0/nodes/node-juju-bootstrap/ instance-state: missing series: saucy "1": agent-state: started agent-version: 1.16.3 dns-name: juju-host.master instance-id: /MAAS/api/1.0/nodes/node-29e47a24-52c6-11e3-ae66-525400ab8ba1/ series: precise services: {} cloud-installer-proper/test/juju-output/lxc.out0000644000175000017500000000227012323623210020656 0ustar adamadamenvironment: local machines: "0": agent-state: started agent-version: 1.15.0.1 dns-name: 10.0.3.1 instance-id: localhost series: raring "1": agent-state: started agent-version: 1.15.0.1 instance-id: tycho-local-machine-1 instance-state: missing series: precise containers: 1/lxc/0: instance-id: pending series: precise "2": agent-state: started agent-version: 1.15.0.1 instance-id: tycho-local-machine-2 instance-state: missing series: precise services: mediawiki: charm: cs:precise/mediawiki-10 exposed: false units: mediawiki/0: agent-state: pending machine: 1/lxc/0 mysql: charm: cs:precise/mysql-27 exposed: false relations: cluster: - mysql units: mysql/0: agent-state: pending agent-version: 1.15.0.1 machine: "1" public-address: 10.0.3.81 wordpress: charm: cs:precise/wordpress-19 exposed: false relations: loadbalancer: - wordpress units: wordpress/0: agent-state: pending agent-version: 1.15.0.1 machine: "2" public-address: 10.0.3.77 cloud-installer-proper/test/juju-output/no-services.out0000644000175000017500000000030012323623210022315 0ustar adamadammachines: 0: agent-state: not-started dns-name: slave1.local instance-id: /MAAS/api/1.0/nodes/node-4c49e73e-e8b8-11e2-ac16-5254002cb1d6/ instance-state: unknown services: {} cloud-installer-proper/test/juju-output/juju-status-single-install.yaml0000644000175000017500000000631212323623210025445 0ustar adamadamenvironment: local machines: "0": agent-state: started agent-version: 1.18.0.1 dns-name: localhost instance-id: localhost series: trusty "2": agent-state: started agent-version: 1.18.0.1 dns-name: 10.0.3.174 instance-id: poe-local-machine-2 series: precise hardware: arch=amd64 cpu-cores=1 mem=512M root-disk=8192M "3": agent-state: started agent-version: 1.18.0.1 dns-name: 10.0.3.218 instance-id: poe-local-machine-3 series: precise hardware: arch=amd64 cpu-cores=1 mem=1024M root-disk=8192M services: ceph: charm: cs:precise/ceph-22 exposed: false relations: mon: - ceph units: ceph/0: agent-state: error agent-state-info: 'hook failed: "config-changed"' agent-version: 1.18.0.1 machine: "3" public-address: 10.0.3.218 glance: charm: cs:precise/glance-30 exposed: false relations: cluster: - glance units: glance/0: agent-state: started agent-version: 1.18.0.1 machine: "3" open-ports: - 9292/tcp public-address: 10.0.3.218 keystone: charm: cs:precise/keystone-31 exposed: false relations: cluster: - keystone shared-db: - mysql units: keystone/0: agent-state: error agent-state-info: 'hook failed: "shared-db-relation-changed"' agent-version: 1.18.0.1 machine: "3" public-address: 10.0.3.218 mysql: charm: cs:precise/mysql-38 exposed: false relations: cluster: - mysql shared-db: - keystone - nova-cloud-controller units: mysql/0: agent-state: started agent-version: 1.18.0.1 machine: "3" public-address: 10.0.3.218 nova-cloud-controller: charm: cs:precise/nova-cloud-controller-32 exposed: false relations: cluster: - nova-cloud-controller shared-db: - mysql units: nova-cloud-controller/0: agent-state: error agent-state-info: 'hook failed: "shared-db-relation-changed"' agent-version: 1.18.0.1 machine: "3" open-ports: - 3333/tcp - 8773/tcp - 8774/tcp public-address: 10.0.3.218 nova-compute: charm: cs:precise/nova-compute-25 exposed: false relations: compute-peer: - nova-compute units: nova-compute/0: agent-state: started agent-version: 1.18.0.1 machine: "2" public-address: 10.0.3.174 openstack-dashboard: charm: cs:precise/openstack-dashboard-15 exposed: false relations: cluster: - openstack-dashboard units: openstack-dashboard/0: agent-state: started agent-version: 1.18.0.1 machine: "3" open-ports: - 80/tcp - 443/tcp public-address: 10.0.3.218 rabbitmq-server: charm: cs:precise/rabbitmq-server-21 exposed: false relations: cluster: - rabbitmq-server units: rabbitmq-server/0: agent-state: started agent-version: 1.18.0.1 machine: "3" open-ports: - 5672/tcp public-address: 10.0.3.218cloud-installer-proper/test/juju-output/juju-status-multi-install.yaml0000644000175000017500000000631412323623210025320 0ustar adamadamenvironment: maas machines: "0": agent-state: started agent-version: 1.18.0 dns-name: mrfxw.maas instance-id: /MAAS/api/1.0/nodes/node-a5d60960-bfed-11e3-b7a8-a0cec8006f97/ series: precise "1": agent-state: started agent-version: 1.18.0 dns-name: cqkw9.maas instance-id: /MAAS/api/1.0/nodes/node-a59c35b4-bfed-11e3-b7a8-a0cec8006f97/ series: precise containers: 1/lxc/0: agent-state: started agent-version: 1.18.0 dns-name: 10.0.100.8 instance-id: juju-machine-1-lxc-0 series: precise hardware: arch=amd64 1/lxc/1: agent-state: started agent-version: 1.18.0 dns-name: 10.0.100.9 instance-id: juju-machine-1-lxc-1 series: precise hardware: arch=amd64 1/lxc/2: agent-state: started agent-version: 1.18.0 dns-name: 10.0.100.10 instance-id: juju-machine-1-lxc-2 series: precise hardware: arch=amd64 1/lxc/3: agent-state: started agent-version: 1.18.0 dns-name: 10.0.100.11 instance-id: juju-machine-1-lxc-3 series: precise hardware: arch=amd64 1/lxc/4: agent-state: started agent-version: 1.18.0 dns-name: 10.0.100.12 instance-id: juju-machine-1-lxc-4 series: precise hardware: arch=amd64 "2": agent-state: started agent-version: 1.18.0 dns-name: tmbp6.maas instance-id: /MAAS/api/1.0/nodes/node-a60a1a52-bfed-11e3-b7a8-a0cec8006f97/ series: precise services: glance: charm: cs:precise/glance-30 exposed: false relations: cluster: - glance units: glance/0: agent-state: pending agent-version: 1.18.0 machine: 1/lxc/3 public-address: 10.0.100.11 keystone: charm: cs:precise/keystone-31 exposed: false relations: cluster: - keystone units: keystone/0: agent-state: installed agent-version: 1.18.0 machine: 1/lxc/4 public-address: 10.0.100.12 mysql: charm: cs:precise/mysql-38 exposed: false relations: cluster: - mysql units: mysql/0: agent-state: started agent-version: 1.18.0 machine: 1/lxc/2 public-address: 10.0.100.10 nova-cloud-controller: charm: cs:precise/nova-cloud-controller-32 exposed: false relations: cluster: - nova-cloud-controller units: nova-cloud-controller/0: agent-state: pending agent-version: 1.18.0 machine: 1/lxc/0 public-address: 10.0.100.8 nova-compute: charm: cs:precise/nova-compute-25 exposed: false relations: compute-peer: - nova-compute units: nova-compute/0: agent-state: started agent-version: 1.18.0 machine: "2" public-address: tmbp6.maas rabbitmq-server: charm: cs:precise/rabbitmq-server-21 exposed: false relations: cluster: - rabbitmq-server units: rabbitmq-server/0: agent-state: started agent-version: 1.18.0 machine: 1/lxc/1 open-ports: - 5672/tcp public-address: 10.0.100.9cloud-installer-proper/test/test_pegasus.py0000644000175000017500000000315112323623210020103 0ustar adamadamimport sys import unittest sys.path.insert(0 ,'../cloudinstall') from cloudinstall.pegasus import parse_state, NOVA_CLOUD_CONTROLLER from cloudinstall.juju import JujuState from cloudinstall.maas import MaasState @unittest.skip def test_poll_state(): with open('test/juju-output/service-pending.out') as js: with open('test/maas-output/twonodes.out') as ms: s = parse_state(JujuState(js), MaasState(ms)) assert s[0]['tag'] == "node-4c49e73e-e8b8-11e2-ac16-5254002cb1d6" assert 'charm' not in s[0] assert s[1]['tag'] == "node-5fb74ba0-e8c1-11e2-b109-5254002cb1d6" assert s[1]['charms'] == ['mysql'] assert s[1]['units'] == ['mysql/1'] assert s[1]['cpu_count'] == "4" assert s[1]['memory'] == "8096" assert s[1]['storage'] == "100.0" @unittest.skip def test_lxc(): with open('test/juju-output/lxc.out') as js: with open('test/maas-output/maas-for-lxc.out') as ms: juju = JujuState(js) s = parse_state(juju, MaasState(ms)) assert s[2]['machine_no'] == '1/lxc/0' assert s[2]['charms'] == ['mediawiki'] assert s[2]['units'] == ['mediawiki/0'] @unittest.skip def test_lxc_controller_deployed(): with open('test/juju-output/lxc-controller-deployed.out') as js: with open('test/maas-output/maas-for-lxc.out') as ms: juju = JujuState(js) s = parse_state(juju, MaasState(ms)) assert NOVA_CLOUD_CONTROLLER in juju.services assert any([NOVA_CLOUD_CONTROLLER in n.get('charms', []) for n in s]) cloud-installer-proper/test/test_gui.py0000644000175000017500000001641012323623210017222 0ustar adamadamimport sys import unittest sys.path.insert(0, '../cloudinstall') from os.path import expanduser import urwid from cloudinstall import gui from cloudinstall import pegasus from cloudinstall.juju import JujuState from cloudinstall.maas import MaasState import helpers import mock DEPLOY_CMD = 'juju deploy --config {p} {{to}} {{charm}}'.format(p='/tmp/openstack.yaml') ADD_RELATION = 'juju add-relation {charm1} {charm2}' REMOVE_UNIT = 'juju remove-unit {unit}' TERMINATE_MACHINE = 'juju terminate-machine {id}' # XXX: HACK: For now, all the tests assume that we are in multi-system mode. pegasus.SINGLE_SYSTEM = False def fake_metadata(identifier=0): metadata = { "fqdn": "line %d" % identifier, "cpu_count": 1, "memory": 2048, "storage": 2048, "id": "not a unique snowflake", "machine_no": 100, "agent_state": "pending", } return metadata class NonRunningCommandRunner(gui.CommandRunner): def _next(self): pass @unittest.skip def test_ControllerOverlay_process_none(): o = gui.ControllerOverlay(urwid.Text(""), NonRunningCommandRunner()) assert o.process([]) assert len(o.command_runner.to_run) == 1 assert o.command_runner.to_run[0] == 'juju add-machine' assert o.text.get_text()[0] == o.PXE_BOOT @unittest.skip def test_ControllerOverlay_process_ready(): o = gui.ControllerOverlay(urwid.Text(""), NonRunningCommandRunner()) assert o.process([{'machine_no': '1'}]) # our hardcoded command above assumes a config, so don't check charms that # omit config. for charm in filter(lambda c: c not in pegasus._OMIT_CONFIG, pegasus.CONTROLLER_CHARMS): cmd = DEPLOY_CMD.format(charm=charm, to="--to lxc:1") assert cmd in o.command_runner.to_run, str(cmd) + str(o.command_runner.to_run) assert o.text.get_text()[0] == o.NODE_SETUP @unittest.skip def test_ControllerOverlay_process_deployed(): o = gui.ControllerOverlay(urwid.Text(""), NonRunningCommandRunner()) assert not o.process([{"charms": pegasus.CONTROLLER_CHARMS}]) assert len(o.command_runner.to_run) == 0 assert o.done @unittest.skip def test_ControllerOverlay__controller_charms_to_allocate(): with open('juju-output/lxc-controller-deployed.out') as juju_out: with open('maas-output/maas-for-lxc.out') as maas_out: maas = MaasState(maas_out) juju = JujuState(juju_out.read()) data = pegasus.parse_state(juju, maas) over = gui.ControllerOverlay(None, None) charms = over._controller_charms_to_allocate(data) assert len(charms) == 0 @unittest.skip def test_Node(): md = { "fqdn": "node", "cpu_count": 1, "memory": 2048, "storage": 2048, "id": "not a unique snowflake", "machine_no": 100, "agent_state": "pending", "charms": [pegasus.OPENSTACK_DASHBOARD], } n = gui.Node(md, lambda: None) assert n.is_horizon assert n.name == "node" @unittest.skip def test_CommandRunner_naked_deploy(): cr = NonRunningCommandRunner() cr.deploy('nova-compute') cr.deploy('glance') assert cr.to_run[0] == DEPLOY_CMD.format(charm='nova-compute', to="") assert cr.to_run[1] == DEPLOY_CMD.format(charm='glance', to="") assert cr.to_run[2] == ADD_RELATION.format(charm1='nova-compute', charm2='glance') assert len(cr.to_run) == 3 md = { 'charms': ['nova-compute', 'glance'], 'units': ['nova-compute/0', 'glance/0'], 'machine_no': 0, } cr.change_allocation(['nova-compute'], md) assert cr.to_run[3] == REMOVE_UNIT.format(unit='glance/0') cr.change_allocation([], md) assert cr.to_run[4] == REMOVE_UNIT.format(unit='nova-compute/0') assert cr.to_run[5] == REMOVE_UNIT.format(unit='glance/0') assert cr.to_run[6] == TERMINATE_MACHINE.format(id=0), cr.to_run[5] @unittest.skip def test_CommandRunner_deploy_to(): cr = NonRunningCommandRunner() cr.deploy(pegasus.NOVA_COMPUTE, id=0) cmd = 'juju deploy --config {p} --to 0 nova-compute'.format( p=expanduser('/tmp/openstack.yaml')) assert cr.to_run[0] == cmd, cr.to_run[0] @unittest.skip def test_CommandRunner_deploy_tag(): cr = NonRunningCommandRunner() cr.deploy(pegasus.NOVA_COMPUTE, tag='foo') cmd = 'juju deploy --config {p} --constraints tags=foo nova-compute'.format( p='/tmp/openstack.yaml') assert cr.to_run[0] == cmd, cr.to_run[0] @unittest.skip def test_ControllerOverlay__process_lxc(): with open('juju-output/lxc-controller-deployed.out') as js: with open('maas-output/maas-for-lxc.out') as ms: juju = JujuState(js) s = pegasus.parse_state(juju, MaasState(ms)) overlay = gui.ControllerOverlay(None, NonRunningCommandRunner()) assert not overlay.process(s) @unittest.skip def test_NodeViewMode_tick(): def get_data(foo=[]): metadata = fake_metadata(len(foo)) if len(foo) == 1: metadata['charms'] = pegasus.CONTROLLER_CHARMS foo.append(metadata) return foo # a fake MainLoop class FakeLoop(object): widget = None def process_input(self, _unused): pass nvm = gui.NodeViewMode(FakeLoop(), get_data, NonRunningCommandRunner()) # fake a maas login, since we're calling nvm directly nvm.logged_in = True nvm.do_update([]) assert nvm.target == nvm.controller_overlay assert nvm.target.text.get_text()[0] == nvm.target.PXE_BOOT nvm.do_update(get_data()) assert nvm.target == nvm.controller_overlay assert nvm.target.text.get_text()[0] == nvm.target.NODE_SETUP nvm.do_update(get_data()) assert nvm.target == nvm assert nvm.url.get_text()[0] == 'http://line 1/horizon' @unittest.skip def test_ChangeStateDialog(): def make_state_asserter(states): states = {pegasus.ALLOCATION[s] for s in states} def on_ok(new_states): assert set(states) == set(new_states) return on_ok def make_dialog(states, additional=None): if additional is None: additional = set() metadata = fake_metadata() metadata['charms'] = states dialog = gui.ChangeStateDialog( None, metadata, make_state_asserter(set(states).union(additional)), id) return dialog # no change dialog = make_dialog([pegasus.NOVA_COMPUTE]) dialog.keypress((100,100), 'tab') dialog.keypress((100,100), 'enter') # select first dialog = make_dialog([], list(pegasus.ALLOCATION.keys())[:1]) dialog.keypress((100,100), 'enter') dialog.keypress((100,100), 'tab') dialog.keypress((100,100), 'enter') @unittest.skip def test_allocation(): with helpers.set_single_system(True): result = helpers.parse_output('initial-install-config') nrc = NonRunningCommandRunner() overlay = gui.ControllerOverlay(None, nrc) class FakeStartKVM: def run(self): pass with mock.patch('cloudinstall.pegasus.StartKVM', FakeStartKVM): overlay.process(result) # we expect all the charms and relations to be added here assert len(nrc.to_run) == 14 @unittest.skip def test_parse_pending_lxcs(): with helpers.set_single_system(True): result = helpers.parse_output('pending') assert len(result) == 7 cloud-installer-proper/test/test_maasclient.py0000644000175000017500000000440712323623210020561 0ustar adamadam#!/usr/bin/env python3 # # test_maasclient.py - Unittests for MaaS REST api # # Copyright 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 as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY 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 . import unittest import os import sys sys.path.insert(0, '../cloudinstall') from cloudinstall.maas.auth import MaasAuth from cloudinstall.maas.client import MaasClient from cloudinstall.utils import randomString ROOT_USER = os.environ['CI_USER'] if 'CI_USER' in os.environ else 'admin' AUTH = MaasAuth() MAAS_INSTALLED = os.path.exists('/etc/maas') @unittest.skipIf(not MAAS_INSTALLED, "Maas is not installed") class MaasAuthTest(unittest.TestCase): def test_get_api_key(self): AUTH.get_api_key(ROOT_USER) self.assertEquals(3, len(AUTH.api_key.split(':'))) @unittest.skipIf(not MAAS_INSTALLED, "Maas is not installed") class MaasClientTest(unittest.TestCase): def setUp(self): self.c = MaasClient(AUTH) self.tag = 'a-test-tag-' + randomString() def test_get_tags(self): res = self.c.tags self.assertGreater(len(res), 0) def test_tag_new(self): res = self.c.tag_new(self.tag) self.assertTrue(res) def test_tag_delete(self): res = self.c.tag_delete(self.tag) self.assertTrue(res) @unittest.skipIf(not MAAS_INSTALLED, "Maas is not installed") class MaasClientZoneTest(unittest.TestCase): def setUp(self): self.c = MaasClient(AUTH) def test_new_zone(self): res = self.c.zone_new('testzone-' + randomString(), 'zone created in unittest') def test_get_zones(self): res = self.c.zones self.assertEquals(len(res), 0) if __name__ == '__main__': unittest.main() cloud-installer-proper/test/test_jujustate.py0000644000175000017500000000633212323623210020456 0ustar adamadam#!/usr/bin/env python3 # # test_jujustate.py - Unittests for JujuState # # Copyright 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 as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY 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 . # import helpers # load_status = lambda f: helpers.load_status(f, JujuState) # @load_status('juju-output/no-services.out') # def test_noservices(s): # assert len(s.assignments) == 0 # assert len(s.services) == 0 # @load_status('juju-output/one-pending.out') # def test_onepending(s): # assert len(s.assignments) == 0 # assert len(s.services) == 0 # @load_status('juju-output/service-pending.out') # def test_servicepending(s): # assert len(s.assignments) == 1 # assert len(s.services) == 1 import unittest import sys import os import ipaddress sys.path.insert(0, '../cloudinstall') from cloudinstall.utils import _run from cloudinstall.juju import JujuState JUJU_USELIVE = os.environ.get('JUJU_USELIVE', 0) JUJU_INSTALLED = os.path.exists("/usr/bin/juju") class JujuStateMultiTest(unittest.TestCase): def setUp(self): self.juju = None if JUJU_USELIVE and JUJU_INSTALLED: self.juju = JujuState(_run('juju status').decode('ascii')) else: with open('test/juju-output/juju-status-multi-install.yaml') as f: self.juju = JujuState(f.read()) def test_verify_instance_id(self): """ Validate we have maas instance-ids Example instance-id: /MAAS/api/1.0/nodes/node-a59c35b4-bfed-11e3-b7a8-a0cec8006f97/ """ for m in self.juju.machines(): self.assertTrue('MAAS/api' in m.instance_id) def test_machine_dns_names(self): """ Are machine dns-names assigned """ for m in self.juju.machines(): self.assertTrue(m.dns_name) def test_machine_dns_names_host(self): """ Machines should have `maas` as the host in their dns-name """ for m in self.juju.machines(): self.assertTrue('maas' in m.dns_name) def test_container_dns_is_ip(self): """ Make sure dns-names are valid ip address """ for m in self.juju.machines(): for c in m.containers: self.assertTrue(ipaddress.ip_address(c.dns_name)) def test_container_lxc_instance_id(self): """ Container instance ids should have lxc defined """ for m in self.juju.machines(): for c in m.containers: self.assertTrue('lxc' in c.instance_id) class JujuStateSingleTest(unittest.TestCase): def setUp(self): with open('juju-output/juju-status-single-install.yaml') as f: self.status_yaml = f.read().decode('ascii') self.juju = JujuState(self.status_yaml) cloud-installer-proper/test/maas-output/0000755000175000017500000000000012323623210017302 5ustar adamadamcloud-installer-proper/test/maas-output/maas-for-lxc.out0000644000175000017500000000233612323623210022330 0ustar adamadam[ { "status": 6, "macaddress_set": [ { "resource_uri": "/MAAS/api/1.0/nodes/node-4c49e73e-e8b8-11e2-ac16-5254002cb1d6/macs/52:54:00:42:a3:06/", "mac_address": "52:54:00:42:a3:06" } ], "netboot": true, "hostname": "slave1.local", "power_type": "", "system_id": "node-4c49e73e-e8b8-11e2-ac16-5254002cb1d6", "architecture": "amd64/generic", "cpu_count": "4", "memory": "8096", "storage": "102400", "tag_names": [], "resource_uri": "tycho-local-machine-1" }, { "status": 6, "macaddress_set": [ { "resource_uri": "/MAAS/api/1.0/nodes/node-5fb74ba0-e8c1-11e2-b109-5254002cb1d6/macs/52:54:00:5f:1a:9e/", "mac_address": "52:54:00:5f:1a:9e" } ], "netboot": false, "hostname": "slave2.local", "power_type": "", "system_id": "node-5fb74ba0-e8c1-11e2-b109-5254002cb1d6", "architecture": "amd64/generic", "cpu_count": "4", "memory": "8096", "storage": "102400", "tag_names": [], "resource_uri": "tycho-local-machine-2" } ] cloud-installer-proper/test/maas-output/pending.out0000644000175000017500000000145512323623210021464 0ustar adamadam[ { "status": 6, "macaddress_set": [ { "resource_uri": "/MAAS/api/1.0/nodes/node-ed4672a0-539e-11e3-888b-525400f40f5c/macs/00:00:5e:cc:cc:cc/", "mac_address": "00:00:5e:cc:cc:cc" } ], "hostname": "juju-host.master", "power_type": "", "routers": null, "netboot": true, "cpu_count": 0, "storage": 0, "system_id": "node-ed4672a0-539e-11e3-888b-525400f40f5c", "architecture": "amd64/generic", "memory": 0, "owner": "root", "tag_names": [ "node-ed4672a0-539e-11e3-888b-525400f40f5c" ], "ip_addresses": [], "resource_uri": "/MAAS/api/1.0/nodes/node-ed4672a0-539e-11e3-888b-525400f40f5c/" } ] cloud-installer-proper/test/maas-output/initial-install-config.out0000644000175000017500000000145512323623210024400 0ustar adamadam[ { "status": 6, "macaddress_set": [ { "resource_uri": "/MAAS/api/1.0/nodes/node-29e47a24-52c6-11e3-ae66-525400ab8ba1/macs/00:00:5e:cc:cc:cc/", "mac_address": "00:00:5e:cc:cc:cc" } ], "hostname": "juju-host.master", "power_type": "", "routers": null, "netboot": true, "cpu_count": 0, "storage": 0, "system_id": "node-29e47a24-52c6-11e3-ae66-525400ab8ba1", "architecture": "amd64/generic", "memory": 0, "owner": "root", "tag_names": [ "node-29e47a24-52c6-11e3-ae66-525400ab8ba1" ], "ip_addresses": [], "resource_uri": "/MAAS/api/1.0/nodes/node-29e47a24-52c6-11e3-ae66-525400ab8ba1/" } ] cloud-installer-proper/test/maas-output/twonodes.out0000644000175000017500000000410412323623210021674 0ustar adamadam[ { "status": 6, "macaddress_set": [ { "resource_uri": "/MAAS/api/1.0/nodes/node-8e9debfc-c059-11e3-9fc4-52540096c280/macs/00%3A16%3A3e%3A93%3A0d%3A9a/", "mac_address": "00:16:3e:93:0d:9a" } ], "hostname": "juju-bootstrap.maas", "zone": { "resource_uri": "/MAAS/api/1.0/zones/default/", "name": "default", "description": "" }, "routers": null, "netboot": true, "cpu_count": 0, "storage": 0, "owner": "root", "system_id": "node-8e9debfc-c059-11e3-9fc4-52540096c280", "architecture": "amd64/generic", "memory": 0, "power_type": "virsh", "tag_names": [ "use-fastpath-installer", "node-8e9debfc-c059-11e3-9fc4-52540096c280" ], "ip_addresses": [ "192.168.100.151" ], "resource_uri": "/MAAS/api/1.0/nodes/node-8e9debfc-c059-11e3-9fc4-52540096c280/" }, { "status": 1, "macaddress_set": [ { "resource_uri": "/MAAS/api/1.0/nodes/node-f63668f6-c05a-11e3-9fc4-52540096c280/macs/52%3A54%3A00%3Ab3%3Aea%3Ae1/", "mac_address": "52:54:00:b3:ea:e1" } ], "hostname": "4jq9g.maas", "zone": { "resource_uri": "/MAAS/api/1.0/zones/default/", "name": "default", "description": "" }, "routers": null, "netboot": true, "cpu_count": 1, "storage": 8096, "owner": null, "system_id": "node-f63668f6-c05a-11e3-9fc4-52540096c280", "architecture": "amd64/generic", "memory": 512, "power_type": "virsh", "tag_names": [ "use-fastpath-installer", "node-f63668f6-c05a-11e3-9fc4-52540096c280" ], "ip_addresses": [ "192.168.100.152" ], "resource_uri": "/MAAS/api/1.0/nodes/node-f63668f6-c05a-11e3-9fc4-52540096c280/" } ] cloud-installer-proper/TODO0000644000175000017500000000046712323623210014543 0ustar adamadam* When node is being formatted and in 'pending' juju state, we should still show the "your node is being allocated" message * urwid 1.1 upgrades * Replace --config openstack.yaml hard coded path with an "autogenerated" one that is deleted after every juju deploy, e.g. https://pastebin.canonical.com/98588/ cloud-installer-proper/man/0000755000175000017500000000000012323631216014624 5ustar adamadamcloud-installer-proper/man/en/0000755000175000017500000000000012323631272015230 5ustar adamadamcloud-installer-proper/man/en/cloud-install.10000644000175000017500000000413412323631272020066 0ustar adamadam.\" Man page generated from reStructuredText. . .TH "CLOUD-INSTALL" "1" "April 16, 2014" "0.13+git20140410" "Ubuntu Cloud Installer" .SH NAME cloud-install \- Ubuntu Cloud Installer Documentation . .nr rst2man-indent-level 0 . .de1 rstReportMargin \\$1 \\n[an-margin] level \\n[rst2man-indent-level] level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] - \\n[rst2man-indent0] \\n[rst2man-indent1] \\n[rst2man-indent2] .. .de1 INDENT .\" .rstReportMargin pre: . RS \\$1 . nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin] . nr rst2man-indent-level +1 .\" .rstReportMargin post: .. .de UNINDENT . RE .\" indent \\n[an-margin] .\" old: \\n[rst2man-indent\\n[rst2man-indent-level]] .nr rst2man-indent-level -1 .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. . .nr rst2man-indent-level 0 . .de1 rstReportMargin \\$1 \\n[an-margin] level \\n[rst2man-indent-level] level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] - \\n[rst2man-indent0] \\n[rst2man-indent1] \\n[rst2man-indent2] .. .de1 INDENT .\" .rstReportMargin pre: . RS \\$1 . nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin] . nr rst2man-indent-level +1 .\" .rstReportMargin post: .. .de UNINDENT . RE .\" indent \\n[an-margin] .\" old: \\n[rst2man-indent\\n[rst2man-indent-level]] .nr rst2man-indent-level -1 .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. .INDENT 0.0 .INDENT 3.5 .sp .nf .ft C cloud\-install [\-ihf] Create an Ubuntu Cloud! (requires root privileges) Options: \-i install only (don\(aqt invoke cloud\-status) \-h print this message \-f bypass any sanity checks .ft P .fi .UNINDENT .UNINDENT .sp Ubuntu Cloud installer is a metal to cloud image that provides an extremely simple way to install, deploy and scale an openstack cloud on top of Ubuntu server. Initial configurations are available for single physical system deployments as well as multiple physical system deployments. .SH AUTHOR Canonical Solutions Engineering .SH COPYRIGHT 2014, Canonical Ltd .\" Generated by docutils manpage writer. . cloud-installer-proper/man/en/cloud-status.10000644000175000017500000000360312323631272017743 0ustar adamadam.\" Man page generated from reStructuredText. . .TH "CLOUD-STATUS" "1" "April 16, 2014" "0.13+git20140410" "Ubuntu Cloud Installer" .SH NAME cloud-status \- Ubuntu Cloud Status Documentation . .nr rst2man-indent-level 0 . .de1 rstReportMargin \\$1 \\n[an-margin] level \\n[rst2man-indent-level] level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] - \\n[rst2man-indent0] \\n[rst2man-indent1] \\n[rst2man-indent2] .. .de1 INDENT .\" .rstReportMargin pre: . RS \\$1 . nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin] . nr rst2man-indent-level +1 .\" .rstReportMargin post: .. .de UNINDENT . RE .\" indent \\n[an-margin] .\" old: \\n[rst2man-indent\\n[rst2man-indent-level]] .nr rst2man-indent-level -1 .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. . .nr rst2man-indent-level 0 . .de1 rstReportMargin \\$1 \\n[an-margin] level \\n[rst2man-indent-level] level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] - \\n[rst2man-indent0] \\n[rst2man-indent1] \\n[rst2man-indent2] .. .de1 INDENT .\" .rstReportMargin pre: . RS \\$1 . nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin] . nr rst2man-indent-level +1 .\" .rstReportMargin post: .. .de UNINDENT . RE .\" indent \\n[an-margin] .\" old: \\n[rst2man-indent\\n[rst2man-indent-level]] .nr rst2man-indent-level -1 .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. Ubuntu Cloud installer is a metal to cloud image that provides an extremely simple way to install, deploy and scale an openstack cloud on top of Ubuntu server. Initial configurations are available for single physical system deployments as well as multiple physical system deployments. .sp This is the user interface for managing the deployed cloud. .SH AUTHOR Canonical Solutions Engineering .SH COPYRIGHT 2014, Canonical Ltd .\" Generated by docutils manpage writer. . cloud-installer-proper/man/en/ubuntucloudinstaller.10000644000175000017500000007425412323631272021615 0ustar adamadam.\" Man page generated from reStructuredText. . .TH "TODO" "1" "April 16, 2014" "0.13+git20140410" "Ubuntu Cloud Installer" .SH NAME Todo \- Ubuntu Cloud Installer Documentation . .nr rst2man-indent-level 0 . .de1 rstReportMargin \\$1 \\n[an-margin] level \\n[rst2man-indent-level] level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] - \\n[rst2man-indent0] \\n[rst2man-indent1] \\n[rst2man-indent2] .. .de1 INDENT .\" .rstReportMargin pre: . RS \\$1 . nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin] . nr rst2man-indent-level +1 .\" .rstReportMargin post: .. .de UNINDENT . RE .\" indent \\n[an-margin] .\" old: \\n[rst2man-indent\\n[rst2man-indent-level]] .nr rst2man-indent-level -1 .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. . .nr rst2man-indent-level 0 . .de1 rstReportMargin \\$1 \\n[an-margin] level \\n[rst2man-indent-level] level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] - \\n[rst2man-indent0] \\n[rst2man-indent1] \\n[rst2man-indent2] .. .de1 INDENT .\" .rstReportMargin pre: . RS \\$1 . nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin] . nr rst2man-indent-level +1 .\" .rstReportMargin post: .. .de UNINDENT . RE .\" indent \\n[an-margin] .\" old: \\n[rst2man-indent\\n[rst2man-indent-level]] .nr rst2man-indent-level -1 .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. .sp \fI\%Github project page\fP .INDENT 0.0 .INDENT 3.5 .sp This is work in progress and some information will be missing. Please consult the source code or file a bug at the Github project page. .sp Contributions are welcomed. .UNINDENT .UNINDENT .SH GUIDES .SS Developer Guide .SS Developer Guide \- Setup .sp The document walks you through installing the necessary packages and environment preparations in order to build the cloud installer. .SS Base system .sp Development and testing is done on Ubuntu and using a release of \fBTrusty\fP or later. .SS Needed packages .INDENT 0.0 .IP \(bu 2 debhelper .IP \(bu 2 dh\-python .IP \(bu 2 python3\-all .IP \(bu 2 python3\-mock .IP \(bu 2 python3\-nose .IP \(bu 2 python3\-oauthlib .IP \(bu 2 python3\-passlib .IP \(bu 2 python3\-requests .IP \(bu 2 python3\-requests\-oauthlib .IP \(bu 2 python3\-setuptools .IP \(bu 2 python3\-urwid .IP \(bu 2 python3\-ws4py .IP \(bu 2 python3\-yaml .UNINDENT .SS Building cloud installer .sp \fBSbuild\fP is the preferred way for building the package set. Please refer to this \fI\%wiki page\fP on setting up sbuild. .sp Just like the base system the sbuild chroots need to be \fITrusty\fP or later. .sp \fBNOTE:\fP .INDENT 0.0 .INDENT 3.5 The architecture of the chroots do not matter. .UNINDENT .UNINDENT .sp Once \fBsbuild\fP is configured, checkout the source code of the installer .INDENT 0.0 .INDENT 3.5 .sp .nf .ft C $ git clone https://github.com/Ubuntu\-Solutions\-Engineering/cloud\-installer.git ~/cloud\-installer $ cd cloud\-installer .ft P .fi .UNINDENT .UNINDENT .sp From here you can build the entire package set by running: .INDENT 0.0 .INDENT 3.5 .sp .nf .ft C $ make sbuild .ft P .fi .UNINDENT .UNINDENT .sp Once finished your packages will be stored in the top level directory where your cloud\-installer project is kept. .INDENT 0.0 .INDENT 3.5 .sp .nf .ft C $ ls ../*.deb .ft P .fi .UNINDENT .UNINDENT .SS Building documentation .sp Documentation will be built in \fBdocs/_build/html\fP, and requires \fBSphinx\fP to build. .INDENT 0.0 .INDENT 3.5 .sp .nf .ft C $ cd docs && make html .ft P .fi .UNINDENT .UNINDENT .SS Running Tests .sp Tests can be ran against a set of exported data(\fBdefault\fP) or a live machine. In order to test against live data the following environment variable is used. .INDENT 0.0 .INDENT 3.5 .sp .nf .ft C $ JUJU_LIVE=1 nosetests3 test .ft P .fi .UNINDENT .UNINDENT .SS Single Installer Guide .SS Pre\-requisites .sp Add the \fIcloud\-installer\fP ppa to your system. .INDENT 0.0 .INDENT 3.5 .sp .nf .ft C $ sudo apt\-add\-repository ppa:cloud\-installer/ppa .ft P .fi .UNINDENT .UNINDENT .sp \fBNOTE:\fP .INDENT 0.0 .INDENT 3.5 Adding the ppa is only necessary until an official release to the archives has been announced. .UNINDENT .UNINDENT .SS Installation .sp Install the cloud\-installer via \fIapt\-get\fP .INDENT 0.0 .INDENT 3.5 .sp .nf .ft C $ sudo apt\-get install cloud\-installer .ft P .fi .UNINDENT .UNINDENT .SS Start the installation .sp To start the installation run the following command .INDENT 0.0 .INDENT 3.5 .sp .nf .ft C $ sudo cloud\-install .ft P .fi .UNINDENT .UNINDENT .sp An initial dialog box will appear asking you to select which type of install, choose \fBSingle system\fP\&. .SS Next Steps .sp The installer will run through a series of steps starting with making sure the necessary bits are available for a single system installation and ending with a \fIjuju\fP bootstrapped system. .sp When the bootstrapping has finished it will immediately load the status screen. From there you can see the nodes listed along with the deployed charms necessary to start your private openstack cloud. .sp Adding additional compute nodes, block storage, object storage, and controllers can be done by pressing \fIF6\fP and making the selection on the dialog box. .sp Finally, once those nodes are displayed and the charms deployed the horizon dashboard will be available to you for managing your openstack cloud. .SS Multi Installer Guide .INDENT 0.0 .INDENT 3.5 .SS Todo .INDENT 0.0 .IP \(bu 2 Discuss a MaaS setup .IP \(bu 2 Outline hardware resources needed for a multi install .UNINDENT .UNINDENT .UNINDENT .SS Pre\-requisites .sp Add the \fIcloud\-installer\fP ppa to your system. .INDENT 0.0 .INDENT 3.5 .sp .nf .ft C $ sudo apt\-add\-repository ppa:cloud\-installer/ppa .ft P .fi .UNINDENT .UNINDENT .sp \fBNOTE:\fP .INDENT 0.0 .INDENT 3.5 Adding the ppa is only necessary until an official release to the archives has been announced. .UNINDENT .UNINDENT .SS Installation .sp Install the cloud\-installer via \fIapt\-get\fP .INDENT 0.0 .INDENT 3.5 .sp .nf .ft C $ sudo apt\-get install cloud\-installer .ft P .fi .UNINDENT .UNINDENT .SS Start the installation .sp To start the installation run the following command .INDENT 0.0 .INDENT 3.5 .sp .nf .ft C $ sudo cloud\-install .ft P .fi .UNINDENT .UNINDENT .sp An initial dialog box will appear asking you to select which type of install, choose \fBMulti\-system\fP\&. .SS Next Steps .sp The installer will run through a series of steps starting with making sure the necessary bits are available for a single system installation and ending with a \fIjuju\fP bootstrapped system. .INDENT 0.0 .INDENT 3.5 .SS Todo .sp Finish this guide. .UNINDENT .UNINDENT .SH REFERENCE .SS \fBcloudinstall.juju\fP \-\-\- Juju interface .sp Represents a juju status .INDENT 0.0 .TP .B class cloudinstall.juju.JujuState(raw_yaml) Bases: \fBbuiltins.object\fP .sp Represents a global Juju state .INDENT 7.0 .TP .B JujuState.machine(instance_id) Return single machine state .INDENT 7.0 .TP .B Parameters \fBinstance_id\fP (\fIstr\fP) \-\- machine instance_id .TP .B Returns machine .TP .B Return type cloudinstall.machine.Machine() .UNINDENT .UNINDENT .INDENT 7.0 .TP .B JujuState.machines() Machines property .INDENT 7.0 .TP .B Returns machines known to juju .TP .B Return type generator .UNINDENT .UNINDENT .INDENT 7.0 .TP .B JujuState.machines_allocated() Machines allocated property .INDENT 7.0 .TP .B Returns Machines in an allocated state .TP .B Return type iter .UNINDENT .UNINDENT .INDENT 7.0 .TP .B JujuState.machines_unallocated() Machines unallocated property .INDENT 7.0 .TP .B Returns Machines in an unallocated state .TP .B Return type iter .UNINDENT .UNINDENT .INDENT 7.0 .TP .B JujuState.service(name) Return a single service entry .INDENT 7.0 .TP .B Parameters \fBname\fP (\fIstr\fP) \-\- service/charm name .TP .B Returns a service entry .TP .B Return type Service() .UNINDENT .UNINDENT .INDENT 7.0 .TP .B JujuState.services Juju services property .INDENT 7.0 .TP .B Returns Service() of all loaded services .TP .B Return type generator .UNINDENT .UNINDENT .UNINDENT .SS \fBcloudinstall.maas\fP \-\-\- Maas interface .INDENT 0.0 .TP .B class cloudinstall.maas.MaasMachine(machine_id, machine) Bases: \fBcloudinstall.machine.Machine\fP .sp Single maas machine .INDENT 7.0 .TP .B arch Return architecture .INDENT 7.0 .TP .B Returns architecture type .TP .B Return type str .UNINDENT .UNINDENT .INDENT 7.0 .TP .B cpu_cores Returns number of cpu\-cores .INDENT 7.0 .TP .B Returns number of cpus .TP .B Return type str .UNINDENT .UNINDENT .INDENT 7.0 .TP .B hostname Query hostname reported by MaaS .INDENT 7.0 .TP .B Returns hostname .TP .B Return type str .UNINDENT .UNINDENT .INDENT 7.0 .TP .B instance_id Returns instance\-id of a machine .INDENT 7.0 .TP .B Returns instance\-id of machine .TP .B Return type str .UNINDENT .UNINDENT .INDENT 7.0 .TP .B ip_addresses Ip addresses for machine .INDENT 7.0 .TP .B Returns ip addresses .TP .B Return type list .UNINDENT .UNINDENT .INDENT 7.0 .TP .B mac_address Macaddress set of maas machine .INDENT 7.0 .TP .B Returns mac_address and resource_uri .TP .B Return type dict .UNINDENT .UNINDENT .INDENT 7.0 .TP .B mem Return memory .INDENT 7.0 .TP .B Returns memory size .TP .B Return type str .UNINDENT .UNINDENT .INDENT 7.0 .TP .B owner Machine owner .INDENT 7.0 .TP .B Returns owner .TP .B Return type str .UNINDENT .UNINDENT .INDENT 7.0 .TP .B power_type Machine power type .INDENT 7.0 .TP .B Returns machines power type .TP .B Return type str .UNINDENT .UNINDENT .INDENT 7.0 .TP .B status Status of machine state .sp Those statuses are defined as follows: DECLARED = 0 COMMISSIONING = 1 FAILED_TESTS = 2 MISSING = 3 READY = 4 RESERVED = 5 ALLOCATED = 6 RETIRED = 7 .INDENT 7.0 .TP .B Returns status .TP .B Return type int .UNINDENT .UNINDENT .INDENT 7.0 .TP .B storage Return storage .INDENT 7.0 .TP .B Returns storage size .TP .B Return type str .UNINDENT .UNINDENT .INDENT 7.0 .TP .B system_id Returns system id of a maas machine .INDENT 7.0 .TP .B Returns system id of machine .TP .B Return type str .UNINDENT .UNINDENT .INDENT 7.0 .TP .B tag Machine tag .INDENT 7.0 .TP .B Returns tag defined .TP .B Return type str .UNINDENT .UNINDENT .INDENT 7.0 .TP .B tag_names Tag names for machine .INDENT 7.0 .TP .B Returns tags associated with machine .TP .B Return type list .UNINDENT .UNINDENT .INDENT 7.0 .TP .B zone Zone information .INDENT 7.0 .TP .B Returns zone information .TP .B Return type dict .UNINDENT .UNINDENT .UNINDENT .INDENT 0.0 .TP .B class cloudinstall.maas.MaasState(maas) Bases: \fBbuiltins.object\fP .sp Represents global MaaS state .INDENT 7.0 .TP .B ALLOCATED = 6 .UNINDENT .INDENT 7.0 .TP .B COMMISSIONING = 1 .UNINDENT .INDENT 7.0 .TP .B DECLARED = 0 .UNINDENT .INDENT 7.0 .TP .B FAILED_TESTS = 2 .UNINDENT .INDENT 7.0 .TP .B MISSING = 3 .UNINDENT .INDENT 7.0 .TP .B READY = 4 .UNINDENT .INDENT 7.0 .TP .B RESERVED = 5 .UNINDENT .INDENT 7.0 .TP .B RETIRED = 7 .UNINDENT .INDENT 7.0 .TP .B machine(instance_id) Return single machine state .INDENT 7.0 .TP .B Parameters \fBinstance_id\fP (\fIstr\fP) \-\- machine instance_id .TP .B Returns machine .TP .B Return type cloudinstall.maas.MaasMachine .UNINDENT .UNINDENT .INDENT 7.0 .TP .B machines() Maas Machines .INDENT 7.0 .TP .B Returns machines known to maas .TP .B Return type generator .UNINDENT .UNINDENT .INDENT 7.0 .TP .B num_in_state(state) Number of machines in a particular state .INDENT 7.0 .TP .B Parameters \fBstate\fP (\fIstr\fP) \-\- a machine state .TP .B Returns number of machines in \fIstatus\fP .TP .B Return type int .UNINDENT .UNINDENT .UNINDENT .INDENT 0.0 .TP .B class cloudinstall.maas.auth.MaasAuth Bases: \fBbuiltins.object\fP .sp MAAS Authorization class .INDENT 7.0 .TP .B consumer_key Maas consumer key .INDENT 7.0 .TP .B Return type str .UNINDENT .UNINDENT .INDENT 7.0 .TP .B get_api_key(username=\(aqroot\(aq) MAAS api key .INDENT 7.0 .TP .B Parameters \fBusername\fP (\fIstr\fP) \-\- (optional) MAAS user to query for credentials .UNINDENT .UNINDENT .INDENT 7.0 .TP .B is_logged_in Checks if we are logged into the MAAS api .INDENT 7.0 .TP .B Return type bool .UNINDENT .UNINDENT .INDENT 7.0 .TP .B login() Login to MAAS api server .INDENT 7.0 .INDENT 3.5 .SS Todo .sp Deprecate once MAAS api matures (\fI\%http://pad.lv/1058137\fP) .UNINDENT .UNINDENT .UNINDENT .INDENT 7.0 .TP .B read_config(url, creds) Read cloud\-init config from given \fIurl\fP into \fIcreds\fP dict. .sp Updates any keys in \fIcreds\fP that are None with their corresponding values in the config. .sp Important keys include \fImetadata_url\fP, and the actual OAuth credentials. .INDENT 7.0 .TP .B Parameters .INDENT 7.0 .IP \(bu 2 \fBurl\fP (\fIstr\fP) \-\- cloud\-init config URL .IP \(bu 2 \fBcreds\fP (\fIdict\fP) \-\- MAAS user credentials .UNINDENT .UNINDENT .UNINDENT .INDENT 7.0 .TP .B token_key Maas oauth token key .INDENT 7.0 .TP .B Return type str .UNINDENT .UNINDENT .INDENT 7.0 .TP .B token_secret Maas oauth token secret .INDENT 7.0 .TP .B Return type str .UNINDENT .UNINDENT .UNINDENT .INDENT 0.0 .TP .B class cloudinstall.maas.client.MaasClient(auth) Bases: \fBbuiltins.object\fP .sp Client Class .INDENT 7.0 .TP .B delete(url, params=None) Performs a authenticated DELETE against a MAAS endpoint .INDENT 7.0 .TP .B Parameters .INDENT 7.0 .IP \(bu 2 \fBurl\fP \-\- MAAS endpoint .IP \(bu 2 \fBparams\fP \-\- extra data sent with the HTTP request .UNINDENT .UNINDENT .UNINDENT .INDENT 7.0 .TP .B get(url, params=None) Performs a authenticated GET against a MAAS endpoint .INDENT 7.0 .TP .B Parameters .INDENT 7.0 .IP \(bu 2 \fBurl\fP \-\- MAAS endpoint .IP \(bu 2 \fBparams\fP \-\- extra data sent with the HTTP request .UNINDENT .UNINDENT .UNINDENT .INDENT 7.0 .TP .B node_commission(system_id) (Re)commission a node .INDENT 7.0 .TP .B Parameters \fBsystem_id\fP \-\- machine identification .TP .B Returns True on success False on failure .UNINDENT .UNINDENT .INDENT 7.0 .TP .B node_remove(system_id) Delete a node .INDENT 7.0 .TP .B Parameters \fBsystem_id\fP \-\- machine identification .TP .B Returns True and success False on failure .UNINDENT .UNINDENT .INDENT 7.0 .TP .B node_start(system_id) Power up a node .INDENT 7.0 .TP .B Parameters \fBsystem_id\fP \-\- machine identification .TP .B Returns True on success False on failure .UNINDENT .UNINDENT .INDENT 7.0 .TP .B node_stop(system_id) Shutdown a node .INDENT 7.0 .TP .B Parameters \fBsystem_id\fP \-\- machine identification .TP .B Returns True on success False on failure .UNINDENT .UNINDENT .INDENT 7.0 .TP .B nodes Nodes managed by MAAS .INDENT 7.0 .TP .B Returns managed nodes .TP .B Return type list .UNINDENT .UNINDENT .INDENT 7.0 .TP .B nodes_accept_all() Accept all commissioned nodes .INDENT 7.0 .TP .B Returns Status .TP .B Return type bool .UNINDENT .UNINDENT .INDENT 7.0 .TP .B post(url, params=None) Performs a authenticated POST against a MAAS endpoint .INDENT 7.0 .TP .B Parameters .INDENT 7.0 .IP \(bu 2 \fBurl\fP \-\- MAAS endpoint .IP \(bu 2 \fBparams\fP \-\- extra data sent with the HTTP request .UNINDENT .UNINDENT .UNINDENT .INDENT 7.0 .TP .B tag_delete(tag) Delete a tag .INDENT 7.0 .TP .B Parameters \fBtag\fP (\fIstr\fP) \-\- tag id .TP .B Returns True on success False on failure .TP .B Return type bool .UNINDENT .UNINDENT .INDENT 7.0 .TP .B tag_fpi(maas) Tag each DECLARED host with the FPI tag. .sp Also a little strange: we could define a tag with \(aqdefinition=true()\(aq and automatically tag each node. However, each time we un\-tag a node, maas evaluates the xpath expression again and re\-tags it. So, we do it once, manually, when the machine is in the DECLARED state (also to avoid re\-tagging things that have already been tagged). .INDENT 7.0 .TP .B Parameters \fBmaas\fP \-\- MAAS object representing all managed nodes .UNINDENT .UNINDENT .INDENT 7.0 .TP .B tag_machine(tag, system_id) Tag the machine with the specified tag. .INDENT 7.0 .TP .B Parameters .INDENT 7.0 .IP \(bu 2 \fBtag\fP (\fIstr\fP) \-\- Tag name .IP \(bu 2 \fBsystem_id\fP (\fIstr\fP) \-\- ID of node .UNINDENT .TP .B Returns Success or Fail .TP .B Return type bool .UNINDENT .UNINDENT .INDENT 7.0 .TP .B tag_name(maas) Tag each node as its hostname. .sp This is a bit ugly. Since we want to be able to juju deploy to a particular node that the user has selected, we use juju\(aqs constraints support for maas. Unfortunately, juju didn\(aqt implement maas\-name directly, we have to tag each node with its hostname for now so that we can pass that tag as a constraint to juju. .INDENT 7.0 .TP .B Parameters \fBmaas\fP \-\- MAAS object representing all managed nodes .UNINDENT .UNINDENT .INDENT 7.0 .TP .B tag_new(tag) Create tag if it doesn\(aqt exist. .INDENT 7.0 .TP .B Parameters \fBtag\fP \-\- Tag name .TP .B Returns Success/Fail boolean .UNINDENT .UNINDENT .INDENT 7.0 .TP .B tags List tags known to MAAS .INDENT 7.0 .TP .B Returns List of tags or empty list .UNINDENT .UNINDENT .INDENT 7.0 .TP .B users List users on MAAS .INDENT 7.0 .TP .B Returns List of registered users or an empty list .UNINDENT .UNINDENT .INDENT 7.0 .TP .B zone_new(name, description=\(aqZone created by API\(aq) Create a physical zone .INDENT 7.0 .TP .B Parameters .INDENT 7.0 .IP \(bu 2 \fBname\fP \-\- Name of the zone .IP \(bu 2 \fBdescription\fP \-\- Description of zone. .UNINDENT .TP .B Returns True on success False on failure .UNINDENT .UNINDENT .INDENT 7.0 .TP .B zones List physical zones .INDENT 7.0 .TP .B Returns List of managed zones or empty list .UNINDENT .UNINDENT .UNINDENT .SS \fBcloudinstall.gui\fP \-\-\- GUI Interface .sp Pegasus \- gui interface to Ubuntu Cloud Installer .INDENT 0.0 .TP .B class cloudinstall.gui.ChangeStateDialog(underlying, machine, on_success, on_cancel) Bases: \fBurwid.container.Overlay\fP .INDENT 7.0 .TP .B ChangeStateDialog.keypress(size, key) .UNINDENT .UNINDENT .INDENT 0.0 .TP .B class cloudinstall.gui.CommandRunner Bases: \fBurwid.listbox.ListBox\fP .INDENT 7.0 .TP .B CommandRunner.add_machine() .UNINDENT .INDENT 7.0 .TP .B CommandRunner.change_allocation(new_states, machine) .UNINDENT .INDENT 7.0 .TP .B CommandRunner.deploy(charm, id=None, tag=None) .UNINDENT .INDENT 7.0 .TP .B CommandRunner.keypress(size, key) .UNINDENT .INDENT 7.0 .TP .B CommandRunner.poll() .UNINDENT .INDENT 7.0 .TP .B CommandRunner.update(juju_state) .UNINDENT .UNINDENT .INDENT 0.0 .TP .B class cloudinstall.gui.ConsoleMode Bases: \fBurwid.container.Frame\fP .INDENT 7.0 .TP .B ConsoleMode.tick() .UNINDENT .UNINDENT .INDENT 0.0 .TP .B class cloudinstall.gui.ControllerOverlay(underlying, command_runner) Bases: \fBcloudinstall.gui.TextOverlay\fP .INDENT 7.0 .TP .B ControllerOverlay.NODE_SETUP = \(aqYour node has been correctly detected. Please wait until setup is complete \(aq .UNINDENT .INDENT 7.0 .TP .B ControllerOverlay.NODE_WAIT = \(aqPlease wait while the cloud controller is installed on your host system.\(aq .UNINDENT .INDENT 7.0 .TP .B ControllerOverlay.PXE_BOOT = \(aqYou need one node to act as the cloud controller. Please PXE boot the node you would like to use.\(aq .UNINDENT .INDENT 7.0 .TP .B ControllerOverlay.process(data) Process a node list. Returns True if the overlay still needs to be shown, false otherwise. .UNINDENT .UNINDENT .INDENT 0.0 .TP .B class cloudinstall.gui.ListWithHeader(header_text) Bases: \fBurwid.container.Frame\fP .INDENT 7.0 .TP .B ListWithHeader.selectable() .UNINDENT .INDENT 7.0 .TP .B ListWithHeader.update(nodes) .UNINDENT .UNINDENT .INDENT 0.0 .TP .B class cloudinstall.gui.LockScreen(underlying, unlock) Bases: \fBurwid.container.Overlay\fP .INDENT 7.0 .TP .B LockScreen.INVALID = (\(aqerror\(aq, \(aqInvalid password.\(aq) .UNINDENT .INDENT 7.0 .TP .B LockScreen.IOERROR = (\(aqerror\(aq, \(aqProblem accessing /home/adam/.cloud\-install/openstack.passwd. Please make sure it contains exactly one line that is the lock password.\(aq) .UNINDENT .INDENT 7.0 .TP .B LockScreen.LOCKED = \(aqThe screen is locked. Please enter a password (this is the password you entered for OpenStack during installation). \(aq .UNINDENT .INDENT 7.0 .TP .B LockScreen.keypress(size, key) .UNINDENT .UNINDENT .INDENT 0.0 .TP .B class cloudinstall.gui.Node(machine, open_dialog) Bases: \fBurwid.widget.Text\fP .sp A single ui node representation .INDENT 7.0 .TP .B Node.is_compute .UNINDENT .INDENT 7.0 .TP .B Node.is_horizon .UNINDENT .INDENT 7.0 .TP .B Node.keypress(size, key) Signal binding for Node .sp Keys: .INDENT 7.0 .IP \(bu 2 Enter \- Opens node state change dialog .IP \(bu 2 F6 \- Opens charm deployments dialog .UNINDENT .UNINDENT .UNINDENT .INDENT 0.0 .TP .B class cloudinstall.gui.NodeViewMode(loop, get_data, command_runner) Bases: \fBurwid.container.Frame\fP .INDENT 7.0 .TP .B NodeViewMode.do_update(machines) Updating node states .INDENT 7.0 .TP .B Params list machines list of known machines .UNINDENT .UNINDENT .INDENT 7.0 .TP .B NodeViewMode.keypress(size, key) Signal binding for NodeViewMode .sp Keys: .INDENT 7.0 .IP \(bu 2 F5 \- Refreshes the node list .UNINDENT .UNINDENT .INDENT 7.0 .TP .B NodeViewMode.open_dialog(machine) .UNINDENT .INDENT 7.0 .TP .B NodeViewMode.refresh_states() Refresh states .sp Make a call to refresh both juju and maas machine states .INDENT 7.0 .TP .B Returns data from the polling of services and the juju state .TP .B Return type tuple (parse_state(), Machine()) .UNINDENT .UNINDENT .INDENT 7.0 .TP .B NodeViewMode.target .UNINDENT .INDENT 7.0 .TP .B NodeViewMode.tick() .UNINDENT .INDENT 7.0 .TP .B NodeViewMode.total_nodes() .UNINDENT .UNINDENT .INDENT 0.0 .TP .B class cloudinstall.gui.PegasusGUI(get_data) Bases: \fBurwid.main_loop.MainLoop\fP .INDENT 7.0 .TP .B PegasusGUI.run() .UNINDENT .INDENT 7.0 .TP .B PegasusGUI.run_async(f, callback) This is a little bit goofy. The urwid API is based on select(), and can\(aqt actually run python functions asynchronously. So, if we want to run a long\-running function which should update the UI, we have to get a fd to have urwid watch for us, and then we send data to it when it\(aqs done. .sp FIXME: Once \fI\%https://github.com/wardi/urwid/pull/57\fP is implemented. .UNINDENT .INDENT 7.0 .TP .B PegasusGUI.tick(unused_loop=None, unused_data=None) .UNINDENT .UNINDENT .INDENT 0.0 .TP .B class cloudinstall.gui.TextOverlay(text, underlying) Bases: \fBurwid.container.Overlay\fP .UNINDENT .SS \fBcloudinstall.log\fP \-\-\- Log Interface .sp Logging interface .sp Simply exports \fIlogger\fP variable .INDENT 0.0 .TP .B cloudinstall.log.logger(name=\(aqubuntu\-cloud\-installer\(aq) setup logging .sp Overridding the default log level(\fBdebug\fP) can be done via an environment variable \fIUCI_LOGLEVEL\fP .sp Available levels: .INDENT 7.0 .IP \(bu 2 CRITICAL .IP \(bu 2 ERROR .IP \(bu 2 WARNING .IP \(bu 2 INFO .IP \(bu 2 DEBUG .UNINDENT .INDENT 7.0 .INDENT 3.5 .sp .nf .ft C # Running cloud\-status from cli $ UCI_LOGLEVEL=INFO cloud\-status .ft P .fi .UNINDENT .UNINDENT .INDENT 7.0 .TP .B Params str name logger name .TP .B Returns a log object .UNINDENT .UNINDENT .SS \fBcloudinstall.machine\fP \-\-\- Maas/Juju machine representation .INDENT 0.0 .TP .B class cloudinstall.machine.Machine(machine_id, machine) Bases: \fBbuiltins.object\fP .sp Base machine class .INDENT 7.0 .TP .B Machine.agent_state Returns agent\-state .INDENT 7.0 .TP .B Return type str .UNINDENT .UNINDENT .INDENT 7.0 .TP .B Machine.arch Return architecture .INDENT 7.0 .TP .B Returns architecture type .TP .B Return type str .UNINDENT .UNINDENT .INDENT 7.0 .TP .B Machine.charms Returns charms for machine .INDENT 7.0 .TP .B Returns charms for machine .TP .B Return type generator .UNINDENT .UNINDENT .INDENT 7.0 .TP .B Machine.container(container_id) Inspect a container .INDENT 7.0 .TP .B Parameters \fBcontainer_id\fP (\fIint\fP) \-\- lxc container id .TP .B Returns Returns a dictionary of the container information for specific machine and lxc id. .TP .B Return type dict .UNINDENT .UNINDENT .INDENT 7.0 .TP .B Machine.containers Return containers for machine .INDENT 7.0 .TP .B Return type generator .UNINDENT .UNINDENT .INDENT 7.0 .TP .B Machine.cpu_cores Return number of cpu\-cores .INDENT 7.0 .TP .B Returns number of cpus .TP .B Return type str .UNINDENT .UNINDENT .INDENT 7.0 .TP .B Machine.dns_name Returns dns\-name .INDENT 7.0 .TP .B Return type str .UNINDENT .UNINDENT .INDENT 7.0 .TP .B Machine.hardware(spec) Get hardware information .INDENT 7.0 .TP .B Parameters \fBspec\fP (\fIstr\fP) \-\- a hardware specification .TP .B Returns hardware of spec .TP .B Return type str .UNINDENT .UNINDENT .INDENT 7.0 .TP .B Machine.instance_id Returns instance\-id of a machine .INDENT 7.0 .TP .B Returns instance\-id of machine .TP .B Return type str .UNINDENT .UNINDENT .INDENT 7.0 .TP .B Machine.is_machine_0 Checks if machine is bootstrapped node .INDENT 7.0 .TP .B Return type bool .UNINDENT .UNINDENT .INDENT 7.0 .TP .B Machine.mem Return memory .INDENT 7.0 .TP .B Returns memory size .TP .B Return type str .UNINDENT .UNINDENT .INDENT 7.0 .TP .B Machine.storage Return storage .INDENT 7.0 .TP .B Returns storage size .TP .B Return type str .UNINDENT .UNINDENT .INDENT 7.0 .TP .B Machine.units Return units for machine .INDENT 7.0 .TP .B Return type list .UNINDENT .UNINDENT .UNINDENT .SS \fBcloudinstall.pegasus\fP \-\-\- GUI helpers .INDENT 0.0 .TP .B exception cloudinstall.pegasus.MaasLoginFailure Bases: \fBbuiltins.Exception\fP .INDENT 7.0 .TP .B MaasLoginFailure.MESSAGE = \(aqCould not read login credentials. Please run: maas\-get\-user\-creds root > ~/.cloud\-install/maas\-creds\(aq .UNINDENT .UNINDENT .INDENT 0.0 .TP .B cloudinstall.pegasus.get_charm_relations(charm) Return a list of (relation, command) of relations to add. .UNINDENT .INDENT 0.0 .TP .B cloudinstall.pegasus.juju_config_arg(charm) Query configuration parameters for openstack charms .INDENT 7.0 .TP .B Parameters \fBcharm\fP (\fIstr\fP) \-\- name of charm .TP .B Returns path of openstack configuration .TP .B Return type str .UNINDENT .UNINDENT .INDENT 0.0 .TP .B cloudinstall.pegasus.parse_state(juju, maas=None) Parses the current state of juju containers and maas nodes .INDENT 7.0 .TP .B Parameters .INDENT 7.0 .IP \(bu 2 \fBjuju\fP (\fIJujuState()\fP) \-\- juju polled state .IP \(bu 2 \fBmaas\fP \-\- maas polled state .UNINDENT .TP .B Returns nodes/containers .TP .B Return type list .UNINDENT .UNINDENT .INDENT 0.0 .TP .B cloudinstall.pegasus.poll_state() Polls current state of Juju and MAAS .UNINDENT .INDENT 0.0 .TP .B cloudinstall.pegasus.wait_for_services() Wait for services to be in ready state .INDENT 7.0 .INDENT 3.5 .SS Todo .UNINDENT .UNINDENT .sp Is this still needed? .UNINDENT .SS \fBcloudinstall.service\fP \-\-\- Service Interface .sp Represents a Juju service .INDENT 0.0 .TP .B class cloudinstall.service.Relation(relation_name, charms) Bases: \fBbuiltins.object\fP .sp Relation class .UNINDENT .INDENT 0.0 .TP .B class cloudinstall.service.Service(service_name, service) Bases: \fBbuiltins.object\fP .sp Service class .INDENT 7.0 .TP .B Service.charm Charm .INDENT 7.0 .TP .B Returns Charm Path .TP .B Return type str .UNINDENT .UNINDENT .INDENT 7.0 .TP .B Service.exposed Exposed .INDENT 7.0 .TP .B Returns if service is exposed .TP .B Return type bool .UNINDENT .UNINDENT .INDENT 7.0 .TP .B Service.relations Service relations .INDENT 7.0 .TP .B Returns list of relations for service .TP .B Return type Relation() .UNINDENT .UNINDENT .INDENT 7.0 .TP .B Service.unit(name) Single unit entry .INDENT 7.0 .TP .B Params str name name of unit .TP .B Returns a Unit entry .TP .B Return type Unit() .UNINDENT .UNINDENT .INDENT 7.0 .TP .B Service.units Service units .INDENT 7.0 .TP .B Returns list associated units for service .TP .B Return type Unit() .UNINDENT .UNINDENT .UNINDENT .INDENT 0.0 .TP .B class cloudinstall.service.Unit(unit_name, unit) Bases: \fBbuiltins.object\fP .sp Unit class .INDENT 7.0 .TP .B Unit.agent_state Unit\(aqs agent state .INDENT 7.0 .TP .B Returns agent state .TP .B Return type str .UNINDENT .UNINDENT .INDENT 7.0 .TP .B Unit.machine_id Associate machine for unit .INDENT 7.0 .TP .B Returns machine id .TP .B Return type str .UNINDENT .UNINDENT .INDENT 7.0 .TP .B Unit.public_address Public address of unit .INDENT 7.0 .TP .B Returns address of unit .TP .B Return type str .UNINDENT .UNINDENT .UNINDENT .SS \fBcloudinstall.utils\fP \-\-\- Utility helpers .INDENT 0.0 .TP .B cloudinstall.utils.console_blank(*args, **kwds) .UNINDENT .INDENT 0.0 .TP .B cloudinstall.utils.get_command_output(command, timeout=300) Execute command through system shell .INDENT 7.0 .TP .B Parameters \fBcommand\fP (\fIstr\fP) \-\- command to run .TP .B Returns (returncode, stdout, 0) .TP .B Return type tuple .UNINDENT .INDENT 7.0 .INDENT 3.5 .sp .nf .ft C # Get output of juju status ret, out, rtime = utils.get_command_output(\(aqjuju status\(aq) .ft P .fi .UNINDENT .UNINDENT .UNINDENT .INDENT 0.0 .TP .B cloudinstall.utils.get_network_interface(iface) Get network interface properties .INDENT 7.0 .TP .B Parameters \fBiface\fP (\fIstr\fP) \-\- Interface to query (ex. eth0) .TP .B Returns interface properties or empty if none .TP .B Return type dict .UNINDENT .INDENT 7.0 .INDENT 3.5 .sp .nf .ft C # Get address, broadcast, and netmask of eth0 iface = utils.get_network_interface(\(aqeth0\(aq) .ft P .fi .UNINDENT .UNINDENT .UNINDENT .INDENT 0.0 .TP .B cloudinstall.utils.get_network_interfaces() Get network interfaces .INDENT 7.0 .TP .B Returns available interfaces and their properties .TP .B Return type generator .UNINDENT .UNINDENT .INDENT 0.0 .TP .B cloudinstall.utils.partition(pred, iterable) Returns tuple of allocated and unallocated systems .INDENT 7.0 .TP .B Parameters .INDENT 7.0 .IP \(bu 2 \fBpred\fP (\fIfunction\fP) \-\- status predicate .IP \(bu 2 \fBiterable\fP (\fIlist\fP) \-\- machine data .UNINDENT .TP .B Returns ([allocated], [unallocated]) .TP .B Return type tuple .UNINDENT .INDENT 7.0 .INDENT 3.5 .sp .nf .ft C def is_allocated(d): allocated_states = [\(aqstarted\(aq, \(aqpending\(aq, \(aqdown\(aq] return \(aqcharms\(aq in d or d[\(aqagent_state\(aq] in allocated_states allocated, unallocated = utils.partition(is_allocated, [{state: \(aqpending\(aq}]) .ft P .fi .UNINDENT .UNINDENT .UNINDENT .INDENT 0.0 .TP .B cloudinstall.utils.randomString(size=6, chars=\(aqABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\(aq) Generate a random string .INDENT 7.0 .TP .B Parameters .INDENT 7.0 .IP \(bu 2 \fBsize\fP (\fIint\fP) \-\- number of string characters .IP \(bu 2 \fBchars\fP (\fIstr\fP) \-\- range of characters (optional) .UNINDENT .TP .B Returns a random string .TP .B Return type str .UNINDENT .UNINDENT .INDENT 0.0 .TP .B cloudinstall.utils.reset_blanking() .UNINDENT .INDENT 0.0 .TP .B cloudinstall.utils.time() Time helper .INDENT 7.0 .TP .B Returns formatted current time string .TP .B Return type str .UNINDENT .UNINDENT .SH AUTHOR Solutions Engineering .SH COPYRIGHT 2014, Canonical Ltd .\" Generated by docutils manpage writer. . cloud-installer-proper/share/0000755000175000017500000000000012323623210015146 5ustar adamadamcloud-installer-proper/share/maas.sh0000644000175000017500000001326312323623210016430 0ustar adamadam# # maas.sh - Shell routines related to MAAS # # Copyright 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 as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY 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 . configBindOptions() { cat <<"EOF" options { directory "/var/cache/bind"; forwarders { EOF for forwarder; do printf "\t\t%s;\n" $forwarder done cat <<"EOF" }; auth-nxdomain no; listen-on-v6 { any; }; }; EOF } configMaasBridge() { cat <<-EOF auto $1 iface $1 inet manual auto br0 EOF cat "$2" printf "\t%s\n" "bridge_ports $1" } configureDns() { configBindOptions $(awk '/^nameserver / { print $2 }' /etc/resolv.conf) \ > /etc/bind/named.conf.options service bind9 restart sed -e '/^iface lo inet loopback$/a\ \ dns-nameservers 127.0.0.1' -i /etc/network/interfaces # lp 1102507 ifdown lo; ifup lo } configureMaasImages() { # don't sync the i386 images sed -i -e '/- i386/d' /etc/maas/bootresources.yaml } configureMaasServices() { # When maas starts, it picks an interface at random and stuffs it in these # config files. If that happens to be an extrnal interface that is, say, # getting an IP via DHCP, then when it gets a new IP our MaaS is totally # broken. So here we switch any external IPs in the configuration files with # the internal MaaS IP, which should effectively be the same here but also # won't change. # # Even though we are changing config files for the services, we don't need to # restart them, since they'll work fine now (and until the server would get a # new IP if this config setting hadn't been changed). current=$(gawk 'match($0, /http:\/\/([0-9.]*)\/MAAS/, m) { print m[1]; }' \ /etc/maas/maas_cluster.conf) if [ "$1" != "$current" ]; then sed -i "s/$current/$1/g" \ /etc/maas/maas_cluster.conf \ /etc/maas/maas_local_celeryconfig.py \ /etc/maas/maas_local_settings.py fi } configureMaasInterfaces() { awk -v interface=$1 -v "bridge_cfg=$2" -f - "$3" <<-"EOF" function strip(s) { sub(/^[[:blank:]]+/, "", s) sub(/[[:blank:]]+$/, "", s) return s } /^[[:blank:]]*(iface|mapping|auto|allow-[^ ]+|source) / { s_iface = 0; iface = 0 } $0 ~ "^[[:blank:]]*auto (" interface "|br0)[[:blank:]]*$" { print "#" $0; next } $0 ~ "^[[:blank:]]*iface (" interface "|br0) " { s_iface = 1 if ($2 == interface) { iface = 1 print "iface br0", $3, $4 > bridge_cfg } print "#" $0 next } s_iface == 1 { if (iface == 1) { print "\t" strip($0) > bridge_cfg } print "#" $0 next } { print $0 } EOF } configureMaasNetworking() { address=$(ipAddress $2) netmask=$(ipNetmask $2) broadcast=$(ipBroadcast $2) if maasInterfaceExists $1 $2; then maas maas node-group-interface update $1 $2 ip=$address \ interface=$2 management=2 subnet_mask=$netmask \ broadcast_ip=$broadcast router_ip=$3 ip_range_low=$4 \ ip_range_high=$5 1>&2 else maas maas node-group-interfaces new $1 ip=$address \ interface=$2 management=2 subnet_mask=$netmask \ broadcast_ip=$broadcast router_ip=$3 ip_range_low=$4 \ ip_range_high=$5 1>&2 fi } createMaasBridge() { ifdown $1 br0 1>&2 || true for cfg in /etc/network/interfaces /etc/network/interfaces.d/*.cfg; do [ -e "$cfg" ] || continue configureMaasInterfaces $1 $TMP/bridge.cfg "$cfg" \ > $TMP/interfaces.cfg mv $TMP/interfaces.cfg "$cfg" done if ! grep -Eq '^[[:blank:]]*source /etc/network/interfaces\.d/\*\.cfg[[:blank:]]*$' \ /etc/network/interfaces; then printf "\n%s\n" "source /etc/network/interfaces.d/*.cfg" \ >> /etc/network/interfaces fi mkdir -p /etc/network/interfaces.d configMaasBridge $1 $TMP/bridge.cfg \ > /etc/network/interfaces.d/cloud-install.cfg ifup $1 br0 1>&2 } createMaasSuperUser() { password=$(cat "/home/$INSTALL_USER/.cloud-install/openstack.passwd") printf "%s\n%s\n" "$password" "$password" \ | setsid sh -c "maas-region-admin createsuperuser --username root --email root@example.com 1>&2" } maasAddress() { echo $1 | tr . - } maasFilePath() { maas maas files list "prefix=$1" \ | python3 -c 'import json; import sys; print(json.load(sys.stdin)[0]["anon_resource_uri"])' } maasInterfaceExists() { exists=$(maas maas node-group-interfaces list $1 \ | python3 -c "import json; import sys; print(len([interface for interface in json.load(sys.stdin) if interface[\"interface\"] == \"$2\"]))") if [ $exists = 1 ]; then return 0 else return 1 fi } maasLogin() { maas login maas http://localhost/MAAS/api/1.0 $1 } maasLogout() { maas logout maas rm -rf /home/$INSTALL_USER/.maascli.db } nodeStatus() { maas maas nodes list id=$1 \ | python3 -c 'import json; import sys; print(json.load(sys.stdin)[0]["status"])' } nodeSystemId() { maas maas nodes list mac_address=$1 \ | python3 -c 'import json; import sys; print(json.load(sys.stdin)[0]["system_id"])' } waitForClusterRegistration() { while true; do uuid=$(maas maas node-groups list \ | python3 -c 'import json; import sys; print(json.load(sys.stdin)[0]["uuid"])') if [ $uuid != master ]; then break fi sleep 5 done } waitForNodeStatus() { while [ $(nodeStatus $1) -ne $2 ]; do sleep 5 done } cloud-installer-proper/share/common.sh0000644000175000017500000000726312323623210017002 0ustar adamadam# # common.sh - helper functions for cloud-install # # Copyright 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 as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY 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 . BACKTITLE="Cloud install" LOG=/var/log/cloud-install.log export PYTHONPATH=/usr/share/cloud-installer/common export PYTHONDONTWRITEBYTECODE=true configIptablesNat() { cat <<-EOF *nat :PREROUTING ACCEPT [0:0] :INPUT ACCEPT [0:0] :OUTPUT ACCEPT [0:0] :POSTROUTING ACCEPT [0:0] -A POSTROUTING -s $1 ! -d $1 -j MASQUERADE COMMIT EOF } configureNat() { iptables -t nat -A POSTROUTING -s $1 ! -d $1 -j MASQUERADE configIptablesNat $1 > /etc/network/iptables.rules chmod 0600 /etc/network/iptables.rules sed -e '/^iface lo inet loopback$/a\ \ pre-up iptables-restore < /etc/network/iptables.rules' -i \ /etc/network/interfaces } createTempDir() { TMP=$(mktemp -d /tmp/cloud-install.XXX) } disableBlank() { tty=$(tty) if [ -n "$tty" ] && [ "${tty%%[0-9]}" = /dev/tty ]; then CONSOLE_BLANK=$(cat /sys/module/kernel/parameters/consoleblank) setterm -blank 0 fi } enableBlank() { if [ -n "$CONSOLE_BLANK" ]; then setterm -blank $((CONSOLE_BLANK/60)) fi } enableIpForwarding() { sed -e 's/^#net.ipv4.ip_forward=1$/net.ipv4.ip_forward=1/' -i \ /etc/sysctl.conf sysctl -p } error() { dialogMsgBox "[!] An error has occurred" Continue \ "Installation aborted\n\nSee /var/log/cloud-install.log for details." \ 10 60 } exitInstall() { ret=$? stopLog wait if [ $ret -gt 0 ]; then error fi enableBlank removeTempDir } generateSshKeys() { if [ ! -e "/home/$INSTALL_USER/.ssh/id_rsa" ]; then sudo -u "$INSTALL_USER" ssh-keygen -N "" \ -f "/home/$INSTALL_USER/.ssh/id_rsa" 1>&2 else echo "*** ssh keys exist for this user, they will be used instead." echo "*** If the current ssh keys are not passwordless you'll be" echo "*** required to enter your ssh key password during juju" echo "*** deployments." fi } getInterfaces() { ifconfig -s | egrep -v 'Iface|lo' | egrep -o '^[a-zA-Z0-9]+' | paste -sd ' ' } gateway() { route -n | awk 'index($4, "G") { print $2 }' } ipAddress() { ifconfig $1 | egrep -o "inet addr:[0-9.]+" | sed -e "s/^inet addr://" } ipBroadcast() { ifconfig $1 | egrep -o "Bcast:[0-9.]+" | sed -e "s/^Bcast://" } ipNetmask() { ifconfig $1 | egrep -o "Mask:[0-9.]+" | sed -e "s/^Mask://" } ipNetwork() { ip addr show $1 | awk '/^ inet / { print $2 }' } removeTempDir() { if [ -n "$TMP" ]; then rm -rf "$TMP" fi } startLog() { (umask 0077; touch "$LOG") printf "Cloud installation started %s\n" "$(date)" >> "$LOG" mkfifo -m 0600 "$TMP/log" ts "$TMP/log" >> "$LOG" & log_pid=$! exec 2> "$TMP/log" } stopLog() { if [ -n "$log_pid" ]; then exec 2>&1 wait $log_pid rm -f "$TMP/log" printf "Cloud installation finished %s\n" "$(date)" >> "$LOG" fi } ts() { awk '{ print strftime("%F %T"), $0; fflush() }' "$1" } INSTALL_USER=$(getent passwd 1000 | cut -d : -f 1) # HELPER TOOLS wait_for_landscape=/usr/share/cloud-installer/bin/wait-for-landscape ip_range=/usr/share/cloud-installer/bin/ip_range.py maas_report_boot_images=/usr/share/cloud-installer/bin/maas-report-boot-images cloud-installer-proper/share/landscape.sh0000644000175000017500000001031112323623210017430 0ustar adamadam# # landscape.sh - Landscape-install interface # # Copyright 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 as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY 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 . getLandscapeCert() { begin="-----BEGIN CERTIFICATE-----" end="-----END CERTIFICATE-----" cert=$(echo | openssl s_client -connect "$1":443 < /dev/null 2>/dev/null) echo "$begin" echo "$cert" | sed '1,/^-----BEGIN CERTIFICATE-----$/d' \ | sed '/^-----END CERTIFICATE-----$/,$d' echo "$end" } getDomain() { echo "$1" | grep -E "^[^@]+@[^@]+\.[^@]+$" | sed -E -e 's/[^@]+@([^@]+\.[^@]+)/\1/' } configureLandscape() { state=1 email_domain=example.com while [ -n "$state" ] && [ "$state" != 4 ]; do next_state=$((state + 1)) case $state in 1) admin_email=$(dialogInput "Landscape login" "Please enter the login email you would like to use for Landscape." 10 60) result=$(getDomain "$admin_email") if [ -z "$result" ]; then popState; continue fi email_domain="$result" ;; 2) suggested_name="$(getent passwd $INSTALL_USER | cut -d ':' -f 5 | cut -d ',' -f 1)" admin_name=$(dialogInput "Landscape user's full name" "Please enter the full name of the admin user for Landscape." 10 60 "$suggested_name") if [ -z "$admin_name" ]; then popState; continue fi ;; 3) system_email=$(dialogInput "Landscape system email" "Please enter the email that landscape should use as the system email." 10 60 "landscape@$email_domain") result=$(getDomain "$system_email") if [ -z "$result" ]; then popState; continue fi ;; esac pushState "$state" state=$next_state done } getDictField() { python3 -c "d = $1; print(d['$2'])" } landscapeInstall() { configureLandscape # The landscape install needs a fully working juju bootstrap environment, # just like the multi install with no status screen does. multiInstall # For now, we assume that the install user has the landscape charm with the # right licensing configs cloned into their home directory; we can fix this # later when the landscape charm deploys with a free license. cd "/home/$INSTALL_USER/landscape-charm/config" && \ juju-deployer -Wdv -c landscape-deployments.yaml landscape-dense-maas # Landscape isn't actually up when juju-deployer exits; the relations take a # while to set up and deployer doesn't wait until they're finished (it has # no way to, viz. LP #1254766), so we wait until everything is ok. landscape_ip=$($wait_for_landscape) certfile=~/.cloud-install/landscape-ca.pem getLandscapeCert "$landscape_ip" > "$certfile" # landscape-api just prints a __repr__ of the response we get, which contains # both LANDSCAPE_API_KEY and LANDSCAPE_API_SECRET for the user. resp=$(landscape-api \ --key anonymous --secret anonymous --uri "https://$landscape_ip/api/" \ --ssl-ca-file "$certfile" \ call BootstrapLDS \ admin_email="$admin_email" \ admin_password=$(cat "/home/$INSTALL_USER/.cloud-install/openstack.passwd") \ admin_name="$admin_name" root_url="https://$landscape_ip/" \ system_email="$system_email") landscape_api_key=$(getDictField "$resp" LANDSCAPE_API_KEY) landscape_api_secret=$(getDictField "$resp" LANDSCAPE_API_SECRET) landscape-api \ --key "$landscape_api_key" \ --secret "$landscape_api_secret" \ --uri "https://$landscape_ip/api/" \ --ssl-ca-file "$certfile" \ register-maas-region-controller \ endpoint="http://$(ipAddress br0)/MAAS" \ credentials="$(cat /home/$INSTALL_USER/.cloud-install/maas-creds)" echo "Your Landscape installation is complete!" echo "Please go to http://$landscape_ip/account/standalone/openstack to" echo "continue with the installation of your OpenStack cloud." } cloud-installer-proper/share/configure.sh0000644000175000017500000000660412323623210017471 0ustar adamadam# # configure.sh - install configuration # # Copyright 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 as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY 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 . configureInstall() { state=1 while [ -n "$state" ] && [ "$state" != 32 ]; do next_state=$((state + 1)) case $state in 1) install_type=$(dialogMenu "Select install type" "" 10 60 3 Multi-system "Single system" "Landscape managed") case $install_type in Multi-system) next_state=10 ;; "Single system") next_state=30 ;; "Landscape managed") next_state=10 ;; *) popState; continue ;; esac ;; 10) interfaces=$(getInterfaces) interfaces_count=$(echo "$interfaces" | wc -w) if [ $interfaces_count -ge 2 ]; then interface=$(dialogMenu "Select network interface" "" 15 60 8 $interfaces) if [ -z "$interface" ]; then popState; continue fi else interface=$interfaces state=$next_state; continue fi ;; 11) bridge_interface="" if [ $interfaces_count -ge 2 ]; then if dialogYesNo "Bridge interface?" "Sometimes it is useful to run MaaS on its own network. If you are running MaaS on its own network and would like to bridge this network to the outside world, please indicate so." 10 60; then bridge_interface=true fi fi state=$next_state; continue ;; 12) network=$(ipNetwork $interface) address=$(ipAddress $interface) if [ -n "$bridge_interface" ]; then dhcp_range=$($ip_range $network $address) else gateway=$(gateway) dhcp_range=$($ip_range $network $address $gateway) fi state=$next_state; continue ;; 13) dhcp_range=$(dialogInput "IP address range (-):" "IP address range for DHCP leases.\nNew nodes will be assigned addresses from this pool." 10 60 "$dhcp_range") if [ -z "$dhcp_range" ]; then popState; continue fi next_state=30 ;; 30) openstack_password=$(dialogPassword "OpenStack admin user password:" "A good password will contain a mixture of letters, numbers and punctuation and should be changed at regular intervals." 10 60) if [ -z "$openstack_password" ]; then popState; continue fi ;; 31) openstack_password2=$(dialogPassword "OpenStack admin user password to verify:" "Please enter the same OpenStack admin user password again to verify that you have typed it correctly." 10 60) if [ -z "$openstack_password2" ]; then popState; continue fi if [ "$openstack_password" != "$openstack_password2" ]; then dialogMsgBox "[!] Password mismatch" Continue "The two passwords you entered were not the same, please try again." 10 60 popState; continue fi ;; esac pushState "$state" state=$next_state done } popState() { if [ -n "$states" ]; then state=${states##* } states=${states% *} else state="" fi } pushState() { states="$states $1" } cloud-installer-proper/share/single.sh0000644000175000017500000000323312323623210016764 0ustar adamadam# # single.sh - Single install interface # # Copyright 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 as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY 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 . singleInstall() { touch /home/$INSTALL_USER/.cloud-install/single memory=$(head -n 1 /proc/meminfo | awk '/[0-9]/ {print $2}') # we require 8gb for the single install if [ "$memory" -lt $((8 * 1024 * 1024)) ] && [ -z "$force_install" ]; then dialogMsgBox "Insufficient Memory!" "Abort" \ "You need at least 8GB of memory to run the single machine install." 10 60 return 0 fi dialogGaugeStart Installing "Please wait" 8 70 0 { dialogAptInstall 2 18 cloud-install-single dialogGaugePrompt 22 "Generating SSH keys" generateSshKeys dialogGaugePrompt 80 "Bootstrapping Juju" configureJuju configLocalEnvironment ( cd "/home/$INSTALL_USER" sudo -H -u "$INSTALL_USER" juju bootstrap sudo -H -u "$INSTALL_USER" juju set-constraints mem=1G ) echo 99 dialogGaugePrompt 100 "Installation complete" sleep 2 } > "$TMP/gauge" dialogGaugeStop } cloud-installer-proper/share/multi.sh0000644000175000017500000000543512323623210016643 0ustar adamadam# # multi.sh - Multi-install interface # # Copyright 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 as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY 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 . multiInstall() { touch /home/$INSTALL_USER/.cloud-install/multi cp /etc/network/interfaces /etc/network/interfaces.cloud.bak dialogGaugeStart Installing "Please wait" 8 70 0 { dialogAptInstall 2 18 cloud-install-multi service lxc-net stop || true sed -e 's/^USE_LXC_BRIDGE="true"/USE_LXC_BRIDGE="false"/' -i \ /etc/default/lxc-net dialogGaugePrompt 22 "Generating SSH keys" generateSshKeys dialogGaugePrompt 24 "Creating MAAS super user" createMaasSuperUser echo 25 maas_creds=$(maas-region-admin apikey --username root) saveMaasCreds $maas_creds maasLogin $maas_creds dialogGaugePrompt 26 "Waiting for MAAS cluster registration" waitForClusterRegistration createMaasBridge $interface dialogGaugePrompt 30 "Configuring MAAS networking" if [ -n "$bridge_interface" ]; then gateway=$(ipAddress br0) configureNat $(ipNetwork br0) enableIpForwarding configureMaasServices $gateway fi # Retrieve dhcp-range configureMaasNetworking $uuid br0 $gateway \ ${dhcp_range%-*} ${dhcp_range#*-} dialogGaugePrompt 35 "Configuring DNS" configureDns dialogGaugePrompt 40 "Importing MAAS boot images" configureMaasImages if [ -n "$MAAS_HTTP_PROXY" ]; then maas maas maas set-config name=http_proxy value="$MAAS_HTTP_PROXY" fi if [ -z "$CLOUD_INSTALL_DEBUG" ]; then http_proxy=$MAAS_HTTP_PROXY HTTP_PROXY=$MAAS_HTTP_PROXY maas-import-pxe-files $maas_report_boot_images fi dialogGaugePrompt 60 "Configuring Juju" address=$(ipAddress br0) admin_secret=$(pwgen -s 32) configureJuju configMaasEnvironment $address $maas_creds $admin_secret dialogGaugePrompt 75 "Bootstrapping Juju" jujuBootstrap $uuid echo 99 maas maas tags new name=use-fastpath-installer definition="true()" maasLogout dialogGaugePrompt 100 "Installation complete" sleep 2 } > "$TMP/gauge" dialogGaugeStop } saveMaasCreds() { echo $1 > "/home/$INSTALL_USER/.cloud-install/maas-creds" chmod 0600 "/home/$INSTALL_USER/.cloud-install/maas-creds" chown "$INSTALL_USER:$INSTALL_USER" \ "/home/$INSTALL_USER/.cloud-install/maas-creds" } cloud-installer-proper/share/display.sh0000644000175000017500000000714212323623210017153 0ustar adamadam# # display.sh - display routines for cloud-install # # Copyright 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 as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY 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 . # Install packages via apt-get displaying progress within an existing gauge # # dialogAptInstall percent range # # See dialogGaugeStart # dialogAptInstall() { download_start=$1 download_range=$(($2 / 2)) install_start=$((download_start + download_range)) install_range=$(($2 - download_range)) shift 2 mkfifo -m 0600 "$TMP/apt-status" DEBIAN_FRONTEND=noninteractive apt-get -qyf \ -o Dpkg::Options::=--force-confdef \ -o Dpkg::Options::=--force-confold \ -o APT::Status-Fd=3 install "$@" \ < /dev/null 1>&2 \ 3> "$TMP/apt-status" & while IFS=: read status pkg percent description; do case $status in dlstatus) text="Downloading packages...$description" p=$((download_start + ((${percent%.*} * download_range) / 100))) ;; pmstatus) text="Installing packages...$description" p=$((install_start + ((${percent%.*} * install_range) / 100))) ;; *) echo "unexpected apt-get status $status" 1>&2 exit 1 ;; esac dialogGaugePrompt $p "$text" done < "$TMP/apt-status" wait $! rm -f "$TMP/apt-status" } # Update a progress gauge # # dialogGaugePrompt percent text # # See dialogGaugeStart # dialogGaugePrompt() { printf "%s\n%s\n%s\n%s\n" XXX $1 "$2" XXX } # Start a progress gauge # # dialogGaugeStart title text height width percent # # See dialogGaugePrompt, dialogGaugeStop # dialogGaugeStart() { mkfifo -m 0600 "$TMP/gauge" whiptail --title "$1" --backtitle "$BACKTITLE" --gauge "$2" $3 $4 $5 \ < "$TMP/gauge" & gauge_pid=$! } # Stop a progress gauge # # See dialogGaugeStart # dialogGaugeStop() { wait $gauge_pid rm -f "$TMP/gauge" } # Display an input box # # dialogInput title text height width input-text # # writes text entry to stdout, empty string if user cancels # dialogInput() { whiptail --title "$1" --backtitle "$BACKTITLE" --inputbox "$2" $3 $4 \ "$5" 3>&1 1>/dev/tty 2>&3 || true } # Display a menu # # dialogMenu title text height width menu-height menu-item... # # writes menu selection to stdout, empty string if user cancels # dialogMenu() { title=$1 text=$2 height=$3 width=$4 menu_height=$5 shift 5 for item; do echo "\"$item\"" echo '""' done | xargs whiptail --title "$title" --backtitle "$BACKTITLE" --menu \ "$text" $height $width $menu_height 3>&1 1>/dev/tty 2>&3 || true } # Display a message # # dialogMsgBox title button-text text height width # dialogMsgBox() { whiptail --title "$1" --backtitle "$BACKTITLE" --ok-button "$2" \ --msgbox "$3" $4 $5 } # Display a password box # # dialogPassword title text height width # # writes text entry to stdout, empty string if user cancels # dialogPassword() { whiptail --title "$1" --backtitle "$BACKTITLE" --passwordbox "$2" $3 \ $4 3>&1 1>/dev/tty 2>&3 || true } # Display a yes/no choice # # dialogYesNo title text height width # # exit 0 on yes, 1 on no # dialogYesNo() { whiptail --title "$1" --backtitle "$BACKTITLE" --yesno "$2" $3 $4 } cloud-installer-proper/share/juju.sh0000644000175000017500000000603712323623210016465 0ustar adamadam# # juju.sh - Shell routines for Juju # # Copyright 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 as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY 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 . configMaasEnvironment() { cat <<-EOF default: maas environments: maas: type: maas maas-server: 'http://$1/MAAS/' maas-oauth: '$2' admin-secret: $3 default-series: precise authorized-keys-path: ~/.ssh/id_rsa.pub apt-http-proxy: 'http://$1:8000/' EOF } configLocalEnvironment() { cat <<-EOF default: local environments: local: type: local container: kvm EOF } configureJuju() { env_type=$1 shift if [ ! -e "/home/$INSTALL_USER/.juju" ]; then mkdir -m 0700 "/home/$INSTALL_USER/.juju" chown "$INSTALL_USER:$INSTALL_USER" "/home/$INSTALL_USER/.juju" fi (umask 0077; $env_type $@ > "/home/$INSTALL_USER/.juju/environments.yaml") chown "$INSTALL_USER:$INSTALL_USER" \ "/home/$INSTALL_USER/.juju/environments.yaml" } # TODO break this function into smaller ones jujuBootstrap() { cluster_uuid=$1 lxc-create -n juju-bootstrap -t ubuntu-cloud -- -r precise sed -e "s/^lxc.network.link.*$/lxc.network.link = br0/" -i \ /var/lib/lxc/juju-bootstrap/config printf "\n%s\n%s\n" "# Autostart on reboot" "lxc.start.auto = 1" \ >> /var/lib/lxc/juju-bootstrap/config # lxc has to look like vanilla maas ubuntu for juju cloud-init script # to run rm /var/lib/lxc/juju-bootstrap/rootfs/etc/network/interfaces.d/eth0.cfg printf "%s\n%s\n" "auto eth0" "iface eth0 inet dhcp" \ >> /var/lib/lxc/juju-bootstrap/rootfs/etc/network/interfaces mac=$(grep lxc.network.hwaddr /var/lib/lxc/juju-bootstrap/config \ | cut -d " " -f 3) # TODO dynamic architecture selection maas maas nodes new architecture=amd64/generic mac_addresses=$mac \ hostname=juju-bootstrap nodegroup=$cluster_uuid power_type=virsh system_id=$(nodeSystemId $mac) wget -O $TMP/maas.creds \ "http://localhost/MAAS/metadata/latest/by-id/$system_id/?op=get_preseed" python2 /etc/maas/templates/commissioning-user-data/snippets/maas_signal.py \ --config $TMP/maas.creds OK || true (cd "/home/$INSTALL_USER"; sudo -H -u "$INSTALL_USER" juju --show-log sync-tools) (cd "/home/$INSTALL_USER"; sudo -H -u "$INSTALL_USER" juju bootstrap --upload-tools) & waitForNodeStatus $system_id 6 rm -rf /var/lib/lxc/juju-bootstrap/rootfs/var/lib/cloud/seed/* cp $TMP/maas.creds \ /var/lib/lxc/juju-bootstrap/rootfs/etc/cloud/cloud.cfg.d/91_maas.cfg lxc-start -n juju-bootstrap -d wait $! } cloud-installer-proper/tools/0000755000175000017500000000000012323623210015204 5ustar adamadamcloud-installer-proper/tools/juju-api-inspect.py0000755000175000017500000000073512323623210020755 0ustar adamadam#!/usr/bin/env python import os import sys sys.path.insert(0, '../cloudinstall') from cloudinstall.juju.client import JujuClient from pprint import pprint import json import time JUJU_PASS = os.environ['JUJU_PASS'] if os.environ['JUJU_PASS'] else randomString() JUJU_URL = os.environ['JUJU_URL'] if os.environ['JUJU_URL'] else 'wss://juju-bootstrap.master:17070/' if __name__ == '__main__': ws = JujuClient(JUJU_URL) ws.login(JUJU_PASS) ws.info() ws.close() cloud-installer-proper/tools/version0000755000175000017500000000224612323623210016623 0ustar adamadam#!/usr/bin/env python3 # -*- mode: python; -*- # # version - Parses and prints version from debian/changelog # # Copyright 2014 Canonical, Ltd. # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This package is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # import re import sys def print_version(): """ Prints version string from debian/changelog @return: 0 on success 1 on failure """ pattern = 'cloud-installer\s.(.*)-\dubuntu\d' with open('debian/changelog', 'r') as f: match = re.match(pattern, f.read()) if match: print(match.groups()[0]) return 0 return 1 if __name__ == "__main__": print_version() cloud-installer-proper/tools/cloud-uninstall0000755000175000017500000000440212323623210020247 0ustar adamadam#!/bin/bash if [ "$#" -eq 1 ]; then WHAT=$1 elif [ -f ~/.cloud-install/multi ]; then WHAT=multi-system elif [ -f ~/.cloud-install/single ]; then WHAT=single-system else echo "could not determine install type" fi apt_purge() { DEBIAN_FRONTEND=noninteractive apt-get -yy purge $@ } case $WHAT in multi-system) echo @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ echo Multi install cleansing. echo @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ juju destroy-environment --yes --force maas rm -r ~/.maascli.db apt_purge '.*maas.*' 'bind9' sudo -u postgres psql -c 'drop database maasdb;' lxc-stop -n juju-bootstrap lxc-destroy -n juju-bootstrap # start lxcbr0 again sed -e 's/^USE_LXC_BRIDGE="false"/USE_LXC_BRIDGE="true"/' -i \ /etc/default/lxc-net service lxc-net start # clean up the networking cp /etc/network/interfaces.cloud.bak /etc/network/interfaces rm /etc/network/interfaces.d/cloud-install.cfg ifconfig br0 down brctl delbr br0 service networking restart echo @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ echo you might need to fix your /etc/resolv.conf echo @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ ;; single-system) echo @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ echo Single install cleansing. echo @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ juju destroy-environment --yes --force local ;; *) echo @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ echo Please run with either single-system or multi-system as an argument. echo Example: echo sudo cloud-uninstall single-system echo @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ exit 1 ;; esac # these may or may not be installed, so we list them all individually apt_purge '.*juju.*' apt_purge cloud-install-single apt_purge cloud-install-multi apt_purge cloud-install-landscape apt_purge cloud-installer # Remove any extra packages that aren't needed after purging. apt-get -yy autoremove # just make sure juju-mongodb died. LP#1306315 MONGOD=$(pgrep mongod) RET=$? if [ ${RET} -eq 0 ]; then if [ ${MONGOD} -gt 0 ]; then kill -9 ${MONGOD} fi fi rm -rf ~/.juju ~/.cloud-install || true rm /etc/.cloud-installed || true cloud-installer-proper/tools/juju-ws-test.py0000755000175000017500000000155712323623210020152 0ustar adamadam#!/usr/bin/env python from ws4py.client.threadedclient import WebSocketClient from pprint import pprint import json import os import time params = {} params['Type'] = "Admin" params['Request'] = "Login" params['RequestId'] = 1 params['Params'] = {"AuthTag": "user-admin", "Password": os.environ['JUJU_PASS']} msg = None class Stupid(WebSocketClient): def opened(self): self.send(json.dumps(params)) def closed(self, code, reason): print(("Closed", code, reason)) def received_message(self, m): print(("Message", json.loads(m.data.decode('utf-8')))) if __name__ == '__main__': ws = Stupid(os.environ['JUJU_URL'], protocols=['https-only']) ws.daemon = False ws.connect() time.sleep(1) info = {'Type': 'Client', 'Request': 'EnvironmentInfo'} ws.send(json.dumps(info)) ws.close()